diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index 9e471b80..08b2ab93 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -127,7 +127,7 @@ Two things live in the `agent-configs` Forgejo organization: [How a hive reads the forge](#how-a-hive-reads-the-forge)). - The dashboard links each container's deployed config commit to this config repo on the swarm's forge (`swarm.forge.publicUrl`; `null` means no - link), so operators can go straight from the 4G3NTS tab into + link), so operators can go straight from the SW4RM tab into the rendered repo without an extra `git` step. The `hive-forge` CLI (separate workspace crate, see diff --git a/docs/scheduler/jobq.md b/docs/scheduler/jobq.md index 15bca667..990a3362 100644 --- a/docs/scheduler/jobq.md +++ b/docs/scheduler/jobq.md @@ -33,7 +33,7 @@ swarm controller and hive-c0re each build their own graph on it. Two views, one per graph, drawn by the same component: the swarm UI's `/jobs` page shows the swarm controller's graph, and the hive dashboard's -**R3BU1LD QU3U3** tab (see +**BU1LDS** page (R3BU1LD QU3U3 — see [`web-ui/dashboard.md`](../web-ui/dashboard.md)) shows that hive's. Each **row** is one job; the rows nested under it are that job's steps, in order (occasionally a couple run side by side). A step shows one of: diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index 9c8f2fe4..fdf14ca6 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -11,23 +11,21 @@ what you actually do here. ## Where things are Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles -linking to every surface (Dashboard, Flow, Logs, Stats, +linking to every surface (Dashboard, Flow, Logs, Builds, Stats, Settings, Core). Every page links back to H0M3, so you're never more than one step from the hub. The **dashboard** itself (`/dashboard.html`) is where you'll spend most -of your time. It's a single page with five tabs: +of your time. It's a single page with exactly two tabs: -- **4G3NTS** — 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. -- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build - lifecycle: the rebuild queue with a live log of whatever is building, - the meta flake inputs you can bump, and the build log history. - **P3RM1SS10NS** — what tools and system-level access each agent has. Everything else lives on its own page instead, all reachable from the H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across -the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and +the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), +**Builds** (`/builds.html`, the rebuild queue and build history), and **Core** (`/core.html`, tombstones and container resource use). Each agent also has its own page — a full terminal view of that @@ -37,7 +35,7 @@ if the gateway isn't in front). ## The things you'll actually do -**Check on an agent.** 4G3NTS shows every container as a row: name, +**Check on an agent.** SW4RM shows every container as a row: name, whether it's running, what it's currently doing (a live status pill — `rebuilding…`, `starting…`, or similar — while something's in flight), and quick links (stats, screen, forge profile). Select the name to open @@ -49,12 +47,12 @@ repo. Review and merge them on the forge; the merge deploys the change. See [`config-changes.md`](../agent-lifecycle/config-changes.md#config-changes). **Start, stop, restart, or rebuild an agent.** Select one or more -agents on 4G3NTS (select the icon) and use the selection bar, or use the +agents on SW4RM (select the icon) and use the selection bar, or use the per-agent `⋮` menu on a single row. Rebuilding re-applies that agent's current config; use it whenever an agent shows as "needs update." -**Watch a build.** The dashboard's R3BU1LD QU3U3 tab shows the rebuild queue live, plus a +**Watch a build.** BU1LDS shows the rebuild queue live, plus a streaming log of whatever's currently building. Useful right after merging a config change or bumping a flake input. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 6cfd43f1..f65593e5 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -10,8 +10,8 @@ matrix accounts, the controller's job graph — are on the hive-c0re serves the dashboard at `/dashboard.html` (with the home page at `/`). It has a fixed chrome header at the top and a `
` that shows exactly -one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`, -`#buildlogs`, `#permissions`) drives which pane is +one tab pane at a time. The URL hash (`#swarm`, +`#permissions`) drives which pane is active; hash changes don't reload the page. FL0W and the other standalone pages are reachable from the H0M3 hub at `/`, not from the dashboard tab strip. @@ -19,18 +19,17 @@ from the dashboard tab strip. **Chrome header** (fixed, overlays the active tab pane): - **← home back-link**: top-left of the chrome, links to the H0M3 hub at `/`. Every surface links back to the hub rather than to each other. -- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`, - `◆ BUILD L0GS ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only — +- **Tab strip**: `◆ SW4RM ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only — the SYST3M panels live on the standalone **C0R3** page (`/core.html`), and FL0W / ST4TS live on their own pages too, all reachable from the - H0M3 hub (not the tab strip). Count pills on 4G3NTS - (stale containers) and R3BU1LD QU3U3 (rebuilds in flight). + H0M3 hub (not the tab strip). Count pill on SW4RM + (container count). - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`) — sits below the tab strip. - **Server-warnings banner** — a generic, sticky top-of-page strip shown on **every** page (dashboard + every stand-alone page — FL0W, - H0M3, C0R3, ST4TS), injected at the top + H0M3, C0R3, BU1LDS, ST4TS), injected at the top of `` by `renderServerWarnings` in `common.js`. Driven by `state.server_warnings` — a list of `{ kind, level, message }` — and coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the @@ -56,7 +55,7 @@ The FL0W page uses a slim header (a `← home` back-link + the page title) rather than the dashboard tab strip — it's a standalone surface, not a tab pane. -## 4G3NTS tab +## SW4RM tab **C0NTAINERS** — live containers as a flat list, one row per container, sorted alphabetically by agent name; every container the @@ -74,13 +73,13 @@ agent is stale. Passive / rare-interaction state, reached from the **Core** tile on the H0M3 hub (served at -`/core.html`), with minimal chrome: a `← home` back-link and a single pane, -**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that -cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for -`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed` -(the latter two re-render the stale-perms sub-section when permission -data changes). (Rebuild queue and meta inputs are dashboard tabs — see -below.) +`/core.html`), with the same minimal chrome as `/builds.html`: a `← home` +back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild +bundle (`core.js`) that cold-loads `/api/state` and subscribes to +`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`, +and `tool_groups_changed` (the latter two re-render the stale-perms +sub-section when permission data changes). (Rebuild queue and meta inputs +live on the BU1LDS page — see below.) 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 @@ -112,23 +111,21 @@ autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE events. Each ghost agent gets a `✕ clear perms` button (`DELETE /api/permissions/{name}`). -## Build tabs (`#rebuild`, `#meta`, `#buildlogs`) +## BU1LDS page (`/builds.html`) -The build lifecycle — rebuild queue, live build log, meta inputs, and -build log history — is three tabs of the dashboard: **R3BU1LD QU3U3** -(`#rebuild`), **M3T4 1NPUTS** (`#meta`) and **BUILD L0GS** -(`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the -dashboard's one tab strip, `/api/state` snapshot and -`/api/dashboard/stream` subscription serve them, with -`rebuild_queue_changed`, `meta_inputs_changed` and `meta_update_running` -on the stream alongside the dashboard's other kinds. The count pill on -R3BU1LD QU3U3 is the number of rebuilds in flight. -The 4G3NTS tab does **not** read the graph endpoint, by design — the swarm -view stays independent of job-queue internals. Its per-agent pending -badges are transient-only, and its queue-summary banner reads the much -narrower `GET /api/jobq/rollup` instead (see Container row, below) — a -handful of pre-tallied counts, not the graph. The banner's "view queue →" -link goes to the `#rebuild` tab. +The build lifecycle hub — rebuild queue, live build log, meta inputs, +and build log history on one page. Standalone page reached from the +**Builds** tile on the H0M3 hub, same minimal chrome as `/core.html`. +Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**, +**BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads +`/api/state` and subscribes to `/api/dashboard/stream` for +`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. +The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at +all, by design — the swarm view stays independent of job-queue +internals. Its per-agent pending badges are transient-only, and +its queue-summary banner reads the much narrower `GET /api/jobq/rollup` +instead (see Container row, below) — a handful of pre-tallied counts, +not the graph. **R3BU1LD QU3U3** — pending, in-flight, and recently settled container operations: rebuilds, meta-update cascades, and first-spawns. One @@ -397,7 +394,7 @@ re-renders the terminal row. The operator addresses the root agent as `@root`. ## H0M3 page (`/`) The H0M3 hub is the primary landing page (served at `/` by default). A -responsive grid of link tiles — Dashboard, Flow, Stats, +responsive grid of link tiles — Dashboard, Flow, Builds, Stats, Core, API — each pointing to their respective surfaces, all unconditionally shown (no gating). The page is a pure portal with no tab-bar or SSE subscriptions. Typography + colours inherit @@ -414,7 +411,7 @@ the tooltip says "select … for bulk actions" or "deselect … (or press Esc to clear all)." The `` points at `/icon`; load failure falls back to the dimmed hyperhive mark (`/favicon.svg`). The card body sits to the right with three stacked lines -(`assets/agents.js::renderContainers`). +(`assets/swarm.js::renderContainers`). **Icon layout + load strategy:** the `` is absolutely @@ -583,10 +580,10 @@ agent is stale. Banner pulses on each broker SSE event **Build-queue summary banner** — when the job queue has any active work, a compact amber banner sits above the container list: `◐ build queue — N running · M queued — view queue →` (the link goes to the -R3BU1LD QU3U3 tab). The shared `JobqRollup` Preact component +BU1LDS page's R3BU1LD QU3U3). The shared `JobqRollup` Preact component (`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page mounts, pointed at swarm-controller's own rollup endpoint instead), -mounted once into `#jobq-rollup-section` by `agents.js::initJobqRollup` +mounted once into `#jobq-rollup-section` by `swarm.js::initJobqRollup` and refreshed via its own handle rather than being re-rendered by `renderContainers`. Reads `GET /api/jobq/rollup` (`hive-jobq-wire::state_rollup`) — `Vec<{ state, nodes, roots }>`, @@ -648,7 +645,7 @@ Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` / buttons — see **Container row** above. Selecting an agent's icon toggles its selection (an in-memory `Set`); `Esc` or the bar's `✕ clear` button drops everything. The selection persists across tab -switches in-memory — the bar just hides on non-4G3NTS tabs since other +switches in-memory — the bar just hides on non-SW4RM tabs since other tabs don't show the agent cards needed to cross-reference. When the operator selects one or more agents (via the icon), a sticky @@ -714,11 +711,11 @@ below — some endpoints aren't in it yet. 404 when the row is missing (vacuum-reaped or stale id). - `GET /api/build-logs/id/{id}/stream` — live SSE stream of stdout+stderr for an in-progress build (newline-delimited JSON - frames). Used by the expanding live-row view in the BUILD L0GS tab. The + frames). Used by the expanding live-row view in BU1LDS. The stream closes when the build finishes or errors. - `GET /api/build-logs/id/{id}/raw` — raw concatenated stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download - raw` links in the build tabs. Works on both finished and in-progress + raw` links in BU1LDS. Works on both finished and in-progress build logs. - `GET /static/marked.js` serves the vendored `marked` bundle used for markdown previews. @@ -857,10 +854,10 @@ payload): full snapshot of destroyed-but-state-kept agents. Emitted on destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes. - `meta_inputs_changed` (seq, inputs: `Vec`) — full - snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab + snapshot of `meta/flake.lock` inputs. BU1LDS M3T4 1NPUTS tab subscribes; same shape as `GET /api/meta-inputs`. - `meta_update_running` (running: bool) — emitted when a - `nix flake update` ripple starts or completes. The M3T4 1NPUTS + `nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS tab uses this to show/hide the "⏳ meta-update running" banner. diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index af9f4382..44850508 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -20,7 +20,7 @@ CSS bundle per page (`colors.css` + `theme.css` + `common.css`, loaded by every page, plus a page-specific bundle — `dashboard.css` / `flow.css` / `home.css` / - `stats.css` / `core.css`); + `stats.css` / `core.css` / `builds.css`); `common.css` inlines `@hive/shared`'s `base.css` + `terminal.css` + `tabs.css` + `chrome.css` + `pill.css` via esbuild's `@import` resolution. @@ -208,7 +208,7 @@ re-render), call `refreshState()`. State shapes live in `hive-c0re/src/dashboard/state_snapshot.rs::StateSnapshot` and `web_ui/state.rs::StateSnapshot` — when adding state fields, plumb through the snapshot struct and the -relevant domain module (`agents.js`, `permissions.js`, etc.) render function. +relevant domain module (`swarm.js`, `permissions.js`, etc.) render function. **Focus preservation:** `refreshState` checks whether `document.activeElement` sits inside one of the managed sections diff --git a/frontend/README.md b/frontend/README.md index 8bab5d97..308451ff 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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`, `agents.js`, imported by the dashboard entry); `shared` holds both kinds of module. No + (`builds.js`, `swarm.js`); `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 diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index ca29bc82..99da2c1f 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -1,5 +1,5 @@ // esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at -// `/`, dashboard/flow/core/stats at their own +// `/`, dashboard/flow/core/builds 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,6 +38,7 @@ await build({ src("home.js"), src("stats.js"), src("core.js"), + src("builds.js"), ], outdir: staticDir(""), bundle: true, @@ -56,7 +57,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 `tabs.js` via `builds.js` — esbuild + // (`JobqGraph.jsx`), pulled in transitively by `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 @@ -102,6 +103,7 @@ for (const entry of [ "home.css", "stats.css", "core.css", + "builds.css", ]) { await build({ entryPoints: [src(entry)], @@ -118,6 +120,7 @@ for (const html of [ "flow.html", "stats.html", "core.html", + "builds.html", ]) { copyFileSync(src(html), dist(html)); } diff --git a/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js b/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js index 8267589f..afa4d261 100644 --- a/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js +++ b/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js @@ -1,6 +1,6 @@ // hive-agent-menu.js — , the per-agent "⋮" context-menu // custom element. One instance per agent card, appended after -// .card-body in the container row's flex layout (agents.js's +// .card-body in the container row's flex layout (swarm.js's // buildContainerLi via buildAgentMenu). // // Deliberately a *light-DOM* element with no shadow root of its own: all @@ -66,7 +66,7 @@ async function agentMenuPost(actionPath, name, body, flags) { class HiveAgentMenu extends HTMLElement { connectedCallback() { // Same reconnect-without-detach hazard as (see its - // connectedCallback comment) — agents.js's row cache can move an + // connectedCallback comment) — swarm.js's row cache can move an // already-built
  • subtree without a real detach. Without this // guard a reconnect would append a second on top of the // first, doubling the dropdown. diff --git a/frontend/packages/dashboard/src/builds.css b/frontend/packages/dashboard/src/builds.css new file mode 100644 index 00000000..a17106ae --- /dev/null +++ b/frontend/packages/dashboard/src/builds.css @@ -0,0 +1,139 @@ +/* ─── /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); +} diff --git a/frontend/packages/dashboard/src/builds.html b/frontend/packages/dashboard/src/builds.html new file mode 100644 index 00000000..90edfd96 --- /dev/null +++ b/frontend/packages/dashboard/src/builds.html @@ -0,0 +1,101 @@ + + + + + + hyperhive // BU1LDS + + + + + + + + + + +
    + +
    +

    + 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. +

    +
    +

    loading…

    +
    + + +
    + + +
    +

    + select inputs to nix flake update in /meta/. + selected agents rebuild in sequence after the lock bump; the + submitting agent learns each outcome via the usual + rebuilt system event. +

    +
    +

    loading…

    +
    +
    + + +
    +

    + all-agents build log history. click a row to expand stdout + stderr. + live builds stream in real time. +

    +
    + +
    +

    loading…

    +
    +
    + + + + diff --git a/frontend/packages/dashboard/src/builds.js b/frontend/packages/dashboard/src/builds.js index cd7d7857..03e41e18 100644 --- a/frontend/packages/dashboard/src/builds.js +++ b/frontend/packages/dashboard/src/builds.js @@ -1,17 +1,23 @@ -// 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 +// 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 (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) -import { $, fmtAgeSecs, openBuildLogStream } from "./common.js"; +import { + $, + fmtAgeSecs, + openStream, + openBuildLogStream, + initServerWarnings, +} 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 ─────────────────────────────────────────────────────────── @@ -168,7 +174,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 `dashboard.html` — +// placeholder `#rebuild-queue-section` starts with in `builds.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 @@ -190,6 +196,7 @@ function renderRebuildQueue() { onUpdate: (nodes) => { jobqNodes = nodes || []; renderRebuildLiveLog(); + updateRebuildCount(); }, onCancel: async (id) => { const node = jobqNodes.find((n) => n.id === id); @@ -377,18 +384,33 @@ function renderRebuildLiveLog() { } // ─── rebuild-queue count pill ───────────────────────────────────────────────── -// 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( +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( (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 ─────────────────────────────────────────────────────────── @@ -397,7 +419,10 @@ export function rebuildCount() { // 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 ""; @@ -406,7 +431,6 @@ function fmtTs(unixSecs) { } async function fetchBuild() { - const buildList = $("build-list"); if (!buildList) return; buildList.replaceChildren(); buildList.append(el("p", { class: "meta" }, "loading…")); @@ -588,54 +612,93 @@ async function 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. - -/** 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(); -} - -/** The meta inputs, from an `/api/state` snapshot. */ -export function syncBuildsFromSnapshot(s) { - syncFromSnapshot(s); - renderMetaInputs({ meta_inputs: metaInputsState }); -} - -/** A tab was shown: BUILD L0GS loads its list the first time. */ -export function onBuildsTabShown(tab) { - if (tab === "buildlogs" && !buildLogsLoaded) { - buildLogsLoaded = true; - fetchBuild(); - } -} +if (buildRefresh) buildRefresh.addEventListener("click", fetchBuild); +// ─── 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 }); + }, +}; -/** `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); +// ─── 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(); + + 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(); + } + }, + }); + + 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); + }; } } -export function onMetaInputsChanged(ev) { - metaInputsState = (ev.inputs || []).slice(); - renderMetaInputs({ meta_inputs: metaInputsState }); -} - -export function onMetaUpdateRunning(ev) { - metaUpdateRunning = !!ev.running; - renderMetaInputs({ meta_inputs: metaInputsState }); -} +init(); diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index 307bc90e..5d5d57ec 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -84,7 +84,7 @@ code { Semantic colour variants only — shape comes from `hive-pill-sm` (shared with the agent UI's pill/chip family, @hive/shared/pill.css), carried directly in the markup alongside each of these classnames - (agents.js/core.js/builds.js). Used on container rows and build-log + (swarm.js/core.js/builds.js). Used on container rows and build-log rows. */ .badge-warn { color: var(--amber); diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index c2b55c29..9f7baeab 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -292,7 +292,7 @@ export function openStream(url) { } // Stream a build log into a
    , returning the EventSource. Used by the
    -// BUILD L0GS tab. Appends `stdout_append` then
    +// BU1LDS page's 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
    diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css
    index f618308b..be613a1d 100644
    --- a/frontend/packages/dashboard/src/core.css
    +++ b/frontend/packages/dashboard/src/core.css
    @@ -1,6 +1,7 @@
     /* ─── /core.html — C0R3 page ───────────────────────────────────────
        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";
     
     body.core-shell {
    diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html
    index 95ac4964..8728f020 100644
    --- a/frontend/packages/dashboard/src/core.html
    +++ b/frontend/packages/dashboard/src/core.html
    @@ -14,8 +14,7 @@
         
    -        
    -          ◆ R3BU1LD QU3U3 ◆
    -          
    -        
    -        
    -          ◆ M3T4 1NPUTS ◆
    -        
    -        
    -          ◆ BUILD L0GS ◆
    -        
    +           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. -->
             
             
     
             
    +           under SW4RM. See tab-pane-swarm.) -->
     
             
         
         
    - + — the tab label SW4RM already says it. --> - - - - - - - - +