Pin the contracts the next round of work meets at

Four pieces of work are about to run in parallel — the shell, the admin
page, the Timers module and the PCO Plan module — and three of them meet at
things that didn't exist yet. This writes those down first, so they are built
once, the same way.

- **Offering timers to the Timers module.** A module declares
  `"provides": ["timers"]` and serves `GET /timers` (a cheap snapshot with
  startedAt/elapsedMs/targetMs, an optional status line, optionally an SSE
  stream). The consumer extrapolates locally, so no fast polling. Modules meet
  over loopback through each other's routes, as PCO Plan already does to
  follow ProPresenter.
- **Admin group controls.** `configGroups[name].help` becomes an ⓘ beside the
  group title rather than a paragraph under it; `configGroups[name].toggle`
  names a switch that renders in the title row as the group's on/off.
- **Switches, labelled with nouns.** On means shown or enabled; the label
  shouldn't say so twice.
- **Themes** swap the shell variables; a tile's own configured colour is set
  closer and wins.

Two shell hooks make it real: `/api/modules` forwards `provides`, and module
servers get `shell.port` in init/routes/tiles instead of guessing it from
proddash.json.

Clock: descriptions gone, "Show seconds" is "Seconds" (1.0.1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-13 12:32:31 -04:00
co-authored by Claude Fable 5.1
parent 7b353cf7f8
commit a6dc2ddb85
3 changed files with 103 additions and 13 deletions
+8 -3
View File
@@ -373,14 +373,17 @@ function mountModule(id) {
// Re-require fresh config on every (re)mount; the module code itself stays cached.
const mod = require(serverPath);
const config = effectiveConfig(id);
// What a module needs to reach the rest of ProdDash — another module's
// routes over loopback, chiefly — without guessing.
const shell = { port: PORT };
if (typeof mod.init === 'function') {
entry.handle = mod.init({ config, log }) || null;
entry.handle = mod.init({ config, log, shell }) || null;
}
if (typeof mod.routes === 'function') {
entry.routes = parseRouteTable(mod.routes({ config, log }));
entry.routes = parseRouteTable(mod.routes({ config, log, shell }));
}
if (typeof mod.tiles === 'function') {
entry.tiles = () => mod.tiles({ config, log });
entry.tiles = () => mod.tiles({ config, log, shell });
}
log('mounted' + (entry.routes.length ? ` (${entry.routes.length} routes)` : ''));
} catch (err) {
@@ -510,6 +513,8 @@ async function clientManifest(id) {
instanceSchema: man.instanceSchema || {},
instanceGroups: man.instanceGroups && typeof man.instanceGroups === 'object' ? man.instanceGroups : {},
hasServer: Boolean(man.server),
// capabilities other modules consume ("timers") — see the module guide
provides: Array.isArray(man.provides) ? man.provides.map(String) : [],
config: clientConfig(id),
tiles: await moduleTiles(id),
};