Compare commits

...
Author SHA1 Message Date
iris
1c1e1dc5a9 docs(dashboard): document live countdown ticker for schedule next-fire and reminder due-at labels 2026-06-05 13:50:44 +02:00
iris
c0d7ec6572 feat(dashboard): live countdown ticker for reminder + schedule due-at labels
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=<unix> 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.
2026-06-05 13:50:44 +02:00
2 changed files with 34 additions and 4 deletions

View file

@ -245,6 +245,13 @@ checked-not-originally-active = `targets_add`; submit PATCHes
`/api/schedules/{id}`), and a `✕` button cancels the whole
schedule (`POST /api/schedules/{id}/cancel`).
The `next` column cell (`.sched-due`) carries a `data-due-at`
Unix timestamp attribute; a shared 1s ticker rewrites it
in-place showing `fmtDuration` while in the future and
`overdue X ago` once the fire time has passed — same
zero-re-render pattern as the reminder due-at labels and the
question TTL chip.
The table's last row is a permanent inline creation row:
inputs live directly in table cells (targets as checkboxes,
body textarea that expands on focus, datetime-local pre-filled
@ -265,7 +272,12 @@ button hard-deletes (`POST /cancel-reminder/{id}`) and a
`R3TRY` button re-arms one whose delivery failed
(`POST /retry-reminder/{id}`). Backed by `GET /api/reminders`.
Lives in the SCH3DUL3S tab alongside operator schedules so the
operator has one place for everything time-fired.
operator has one place for everything time-fired. The due-time
label (`.reminder-due`) carries a `data-due-at` Unix timestamp
attribute; a shared 1s ticker rewrites it in-place — showing
`in Xm Ys` while the reminder is in the future and
`overdue X ago` once the deadline passes — without triggering a
full re-render of the list.
## P33RS tab

View file

@ -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);