# 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. 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) └── ... 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. Developing locally? Drop the folder into `modules/` (bundled with the checkout) or into the data directory's `modules/` and restart. No shell code changes, ever. Modules bundled with a checkout and modules installed from the admin page 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 (a bundled one is hidden instead, since its folder belongs to the app); its settings are kept for a later reinstall. ## 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 (12 columns wide; rows are ~72 px). Defaults: min 1×1, default 4×3. | | `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 `