dashboard: live cache-age indicator

The web server's dashboardStats now reports how stale its shared cache
snapshot is — data.cache = { enabled, ttlMs, ageMs }, using the age of the
oldest dataset feeding the view (OLT-STATE, plus ONU-STATE under detailed
scan). cache.js gains ageOf(key).

The dashboard renders a live-ticking badge from it (renderCacheBadge):
  - within TTL  -> "◷ cached Ns ago" (info)
  - past TTL    -> same, warn-coloured (next load will refetch)
  - no cache    -> "● live" (Electron app, or PFW_CACHE_TTL_SECONDS=0)
The Dashboard Refresh button now sends fresh:true to bypass the cache and
reset the age; the detailed-stats toggle and auto-loads use the cache.

Shared renderer change, so the Electron app shows "live" (it sends no cache
field). Verified offscreen over HTTP: cached / stale / live states render
with the right text + colour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-23 14:25:46 +02:00
parent 6ee1d37a79
commit c2206ad5a7
6 changed files with 84 additions and 5 deletions

View file

@ -1551,8 +1551,10 @@ $('#btn-olt-execute').addEventListener('click', async () => {
// -------- Dashboard: network telemetry --------
$('#dash-refresh').addEventListener('click', loadDashboard);
$('#dash-extra').addEventListener('change', loadDashboard);
// Refresh forces a fresh server fetch (bypasses the cache); the toggle and
// auto-loads use the cache. Wrap so the DOM event isn't passed as `force`.
$('#dash-refresh').addEventListener('click', () => loadDashboard(true));
$('#dash-extra').addEventListener('change', () => loadDashboard());
// Bit-rate formatter, ported from the iOS app's Format.bps.
function fmtBps(v) {
@ -1578,6 +1580,44 @@ function fmtDbm(n) {
return typeof n === 'number' ? n.toFixed(1) : '—';
}
function fmtAge(ms) {
const s = Math.max(0, Math.round(ms / 1000));
if (s < 90) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ${s % 60}s`;
return `${Math.floor(m / 60)}h ${m % 60}m`;
}
// Cache-age badge. The web server reports how old its shared cache snapshot
// is (`d.cache`); the Electron app sends no cache info → "live". The badge
// ticks every second so the operator can see the data going stale.
let dashCacheTimer = null;
function renderCacheBadge(cache) {
const el = $('#dash-cache');
if (!el) return;
if (dashCacheTimer) { clearInterval(dashCacheTimer); dashCacheTimer = null; }
if (!cache || !cache.enabled) {
el.className = 'dash-cache status-ok';
el.textContent = '● live';
el.title = 'Fetched directly from MCMS (no server-side cache).';
return;
}
const ttl = cache.ttlMs || 0;
const baseAge = cache.ageMs || 0;
const receivedAt = Date.now();
const paint = () => {
const age = baseAge + (Date.now() - receivedAt);
const stale = ttl > 0 && age > ttl;
el.className = 'dash-cache ' + (stale ? 'status-warn' : 'status-info');
el.textContent = `◷ cached ${fmtAge(age)} ago`;
el.title = stale
? `Shared server cache is past its ${Math.round(ttl / 1000)}s TTL — the next load refetches from MCMS.`
: `Shared server cache (TTL ${Math.round(ttl / 1000)}s) — collapses repeated MCMS requests across operators. Refresh to force-refetch.`;
};
paint();
dashCacheTimer = setInterval(paint, 1000);
}
function dashHealthRow(title, detail, count, drillKey) {
const cls = count > 0 ? 'status-warn' : 'status-ok';
const tappable = drillKey && count > 0;
@ -1613,13 +1653,13 @@ function showListModal(title, items) {
overlay.querySelector('.modal-ok').focus();
}
async function loadDashboard() {
async function loadDashboard(force) {
const extra = $('#dash-extra').checked;
$('#dash-status').textContent = extra
? 'Loading network + per-ONU Rx/FEC scan… (this fetches every ONU-STATE)'
: 'Loading network overview…';
$('#dash-refresh').disabled = true;
const res = await window.api.dashboardStats({ extraStats: extra });
const res = await window.api.dashboardStats({ extraStats: extra, fresh: !!force });
$('#dash-refresh').disabled = false;
if (!res.ok) {
$('#dash-status').textContent = `Error: ${res.error?.message || 'unknown'}`;
@ -1741,4 +1781,6 @@ function renderDashboard(d) {
const stamp = d.sampledAt ? new Date(d.sampledAt).toLocaleTimeString() : '';
$('#dash-status').textContent =
`${d.onuTotal} ONUs · ${d.oltCount} OLTs${d.extraStats ? ' · detailed scan' : ''}${stamp ? ' · ' + stamp : ''}`;
renderCacheBadge(d.cache);
}