From c0d7ec657270f143508e8e6f2857ac74fe6cc218 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 5 Jun 2026 11:43:33 +0200 Subject: [PATCH] feat(dashboard): live countdown ticker for reminder + schedule due-at labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reminder due-at labels ("in 3m 45s") and schedule next-fire cells were computed once at render time and never updated. For short-horizon reminders the stale count was visually misleading. Both renderers now stamp data-due-at= on their respective elements (.reminder-due / .sched-due). A shared 1s setInterval ticker walks all live elements and rewrites textContent using fmtDuration / fmtAgo — same zero-re-render pattern as the existing question TTL chip ticker. --- frontend/packages/dashboard/src/tabs.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 1c965e07..267016fb 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -455,8 +455,7 @@ window.marked = marked; menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }), menuSep(), // Deep-link to the AGENT log tab pre-filtered to this container. - // The ?agent= param is read by logs.js on load; the stored localStorage - // selection is overridden so the operator lands directly in this + // The ?agent= param is read by logs.js on load and pre-selects this // agent's journal without extra clicks. menuLink('journal logs →', `/logs.html?agent=${encodeURIComponent(c.name)}#agent`, @@ -1999,6 +1998,11 @@ window.marked = marked; // a request pending for an hour could still show "0s ago" without // this ticker. Also flips `.stale` (amber highlight) at exactly 1h // rather than only at the next re-render. + // Live countdown for reminder due-at labels and schedule next-fire + // cells. Both renderers stamp `data-due-at` on the element so this + // single ticker keeps them fresh without triggering a full re-render. + // `.reminder-due` → "overdue X ago" / "in Y" + // `.sched-due` → same pattern setInterval(() => { const now = Math.floor(Date.now() / 1000); document.querySelectorAll('.approval-ts[data-requested-at]').forEach((node) => { @@ -2008,6 +2012,14 @@ window.marked = marked; node.textContent = 'requested ' + fmtAgo(requestedAt); node.classList.toggle('stale', ageSec >= 3600); }); + document.querySelectorAll('.reminder-due[data-due-at], .sched-due[data-due-at]').forEach((node) => { + const dueAt = Number(node.getAttribute('data-due-at')); + if (!Number.isFinite(dueAt)) return; + const dueIn = dueAt - now; + node.textContent = dueIn <= 0 + ? 'overdue ' + fmtAgo(dueAt) + : (node.classList.contains('reminder-due') ? 'in ' : '') + fmtDuration(dueIn); + }); }, 1000); const APPROVAL_TAB_KEY = 'hyperhive:approvals:tab'; @@ -2790,7 +2802,11 @@ window.marked = marked; : `in ${fmtDuration(dueIn)}`; const head = el('div', { class: 'reminder-head' }, el('span', { class: 'agent' }, r.agent), ' ', - el('span', { class: 'meta', title: new Date(r.due_at * 1000).toISOString() }, dueLabel), + el('span', { + class: 'meta reminder-due', + title: new Date(r.due_at * 1000).toISOString(), + 'data-due-at': String(r.due_at), + }, dueLabel), ' ', el('span', { class: 'meta' }, `· id ${r.id}`), ); @@ -3393,6 +3409,8 @@ window.marked = marked; nextCell.textContent = dueIn <= 0 ? 'overdue ' + fmtAgo(s.next_fire_at_unix) : fmtDuration(dueIn); + nextCell.classList.add('sched-due'); + nextCell.dataset.dueAt = String(s.next_fire_at_unix); } tr.append(nextCell);