From 67bcd3640a1d969edf5e399e467d03cda9d130ac Mon Sep 17 00:00:00 2001 From: Waters Church Date: Wed, 16 Sep 2026 18:23:20 -0400 Subject: [PATCH] Updates can come from your own git server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.tar.gz rather than /tar.gz/ — 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 --- README.md | 16 +++++++++++++- config/proddash.json | 1 + server.js | 52 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8856226..8a61437 100644 --- a/README.md +++ b/README.md @@ -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. | | `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). | +**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/server.js b/server.js index 4c77a57..dd91463 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,8 @@ * proddash.json in the data directory (this machine's overrides), or env vars: * PORT (default: value in proddash.json, else 24500) * 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) */ 'use strict'; @@ -765,14 +767,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) { @@ -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.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, @@ -941,8 +981,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(), }; } @@ -1693,7 +1733,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.