diff --git a/frontend/packages/dashboard/src/call.js b/frontend/packages/dashboard/src/call.js index 2440c6df..a332d20e 100644 --- a/frontend/packages/dashboard/src/call.js +++ b/frontend/packages/dashboard/src/call.js @@ -16,7 +16,7 @@ import { $, el, form, Panel, appendLinkified } from './common.js'; import { themedToast } from './modal.js'; -import { fmtAgo, fmtDuration } from './util.js'; +import { epochSec, fmtAgo, fmtDuration } from './util.js'; import { questionsState, QUESTION_HISTORY_LIMIT } from './state.js'; // Registered by the dashboard entry at boot; defaults to a no-op so the @@ -70,7 +70,7 @@ function renderOperatorInbox() { `✓ mark all read (${operatorInbox.length})`); mark.addEventListener('click', markOperatorInboxRead); root.append(el('div', { class: 'inbox-toolbar' }, mark)); - const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); + const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19); const ul = el('ul', { class: 'inbox' }); for (const m of operatorInbox) { const body = el('span', { class: 'msg-body' }); @@ -333,11 +333,12 @@ export function renderApprovals() { // Goes amber once it's been pending an hour so a stale request is // obvious at a glance (see docs/web-ui.md::Approval card). if (a.requested_at != null) { - const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - a.requested_at)); + const requestedSec = epochSec(a.requested_at); + const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - requestedSec)); head.append(el('span', { class: 'approval-ts' + (ageSec >= 3600 ? ' stale' : ''), - title: 'requested ' + new Date(a.requested_at * 1000).toLocaleString(), - 'data-requested-at': String(a.requested_at), + title: 'requested ' + new Date(a.requested_at).toLocaleString(), + 'data-requested-at': String(requestedSec), }, 'requested ' + fmtAgo(a.requested_at))); } li.append(head); @@ -556,7 +557,7 @@ function questionRowFingerprint(q) { // Event listeners attached here (keydown on textarea, submit on form) are // preserved in the reused node — no re-attachment needed. function buildQuestionLi(q) { - const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); + const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19); const targetLabel = q.target || 'operator'; const li = el('li', { class: 'question' + (q.target ? ' question-peer' : '') }); const head = el('div', { class: 'q-head' }, @@ -570,10 +571,10 @@ function buildQuestionLi(q) { // Tag the chip with its deadline so the global 1s ticker // can refresh the text without re-rendering the questions section. const ttlEl = el('span', { - class: 'q-ttl', 'data-deadline': String(q.deadline_at), + class: 'q-ttl', 'data-deadline': String(epochSec(q.deadline_at)), }); ttlEl.textContent = formatTtl( - q.deadline_at - Math.floor(Date.now() / 1000), + epochSec(q.deadline_at) - Math.floor(Date.now() / 1000), ); head.append(' ', ttlEl); } @@ -694,7 +695,7 @@ export function renderQuestions() { //
  • nodes (preserving textarea/checkbox state) and only rebuilds // cache-miss rows, so we no longer wipe the DOM at the start. const openDetails = snapshotOpenDetails(root); - const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); + const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19); const allPending = questionsState.pending; // Filter chips. Always include `all` / `operator` / `peer`; add diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js index d8cc681e..94ab12f4 100644 --- a/frontend/packages/dashboard/src/flow.js +++ b/frontend/packages/dashboard/src/flow.js @@ -18,6 +18,7 @@ import { appendLinkified, openStream, initServerWarnings, } from './common.js'; +import { epochSec } from './util.js'; (() => { NOTIF.bind(); @@ -92,7 +93,7 @@ import { const flow = $('msgflow'); if (!flow) return; flow.replaceChildren(); - const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19); + const tsFmt = (ts) => new Date(ts).toISOString().slice(11, 19); // Pulse the page banner whenever a broker event lands. The // `.banner` element lives in the dashboard's