ProdDash now has a version (package.json, 1.1.0) and modules declare the
shell versions they need ("proddash": ">=1.1.0" in the manifest). A module
whose requirement this shell doesn't meet is listed in the admin page but
never loaded, and cannot be installed from the catalog.
Modules come from two places: bundled with the checkout (modules/) and
installed from the admin page into the data directory (<data dir>/modules/).
Same id in both → the newer version runs; a downloaded copy the bundled one
has overtaken is removed. The admin page's new "Available modules" section
lists modules in the ProdDash repo that aren't installed here — one small
tar.gz of the branch is fetched (at most every few minutes, only while the
admin page is open or for the periodic update check) and read with a
dependency-free tar reader (ustar, GNU long names, pax headers; the pax
global comment carries the commit sha). Install downloads the module folder
into the data directory and mounts it immediately; a module card offers
"Update to vX" when the repo has a newer version. Uninstall removes an
installed module's files; a bundled module is hidden instead (its folder
belongs to the app) and can be restored from Available modules without a
download. Settings are kept across uninstall/reinstall.
Shell updates: the server compares the repo's package.json version and
commit sha with its own (git rev-parse when the app folder is a checkout),
shows a banner in the admin page, and "Update now" brings the app folder up
to date — git pull --ff-only for a checkout that is on the repo branch with
no local changes (file modes ignored), otherwise the archive is unpacked over
the folder (runtime state and .git untouched). The server then exits with
code 75 and starts its own replacement unless PRODDASH_LAUNCHER is set; the
replacement waits for the port to free up (EADDRINUSE retry). The page polls
until the new version answers. PRODDASH_REPO (owner/repo#branch),
PRODDASH_ARCHIVE_BASE and PRODDASH_NO_REMOTE_CHECK configure/disable it.
Docs: MODULE-GUIDE gets the rules every module follows (all settings in the
admin page, dependencies shipped inside the module, everything runs inside
the module's server entry — no helper programs, declare the ProdDash
requirement and bump the version), the proddash/version manifest fields and
the new installation story; README gets "Installing modules and updating";
CLAUDE.md summarises the rules.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
Connection settings, API keys, enabled flags and named layouts were written
to config/ inside the checkout, so a re-clone or reset from main started the
server with nothing configured. They now live in a data directory outside
the app folder:
Windows %APPDATA%\ProdDash
macOS ~/Library/Application Support/ProdDash
other $XDG_CONFIG_HOME/proddash, else ~/.config/proddash
PRODDASH_DATA_DIR overrides the location (a relative path resolves against
the app folder, so "config" restores the old spot). The first start after
upgrading copies an existing config/modules.json and config/layouts/ into
the data directory, so nothing has to be re-entered. If the directory cannot
be created or written, the server warns and falls back to config/ rather
than refusing to start. A proddash.json in the data directory overrides the
checked-in defaults (port, adminPasscode).
The startup banner prints the directory in use, /api/admin/state exposes it,
and the admin page shows it under the Modules heading. Admin config and
layout writes that fail on disk now report the path instead of a generic
server error. README, MODULE-GUIDE and CLAUDE.md updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
New configSchema type 'endpoint' declared in the module spec: anything
that points at a device or service on the network must be one endpoint
field, rendered identically for every module in /admin as a joined
Host/IP : Port control and stored/handed to the module server as
{ host, port }. Migrates all three connection modules — ProdCom (was a
full URL field), ProPresenter and Timers (were separate host + port
fields). Legacy stored configs (prodcomUrl / flat host+port) keep
working until the admin form is re-saved, at which point the new shape
replaces them. MODULE-GUIDE documents the type and the presentation
rule: no split host/port fields, and full URLs only for genuine web
addresses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
A module may now present a list of tiles instead of a single picker
entry. A server module exports tiles({config, log}) — sync or async,
called on every picker load so the list can be live data (discovered
ProPresenter timers, admin-configured pages); a client-only module can
declare a static tiles array in module.json. The + picker groups the
entries under the module's name; adding one presets that entry's
instance settings, gives the tile the entry's name as its title, and
honors per-entry default/min sizes, all persisted with the layout.
moduleApi.variant tells the instance which entry it was added as. A
throwing or hung tiles() (2 s deadline) falls back to the classic single
entry so a dead upstream can never break the picker; modules without
tiles are byte-for-byte unaffected. Documented in docs/MODULE-GUIDE.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
Milestone 6. Layouts CRUD as JSON under config/layouts/ — GET list,
GET/PUT by name (open to every dashboard), DELETE and rename gated by the
admin passcode. Layout menu grows 'Save as named layout…' and a
'Load from server' list; loading copies the layout into the browser's own
local state, never live-links viewers. Admin page lists, renames and
deletes named layouts. Verified with two isolated browser contexts
holding different layouts simultaneously.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
Milestone 5. GET /api/admin/state, PUT /api/admin/modules/<id>/config and
/enabled persist to config/modules.json and re-init the module in place —
no restart. /api/events (SSE) tells open dashboards about admin changes;
the shell then calls onConfigChange(cfg) on instances that implement it
and cleanly remounts the rest, so tiles reconnect on their own. Admin
page renders forms from each configSchema (text/number/boolean/select/
password), shows module health from handle.health(), and never echoes
password values back to a browser. Optional shared passcode
(config/proddash.json adminPasscode or PRODDASH_PASSCODE) gates the admin
API behind a cookie; the dashboard API stays open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
Milestone 2. Server scans modules/*/module.json, serves module assets at
/modules/<id>/, mounts optional server routes (exact + trailing-/* prefix
patterns) under /api/modules/<id>/ with per-module config from
config/modules.json, and lists enabled manifests at GET /api/modules.
Shell dynamically imports each module's client.js and hands it the
moduleApi (config, instanceSettings, scoped fetch, auto-reconnecting sse,
setStatus). Clock module proves the contract end to end; password-typed
config fields never leave the server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
Milestone 1 of SPEC.md. Zero-dependency Node server serving public/;
12-column tile grid with add (placeholder tile), drag to move, drag to
resize, remove; collision-free placement; layout autosaves to this
browser's localStorage and survives reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH