Simplify Dashboard Uptime onto the shared 7-second hero poll

This commit is contained in:
marvin
2026-09-19 20:05:14 -04:00
parent a413f53577
commit cf313f7abf
7 changed files with 30 additions and 18 deletions
+8 -8
View File
@@ -147,7 +147,6 @@ function probeCopy(service, ready, error, unconfigured = "Not configured") {
function renderDashboardJobs(system) { const columns = document.querySelector("#dashboard-view .dashboard-columns"), health = columns?.firstElementChild; if (!columns) return; let panel = document.querySelector("#dashboard-jobs"); if (!panel) { panel = document.createElement("section"); panel.id = "dashboard-jobs"; panel.className = "dashboard-panel dashboard-jobs-panel"; columns.insertBefore(panel, columns.children[1] || null); } if (health && health.parentElement === columns) columns.parentElement.insertBefore(health, columns); panel.innerHTML = `<div class="panel-heading"><div><p class="eyebrow">Operations</p><h2>Scheduled jobs</h2></div></div><div class="dashboard-jobs-list">${(system.jobs || []).map(job => `<div class="dashboard-list-item"><span class="status-dot ${job.enabled ? "running" : "idle"}"></span><span><strong>${escapeHtml(job.name)}</strong><small>${job.enabled ? `Active · ${escapeHtml(job.schedule)}` : "Disabled"}</small></span></div>`).join("")}</div>`; }
function updateDashboardUptime(seconds) { if (Number.isFinite(seconds)) window.__dashboardStartedAt = Date.now() - seconds * 1000; const started = window.__dashboardStartedAt; const target = document.querySelector("#system-uptime"); if (!target || started === undefined) return; const elapsed = Math.max(0, Math.floor((Date.now() - started) / 1000)); target.textContent = formatDuration(elapsed); }
// Dashboard tiles share one baseline accent (green) and switch to the existing
// --warning / --danger tokens when the thing they count is actually in trouble --
// the same mechanism the "Needs attention" chip already used.
@@ -214,7 +213,6 @@ function renderDashboard() {
$("#upstream-health-dot").className = `status-dot ${!upstreams.total ? "inactive" : upstreams.unhealthy > 0 ? "error" : "running"}`;
$("#upstream-health-copy").textContent = !upstreams.total ? "No proxy hosts configured" : `${upstreams.healthy} of ${upstreams.total} healthy`;
$("#health-checked").innerHTML = `<span class="live-dot" id="health-live-dot"></span>Last checked ${formatTime(data.checkedAt)}`;
updateDashboardUptime(data.system.uptimeSeconds);
// Memory/Data/Storage/Version/Database/Public IP moved to the Administration > System tab's
// Version panel -- the Dashboard's own Runtime/System panel is now the shared hero component
// (see renderHeroPanel/refreshDashboardHero), which reads real container-scoped CPU/memory/
@@ -228,7 +226,6 @@ function renderDashboard() {
$("#activity-list").innerHTML = data.activity.length ? data.activity.slice(0, 5).map(item => `<div class="activity-tile"><span class="activity-mark ${item.status === "error" ? "bad" : item.status === "warning" ? "warn" : ""}">${item.status === "error" || item.status === "warning" ? "!" : "✓"}</span><span class="activity-copy"><strong>${escapeHtml(item.message)}</strong><small title="${escapeHtml(formatTime(item.at))}">${escapeHtml(formatRelativeTime(item.at))}</small></span></div>`).join("") : '<p class="quiet-state">No recent activity.</p>';
}
setInterval(() => { if (!document.querySelector("#dashboard-view.hidden")) updateDashboardUptime(); }, 1000);
// --- Card rendering helpers (icons, permissions) -----------------------------------
@@ -550,17 +547,20 @@ async function refreshDashboard() {
try { state.dashboard = await api("/api/dashboard"); renderDashboard(); }
finally { button.disabled = false; button.classList.remove("spinning"); }
}
// Populates the Dashboard's hero panel (CPU/memory/swap/disk/network -- Throughput is skipped
// here since the Dashboard already has its own live-requests chip, and Uptime is handled by the
// existing updateDashboardUptime() ticker rather than this endpoint) directly from
// Populates the Dashboard's hero panel (CPU/memory/swap/disk/network/uptime) directly from
// /api/system/health, the same call and the same renderHeroPanel() the Administration > System
// tab's hero uses, so the two can never show different numbers for the same live stat again.
// Uptime uses sixthSlot: "uptime" here (the System tab uses the default "throughput" slot instead,
// since the Dashboard already has its own live-requests chip elsewhere -- see below). There's no
// separate ticker or anchor for Uptime anymore: formatDuration() only ever shows minute-level
// granularity, so refreshing it on this same 7s poll as everything else is all the precision the
// display needs, and it removes a whole class of ticker/anchor race-condition bugs for free.
async function refreshDashboardHero() {
try {
const health = await api("/api/system/health");
window.renderHeroPanel?.("dashboard-hero", health, { includeThroughput: false });
window.renderHeroPanel?.("dashboard-hero", health, { sixthSlot: "uptime" });
// /api/system/health already computes throughput.liveRequests (the hero just doesn't display
// it here, since the Dashboard shows it in its own chip instead -- see includeThroughput above).
// it here, since the Dashboard shows it in its own chip instead -- see sixthSlot above).
// Reuse that number to keep the chip on the same 7s cadence as the hero, instead of leaving it
// on the separate 30s refreshDashboard() timer, which was the actual bug being reported here.
const throughputTotal = $("#dash-throughput-total"); if (throughputTotal && health.throughput) throughputTotal.textContent = health.throughput.liveRequests ?? 0;
+10 -5
View File
@@ -595,8 +595,12 @@ function setHeroStat(prefix, key, { value, percent, detail, tone } = {}) {
// isn't available (e.g. no cgroup v2, no readable network interfaces, swap disabled on the host).
// Throughput is System-tab-only -- the Dashboard already shows live requests/min in its own chip,
// so `includeThroughput: false` there skips it rather than showing the same number twice.
function renderHeroPanel(prefix, health, { includeThroughput } = { includeThroughput: true }) {
const keys = includeThroughput ? ["cpu", "memory", "swap", "disk", "network", "throughput"] : ["cpu", "memory", "swap", "disk", "network"];
function renderHeroPanel(prefix, health, { sixthSlot = "throughput" } = {}) {
// sixthSlot picks what the panel's sixth stat is: "throughput" (Administration > System, since
// that page has no other requests/min display) or "uptime" (the Dashboard, which already has
// its own Throughput chip elsewhere -- showing it twice added nothing). Both come straight off
// the same /api/system/health poll as everything else here, no separate ticker or anchor.
const keys = ["cpu", "memory", "swap", "disk", "network", sixthSlot];
if (!document.querySelector(`#${prefix}-${keys[0]}-value`)) return;
if (!health) { keys.forEach(key => setHeroStat(prefix, key, { value: "\u2014", detail: "Unavailable" })); return; }
const tone = percent => percent >= 90 ? "critical" : percent >= 75 ? "warning" : "";
@@ -622,7 +626,8 @@ function renderHeroPanel(prefix, health, { includeThroughput } = { includeThroug
else setHeroStat(prefix, "disk", { value: "\u2014", detail: "Disk stats unavailable" });
if (health.network) setHeroStat(prefix, "network", { value: formatRate(health.network.rxBytesPerSec + health.network.txBytesPerSec), percent: 0, detail: `\u2193 ${formatRate(health.network.rxBytesPerSec)} \u00b7 \u2191 ${formatRate(health.network.txBytesPerSec)}` });
else setHeroStat(prefix, "network", { value: "\u2014", detail: "Sampling\u2026" });
if (includeThroughput) setHeroStat(prefix, "throughput", { value: String(health.throughput?.liveRequests ?? 0), percent: 0, detail: "requests in the last minute" });
if (sixthSlot === "throughput") setHeroStat(prefix, "throughput", { value: String(health.throughput?.liveRequests ?? 0), percent: 0, detail: "requests in the last minute" });
else if (sixthSlot === "uptime") setHeroStat(prefix, "uptime", Number.isFinite(health.uptimeSeconds) ? { value: formatDuration(health.uptimeSeconds), detail: "Since last restart" } : { value: "\u2014", detail: "Unavailable" });
}
// --- System tab: environment/integration status, storage, scheduled jobs, sync, restart --------
function renderSystemPanel() {
@@ -686,7 +691,7 @@ 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, { includeThroughput: true })).catch(() => {});
api("/api/system/health").then(health => renderHeroPanel("system-hero", health)).catch(() => {});
}, 7000);
}
renderSystemStatus(panel);
@@ -728,7 +733,7 @@ async function renderSystemStatus(panel) {
api("/api/system/restart-policy"),
api("/api/system/health").catch(() => null),
]);
renderHeroPanel("system-hero", health, { includeThroughput: true });
renderHeroPanel("system-hero", 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." },
+3 -3
View File
@@ -8,7 +8,7 @@
<title>Site Gateway</title>
<meta name="description" content="Host sites, proxy services, and manage HTTPS from one simple dashboard.">
<link rel="icon" type="image/png" href="/site-gateway-icon-approved.png">
<link rel="stylesheet" href="/styles.css?v=0.16.38">
<link rel="stylesheet" href="/styles.css?v=0.16.39">
</head>
<!-- ================================================================
@@ -138,7 +138,7 @@
<div class="system-hero-stat" data-hero-stat="swap"><span class="system-hero-label">Swap</span><strong class="system-hero-value" id="dashboard-hero-swap-value"></strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-swap-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-swap-detail"></small></div>
<div class="system-hero-stat" data-hero-stat="disk"><span class="system-hero-label">Disk</span><strong class="system-hero-value" id="dashboard-hero-disk-value"></strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-disk-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-disk-detail"></small></div>
<div class="system-hero-stat" data-hero-stat="network"><span class="system-hero-label">Network</span><strong class="system-hero-value" id="dashboard-hero-network-value"></strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-network-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-network-detail"></small></div>
<div class="system-hero-stat" data-hero-stat="uptime"><span class="system-hero-label">Uptime</span><strong class="system-hero-value" id="system-uptime"></strong><div class="system-hero-bar"></div><small class="system-hero-detail">Since last restart</small></div>
<div class="system-hero-stat" data-hero-stat="uptime"><span class="system-hero-label">Uptime</span><strong class="system-hero-value" id="dashboard-hero-uptime-value"></strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-uptime-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-uptime-detail"></small></div>
</div>
</section>
</div>
@@ -432,6 +432,6 @@
<div id="toast" class="toast" role="status"></div>
<div id="update-banner" class="update-banner hidden" role="status"><span>A new version of Site Gateway is available.</span><div class="update-banner-actions"><button id="update-banner-refresh" class="button primary">Refresh</button><button id="update-banner-dismiss" class="text-button">Dismiss</button></div></div>
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
<script src="/app.js?v=0.16.38" defer></script><script src="/features.js?v=0.16.38" defer></script><script src="/select-enhance.js?v=0.16.38" defer></script>
<script src="/app.js?v=0.16.39" defer></script><script src="/features.js?v=0.16.39" defer></script><script src="/select-enhance.js?v=0.16.39" defer></script>
</body>
</html>