Files
ProdDash/launcher/macos
Waters ChurchandClaude Opus 5 1dbd55aa24 macOS menu-bar launcher: run ProdDash, and own its permissions
A native Swift app (launcher/macos, ~2 MB, no dependencies) that starts,
stops and watches over the server from the menu bar — and holds the macOS
permissions its modules need.

The permissions are the point as much as the buttons are. macOS grants
privacy permissions to the application that owns a process, and a child
inherits its parent's identity, so with the server started from here node —
and the Timers module's ltc-capture — belong to this app bundle and the
microphone and local-network prompts are asked, and remembered, as ProdDash.
Started over SSH or from a launchd job there is nobody to ask: the permission
is silently absent rather than refused, which is the "endless zeroes" failure
ltc-listener.js warns about.

The shell was already built for a supervisor: with PRODDASH_LAUNCHER=1 it
exits 75 instead of respawning itself when the admin page applies an update,
which the launcher reads as a restart request rather than a fault. Crashes
get a backed-off restart (1s, 2s, 5s, 10s) and four in two minutes stops the
cycle. The server is spawned into its own process group, so stopping it also
stops anything it started — an orphaned ltc-capture would otherwise hold an
audio device indefinitely — and signals, a logout and Quit all stop it first.

- Menu bar: ProdDash's own 2x2 tile mark, its accent tile lit green while
  serving, amber mid-move, red when something needs a person. Start, stop,
  restart, open the dashboard or admin, open at login, quit.
- Window: status and both addresses, plus Log (also written to
  ~/Library/Logs/ProdDash, kept a week), Settings, and Permissions.
- The port is written to the data directory's proddash.json — ProdDash's own
  file — so the port set here holds however it is next started. Something
  already on the port is reported, and offered as a takeover when it answers
  like a ProdDash started another way.
- Modules declare what they need in their manifests ("permissions": [...],
  documented in docs/MODULE-GUIDE.md and ignored by the shell); the launcher
  lists exactly that, with each module's reason, asks for the microphone,
  and — since macOS reports nothing about local network access — offers a
  check that browses for services and tries every configured upstream.

Timers, ProdCom and ProPresenter modules declare their permissions (patch
bumps). Build with launcher/macos/build.sh; output is gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:27:08 -04:00
..

ProdDash Launcher (macOS)

A menu-bar app that starts, stops and watches over the ProdDash server on a Mac — and, just as importantly, owns the permissions its modules need.

It is a native Swift app: about 1 MB, no dependencies, nothing to install but the app itself. Build it with ./build.sh.

Why a launcher, and not just Start ProdDash.command

Two reasons, and the second is the one that bites.

Supervision. The server is started as a child process, its output is kept in a log, and it is brought back if it stops on its own. The shell was built for this: with PRODDASH_LAUNCHER=1 in its environment it exits 75 rather than respawning itself when the admin page applies an update, which the launcher reads as "start me again" instead of as a fault. A crash gets a backed-off restart (1s, 2s, 5s, 10s); four crashes in two minutes stops the cycle, so a broken install doesn't spin forever.

Permissions. macOS grants privacy permissions to the application that owns a process, and a child inherits its parent's identity. Start ProdDash from here and node — plus the Timers module's ltc-capture tool — belong to this app bundle, so the microphone and local-network prompts are asked, and remembered, as ProdDash. Start it over SSH or from a bare launchd job and there is no owner to ask: the permission is silently absent rather than refused, and an LTC input reads as endless zeroes with no error anywhere. That is the failure modules/propresenter-timers/ltc-listener.js warns about, and this is the fix.

Build

cd launcher/macos
./build.sh                  # build/ProdDash.app, universal (Apple Silicon + Intel)
./build.sh --install        # …and copy it to /Applications
./build.sh --run            # …and open it
./build.sh --arch native    # this Mac's architecture only — faster while iterating

Needs the Xcode command line tools (xcode-select --install) and macOS 13 or newer. The app icon is built from public/icons/icon-512.png, so ProdDash's own icon is the app's icon.

The build is signed ad-hoc, which is all a local tool needs — but the signature changes on every rebuild, so macOS may treat a rebuilt app as a new one and ask for the microphone again. Build once, install it, and leave it.

Using it

The menu-bar icon is ProdDash's 2×2 tile mark; the accent tile is lit green while the server runs, amber mid-move, red when something needs a person, and hollow when it is stopped. Its menu has the whole app in it:

  • Open Dashboard / Open Admin — the running server, in a browser.
  • Start / Stop / Restart ProdDash.
  • Launcher… — the window below.
  • Open at Login — a login item, through the modern SMAppService API.
  • Quit ProdDash — stops the server too, and asks first while it is running.

The window has the status, both addresses the dashboard answers on, and three tabs:

  • Log — everything the server prints, plus what the launcher did and why. Also written to ~/Library/Logs/ProdDash/proddash-<date>.log, kept a week.
  • Settings — the port, and how it starts (at login, server on open, restart on crash, open a browser, show this window). It also shows the ProdDash folder, the node binary in use and where settings are kept.
  • Permissions — what the installed modules declare they need, who needs it, and what macOS currently allows. See below.

Port

The port belongs to ProdDash, not to the launcher, so it is written to proddash.json in the data directory — the file the server already reads. Set it here and ProdDash keeps that port however it is started next. Changing it while the server runs restarts it.

Permissions

The pane lists what the installed modules declare in their manifests ("permissions": [ … ] — see docs/MODULE-GUIDE.md), with the module's own reason:

  • Microphone — asked for directly, with the status macOS reports.
  • Local Network — macOS has no API that reports whether this was granted, so Check Access asks the network instead: it browses for Bonjour services (which is what raises the prompt) and tries every upstream the admin page is pointed at, then says what the result means. Nothing answering, when the gear is on, means macOS is withholding access.
  • Notifications — the launcher's own, for a server that stops while nobody is watching.

On first run the launcher asks for what the modules declare, while someone is still at the keyboard, rather than halfway through a service.

Details worth knowing

  • One instance. Opening a second copy raises the first one's window.
  • Port already in use. If something already holds the port, the launcher asks before doing anything. When that something answers like ProdDash — an SSH-started copy, say, with no microphone access — it offers to take the port over.
  • Stopping is thorough. The server is spawned into its own process group, so stopping it also stops anything it started. An orphaned ltc-capture would otherwise hold an audio device until someone found it in Activity Monitor.
  • Signals. killall ProdDash, a logout or a shutdown stops the server first rather than leaving node running.
  • Where the settings live. ProdDash's port is in the data directory's proddash.json; everything else (auto-start, restart-on-crash, the paths) is the launcher's own, in its preferences.
  • Finding things. The ProdDash folder is the checkout the app was built inside, or ~/Apps/ProdDash and the other usual spots, or whatever you pick under Settings. Node is the checkout's bundled runtime/bin/node if there is one, then Homebrew, /usr/local, nvm, Volta, and finally whatever a login shell finds.

Troubleshooting

What you see What it means
"Can't find the ProdDash folder" Point it at the folder with server.js in it, under Settings.
"Node.js 18 or newer isn't installed" Install Node, or set the path under Settings. A GUI app inherits almost no PATH, so a node in an unusual place has to be named.
"ProdDash stopped 4 times in two minutes" The server is failing on startup — the Log tab has its own error.
LTC shows no signal Permissions tab → Microphone. If it says granted and the input is still silent, the server may be an older copy started outside the launcher — Stop, then Start.
Nothing on the network answers Permissions tab → Check Access.