From ac1f17b9ed3e865c90256f665acf273116ccb770 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Wed, 30 Sep 2026 09:34:10 -0400 Subject: [PATCH] Shell 1.10.0: a packaged ProdDash carries no modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS app and the Windows bundle shipped every module in the repo, on by default, and a shell update silently added whatever new module had landed on main — Band Lineup appeared on AB that way. A module was also only as fresh as the last shell update. Now a packaged install (no .git — the same test the updater uses to choose the archive over git pull) carries no modules at all: every one arrives from the admin page's catalog into the data directory, and the shell's own update never writes modules/. A git checkout keeps running the modules in its tree. Existing installs move over by themselves: at the first start, the bundled modules that are switched on are copied into the data directory as installed copies (a compiled helper keeps its mode), an uninstalled one is dropped, and the app folder's modules/ is removed. Settings are keyed by module id, so nothing changes for the operator. Around it: the dashboard's empty state says "No modules yet — open Admin" when the registry is empty; the admin page says "Nothing installed yet", puts a checkbox on every compatible catalog entry and an Install selected button (POST /api/admin/modules/install {ids}) that fetches them in one go. The Mac build stops copying modules/ and no longer compiles or signs Swift helpers — those ship prebuilt inside a module, per the guide. tools/unbundled.test.js drives a packaged copy of the shell through the move, an archive update that must not bring modules back, and a multi-install. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 3 +- README.md | 18 ++-- docs/MODULE-GUIDE.md | 17 ++-- launcher/macos/README.md | 7 +- launcher/macos/build.sh | 27 +----- package.json | 2 +- public/admin/admin.css | 1 + public/admin/admin.js | 43 +++++++++ public/admin/index.html | 1 + public/shell.js | 10 ++ server.js | 79 +++++++++++++++ tools/unbundled.test.js | 203 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 370 insertions(+), 41 deletions(-) create mode 100644 tools/unbundled.test.js diff --git a/CLAUDE.md b/CLAUDE.md index ab20021..572c2c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,8 @@ Modular production dashboard web app for Waters Church, developed in this repo. The app lives at the repo root and is built: zero-dependency Node server (`server.js`, Node 18+), dashboard shell client in `public/`, admin page at -`/admin`, modules in `modules/`, and a native macOS menu-bar launcher in +`/admin`, modules in `modules/` (the repo's copies — a packaged install ships +none and installs them from the admin page's catalog), and a native macOS menu-bar launcher in `launcher/macos/` (Swift, no dependencies — the shipped macOS app: it carries the server and its own Node runtime, supervises them, and owns the macOS permissions modules declare; see its README). Runtime state (module diff --git a/README.md b/README.md index 0efedf6..515c758 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ and a clear degraded state, then recover on their own. Open the disk image and drag **ProdDash** to Applications. That's the whole installation: the app carries the dashboard and its own Node runtime, so -nothing else has to be installed and nothing can be missing. It lives in the +nothing else has to be installed. It carries no modules — open **Admin → +Available modules**, tick the ones this machine needs and press **Install +selected**; each updates on its own from then on. It lives in the menu bar, starts and stops the server, can open at login, and holds the permissions the modules need — the microphone the LTC listener reads, and local network access for ProdCom and ProPresenter. macOS grants those to the @@ -179,13 +181,15 @@ below. Nothing is installed without someone clicking. - **Available modules** lists modules in the repo that aren't installed here. **Install** downloads the module into the data directory's `modules/` and starts it right away — nothing is written into the app folder, and no - restart is needed. Modules that ship with your copy of ProdDash are - "bundled"; ones you installed are "installed"; if both exist, the newer - version runs. + restart is needed. Tick several and press **Install selected** to fetch + them in one go. A packaged ProdDash (the app, the Windows bundle) ships + with no modules at all: this list is how every one of them arrives, and a + ProdDash update never brings one along. A git checkout also runs the + modules in its own tree ("bundled"); if both exist, the newer version runs. - **Uninstall** on a module card removes an installed module's files. For a - bundled module (its folder belongs to the app) it hides the module from - every dashboard instead; it reappears under Available modules and can be - brought back without downloading. Settings are kept either way. + checkout's bundled module (its folder belongs to the repo) it hides the + module from every dashboard instead; it reappears under Available modules + and can be brought back without downloading. Settings are kept either way. - **Module updates**: when the repo has a newer version of an installed module, its card shows **Update to vX**. - **Version requirements**: every module declares the ProdDash versions it diff --git a/docs/MODULE-GUIDE.md b/docs/MODULE-GUIDE.md index 7de6e4f..e41c2f5 100644 --- a/docs/MODULE-GUIDE.md +++ b/docs/MODULE-GUIDE.md @@ -96,13 +96,16 @@ 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. +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 diff --git a/launcher/macos/README.md b/launcher/macos/README.md index 9ea0e6d..c287a19 100644 --- a/launcher/macos/README.md +++ b/launcher/macos/README.md @@ -68,9 +68,10 @@ Builds for **Apple Silicon**. To change that, or to move to a newer Node, edit against nodejs.org's published checksums either way. The app icon is built from `public/icons/icon-512.png`, so ProdDash's own icon -is the app's icon. A module's native helper is compiled during the build -(`.swift` beside a module's files becomes ``), so the Mac running -ProdDash never needs a compiler. +is the app's icon. The app carries no modules: they are installed from the +admin page's catalog into the data directory, and a module's native helper +ships prebuilt inside it (see the module guide), so the Mac running ProdDash +never needs a compiler. ## Signing diff --git a/launcher/macos/build.sh b/launcher/macos/build.sh index 5bb9b0b..43d3580 100755 --- a/launcher/macos/build.sh +++ b/launcher/macos/build.sh @@ -1,6 +1,8 @@ #!/bin/zsh # Build ProdDash.app — the whole application: launcher, server and its own -# Node runtime, with nothing to install first. +# Node runtime, with nothing to install first. It carries no modules: those +# come from the admin page's catalog into the data directory, and a module's +# native helper ships prebuilt inside it (docs/MODULE-GUIDE.md). # # ./build.sh build/ProdDash.app, self-contained # ./build.sh --dmg …and build/ProdDash--arm64.dmg @@ -127,23 +129,14 @@ if [[ $DEV -eq 0 ]]; then tar -xzf "$CACHE_DIR/$TARBALL" -C "$CONTENTS/Resources/node/bin" --strip-components 2 \ "node-$NODE_VERSION-darwin-$ARCH/bin/node" - echo " embedding ProdDash $VERSION" + echo " embedding ProdDash $VERSION (no modules — the catalog installs them)" mkdir -p "$PAYLOAD/config" cp "$REPO_ROOT/server.js" "$REPO_ROOT/package.json" "$PAYLOAD/" - cp -R "$REPO_ROOT/public" "$REPO_ROOT/modules" "$PAYLOAD/" + cp -R "$REPO_ROOT/public" "$PAYLOAD/" # Only the checked-in defaults: config/ also holds this machine's own # modules.json and layouts when ProdDash has ever run from the checkout. cp "$REPO_ROOT/config/proddash.json" "$PAYLOAD/config/" find "$PAYLOAD" -name '.DS_Store' -delete - - # A module's native helper is built here, so the Mac running ProdDash needs - # no compiler: .swift beside a module's files becomes . - for source in "$PAYLOAD"/modules/*/*.swift; do - [[ -e "$source" ]] || continue - helper="${source%.swift}" - echo " compiling $(basename "$(dirname "$source")")/$(basename "$helper")" - swiftc -O -target "$ARCH-apple-macos$DEPLOYMENT_TARGET" -o "$helper" "$source" - done fi # ── signing ────────────────────────────────────────────────────────────── @@ -165,16 +158,6 @@ fi if [[ -f "$CONTENTS/Resources/node/bin/node" ]]; then codesign "${SIGN[@]}" --entitlements Resources/node.entitlements "$CONTENTS/Resources/node/bin/node" fi -if [[ $DEV -eq 0 ]]; then - for helper in "$PAYLOAD"/modules/*/*; do - # A compiled helper is the extensionless executable beside a module's - # sources — test the name, not the path, which has dots all over it. - name=$(basename "$helper") - [[ -f "$helper" && -x "$helper" && "$name" != *.* ]] || continue - echo " signing $name" - codesign "${SIGN[@]}" --entitlements Resources/helper.entitlements "$helper" - done -fi codesign "${SIGN[@]}" --entitlements Resources/ProdDash.entitlements --identifier "$BUNDLE_ID" "$APP" codesign --verify --deep --strict "$APP" && echo " signature verifies" diff --git a/package.json b/package.json index c07ca1e..d70640d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proddash", - "version": "1.9.2", + "version": "1.10.0", "private": true, "description": "Modular production dashboard for Waters Church \u2014 zero-dependency Node server + vanilla client", "license": "UNLICENSED", diff --git a/public/admin/admin.css b/public/admin/admin.css index 12f0018..0a7ae91 100644 --- a/public/admin/admin.css +++ b/public/admin/admin.css @@ -443,6 +443,7 @@ a.btn { text-decoration: none; } margin-bottom: 10px; } .catalog-main { flex: 1; min-width: 0; } +.catalog-pick { margin: 5px 10px 0 0; flex: none; } .catalog-title { display: flex; align-items: center; gap: 8px; } .catalog-row .module-desc { margin: 4px 0 2px; } .catalog-row .module-health { margin-top: 2px; } diff --git a/public/admin/admin.js b/public/admin/admin.js index f035683..18dff9b 100644 --- a/public/admin/admin.js +++ b/public/admin/admin.js @@ -962,6 +962,13 @@ async function loadState() { for (const mod of state.modules || []) { modulesList.appendChild(buildModuleCard(mod)); } + if (!(state.modules || []).length) { + // A packaged ProdDash starts like this: the catalog below is the way in. + const none = document.createElement('div'); + none.className = 'layouts-empty'; + none.textContent = 'Nothing installed yet — pick from Available modules below.'; + modulesList.appendChild(none); + } renderPin(state); loadTheme(); loadLayouts(); @@ -1163,6 +1170,7 @@ function renderAvailable(cat) { els.updateSlot.appendChild(btn); } const available = cat.available || []; + installSelectedBtn.hidden = available.filter((a) => a.compatible).length < 2; if (!available.length) { const none = document.createElement('div'); none.className = 'layouts-empty'; @@ -1205,11 +1213,46 @@ function renderAvailable(cat) { btn.disabled = !item.compatible; btn.title = item.compatible ? '' : item.reason; btn.addEventListener('click', () => installFromCatalog(item, btn)); + if (item.compatible) { + const pick = document.createElement('input'); + pick.type = 'checkbox'; + pick.className = 'catalog-pick'; + pick.dataset.id = item.id; + pick.dataset.name = item.name; + pick.title = 'Tick several, then Install selected'; + row.append(pick); + } row.append(main, btn); availableList.appendChild(row); } } +const installSelectedBtn = document.getElementById('install-selected'); + +/** The ticked modules, in one request; the answer says which ones landed. */ +async function installSelected() { + const picks = [...availableList.querySelectorAll('.catalog-pick:checked')]; + if (!picks.length) return toast('Tick the modules to install first', true); + installSelectedBtn.disabled = true; + installSelectedBtn.textContent = `Installing ${picks.length}…`; + try { + const r = await api('/api/admin/modules/install', { method: 'POST', body: JSON.stringify({ ids: picks.map((p) => p.dataset.id) }) }); + const done = (r.results || []).filter((x) => x.ok).length; + const failed = (r.results || []).filter((x) => !x.ok); + toast(failed.length + ? `${done} installed; ${failed.map((f) => `${f.id}: ${f.error}`).join(' · ')}` + : `${done} module${done === 1 ? '' : 's'} installed`, failed.length > 0); + await loadState(); + } catch (e) { + if (e.authRequired) showLogin(); + else toast(e.message, true); + } finally { + installSelectedBtn.disabled = false; + installSelectedBtn.textContent = 'Install selected'; + } +} +installSelectedBtn.addEventListener('click', installSelected); + async function installFromCatalog(item, btn) { btn.disabled = true; const label = btn.textContent; diff --git a/public/admin/index.html b/public/admin/index.html index ef9fe9c..fa819f0 100644 --- a/public/admin/index.html +++ b/public/admin/index.html @@ -58,6 +58,7 @@