diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 08f8e686..0328ed54 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -179,17 +179,6 @@ cancel flips the row to `⊘ cancelled` via the next Cold-loaded from `/api/state.rebuild_queue`; live updates via `rebuild_queue_changed` snapshot event. -Below the queue, a **live build-log panel** (`#rebuild-live-log`, -`renderRebuildLiveLog`) streams the currently-running rebuild's output -inline — collapsible, with a live/ok/fail badge and a `↓ raw` download. -It's keyed to the running entry's `build_log_id` and opens one -`EventSource` to `GET /api/build-logs/id/{id}/stream` (the same stream -the L0GS page BUILD tab uses; the stream replays accumulated output on -connect). It lives in its own container outside `#rebuild-queue-section` -so the queue's per-row re-render (rows rebuild as the `step` advances) -never tears down the open stream; it hides when nothing is building and -each row keeps its `logs →` link out to the full L0GS history. - **K3PT ST4T3** — destroyed-but-state-kept tombstones (size + age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a Spawn approval; existing state is reused), `PURG3` (wipes diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 2a66ffdf..370517e5 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -305,52 +305,6 @@ export function openStream(url) { return target; } -// Stream a build log into a
, returning the EventSource. Shared by the
-// L0GS page BUILD tab and the C0R3 rebuild-queue live-log panel so the
-// append / sticky-scroll / stderr-separator / reconnect-replay logic lives in
-// one place. Appends `stdout_append` then `stderr_append` (one `--- stderr ---`
-// separator) frames from `GET /api/build-logs/id/{id}/stream`; auto-scrolls to
-// the bottom unless the operator scrolled up; on the terminal `done` frame
-// closes the stream and calls `onDone(status)`; on a non-transient error
-// closes and calls `onError()`. The backend replays accumulated output on each
-// (re)connect, so a CONNECTING reconnect resets the to avoid doubling.
-export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
- let atBottom = true;
- pre.addEventListener('scroll', () => {
- atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
- });
- let stderrSeen = false;
- const es = new EventSource('/api/build-logs/id/' + id + '/stream');
- es.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;
- }
- if (frame.stderr_append) {
- if (!stderrSeen) { pre.textContent += '\n--- stderr ---\n'; stderrSeen = true; }
- pre.textContent += frame.stderr_append;
- if (atBottom) pre.scrollTop = pre.scrollHeight;
- }
- if (frame.done) {
- es.close();
- if (onDone) onDone(frame.status || 'done');
- }
- };
- es.onerror = () => {
- // CONNECTING = the browser is auto-reconnecting; the stream replays from
- // the start, so clear the to avoid duplicated output and wait.
- if (es.readyState === EventSource.CONNECTING) {
- pre.textContent = ''; stderrSeen = false;
- return;
- }
- es.close();
- if (onError) onError();
- };
- return es;
-}
-
// ─── side panel ─────────────────────────────────────────────────────────
// Singleton drawer that swipes in from the right. Long content
// (file previews, approval diffs, journald logs, applied config)
diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css
index 57509df9..46755bdb 100644
--- a/frontend/packages/dashboard/src/core.css
+++ b/frontend/packages/dashboard/src/core.css
@@ -33,70 +33,3 @@ body.core-shell {
/* createTabStrip toggles the native `hidden` attribute on inactive
panes; ensure it wins over any inherited display. */
.core-pane[hidden] { display: none; }
-
-/* ─── running-rebuild live log (renderRebuildLiveLog in core.js) ──────────
- Streams the currently-running rebuild's build log inline under the queue.
- Hidden (native `hidden` attr) when nothing is building. */
-.rebuild-live-log {
- margin-top: 0.8em;
- border: 1px solid var(--purple-dim);
- border-radius: 4px;
- background: color-mix(in srgb, var(--crust) 70%, transparent);
-}
-.rebuild-live-log-header {
- display: flex;
- align-items: center;
- gap: 0.4em;
- padding: 0.4em 0.6em;
- font-size: 0.85em;
- border-bottom: 1px solid var(--border);
-}
-.rebuild-live-log-toggle {
- background: transparent;
- border: 0;
- color: var(--muted);
- cursor: pointer;
- font-family: inherit;
- font-size: 1em;
- padding: 0 0.2em;
-}
-.rebuild-live-log-toggle:hover { color: var(--fg); }
-.rebuild-live-log-title { color: var(--muted); }
-.rebuild-live-log-raw {
- margin-left: auto;
- color: var(--purple);
- text-decoration: none;
- font-size: 0.9em;
-}
-.rebuild-live-log-raw:hover { text-decoration: underline; }
-.rebuild-live-log-badge {
- font-size: 0.75em;
- font-weight: bold;
- padding: 0.05em 0.5em;
- border-radius: 999px;
- letter-spacing: 0.05em;
-}
-.rebuild-live-log-badge.rll-running {
- color: var(--amber);
- background: color-mix(in srgb, var(--amber) 18%, transparent);
-}
-.rebuild-live-log-badge.rll-ok {
- color: var(--green);
- background: color-mix(in srgb, var(--green) 18%, transparent);
-}
-.rebuild-live-log-badge.rll-fail {
- color: var(--red);
- background: color-mix(in srgb, var(--red) 18%, transparent);
-}
-.rebuild-live-log-output {
- margin: 0;
- padding: 0.5em 0.7em;
- max-height: 22em;
- overflow-y: auto;
- white-space: pre-wrap;
- word-break: break-word;
- font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
- font-size: 0.8em;
- line-height: 1.4;
- color: var(--fg);
-}
diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html
index 6de67577..51c99803 100644
--- a/frontend/packages/dashboard/src/core.html
+++ b/frontend/packages/dashboard/src/core.html
@@ -55,11 +55,6 @@
-
-
diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js
index e58ed487..d613a2e8 100644
--- a/frontend/packages/dashboard/src/core.js
+++ b/frontend/packages/dashboard/src/core.js
@@ -11,7 +11,7 @@
// dashboard SYST3M tab); the dashboard keeps its own copies for now —
// de-duplication + removing the SYST3M tab is a deliberate follow-up.
-import { $, el, form, openStream, openBuildLogStream, initServerWarnings, bindAsyncForms } from './common.js';
+import { $, el, form, openStream, initServerWarnings, bindAsyncForms } from './common.js';
import { fmtAgo, fmtElapsed, truncate } from './util.js';
import { createTabStrip } from '@hive/shared/tabs.js';
@@ -158,10 +158,9 @@ function rebuildQueueEntryFingerprint(entry, isChild) {
}
function renderRebuildQueue(s) {
- const queue = s.rebuild_queue || [];
- renderRebuildLiveLog(queue);
const root = $('rebuild-queue-section');
if (!root) return;
+ const queue = s.rebuild_queue || [];
if (!queue.length) {
rebuildQueueRowCache.clear();
@@ -295,100 +294,6 @@ function renderQueueEntry(entry, _byId, isChild) {
return li;
}
-// ─── running-rebuild live log ─────────────────────────────────────────────
-// One persistent live-log panel for the currently-running rebuild (the queue
-// runs one build at a time). Keyed to the running entry's build_log_id and
-// kept in its own container (#rebuild-live-log) so the queue's row re-render
-// — which rebuilds rows as the build step advances — never tears down the open
-// SSE stream. Reuses the build-log stream the logs page BUILD tab uses
-// (GET /api/build-logs/id/{id}/stream; frames {stdout_append, stderr_append,
-// done, status}); the stream replays accumulated output on connect, so
-// attaching mid-build still shows the backlog.
-let liveLogEs = null;
-let liveLogId = null;
-let liveLogDone = false;
-let liveLogCollapsed = false;
-
-function closeLiveLogStream() {
- if (liveLogEs) { liveLogEs.close(); liveLogEs = null; }
-}
-
-function renderRebuildLiveLog(queue) {
- const root = $('rebuild-live-log');
- if (!root) return;
- const running = (queue || []).find(
- (e) => e.state === 'running' && e.build_log_id != null);
-
- if (!running) {
- // No running build with a log → tear down + hide.
- closeLiveLogStream();
- liveLogId = null;
- liveLogDone = false;
- if (!root.hidden) { root.hidden = true; root.replaceChildren(); }
- return;
- }
- // Same build still streaming (or already finished) → leave the panel as-is.
- // Re-renders fire on every queue mutation; don't reconnect mid-build, and
- // don't reopen a stream that already sent `done`.
- if (running.build_log_id === liveLogId && (liveLogEs || liveLogDone)) return;
-
- // New running build → (re)build the panel + open its stream.
- closeLiveLogStream();
- liveLogId = running.build_log_id;
- liveLogDone = false;
- root.hidden = false;
- root.replaceChildren();
-
- const pre = el('pre', { class: 'rebuild-live-log-output' }, '');
- pre.hidden = liveLogCollapsed;
- const badge = el('span', { class: 'rebuild-live-log-badge rll-running' }, 'live');
- const toggle = el('button', {
- type: 'button',
- class: 'rebuild-live-log-toggle',
- 'aria-expanded': String(!liveLogCollapsed),
- title: liveLogCollapsed ? 'expand live log' : 'collapse live log',
- }, liveLogCollapsed ? '▸' : '▾');
- toggle.addEventListener('click', () => {
- liveLogCollapsed = !liveLogCollapsed;
- pre.hidden = liveLogCollapsed;
- toggle.textContent = liveLogCollapsed ? '▸' : '▾';
- toggle.setAttribute('aria-expanded', String(!liveLogCollapsed));
- toggle.title = liveLogCollapsed ? 'expand live log' : 'collapse live log';
- });
- const header = el('div', { class: 'rebuild-live-log-header' },
- toggle, ' ',
- el('span', { class: 'rebuild-live-log-title' }, 'live build log — '),
- el('code', { class: 'rqe-agent' }, running.agent),
- ' ', el('span', { class: 'rqe-kind' }, running.kind || 'rebuild'),
- ' ', badge, ' ',
- el('a', {
- class: 'rebuild-live-log-raw',
- href: '/api/build-logs/id/' + running.build_log_id + '/raw',
- download: 'build-log-' + running.build_log_id + '.txt',
- }, '↓ raw'),
- );
- root.append(header, pre);
-
- // Stream via the shared helper (same path the L0GS BUILD tab uses). It owns
- // the append / sticky-scroll / stderr-separator / reconnect-replay logic; we
- // just wire the badge. onDone keeps liveLogId + liveLogDone set so a
- // re-render before the entry leaves 'running' won't reopen the finished
- // stream — the next render with no running entry clears the panel.
- liveLogEs = openBuildLogStream(running.build_log_id, pre, {
- onDone: (status) => {
- liveLogDone = true;
- badge.className = 'rebuild-live-log-badge ' + (status === 'ok' ? 'rll-ok' : 'rll-fail');
- badge.textContent = status;
- liveLogEs = null;
- },
- onError: () => {
- badge.className = 'rebuild-live-log-badge rll-fail';
- badge.textContent = 'stream error';
- liveLogEs = null;
- },
- });
-}
-
// ─── kept state (tombstones) ──────────────────────────────────────────────
function renderTombstones(s) {
const root = $('tombstones-section');
diff --git a/frontend/packages/dashboard/src/logs.js b/frontend/packages/dashboard/src/logs.js
index 924dd6d5..7d82be4a 100644
--- a/frontend/packages/dashboard/src/logs.js
+++ b/frontend/packages/dashboard/src/logs.js
@@ -23,7 +23,7 @@
// SYSTEM tabs so the operator knows how stale the output is.
import {
- $, el, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings,
+ $, el, fmtAgeSecs, openStream, initServerWarnings,
} from './common.js';
import { createTabStrip } from '@hive/shared/tabs.js';
@@ -138,29 +138,53 @@ import { createTabStrip } from '@hive/shared/tabs.js';
const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live');
detail.append(badge, pre);
- // 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) => {
+ // 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.
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 = status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
- badge.textContent = status;
- streamEs = null;
+ badge.className = frame.status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
+ badge.textContent = frame.status || 'done';
+ streamEs.close(); streamEs = null;
detail.dataset.loaded = '1';
- },
- onError: () => {
+ }
+ };
+ streamEs.onerror = () => {
+ if (streamEs && streamEs.readyState === EventSource.CONNECTING) {
+ pre.textContent = ''; stdoutLen = 0; stderrLen = 0;
+ } else {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error';
badge.className = 'badge badge-fail';
- streamEs = null;
- },
- });
+ if (streamEs) { streamEs.close(); streamEs = null; }
+ }
+ };
} else {
const pre = el('pre', { class: 'build-logs-output' }, 'fetching…');
detail.append(pre);