ProdDash Viewer 1.0.0: a Mac app that only shows ProdDash, with a tab per installation

A new Apple-silicon app in viewer/macos (Swift, no dependencies, ~1 MB): one
window showing a ProdDash dashboard in WebKit, and nothing else. It runs no
server; it points at any ProdDash on the network.

- Connections (name + address) in Settings; the add/edit sheet asks
  /api/modules whether ProdDash answers as you type. No port means 24500.
- Two or more connections bring a slim tab bar of their names. In a window
  it floats over the dashboard and comes out while the pointer is on the
  title bar; it is not part of the title bar, because anything there makes
  WebKit push the page down. In full screen it is a titlebar accessory that
  comes down with the menu bar, and it flashes up after each switch.
  ⌃Tab / ⌃⇧Tab and ⌘1–9 switch from the keyboard.
- Full screen, reopening in full screen if it quit there; window size and
  the selected tab are kept too. The dashboard's own full-screen button
  works as well.
- Menus: View (Back, Forward, Reload Page, Enter Full Screen), Settings,
  Edit (copy/paste in the page's text fields), Window, Help (a guide that
  ships in the app).
- Every installation keeps its own live web view. One that can't be
  reached says why and retries at 2, 4, 8, then every 15 s; a redirect off
  the installation is reported, not handed to the browser. JS alert,
  confirm and prompt show as sheets; links to other sites open in the
  browser.
- General settings: open at login (SMAppService), keep the display awake
  while a dashboard is on screen (default on).

build.sh signs with the Developer ID when one is installed and makes a
notarised DMG with --notarize, as the launcher's does. It needs Xcode: with
only the command line tools the macOS 27 SDK can't expand SwiftUI's @State.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-10-25 13:05:14 -04:00
co-authored by Claude Opus 5.5
parent c043834d1e
commit 8d8c0ee0c3
18 changed files with 2229 additions and 2 deletions
+78
View File
@@ -0,0 +1,78 @@
// Draws AppIcon.png, the Viewer's icon: ProdDash's 2×2 tile mark on a
// display, so the Viewer and ProdDash itself tell apart in Finder and the
// Dock. Run it again only to change the icon; build.sh uses the PNG.
//
// swift Resources/make-icon.swift Resources/AppIcon.png
import AppKit
let side = 1024
let output = CommandLine.arguments.dropFirst().first ?? "AppIcon.png"
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: side, pixelsHigh: side, bitsPerSample: 8,
samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB,
bytesPerRow: 0, bitsPerPixel: 0),
let context = NSGraphicsContext(bitmapImageRep: bitmap) else { fatalError("no bitmap") }
NSGraphicsContext.current = context
let g = context.cgContext
// Top-left origin, as the measurements below are written.
g.translateBy(x: 0, y: CGFloat(side))
g.scaleBy(x: 1, y: -1)
func color(_ hex: UInt32, _ alpha: CGFloat = 1) -> CGColor {
CGColor(srgbRed: CGFloat((hex >> 16) & 0xff) / 255, green: CGFloat((hex >> 8) & 0xff) / 255,
blue: CGFloat(hex & 0xff) / 255, alpha: alpha)
}
func rounded(_ rect: CGRect, _ radius: CGFloat) -> CGPath {
CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
}
func fill(_ path: CGPath, _ fillColor: CGColor) {
g.addPath(path); g.setFillColor(fillColor); g.fillPath()
}
// The body: Apple's icon grid — an 824-point rounded square, centred, on the
// soft shadow macOS icons sit on. ProdDash's own near-black.
let body = CGRect(x: 100, y: 100, width: 824, height: 824)
let bodyPath = rounded(body, 185)
g.saveGState()
g.setShadow(offset: CGSize(width: 0, height: 12), blur: 28, color: color(0x000000, 0.35))
fill(bodyPath, color(0x0b0e12))
g.restoreGState()
g.saveGState()
g.addPath(bodyPath); g.clip()
let shade = CGGradient(colorsSpace: CGColorSpace(name: CGColorSpace.sRGB),
colors: [color(0x1b222b), color(0x0a0d11)] as CFArray, locations: [0, 1])!
g.drawLinearGradient(shade, start: CGPoint(x: 0, y: body.minY), end: CGPoint(x: 0, y: body.maxY), options: [])
g.restoreGState()
g.addPath(rounded(body.insetBy(dx: 1.5, dy: 1.5), 183.5))
g.setStrokeColor(color(0xffffff, 0.08)); g.setLineWidth(3); g.strokePath()
// The display: bezel, screen, neck and foot.
let bezel = CGRect(x: 212, y: 250, width: 600, height: 410)
let metal = color(0x3a434f)
fill(rounded(CGRect(x: 474, y: 650, width: 76, height: 84), 8), metal)
fill(rounded(CGRect(x: 372, y: 724, width: 280, height: 36), 18), color(0x4a5462))
fill(rounded(bezel, 44), metal)
let screen = bezel.insetBy(dx: 22, dy: 22)
fill(rounded(screen, 26), color(0x0c1015))
// ProdDash's mark on the screen: three outlined tiles and the lit one.
let padding: CGFloat = 46, gap: CGFloat = 30
let tileW = (screen.width - padding * 2 - gap) / 2
let tileH = (screen.height - padding * 2 - gap) / 2
for (column, row) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let tile = CGRect(x: screen.minX + padding + CGFloat(column) * (tileW + gap),
y: screen.minY + padding + CGFloat(row) * (tileH + gap),
width: tileW, height: tileH)
if column == 1 && row == 1 {
fill(rounded(tile, 22), color(0x2fe39a))
} else {
let inset = tile.insetBy(dx: 5, dy: 5)
fill(rounded(inset, 18), color(0x121820))
g.addPath(rounded(inset, 18)); g.setStrokeColor(color(0x3d4652)); g.setLineWidth(10); g.strokePath()
}
}
guard let png = bitmap.representation(using: .png, properties: [:]) else { fatalError("no png") }
try png.write(to: URL(fileURLWithPath: output))
print("wrote \(output)")