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

@ -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