From 8e6a5f91962bd0ad3f033ac3080f23d1823901eb Mon Sep 17 00:00:00 2001 From: marvin Date: Sat, 19 Sep 2026 20:44:08 -0400 Subject: [PATCH] Sync Version panel's Uptime to the shared 7-second hero poll --- README.md | 2 +- ROADMAP.md | 3 +++ package.json | 2 +- src/public/features.js | 24 ++++++++++++++++++------ src/public/index.html | 6 +++--- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5fe32e8..f0766c7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Docker Architectures Caddy - Version + Version

Why Site Gateway · diff --git a/ROADMAP.md b/ROADMAP.md index 798df58..5a86f27 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -205,3 +205,6 @@ Roughly in priority order: `v0.16.39` retires the Dashboard Uptime tile's separate 1-second client-side ticker, the same mechanism behind the v0.16.38 "resets to 0m on page load" bug, in favor of treating Uptime as just another field on the shared 7-second `/api/system/health` poll that already drives CPU, memory, swap, disk, and network on both the Dashboard and the Administration > System tab's hero panel. Revisiting the ticker after fixing its race condition, it turned out to be solving a precision problem the display doesn't actually have: `formatDuration()` only ever renders minute-level granularity ("2h 59m"), never seconds, so a per-second tick never changed what was on screen between one 7-second poll and the next. `systemHealthSnapshot()` now includes `uptimeSeconds` (from `process.uptime()`, the same source `dashboardSnapshot()` already used), and `renderHeroPanel()`'s former `includeThroughput` boolean became a `sixthSlot` option ("throughput" for the System tab, which has no other requests/min display, or "uptime" for the Dashboard, which already shows Throughput in its own chip) so the sixth hero slot can be either stat without a special case. The client ticker, its anchor state, and the whole race-condition class it enabled are gone: one poll, one code path, six stats, no anchor to get out of sync. + + +`v0.16.40` moves the Administration → System tab's Version panel Uptime figure onto the same 7-second `/api/system/health` poll driving the hero panel above it, instead of only updating when the page's slower, general dashboard snapshot refetches (on load, or every ~30 seconds while the Dashboard view specifically is active). Found while reviewing the v0.16.39 change: the hero's own Uptime slot was now current to the second, but the separate Version-panel line right below it — the same number, shown twice on the same page — could still be stale by up to half a minute or more. `renderSystemStatus()`'s Uptime is now wrapped in its own `#system-version-uptime` span and updated by a small `updateSystemVersionUptime(health)` helper, called both from the initial render and from the same 7-second timer that already refreshes the hero, so the two Uptime figures on that page can no longer drift apart. diff --git a/package.json b/package.json index 9ef6e4b..78dc748 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "site-gateway", - "version": "0.16.39", + "version": "0.16.40", "private": true, "description": "Site Gateway: simple self-hosted website publishing, reverse proxying, and automatic HTTPS.", "type": "module", diff --git a/src/public/features.js b/src/public/features.js index 5cc20e3..9440cd9 100644 --- a/src/public/features.js +++ b/src/public/features.js @@ -691,11 +691,19 @@ function renderSystemPanel() { if (!state.systemHealthTimer) state.systemHealthTimer = setInterval(() => { const systemPanel = document.querySelector('[data-admin-panel="system"]'); if (state.view !== "administration" || !systemPanel || systemPanel.classList.contains("hidden")) return; - api("/api/system/health").then(health => renderHeroPanel("system-hero", health)).catch(() => {}); + api("/api/system/health").then(health => { renderHeroPanel("system-hero", health); updateSystemVersionUptime(health); }).catch(() => {}); }, 7000); } renderSystemStatus(panel); } +// Updates just the Version panel's Uptime figure from a fresh /api/system/health payload -- +// kept separate from the rest of renderSystemStatus() so it can be called on the fast 7s hero +// poll without re-rendering (or re-fetching) everything else in that panel. +function updateSystemVersionUptime(health) { + const el = document.querySelector("#system-version-uptime"); + if (!el) return; + el.textContent = Number.isFinite(health?.uptimeSeconds) ? formatDuration(health.uptimeSeconds) : "Unavailable"; +} async function renderSystemStatus(panel) { panel = panel || document.querySelector('[data-admin-panel="system"]'); if (!panel) return; @@ -715,16 +723,19 @@ async function renderSystemStatus(panel) { } if (syncStatus) { const drift = (state.dashboard?.attention || []).some(item => item.kind === "drift"); syncStatus.textContent = drift ? "Configuration drift detected \u2014 the running gateway no longer matches the last known-good configuration." : `Gateway configuration is in sync. Last reload: ${state.dashboard?.gateway?.lastReload ? formatTime(state.dashboard.gateway.lastReload) : "unknown"}.`; syncStatus.className = drift ? "muted status-warning" : "muted"; } if (version) { - // Uptime, Caddy version, Database status, and Public IP used to live on the Dashboard's - // Runtime/System panel -- that panel is now the shared hero component (CPU/memory/swap/disk/ - // network), so this operational metadata moved here instead, reusing the same system.* fields - // from the global dashboard snapshot rather than a separate fetch. + // Caddy version, Database status, and Public IP used to live on the Dashboard's Runtime/System + // panel -- that panel is now the shared hero component (CPU/memory/swap/disk/network), so this + // operational metadata moved here instead, reusing the same system.* fields from the global + // dashboard snapshot rather than a separate fetch. Uptime is the one exception: it's wrapped in + // its own #system-version-uptime span and kept current by updateSystemVersionUptime(), called + // from the same 7-second /api/system/health poll that drives the hero panel above, instead of + // only refreshing on the slower ~30s dashboard snapshot like the rest of this block. const sys = state.dashboard?.system || {}; const uptime = Number.isFinite(sys.uptimeSeconds) ? formatDuration(sys.uptimeSeconds) : "Unavailable"; const database = sys.databaseEngine ? `${extendedEscape(sys.databaseEngine)} \u00b7 ${extendedEscape(sys.databaseStatus || "unknown")} \u00b7 ${formatBytes(sys.databaseBytes)}` : "Unavailable"; const publicIp = sys.publicIp || (sys.publicIpError ? "Unavailable" : "Checking\u2026"); const publicIpDetail = sys.publicIpError ? `check failed \u00b7 ${extendedEscape(sys.publicIpError)}` : sys.publicIpCheckedAt ? `checked ${extendedEscape(formatTime(sys.publicIpCheckedAt))}` : "not yet checked"; - version.innerHTML = `Site Gateway v${extendedEscape(state.config?.version || "unknown")} \u00b7 Caddy ${extendedEscape(sys.caddyVersion || "unknown")}
Uptime: ${uptime} \u00b7 Database: ${database} \u00b7 Public IP: ${extendedEscape(publicIp)} (${publicIpDetail})
Access this dashboard at: ${extendedEscape(location.origin)}
Data directory: ${extendedEscape(state.config?.storage?.databasePath ? state.config.storage.databasePath.replace(/\/database\/.*/, "") : "/data")} · Site ports: ${extendedEscape(String(state.config?.minPort ?? ""))}\u2013${extendedEscape(String(state.config?.maxPort ?? ""))}`; + version.innerHTML = `Site Gateway v${extendedEscape(state.config?.version || "unknown")} \u00b7 Caddy ${extendedEscape(sys.caddyVersion || "unknown")}
Uptime: ${uptime} \u00b7 Database: ${database} \u00b7 Public IP: ${extendedEscape(publicIp)} (${publicIpDetail})
Access this dashboard at: ${extendedEscape(location.origin)}
Data directory: ${extendedEscape(state.config?.storage?.databasePath ? state.config.storage.databasePath.replace(/\/database\/.*/, "") : "/data")} · Site ports: ${extendedEscape(String(state.config?.minPort ?? ""))}\u2013${extendedEscape(String(state.config?.maxPort ?? ""))}`; } try { const [sec, store, policy, health] = await Promise.all([ @@ -734,6 +745,7 @@ async function renderSystemStatus(panel) { api("/api/system/health").catch(() => null), ]); renderHeroPanel("system-hero", health); + if (health) updateSystemVersionUptime(health); if (security) security.innerHTML = [ { ok: !sec.adminPasswordIsDefault, label: "ADMIN_PASSWORD", detail: sec.adminPasswordIsDefault ? "Still using the built-in default \u2014 set this before exposing the dashboard." : "Configured." }, { ok: !sec.sessionSecretIsDefault, label: "SESSION_SECRET", detail: sec.sessionSecretIsDefault ? "Not set \u2014 sessions are keyed off the admin credentials instead of an independent secret." : "Configured." }, diff --git a/src/public/index.html b/src/public/index.html index 446300b..71fe66a 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -8,7 +8,7 @@ Site Gateway - +

- +