diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index 033fa629..2483fdb4 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -20,9 +20,8 @@ page with four tabs: - **4G3NTS** — every agent, live. This is the default tab and the one you'll check most. -- **R3BU1LD QU3U3** and **BUILD L0GS** — the build - lifecycle: the rebuild queue with a live log of whatever is building, - and the build log history. +- **R3BU1LD QU3U3** — the rebuild queue. A build's output and history are on the + swarm's `builds` Grafana board. - **K3PT ST4T3** — kept state from agents with no container (tombstones), with a purge button per entry. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 1a08b36f..4329234c 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -11,7 +11,7 @@ matrix accounts, the controller's job graph — are on the hive-c0re serves the dashboard at `/`. It has a fixed chrome header at the top and a `
` that shows exactly 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 is a standalone page reached from the pages menu, not from the 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 popover has no links. - **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, - `◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS - lives on its own page, reached from the pages menu. Count pills on - 4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and - K3PT ST4T3 (kept-state entries). + `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS + lives on its own page, reached from the pages menu. - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`) — sits below the tab strip. - **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 / snapshot-storage rework where the problem changes shape. -## Build tabs (`#rebuild`, `#buildlogs`) +## Build tab (`#rebuild`) -The build lifecycle — rebuild queue, live build log, and -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 +The rebuild queue is the **R3BU1LD QU3U3** tab (`#rebuild`). It lives in `builds.js`, which `tabs.js` imports: the dashboard's one tab strip, `/api/state` snapshot and `/api/dashboard/stream` subscription serve them, with `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 `GET /api/jobq/graph?states=` rather than 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** 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 @@ -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 compensate. -Below the queue, a **live build-log panel** (`#rebuild-live-log`, -`renderRebuildLiveLog`) shows the currently running rebuild's output -inline — collapsible, with a live/ok/fail badge and a `↓ raw` -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. +A build's output and history aren't on this page. hive-c0re logs every container build +to the journal, and the swarm's `builds` Grafana board reads those lines: the builds with +their outcome and runtime, and the output of the one you pick. ## 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). - `GET /api/build-logs/id/{id}/stream` — live SSE stream of 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. - `GET /api/build-logs/id/{id}/raw` — raw concatenated - stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download - raw` links in the build tabs. Works on both finished and in-progress + stdout+stderr as `text/plain`. Works on both finished and in-progress build logs. - `GET /api/reminders` — list pending reminders for the dashboard's queued-reminders panel. diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js index bd61b712..183ed3c4 100644 --- a/frontend/packages/dashboard/src/builds.js +++ b/frontend/packages/dashboard/src/builds.js @@ -1,24 +1,18 @@ -// The build lifecycle tabs of the dashboard — rebuild queue, live build -// log, and build log history. Not an entry point: tabs.js -// imports it, owns the tab strip and the dashboard event stream, and calls -// the exports below. Two tabs, the panes' ids matching the tab ids: -// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log -// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded -// the first time the tab is shown +// The rebuild queue tab of the dashboard (R3BU1LD QU3U3): pending and running +// rebuilds as a `JobqGraph`. Not an entry point: tabs.js imports it, owns the +// tab strip and the dashboard event stream, and calls the exports below. The +// builds' output and history are not here: they are the `builds` Grafana board, +// from the lines hive-c0re logs for each build. -import { $, fmtAgeSecs, openBuildLogStream } from "./common.js"; -import { el } from "@hive/shared/dom.js"; +import { $ } from "./common.js"; import { themedConfirm } from "@hive/shared/modal.js"; import { h, render } from "preact"; -import { fmtDuration } from "./util.js"; import { JobqGraph } from "@hive/shared/jobq-graph.js"; -// ─── derived state ─────────────────────────────────────────────────────────── - -// Kept in sync from JobqGraph's `onUpdate` callback (see mountRebuildQueue -// below) — the component owns fetching GET /api/jobq/graph and renders -// the tree itself; this flat array exists only for the two things it -// doesn't render: the count-pill and the live-log panel. +// Kept in sync from JobqGraph's `onUpdate` callback (see renderRebuildQueue +// below) — the component owns fetching GET /api/jobq/graph and renders the tree +// itself; this flat array is only for `onCancel`, which names the node it +// cancels in its confirm. let jobqNodes = []; let jobqGraphToken = 0; @@ -29,8 +23,7 @@ let jobqGraphToken = 0; // no hand-rolled tree/roll-up rendering here anymore. The component owns // fetching GET /api/jobq/graph and its own refetch (bump `jobqGraphToken` // and render again); this page just reads its `onUpdate` callback to keep -// `jobqNodes` (the flat array) in sync for the two things the generic -// view doesn't render itself: the count-pill and the live-log panel below. +// `jobqNodes` (the flat array) in sync for `onCancel` below. // // 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 @@ -60,7 +53,6 @@ function renderRebuildQueue() { refreshToken: jobqGraphToken, onUpdate: (nodes) => { jobqNodes = nodes || []; - renderRebuildLiveLog(); }, onCancel: async (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/ (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 ───────────────────────────────────────────────── // Everything below is called by tabs.js, which owns the tab strip, the // `/api/state` snapshot and the dashboard event stream. -/** Mount the rebuild queue and wire the build-log refresh button. Called - * once, after the tab strip exists. The queue self-fetches on mount - * (JobqGraph's own effect). */ +/** Mount the rebuild queue. Called once, after the tab strip exists. The queue + * self-fetches on mount (JobqGraph's own effect). */ export function initBuilds() { - const refresh = $("build-refresh"); - if (refresh) refresh.addEventListener("click", fetchBuild); 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` * owns the actual fetch. */ export function onRebuildQueueChanged() { jobqGraphToken += 1; 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); - } } diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index b4b82031..6f28f8fb 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -54,7 +54,7 @@ export const form = ( // `asyncBtn` live in `@hive/shared/forms.js`, not here — both the // 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 — // their phrasing ("X running", "X ago") is dashboard-specific, so they // stay out of the cross-page `common.js` but are shared across the @@ -287,58 +287,6 @@ export function openStream(url) { return target; } -// Stream a build log into a
, 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 
 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;
-}
-
 // ─── sticky top region ───────────────────────────────────────────────
 // One sticky container wrapping the page's chrome (tab bar / page
 // header), built once by wrapping the existing chrome element. A page
diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css
index 2a4a0fd8..cd005b55 100644
--- a/frontend/packages/dashboard/src/dashboard.css
+++ b/frontend/packages/dashboard/src/dashboard.css
@@ -604,120 +604,3 @@ body.dashboard-shell.has-selection {
 /* ST4TS moved to its own page (`/stats.html`): the
    window selector / summary chips / bars live in stats.css, and the
    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);
-}
diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html
index 1092e294..75781aad 100644
--- a/frontend/packages/dashboard/src/index.html
+++ b/frontend/packages/dashboard/src/index.html
@@ -42,16 +42,6 @@
           ◆ R3BU1LD QU3U3 ◆
         
 
-        
-          ◆ BUILD L0GS ◆
-        
         
             

loading…

- - - - - -