# ProdDash Module Guide How to build, install and test a ProdDash module. Everything a module author needs is on this page — you never have to read the shell's source. A **module** is one kind of dashboard tile: the live transcript, the Now/Next display, a clock. Users add any number of tiles of your module to their own dashboards, move and resize them, and give each tile its own per-tile settings. Your module renders inside the tile and talks to the outside world only through the small API the shell hands it. ## Rules every module follows These are not style preferences — the admin page, the installer and the people running the booth depend on them. 1. **All settings live in the ProdDash admin page.** Server-wide settings go in `configSchema`, per-tile settings in `instanceSchema`. Never a separate setup page, wizard, config file the operator edits by hand, or environment variable. 2. **Dependencies ship inside the module folder.** Zero dependencies is the norm (the shell has none). Anything you genuinely need is vendored into the module — or fetched by your own `init()` on first start, with the admin health line saying what is happening. No `npm install`, no installer, no "run this first". 3. **Everything the module does runs inside the module.** No helper scripts, companion programs, daemons or services the operator has to start next to ProdDash. If your feature needs a listener, a decoder or a worker, your server entry owns it: start it in `init()`, stop it in `stop()`, report it in `health()`. A child process the module itself spawns and supervises is fine; a program someone launches by hand is not. **A native helper — anything that is not JavaScript — ships prebuilt inside the module folder, for Apple silicon macOS and 64-bit Windows; nothing is ever compiled on the machine running ProdDash** (see "Native helpers" below). 4. **Declare the ProdDash version you need** (`"proddash": ">=1.1.0"` in the manifest) and **bump your own `version`** whenever you change the module. The admin page refuses to install a module whose requirement the running shell doesn't meet, and it offers updates by comparing versions. ### Keeping the interface clean A dashboard is read across a booth, at a glance, by someone doing three other things. These rules are what keep every tile looking like one product instead of six. They are as binding as the four above. 5. **No explanatory text in a tile's gear.** No `help` on an `instanceSchema` field or an `instanceGroups` entry. Labels are **three words at most, one or two preferred** — "Slide count", "Item tags", "Running clock". A control that cannot be understood in three words is not a display choice; it belongs in `configSchema`, where `help` is welcome and expected. *One exception:* a `template` field may keep a single `help` line naming its placeholders, which nobody can guess. 6. **Server-side settings are admin-only.** Anything the module's server acts on — a host, a credential, what to fetch, how often to poll — is `configSchema`. `instanceSchema` is display choices and nothing else: what this tile shows, and how big. 7. **A tile setting changes that tile and nothing else.** Never another tile, never the module as a whole, never the server. If two tiles must agree on something, it is either admin config or page state the shell owns — not a setting each tile keeps its own copy of. 8. **Offer toggles for the elements you draw.** Every tile should let the operator switch off the parts it does not need. **One to four toggles:** give them no `group` at all, so they sit unlabelled at the top of the gear. **Five or more:** one group named **Show**, `collapsed: true`, `columns: 2`. Nothing else goes in that group — colours, formats and text belong in groups of their own. 9. **Title-bar controls are icons.** No `label` on `addButton` or `addMenu`; the tooltip carries the words, and `docs` above covers drawing the icon. A small state indicator may ride alongside an icon — ProdCom's channel button shows a count while a filter is on — but a control is never named in the bar. 10. **If a control needs words to explain it, it is not a title-bar control.** Put it in the gear. The bar is for actions whose icon and tooltip say everything: refresh, clear, jump to latest, bigger text. 11. **One control per action.** Whatever has a dedicated title-bar button is not offered again in the gear, and the reverse. Text size is A− / A+ up there, never a "Text size" field; a timestamps button in the bar means no timestamps switch below it. ## The shape of a module A module is one folder dropped into `modules/`: ``` modules// ├── module.json manifest (required) ├── client.js ES module, client entry (required) ├── server.js server entry (optional — only if you need server code) ├── bin/ native helpers, prebuilt per platform (only if you need one) │ ├── -macos-arm64 │ └── -win-x64.exe ├── native/ their source and the script that builds every bin/ file └── ... assets (css, images…) served statically at /modules// ``` **Installation is exactly this:** in the admin page, open **Available modules** and press **Install** — the module is downloaded from the ProdDash repo into the server's data directory (`/modules//`) and starts immediately. A packaged ProdDash — the macOS app, the Windows bundle — ships with **no modules at all**: every one of them arrives this way, and the shell's own update never brings one along, so what is in git is exactly what runs on a booth. Developing locally? A git checkout runs whatever is in its own `modules/` folder (the repo's copies, "bundled"), or drop a folder into the data directory's `modules/` and restart. No shell code changes, ever. In a checkout the two live side by side; when both have the same id the newer version runs. **Uninstall** in the admin page removes an installed module's files (in a checkout, the tree's copy is hidden instead, since that folder belongs to the repo); its settings are kept for a later reinstall. ### Native helpers Some features need a program JavaScript cannot be: reading an audio input, decoding timecode, measuring sound. Rule 3 says the module owns that program; this section says how it ships: **already built, for every platform it runs on, inside the module folder.** The machine running ProdDash is a packaged app or the Windows bundle plus modules from the admin page's catalog. It has no compiler and never will — the catalog delivers exactly the folder that is in the repo, so the binaries live in git next to the code. - **Platforms.** Apple silicon macOS and 64-bit Windows are required. Intel macOS is welcome but optional. One file per platform in `bin/`, named for what it is: | Platform | File | | --- | --- | | macOS, Apple silicon | `bin/-macos-arm64` | | Windows, 64-bit | `bin/-win-x64.exe` | | macOS, Intel (optional) | `bin/-macos-x86_64` | - **Pick it at runtime, and cope without it.** Choose by `process.platform` and `process.arch`; when there is no file for this machine, the feature says so — in `health()` and in the tile's `setStatus` — and everything else in the module keeps working. Never try to build it, never look for a copy elsewhere on the machine, never tell the operator to install anything. ```js const HELPERS = { darwin: { arm64: 'ltc-capture-macos-arm64', x64: 'ltc-capture-macos-x86_64' }, win32: { x64: 'ltc-capture-win-x64.exe' }, }; const name = HELPERS[process.platform]?.[process.arch]; const helper = name ? path.join(__dirname, 'bin', name) : ''; if (!helper || !fs.existsSync(helper)) { // Timecode is off on this platform. Say it once, keep the clock running. } ``` - **One contract, any language.** Every platform's binary speaks the same command line and output format, so `server.js` has one code path. What is behind it is your choice per platform — Swift against CoreAudio on macOS, C#, C or Rust on Windows — the module does not care. - **Source and build beside it.** Keep the source under `native/` with one script that produces every `bin/` file from a checkout, so any binary can be rebuilt from what is committed. A helper that cannot be rebuilt does not ship. - **Bump the module version whenever a binary changes**, like any other file — that is how installs learn about it. - **Keep them lean.** Optimised builds, no debug symbols, a few megabytes at most: the catalog fetches the whole repo as one archive, capped at 80 MB. - **Signing.** Apple silicon refuses unsigned code, so sign the macOS binary — Developer ID when you have it, ad-hoc at least. Windows needs nothing: files the app writes carry no download mark, so SmartScreen never sees them. Permissions come from the launcher (see "Permissions macOS has to grant"), not from the binary. A helper that ends up existing for one platform only is an exception made on purpose, not a default: say so in the module's README and in the health line, so a Windows booth knows why the feature is off. ## module.json ```json { "id": "my-module", "name": "My Module", "version": "1.0.0", "proddash": ">=1.1.0", "description": "One line shown in the Add-tile picker and the admin page", "permissions": [ { "kind": "microphone", "reason": "Why this module needs it, in one line" } ], "client": "client.js", "server": "server.js", "style": "style.css", "minSize": { "w": 2, "h": 2 }, "defaultSize": { "w": 4, "h": 3 }, "configSchema": { "upstreamUrl": { "type": "string", "label": "Upstream URL", "default": "http://10.0.0.5:1234" }, "apiKey": { "type": "password", "label": "API key", "default": "" } }, "instanceSchema": { "channel": { "type": "string", "label": "Channel filter", "default": "" } } } ``` | Field | Required | Meaning | | --- | --- | --- | | `id` | yes | Must equal the folder name. Lowercase, dashes. | | `name` | yes | Human name shown on the tile header and in menus. | | `version` | yes | Your module's version (`x.y.z`). Bump it on every change — the admin page offers updates when the repo's copy is newer than the installed one. | | `proddash` | yes | The ProdDash versions this module works with: `">=1.1.0"`, `"^1.1.0"`, `">=1.1.0 <2.0.0"`, or `"*"`. A module whose requirement the running shell doesn't meet is listed in the admin page but never loaded, and can't be installed from the catalog. | | `description` | no | Shown in the admin page / picker. | | `permissions` | no | What macOS must allow before this module can work — see **Permissions macOS has to grant**. Ignored by the shell; read by the macOS launcher. | | `provides` | no | Capabilities other modules can consume — `["timers"]` today. Forwarded in `/api/modules` so a consumer can find you. See **Working with other modules**. | | `client` | yes | Client entry file, loaded as an ES module. | | `server` | no | Server entry file (CommonJS), loaded with `require()`. | | `style` | no | A stylesheet the shell injects once per module. | | `minSize`, `defaultSize` | no | Tile size in grid cells, declared for the default grid (12 columns, 72 px rows). Defaults: min 1×1, default 4×3. A dashboard may cut its board finer or coarser (8–24 columns, from the Layouts menu; row height follows). `defaultSize` is scaled to that density when a tile is added, so a new tile lands at about the size you meant; `minSize` is enforced in cells as declared, so on a finer grid a tile can be made smaller than it could at 12. | | `configSchema` | no | **Admin config** — server-wide settings (URLs, ports, keys) edited once at `/admin` for everyone. | | `instanceSchema` | no | **Per-tile settings** — chosen in each tile's gear menu and stored with that browser's layout (filters, text sizes, display options). They apply as they are changed, so keep them to display choices. | | `tiles` | no | Fixed multi-tile list for client-only modules; server modules export a dynamic `tiles()` instead. See **Presenting multiple tiles**. | Both schemas map field names to specs: ``` { "type": "string" | "text" | "number" | "boolean" | "switch" | "select" | "multiselect" | "template" | "color" | "password" | "endpoint", "label": "Shown next to the field", "default": , "group": "Section heading", // optional; see below "showWhen": "", // optional; see below "help": "One line shown under the control", // optional, admin config only "options": [ { "value": "a", "label": "A", "group": "Optgroup" }, … ], // "select" only "optionsRoute": "/devices", // "select" only, admin config only "optionsDependsOn": "" // with optionsRoute; see below } ``` `boolean` and `switch` are the same true/false value. In a tile's gear popover both render as a switch; in the admin page `switch` is the toggle and `boolean` the plainer checkbox (use `switch` for a feature on/off that other fields depend on). **Label a switch with the noun of what it turns on** — "Seconds", "Top bar", "Section headers" — never "Show seconds": on means shown or enabled, off means hidden or disabled, and the label shouldn't say so twice. `color` renders a swatch picker and stores a `#rrggbb` string (give it a hex default; the tile applies it as a CSS custom property — see `pco-plan`). `multiselect` is several picks from one list — the same `options` / `optionsRoute` as `select`, rendered as one switch per option and stored as an array of values. A saved value the list no longer offers stays checked and marked, so nothing is dropped behind anyone's back. In a tile's gear popover it also takes `columns` (2–4) and `allByDefault: true`: a tile that has never chosen shows every option on and saves nothing until someone flips one, so options that turn up later are on as well (give it no `default` then). `select` and `multiselect` may use `optionsRoute` in `instanceSchema` too — the popover GETs `/api/modules/` like the admin page does. `template` (tile settings) is a text with placeholders, built by dragging or clicking the variables the module offers into it: `"variables": [ { "value": "{series}", "label": "Series" }, … ]`. Keep `help` to the joining rules; the chips carry the names. `text` is a multi-line string — a `