Watch
0
0
Fork
You've already forked hyperhive
0

dashboard: the build pages are tabs of the dashboard

The build lifecycle used to be its own page with three sub-tabs. They are
now dashboard tabs (rebuild, meta, buildlogs) beside SW4RM and
P3RM1SS10NS: builds.js is a module tabs.js imports, so the dashboard's one
tab strip, /api/state snapshot and event stream (plus the meta_inputs_changed
and meta_update_running kinds) serve them, and the page, its bundle entry,
stylesheet and home tile are gone. The SW4RM banner's "view queue" link
points at the #rebuild tab, and the rebuild tab carries a count pill of the
rebuilds in flight.

An empty tab-count pill drew a "0": its display beat the UA [hidden] rule.
This commit is contained in:
iris 2026-10-14 20:18:16 +02:00 • committed by mara
commit ee5c057d8f
22 changed files with 386 additions and 456 deletions

View file

@ -33,7 +33,7 @@ swarm controller and hive-c0re each build their own graph on it.
Two views, one per graph, drawn by the same component: the swarm UI's
`/jobs` page shows the swarm controller's graph, and the hive dashboard's
**BU1LDS** page (R3BU1LD QU3U3 — see
**R3BU1LD QU3U3** tab (see
[`web-ui/dashboard.md`](../web-ui/dashboard.md)) shows that hive's. Each
**row** is one job; the rows nested under it are that job's steps, in
order (occasionally a couple run side by side). A step shows one of:

View file

@ -11,21 +11,23 @@ what you actually do here.
## Where things are
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
linking to every surface (Dashboard, Flow, Logs, Builds, Stats,
linking to every surface (Dashboard, Flow, Logs, Stats,
Settings, Core). Every page links back to H0M3, so you're
never more than one step from the hub.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
of your time. It's a single page with exactly two tabs:
of your time. It's a single page with five tabs:
- **SW4RM** — every agent, live. This is the default tab and the one
you'll check most.
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
lifecycle: the rebuild queue with a live log of whatever is building,
the meta flake inputs you can bump, and the build log history.
- **P3RM1SS10NS** — what tools and system-level access each agent has.
Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats),
**Builds** (`/builds.html`, the rebuild queue and build history), and
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and
**Core** (`/core.html`, tombstones and container resource use).
Each agent also has its own page — a full terminal view of that
@ -52,7 +54,7 @@ per-agent `⋮` menu on a single row. Rebuilding re-applies that agent's
current config; use it whenever an agent
shows as "needs update."
**Watch a build.** BU1LDS shows the rebuild queue live, plus a
**Watch a build.** The dashboard's R3BU1LD QU3U3 tab shows the rebuild queue live, plus a
streaming log of whatever's currently building. Useful right after
merging a config change or bumping a flake input.

View file

@ -10,8 +10,8 @@ matrix accounts, the controller's job graph — are on the
hive-c0re serves the dashboard at `/dashboard.html` (with the home page at `/`).
It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#swarm`,
`#permissions`) drives which pane is
one tab pane at a time. The URL hash (`#swarm`, `#rebuild`, `#meta`,
`#buildlogs`, `#permissions`) drives which pane is
active; hash changes don't reload the page. FL0W and the other
standalone pages are reachable from the H0M3 hub at `/`, not
from the dashboard tab strip.
@ -19,17 +19,18 @@ from the dashboard tab strip.
**Chrome header** (fixed, overlays the active tab pane):
- **← home back-link**: top-left of the chrome, links to the H0M3 hub
at `/`. Every surface links back to the hub rather than to each other.
- **Tab strip**: `◆ SW4RM ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only —
- **Tab strip**: `◆ SW4RM ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
`◆ BUILD L0GS ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only —
the SYST3M panels live on the
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
live on their own pages too, all reachable from the
H0M3 hub (not the tab strip). Count pill on SW4RM
(container count).
H0M3 hub (not the tab strip). Count pills on SW4RM
(stale containers) and R3BU1LD QU3U3 (rebuilds in flight).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
on **every** page (dashboard + every stand-alone page — FL0W,
H0M3, C0R3, BU1LDS, ST4TS), injected at the top
H0M3, C0R3, ST4TS), injected at the top
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
`state.server_warnings` — a list of `{ kind, level, message }` — and
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the
@ -73,13 +74,13 @@ agent is stale.
Passive / rare-interaction state, reached from the **Core** tile on the
H0M3 hub (served at
`/core.html`), with the same minimal chrome as `/builds.html`: a `← home`
back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild
bundle (`core.js`) that cold-loads `/api/state` and subscribes to
`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`,
and `tool_groups_changed` (the latter two re-render the stale-perms
sub-section when permission data changes). (Rebuild queue and meta inputs
live on the BU1LDS page — see below.)
`/core.html`), with minimal chrome: a `← home` back-link and a single pane,
**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed`
(the latter two re-render the stale-perms sub-section when permission
data changes). (Rebuild queue and meta inputs are dashboard tabs — see
below.)
Per-container CPU, memory and disk load isn't shown in the hive UI. The
same figures go out as OTEL metrics and appear in Grafana (see
@ -111,21 +112,23 @@ autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE
events. Each ghost agent gets a `✕ clear perms` button
(`DELETE /api/permissions/{name}`).
## BU1LDS page (`/builds.html`)
## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
The build lifecycle hub — rebuild queue, live build log, meta inputs,
and build log history on one page. Standalone page reached from the
**Builds** tile on the H0M3 hub, same minimal chrome as `/core.html`.
Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**,
**BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads
`/api/state` and subscribes to `/api/dashboard/stream` for
`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`.
The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at
all, by design — the swarm view stays independent of job-queue
internals. Its per-agent pending badges are transient-only, and
its queue-summary banner reads the much narrower `GET /api/jobq/rollup`
instead (see Container row, below) — a handful of pre-tallied counts,
not the graph.
The build lifecycle — rebuild queue, live build log, meta inputs, and
build log history — is three tabs of the dashboard: **R3BU1LD QU3U3**
(`#rebuild`), **M3T4 1NPUTS** (`#meta`) and **BUILD L0GS**
(`#buildlogs`). They live 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`, `meta_inputs_changed` and `meta_update_running`
on the stream alongside the dashboard's other kinds. The count pill on
R3BU1LD QU3U3 is the number of rebuilds in flight.
The SW4RM tab does **not** read the graph endpoint, by design — the swarm
view stays independent of job-queue internals. Its per-agent pending
badges are transient-only, and its queue-summary banner reads the much
narrower `GET /api/jobq/rollup` instead (see Container row, below) — a
handful of pre-tallied counts, not the graph. The banner's "view queue →"
link goes to the `#rebuild` tab.
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
operations: rebuilds, meta-update cascades, and first-spawns. One
@ -394,7 +397,7 @@ re-renders the terminal row. The operator addresses the root agent as `@root`.
## H0M3 page (`/`)
The H0M3 hub is the primary landing page (served at `/` by default). A
responsive grid of link tiles — Dashboard, Flow, Builds, Stats,
responsive grid of link tiles — Dashboard, Flow, Stats,
Core, API — each pointing to their respective
surfaces, all unconditionally shown (no gating). The page is a pure
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
@ -580,7 +583,7 @@ agent is stale. Banner pulses on each broker SSE event
**Build-queue summary banner** — when the job queue has any active
work, a compact amber banner sits above the container list: `◐ build
queue — N running · M queued — view queue →` (the link goes to the
BU1LDS page's R3BU1LD QU3U3). The shared `JobqRollup` Preact component
R3BU1LD QU3U3 tab). The shared `JobqRollup` Preact component
(`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
mounts, pointed at swarm-controller's own rollup endpoint instead),
mounted once into `#jobq-rollup-section` by `swarm.js::initJobqRollup`
@ -711,11 +714,11 @@ 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 BU1LDS. The
frames). Used by the expanding live-row view in the BUILD L0GS tab. 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 BU1LDS. Works on both finished and in-progress
raw` links in the build tabs. Works on both finished and in-progress
build logs.
- `GET /static/marked.js` serves the vendored `marked` bundle used
for markdown previews.
@ -854,10 +857,10 @@ payload):
full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes.
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
snapshot of `meta/flake.lock` inputs. BU1LDS M3T4 1NPUTS tab
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
subscribes; same shape as `GET /api/meta-inputs`.
- `meta_update_running` (running: bool) — emitted when a
`nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS
`nix flake update` ripple starts or completes. The M3T4 1NPUTS
tab uses this to show/hide the "⏳ meta-update running" banner.
<!-- vale write-good.Passive = NO -->

View file

@ -20,7 +20,7 @@
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `home.css` /
`stats.css` / `core.css` / `builds.css`);
`stats.css` / `core.css`);
`common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`pill.css` via esbuild's `@import` resolution.

View file

@ -30,7 +30,7 @@ layers per-agent additions on top of the default agent dist.
output is reproducible without network access at build time.
- **esbuild**: `agent` and `swarm-ui` are Preact apps; `dashboard` is a
vanilla-JS + custom-element MPA with a few Preact-rendered pages
(`builds.js`, `swarm.js`); `shared` holds both kinds of module. No
(`builds.js`, `swarm.js`, imported by the dashboard entry); `shared` holds both kinds of module. No
framework runtime beyond Preact where it's used. Each workspace's
`build.mjs` is a short esbuild script.
- **Single-PR migration**: see the tracked design proposal and

View file

@ -1,5 +1,5 @@
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at
// `/`, dashboard/flow/core/builds at their own
// `/`, dashboard/flow/core/stats at their own
// `.html`), each named after its own entry point below — see the
// `entryPoints`/`for` lists for the exact map, not repeated here to
// avoid this comment drifting out of sync with the real build steps.
@ -38,7 +38,6 @@ await build({
src("home.js"),
src("stats.js"),
src("core.js"),
src("builds.js"),
],
outdir: staticDir(""),
bundle: true,
@ -57,7 +56,7 @@ await build({
// runs as its own esbuild invocation over different entry points.
loader: { ".css": "text" },
// `@hive/shared/jobq-graph.js` resolves to a real `.jsx` file
// (`JobqGraph.jsx`), pulled in transitively by `builds.js` — esbuild
// (`JobqGraph.jsx`), pulled in transitively by `tabs.js` via `builds.js` — esbuild
// already picks the `jsx` loader for `.jsx` by extension, this just
// sets the transform mode to match swarm-ui's (which also authors
// this file). No other entry here uses JSX today; this doesn't turn
@ -103,7 +102,6 @@ for (const entry of [
"home.css",
"stats.css",
"core.css",
"builds.css",
]) {
await build({
entryPoints: [src(entry)],
@ -120,7 +118,6 @@ for (const html of [
"flow.html",
"stats.html",
"core.html",
"builds.html",
]) {
copyFileSync(src(html), dist(html));
}

View file

@ -1,139 +0,0 @@
/* ─── /builds.html — BU1LDS page ────────────────────────────────────
The build lifecycle hub: rebuild queue + live log, meta inputs, and
build log history on one page.
Imports system-sections.css for rebuild queue + meta inputs + live-log
component styles; the build-logs-* rules for the BUILD L0GS list are
below. */
@import "./system-sections.css";
@import "@hive/shared/jobq-graph.css";
body.builds-shell {
margin: 0;
padding: 0;
}
.builds-tabbar {
flex: 1;
}
.builds-main {
padding: 1.2em 1.5em 2em;
}
.builds-pane[hidden] {
display: none;
}
.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

@ -1,101 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>hyperhive // BU1LDS</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/static/colors.css" />
<link rel="stylesheet" href="/static/theme.css" />
<link rel="stylesheet" href="/static/common.css" />
<link rel="stylesheet" href="/static/builds.css" />
</head>
<body class="builds-shell">
<!-- BU1LDS: the build lifecycle hub — rebuild queue, live build log,
meta inputs, and build log history on one page. Same minimal
chrome as /core.html — a `← home` back-link + a <hive-tab-strip>
sub-tab nav. Default sub-tab is the rebuild queue. -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<hive-tab-strip
class="hive-tabbar builds-tabbar"
id="builds-tabbar"
prefix="builds"
role="tablist"
></hive-tab-strip>
</header>
<main class="builds-main">
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
first-spawns. Rendered from GET /api/jobq/graph by the
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
`rebuild_queue_changed` over /api/dashboard/stream is the
refresh trigger and carries no payload of its own. Default tab. -->
<section
class="builds-pane"
id="builds-pane-rebuild"
data-tab-pane="rebuild"
role="tabpanel"
aria-labelledby="builds-tab-rebuild"
>
<p class="meta">
pending + running rebuilds, meta-updates, and first-spawns. one runs
at a time; meta-update cascades nest under their parent. dedup:
re-enqueueing a still-queued op collapses into the existing entry.
</p>
<div id="rebuild-queue-section">
<p class="meta">loading…</p>
</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 SSE stream. -->
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
</section>
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
<section
class="builds-pane"
id="builds-pane-meta"
data-tab-pane="meta"
role="tabpanel"
aria-labelledby="builds-tab-meta"
>
<p class="meta">
select inputs to <code>nix flake update</code> in <code>/meta/</code>.
selected agents rebuild in sequence after the lock bump; the
submitting agent learns each outcome via the usual
<code>rebuilt</code> system event.
</p>
<div id="meta-inputs-section">
<p class="meta">loading…</p>
</div>
</section>
<!-- BUILD L0GS: all-agents build log history.
Click a row to expand stdout + stderr. Live builds stream in real
time. Lazy-loaded on first tab activation; auto-refreshes when
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->
<section
class="builds-pane"
id="builds-pane-buildlogs"
data-tab-pane="buildlogs"
role="tabpanel"
aria-labelledby="builds-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>
</main>
<script type="module" src="/static/builds.js" defer></script>
</body>
</html>

View file

@ -1,23 +1,17 @@
// BU1LDS page entry (/builds.html). The build lifecycle hub — rebuild
// queue, live build log, meta inputs, and build log history on one page.
// Three sub-tabs via <hive-tab-strip> (default = Rebuild Queue):
// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline
// - M3T4 1NPUTS: nix flake input selector + meta-update trigger
// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show)
// The build lifecycle tabs of /dashboard.html — rebuild queue, live build
// log, meta inputs, 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. Three tabs, the panes' ids matching the tab ids:
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
// - meta (M3T4 1NPUTS): nix flake input selector + meta-update
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
// the first time the tab is shown
import {
$,
fmtAgeSecs,
openStream,
openBuildLogStream,
initServerWarnings,
} from "./common.js";
import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { bindAsyncForms } from "@hive/shared/forms.js";
import { themedConfirm } from "@hive/shared/modal.js";
import { h, render } from "preact";
import { fmtAgo, fmtDuration, truncate } from "./util.js";
import "@hive/shared/hive-tab-strip.js";
import { JobqGraph } from "@hive/shared/jobq-graph.js";
// ─── derived state ───────────────────────────────────────────────────────────
@ -174,7 +168,7 @@ function renderMetaInputs(s) {
// the generic component's.
// Split from `renderRebuildQueue` below: `replaceChildren()` only
// belongs on the *first* render, to clear the static "loading…"
// placeholder `#rebuild-queue-section` starts with in `builds.html` —
// placeholder `#rebuild-queue-section` starts with in `dashboard.html` —
// Preact doesn't know that placeholder is there, so a later render
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
// tracked children out from under its diffing rather than let it
@ -196,7 +190,6 @@ function renderRebuildQueue() {
onUpdate: (nodes) => {
jobqNodes = nodes || [];
renderRebuildLiveLog();
updateRebuildCount();
},
onCancel: async (id) => {
const node = jobqNodes.find((n) => n.id === id);
@ -384,33 +377,18 @@ function renderRebuildLiveLog() {
}
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
function updateRebuildCount() {
const pill = $("builds-tab-count-rebuild");
if (!pill) return;
// Pending/Running/Finishing = in flight (Finishing = own work done, a
// sub-node still running — still counts). Root nodes only: each is one
// queue entry.
const n = jobqNodes.filter(
// The queue's in-flight count, for the R3BU1LD QU3U3 tab's pill (tabs.js
// reads it on its refresh tick). Pending/Running/Finishing = in flight
// (Finishing = own work done, a sub-node still running — still counts).
// Root nodes only: each is one queue entry.
export function rebuildCount() {
return jobqNodes.filter(
(n) =>
n.parent == null &&
(n.state === "Pending" ||
n.state === "Running" ||
n.state === "Finishing"),
).length;
if (n > 0) {
pill.textContent = String(n);
pill.hidden = false;
} else {
pill.hidden = true;
}
}
// ─── render-all (cold load + any full re-render) ──────────────────────────────
function renderAll() {
renderMetaInputs({ meta_inputs: metaInputsState });
// Rebuild-queue rendering + the count-pill/live-log it drives all happen
// off JobqGraph's own onUpdate callback (see mountRebuildQueue) —
// nothing to render here directly.
}
// ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
@ -419,10 +397,7 @@ function renderAll() {
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
// Deep-link: ?id=N#buildlogs auto-expands the target row.
const buildList = $("build-list");
const buildRefresh = $("build-refresh");
let buildLogsLoaded = false;
let buildTabs; // set in init
function fmtTs(unixSecs) {
if (!unixSecs) return "";
@ -431,6 +406,7 @@ function fmtTs(unixSecs) {
}
async function fetchBuild() {
const buildList = $("build-list");
if (!buildList) return;
buildList.replaceChildren();
buildList.append(el("p", { class: "meta" }, "loading…"));
@ -612,93 +588,54 @@ async function fetchBuild() {
}
}
if (buildRefresh) buildRefresh.addEventListener("click", fetchBuild);
// ─── 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.
// ─── SSE handlers ─────────────────────────────────────────────────────────────
let buildRefreshTimer = null;
const SSE_HANDLERS = {
rebuild_queue_changed() {
// No `ev.queue` payload read anymore — this event still carries its own
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the
// swarm badges, untouched by this), this page just treats it as a
// refetch trigger. `JobqGraph` owns the actual fetch.
jobqGraphToken += 1;
renderRebuildQueue();
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
if (buildTabs && buildTabs.active() === "buildlogs") {
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
buildRefreshTimer = setTimeout(fetchBuild, 2000);
}
},
meta_inputs_changed(ev) {
metaInputsState = (ev.inputs || []).slice();
renderMetaInputs({ meta_inputs: metaInputsState });
},
meta_update_running(ev) {
metaUpdateRunning = !!ev.running;
renderMetaInputs({ meta_inputs: metaInputsState });
},
};
// ─── boot ─────────────────────────────────────────────────────────────────────
async function refreshState() {
try {
const resp = await fetch("/api/state");
if (resp.ok) syncFromSnapshot(await resp.json());
} catch {
// best-effort
}
renderAll();
}
async function init() {
initServerWarnings();
bindAsyncForms(() => refreshState());
// Self-fetches on mount (JobqGraph's own effect) — no explicit initial
// fetch needed here, unlike meta inputs below.
/** 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), unlike the meta inputs, which wait for the
* snapshot. */
export function initBuilds() {
const refresh = $("build-refresh");
if (refresh) refresh.addEventListener("click", fetchBuild);
mountRebuildQueue();
}
buildTabs = document.getElementById("builds-tabbar").configure({
tabs: [
{
id: "rebuild",
label: "R3BU1LD QU3U3",
badgeId: "builds-tab-count-rebuild",
},
{ id: "meta", label: "M3T4 1NPUTS" },
{ id: "buildlogs", label: "BUILD L0GS" },
],
defaultId: "rebuild",
onShow: (id) => {
if (id === "buildlogs" && !buildLogsLoaded) {
buildLogsLoaded = true;
fetchBuild();
}
},
});
/** The meta inputs, from an `/api/state` snapshot. */
export function syncBuildsFromSnapshot(s) {
syncFromSnapshot(s);
renderMetaInputs({ meta_inputs: metaInputsState });
}
await refreshState();
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
// 13 wire kinds down to the 3 this page acts on. This page was one
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
// (subscription discipline, part 1 of the dashboard-event-stream-
// split issue).
const es = openStream(
"/api/dashboard/stream?kinds=rebuild_queue_changed,meta_inputs_changed,meta_update_running",
);
if (es) {
es.onmessage = (e) => {
let ev;
try {
ev = JSON.parse(e.data);
} catch {
return;
}
const h = SSE_HANDLERS[ev.kind];
if (h) h(ev);
};
/** A tab was shown: BUILD L0GS loads its list the first time. */
export function onBuildsTabShown(tab) {
if (tab === "buildlogs" && !buildLogsLoaded) {
buildLogsLoaded = true;
fetchBuild();
}
}
init();
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);
}
}
export function onMetaInputsChanged(ev) {
metaInputsState = (ev.inputs || []).slice();
renderMetaInputs({ meta_inputs: metaInputsState });
}
export function onMetaUpdateRunning(ev) {
metaUpdateRunning = !!ev.running;
renderMetaInputs({ meta_inputs: metaInputsState });
}

View file

@ -292,7 +292,7 @@ export function openStream(url) {
}
// Stream a build log into a <pre>, returning the EventSource. Used by the
// BU1LDS page's BUILD L0GS tab. Appends `stdout_append` then
// 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

View file

@ -1,7 +1,6 @@
/* ─── /core.html — C0R3 page ───────────────────────────────────────
Kept state (tombstones) and stale permission entries. Section rules
imported from system-sections.css (shared with dashboard.css +
builds.css). */
imported from system-sections.css (shared with dashboard.css). */
@import "./system-sections.css";
body.core-shell {

View file

@ -14,7 +14,8 @@
<!-- C0R3: the host/coordinator surface. Standalone page (served at
/core.html) carved out of the dashboard's old SYST3M tab so the
dashboard tab strip stays lean: kept state (tombstones) and the
stale permission entries. The section <div> ids (tombstones-section,
stale permission entries. Rebuild queue + meta inputs are tabs of
/dashboard.html. The section <div> ids (tombstones-section,
tombstones-stale-perms) match what core.js's renderers target. Hive
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
no dashboard panel — `hivectl stop`/`start`/`restart` is the only

View file

@ -2,7 +2,8 @@
// standalone page so the dashboard tab strip stays lean: kept state
// (tombstones) and the stale permission entries.
//
// This page is its own esbuild bundle with its own runtime: it cold-loads
// Rebuild queue + meta inputs are tabs of /dashboard.html, not here. This
// page is its own esbuild bundle with its own runtime: it cold-loads
// /api/state and subscribes to /dashboard/stream (the same broker event
// channel the dashboard uses), maintaining its own copy of the tombstones
// state.

View file

@ -8,6 +8,7 @@
pulls them in here. */
@import "./system-sections.css";
@import "@hive/shared/jobq-rollup.css";
@import "@hive/shared/jobq-graph.css";
/* ─── tabbed dashboard chrome ──────────────────────────────────────
Top-of-page sticky header with banner + tab strip. SSE stays
@ -125,6 +126,11 @@ body.dashboard-shell {
font-variant-numeric: tabular-nums;
font-weight: bold;
}
/* `display` above beats the UA's `[hidden]` rule, so an empty count (the
SW4RM and R3BU1LD QU3U3 pills at zero) would otherwise draw a "0". */
.tab-count[hidden] {
display: none;
}
/* Tab pane visibility — createTabStrip (@hive/shared/tabs.js) sets the
`hidden` attribute on every inactive pane (resolved from the URL hash;
@ -772,3 +778,120 @@ 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 and M3T4 1NPUTS are 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

@ -37,9 +37,40 @@
</a>
<!-- SYST3M moved to its own standalone page (/core.html, "C0R3"),
reached from the H0M3 hub — keeps the dashboard tab strip lean.
The rebuild-queue STATE still lives in tabs.js (it drives the
"building…" badges on SW4RM agent cards); only the SYST3M tab +
its section renderers moved out. -->
The build lifecycle is three tabs here (builds.js): the rebuild
queue with its live log, the meta flake inputs, and the build log
history. -->
<a
class="tab"
id="tab-rebuild"
href="#rebuild"
role="tab"
aria-controls="tab-pane-rebuild"
data-tab="rebuild"
>
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
<span class="tab-count" id="tab-count-rebuild" hidden></span>
</a>
<a
class="tab"
id="tab-meta"
href="#meta"
role="tab"
aria-controls="tab-pane-meta"
data-tab="meta"
>
<span class="tab-label">◆ M3T4 1NPUTS ◆</span>
</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>
<!-- P3RM1SS10NS: per-agent capability grants and tool-group
assignments. Both tables are fetched on tab activation. -->
<a
@ -102,9 +133,77 @@
</div>
</section>
<!-- SYST3M pane moved to /core.html (the standalone "C0R3" page):
kept state and stale permissions. Reached
from the H0M3 hub. -->
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
first-spawns. Rendered from GET /api/jobq/graph by the
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
`rebuild_queue_changed` over /api/dashboard/stream is the
refresh trigger and carries no payload of its own. -->
<section
class="tab-pane"
id="tab-pane-rebuild"
data-tab-pane="rebuild"
hidden
role="tabpanel"
aria-labelledby="tab-rebuild"
>
<p class="meta">
pending + running rebuilds, meta-updates, and first-spawns. one runs
at a time; meta-update cascades nest under their parent. dedup:
re-enqueueing a still-queued op collapses into the existing entry.
</p>
<div id="rebuild-queue-section">
<p class="meta">loading…</p>
</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>
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
<section
class="tab-pane"
id="tab-pane-meta"
data-tab-pane="meta"
hidden
role="tabpanel"
aria-labelledby="tab-meta"
>
<p class="meta">
select inputs to <code>nix flake update</code> in
<code>/meta/</code>. selected agents rebuild in sequence after the
lock bump; the submitting agent learns each outcome via the usual
<code>rebuilt</code> system event.
</p>
<div id="meta-inputs-section">
<p class="meta">loading…</p>
</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>
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
assignments. Both tables are column-driven from the backend

View file

@ -42,7 +42,7 @@
<span class="home-tile-label">Dashboard</span>
</span>
<span class="home-tile-desc"
>containers · permissions · system</span
>containers · builds · permissions</span
>
</a>
@ -54,16 +54,6 @@
<span class="home-tile-desc">live all-agents message firehose</span>
</a>
<a class="home-tile" href="/builds.html">
<span class="home-tile-head">
<span class="home-tile-icon" aria-hidden="true">🔨</span>
<span class="home-tile-label">Builds</span>
</span>
<span class="home-tile-desc"
>rebuild queue · meta inputs · build logs</span
>
</a>
<a class="home-tile" href="/stats.html">
<span class="home-tile-head">
<span class="home-tile-icon" aria-hidden="true">📊</span>

View file

@ -70,7 +70,7 @@ export function initJobqRollup() {
render(
h(JobqRollup, {
endpoint: "/api/jobq/rollup",
queueHref: "/builds.html",
queueHref: "#rebuild",
refreshToken: jobqRollupToken,
}),
root,
@ -80,8 +80,7 @@ export function initJobqRollup() {
// `rebuild_queue_changed` fires — a payload-less push trigger by
// design, confirmed with atlas on the jobq-deletion tracker: the event
// carries no `queue` field this page reads, same "something changed,
// go refetch" treatment builds.js already gives its own JobqGraph mount
// handle's .refresh().
// go refetch" treatment builds.js gives the R3BU1LD QU3U3 tab's JobqGraph.
export function applyRebuildQueueChanged() {
jobqRollupToken += 1;
initJobqRollup();

View file

@ -1,7 +1,6 @@
/* ─── SYST3M/BU1LDS section panels — shared by dashboard, /core.html,
and /builds.html ──────────────────────────────────────────────────
/* ─── SYST3M/BU1LDS section panels — shared by dashboard and /core.html ───
Rebuild queue, meta inputs and kept-state panels.
Imported by core.css, dashboard.css, and builds.css. */
Imported by core.css and dashboard.css. */
/* ─── meta inputs panel ────────────────────────────────────────────── */
.meta-inputs {
@ -124,9 +123,8 @@
}
/* ─── 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
/builds.html; lives here (not in builds.css) so it can be shared if
the dashboard ever gains an inline live-log view. */
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);

View file

@ -34,6 +34,15 @@ import {
renderContainers,
renderSelectionBar,
} from "./swarm.js";
import {
initBuilds,
onBuildsTabShown,
onMetaInputsChanged,
onMetaUpdateRunning,
onRebuildQueueChanged,
rebuildCount,
syncBuildsFromSnapshot,
} from "./builds.js";
// mdNode (in common.js) reads `window.marked` for the markdown side
// panel preview path. Set it here on the dashboard entry so file
@ -146,6 +155,7 @@ window.marked = marked;
// initJobqRollup mounts once and is a no-op on later calls; the
// mount's own effect handles the actual fetch.
initJobqRollup();
syncBuildsFromSnapshot(s);
renderContainers(s);
restoreOpenDetails(openDetails);
// No periodic refresh timer. Phase 6 covers every container
@ -191,11 +201,15 @@ window.marked = marked;
transient_cleared: applyTransientCleared,
container_state_changed: applyContainerStateChanged,
container_removed: applyContainerRemoved,
// rebuild_queue_changed: refreshes the SW4RM queue-summary banner
// (see swarm.js) — a payload-less push trigger, same treatment
// /builds.html gives it for its JobqGraph mount handle's .refresh()
// (its own separate subscription).
rebuild_queue_changed: applyRebuildQueueChanged,
// rebuild_queue_changed: a payload-less push trigger. Refreshes the
// SW4RM queue-summary banner (see swarm.js) and the R3BU1LD QU3U3
// tab's JobqGraph (see builds.js).
rebuild_queue_changed: () => {
applyRebuildQueueChanged();
onRebuildQueueChanged();
},
meta_inputs_changed: onMetaInputsChanged,
meta_update_running: onMetaUpdateRunning,
capabilities_changed: applyCapabilitiesChanged,
tool_groups_changed: applyToolGroupsChanged,
};
@ -208,11 +222,12 @@ window.marked = marked;
// fallback when SharedWorker isn't supported.
//
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
// only the 7 wire kinds this page acts on.
// only the 9 wire kinds this page acts on.
const es = openStream(
"/api/dashboard/stream?kinds=" +
"transient_set,transient_cleared," +
"container_state_changed,container_removed,rebuild_queue_changed," +
"meta_inputs_changed,meta_update_running," +
"capabilities_changed,tool_groups_changed",
);
es.onmessage = (e) => {
@ -245,8 +260,8 @@ window.marked = marked;
})();
// ─── tab routing ───────────────────────────────────────────────────────
// Hash-based: `#swarm` / `#system` / `#permissions` activate the
// matching pane on the dashboard. Empty
// Hash-based: `#swarm` / `#rebuild` / `#meta` / `#buildlogs` /
// `#permissions` activate the matching pane on the dashboard. Empty
// hash defaults to SW4RM. FL0W and ST4TS are NOT tabs —
// they're separate pages (`/flow.html`, `/stats.html`) reached from
// the H0M3 hub. Tab routing only
@ -272,6 +287,7 @@ window.marked = marked;
fetchAndRenderCapabilities();
fetchAndRenderToolGroups();
}
onBuildsTabShown(target);
}
// Wire the shared tab strip now that activateTab + the lazy-load fns it
@ -279,6 +295,9 @@ window.marked = marked;
// (default SW4RM), toggles the active tab/pane + aria-selected, and
// fires activateTab for the per-tab side-effects on every change.
createTabStrip($("tabbar"), { defaultId: "swarm", onShow: activateTab });
// The rebuild queue mounts now, not on first show, so its tab's count pill
// is live from the start.
initBuilds();
// Tab count pills — pure derived data from the existing state
// stores so SSE-driven updates flow through without extra plumbing.
@ -302,6 +321,8 @@ window.marked = marked;
if (c.needs_update) swarm++;
}
setTabCount("swarm", swarm);
// R3BU1LD QU3U3 — rebuilds in flight.
setTabCount("rebuild", rebuildCount());
}
// Poll the state stores on a 1s tick to keep the pill counts in
// sync. The state stores are mutated synchronously by every SSE

View file

@ -1,8 +1,8 @@
/* ─── page chrome: back-link header ────────────────────────────────
The minimal sticky "frosted bar" header used by every standalone page
reached from the H0M3 hub — /flow, /stats, /core, /builds. A back
link to H0M3 on the left, then either a page title or (on /core and
/builds) a sub-tab nav. The operator dashboard uses its own richer
reached from the H0M3 hub — /flow, /stats, /core. A back
link to H0M3 on the left, then either a page title or (on /core) a
sub-tab nav. The operator dashboard uses its own richer
sticky chrome (`.dashboard-chrome`); these classes cover the simple
pages.

View file

@ -15,7 +15,7 @@
// worth a line of chrome).
//
// `queueHref`, when given, adds a "view queue →" link (dashboard links to
// /builds.html; swarm-ui's /jobs page omits it — a link to the page
// the dashboard's #rebuild tab; swarm-ui's /jobs page omits it — a link to the page
// you're already on is noise).
//
// The glyph carries `.spinner` (`@hive/shared/base.css`, imported by

View file

@ -1,5 +1,5 @@
// hive-tab-strip.js — <hive-tab-strip>, the markup-owning tab-strip custom
// element behind the logs/core/builds sub-page tabbars. Owns
// element behind the logs/core sub-page tabbars. Owns
// rendering the <a class="hive-tab"> markup from a declarative `tabs` list
// instead of every page hand-writing the same <nav><a>...</a></nav>
// boilerplate, then wires the existing createTabStrip() behaviour
@ -21,7 +21,7 @@
// — same id/aria-controls scheme the hand-written markup used, so each
// page's panes (aria-labelledby="${prefix}-tab-${id}") don't need to
// change. A `badgeId` tab also gets a nested `<span class="tab-count"
// id="${badgeId}" hidden>` (matches builds.html's rebuild-queue count
// id="${badgeId}" hidden>` (e.g. a rebuild-queue count
// pill), then wires up createTabStrip() and returns exactly what it
// returns ({ show, active }) — every existing `.active()`/`.show(id)`
// call site keeps working unchanged. Call configure() exactly once per