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 @@