Compare commits

..
Author SHA1 Message Date
Waters ChurchandClaude Opus 5 1be5cbef31 The church's own Gitea is where ProdDash lives
The shipped defaults pointed at GitHub, so a fresh install with no
configuration would have looked there — and that repository is going away.
They now name michael/ProdDash on chronicles-ab.waterschurch.org:3000, which
a fresh install was verified to reach with nothing configured at all.

The hostname rather than the IP, because the UniFi DNS record is
authoritative and this survives the NAS changing address. Both production
machines already carry an explicit setting of their own, so neither is
affected either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:00:22 -04:00
Michael McGary 1c8cd63cd0 Merge pull request #20 from michaelmcgary2008/claude/shell-1-5-0
Shell 1.5.0
2026-09-16 18:25:02 -04:00
Waters ChurchandClaude Opus 5 af6322d079 Shell 1.5.0: updates can come from your own git server
The forge setting is a new capability, so the version moves — which is
also how a machine already running 1.4.0 is offered the update at all
(an archive install compares versions, having no commit of its own).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:24:57 -04:00
Michael McGary 168f717398 Merge pull request #19 from michaelmcgary2008/claude/self-hosted-forge
Updates can come from your own git server
2026-09-16 18:23:38 -04:00
Waters ChurchandClaude Opus 5 67bcd3640a Updates can come from your own git server
ProdDash fetched one tar.gz of the branch from GitHub's codeload host, with
that URL shape hardcoded. A `forge` setting now names where the code comes
from: absent or "github" behaves exactly as before, and a Gitea or Forgejo
base URL such as http://10.3.8.90:3000 fetches from a server of your own.

Gitea spells an archive URL differently — /archive/<branch>.tar.gz rather
than /tar.gz/<branch> — and that is the only real difference. Its tarball
carries the same pax header with the same commit hash, so the archive
parser, update detection and module installs are untouched. The "what
changed" link follows the forge too, using each one's own compare and
commits paths.

Verified against both: the same commit and version come back from GitHub
and from a self-hosted Gitea, and a module installs from each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:23:20 -04:00
Michael McGary 095fe66118 Merge pull request #17 from michaelmcgary2008/claude/no-campus-defaults
No campus's address ships in the repo
2026-09-16 10:38:32 -04:00
Waters ChurchandClaude Opus 5 a3e1282354 No campus's address ships in the repo
prodcom-transcript's endpoint defaulted to 10.3.11.152:24480 — Apollo
Beach's ProdCom. A fresh install anywhere therefore came up reading AB's
comms and reported itself healthy doing it: that is exactly what the new
North Attleboro install did. The default is now blank, so a new install
says "No ProdCom server configured" (in red, as the ProPresenter modules
already do for a missing host) until someone points it at the ProdCom for
that location. Setting a host still connects as before.

The same address is out of SPEC.md's worked example, CLAUDE.md's note
about the reference app, and the prodcom-listener snapshot's config,
README and fallback — that fallback now exits with a line telling the
operator what to set, rather than throwing on an empty URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:37:56 -04:00
Michael McGary 6417c3aa54 Merge pull request #16 from michaelmcgary2008/claude/slack-module
Slack module: channel transcripts, PIN-gated sending as the location
2026-09-15 15:25:24 -04:00
Michael McGary c1d9fa0527 Merge pull request #14 from michaelmcgary2008/claude/proddash-app-tweaks
Shell: quieter menu handle, calmer fullscreen, live tile settings
2026-09-15 15:24:27 -04:00
11 changed files with 88 additions and 23 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ title-bar controls). The rules that matter most:
- `prodcom-listener/` — live ProdCom transcript web app. Zero-dependency Node - `prodcom-listener/` — live ProdCom transcript web app. Zero-dependency Node
`server.js` that serves `public/` and proxies `/prodcom/*` (including the SSE `server.js` that serves `public/` and proxies `/prodcom/*` (including the SSE
transcript stream) to the ProdCom API at `config.json → prodcomUrl` transcript stream) to the ProdCom API at `config.json → prodcomUrl`
(default `http://10.3.11.152:24480`). Good reference for: SSE handling, (set per site; each campus runs its own). Good reference for: SSE handling,
reconnect logic, channel colors/filtering, PWA install. reconnect logic, channel colors/filtering, PWA install.
- `band-lineup-display/` — backstage Electron app (stage lineup photos with - `band-lineup-display/` — backstage Electron app (stage lineup photos with
mic/pack labels) plus ProPresenter Now/Next: mic/pack labels) plus ProPresenter Now/Next:
+20 -5
View File
@@ -100,10 +100,24 @@ with this machine's shell settings — the same keys as the checked-in
| --- | --- | | --- | --- |
| `port` | Port ProdDash serves on (default `24500`). The macOS launcher writes this one, so the port it is set to holds however ProdDash is started. | | `port` | Port ProdDash serves on (default `24500`). The macOS launcher writes this one, so the port it is set to holds however ProdDash is started. |
| `adminPasscode` | If set, the admin page asks for this shared passcode. Leave `""` for none — it's a plain-HTTP LAN tool. | | `adminPasscode` | If set, the admin page asks for this shared passcode. Leave `""` for none — it's a plain-HTTP LAN tool. |
| `forge` | Where updates and module installs are fetched from. Absent or `"github"` uses GitHub; a Gitea or Forgejo base URL such as `"http://10.3.8.90:3000"` uses a server of your own. Set it per machine in the data directory, alongside `repo`. |
| `theme` | The dashboard palette: `booth` (default), `harbor`, `graphite`, `ember` or `daylight`. The admin page's **Theme** section writes this one — see [Themes](#themes). | | `theme` | The dashboard palette: `booth` (default), `harbor`, `graphite`, `ember` or `daylight`. The admin page's **Theme** section writes this one — see [Themes](#themes). |
**Updating from your own git server.** ProdDash fetches one `tar.gz` of the
branch — nothing else — so any Gitea or Forgejo works. Point `forge` at its
base URL and `repo` at the owner/repo there:
```json
{ "forge": "http://10.3.8.90:3000", "repo": "michael/ProdDash", "branch": "main" }
```
Gitea spells an archive URL differently from GitHub, which is the only thing
that changes; the tarball itself is identical, commit hash included, so update
detection and module installs behave exactly as before. Make the repository
readable without signing in, or ProdDash cannot fetch it.
Environment variables override both files: `PORT`, `PRODDASH_PASSCODE`, and Environment variables override both files: `PORT`, `PRODDASH_PASSCODE`, and
`PRODDASH_DATA_DIR` to put the data directory somewhere else (a relative path `PRODDASH_FORGE`, and `PRODDASH_DATA_DIR` to put the data directory somewhere else (a relative path
is taken from the app folder; `PRODDASH_DATA_DIR=config` restores the old is taken from the app folder; `PRODDASH_DATA_DIR=config` restores the old
in-repo location). If the default directory can't be created, the server says in-repo location). If the default directory can't be created, the server says
so and falls back to the app's `config/` folder. so and falls back to the app's `config/` folder.
@@ -131,10 +145,11 @@ set on a tile itself (a PCO Plan header colour, say) is deliberately kept.
## Installing modules and updating ## Installing modules and updating
Everything below happens in the **admin page**; ProdDash talks to its GitHub Everything below happens in the **admin page**; ProdDash talks to its git
repository (`michaelmcgary2008/ProdDash`, branch `main` — change with repository — `michael/ProdDash` on the church's own Gitea at
`repo` / `branch` in `proddash.json` or the `PRODDASH_REPO` variable, `chronicles-ab.waterschurch.org:3000`, branch `main`. Change it with `forge`,
`owner/repo#branch`). It downloads one small archive of the branch, at most `repo` and `branch` in `proddash.json`, or the `PRODDASH_FORGE` and
`PRODDASH_REPO` variables (`owner/repo#branch`). It downloads one small archive of the branch, at most
every few minutes, only while the admin page is open or for the update check every few minutes, only while the admin page is open or for the update check
below. Nothing is installed without someone clicking. below. Nothing is installed without someone clicking.
+1 -1
View File
@@ -129,7 +129,7 @@ modules/<id>/
"minSize": { "w": 2, "h": 2 }, "minSize": { "w": 2, "h": 2 },
"defaultSize": { "w": 4, "h": 3 }, "defaultSize": { "w": 4, "h": 3 },
"configSchema": { "configSchema": {
"prodcomUrl": { "type": "string", "label": "ProdCom URL", "default": "http://10.3.11.152:24480" }, "prodcomUrl": { "type": "string", "label": "ProdCom URL", "default": "" },
"apiKey": { "type": "password", "label": "API key (if PSK auth enabled)", "default": "" } "apiKey": { "type": "password", "label": "API key (if PSK auth enabled)", "default": "" }
}, },
"instanceSchema": { "instanceSchema": {
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"port": 24500, "port": 24500,
"adminPasscode": "", "adminPasscode": "",
"repo": "michaelmcgary2008/ProdDash", "forge": "http://chronicles-ab.waterschurch.org:3000",
"repo": "michael/ProdDash",
"branch": "main" "branch": "main"
} }
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"id": "prodcom-transcript", "id": "prodcom-transcript",
"name": "ProdCom", "name": "ProdCom",
"version": "1.3.0", "version": "1.3.1",
"proddash": ">=1.1.0", "proddash": ">=1.1.0",
"description": "Live ProdCom transcript", "description": "Live ProdCom transcript",
"permissions": [ "permissions": [
@@ -26,9 +26,10 @@
"type": "endpoint", "type": "endpoint",
"label": "ProdCom server", "label": "ProdCom server",
"default": { "default": {
"host": "10.3.11.152", "host": "",
"port": 24480 "port": 24480
} },
"help": "The ProdCom server at THIS location — every campus has its own. Blank until you set it, so a new install never reads another campus's comms."
}, },
"apiKey": { "apiKey": {
"type": "password", "type": "password",
+3
View File
@@ -269,6 +269,9 @@ module.exports = {
streams, streams,
health() { health() {
if (status.state === 'ok') return { status: 'ok', message: `Live — ${channels.length} channel${channels.length === 1 ? '' : 's'}, ${entries.size} entries` }; if (status.state === 'ok') return { status: 'ok', message: `Live — ${channels.length} channel${channels.length === 1 ? '' : 's'}, ${entries.size} entries` };
// Nothing to connect to is a setting that is missing, not a link that
// is coming up — it reads red, like the other modules say it.
if (!target) return { status: 'error', message: status.message };
return { status: status.state === 'error' ? 'error' : 'connecting', message: status.message }; return { status: status.state === 'error' ? 'error' : 'connecting', message: status.message };
}, },
}; };
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "proddash", "name": "proddash",
"version": "1.4.0", "version": "1.5.0",
"private": true, "private": true,
"description": "Modular production dashboard for Waters Church — zero-dependency Node server + vanilla client", "description": "Modular production dashboard for Waters Church \u2014 zero-dependency Node server + vanilla client",
"license": "UNLICENSED", "license": "UNLICENSED",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
+1 -1
View File
@@ -38,7 +38,7 @@ Edit [config.json](config.json):
| Key | Meaning | | Key | Meaning |
| --- | --- | | --- | --- |
| `prodcomUrl` | Where the ProdCom API lives (default `http://10.3.11.152:24480`) | | `prodcomUrl` | Where the ProdCom API lives — set it per site, e.g. `http://<prodcom-host>:24480` |
| `port` | Port this listener serves on (default `24481`) | | `port` | Port this listener serves on (default `24481`) |
| `apiKey` | Only needed if PSK auth is enabled in ProdCom settings | | `apiKey` | Only needed if PSK auth is enabled in ProdCom settings |
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"prodcomUrl": "http://10.3.11.152:24480", "prodcomUrl": "",
"port": 24481, "port": 24481,
"apiKey": "" "apiKey": ""
} }
+6 -1
View File
@@ -23,10 +23,15 @@ try {
console.warn('config.json not found or invalid, using defaults/env vars'); console.warn('config.json not found or invalid, using defaults/env vars');
} }
const PRODCOM_URL = process.env.PRODCOM_URL || cfg.prodcomUrl || 'http://10.3.11.152:24480'; const PRODCOM_URL = process.env.PRODCOM_URL || cfg.prodcomUrl || '';
const PORT = Number(process.env.PORT || cfg.port || 24481); const PORT = Number(process.env.PORT || cfg.port || 24481);
const API_KEY = process.env.PRODCOM_API_KEY || cfg.apiKey || ''; const API_KEY = process.env.PRODCOM_API_KEY || cfg.apiKey || '';
if (!PRODCOM_URL) {
console.error('No ProdCom API configured. Set prodcomUrl in config.json (or the PRODCOM_URL');
console.error('environment variable) to this site\'s ProdCom, e.g. http://<prodcom-host>:24480');
process.exit(1);
}
const target = new URL(PRODCOM_URL); const target = new URL(PRODCOM_URL);
const PUBLIC_DIR = path.join(__dirname, 'public'); const PUBLIC_DIR = path.join(__dirname, 'public');
+47 -7
View File
@@ -13,6 +13,8 @@
* proddash.json in the data directory (this machine's overrides), or env vars: * proddash.json in the data directory (this machine's overrides), or env vars:
* PORT (default: value in proddash.json, else 24500) * PORT (default: value in proddash.json, else 24500)
* PRODDASH_PASSCODE (overrides adminPasscode in proddash.json) * PRODDASH_PASSCODE (overrides adminPasscode in proddash.json)
* PRODDASH_FORGE (overrides `forge`: "github", or a Gitea/Forgejo base
* URL such as http://10.3.8.90:3000)
* PRODDASH_DATA_DIR (where module config + layouts are kept; see below) * PRODDASH_DATA_DIR (where module config + layouts are kept; see below)
*/ */
'use strict'; 'use strict';
@@ -764,15 +766,53 @@ function parseRepoSetting(value, fallbackBranch) {
} }
const REPO = parseRepoSetting(process.env.PRODDASH_REPO || shellConfig.repo, shellConfig.branch) const REPO = parseRepoSetting(process.env.PRODDASH_REPO || shellConfig.repo, shellConfig.branch)
|| parseRepoSetting('michaelmcgary2008/ProdDash', 'main'); || parseRepoSetting('michael/ProdDash', 'main');
/**
* Which server the code comes from. GitHub unless `forge` names another —
* give it a Gitea (or Forgejo) base URL, "http://10.3.8.90:3000", and updates
* and module installs come from there instead. The two hosts differ only in
* how an archive URL is spelled: the tarball itself is the same, pax header
* and commit sha included, so everything downstream is untouched.
*/
function parseForge(value) {
const s = String(value || '').trim().replace(/\/+$/, '');
if (!s || s.toLowerCase() === 'github') {
return { kind: 'github', base: 'https://github.com', archiveBase: 'https://codeload.github.com' };
}
if (!/^https?:\/\//i.test(s)) return null;
// A GitHub URL still means codeload for archives.
if (/^https?:\/\/(www\.)?github\.com$/i.test(s)) {
return { kind: 'github', base: s, archiveBase: 'https://codeload.github.com' };
}
return { kind: 'gitea', base: s, archiveBase: s };
}
const FORGE = parseForge(process.env.PRODDASH_FORGE || shellConfig.forge) || parseForge('github');
/** Where branch archives come from; tests point this at a local mock. */ /** Where branch archives come from; tests point this at a local mock. */
const ARCHIVE_BASE = String(process.env.PRODDASH_ARCHIVE_BASE || 'https://codeload.github.com').replace(/\/+$/, ''); const ARCHIVE_BASE = String(process.env.PRODDASH_ARCHIVE_BASE || FORGE.archiveBase).replace(/\/+$/, '');
const REMOTE_TTL_MS = 10 * 60 * 1000; const REMOTE_TTL_MS = 10 * 60 * 1000;
const REMOTE_CHECK_MS = 6 * 60 * 60 * 1000; const REMOTE_CHECK_MS = 6 * 60 * 60 * 1000;
const MAX_ARCHIVE_BYTES = 80 * 1024 * 1024; const MAX_ARCHIVE_BYTES = 80 * 1024 * 1024;
function tarballUrl() { function tarballUrl() {
return `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/tar.gz/${encodeURIComponent(REPO.branch)}`; const branch = encodeURIComponent(REPO.branch);
return FORGE.kind === 'gitea'
? `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/archive/${branch}.tar.gz`
: `${ARCHIVE_BASE}/${REPO.owner}/${REPO.repo}/tar.gz/${branch}`;
}
/** The page a human should read to see what changed between two commits. */
function forgeCompareUrl(from, to) {
return `${FORGE.base}/${REPO.owner}/${REPO.repo}/compare/${from}...${to}`;
}
/** The commit list for the branch we follow. */
function forgeCommitsUrl() {
const branch = encodeURIComponent(REPO.branch);
return FORGE.kind === 'gitea'
? `${FORGE.base}/${REPO.owner}/${REPO.repo}/commits/branch/${branch}`
: `${FORGE.base}/${REPO.owner}/${REPO.repo}/commits/${branch}`;
} }
function httpError(status, message) { function httpError(status, message) {
@@ -929,11 +969,11 @@ function shellStatus() {
else if (local.branch !== REPO.branch) blocker = `this checkout is on branch "${local.branch}", not "${REPO.branch}"`; else if (local.branch !== REPO.branch) blocker = `this checkout is on branch "${local.branch}", not "${REPO.branch}"`;
else if (local.dirty) blocker = 'this checkout has local changes — commit or discard them first'; else if (local.dirty) blocker = 'this checkout has local changes — commit or discard them first';
} }
const gh = `https://github.com/${REPO.owner}/${REPO.repo}`;
return { return {
version: SHELL_VERSION, version: SHELL_VERSION,
repo: `${REPO.owner}/${REPO.repo}`, repo: `${REPO.owner}/${REPO.repo}`,
branch: REPO.branch, branch: REPO.branch,
forge: FORGE.base,
local: { isGit: local.isGit, sha: local.sha, branch: local.branch, dirty: local.dirty }, local: { isGit: local.isGit, sha: local.sha, branch: local.branch, dirty: local.dirty },
remote: { version: remote.version, sha: remote.sha, checkedAt: remote.checkedAt, error: remote.error }, remote: { version: remote.version, sha: remote.sha, checkedAt: remote.checkedAt, error: remote.error },
updateAvailable: versionBehind || commitsBehind, updateAvailable: versionBehind || commitsBehind,
@@ -941,8 +981,8 @@ function shellStatus() {
method: local.isGit ? 'git' : 'archive', method: local.isGit ? 'git' : 'archive',
updateBlocker: blocker, updateBlocker: blocker,
changesUrl: local.sha && remote.sha changesUrl: local.sha && remote.sha
? `${gh}/compare/${local.sha.slice(0, 12)}...${remote.sha.slice(0, 12)}` ? forgeCompareUrl(local.sha.slice(0, 12), remote.sha.slice(0, 12))
: `${gh}/commits/${REPO.branch}`, : forgeCommitsUrl(),
}; };
} }
@@ -1693,7 +1733,7 @@ server.on('listening', () => {
? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)') + (manifests.get(id).incompatible ? ' (needs newer ProdDash)' : '')).join(', ') ? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)') + (manifests.get(id).incompatible ? ' (needs newer ProdDash)' : '')).join(', ')
: ' Modules : none installed'); : ' Modules : none installed');
console.log(' Settings : ' + DATA_DIR + (DATA_DIR === REPO_CONFIG_DIR ? ' (inside the app folder)' : '')); console.log(' Settings : ' + DATA_DIR + (DATA_DIR === REPO_CONFIG_DIR ? ' (inside the app folder)' : ''));
console.log(' Repo : ' + REPO.owner + '/' + REPO.repo + '@' + REPO.branch); console.log(' Repo : ' + REPO.owner + '/' + REPO.repo + '@' + REPO.branch + ' (' + FORGE.base + ')');
}); });
// A restart hands the port over from the exiting ProdDash: wait for it. // A restart hands the port over from the exiting ProdDash: wait for it.