diff --git a/README.md b/README.md index a76cf4e..56bbf60 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ 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. | | `adminPin` | The admin PIN, as a scrypt hash. **Set it in the admin page's Admin PIN section**, not by hand — 6 digits, and only the hash is stored. Absent means admin is open to anyone on the network. | | `adminPasscode` | An older free-text passcode. Still honoured, so an existing install keeps working; new installs should use `adminPin`. | +| `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). | **Locked out?** Delete `adminPin` from the data directory's `proddash.json` @@ -111,8 +112,21 @@ Only `/admin` and the admin API are behind the PIN. Dashboards stay open, so a wall display needs no PIN, and it is unrelated to a module's own PIN (Slack's 4–8 digit send PIN, say), which guards that module's actions only. +**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 -`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 in-repo location). If the default directory can't be created, the server says so and falls back to the app's `config/` folder. diff --git a/config/proddash.json b/config/proddash.json index d43bd7c..4a1d7fe 100644 --- a/config/proddash.json +++ b/config/proddash.json @@ -1,6 +1,7 @@ { "port": 24500, "adminPasscode": "", + "forge": "github", "repo": "michaelmcgary2008/ProdDash", "branch": "main" } diff --git a/package.json b/package.json index ae778d6..88de406 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "proddash", - "version": "1.4.0", + "version": "1.5.0", "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", "engines": { "node": ">=18" diff --git a/server.js b/server.js index d937b24..aeb13c1 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,8 @@ * PORT (default: value in proddash.json, else 24500) * PRODDASH_PASSCODE (a passcode that overrides the stored admin PIN — * also the way back in if the PIN is ever forgotten) + * 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) */ 'use strict'; @@ -766,14 +768,52 @@ function parseRepoSetting(value, fallbackBranch) { const REPO = parseRepoSetting(process.env.PRODDASH_REPO || shellConfig.repo, shellConfig.branch) || 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. */ -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_CHECK_MS = 6 * 60 * 60 * 1000; const MAX_ARCHIVE_BYTES = 80 * 1024 * 1024; 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) { @@ -930,11 +970,11 @@ function shellStatus() { 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'; } - const gh = `https://github.com/${REPO.owner}/${REPO.repo}`; return { version: SHELL_VERSION, repo: `${REPO.owner}/${REPO.repo}`, branch: REPO.branch, + forge: FORGE.base, 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 }, updateAvailable: versionBehind || commitsBehind, @@ -942,8 +982,8 @@ function shellStatus() { method: local.isGit ? 'git' : 'archive', updateBlocker: blocker, changesUrl: local.sha && remote.sha - ? `${gh}/compare/${local.sha.slice(0, 12)}...${remote.sha.slice(0, 12)}` - : `${gh}/commits/${REPO.branch}`, + ? forgeCompareUrl(local.sha.slice(0, 12), remote.sha.slice(0, 12)) + : forgeCommitsUrl(), }; } @@ -1838,7 +1878,7 @@ server.on('listening', () => { ? ' Modules : ' + ids.map((id) => id + (isEnabled(id) ? '' : ' (disabled)') + (manifests.get(id).incompatible ? ' (needs newer ProdDash)' : '')).join(', ') : ' Modules : none installed'); 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.