Files
ProdDash/SPEC.md
Waters ChurchandClaude Opus 5 a3e1282354 No campus's address ships in the repo
prodcom-transcript's endpoint defaulted to 10.3.11.152:24480 — Apollo
Beach's ProdCom. A fresh install anywhere therefore came up reading AB's
comms and reported itself healthy doing it: that is exactly what the new
North Attleboro install did. The default is now blank, so a new install
says "No ProdCom server configured" (in red, as the ProPresenter modules
already do for a missing host) until someone points it at the ProdCom for
that location. Setting a host still connects as before.

The same address is out of SPEC.md's worked example, CLAUDE.md's note
about the reference app, and the prodcom-listener snapshot's config,
README and fallback — that fallback now exits with a line telling the
operator what to set, rather than throwing on an empty URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:37:56 -04:00

11 KiB

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/<id>/....
  • Call each enabled module's server entry (if it has one) and mount its routes under /api/modules/<id>/..., 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/<id>/
├── module.json      manifest (required)
├── client.js        ES module, client entry (required)
├── server.js        server entry (optional)
└── ... assets (css, images) served statically at /modules/<id>/

module.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": "" },
    "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:

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/<id>/
  //   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:

module.exports = {
  init({ config, log }) { /* return handle with stop() */ },
  routes({ config, log }) {
    // return { 'GET /state': handler, 'GET /stream': sseHandler, ... }
    // mounted under /api/modules/<id>/
  }
};

Rules for modules (enforce these in the guide):

  • Never touch DOM outside root; never define global CSS (scope with a [data-module="<id>"] 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.