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:
co-authored by
Claude Opus 5.5
parent
c043834d1e
commit
8d8c0ee0c3
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ProdDash Viewer Help</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; --muted: #6e6e73; --rule: #d2d2d7; --key: #f2f2f7; }
|
||||
@media (prefers-color-scheme: dark) { :root { --muted: #98989d; --rule: #3a3a3c; --key: #2c2c2e; } }
|
||||
body { font: 13px/1.55 -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 28px 36px 48px; max-width: 640px; }
|
||||
h1 { font-size: 22px; margin: 0 0 4px; }
|
||||
h2 { font-size: 15px; margin: 28px 0 6px; padding-top: 18px; border-top: 1px solid var(--rule); }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 20px; }
|
||||
.lead { color: var(--muted); margin-bottom: 18px; }
|
||||
kbd { font: 12px -apple-system, sans-serif; background: var(--key); border: 1px solid var(--rule); border-radius: 4px; padding: 0 5px; }
|
||||
code { font: 12px ui-monospace, Menlo, monospace; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 8px; }
|
||||
td { padding: 5px 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
||||
td:first-child { width: 42%; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ProdDash Viewer</h1>
|
||||
<p class="lead">A window onto ProdDash. It shows one installation’s dashboard — or several, with a tab for each — and nothing else.</p>
|
||||
|
||||
<h2>Add an installation</h2>
|
||||
<ul>
|
||||
<li>Choose <b>ProdDash Viewer → Settings…</b> (<kbd>⌘,</kbd>), then <b>Connections</b>, then <b>+</b>.</li>
|
||||
<li><b>Name</b> is what its tab says — “AB Booth”, “NA Ingest”.</li>
|
||||
<li><b>Address</b> is the one you open ProdDash at in a browser, such as <code>10.3.11.152:24500</code>. Leave the port off and ProdDash’s own, 24500, is used. The sheet tells you as you type whether ProdDash is answering there.</li>
|
||||
<li>Double-click a connection to change it; drag connections to change the order of the tabs.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Switch between installations</h2>
|
||||
<p>With two or more connections, a slim tab bar of their names sits under the title bar. It tucks itself away so the dashboard has the whole window: point at the title bar and it slides back out.</p>
|
||||
<ul>
|
||||
<li>In full screen, move the pointer to the top of the screen — the tab bar comes down with the menu bar. It also shows for a moment after each switch, so you can see where you are.</li>
|
||||
<li><kbd>⌃Tab</kbd> and <kbd>⌃⇧Tab</kbd> go to the next and previous installation; <kbd>⌘1</kbd>–<kbd>⌘8</kbd> go straight to one, <kbd>⌘9</kbd> to the last.</li>
|
||||
<li>Every dashboard stays live in the background, so switching is instant.</li>
|
||||
<li>A tab with <b>⚠︎</b> is an installation that isn’t answering.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Full screen</h2>
|
||||
<p>Click the green button, or choose <b>View → Enter Full Screen</b> (<kbd>fn F</kbd> or <kbd>⌃⌘F</kbd>). If the Viewer was in full screen when it quit, it opens in full screen again — so a booth Mac that restarts comes back as it was left.</p>
|
||||
<p>The dashboard’s own full-screen button works too: it fills the screen with the dashboard alone, tab bar and all put away. Press <kbd>Esc</kbd> to leave it.</p>
|
||||
|
||||
<h2>Moving around</h2>
|
||||
<ul>
|
||||
<li><b>View → Back</b> (<kbd>⌘[</kbd>), <b>Forward</b> (<kbd>⌘]</kbd>) — after opening the admin page from the dashboard, for example. Swiping with two fingers works as in Safari.</li>
|
||||
<li><b>View → Reload Page</b> (<kbd>⌘R</kbd>).</li>
|
||||
<li>Links to anywhere outside the installation open in your web browser.</li>
|
||||
</ul>
|
||||
|
||||
<h2>When an installation can’t be reached</h2>
|
||||
<p>The Viewer says why — nothing answering, the address can’t be found, no network — and keeps trying by itself, so a dashboard comes back on its own after ProdDash restarts or its Mac reboots. <b>Try Again</b> asks right away.</p>
|
||||
<p>If macOS asked whether ProdDash Viewer may find devices on your local network and the answer was no, it can’t reach anything: allow it in <b>System Settings → Privacy & Security → Local Network</b>.</p>
|
||||
|
||||
<h2>General settings</h2>
|
||||
<ul>
|
||||
<li><b>Open at login</b> — start the Viewer when this Mac’s user logs in. macOS may ask you to approve it in System Settings first.</li>
|
||||
<li><b>Keep the display awake</b> — while a dashboard is on screen, the display doesn’t dim or sleep. On unless you turn it off.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Keyboard shortcuts</h2>
|
||||
<table>
|
||||
<tr><td><kbd>⌘,</kbd></td><td>Settings</td></tr>
|
||||
<tr><td><kbd>⌘[</kbd> <kbd>⌘]</kbd></td><td>Back, Forward</td></tr>
|
||||
<tr><td><kbd>⌘R</kbd></td><td>Reload Page</td></tr>
|
||||
<tr><td><kbd>fn F</kbd> <kbd>⌃⌘F</kbd></td><td>Enter or leave full screen</td></tr>
|
||||
<tr><td><kbd>⌃Tab</kbd> <kbd>⌃⇧Tab</kbd></td><td>Next, previous installation</td></tr>
|
||||
<tr><td><kbd>⌘1</kbd> … <kbd>⌘9</kbd></td><td>An installation by its place in the tab bar</td></tr>
|
||||
<tr><td><kbd>⌘W</kbd></td><td>Close the window, which quits the Viewer</td></tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>ProdDash Viewer</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ProdDash Viewer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ProdDashViewer</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.waterschurch.proddash.viewer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2026 Waters Church</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSArchitecturePriority</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<!-- ProdDash is served over plain http on the local network, and the
|
||||
connection sheet asks each address whether ProdDash is there. -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<!-- macOS shows this sentence verbatim when it asks. -->
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>ProdDash Viewer shows the dashboards of the ProdDash installations you add in Settings, which run on this network.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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)")
|
||||
Reference in New Issue
Block a user