A new module that subscribes to AbleSet's OSC API (UDP 39051) and streams what it is playing to ten tiles: Now Playing (song, tempo, meter, duration, time left, section, progress bar, next song, songs left), Song Sections (a sliding strip, or a lottery wheel from the title bar), Setlist, Transport, Tempo, Song Time, Set Time, Section, Next Song and Status, each coloured by its state. The song and set countdowns are offered to the Timers module. A heartbeat watchdog resubscribes when AbleSet goes quiet (a restarted AbleSet forgets its subscribers). Time left falls back to the song length and playhead when AbleSet stops sending it (Live disconnected). tools/ableset-mock.js plays a setlist over the same API for testing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProdDash
A modular production dashboard for Waters Church. Any browser on the
building network opens one page and composes its own dashboard out of
modules — independent tiles that each show one aspect of a live service.
Ships with the live ProdCom transcript, the ProPresenter Now / Next
display, and a booth clock; new modules drop into modules/ without
touching the shell (see docs/MODULE-GUIDE.md).
How it works
server.js (zero dependencies, just Node.js) does four things:
- Serves the dashboard shell (
public/) and the admin page (/admin). - Discovers modules in
modules/and serves each one's client at/modules/<id>/. - Mounts each enabled module's server routes under
/api/modules/<id>/— that's where module servers follow ProdCom, poll ProPresenter, and stream live updates over SSE — one upstream connection per module, however many dashboards are open — so browsers never talk to production gear directly (no CORS, and API keys never leave the server). - Stores server-wide module config and named layouts in a per-machine data directory outside the app folder, so updating ProdDash never loses them.
Each browser arranges its own tiles (add, drag, resize, remove, per-tile
settings); the layout autosaves to that browser's localStorage. Named
layouts can also be saved to the server and loaded from any other browser.
Every module survives its upstream disappearing: tiles show a red status dot
and a clear degraded state, then recover on their own.
Run it
macOS — the app
Open the disk image and drag ProdDash to Applications. That's the whole installation: the app carries the dashboard and its own Node runtime, so nothing else has to be installed and nothing can be missing. It lives in the menu bar, starts and stops the server, can open at login, and holds the permissions the modules need — the microphone the LTC listener reads, and local network access for ProdCom and ProPresenter. macOS grants those to the app that owns the process, so a server started any other way (over SSH, from a launchd job) simply doesn't have them, and an LTC input goes silently to zeroes.
Build the disk image from a checkout with
launcher/macos/build.sh:
cd launcher/macos && ./build.sh --dmg
Anywhere else, or from a checkout
Requires Node.js 18 or newer.
node server.js
Or double-click Start ProdDash.command (macOS) / start.bat (Windows).
Then open the URL it prints — http://localhost:24500 on this machine, or
the http://<this-machine's-IP>:24500 address from any other device on the
network.
Configure
Server-wide module settings (ProdCom URL, ProPresenter host, API keys,
enable/disable) live in the admin page — open Admin from the
dashboard's top bar, or go to /admin. Saving re-initialises the module
immediately; open tiles reconnect on their own.
Where settings are kept
Everything you enter in the admin page (module connection settings, API keys, enabled/disabled flags) and every named layout is written to a data directory on the machine running the server — not into the app folder:
| OS | Data directory |
|---|---|
| Windows | %APPDATA%\ProdDash (e.g. C:\Users\<you>\AppData\Roaming\ProdDash) |
| macOS | ~/Library/Application Support/ProdDash |
| Linux | $XDG_CONFIG_HOME/proddash, else ~/.config/proddash |
Because it lives outside the checkout, you can git pull, re-clone, or reset
the app folder from main and start the new version with everything still
configured. The admin page shows the exact path in use, and the server prints
it at startup (Settings : …).
The directory holds modules.json (module config), layouts/ (named
layouts) and modules/ (modules installed from the admin page), all written
by the app itself. Installed from the macOS app, it also holds app/ — the
copy of ProdDash that actually runs, laid down from inside the app bundle on
first launch and updated in place from then on (nothing may write inside a
signed .app, and ProdDash updates itself). It may also hold a proddash.json
with this machine's shell settings — the same keys as the checked-in
config/proddash.json, which supplies the defaults:
| Key | Meaning |
|---|---|
port |
Port ProdDash serves on (default 24500). The macOS launcher writes this one, so the port it is set to holds however ProdDash is started. |
adminPin |
The admin PIN, as a scrypt hash. Set it in the admin page's Admin PIN section, not by hand — 6 digits, and only the hash is stored. Absent means admin is open to anyone on the network. |
adminPasscode |
An older free-text passcode. Still honoured, so an existing install keeps working; new installs should use adminPin. |
forge |
Where updates and module installs are fetched from. Absent or "github" uses GitHub; a Gitea or Forgejo base URL such as "http://10.3.8.90:3000" uses a server of your own. Set it per machine in the data directory, alongside repo. |
theme |
The dashboard palette: booth (default), harbor, graphite, ember, daylight, midnight, moss, plum, sepia, blackout, paper or iron. The admin page's Theme section writes this one — see Themes. |
Locked out? Delete adminPin from the data directory's proddash.json
and restart, or start the server with PRODDASH_PASSCODE set — that passcode
overrides the PIN and always gets you in.
Only /admin and the admin API are behind the PIN. Dashboards stay open, so a
wall display needs no PIN, and it is unrelated to a module's own PIN (Slack's
4–8 digit send PIN, say), which guards that module's actions only.
Updating from your own git server. ProdDash fetches one tar.gz of the
branch — nothing else — so any Gitea or Forgejo works. Point forge at its
base URL and repo at the owner/repo there:
{ "forge": "http://10.3.8.90:3000", "repo": "michael/ProdDash", "branch": "main" }
Gitea spells an archive URL differently from GitHub, which is the only thing that changes; the tarball itself is identical, commit hash included, so update detection and module installs behave exactly as before. Make the repository readable without signing in, or ProdDash cannot fetch it.
Environment variables override both files: PORT, PRODDASH_PASSCODE, and
PRODDASH_FORGE, and PRODDASH_DATA_DIR to put the data directory somewhere else (a relative path
is taken from the app folder; PRODDASH_DATA_DIR=config restores the old
in-repo location). If the default directory can't be created, the server says
so and falls back to the app's config/ folder.
Upgrading from an earlier version: the first start copies an existing
config/modules.json and config/layouts/ from the app folder into the data
directory, so nothing has to be re-entered. The old files are left in place and
ignored from then on.
Themes
The whole dashboard — every tile, on every browser — draws with one palette, chosen in the admin page's Theme section and switched live: pick a swatch and open dashboards change without a reload. The section starts folded — a building picks its palette once — and holds twelve presets:
| Booth | the default dark, mint accent |
| Harbor | deep navy, cyan |
| Graphite | neutral charcoal, electric blue |
| Ember | warm broadcast black, orange |
| Daylight | a cool light palette, for a front-of-house desk in daytime |
| Midnight | indigo night, violet |
| Moss | forest floor, lime |
| Plum | aubergine, purple |
| Sepia | warm brown room, cool teal — Ember's arrangement reversed |
| Blackout | true black, cyan, for a dark room where any glow carries |
| Paper | the warm light one, indigo accent |
| Iron | no colour of its own: a near-white accent, so the only hues on screen are a module's own and the red and amber that mean something |
Each keeps the status colours apart — the accent is never near the danger red
or the warning amber — and muted text readable in a dark booth. The choice is
stored as theme in the data directory's proddash.json (table above), so
it holds across restarts and updates; a browser remembers the last theme it
saw and applies it before the server answers, so nothing flashes. Modules
style with the shell's colour variables, which is why they follow; a colour
set on a tile itself (a PCO Plan header colour, say) is deliberately kept.
Installing modules and updating
Everything below happens in the admin page; ProdDash talks to its git
repository — michael/ProdDash on the church's own Gitea at
chronicles-ab.waterschurch.org:3000, branch main. Change it with forge,
repo and branch in proddash.json, or the PRODDASH_FORGE and
PRODDASH_REPO variables (owner/repo#branch). It downloads one small archive of the branch, at most
every few minutes, only while the admin page is open or for the update check
below. Nothing is installed without someone clicking.
- Available modules lists modules in the repo that aren't installed
here. Install downloads the module into the data directory's
modules/and starts it right away — nothing is written into the app folder, and no restart is needed. Modules that ship with your copy of ProdDash are "bundled"; ones you installed are "installed"; if both exist, the newer version runs. - Uninstall on a module card removes an installed module's files. For a bundled module (its folder belongs to the app) it hides the module from every dashboard instead; it reappears under Available modules and can be brought back without downloading. Settings are kept either way.
- Module updates: when the repo has a newer version of an installed module, its card shows Update to vX.
- Version requirements: every module declares the ProdDash versions it
needs (
"proddash": ">=1.1.0"in its manifest). A module that needs a newer shell is listed but not loaded, and cannot be installed until ProdDash is updated. - ProdDash updates: the admin page checks the repo shortly after start
and every six hours, and shows a banner when
mainhas a newer version (or new commits). Update now brings the app folder up to date and restarts the server; dashboards reconnect on their own. A git checkout is updated withgit pull --ff-only(only when it is on the repo branch with no local changes — otherwise the banner says why and you update with git). A plain folder (no.git) has the repo archive unpacked over it; files removed upstream are left behind but harmless. Settings and layouts are never touched — nothing inside the data directory is written, even on an install where that directory is the app's ownconfig/. What changed opens the commit comparison on the forge. - If an update goes wrong: the download has to parse before anything is
swapped in, so a truncated or mangled
server.jsis refused with the reason and the running copy is left exactly as it was. Past that, the copy being replaced is set aside asapp.previousnext to the app folder, and dropped once the new version has stayed up for ninety seconds — so that folder existing means "the last update has not proven itself yet". The macOS app watches for a version that will not stay up (four stops in two minutes) and puts the old one back by itself, once, with a notification. Running some other way, the way back is a rename:mv app.previous app. - Restarting: after an update the server exits with code 75 and starts
its own replacement, which waits for the port to free up. If you supervise
ProdDash with your own launcher, set
PRODDASH_LAUNCHER=1so the server just exits and your launcher restarts it on exit code 75.PRODDASH_NO_REMOTE_CHECK=1turns off the periodic check (the admin page still checks when opened).
Using the dashboard
- + — add any enabled module; multiple tiles of the same module are fine (e.g. two transcripts filtered to different channels).
- Drag a tile by its title bar to move it; ✕ removes it.
- Resize from any edge or bottom corner, the way a window resizes: nothing is drawn on a tile to mark the spots — the cursor over an edge is what says it can be dragged. An edge moves only itself, so the tile grows or shrinks into the space beside it while the opposite edge stays put, stopping at a neighbour or at the module's own minimum size. Two exceptions, both of which move a shared edge instead: with another tile directly below, the bottom edge trades rows between the two so the column keeps its total height; and where two tiles meet side by side, the seam between them trades width (macOS split-view style), showing itself only while you drag it.
- Fullscreen (the ⛶ button) is a viewing mode: the menu bar hides (its green lip at the top-left brings it back, minus the Admin button), the occupied columns stretch to the full width, and every column runs to the bottom of the screen — the last tile in each column is stretched to end there. A short tile like the clock is never stretched into a ribbon: under another tile it slides to the bottom edge and the tile above grows into the gap; alone in its columns it stays as it is. Nothing is saved — the layout reverts when you leave fullscreen. A layout deeper than the screen keeps its normal row height and scrolls.
- A tile's actions (menus, text size, clear…) live in its title bar.
- Auto-hide bars (the button left of ⛶) folds every title bar away, for a windowed screen that should read like a wall display. A folded bar is not a gone bar: point at the tile's top edge and it slides back in over the content, where its buttons click and its width drags the tile exactly as before — so a dashboard can be arranged with every bar out of the way. The switch is off on a fresh dashboard and saves with the layout — including a named layout on the server, so a booth loads its screen with the bars already tucked away. Fullscreen folds every bar regardless, and the same hover brings one back there too.
- The pin beside a tile's title keeps that tile's bar showing while auto-hide is on — for the one or two tiles whose controls are wanted at a glance. It turns the accent colour when it is on and saves with the layout. Fullscreen still folds a pinned bar with the rest.
- The menu bar's own handle is the ProdDash name itself, wrapped in a translucent green pill at the left — click it to hide the bar; the slim green lip it leaves at the top-left edge brings the bar back. It stays at the left so it never sits on top of a tile's own tab.
- The gear on a tile holds per-tile settings (filters, text size, display options) — these belong to this browser's layout, not the server.
- The grid icon — save the current arrangement to the server under a name, load a named layout (it becomes a local copy — browsers are never linked), or reset this browser's dashboard. Loading says what it did — Loaded "FOH": 9 tiles, 2 not shown — Service, #ops aren't offered right now — with Undo in the message to get back the arrangement you had. A tile whose module or picker entry isn't offered at the moment keeps its place and shows nothing, then appears when it is; it is never replaced by the module's default tile. Saving over an existing name asks first, and the server keeps the copy it replaced: Restore previous in Admin → Saved layouts swaps it back (and swapping again undoes that). The menu's Grid Size stepper sets how many columns this browser cuts the board into — 8 to 24, 12 by default. Finer lets tiles be smaller so more fit across (a 4K wall), coarser suits a small screen read close up; row height follows, so tiles keep their shape and simply change size. Going coarser, a tile that no longer fits where it was moves to the first free spot rather than onto another tile. It saves with the layout, so a named layout comes back at the density it was arranged on.
- Every tile has a status dot: green = live, amber = connecting, red = upstream unreachable (hover it for details). Tiles reconnect automatically.
Included modules
-
ProdCom — live transcript stream with in-place updates of in-progress speech, channel colors, per-tile channel visibility toggles and group presets, timestamps, text size, a top-down / bottom-up direction toggle and jump-to-latest in the title bar. Admin config: ProdCom server (host : port), API key. Tile settings: channel icons, direction and timestamps.
-
ProPresenter — what's live and what's next, with slide progress, driven by the shared
propresenter-coreclient (ProPresenter 7/20). The + picker offers two tiles: Now / Next, the full card, and Now, the live item alone with the gradient filling the tile edge to edge — a wall panel that says one thing. Admin config: ProPresenter endpoint (host : port), password. Tile settings: a Show group of switches for the playlist name, live status, NOW and NEXT labels, slide count, progress bar, the next list and its item tags, plus tidy titles. -
Timers — live ProPresenter timers (countdown, countdown-to-time, elapsed) and LTC timecode as auto-sized cards. The + picker lists every discovered timer (and the LTC card) as its own tile, or add "All timers" and pick a selection via the tile's "Timers ▾" menu; overruns are unmistakably red. Read-only — no start/stop controls. Admin config is in three groups: ProPresenter (endpoint host : port, API password), LTC Timecode (the listener switch, and — when it's on — the audio device and channel), and Display (Timer font — Default or Monospace, which keeps the digits from shifting as the numbers change).
ProPresenter exposes no timecode over any API, so LTC is decoded from the audio signal, behind the admin LTC listener switch (off ⇒ no LTC tile in the picker at all). On, the module captures the selected audio input device/channel on the ProdDash machine — a native
ltc-capturehelper, compiled on first use — and decodes the SMPTE frames in-process: running/stopped/no-signal, the timecode counting frame-accurately in real time, and the frame rate (incl. 29.97 drop-frame). macOS note: the ProdDash server needs microphone permission — launch it from a logged-in session (Start ProdDash.command) and approve the one-time prompt; a server started over SSH or by bare launchd silently captures zeros. (Engine self-test:node modules/propresenter-timers/ltc.selftest.js.) -
PCO Plan — the order of service from Planning Center Services: every plan item with its scheduled start time, length, type, song key, description and item notes. When the ProPresenter module is running it follows that module's live item, highlights the matching plan item, dims what's done and shows the elapsed vs planned runtime and how far ahead/behind the service is. Items are matched by name — a document named like the Planning Center item or its song, tags and all. The message is the exception: a sermon's document is named after the sermon, so a live document that reads like the plan itself — its date code, series title and part, as in
260927 Where is Your Faith? - Part 6— is taken as the message and lands on the plan's message item (an item titled Message, Sermon or Teaching, else the first item under such a header). Admin config is the connection: Planning Center credentials (Personal Access Token app ID + secret, or an OAuth token), the service type (a list grouped by your Planning Center folders) and an optional pinned plan. Everything about the display is per tile (gear menu): the top-bar text (placeholders such as{series} • {part} | {serviceType} • {date}), colors, text size, and a show/hide checklist for every detail. -
Slack — live transcripts of this location's Slack channels, plus PIN-protected sending as the location. The + picker lists one transcript tile per configured channel, titled
#channel, and two send surfaces (one of each per dashboard): Send message, a free-text box with the admin's quick emojis one press away, and Quick replies, a grid of preset buttons. Both post to the channel of whichever transcript was clicked last — its bottom edge lights up in the accent color — and to the first configured channel until one is clicked. Messages arrive with avatars, names and times, Slack's inline formatting and the workspace's custom emoji; a right-click on one opens the reaction menu, and text size and jump-to-latest sit in the title bar. Nothing leaves the machine until the PIN is entered: one PIN unlocks every Slack tile on that dashboard page until the auto-lock idles out, a reload asks again, and the lock button on a send tile's title bar ends it early. Admin config is in four groups: Slack account (the location account's User OAuth Token —xoxp-…, so messages are literally from the campus — or a bot token plus "Send as"), Channels (the joined channels to follow, one transcript tile each), Transcript (refresh interval, messages kept, thread replies) and Sending (the switch on the group's title row, a 4–8 digit PIN, auto-lock minutes, quick replies one per line asLabel | Message— add| #channelto bind one to a channel — and up to eight quick emojis). Tile settings: avatars, timestamps, names and channel notices. -
Band Lineup — the backstage stage board: everyone scheduled on the band, vocal and production teams for the upcoming Planning Center plan, placed into their mic slot with photo, name and mic/pack label; birthdays in the service week get confetti. Guests and MCs not on the plan are pinned per slot from the title bar's Edit button. Admin config: Planning Center credentials, service type and plan, the teams to include, the stage slots (one mic/pack label per line, in stage order), default columns, and a Google Drive folder for manual photos. Tile settings: names, mic labels, parallax, and the birthday confetti with its intensity.
-
Clock — a big booth clock; also the smallest possible module and the reference for module authors.
To develop modules against mocks instead of real gear:
node tools/prodcom-mock.js (ProdCom on 127.0.0.1:24480),
node band-lineup-display/tmp/pp-mock.js 1599 (ProPresenter slides on
127.0.0.1:1599), node tools/pp-timers-mock.js 1600 (ProPresenter
timers + timecode on 127.0.0.1:1600), node tools/pco-mock.js
(Planning Center Services on 127.0.0.1:24700 — run ProdDash with
PCO_PLAN_API_BASE=http://127.0.0.1:24700) and
node tools/slack-mock.js 24716 (the Slack Web API on 127.0.0.1:24716 —
run ProdDash with SLACK_API_BASE=http://127.0.0.1:24716/api) and
node tools/pco-band-mock.js 24710 (Planning Center for the Band Lineup
board — a plan, its team members, birthdays and photos — run ProdDash with
BAND_LINEUP_API_BASE=http://127.0.0.1:24710). See the testing section of
the module guide.
Install as a desktop web app
Open the page in a browser first, then:
- macOS — Safari: File → Add to Dock…
- macOS/Windows — Chrome: click the install icon in the address bar, or ⋮ menu → Cast, save and share → Install page as app…
- Windows — Edge: ⋯ menu → Apps → Install this site as an app
Note: Chrome only offers the one-click PWA install on http://localhost (a
"secure context"). When opening from another machine over plain HTTP, use
Edge's "Install this site as an app" or Chrome's ⋮ → Cast, save and share →
"Create shortcut…" (check "Open as window") — same result: a dock/taskbar
app in its own window.
Repository layout
ProdDash/
├── server.js shell server: static files, module discovery/mounting,
│ admin + layouts APIs
├── public/ dashboard shell client (index.html, shell.js, style.css)
│ └── admin/ admin page client
├── modules/ installed modules, one folder each
│ ├── clock/
│ ├── prodcom-transcript/
│ └── propresenter-now-next/ (carries its own copy of propresenter-core)
├── package.json name + version (what the update check compares)
├── config/ proddash.json defaults (runtime state lives in the
│ per-machine data directory — see "Configure")
├── docs/MODULE-GUIDE.md how to build and install a module
├── tools/prodcom-mock.js mock ProdCom API for development
├── prodcom-listener/ reference fork — read-only, not served
└── band-lineup-display/ reference fork — read-only, not served
The two reference folders are snapshots of the apps ProdDash grew out of;
they are not wired into anything (but band-lineup-display/tmp/pp-mock.js
is handy for testing).