diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index b529b00..807484a 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