From 6f3b56ad84d1d40e4009651836724553da663c80 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 23 May 2026 10:44:05 +0200 Subject: [PATCH] dashboard: tick question TTL chip every second MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- hive-c0re/assets/app.js | 49 +++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index b529b008..807484a1 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -1028,15 +1028,17 @@ el('span', { class: 'msg-sep' }, 'asks:'), ); if (q.deadline_at) { - const remaining = q.deadline_at - Math.floor(Date.now() / 1000); - let txt; - if (remaining <= 0) txt = 'expiring…'; - else if (remaining < 60) txt = '⏳ ' + remaining + 's'; - else if (remaining < 3600) txt = '⏳ ' + Math.floor(remaining / 60) + 'm ' - + (remaining % 60) + 's'; - else txt = '⏳ ' + Math.floor(remaining / 3600) + 'h ' - + Math.floor((remaining % 3600) / 60) + 'm'; - head.append(' ', el('span', { class: 'q-ttl' }, txt)); + // Tag the chip with its deadline so the global 1s ticker + // (set up just below this function) can refresh the text + // without re-rendering the whole questions section + // (issue #335). + const ttlEl = el('span', { + class: 'q-ttl', 'data-deadline': String(q.deadline_at), + }); + ttlEl.textContent = formatTtl( + q.deadline_at - Math.floor(Date.now() / 1000), + ); + head.append(' ', ttlEl); } const qBody = el('div', { class: 'q-body' }); 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) ─────────── // No longer shipped on `/api/state.operator_inbox`. The dashboard // terminal's HiveTerminal feeds this via `onAnyEvent` — backfill from