From 2a5444fdc869956d4389bae4109ff942b444c5be Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 5 Jun 2026 12:18:24 +0200 Subject: [PATCH] feat(dashboard): live-tick agent status-age chip via data-set-at + 30s interval Stamp data-set-at on .status-age spans so the '(set N ago)' label stays accurate as time passes. Previously the age was computed once when the async dashboard-state fetch completed and never updated. With the keyed container row cache, rows persist much longer between rebuilds (no more full rebuild on every SSE event), making stale status-age labels more noticeable. The 30s ticker fixes this without requiring a full row rebuild or re-fetch. --- frontend/packages/dashboard/src/tabs.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index eb949387..5ef70fc0 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -737,13 +737,18 @@ window.marked = marked; const nowUnix = Math.floor(Date.now() / 1000); const ageStr = ds.status_set_at != null ? ` (set ${fmtAgeSecs(nowUnix - ds.status_set_at)} ago)` : ''; + // Stamp data-set-at so the 30s ticker below keeps the age + // label current even when the row is reused from the cache. + const ageAttrs = ds.status_set_at != null + ? { class: 'status-age', 'data-set-at': String(ds.status_set_at) } + : { class: 'status-age' }; body.append(el('div', { class: 'agent-status', title: `agent self-reported status${ageStr}`, }, el('span', { class: 'status-icon' }, '◈ '), ds.status_text, - el('span', { class: 'status-age' }, ageStr), + el('span', ageAttrs, ageStr), )); } }) @@ -1896,6 +1901,20 @@ window.marked = marked; }); }, 1000); + // 30s ticker for agent status-age chips. Renderers stamp `data-set-at` + // (unix seconds) on the `.status-age` span. Keyed container rows persist + // across re-renders, so without this ticker the "(set N ago)" label would + // become stale as time passes. 30s granularity matches fmtAgeSecs precision + // (sub-minute values round to seconds, coarser above that). + setInterval(() => { + const now = Math.floor(Date.now() / 1000); + document.querySelectorAll('.status-age[data-set-at]').forEach((node) => { + const setAt = Number(node.dataset.setAt); + if (!Number.isFinite(setAt) || setAt === 0) return; + node.textContent = ` (set ${fmtAgeSecs(now - setAt)} ago)`; + }); + }, 30_000); + const APPROVAL_TAB_KEY = 'hyperhive:approvals:tab'; // Derived approval state — cold-loaded from /api/state, then mutated // live by `approval_added` / `approval_resolved` dashboard events.