diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 54069c86..1d4d21ea 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -2585,9 +2585,15 @@ window.marked = marked; // Source chip (manual / meta_update / auto_update / crash_recover). li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source)); // Timing: queued Xs ago when pending, elapsed when running, - // finished Xs ago for terminal. + // finished Xs ago for terminal. Queued + terminal stamps use + // data-rqe-enqueued / data-rqe-finished so the 30s ticker below + // can keep them fresh — keyed rows persist across snapshots, so + // without a ticker "queued 2m ago" would never advance. if (entry.state === 'queued') { - li.append(' ', el('span', { class: 'rqe-when' }, '· queued ' + fmtAgo(entry.enqueued_at))); + li.append(' ', el('span', { + class: 'rqe-when', + 'data-rqe-enqueued': String(entry.enqueued_at), + }, '· queued ' + fmtAgo(entry.enqueued_at))); } else if (entry.state === 'running' && entry.started_at) { const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - entry.started_at)); li.append(' ', el('span', { @@ -2595,7 +2601,11 @@ window.marked = marked; 'data-rqe-elapsed': String(entry.started_at), }, '· ' + fmtElapsed(elapsed))); } else if (entry.finished_at) { - li.append(' ', el('span', { class: 'rqe-when' }, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at))); + li.append(' ', el('span', { + class: 'rqe-when', + 'data-rqe-finished': String(entry.finished_at), + 'data-rqe-state': entry.state, + }, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at))); } // Reason (truncated; full text on hover). if (entry.reason) { @@ -2671,14 +2681,39 @@ window.marked = marked; // `renderQueueEntry`; the ticker avoids a full re-render just for the // wall-clock update. setInterval(() => { + const now = Math.floor(Date.now() / 1000); for (const span of document.querySelectorAll('.rqe-when[data-rqe-elapsed]')) { const started = parseInt(span.dataset.rqeElapsed, 10); if (!started) continue; - const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - started)); + const elapsed = Math.max(0, now - started); span.textContent = '· ' + fmtElapsed(elapsed); } + for (const span of document.querySelectorAll('.build-logs-runtime[data-bl-elapsed]')) { + const started = parseInt(span.dataset.blElapsed, 10); + if (!started) continue; + const elapsed = Math.max(0, now - started); + span.textContent = fmtElapsed(elapsed); + } }, 1000); + // 30s ticker for queued-age and terminal-age labels. Keyed rows + // persist across rebuild_queue_changed snapshots, so without this + // "queued 1m ago" / "done 5m ago" labels would never advance. + setInterval(() => { + const now = Math.floor(Date.now() / 1000); + for (const span of document.querySelectorAll('.rqe-when[data-rqe-enqueued]')) { + const enqueued = parseInt(span.dataset.rqeEnqueued, 10); + if (!enqueued) continue; + span.textContent = '· queued ' + fmtAgo(enqueued); + } + for (const span of document.querySelectorAll('.rqe-when[data-rqe-finished]')) { + const finished = parseInt(span.dataset.rqeFinished, 10); + if (!finished) continue; + const state = span.dataset.rqeState || ''; + span.textContent = '· ' + state + ' ' + fmtAgo(finished); + } + }, 30_000); + // ─── reminders ────────────────────────────────────────────────────────── // Reminders aren't part of /api/state (separate sqlite table, separate // mutation cadence). refreshReminders() is called from refreshState() for