From 26c9c13cf518b19c975c111167c8d87e1f5c270f Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 17:26:38 +0200 Subject: [PATCH] build-logs: surface runtime_secs in header; show duration in dashboard row --- frontend/packages/dashboard/src/tabs.js | 15 +++++++++++++++ hive-c0re/src/build_logs.rs | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index e62183d9..34b348f2 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -1126,10 +1126,19 @@ window.marked = marked; : 'running'; const age = h.started_at ? fmtAgeSecs(nowUnix - h.started_at) + ' ago' : '?'; + const runtimeSpan = h.runtime_secs != null + ? el('span', { class: 'build-logs-runtime meta' }, fmtDuration(h.runtime_secs)) + : h.started_at && !h.finished_at + ? el('span', { + class: 'build-logs-runtime meta', + 'data-bl-elapsed': String(h.started_at), + }, fmtElapsed(Math.max(0, nowUnix - h.started_at))) + : null; const rowBtn = el('button', { type: 'button', class: 'build-logs-row-btn', 'aria-expanded': 'false' }, el('span', { class: `badge ${statusCls}` }, statusLabel), el('span', { class: 'build-logs-kind' }, h.kind), + runtimeSpan, el('span', { class: 'build-logs-age meta' }, age), el('span', { class: 'build-logs-cmdline meta' }, h.cmdline), ); @@ -2192,6 +2201,12 @@ window.marked = marked; const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - 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, Math.floor(Date.now() / 1000 - started)); + span.textContent = fmtElapsed(elapsed); + } }, 1000); // ─── reminders ────────────────────────────────────────────────────────── diff --git a/hive-c0re/src/build_logs.rs b/hive-c0re/src/build_logs.rs index f629cd93..31fcafd4 100644 --- a/hive-c0re/src/build_logs.rs +++ b/hive-c0re/src/build_logs.rs @@ -96,6 +96,9 @@ pub struct BuildLogHeader { pub started_at: i64, pub finished_at: Option, pub status: Option, + /// Elapsed seconds from `started_at` to `finished_at`. `None` while + /// the build is still in progress. + pub runtime_secs: Option, } /// Full row with stdout/stderr text inlined. Returned by `get_full`, @@ -328,15 +331,19 @@ impl BuildLogs { )?; let row = stmt .query_row(params![id], |r| { + let started_at: i64 = r.get(4)?; + let finished_at: Option = r.get(5)?; + let runtime_secs = finished_at.map(|f| f - started_at); Ok(BuildLogFull { header: BuildLogHeader { id: r.get(0)?, agent: r.get(1)?, kind: r.get(2)?, cmdline: r.get(3)?, - started_at: r.get(4)?, - finished_at: r.get(5)?, + started_at, + finished_at, status: r.get(6)?, + runtime_secs, }, stdout: r.get(7)?, stderr: r.get(8)?, @@ -404,14 +411,18 @@ pub fn spawn_vacuum(coord: &Arc) { } fn row_to_header(r: &rusqlite::Row) -> rusqlite::Result { + let started_at: i64 = r.get(4)?; + let finished_at: Option = r.get(5)?; + let runtime_secs = finished_at.map(|f| f - started_at); Ok(BuildLogHeader { id: r.get(0)?, agent: r.get(1)?, kind: r.get(2)?, cmdline: r.get(3)?, - started_at: r.get(4)?, - finished_at: r.get(5)?, + started_at, + finished_at, status: r.get(6)?, + runtime_secs, }) }