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

@ -51,6 +51,12 @@ class TtlCache {
return p;
}
/** Age (ms) of the cached entry for `key`, or null if not cached. */
ageOf(key) {
const e = this.entries.get(key);
return e ? Date.now() - e.at : null;
}
/** Drop every entry whose key starts with `prefix` (e.g. one MCMS host's
* dataset). Returns how many were removed. */
invalidate(prefix) {

View file

@ -331,7 +331,18 @@ const handlers = {
: null;
let controllerCount = null;
try { controllerCount = (await cachedBulk(s, 'controllerConfigs', () => c.listAllControllerConfigs(), fresh)).length; } catch (_) { /* best-effort */ }
return { ok: true, data: summarizeDashboard({ olts, onuStates, controllerCount }) };
const data = summarizeDashboard({ olts, onuStates, controllerCount });
// Tell the client how stale the cached inputs are. Use the OLDEST dataset
// feeding this view so the indicator never understates staleness.
const ages = [cache.ageOf(ckey(s, 'oltStates'))];
if (extraStats) ages.push(cache.ageOf(ckey(s, 'onuStates')));
const known = ages.filter((a) => a !== null);
data.cache = {
enabled: cache.enabled(),
ttlMs: CACHE_TTL_MS,
ageMs: known.length ? Math.max(...known) : 0,
};
return { ok: true, data };
},
async listOlts(s, body) {