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>
3.6 KiB
ProdDash
Modular production dashboard web app for Waters Church, developed in this repo.
The app lives at the repo root and is built: zero-dependency Node server
(server.js, Node 18+), dashboard shell client in public/, admin page at
/admin, modules in modules/, and a native macOS menu-bar launcher in
launcher/macos/ (Swift, no dependencies — it supervises the server and owns
the macOS permissions modules declare; see its README). Runtime state (module
config, named layouts) is written to a per-machine data directory outside the
checkout (PRODDASH_DATA_DIR overrides; see README "Where settings are
kept") — never add code that writes runtime state into the repo folder.
SPEC.md was the authority for the initial build;
README.md has run/configure/use instructions.
Building a new module — read docs/MODULE-GUIDE.md first
docs/MODULE-GUIDE.md is the authority on the module contract (manifest, client factory + moduleApi, optional server entry, title-bar controls). The rules that matter most:
- A module is one self-contained folder in
modules/— never modify the shell (server.js,public/) to add one. Installation = the admin page's Available modules (downloads from the repo into the data directory), or drop the folder and restart; configure in/admin. - All settings in the admin page (
configSchema/instanceSchema) — never a separate setup page, hand-edited file or env var. Dependencies ship inside the module folder (zero deps is the norm). Everything the module does runs inside its server entry — no helper scripts, companion programs or daemons the operator starts by hand; a listener or decoder is started ininit()and stopped instop(). Declare"proddash": ">=x.y.z"and bump the module'sversionon every change. - Browsers never call upstream services directly; the module's server routes proxy everything (no CORS, secrets stay server-side).
- Style with the shell's CSS variables only, scoped under
[data-module="<id>"]; survive upstream loss (reconnect +setStatus). - Verify against the guide's testing section and resilience checklist. Mocks
for the existing upstreams:
node tools/prodcom-mock.js(ProdCom) andnode band-lineup-display/tmp/pp-mock.js 1599(ProPresenter).
Reference code (read-only forks — don't develop these here)
prodcom-listener/— live ProdCom transcript web app. Zero-dependency Nodeserver.jsthat servespublic/and proxies/prodcom/*(including the SSE transcript stream) to the ProdCom API atconfig.json → prodcomUrl(defaulthttp://10.3.11.152:24480). Good reference for: SSE handling, reconnect logic, channel colors/filtering, PWA install.band-lineup-display/— backstage Electron app (stage lineup photos with mic/pack labels) plus ProPresenter Now/Next:band-lineup-display/propresenter-core/— shared ProPresenter 7/20 client, no deps, no Electron. All ProPresenter quirks live here (PP20 runtime route discovery, arrangement-expanded slide totals, uuid-then-name live-item matching). Reuse this rather than re-implementing ProPresenter access.band-lineup-display/propresenter-app/— standalone Now/Next web display.band-lineup-display/tmp/pp-mock.js— mock ProPresenter API for testing without the real thing:node tmp/pp-mock.js 1599, then point at127.0.0.1:1599.
Upstream sources: ProdCom Listener lives at
github.com/michaelmcgary2008/prodcom-listener; BandLineupDisplay exists only
on the production machine. These folders are snapshots, not submodules.