dashboard: stream the running rebuild's build log inline in the queue

The C0R3 rebuild queue only linked out to the logs page (logs →). Add an
inline live-log panel under the queue that streams the currently-running
rebuild's build output, so the operator watches progress without leaving the
page.

Extract the build-log SSE streaming logic (append stdout/stderr, sticky-bottom
scroll, stderr separator, reconnect-replay reset, done/error handling) into a
shared `openBuildLogStream(id, pre, {onDone, onError})` in common.js, and use it
from BOTH the L0GS page BUILD tab (logs.js, previously inline) and the new C0R3
panel (core.js) — one implementation, no duplication.

The panel is one persistent instance keyed to the running entry's build_log_id
(the queue runs one build at a time), in its own container (#rebuild-live-log)
outside rebuild-queue-section so the queue's per-row re-render — rows rebuild as
the build step advances — never tears down the open stream; it reconnects only
when the running build_log_id changes and won't reopen a stream that already
sent done. Collapsible, live/ok/fail badge, raw download. Hidden when nothing
is building; each row keeps its logs → link for full history.

Frontend-only — no backend change (endpoint + build_log_id already existed).
Closes #1860.
This commit is contained in:
iris 2026-06-22 01:55:38 +02:00
commit e797b75ca9
3 changed files with 79 additions and 78 deletions

View file

@ -23,7 +23,7 @@
// SYSTEM tabs so the operator knows how stale the output is.
import {
$, el, fmtAgeSecs, openStream, initServerWarnings,
$, el, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings,
} from './common.js';
import { createTabStrip } from '@hive/shared/tabs.js';
@ -138,53 +138,29 @@ import { createTabStrip } from '@hive/shared/tabs.js';
const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live');
detail.append(badge, pre);
// Sticky-bottom: auto-scroll to the bottom as new lines arrive
// unless the operator has manually scrolled up. Track intent via
// the scroll event rather than recomputing on each append.
let atBottom = true;
pre.addEventListener('scroll', () => {
atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
});
streamEs = new EventSource('/api/build-logs/id/' + h.id + '/stream');
let stdoutLen = 0, stderrLen = 0;
streamEs.onmessage = (e) => {
let frame;
try { frame = JSON.parse(e.data); } catch { return; }
if (frame.stdout_append) {
pre.textContent += frame.stdout_append;
if (atBottom) pre.scrollTop = pre.scrollHeight;
stdoutLen += frame.stdout_append.length;
}
if (frame.stderr_append) {
if (stderrLen === 0) pre.textContent += '\n--- stderr ---\n';
pre.textContent += frame.stderr_append;
if (atBottom) pre.scrollTop = pre.scrollHeight;
stderrLen += frame.stderr_append.length;
}
if (frame.done) {
// Stop the elapsed-time ticker and show the final duration.
// Stream via the shared helper (also used by the C0R3 rebuild-queue
// live-log panel); it owns the append / sticky-scroll /
// stderr-separator / reconnect-replay logic. We wire the badge +
// the row's elapsed-time ticker here.
streamEs = openBuildLogStream(h.id, pre, {
onDone: (status) => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
if (h.started_at) {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
runtime.textContent = fmtDuration(Math.max(0, elapsed));
}
badge.className = frame.status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
badge.textContent = frame.status || 'done';
streamEs.close(); streamEs = null;
badge.className = status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
badge.textContent = status;
streamEs = null;
detail.dataset.loaded = '1';
}
};
streamEs.onerror = () => {
if (streamEs && streamEs.readyState === EventSource.CONNECTING) {
pre.textContent = ''; stdoutLen = 0; stderrLen = 0;
} else {
},
onError: () => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error';
badge.className = 'badge badge-fail';
if (streamEs) { streamEs.close(); streamEs = null; }
}
};
streamEs = null;
},
});
} else {
const pre = el('pre', { class: 'build-logs-output' }, 'fetching…');
detail.append(pre);