From 172e4a93583274149b9d57576483ccea18b75921 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 16 Jul 2026 07:55:52 -0400 Subject: [PATCH] Add version numbering (v1.0.0) - package.json: bump to 1.0.0 - server/config.js: export APP_VERSION (from package.json) and GIT_COMMIT (from COMMIT_SHA env var set by CI, or git rev-parse --short HEAD fallback) - GET /api/version: new public endpoint returning { version, commit } - GET /api/admin-auth/status: includes version and commit in response - AdminPage sidebar: displays version string (e.g. "v1.0.0 (1d43875)") below the Log Out button, styled as muted metadata text Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- server/config.js | 28 ++++++++++++++++++++++++++++ server/routes/admin-auth.js | 3 +++ server/routes/public.js | 6 ++++++ src/AdminPage.tsx | 9 ++++++++- src/App.css | 7 +++++++ 6 files changed, 53 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2594b4f..4655d3d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/config.js b/server/config.js index 23fcb75..4dae9e5 100644 --- a/server/config.js +++ b/server/config.js @@ -1,9 +1,37 @@ import path from 'node:path' +import { readFileSync } from 'node:fs' +import { execSync } from 'node:child_process' import { validateAdminPasswordSetup } from './auth.js' import { ROOT_DIR, DATA_DIR } from './paths.js' export { ROOT_DIR, DATA_DIR } +function readPackageVersion() { + try { + const pkg = JSON.parse(readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8')) + return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : 'unknown' + } catch { + return 'unknown' + } +} + +function readGitCommit() { + // CI injects the full SHA as COMMIT_SHA; fall back to running git locally. + if (typeof process.env.COMMIT_SHA === 'string' && process.env.COMMIT_SHA.trim()) { + return process.env.COMMIT_SHA.trim().slice(0, 7) + } + try { + return execSync('git rev-parse --short HEAD', { cwd: ROOT_DIR, timeout: 2000 }).toString().trim() + } catch { + return 'unknown' + } +} + +// APP_VERSION is sourced from package.json. +// GIT_COMMIT is the 7-char short SHA, from COMMIT_SHA env var (set by CI) or git at startup. +export const APP_VERSION = readPackageVersion() +export const GIT_COMMIT = readGitCommit() + export const DATA_FILE = path.join(DATA_DIR, 'admin-content.json') export const DRAFT_DATA_FILE = path.join(DATA_DIR, 'admin-content-draft.json') export const HIT_STATS_FILE = path.join(DATA_DIR, 'hit-stats.json') diff --git a/server/routes/admin-auth.js b/server/routes/admin-auth.js index 08baaba..63dd6af 100644 --- a/server/routes/admin-auth.js +++ b/server/routes/admin-auth.js @@ -1,6 +1,7 @@ import rateLimit from 'express-rate-limit' import qrcode from 'qrcode' import { parseCookies } from '../helpers.js' +import { APP_VERSION, GIT_COMMIT } from '../config.js' import { isAdminPasswordConfigured, isValidAdminSession, @@ -40,6 +41,8 @@ export function register(app) { authenticated: isValidAdminSession(req), configured: isAdminPasswordConfigured(), totpEnabled: await isTotpEnabled(), + version: APP_VERSION, + commit: GIT_COMMIT, }) }) diff --git a/server/routes/public.js b/server/routes/public.js index 54bef73..13007e6 100644 --- a/server/routes/public.js +++ b/server/routes/public.js @@ -9,6 +9,8 @@ import { UPLOADS_DIR, INDEX_FILE, DEFAULT_SEO, + APP_VERSION, + GIT_COMMIT, } from '../config.js' import { state } from '../state.js' import { loadSiteContentFile } from '../data.js' @@ -18,6 +20,10 @@ import { sanitizeRedirectRules } from '../study-helpers.js' export function register(app) { const SALVATION_INDEX_FILE = path.join(DIST_DIR, 'salvation', 'index.html') + app.get('/api/version', (_req, res) => { + res.json({ version: APP_VERSION, commit: GIT_COMMIT }) + }) + app.get('/robots.txt', async (_req, res) => { let content = state.cachedSiteContent if (!content) { diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 7e3f4c1..3a01812 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1070,6 +1070,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { // TOTP management state const [totpEnabled, setTotpEnabled] = useState(null) + const [appVersion, setAppVersion] = useState('') const [totpSetupQr, setTotpSetupQr] = useState(null) const [totpSetupSecret, setTotpSetupSecret] = useState(null) const [totpConfirmCode, setTotpConfirmCode] = useState('') @@ -1291,7 +1292,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { useEffect(() => { fetch('/api/admin-auth/status') .then(r => r.ok ? r.json() : Promise.reject()) - .then(data => setTotpEnabled(!!(data as { totpEnabled?: boolean }).totpEnabled)) + .then((data: { totpEnabled?: boolean; version?: string; commit?: string }) => { + setTotpEnabled(!!data.totpEnabled) + if (data.version) setAppVersion(data.commit ? `v${data.version} (${data.commit})` : `v${data.version}`) + }) .catch(() => {}) }, []) @@ -3102,6 +3106,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { Log Out + {appVersion && ( +
{appVersion}
+ )}
diff --git a/src/App.css b/src/App.css index c582cb9..43f3427 100644 --- a/src/App.css +++ b/src/App.css @@ -7203,6 +7203,13 @@ color: var(--brand-warm-white); } + .admin-sidebar-version { + padding: 0.5rem 1.1rem 0.75rem; + font-size: 0.72rem; + color: rgba(240,234,216,0.28); + letter-spacing: 0.04em; + } + .admin-nav-search { display: grid; gap: 0.4rem;