dashboard: tick question TTL chip every second

The ` MM:SS` chip on an asked-with-timeout question was rendered
once and then frozen — the operator saw stale info (e.g. 48s
sitting unchanged for the whole TTL window) (issue #335).

Stamp the deadline onto the chip as `data-deadline` and run a
single page-wide setInterval that refreshes every `.q-ttl[data-
deadline]`'s textContent each second. No re-render of the
questions section; no new state on the client. No-op when no
chips are on screen.

Also pulls the bucketed seconds-to-string logic into a
`formatTtl` helper so the renderer and the ticker share one
source of truth.

Closes #335.
This commit is contained in:
iris 2026-05-23 10:44:05 +02:00
parent 5887111327
commit 6f3b56ad84

View file

@ -1028,15 +1028,17 @@
el('span', { class: 'msg-sep' }, 'asks:'), el('span', { class: 'msg-sep' }, 'asks:'),
); );
if (q.deadline_at) { if (q.deadline_at) {
const remaining = q.deadline_at - Math.floor(Date.now() / 1000); // Tag the chip with its deadline so the global 1s ticker
let txt; // (set up just below this function) can refresh the text
if (remaining <= 0) txt = 'expiring…'; // without re-rendering the whole questions section
else if (remaining < 60) txt = '⏳ ' + remaining + 's'; // (issue #335).
else if (remaining < 3600) txt = '⏳ ' + Math.floor(remaining / 60) + 'm ' const ttlEl = el('span', {
+ (remaining % 60) + 's'; class: 'q-ttl', 'data-deadline': String(q.deadline_at),
else txt = '⏳ ' + Math.floor(remaining / 3600) + 'h ' });
+ Math.floor((remaining % 3600) / 60) + 'm'; ttlEl.textContent = formatTtl(
head.append(' ', el('span', { class: 'q-ttl' }, txt)); q.deadline_at - Math.floor(Date.now() / 1000),
);
head.append(' ', ttlEl);
} }
const qBody = el('div', { class: 'q-body' }); const qBody = el('div', { class: 'q-body' });
appendLinkified(qBody, q.question, q.question_refs); appendLinkified(qBody, q.question, q.question_refs);
@ -1156,6 +1158,35 @@
} }
} }
// Format a remaining-seconds count as the `⏳ …` TTL chip text on a
// question card. Bucketed at minutes / hours so a long deadline stays
// readable; "expiring…" once the deadline has passed (the host-side
// ttl-watchdog will fire shortly).
function formatTtl(remaining) {
if (remaining <= 0) return 'expiring…';
if (remaining < 60) return '⏳ ' + remaining + 's';
if (remaining < 3600) {
return '⏳ ' + Math.floor(remaining / 60) + 'm '
+ (remaining % 60) + 's';
}
return '⏳ ' + Math.floor(remaining / 3600) + 'h '
+ Math.floor((remaining % 3600) / 60) + 'm';
}
// Single page-wide ticker that refreshes every TTL chip in place
// each second (issue #335). Renderers stamp `data-deadline` on the
// chip; this just updates `textContent`, no re-render of the
// questions section. No-op when no chips are on screen, so the
// cost is negligible.
setInterval(() => {
const now = Math.floor(Date.now() / 1000);
document.querySelectorAll('.q-ttl[data-deadline]').forEach((node) => {
const deadline = Number(node.getAttribute('data-deadline'));
if (!Number.isFinite(deadline)) return;
node.textContent = formatTtl(deadline - now);
});
}, 1000);
// ─── operator inbox (derived from the broker message stream) ─────────── // ─── operator inbox (derived from the broker message stream) ───────────
// No longer shipped on `/api/state.operator_inbox`. The dashboard // No longer shipped on `/api/state.operator_inbox`. The dashboard
// terminal's HiveTerminal feeds this via `onAnyEvent` — backfill from // terminal's HiveTerminal feeds this via `onAnyEvent` — backfill from