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 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 `/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 [`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 **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: 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 ## Where things are
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles 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 Settings, Core). Every page links back to H0M3, so you're
never more than one step from the hub. never more than one step from the hub.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most 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 - **SW4RM** — every agent, live. This is the default tab and the one
you'll check most. 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. - **P3RM1SS10NS** — what tools and system-level access each agent has.
Everything else lives on its own page instead, all reachable from the Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and
**Builds** (`/builds.html`, the rebuild queue and build history), and
**Core** (`/core.html`, tombstones and container resource use). **Core** (`/core.html`, tombstones and container resource use).
Each agent also has its own page — a full terminal view of that 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 current config; use it whenever an agent
shows as "needs update." 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 streaming log of whatever's currently building. Useful right after
merging a config change or bumping a flake input. 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 `/`). 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 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`, one tab pane at a time. The URL hash (`#swarm`, `#rebuild`, `#meta`,
`#permissions`) drives which pane is `#buildlogs`, `#permissions`) drives which pane is
active; hash changes don't reload the page. FL0W and the other active; hash changes don't reload the page. FL0W and the other
standalone pages are reachable from the H0M3 hub at `/`, not standalone pages are reachable from the H0M3 hub at `/`, not
from the dashboard tab strip. from the dashboard tab strip.
@ -19,17 +19,18 @@ from the dashboard tab strip.
**Chrome header** (fixed, overlays the active tab pane): **Chrome header** (fixed, overlays the active tab pane):
- **← home back-link**: top-left of the chrome, links to the H0M3 hub - **← 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. 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 the SYST3M panels live on the
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
live on their own pages too, all reachable from the live on their own pages too, all reachable from the
H0M3 hub (not the tab strip). Count pill on SW4RM H0M3 hub (not the tab strip). Count pills on SW4RM
(container count). (stale containers) and R3BU1LD QU3U3 (rebuilds in flight).
- **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.
- **Server-warnings banner** — a generic, sticky top-of-page strip shown - **Server-warnings banner** — a generic, sticky top-of-page strip shown
on **every** page (dashboard + every stand-alone page — FL0W, 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 of `<body>` by `renderServerWarnings` in `common.js`. Driven by
`state.server_warnings` — a list of `{ kind, level, message }` — and `state.server_warnings` — a list of `{ kind, level, message }` — and
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the 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 Passive / rare-interaction state, reached from the **Core** tile on the
H0M3 hub (served at H0M3 hub (served at
`/core.html`), with the same minimal chrome as `/builds.html`: a `← home` `/core.html`), with minimal chrome: a `← home` back-link and a single pane,
back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild **K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
bundle (`core.js`) that cold-loads `/api/state` and subscribes to cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`, `tombstones_changed`, `capabilities_changed`, and `tool_groups_changed`
and `tool_groups_changed` (the latter two re-render the stale-perms (the latter two re-render the stale-perms sub-section when permission
sub-section when permission data changes). (Rebuild queue and meta inputs data changes). (Rebuild queue and meta inputs are dashboard tabs — see
live on the BU1LDS page — see below.) below.)
Per-container CPU, memory and disk load isn't shown in the hive UI. The 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 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 events. Each ghost agent gets a `✕ clear perms` button
(`DELETE /api/permissions/{name}`). (`DELETE /api/permissions/{name}`).
## BU1LDS page (`/builds.html`) ## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
The build lifecycle hub — rebuild queue, live build log, meta inputs, The build lifecycle — rebuild queue, live build log, meta inputs, and
and build log history on one page. Standalone page reached from the build log history — is three tabs of the dashboard: **R3BU1LD QU3U3**
**Builds** tile on the H0M3 hub, same minimal chrome as `/core.html`. (`#rebuild`), **M3T4 1NPUTS** (`#meta`) and **BUILD L0GS**
Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**, (`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the
**BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads dashboard's one tab strip, `/api/state` snapshot and
`/api/state` and subscribes to `/api/dashboard/stream` for `/api/dashboard/stream` subscription serve them, with
`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. `rebuild_queue_changed`, `meta_inputs_changed` and `meta_update_running`
The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at on the stream alongside the dashboard's other kinds. The count pill on
all, by design — the swarm view stays independent of job-queue R3BU1LD QU3U3 is the number of rebuilds in flight.
internals. Its per-agent pending badges are transient-only, and The SW4RM tab does **not** read the graph endpoint, by design — the swarm
its queue-summary banner reads the much narrower `GET /api/jobq/rollup` view stays independent of job-queue internals. Its per-agent pending
instead (see Container row, below) — a handful of pre-tallied counts, badges are transient-only, and its queue-summary banner reads the much
not the graph. 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 **R3BU1LD QU3U3** — pending, in-flight, and recently settled container
operations: rebuilds, meta-update cascades, and first-spawns. One 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 (`/`) ## H0M3 page (`/`)
The H0M3 hub is the primary landing page (served at `/` by default). A 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 Core, API — each pointing to their respective
surfaces, all unconditionally shown (no gating). The page is a pure surfaces, all unconditionally shown (no gating). The page is a pure
portal with no tab-bar or SSE subscriptions. Typography + colours inherit 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 **Build-queue summary banner** — when the job queue has any active
work, a compact amber banner sits above the container list: `◐ build work, a compact amber banner sits above the container list: `◐ build
queue — N running · M queued — view queue →` (the link goes to the 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 (`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
mounts, pointed at swarm-controller's own rollup endpoint instead), mounts, pointed at swarm-controller's own rollup endpoint instead),
mounted once into `#jobq-rollup-section` by `swarm.js::initJobqRollup` 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). 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 BU1LDS. The frames). Used by the expanding live-row view in the BUILD L0GS tab. 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`. 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. build logs.
- `GET /static/marked.js` serves the vendored `marked` bundle used - `GET /static/marked.js` serves the vendored `marked` bundle used
for markdown previews. for markdown previews.
@ -854,10 +857,10 @@ payload):
full snapshot of destroyed-but-state-kept agents. Emitted on full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes. destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes.
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full - `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`. subscribes; same shape as `GET /api/meta-inputs`.
- `meta_update_running` (running: bool) — emitted when a - `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. tab uses this to show/hide the "⏳ meta-update running" banner.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->

View file

@ -20,7 +20,7 @@
CSS bundle per page (`colors.css` + `theme.css` + `common.css`, CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle — loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `home.css` / `dashboard.css` / `flow.css` / `home.css` /
`stats.css` / `core.css` / `builds.css`); `stats.css` / `core.css`);
`common.css` inlines `@hive/shared`'s `common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` + `base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`pill.css` via esbuild's `@import` resolution. `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. output is reproducible without network access at build time.
- **esbuild**: `agent` and `swarm-ui` are Preact apps; `dashboard` is a - **esbuild**: `agent` and `swarm-ui` are Preact apps; `dashboard` is a
vanilla-JS + custom-element MPA with a few Preact-rendered pages 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 framework runtime beyond Preact where it's used. Each workspace's
`build.mjs` is a short esbuild script. `build.mjs` is a short esbuild script.
- **Single-PR migration**: see the tracked design proposal and - **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 // 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 // `.html`), each named after its own entry point below — see the
// `entryPoints`/`for` lists for the exact map, not repeated here to // `entryPoints`/`for` lists for the exact map, not repeated here to
// avoid this comment drifting out of sync with the real build steps. // avoid this comment drifting out of sync with the real build steps.
@ -38,7 +38,6 @@ await build({
src("home.js"), src("home.js"),
src("stats.js"), src("stats.js"),
src("core.js"), src("core.js"),
src("builds.js"),
], ],
outdir: staticDir(""), outdir: staticDir(""),
bundle: true, bundle: true,
@ -57,7 +56,7 @@ await build({
// runs as its own esbuild invocation over different entry points. // runs as its own esbuild invocation over different entry points.
loader: { ".css": "text" }, loader: { ".css": "text" },
// `@hive/shared/jobq-graph.js` resolves to a real `.jsx` file // `@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 // already picks the `jsx` loader for `.jsx` by extension, this just
// sets the transform mode to match swarm-ui's (which also authors // 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 // this file). No other entry here uses JSX today; this doesn't turn
@ -103,7 +102,6 @@ for (const entry of [
"home.css", "home.css",
"stats.css", "stats.css",
"core.css", "core.css",
"builds.css",
]) { ]) {
await build({ await build({
entryPoints: [src(entry)], entryPoints: [src(entry)],
@ -120,7 +118,6 @@ for (const html of [
"flow.html", "flow.html",
"stats.html", "stats.html",
"core.html", "core.html",
"builds.html",
]) { ]) {
copyFileSync(src(html), dist(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 // The build lifecycle tabs of /dashboard.html — rebuild queue, live build
// queue, live build log, meta inputs, and build log history on one page. // log, meta inputs, and build log history. Not an entry point: tabs.js
// Three sub-tabs via <hive-tab-strip> (default = Rebuild Queue): // imports it, owns the tab strip and the dashboard event stream, and calls
// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline // the exports below. Three tabs, the panes' ids matching the tab ids:
// - M3T4 1NPUTS: nix flake input selector + meta-update trigger // - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show) // - 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 { import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
$,
fmtAgeSecs,
openStream,
openBuildLogStream,
initServerWarnings,
} from "./common.js";
import { el } from "@hive/shared/dom.js"; import { el } from "@hive/shared/dom.js";
import { bindAsyncForms } from "@hive/shared/forms.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 { fmtAgo, fmtDuration, truncate } from "./util.js"; import { fmtAgo, fmtDuration, truncate } from "./util.js";
import "@hive/shared/hive-tab-strip.js";
import { JobqGraph } from "@hive/shared/jobq-graph.js"; import { JobqGraph } from "@hive/shared/jobq-graph.js";
// ─── derived state ─────────────────────────────────────────────────────────── // ─── derived state ───────────────────────────────────────────────────────────
@ -174,7 +168,7 @@ function renderMetaInputs(s) {
// the generic component's. // the generic component's.
// Split from `renderRebuildQueue` below: `replaceChildren()` only // Split from `renderRebuildQueue` below: `replaceChildren()` only
// belongs on the *first* render, to clear the static "loading…" // 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 // Preact doesn't know that placeholder is there, so a later render
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own // call must NOT repeat `replaceChildren()` (it would wipe Preact's own
// tracked children out from under its diffing rather than let it // tracked children out from under its diffing rather than let it
@ -196,7 +190,6 @@ function renderRebuildQueue() {
onUpdate: (nodes) => { onUpdate: (nodes) => {
jobqNodes = nodes || []; jobqNodes = nodes || [];
renderRebuildLiveLog(); renderRebuildLiveLog();
updateRebuildCount();
}, },
onCancel: async (id) => { onCancel: async (id) => {
const node = jobqNodes.find((n) => n.id === id); const node = jobqNodes.find((n) => n.id === id);
@ -384,33 +377,18 @@ function renderRebuildLiveLog() {
} }
// ─── rebuild-queue count pill ───────────────────────────────────────────────── // ─── rebuild-queue count pill ─────────────────────────────────────────────────
function updateRebuildCount() { // The queue's in-flight count, for the R3BU1LD QU3U3 tab's pill (tabs.js
const pill = $("builds-tab-count-rebuild"); // reads it on its refresh tick). Pending/Running/Finishing = in flight
if (!pill) return; // (Finishing = own work done, a sub-node still running — still counts).
// Pending/Running/Finishing = in flight (Finishing = own work done, a // Root nodes only: each is one queue entry.
// sub-node still running — still counts). Root nodes only: each is one export function rebuildCount() {
// queue entry. return jobqNodes.filter(
const n = jobqNodes.filter(
(n) => (n) =>
n.parent == null && n.parent == null &&
(n.state === "Pending" || (n.state === "Pending" ||
n.state === "Running" || n.state === "Running" ||
n.state === "Finishing"), n.state === "Finishing"),
).length; ).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 ─────────────────────────────────────────────────────────── // ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
@ -419,10 +397,7 @@ function renderAll() {
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active. // 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
// Deep-link: ?id=N#buildlogs auto-expands the target row. // Deep-link: ?id=N#buildlogs auto-expands the target row.
const buildList = $("build-list");
const buildRefresh = $("build-refresh");
let buildLogsLoaded = false; let buildLogsLoaded = false;
let buildTabs; // set in init
function fmtTs(unixSecs) { function fmtTs(unixSecs) {
if (!unixSecs) return ""; if (!unixSecs) return "";
@ -431,6 +406,7 @@ function fmtTs(unixSecs) {
} }
async function fetchBuild() { async function fetchBuild() {
const buildList = $("build-list");
if (!buildList) return; if (!buildList) return;
buildList.replaceChildren(); buildList.replaceChildren();
buildList.append(el("p", { class: "meta" }, "loading…")); 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 ───────────────────────────────────────────────────────────── /** Mount the rebuild queue and wire the build-log refresh button. Called
let buildRefreshTimer = null; * once, after the tab strip exists. The queue self-fetches on mount
const SSE_HANDLERS = { * (JobqGraph's own effect), unlike the meta inputs, which wait for the
rebuild_queue_changed() { * snapshot. */
// No `ev.queue` payload read anymore — this event still carries its own export function initBuilds() {
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the const refresh = $("build-refresh");
// swarm badges, untouched by this), this page just treats it as a if (refresh) refresh.addEventListener("click", fetchBuild);
// 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.
mountRebuildQueue(); mountRebuildQueue();
}
buildTabs = document.getElementById("builds-tabbar").configure({ /** The meta inputs, from an `/api/state` snapshot. */
tabs: [ export function syncBuildsFromSnapshot(s) {
{ syncFromSnapshot(s);
id: "rebuild", renderMetaInputs({ meta_inputs: metaInputsState });
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();
}
},
});
await refreshState(); /** A tab was shown: BUILD L0GS loads its list the first time. */
export function onBuildsTabShown(tab) {
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all if (tab === "buildlogs" && !buildLogsLoaded) {
// 13 wire kinds down to the 3 this page acts on. This page was one buildLogsLoaded = true;
// of 4 unfiltered `/api/dashboard/stream` subscribers before this fetchBuild();
// (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);
};
} }
} }
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 // 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 // `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 // `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 the stream and calls `onDone(status)`; on a non-transient error

View file

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

View file

@ -14,7 +14,8 @@
<!-- C0R3: the host/coordinator surface. Standalone page (served at <!-- C0R3: the host/coordinator surface. Standalone page (served at
/core.html) carved out of the dashboard's old SYST3M tab so the /core.html) carved out of the dashboard's old SYST3M tab so the
dashboard tab strip stays lean: kept state (tombstones) and 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 tombstones-stale-perms) match what core.js's renderers target. Hive
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
no dashboard panel — `hivectl stop`/`start`/`restart` is the only 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 // standalone page so the dashboard tab strip stays lean: kept state
// (tombstones) and the stale permission entries. // (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 // /api/state and subscribes to /dashboard/stream (the same broker event
// channel the dashboard uses), maintaining its own copy of the tombstones // channel the dashboard uses), maintaining its own copy of the tombstones
// state. // state.

View file

@ -8,6 +8,7 @@
pulls them in here. */ pulls them in here. */
@import "./system-sections.css"; @import "./system-sections.css";
@import "@hive/shared/jobq-rollup.css"; @import "@hive/shared/jobq-rollup.css";
@import "@hive/shared/jobq-graph.css";
/* ─── tabbed dashboard chrome ────────────────────────────────────── /* ─── tabbed dashboard chrome ──────────────────────────────────────
Top-of-page sticky header with banner + tab strip. SSE stays Top-of-page sticky header with banner + tab strip. SSE stays
@ -125,6 +126,11 @@ body.dashboard-shell {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-weight: bold; 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 /* Tab pane visibility — createTabStrip (@hive/shared/tabs.js) sets the
`hidden` attribute on every inactive pane (resolved from the URL hash; `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 /* 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 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> </a>
<!-- SYST3M moved to its own standalone page (/core.html, "C0R3"), <!-- SYST3M moved to its own standalone page (/core.html, "C0R3"),
reached from the H0M3 hub — keeps the dashboard tab strip lean. reached from the H0M3 hub — keeps the dashboard tab strip lean.
The rebuild-queue STATE still lives in tabs.js (it drives the The build lifecycle is three tabs here (builds.js): the rebuild
"building…" badges on SW4RM agent cards); only the SYST3M tab + queue with its live log, the meta flake inputs, and the build log
its section renderers moved out. --> 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 <!-- P3RM1SS10NS: per-agent capability grants and tool-group
assignments. Both tables are fetched on tab activation. --> assignments. Both tables are fetched on tab activation. -->
<a <a
@ -102,9 +133,77 @@
</div> </div>
</section> </section>
<!-- SYST3M pane moved to /core.html (the standalone "C0R3" page): <!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
kept state and stale permissions. Reached first-spawns. Rendered from GET /api/jobq/graph by the
from the H0M3 hub. --> `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 <!-- P3RM1SS10NS: per-agent capability grants + tool-group
assignments. Both tables are column-driven from the backend assignments. Both tables are column-driven from the backend

View file

@ -42,7 +42,7 @@
<span class="home-tile-label">Dashboard</span> <span class="home-tile-label">Dashboard</span>
</span> </span>
<span class="home-tile-desc" <span class="home-tile-desc"
>containers · permissions · system</span >containers · builds · permissions</span
> >
</a> </a>
@ -54,16 +54,6 @@
<span class="home-tile-desc">live all-agents message firehose</span> <span class="home-tile-desc">live all-agents message firehose</span>
</a> </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"> <a class="home-tile" href="/stats.html">
<span class="home-tile-head"> <span class="home-tile-head">
<span class="home-tile-icon" aria-hidden="true">📊</span> <span class="home-tile-icon" aria-hidden="true">📊</span>

View file

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

View file

@ -1,7 +1,6 @@
/* ─── SYST3M/BU1LDS section panels — shared by dashboard, /core.html, /* ─── SYST3M/BU1LDS section panels — shared by dashboard and /core.html ───
and /builds.html ──────────────────────────────────────────────────
Rebuild queue, meta inputs and kept-state panels. 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 panel ────────────────────────────────────────────── */
.meta-inputs { .meta-inputs {
@ -124,9 +123,8 @@
} }
/* ─── running-rebuild live log (renderRebuildLiveLog in builds.js) ────────── /* ─── running-rebuild live log (renderRebuildLiveLog in builds.js) ──────────
Streams the currently-running rebuild's build log inline under the queue. Streams the currently-running rebuild's build log inline under the queue.
Hidden (native `hidden` attr) when nothing is building. Used on Hidden (native `hidden` attr) when nothing is building. Used on the
/builds.html; lives here (not in builds.css) so it can be shared if dashboard's R3BU1LD QU3U3 tab. */
the dashboard ever gains an inline live-log view. */
.rebuild-live-log { .rebuild-live-log {
margin-top: 0.8em; margin-top: 0.8em;
border: 1px solid var(--purple-dim); border: 1px solid var(--purple-dim);

View file

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

View file

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

View file

@ -15,7 +15,7 @@
// worth a line of chrome). // worth a line of chrome).
// //
// `queueHref`, when given, adds a "view queue →" link (dashboard links to // `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). // you're already on is noise).
// //
// The glyph carries `.spinner` (`@hive/shared/base.css`, imported by // 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 // 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 // rendering the <a class="hive-tab"> markup from a declarative `tabs` list
// instead of every page hand-writing the same <nav><a>...</a></nav> // instead of every page hand-writing the same <nav><a>...</a></nav>
// boilerplate, then wires the existing createTabStrip() behaviour // boilerplate, then wires the existing createTabStrip() behaviour
@ -21,7 +21,7 @@
// — same id/aria-controls scheme the hand-written markup used, so each // — same id/aria-controls scheme the hand-written markup used, so each
// page's panes (aria-labelledby="${prefix}-tab-${id}") don't need to // page's panes (aria-labelledby="${prefix}-tab-${id}") don't need to
// change. A `badgeId` tab also gets a nested `<span class="tab-count" // 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 // pill), then wires up createTabStrip() and returns exactly what it
// returns ({ show, active }) — every existing `.active()`/`.show(id)` // returns ({ show, active }) — every existing `.active()`/`.show(id)`
// call site keeps working unchanged. Call configure() exactly once per // call site keeps working unchanged. Call configure() exactly once per