diff --git a/CLAUDE.md b/CLAUDE.md index 7a75f17..4cd911d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,8 +1,10 @@ # ProdDash -New web application for Waters Church production, to be developed in this repo. -The stack and structure of the new app are not yet decided — build it at the -repo root (or in an `app/` folder if that stays cleaner). +Modular production dashboard web app for Waters Church, developed in this repo. +**Read [SPEC.md](SPEC.md) first — it is the authority on what to build**: the +dashboard shell, the module system and its contract, the admin page, and the +two initial modules ported from the reference forks below. The app lives at +the repo root. ## Reference code (read-only forks — don't develop these here) diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..048af6b --- /dev/null +++ b/SPEC.md @@ -0,0 +1,245 @@ +# ProdDash — Build Instructions + +ProdDash is a flexible **production dashboard** web app for Waters Church. A +browser anywhere on the building network loads one page and composes its own +dashboard out of **modules** — independent tiles that each show one aspect of a +live service. Ships with two modules ported from the reference forks in this +repo: the live **ProdCom transcript** and the **ProPresenter Now/Next** display. + +This document is the authority for the initial build. Where it is silent, match +the conventions of the reference code (`prodcom-listener/`, +`band-lineup-display/`). + +## Goals + +1. A dashboard shell where each browser independently enables, arranges, and + resizes module tiles. Layouts persist in that browser's `localStorage` by + default; named layouts can also be saved to the server and loaded from any + browser. +2. An **admin page** where installed modules are configured (connection + settings, credentials, enable/disable) once, server-side, for everyone. +3. A **module system** with a documented contract, so future modules can be + developed and installed without touching the shell. Producing that guide + (`docs/MODULE-GUIDE.md`) is part of this build, not an afterthought. + +## Constraints and house style + +- **Runtime:** Node.js server + browser client, plain HTTP on the LAN. No + build step required to run (`node server.js` and it works), matching + `prodcom-listener`. Windows + macOS. +- **Dependencies:** keep the zero/low-dependency ethos. Prefer no npm deps on + the server. On the client, vanilla JS/CSS is preferred; if a grid library + genuinely earns its place for drag/resize (e.g. gridstack), **vendor it into + the repo** — no CDN loads, the app must work on a network with no internet. +- **Resilience first:** this runs during live services. Every module must + survive its upstream (ProdCom, ProPresenter) disappearing and reconnect + automatically, showing a clear degraded state — copy the reconnect patterns + in `prodcom-listener/public/app.js` and `propresenter-core`. +- **Look:** dark, high-contrast, production-booth friendly. Reuse the visual + language of `prodcom-listener/public/style.css` (its CSS variables, themed + scrollbars). All module styling via shared CSS variables so modules look + native. +- Installable as a PWA like prodcom-listener (manifest + service worker), + nice-to-have, last. + +## Architecture + +``` +ProdDash/ +├── server.js shell server: static files, config store, layouts API, +│ module discovery, mounts each module's server routes +├── config/ +│ ├── proddash.json port, admin passcode (optional), etc. +│ └── modules.json per-module config + enabled flags (written by admin page) +├── public/ dashboard shell client (index.html, shell.js, style.css) +│ └── admin/ admin page client +├── modules/ installed modules, one folder each (see contract) +│ ├── prodcom-transcript/ +│ └── propresenter-now-next/ +├── docs/MODULE-GUIDE.md how to build and install a module (deliverable) +├── prodcom-listener/ reference fork — read-only, not served +└── band-lineup-display/ reference fork — read-only, not served +``` + +Server responsibilities: + +- Discover modules by scanning `modules/*/module.json` at startup. +- Serve each module's client assets at `/modules//...`. +- Call each enabled module's server entry (if it has one) and mount its routes + under `/api/modules//...`, passing that module's config object. +- Shell API: `GET /api/modules` (installed+enabled manifests for the client), + layouts CRUD (`GET/PUT /api/layouts`, named layouts as JSON on disk under + `config/layouts/`), admin config API. +- Config changes from the admin page take effect without a manual restart + where practical (re-init the affected module); a clean restart is acceptable + for v1 if flagged in the admin UI. + +Client shell responsibilities: + +- Grid of tiles: add module (from enabled list), drag to move, drag to resize, + remove. Multiple instances of the same module are allowed (e.g. two + transcript tiles filtered to different channels). +- **Per-instance settings** (e.g. transcript channel filter, font size) are + chosen in the tile and stored with the layout — distinct from admin config, + which is server-wide (URLs, ports, API keys). +- Layout (tiles, positions, sizes, per-instance settings) autosaves to + `localStorage`. A layout menu offers: Save as named layout (to server), Load + named layout, Reset. Loading a named layout copies it into local state; it + does not live-link browsers together. +- Each browser is fully independent — no shared runtime state between viewers. + +## Admin page + +At `/admin` (link from the shell's menu): + +- Lists every installed module with name, version, description, and status + (enabled/disabled, connected/erroring if the module reports health). +- Per-module config form **generated from the `configSchema` in its manifest** + (text, number, boolean, select, password fields). Saving writes + `config/modules.json` and re-inits the module. +- Enable/disable toggle per module — disabled modules don't appear in the + shell's picker and their server routes are not mounted. +- Manage named layouts (list, rename, delete). +- Protect with a single shared passcode if `adminPasscode` is set in + `config/proddash.json`; no accounts, no sessions beyond a cookie. Plain HTTP + LAN tool — keep it simple. + +## Module contract (v1) + +A module is a folder in `modules/` containing: + +``` +modules// +├── module.json manifest (required) +├── client.js ES module, client entry (required) +├── server.js server entry (optional) +└── ... assets (css, images) served statically at /modules// +``` + +`module.json`: + +```json +{ + "id": "prodcom-transcript", + "name": "ProdCom Transcript", + "version": "1.0.0", + "description": "Live ProdCom transcript stream", + "client": "client.js", + "server": "server.js", + "minSize": { "w": 2, "h": 2 }, + "defaultSize": { "w": 4, "h": 3 }, + "configSchema": { + "prodcomUrl": { "type": "string", "label": "ProdCom URL", "default": "http://10.3.11.152:24480" }, + "apiKey": { "type": "password", "label": "API key (if PSK auth enabled)", "default": "" } + }, + "instanceSchema": { + "channel": { "type": "string", "label": "Channel filter", "default": "" } + } +} +``` + +Client entry (`client.js`) default-exports a factory: + +```js +export default function create({ root, moduleApi }) { + // root: the tile's DOM element (module owns everything inside it) + // moduleApi: { + // id, instanceId, + // config, // admin (server-wide) config, read-only + // instanceSettings, saveInstanceSettings(patch), // per-tile, persisted in layout + // fetch(path, opts), // fetch scoped to /api/modules// + // sse(path, handlers), // EventSource scoped the same way, with auto-reconnect + // setStatus('ok'|'connecting'|'error', msg) // drives the tile's status dot + // } + return { + start() {}, // begin rendering / connecting + stop() {}, // tear down everything (timers, streams, listeners) + onResize(w, h) {}, // optional; CSS should handle most of it + onConfigChange(cfg) {} // optional; admin config was updated + }; +} +``` + +Server entry (`server.js`, optional) exports: + +```js +module.exports = { + init({ config, log }) { /* return handle with stop() */ }, + routes({ config, log }) { + // return { 'GET /state': handler, 'GET /stream': sseHandler, ... } + // mounted under /api/modules// + } +}; +``` + +Rules for modules (enforce these in the guide): + +- Never touch DOM outside `root`; never define global CSS (scope with a + `[data-module=""]` prefix or shadow DOM). +- Style with the shell's CSS variables; no hardcoded colors. +- No external network calls from the client — anything upstream goes through + the module's server routes (same proxy reasoning as prodcom-listener: no + CORS, browsers never need to reach ProdCom/ProPresenter directly). +- Handle upstream loss: reconnect with backoff, report via `setStatus`. +- Installation = drop the folder into `modules/`, restart ProdDash, configure + in `/admin`. No shell code changes. + +`docs/MODULE-GUIDE.md` must document all of the above from a module author's +point of view, with a complete minimal example module (e.g. a clock) that can +be copy-pasted as a starter, and a testing section. + +## Initial modules + +### 1. `prodcom-transcript` + +Port `prodcom-listener/` into the module system: + +- Server part: the `/prodcom/*` proxy from `prodcom-listener/server.js` + (including SSE passthrough), driven by admin config instead of its own + `config.json`. +- Client part: the transcript UI from `prodcom-listener/public/` — history + load, live SSE, in-place updates of in-progress speech, channel colors, + auto-scroll with "↓ New messages", channel visibility toggles. +- Admin config: `prodcomUrl`, `apiKey`. Instance settings: channel filter, + text size. + +### 2. `propresenter-now-next` + +Port the Now/Next feature using `band-lineup-display/propresenter-core/` +**as-is** (copy it into the module; it has no deps and holds every +ProPresenter 7/20 quirk — do not re-implement it): + +- Server part: connect/poll ProPresenter via propresenter-core, expose current + state at `GET /state` and push changes over `GET /stream` (SSE). +- Client part: the Now/Next display, based on the page in + `band-lineup-display/propresenter-app/public/`. +- Admin config: ProPresenter host, port. Instance settings: display options + the existing page already supports. +- Must work against the mock: `node band-lineup-display/tmp/pp-mock.js 1599` + with host `127.0.0.1`, port `1599` — including the arrangement-repeat case + it simulates. + +## Milestones (build in this order, each independently verifiable) + +1. **Shell:** server + dashboard grid with a placeholder tile; add/move/ + resize/remove; layout survives reload via localStorage. +2. **Module system:** discovery, client loader, module API; placeholder + becomes a real trivial module (clock) proving the contract end-to-end. +3. **prodcom-transcript** module, verified against a live or mocked ProdCom. +4. **propresenter-now-next** module, verified against `pp-mock.js`. +5. **Admin page:** schema-driven config forms, enable/disable, persisted to + `config/modules.json`. +6. **Named layouts:** save to server, load from another browser. +7. **Docs:** `docs/MODULE-GUIDE.md` + README rewrite (run instructions in the + style of the prodcom-listener README). + +## Definition of done + +- `node server.js` on a clean clone starts everything; the printed URL works + from another device on the LAN. +- Two different browsers hold different layouts simultaneously; a named layout + saved in one loads in the other. +- Kill and restart ProdCom / the ProPresenter mock while tiles are open: tiles + show degraded status and recover on their own. +- A new module can be added by following MODULE-GUIDE.md alone, without + reading shell source.