A module that sees something a tech must act on now (a timer overrun, a meter past its limit, timecode gone) raises an alert on a channel, 1–6, and every dashboard listening to that channel flashes: the menu bar — brought out for the duration if it was hidden — and a frame round the screen alternate between the level's two colours, with the alert's text bold across the bar. Low fades slowly, medium faster, critical snaps with no fade and floods the board behind the tiles too, in a muted cut that shows faintly through the tile panels without costing their content any contrast. Server: the hub. One alert per channel (a new one replaces the last), timed out server-side (1–600 s, 15 by default, 0 = held until cleared), broadcast over the shell stream as `alert` / `alert-clear`; GET /api/alerts for a dashboard that opens or reconnects mid-alert; POST /api/alerts and DELETE /api/alerts/<n>, open like a layout save. Modules raise from their server entry with shell.alert() / shell.clearAlert() or from a tile with moduleApi.alert() / moduleApi.clearAlert(); a module's alerts end when it stops. Colours live as alerts.colors in the data directory's proddash.json. Dashboard: the Layouts menu gains a folded Alerts row with six numbered toggles; the channel list saves with the layout, local and named (the server keeps it in a saved layout). Admin: a folded Alerts section with two colour pickers per level, a swatch running each level's cadence, and a Test row. Docs: docs/ALERTS.md (the feature), a "Raising alerts" section in the module guide, README. Tools: tools/alert.js raises/clears/lists from the command line; tools/alerts.test.js drives it all end to end. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
594 lines
35 KiB
Markdown
594 lines
35 KiB
Markdown
# 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](docs/MODULE-GUIDE.md)).
|
||
|
||
## How it works
|
||
|
||
`server.js` (zero dependencies, just Node.js) does four things:
|
||
|
||
1. Serves the dashboard shell (`public/`) and the admin page (`/admin`).
|
||
2. Discovers modules in `modules/` and serves each one's client at
|
||
`/modules/<id>/`.
|
||
3. 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).
|
||
4. 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
|
||
|
||
Ready-made builds are on the
|
||
[Releases page](http://chronicles-ab.waterschurch.org:3000/michael/ProdDash/releases);
|
||
the files themselves live in
|
||
[ProdDash-releases](http://chronicles-ab.waterschurch.org:3000/michael/ProdDash-releases).
|
||
There is a disk image for Apple silicon Macs, one for Intel Macs, and a
|
||
bundle for 64-bit Windows. Every build updates itself afterwards from the
|
||
admin page, so the download only has to be recent, not current.
|
||
|
||
### macOS — the app
|
||
|
||
Download the disk image for your Mac — `ProdDash-<version>-arm64.dmg` for
|
||
Apple silicon, `ProdDash-<version>-x86_64.dmg` for Intel — open it 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. It carries no modules — open **Admin →
|
||
Available modules**, tick the ones this machine needs and press **Install
|
||
selected**; each updates on its own from then on. 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](launcher/macos)/`build.sh` (`ARCH=x86_64` in front of it
|
||
for Intel):
|
||
|
||
```bash
|
||
cd launcher/macos && ./build.sh --dmg
|
||
```
|
||
|
||
### Windows — the bundle
|
||
|
||
Download `ProdDash-<version>-win-x64.zip`, unzip it somewhere you can write
|
||
to — `C:\ProdDash`, say, not Program Files, because updates write into the
|
||
folder — and double-click **ProdDash.bat**. The window it opens is the
|
||
dashboard running: it stays open, and it starts the server again when the
|
||
admin page applies an update. Allow `node.exe` on private networks when the
|
||
firewall asks, so other screens can reach it. Settings and layouts live in
|
||
`%APPDATA%\ProdDash`, so a new bundle never loses them. To start with
|
||
Windows, put a shortcut to `ProdDash.bat` in `shell:startup`. There is no
|
||
menu-bar-style launcher for Windows yet; the bundle is built with
|
||
[launcher/windows](launcher/windows)/`build.sh`.
|
||
|
||
### Anywhere else, or from a checkout
|
||
|
||
Requires [Node.js](https://nodejs.org) 18 or newer.
|
||
|
||
```bash
|
||
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](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](#themes). |
|
||
| `alerts` | `{ "colors": { "low": [a, b], "medium": [a, b], "critical": [a, b] } }` — the two colours each alert level flashes between, as `#rrggbb`. The admin page's **Alerts** section writes this one — see [Alerts](#alerts). |
|
||
|
||
**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:
|
||
|
||
```json
|
||
{ "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.
|
||
|
||
### Alerts
|
||
|
||
A module that sees something a tech must act on now — a timer overrun, a
|
||
meter past its limit, timecode gone — can raise an **alert** on one of six
|
||
channels, and every dashboard listening to that channel flashes: the menu
|
||
bar (brought out if it was hidden, for as long as the alert lasts) and a
|
||
frame round the screen alternate between two colours, with the alert's text
|
||
bold across the bar. **Low** fades slowly between its two colours,
|
||
**medium** faster, **critical** snaps between them with no fade and floods
|
||
the board behind the tiles as well, in a muted cut of the two that shows
|
||
faintly through the tiles without costing their content any contrast. The admin
|
||
page's **Alerts** section (folded, near the bottom) holds the two colour
|
||
pickers for each level, with a swatch running that level's cadence, and a
|
||
**Test** row that flashes any level on any channel for eight seconds. The
|
||
choice is stored as `alerts.colors` in the data directory's `proddash.json`.
|
||
Which channels a dashboard listens to is chosen on the dashboard itself —
|
||
see *Using the dashboard* below — and [docs/ALERTS.md](docs/ALERTS.md) has
|
||
the whole feature, module side included.
|
||
|
||
## 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. Tick several and press **Install selected** to fetch
|
||
them in one go. A packaged ProdDash (the app, the Windows bundle) ships
|
||
with no modules at all: this list is how every one of them arrives, and a
|
||
ProdDash update never brings one along. A git checkout also runs the
|
||
modules in its own tree ("bundled"); if both exist, the newer version runs.
|
||
- **Uninstall** on a module card removes an installed module's files. For a
|
||
checkout's bundled module (its folder belongs to the repo) 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 `main` has 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 with `git 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 own `config/`. **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.js` is refused with the
|
||
reason and the running copy is left exactly as it was. Past that, the copy
|
||
being replaced is set aside as `app.previous` next 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=1` so the server
|
||
just exits and your launcher restarts it on exit code 75.
|
||
`PRODDASH_NO_REMOTE_CHECK=1` turns 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.
|
||
- The same menu's **Alerts** row says which of the six alert channels this
|
||
screen listens to, and opens to six numbered buttons that toggle them
|
||
without closing the menu. An alert a module raises on a chosen channel
|
||
flashes this dashboard's menu bar and screen edge with its text in the
|
||
bar; one on any other channel leaves it alone. A fresh dashboard listens
|
||
to nothing. The choice saves with the layout, local and named — a booth
|
||
loading its "Stage" layout gets the stage's channels with it. See
|
||
[Alerts](#alerts).
|
||
- 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. Text typed into a channel (from a
|
||
dashboard, or ProdCom's own box) is marked with a keyboard; an automation's
|
||
with a bolt. The module also offers its channels to **Send Message** as
|
||
identities: a click anywhere in the transcript points that module's tiles
|
||
at ProdCom (the tile's bottom edge lights up in the accent color), and the
|
||
channel the text goes out *as* is picked from their Send as menu — only
|
||
channels ProdCom lets text be typed onto are offered.
|
||
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-core` client (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), the **video countdown** (time left on what ProPresenter is
|
||
playing, with a per-tile "Looping videos" switch so looping backgrounds
|
||
stay hidden) 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-capture` helper,
|
||
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** — Planning Center Services, as three tiles in the + picker:
|
||
**Plan** (the order of service), **Teams** (who is scheduled) and **Song
|
||
Key** (the key of the item on screen).
|
||
|
||
**Plan** shows 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.
|
||
|
||
With an **AbleSet** module installed and configured, the Plan tile also
|
||
follows the band: while AbleSet plays a song that is the item on screen,
|
||
that item's background fills as the song goes, and its sections slide past
|
||
in a strip at the bottom right of the item, the one playing filling in
|
||
turn. Both are switches in the tile's gear (**Song progress**, **Song
|
||
sections**), and both are simply absent when there is no AbleSet module, it
|
||
is switched off, or it has no host — nothing to turn off and nothing to
|
||
explain.
|
||
|
||
**Song Key** is one value, as large as the tile allows: the key of the item
|
||
ProPresenter has live, with the song's name and the next key under it — both
|
||
switches, as is the colour. When the feed that picks the item is gone the key
|
||
stays up, faded, and the tile's dot says so rather than pretending to be
|
||
current.
|
||
|
||
Keys are written as notes wherever the module shows one: Planning Center's
|
||
`Bb` and `F#m` read as B♭ and F♯m, on the Plan tile's item badge and in the
|
||
Song Key tile alike.
|
||
|
||
**Teams** shows the same plan's team and position assignments — each team
|
||
as a band of its people, with their photo (fetched through ProdDash, never
|
||
by the browser; initials when Planning Center has none) and a dot for
|
||
confirmed, unconfirmed or declined. Its gear menu picks which teams and
|
||
which positions appear, groups people under their position or lists them
|
||
plainly, and sets the colours: one for the team headers, or **Tint teams**
|
||
to give each team its own, optionally carried into the rows. Teams nobody
|
||
has signed up for are hidden until you ask for them — that list comes from
|
||
the service type, since Planning Center only returns a team once somebody
|
||
is on it.
|
||
- **Slack** — live transcripts of this location's Slack channels. The +
|
||
picker lists one transcript tile per configured channel, titled
|
||
`#channel`. Messages arrive with avatars, names and times, Slack's inline
|
||
formatting and the workspace's custom emoji; text size and jump-to-latest
|
||
sit in the title bar. Sending is the **Send Message** module's: a click
|
||
anywhere in a transcript points its tiles at that channel (the bottom
|
||
edge lights up in the accent color), and a message then leaves through
|
||
this module's one Slack connection, as the location. A right-click on a
|
||
message opens the reaction menu, which rides on the same unlock — with
|
||
the page locked, or no Send message tile on it, it says so instead of
|
||
reacting. This module has no PIN, send tile or quick replies of its own.
|
||
Admin config is in three 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 and one Send Message destination each) and
|
||
**Transcript** (refresh interval, messages kept, thread replies).
|
||
Tile settings: avatars, timestamps, names and channel notices.
|
||
- **Send Message** — the one place a dashboard sends from, whatever the
|
||
platform. Two tiles, 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 go wherever the operator last
|
||
pointed them with a click on a Slack or ProdCom transcript. A platform
|
||
whose destinations are identities rather than recipients — ProdCom, where
|
||
typed text goes out *as* a channel — also gets a **Send as** menu in the
|
||
title bar listing those identities; the menu exists only while such a
|
||
platform is installed, and Slack's channels are never in it. The choice is
|
||
saved with the tile, so a named layout remembers it; the transcript it
|
||
points at carries the accent stripe. The module
|
||
owns no connection of its own — it finds every installed module that
|
||
offers messaging and hands the message to it over loopback — and it owns
|
||
the PIN: nothing leaves the machine until it is entered on one of its
|
||
tiles, one entry unlocks every messaging tile on that dashboard page
|
||
(Slack's reaction menu included) until the auto-lock idles out, a reload
|
||
asks again, and the lock button ends it early. The PIN pad takes taps, a
|
||
keyboard, or a phone's — tapping the dots raises it. The PIN can be
|
||
switched off for a booth that doesn't need one.
|
||
Admin config: **PIN** (the switch on the group's title row, 4–8 digits,
|
||
auto-lock minutes) and **Presets** (quick replies one per line as
|
||
`Label | Message` — add `| slack:#channel` or `| prodcom:Channel` to bind
|
||
one to a destination — and up to eight quick emojis).
|
||
Tile settings: quick emojis on the Send message tile.
|
||
- **Resi Cues** — how long until the next Resi cue point, for a campus
|
||
running video from another site. The tile is one number, as large as it
|
||
will go: the countdown to the next cue, its name above it, and the playhead
|
||
and the cue just passed underneath. It also offers that countdown and the
|
||
playhead to the **Timers** module, so they can sit among the ProPresenter
|
||
timers instead of in a tile of their own.
|
||
Both the cue list and the decoder's position come from Resi's cloud rather
|
||
than the decoder — the decoder reports upstream and central.resi.io hands it
|
||
back — so nothing is installed at the decoder end. Resi republishes a
|
||
position only every few seconds and each reading is already stale, so the
|
||
playhead is estimated between readings rather than repeated; a countdown
|
||
built straight from Resi's number sits seconds behind and moves in lurches.
|
||
Admin config: Resi login and password, which decoder to follow (the list is
|
||
read from the account), and a folded **Timing** group for the poll
|
||
intervals and a position trim.
|
||
Tile settings: cue name, previous cue, playhead, cue list.
|
||
- **Band Lineup** — the backstage stage board: everyone on stage in their mic
|
||
slot with photo, name and mic/pack label, on one shared board that every
|
||
screen sees the same way — whoever edits it changes it everywhere, over SSE.
|
||
A photo library fills it: point the module at a Google Drive folder shared
|
||
"anyone with the link" (file name → person's name) or at a folder on the
|
||
ProdDash machine, and photos added or removed there follow. **Edit** in the
|
||
title bar opens that library — drag a photo onto a slot, drag between slots
|
||
to swap, rename or clear. Planning Center is optional auto-fill on top, not
|
||
the source of truth: **Sync** reads the upcoming plan's team members and
|
||
drops each into the slot matching their position, filling only slots nobody
|
||
placed by hand, so a guest pinned on Thursday survives Sunday's sync.
|
||
Birthdays in the service week (Wednesday→Tuesday) bring a gold glow and
|
||
raining confetti, and a Choir button adds the extra spot for the weeks there
|
||
is one. 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 the photo folder. Tile settings: parallax,
|
||
names, mic labels, and birthday confetti with its intensity and a test
|
||
picker — columns are − / + in the title bar.
|
||
- **Service Times** — every campus's Sunday service times, read from the
|
||
church website and re-read on a timer, so the board follows the site when
|
||
the times change through the year. Any service can be marked for
|
||
production — issue, replay, or do not use — with a note, and a campus
|
||
taking resi records which of the origin's services it is playing. Marking
|
||
a broadcast service "do not use" flags every campus riding on it, which is
|
||
the question the board exists to answer. Marks are shared by every tile
|
||
and every browser, survive a restart, and expire on their own so last
|
||
week's never greet a new Sunday. The + picker offers the whole board or a
|
||
single campus.
|
||
Admin config is in four groups: **Website** (address, which campuses to
|
||
follow, refresh interval), **Broadcast** (the campuses the broadcast comes
|
||
from, and each other campus's resi source), **Manual campuses** (a stopgap
|
||
for a campus the site has not published yet — the website takes over as
|
||
soon as it does) and **Marks** (how long a mark lives).
|
||
Tile settings: campuses, card or list layout, and a **Show** group for the
|
||
day, role, resi source, campuses fed, notes and the updated line.
|
||
- **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`, including the
|
||
send route the Send Message module uses),
|
||
`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](docs/MODULE-GUIDE.md).
|
||
|
||
## 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
|
||
├── docs/ALERTS.md the alerts feature: levels, channels, admin, API
|
||
├── tools/prodcom-mock.js mock ProdCom API for development
|
||
├── tools/alert.js raise / clear / list alerts from the command line
|
||
├── 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).
|