Compare commits

..
Author SHA1 Message Date
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
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
4 changed files with 64 additions and 9 deletions
+15 -1
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.
+1
View File
@@ -1,6 +1,7 @@
{ {
"port": 24500, "port": 24500,
"adminPasscode": "", "adminPasscode": "",
"forge": "github",
"repo": "michaelmcgary2008/ProdDash", "repo": "michaelmcgary2008/ProdDash",
"branch": "main" "branch": "main"
} }
+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"
+46 -6
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';
@@ -765,14 +767,52 @@ 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('michaelmcgary2008/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.