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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,9 +1,37 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { execSync } from 'node:child_process'
|
||||||
import { validateAdminPasswordSetup } from './auth.js'
|
import { validateAdminPasswordSetup } from './auth.js'
|
||||||
import { ROOT_DIR, DATA_DIR } from './paths.js'
|
import { ROOT_DIR, DATA_DIR } from './paths.js'
|
||||||
|
|
||||||
export { ROOT_DIR, DATA_DIR }
|
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 DATA_FILE = path.join(DATA_DIR, 'admin-content.json')
|
||||||
export const DRAFT_DATA_FILE = path.join(DATA_DIR, 'admin-content-draft.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')
|
export const HIT_STATS_FILE = path.join(DATA_DIR, 'hit-stats.json')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import rateLimit from 'express-rate-limit'
|
import rateLimit from 'express-rate-limit'
|
||||||
import qrcode from 'qrcode'
|
import qrcode from 'qrcode'
|
||||||
import { parseCookies } from '../helpers.js'
|
import { parseCookies } from '../helpers.js'
|
||||||
|
import { APP_VERSION, GIT_COMMIT } from '../config.js'
|
||||||
import {
|
import {
|
||||||
isAdminPasswordConfigured,
|
isAdminPasswordConfigured,
|
||||||
isValidAdminSession,
|
isValidAdminSession,
|
||||||
@@ -40,6 +41,8 @@ export function register(app) {
|
|||||||
authenticated: isValidAdminSession(req),
|
authenticated: isValidAdminSession(req),
|
||||||
configured: isAdminPasswordConfigured(),
|
configured: isAdminPasswordConfigured(),
|
||||||
totpEnabled: await isTotpEnabled(),
|
totpEnabled: await isTotpEnabled(),
|
||||||
|
version: APP_VERSION,
|
||||||
|
commit: GIT_COMMIT,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
UPLOADS_DIR,
|
UPLOADS_DIR,
|
||||||
INDEX_FILE,
|
INDEX_FILE,
|
||||||
DEFAULT_SEO,
|
DEFAULT_SEO,
|
||||||
|
APP_VERSION,
|
||||||
|
GIT_COMMIT,
|
||||||
} from '../config.js'
|
} from '../config.js'
|
||||||
import { state } from '../state.js'
|
import { state } from '../state.js'
|
||||||
import { loadSiteContentFile } from '../data.js'
|
import { loadSiteContentFile } from '../data.js'
|
||||||
@@ -18,6 +20,10 @@ import { sanitizeRedirectRules } from '../study-helpers.js'
|
|||||||
export function register(app) {
|
export function register(app) {
|
||||||
const SALVATION_INDEX_FILE = path.join(DIST_DIR, 'salvation', 'index.html')
|
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) => {
|
app.get('/robots.txt', async (_req, res) => {
|
||||||
let content = state.cachedSiteContent
|
let content = state.cachedSiteContent
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
|||||||
+8
-1
@@ -1070,6 +1070,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
|
|
||||||
// TOTP management state
|
// TOTP management state
|
||||||
const [totpEnabled, setTotpEnabled] = useState<boolean | null>(null)
|
const [totpEnabled, setTotpEnabled] = useState<boolean | null>(null)
|
||||||
|
const [appVersion, setAppVersion] = useState<string>('')
|
||||||
const [totpSetupQr, setTotpSetupQr] = useState<string | null>(null)
|
const [totpSetupQr, setTotpSetupQr] = useState<string | null>(null)
|
||||||
const [totpSetupSecret, setTotpSetupSecret] = useState<string | null>(null)
|
const [totpSetupSecret, setTotpSetupSecret] = useState<string | null>(null)
|
||||||
const [totpConfirmCode, setTotpConfirmCode] = useState('')
|
const [totpConfirmCode, setTotpConfirmCode] = useState('')
|
||||||
@@ -1291,7 +1292,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/admin-auth/status')
|
fetch('/api/admin-auth/status')
|
||||||
.then(r => r.ok ? r.json() : Promise.reject())
|
.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(() => {})
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -3102,6 +3106,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
Log Out
|
Log Out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{appVersion && (
|
||||||
|
<div className="admin-sidebar-version">{appVersion}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="admin-nav-search">
|
<div className="admin-nav-search">
|
||||||
<label htmlFor="admin-nav-search-input">Find a section</label>
|
<label htmlFor="admin-nav-search-input">Find a section</label>
|
||||||
|
|||||||
@@ -7203,6 +7203,13 @@
|
|||||||
color: var(--brand-warm-white);
|
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 {
|
.admin-nav-search {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user