Watch
0
0
Fork
You've already forked hyperhive
0

hive ui: remove the BUILD L0GS tab and the live build-log panel

A build's output and history are the swarm's builds Grafana board now, from the
lines hive-c0re logs. Removes the tab and its pane, the live log under the
rebuild queue, their styles, the log stream helper and the duration formatter
only they used, and the docs; also the tab-counts sentence 5925 missed.
This commit is contained in:
iris 2026-11-01 16:22:40 +01:00
commit deeb5ecd86
9 changed files with 29 additions and 741 deletions

View file

@ -20,9 +20,8 @@ page with four tabs:
- **4G3NTS** — every agent, live. This is the default tab and the one - **4G3NTS** — every agent, live. This is the default tab and the one
you'll check most. you'll check most.
- **R3BU1LD QU3U3** and **BUILD L0GS** — the build - **R3BU1LD QU3U3** — the rebuild queue. A build's output and history are on the
lifecycle: the rebuild queue with a live log of whatever is building, swarm's `builds` Grafana board.
and the build log history.
- **K3PT ST4T3** — kept state from agents with no container - **K3PT ST4T3** — kept state from agents with no container
(tombstones), with a purge button per entry. (tombstones), with a purge button per entry.

View file

@ -11,7 +11,7 @@ matrix accounts, the controller's job graph — are on the
hive-c0re serves the dashboard at `/`. hive-c0re serves the dashboard at `/`.
It has a fixed chrome header at the top and a `<main>` that shows exactly It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`, one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
`#buildlogs`, `#kept`) drives which pane is `#kept`) drives which pane is
active; hash changes don't reload the page. ST4TS active; hash changes don't reload the page. ST4TS
is a standalone page reached from the pages menu, not from the is a standalone page reached from the pages menu, not from the
dashboard tab strip. dashboard tab strip.
@ -35,10 +35,8 @@ dashboard tab strip.
If the name doesn't load the avatar shows a question mark, and without the URL the If the name doesn't load the avatar shows a question mark, and without the URL the
popover has no links. popover has no links.
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, - **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`,
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS
lives on its own page, reached from the pages menu. Count pills on lives on its own page, reached from the pages menu.
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
K3PT ST4T3 (kept-state entries).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`) - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip. — sits below the tab strip.
- **No warning banner.** No page shows hive-level warnings (host disk - **No warning banner.** No page shows hive-level warnings (host disk
@ -97,12 +95,9 @@ container is all hive-c0re can actually prove. Fixing it properly
needs a recorded destroy — **deferred** to the swarm-controller / needs a recorded destroy — **deferred** to the swarm-controller /
snapshot-storage rework where the problem changes shape. snapshot-storage rework where the problem changes shape.
## Build tabs (`#rebuild`, `#buildlogs`) ## Build tab (`#rebuild`)
The build lifecycle — rebuild queue, live build log, and The rebuild queue is the **R3BU1LD QU3U3** tab (`#rebuild`). It lives in `builds.js`, which `tabs.js` imports: the
build log history — is two tabs of the dashboard: **R3BU1LD QU3U3**
(`#rebuild`) and **BUILD L0GS**
(`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the
dashboard's one tab strip, `/api/state` snapshot and dashboard's one tab strip, `/api/state` snapshot and
`/api/dashboard/stream` subscription serve them, with `/api/dashboard/stream` subscription serve them, with
`rebuild_queue_changed` on the stream alongside the dashboard's other `rebuild_queue_changed` on the stream alongside the dashboard's other
@ -156,7 +151,7 @@ the tree — one per lifecycle state, matching the row glyphs — lets the
operator narrow which root groups render; unchecking a state re-fetches operator narrow which root groups render; unchecking a state re-fetches
`GET /api/jobq/graph?states=<checked, comma-joined>` rather than `GET /api/jobq/graph?states=<checked, comma-joined>` rather than
hiding rows client-side, so the `onUpdate` callback's node list (and hiding rows client-side, so the `onUpdate` callback's node list (and
everything downstream of it — the live-log panel) only everything downstream of it) only
ever sees what's actually shown. Filtering is by a **root's own** ever sees what's actually shown. Filtering is by a **root's own**
state, which is already its subtree's rolled-up answer, so the filter state, which is already its subtree's rolled-up answer, so the filter
keeps or drops a whole group, never splitting mid-tree. Default selection is keeps or drops a whole group, never splitting mid-tree. Default selection is
@ -169,34 +164,9 @@ root set — the history cap and the state filter are independent
concerns, so a narrow filter never reaches further back in time to concerns, so a narrow filter never reaches further back in time to
compensate. compensate.
Below the queue, a **live build-log panel** (`#rebuild-live-log`, A build's output and history aren't on this page. hive-c0re logs every container build
`renderRebuildLiveLog`) shows the currently running rebuild's output to the journal, and the swarm's `builds` Grafana board reads those lines: the builds with
inline — collapsible, with a live/ok/fail badge and a `↓ raw` their outcome and runtime, and the output of the one you pick.
download. It's keyed to the first `Running` node (in wire order)
whose `payload.data.build_log_id` has a value — read from the `onUpdate`
callback's node list, no separate
fetch — and **polls** `GET /api/build-log/{id}` every 2s
(`fetchAndRenderLiveLog` / `liveLogPollTimer`); not an `EventSource`
(that's the BUILD L0GS tab's own per-row expand view below — `GET
/api/build-logs/id/{id}/stream`, real SSE, replays accumulated output
on connect — a separate mechanism). The live-log panel lives in its
own container outside `#rebuild-queue-section` so the mounted
`JobqGraph`'s own re-renders never disturb the open poll; it hides
when nothing is building.
**BUILD L0GS** — all-agents build log history. Lazy-loaded on first tab
activation; autorefreshes when
`rebuild_queue_changed` fires. Fetches `GET /api/build-logs?limit=30`.
Renders a scrollable list of build entries; each row is a collapsible
button showing status badge (`live` / `ok` / `fail`), agent name,
elapsed duration, build kind, age, and the invocation command line.
Expanding a row fetches the full stdout+stderr via
`GET /api/build-logs/id/{id}`. A live in-progress build shows a `live`
badge with a ticking elapsed-time chip; expanding streams output via
`GET /api/build-logs/id/{id}/stream` with sticky-bottom autoscroll
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
entry with that id pre-expanded.
## ST4TS page (`/stats.html`) ## ST4TS page (`/stats.html`)
@ -544,11 +514,10 @@ below — some endpoints aren't in it yet.
404 when the row is missing (vacuum-reaped or stale id). 404 when the row is missing (vacuum-reaped or stale id).
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of - `GET /api/build-logs/id/{id}/stream` — live SSE stream of
stdout+stderr for an in-progress build (newline-delimited JSON stdout+stderr for an in-progress build (newline-delimited JSON
frames). Used by the expanding live-row view in the BUILD L0GS tab. The frames). The
stream closes when the build finishes or errors. stream closes when the build finishes or errors.
- `GET /api/build-logs/id/{id}/raw` — raw concatenated - `GET /api/build-logs/id/{id}/raw` — raw concatenated
stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download stdout+stderr as `text/plain`. Works on both finished and in-progress
raw` links in the build tabs. Works on both finished and in-progress
build logs. build logs.
- `GET /api/reminders` — list pending reminders for the - `GET /api/reminders` — list pending reminders for the
dashboard's queued-reminders panel. dashboard's queued-reminders panel.

View file

@ -1,24 +1,18 @@
// The build lifecycle tabs of the dashboard — rebuild queue, live build // The rebuild queue tab of the dashboard (R3BU1LD QU3U3): pending and running
// log, and build log history. Not an entry point: tabs.js // rebuilds as a `JobqGraph`. Not an entry point: tabs.js imports it, owns the
// imports it, owns the tab strip and the dashboard event stream, and calls // tab strip and the dashboard event stream, and calls the exports below. The
// the exports below. Two tabs, the panes' ids matching the tab ids: // builds' output and history are not here: they are the `builds` Grafana board,
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log // from the lines hive-c0re logs for each build.
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
// the first time the tab is shown
import { $, fmtAgeSecs, openBuildLogStream } from "./common.js"; import { $ } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { themedConfirm } from "@hive/shared/modal.js"; import { themedConfirm } from "@hive/shared/modal.js";
import { h, render } from "preact"; import { h, render } from "preact";
import { fmtDuration } from "./util.js";
import { JobqGraph } from "@hive/shared/jobq-graph.js"; import { JobqGraph } from "@hive/shared/jobq-graph.js";
// ─── derived state ─────────────────────────────────────────────────────────── // Kept in sync from JobqGraph's `onUpdate` callback (see renderRebuildQueue
// below) — the component owns fetching GET /api/jobq/graph and renders the tree
// Kept in sync from JobqGraph's `onUpdate` callback (see mountRebuildQueue // itself; this flat array is only for `onCancel`, which names the node it
// below) — the component owns fetching GET /api/jobq/graph and renders // cancels in its confirm.
// the tree itself; this flat array exists only for the two things it
// doesn't render: the count-pill and the live-log panel.
let jobqNodes = []; let jobqNodes = [];
let jobqGraphToken = 0; let jobqGraphToken = 0;
@ -29,8 +23,7 @@ let jobqGraphToken = 0;
// no hand-rolled tree/roll-up rendering here anymore. The component owns // no hand-rolled tree/roll-up rendering here anymore. The component owns
// fetching GET /api/jobq/graph and its own refetch (bump `jobqGraphToken` // fetching GET /api/jobq/graph and its own refetch (bump `jobqGraphToken`
// and render again); this page just reads its `onUpdate` callback to keep // and render again); this page just reads its `onUpdate` callback to keep
// `jobqNodes` (the flat array) in sync for the two things the generic // `jobqNodes` (the flat array) in sync for `onCancel` below.
// view doesn't render itself: the count-pill and the live-log panel below.
// //
// Cancel is the one action this page *does* wire up: the `cancellable` // Cancel is the one action this page *does* wire up: the `cancellable`
// prop turns on the component's own per-node cancel button, which calls // prop turns on the component's own per-node cancel button, which calls
@ -60,7 +53,6 @@ function renderRebuildQueue() {
refreshToken: jobqGraphToken, refreshToken: jobqGraphToken,
onUpdate: (nodes) => { onUpdate: (nodes) => {
jobqNodes = nodes || []; jobqNodes = nodes || [];
renderRebuildLiveLog();
}, },
onCancel: async (id) => { onCancel: async (id) => {
const node = jobqNodes.find((n) => n.id === id); const node = jobqNodes.find((n) => n.id === id);
@ -90,391 +82,19 @@ function renderRebuildQueue() {
); );
} }
// ─── running-rebuild live log ─────────────────────────────────────────────────
// One persistent live-log panel for the first currently-building node.
// Keyed to that node's id and kept in its own container (#rebuild-live-log)
// so the queue's row re-render never tears down an open poll.
//
// Build logs are fetched by polling GET /api/build-log/<node_id> (returns
// {stdout, stderr}) rather than SSE — a running node's log re-fetches on the
// rebuild_queue_changed tick; a terminal node's log is static (one last fetch
// on transition, then done).
let liveLogId = null; // current node id being shown
let liveLogDone = false; // true once the node left 'Running'
let liveLogCollapsed = false;
let liveLogPollTimer = null;
function clearLiveLogPoll() {
if (liveLogPollTimer) {
clearInterval(liveLogPollTimer);
liveLogPollTimer = null;
}
}
// First running node with a log, in wire order (root-then-subtree per
// root, roots in enqueue order — see hive-jobq-wire's wire_snapshot doc).
// Gate on build_log_id so lock/noop/store-only nodes don't open a blank
// panel.
function findLiveBuild() {
return (
jobqNodes.find(
(n) =>
n.state === "Running" &&
n.payload.data &&
n.payload.data.build_log_id != null,
) || null
);
}
async function fetchAndRenderLiveLog(nodeId, pre) {
try {
const r = await fetch("/api/build-log/" + nodeId);
if (!r.ok) return;
const data = await r.json();
const text = [
data.stdout,
data.stderr ? "--- stderr ---\n" + data.stderr : "",
]
.filter(Boolean)
.join("\n");
if (pre.textContent !== text) {
const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
pre.textContent = text;
if (atBottom) pre.scrollTop = pre.scrollHeight;
}
} catch {
/* network blip — ignore, next poll will retry */
}
}
function renderRebuildLiveLog() {
const root = $("rebuild-live-log");
if (!root) return;
const liveNode = findLiveBuild();
if (!liveNode) {
clearLiveLogPoll();
liveLogId = null;
liveLogDone = false;
if (!root.hidden) {
root.hidden = true;
root.replaceChildren();
}
return;
}
// Same node, already polling — just let the timer tick (or do a final
// fetch if the node just went non-running and we haven't marked done yet).
if (liveNode.id === liveLogId) {
if (!liveLogDone && liveNode.state !== "Running") {
clearLiveLogPoll();
liveLogDone = true;
const pre = root.querySelector(".rebuild-live-log-output");
const badge = root.querySelector(".rebuild-live-log-badge");
if (pre) fetchAndRenderLiveLog(liveNode.id, pre);
if (badge) {
const ok = liveNode.state !== "Failed";
badge.className =
"rebuild-live-log-badge " + (ok ? "rll-ok" : "rll-fail");
badge.textContent = liveNode.state;
}
}
return;
}
// New node — tear down old poll, rebuild the panel.
clearLiveLogPoll();
liveLogId = liveNode.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 liveAgent = liveNode.payload.data && liveNode.payload.data.agent;
const header = el(
"div",
{ class: "rebuild-live-log-header" },
toggle,
" ",
el("span", { class: "rebuild-live-log-title" }, "live build log — "),
// Label the specific node's agent, not the whole group's agent set.
el("code", { class: "rqe-agent" }, liveAgent || ""),
" ",
el("span", { class: "rqe-kind" }, liveNode.payload.label),
" ",
badge,
" ",
el(
"a",
{
class: "rebuild-live-log-raw",
href: "/api/build-log/" + liveNode.id + "/raw",
download: "build-log-" + liveNode.id + ".txt",
},
"↓ raw",
),
);
root.append(header, pre);
// Start polling.
fetchAndRenderLiveLog(liveNode.id, pre);
liveLogPollTimer = setInterval(
() => fetchAndRenderLiveLog(liveNode.id, pre),
2000,
);
}
// ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
// All-agents build log history with expand-to-detail. Live builds stream in
// real time. Lazy-loaded on first tab activation; auto-refreshes (debounced
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
// Deep-link: ?id=N#buildlogs auto-expands the target row.
let buildLogsLoaded = false;
function fmtTs(unixSecs) {
if (!unixSecs) return "";
const age = Math.floor(Date.now() / 1000) - unixSecs;
return fmtAgeSecs(Math.max(0, age)) + " ago";
}
async function fetchBuild() {
const buildList = $("build-list");
if (!buildList) return;
buildList.replaceChildren();
buildList.append(el("p", { class: "meta" }, "loading…"));
try {
const resp = await fetch("/api/build-logs?limit=30");
if (!resp.ok) throw new Error("http " + resp.status);
const rows = await resp.json();
buildList.replaceChildren();
if (!rows || rows.length === 0) {
buildList.append(el("p", { class: "meta" }, "(no build logs yet)"));
return;
}
const ul = el("ul", { class: "build-logs-list" });
for (const h of rows) {
const li = el("li", {
class: "build-logs-item",
"data-log-id": String(h.id),
});
const live = !h.status;
const ok = h.status === "ok";
const statusClass = live
? "hive-pill-sm badge-running"
: ok
? "hive-pill-sm badge-ok"
: "hive-pill-sm badge-fail";
const statusLabel = live ? "live" : ok ? "ok" : "fail";
const age = h.finished_at
? fmtTs(h.finished_at)
: live
? ""
: fmtTs(h.started_at);
const runtime =
h.runtime_secs != null
? el(
"span",
{ class: "build-logs-runtime meta" },
fmtDuration(Math.max(0, h.runtime_secs)),
)
: live
? el(
"span",
{ class: "build-logs-runtime meta build-logs-live-dur" },
"…",
)
: el("span", { class: "build-logs-runtime meta" }, "");
let durTimer = null;
if (live && h.started_at) {
const updateDur = () => {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
runtime.textContent = fmtDuration(Math.max(0, elapsed));
};
updateDur();
durTimer = setInterval(updateDur, 1000);
}
const rowBtn = el(
"button",
{
type: "button",
class: "build-logs-row-btn",
"aria-expanded": "false",
},
el("span", { class: statusClass }, statusLabel),
el("span", { class: "build-logs-agent" }, h.agent),
runtime,
el("span", { class: "build-logs-kind" }, h.kind),
el("span", { class: "build-logs-age meta" }, age),
el("span", { class: "build-logs-cmdline meta" }, h.cmdline),
);
const detail = el("div", { class: "build-logs-detail" });
detail.hidden = true;
let streamEs = null;
rowBtn.addEventListener("click", async () => {
const expanded = rowBtn.getAttribute("aria-expanded") === "true";
rowBtn.setAttribute("aria-expanded", String(!expanded));
detail.hidden = expanded;
if (expanded) {
if (streamEs) {
streamEs.close();
streamEs = null;
}
return;
}
if (detail.dataset.loaded) return;
detail.replaceChildren();
detail.append(
el(
"a",
{
href: "/api/build-logs/id/" + h.id + "/raw",
download: "build-log-" + h.id + ".txt",
class: "build-logs-dl",
},
"↓ download raw",
),
);
if (live) {
const pre = el(
"pre",
{ class: "build-logs-output build-logs-live" },
"",
);
const badge = el(
"span",
{ class: "build-logs-live-badge hive-pill-sm badge-running" },
"live",
);
detail.append(badge, pre);
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 =
status === "ok"
? "hive-pill-sm badge-ok"
: "hive-pill-sm badge-fail";
badge.textContent = status;
streamEs = null;
detail.dataset.loaded = "1";
},
onError: () => {
if (durTimer) {
clearInterval(durTimer);
durTimer = null;
}
badge.textContent = "stream error";
badge.className = "hive-pill-sm badge-fail";
streamEs = null;
},
});
} else {
const pre = el("pre", { class: "build-logs-output" }, "fetching…");
detail.append(pre);
try {
const r2 = await fetch("/api/build-logs/id/" + h.id);
if (!r2.ok) {
pre.textContent = "error " + r2.status;
} else {
const full = await r2.json();
const out = [full.stdout, full.stderr]
.filter(Boolean)
.join("\n--- stderr ---\n");
pre.textContent = out || "(empty)";
}
detail.dataset.loaded = "1";
} catch (err) {
pre.textContent = "fetch failed: " + err;
}
}
});
li.append(rowBtn, detail);
ul.append(li);
}
buildList.append(ul);
// Deep-link: ?id=N auto-expands the target row.
const deepId = new URLSearchParams(location.search).get("id");
if (deepId) {
const target = ul.querySelector('[data-log-id="' + deepId + '"]');
if (target) {
const btn = target.querySelector(".build-logs-row-btn");
if (btn) {
btn.click();
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}
} catch (err) {
buildList.replaceChildren();
buildList.append(el("p", { class: "meta" }, "fetch failed: " + err));
}
}
// ─── entry points for tabs.js ───────────────────────────────────────────────── // ─── entry points for tabs.js ─────────────────────────────────────────────────
// Everything below is called by tabs.js, which owns the tab strip, the // Everything below is called by tabs.js, which owns the tab strip, the
// `/api/state` snapshot and the dashboard event stream. // `/api/state` snapshot and the dashboard event stream.
/** Mount the rebuild queue and wire the build-log refresh button. Called /** Mount the rebuild queue. Called once, after the tab strip exists. The queue
* once, after the tab strip exists. The queue self-fetches on mount * self-fetches on mount (JobqGraph's own effect). */
* (JobqGraph's own effect). */
export function initBuilds() { export function initBuilds() {
const refresh = $("build-refresh");
if (refresh) refresh.addEventListener("click", fetchBuild);
mountRebuildQueue(); mountRebuildQueue();
} }
/** A tab was shown: BUILD L0GS loads its list the first time. */
export function onBuildsTabShown(tab) {
if (tab === "buildlogs" && !buildLogsLoaded) {
buildLogsLoaded = true;
fetchBuild();
}
}
let buildRefreshTimer = null;
/** `rebuild_queue_changed`: a payload-less refetch trigger — `JobqGraph` /** `rebuild_queue_changed`: a payload-less refetch trigger — `JobqGraph`
* owns the actual fetch. */ * owns the actual fetch. */
export function onRebuildQueueChanged() { export function onRebuildQueueChanged() {
jobqGraphToken += 1; jobqGraphToken += 1;
renderRebuildQueue(); renderRebuildQueue();
// Auto-refresh the build log list while BUILD L0GS is the active tab.
if (document.body.dataset.activeTab === "buildlogs") {
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
buildRefreshTimer = setTimeout(fetchBuild, 2000);
}
} }

View file

@ -54,7 +54,7 @@ export const form = (
// `asyncBtn` live in `@hive/shared/forms.js`, not here — both the // `asyncBtn` live in `@hive/shared/forms.js`, not here — both the
// dashboard and the per-agent UI import them directly from there. // dashboard and the per-agent UI import them directly from there.
// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` + the `paintAtomic` // `truncate`, `fmtAgo`, `fmtElapsed` + the `paintAtomic`
// render helper live in the dashboard-internal `./util.js`, not here — // render helper live in the dashboard-internal `./util.js`, not here —
// their phrasing ("X running", "X ago") is dashboard-specific, so they // their phrasing ("X running", "X ago") is dashboard-specific, so they
// stay out of the cross-page `common.js` but are shared across the // stay out of the cross-page `common.js` but are shared across the
@ -287,58 +287,6 @@ export function openStream(url) {
return target; return target;
} }
// Stream a build log into a <pre>, returning the EventSource. Used by the
// BUILD L0GS tab. 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 <pre> 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 <pre> to avoid duplicated output and wait.
if (es.readyState === EventSource.CONNECTING) {
pre.textContent = "";
stderrSeen = false;
return;
}
es.close();
if (onError) onError();
};
return es;
}
// ─── sticky top region ─────────────────────────────────────────────── // ─── sticky top region ───────────────────────────────────────────────
// One sticky container wrapping the page's chrome (tab bar / page // One sticky container wrapping the page's chrome (tab bar / page
// header), built once by wrapping the existing chrome element. A page // header), built once by wrapping the existing chrome element. A page

View file

@ -604,120 +604,3 @@ body.dashboard-shell.has-selection {
/* ST4TS moved to its own page (`/stats.html`): the /* ST4TS moved to its own page (`/stats.html`): the
window selector / summary chips / bars live in stats.css, and the window selector / summary chips / bars live in stats.css, and the
shared `.hive-stats-table` moved to common.css. */ shared `.hive-stats-table` moved to common.css. */
/* ─── build tabs (builds.js) ────────────────────────────────────────
R3BU1LD QU3U3 is styled by system-sections.css and
jobq-graph.css; the BUILD L0GS list rules are below. */
.logs-toolbar {
display: flex;
gap: 0.6em;
align-items: center;
margin-bottom: 0.9em;
flex-wrap: wrap;
}
/* ─── build-log list ──────────────────────────────────────────────
Scrollable list of log header rows; clicking a row expands full
stdout+stderr inline. */
.build-logs-list {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
flex: 1 1 0;
min-height: 0;
}
.build-logs-item {
border-bottom: 1px solid var(--border);
padding: 0.15em 0;
}
.build-logs-row-btn {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
align-items: baseline;
width: 100%;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 0.85em;
text-align: left;
padding: 0.25em 0.4em;
cursor: pointer;
border-radius: 3px;
}
.build-logs-row-btn:hover {
background: var(--bg-elev);
}
.build-logs-kind {
font-weight: 600;
}
.build-logs-age {
font-size: 0.88em;
}
.build-logs-cmdline {
color: var(--muted);
font-size: 0.82em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 40em;
}
.build-logs-detail {
padding: 0 0.4em 0.4em;
}
.build-logs-output {
margin: 0.3em 0 0;
padding: 0.4em 0.6em;
background: var(--crust);
color: var(--fg);
border: 1px solid var(--purple-dim);
font-size: 0.82em;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
max-height: 30em;
overflow-y: auto;
}
.build-logs-dl {
color: var(--muted);
font-size: 0.8em;
text-decoration: none;
border: 1px solid var(--purple-dim);
padding: 0.1em 0.4em;
border-radius: 2px;
margin-top: 0.3em;
display: none;
}
.build-logs-dl:not([hidden]) {
display: inline-block;
}
.build-logs-dl:hover {
color: var(--fg);
border-color: var(--purple-dim);
}
.build-logs-live-badge {
margin-bottom: 0.4em;
}
@keyframes live-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
.build-logs-live-badge.badge-running {
animation: live-pulse 1.4s ease-in-out infinite;
}
.build-logs-runtime {
font-size: 0.85em;
color: var(--muted);
}

View file

@ -42,16 +42,6 @@
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span> <span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
</a> </a>
<a
class="tab"
id="tab-buildlogs"
href="#buildlogs"
role="tab"
aria-controls="tab-pane-buildlogs"
data-tab="buildlogs"
>
<span class="tab-label">◆ BUILD L0GS ◆</span>
</a>
<a <a
class="tab" class="tab"
id="tab-kept" id="tab-kept"
@ -137,35 +127,6 @@
<div id="rebuild-queue-section"> <div id="rebuild-queue-section">
<p class="meta">loading…</p> <p class="meta">loading…</p>
</div> </div>
<!-- Live build log of the currently-running rebuild (one runs at a
time). Managed by renderRebuildLiveLog in builds.js, separate from
rebuild-queue-section so the queue's row re-render never disturbs
the open poll. -->
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
</section>
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
stdout + stderr. Live builds stream in real time. Lazy-loaded the
first time the tab is shown; auto-refreshes when
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->
<section
class="tab-pane"
id="tab-pane-buildlogs"
data-tab-pane="buildlogs"
hidden
role="tabpanel"
aria-labelledby="tab-buildlogs"
>
<p class="meta">
all-agents build log history. click a row to expand stdout + stderr.
live builds stream in real time.
</p>
<div class="logs-toolbar">
<button type="button" class="btn btn-restart" id="build-refresh">
↻ refresh
</button>
</div>
<div id="build-list"><p class="meta">loading…</p></div>
</section> </section>
<!-- K3PT ST4T3: kept state from agents whose container is absent, <!-- K3PT ST4T3: kept state from agents whose container is absent,

View file

@ -27,80 +27,6 @@
color: var(--amber); color: var(--amber);
font-weight: bold; font-weight: bold;
} }
/* ─── running-rebuild live log (renderRebuildLiveLog in builds.js) ──────────
Streams the currently-running rebuild's build log inline under the queue.
Hidden (native `hidden` attr) when nothing is building. Used on the
dashboard's R3BU1LD QU3U3 tab. */
.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);
}
/* Remove the now-dead `.rqe-step` rule (the `step` sub-step label was /* Remove the now-dead `.rqe-step` rule (the `step` sub-step label was
retired from the wire in `feat(job_queue): retire the now-off-wire step retired from the wire in `feat(job_queue): retire the now-off-wire step

View file

@ -24,11 +24,7 @@ import {
renderContainers, renderContainers,
renderSelectionBar, renderSelectionBar,
} from "./agents.js"; } from "./agents.js";
import { import { initBuilds, onRebuildQueueChanged } from "./builds.js";
initBuilds,
onBuildsTabShown,
onRebuildQueueChanged,
} from "./builds.js";
import { import {
applyTombstonesChanged, applyTombstonesChanged,
renderKept, renderKept,
@ -260,7 +256,7 @@ import { renderUserMenu } from "./user-menu.js";
})(); })();
// ─── tab routing ─────────────────────────────────────────────────────── // ─── tab routing ───────────────────────────────────────────────────────
// Hash-based: `#agents` / `#rebuild` / `#buildlogs` / `#kept` // Hash-based: `#agents` / `#rebuild` / `#kept`
// activate the matching pane on the dashboard. Empty // activate the matching pane on the dashboard. Empty
// hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page // hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page
// (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing // (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing
@ -276,7 +272,6 @@ import { renderUserMenu } from "./user-menu.js";
// visibility (bar only belongs on 4G3NTS where agent cards live). // visibility (bar only belongs on 4G3NTS where agent cards live).
document.body.dataset.activeTab = target; document.body.dataset.activeTab = target;
renderSelectionBar(Array.from(containersState.values())); renderSelectionBar(Array.from(containersState.values()));
onBuildsTabShown(target);
} }
// Wire the shared tab strip now that activateTab + the lazy-load fns it // Wire the shared tab strip now that activateTab + the lazy-load fns it

View file

@ -54,16 +54,3 @@ export function fmtElapsed(secs) {
"m running" "m running"
); );
} }
// Compact duration label, two units deep ("1h 5m", "2d 3h").
export function fmtDuration(secs) {
if (secs < 60) return secs + "s";
if (secs < 3600) return Math.floor(secs / 60) + "m " + (secs % 60) + "s";
if (secs < 86400)
return (
Math.floor(secs / 3600) + "h " + Math.floor((secs % 3600) / 60) + "m"
);
return (
Math.floor(secs / 86400) + "d " + Math.floor((secs % 86400) / 3600) + "h"
);
}