Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/web-ui/dashboard.md
iris e7f977b99e dashboard: drop the path-link and side-panel code the flow page was the only user of
appendLinkified had one caller, the flow page, and it pulled in the
path-link and file-preview chain, the side-panel singleton, the marked
and DOMPurify wiring and the css only they used. The docs stop
describing the dashboard side panel and the client reading file_refs.
2026-10-21 23:57:37 +02:00

651 lines
34 KiB
Markdown

# Dashboard layout
> Part of [Web UI](README.md). See also:
> [Shape (shared)](shape.md) · [Per-agent page](agent.md)
This page covers the per-hive UI that hive-c0re serves. Swarm-wide
surfaces — the agent roster, agent creation, linking external forge and
matrix accounts, the controller's job graph — are on the
[swarm UI](../swarm/ui.md).
hive-c0re serves the dashboard at `/`.
It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
`#buildlogs`, `#kept`) drives which pane is
active; hash changes don't reload the page. ST4TS
is a standalone page reached from the pages menu, not from the
dashboard tab strip.
**Chrome header** (fixed, overlays the active tab pane):
- **Pages menu**: a `☰` button at the left of the chrome (`links-menu.js`)
opening a popover with links to **stats**
(`/stats.html`), **api docs** (`/api/docs/`) and **swarm** (the swarm
UI at the swarm apex domain, opened in a new tab). The swarm entry
comes from `state.swarm_public_url`, which hive-c0re reads from
`HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`);
with no domain set there is no swarm entry. The standalone pages link back to `/`.
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS
lives on its own page, reached from the pages menu. Count pills on
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
K3PT ST4T3 (kept-state entries).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
- **No warning banner.** No page shows hive-level warnings (host disk
pressure, failing background sweeps, forge boot failures, agents that
need a claude login). They live in a push-based registry
(`hive-c0re`'s `warnings.rs`): any subsystem raises or clears its own
entry through an RAII guard, and each raise logs one
`hive warning raised` line. The operator sees the registry as metrics and
Grafana alerts: see
[Hive-scoped metrics](../scheduler/observability.md#hive-scoped-metrics-hive-c0re).
A container crash is a swarm-level Grafana alert on the crash watcher's
own log line, so a hive with no otel export has no operator-visible
warning signal beyond `/health/ready`.
- **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
arrive in the state snapshot.
## 4G3NTS tab
**C0NTAINERS** — live containers as a flat list, one row per
container, sorted alphabetically by agent name; every container the
rescan reports gets a row. Pulsing red banner at the top
of this section if any two sub-agents hash to the same port
(`port_conflicts` from `/api/state`): the operator must rename
one of them and rebuild. `lifecycle::{spawn,rebuild}` also
preflight this and refuse with a clear error message naming the
conflicting agent.
`↻ UPD4TE 4LL` button appears above the containers list when any
agent is stale.
## K3PT ST4T3 tab
Passive / rare-interaction state. `kept.js` renders it from the
`/api/state` snapshot and keeps it live from the `tombstones_changed`
stream event. (Rebuild queue and meta inputs are dashboard tabs too —
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
[observability](../scheduler/observability.md)). An agent's CPU and memory
caps live per agent in the swarm UI.
**K3PT ST4T3** — agents with kept state and **no container** (size + age +
claude-creds badge). One action: `PURG3` (wipes state + applied
dirs; `POST /api/purge-tombstone/{name}`).
⚠️ **Not only *destroyed* agents.** Nothing records a destroy: `Provision`
writes every definition-side artifact (state subvolume, proposed + applied
repos, meta registration, topology entry) *before* the container exists,
and that artifact set survives `lifecycle::destroy`. An agent
part-way through a spawn is byte-identical on disk to a tombstone, and
the pane lists both. The pane carries a standing warning to that effect, and
the row badge says `offline` rather than `destroyed` — the absence of a
container is all hive-c0re can actually prove. Fixing it properly
needs a recorded destroy — **deferred** to the swarm-controller /
snapshot-storage rework where the problem changes shape.
## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
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.
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
operations: rebuilds, meta-update cascades, and first-spawns. One
operation runs at a time; the worker drains FIFO. **Is a mounted
`JobqGraph`** (the shared generic graph-viewer Preact component,
`@hive/shared/jobq-graph.js` — the same one swarm-ui uses) —
`builds.js` doesn't render the queue itself; it just mounts the component
(`mountJobqGraph(container, props)`, since this page has no JSX
pipeline) with an `onUpdate` callback to drive the two things below it
that the generic view doesn't show. The component owns fetching, cold
and live: `GET /api/jobq/graph` on mount, and a refetch via the mount
handle's `.refresh()` on every `rebuild_queue_changed` SSE tick (that
event is a bare `{ seq }` trigger carrying no queue payload; both
dashboard pages treat the tick as a pure refetch trigger).
Each row is one root graph node (`parent: null`); a multi-step op's
per-agent subgraphs and sub-steps render as nodes within that one
entry (structural `parent` edges define the tree; sibling order is
array order, unchanged from the wire). A row shows a state glyph (`⏸`
pending / `▶` running / `◐` finishing — own work done, a sub-node
still running / `✔` done / `✖` failed / `⊘` cancelled / `·` skipped)
and each step's own label/agent. A `Node`-kind dep on a sibling shows
as a plain "waits on: `<label>`" text line under the row rather than a
gutter rail — a rail breaks visually whenever a nested subtree sits
between the two related rows, since a text line needs no continuous
vertical space to draw.
`builds.js` mounts the component with `cancellable` set, which turns on
a per-node cancel button (`✕`) on any non-terminal row — the button
calls the mount's `onCancel(id)` callback, and the page does the actual
`POST /api/rebuild-queue/{id}/cancel`, matching that callback to its
own domain concept (the component knows nothing about that endpoint).
**Rows carry no source chip, kind label, timing, or build-log
deep-link** — the generic graph wire doesn't carry those fields, and
rows deliberately present exactly what the endpoint provides rather
than reconstructing chrome hive-c0re doesn't send. Settled entries
render their **full step tree**, not just a bare summary — the wire
doesn't filter `Done` nodes out.
**State filter.** A row of per-state checkboxes above
the tree — one per lifecycle state, matching the row glyphs — lets the
operator narrow which root groups render; unchecking a state re-fetches
`GET /api/jobq/graph?states=<checked, comma-joined>` rather than
hiding rows client-side, so the `onUpdate` callback's node list (and
everything downstream of it — the count pill, the live-log panel) only
ever sees what's actually shown. Filtering is by a **root's own**
state, which is already its subtree's rolled-up answer, so the filter
keeps or drops a whole group, never splitting mid-tree. Default selection is
every state **except** `Done`/`Skipped` — a fresh queue view leads
with what's still moving or needs attention, not the settled tail;
builds.js omits the `states` param entirely (identical request to
before this filter existed) when the operator has checked every state. Server-side: the query
narrows [`Queue::graph_snapshot`]'s already-bounded (`MAX_HISTORY_DAGS`)
root set — the history cap and the state filter are independent
concerns, so a narrow filter never reaches further back in time to
compensate.
Below the queue, a **live build-log panel** (`#rebuild-live-log`,
`renderRebuildLiveLog`) shows the currently running rebuild's output
inline — collapsible, with a live/ok/fail badge and a `↓ raw`
download. It's keyed to the first `Running` node (in wire order)
whose `payload.data.build_log_id` has a value — read from the `onUpdate`
callback's node list, same source as the count pill, no separate
fetch — and **polls** `GET /api/build-log/{id}` every 2s
(`fetchAndRenderLiveLog` / `liveLogPollTimer`); not an `EventSource`
(that's the BUILD L0GS tab's own per-row expand view below — `GET
/api/build-logs/id/{id}/stream`, real SSE, replays accumulated output
on connect — a separate mechanism). The live-log panel lives in its
own container outside `#rebuild-queue-section` so the mounted
`JobqGraph`'s own re-renders never disturb the open poll; it hides
when nothing is building.
**M3T4 1NPUTS** — inputs in `meta/flake.lock` the operator can
selectively `nix flake update`, rendered as an indented tree:
every fetched input at every depth (`hyperhive`,
`hyperhive/nixpkgs`, `agent-<n>`, `agent-<n>/mcp-<x>`, …), each
shown once at its shallowest path. `read_meta_inputs` walks the
lock graph with a `visited` set, skipping `follows` aliases and rev-less
nodes. A `select all / select none`
control sits above the tree. Checking inputs + submitting bumps
the lock in `/meta/` and rebuilds the selected agents in
sequence; each outcome reaches the root agent as a `rebuilt`
system event. `POST /meta-update`. While a lock-bump ripple runs,
the panel shows a pulsing "⏳ meta-update running" banner and disables
the update button (snapshot field `meta_update_running`,
live event `meta_update_running`).
**BUILD L0GS** — all-agents build log history. Lazy-loaded on first tab
activation; autorefreshes when
`rebuild_queue_changed` fires. Fetches `GET /api/build-logs?limit=30`.
Renders a scrollable list of build entries; each row is a collapsible
button showing status badge (`live` / `ok` / `fail`), agent name,
elapsed duration, build kind, age, and the invocation command line.
Expanding a row fetches the full stdout+stderr via
`GET /api/build-logs/id/{id}`. A live in-progress build shows a `live`
badge with a ticking elapsed-time chip; expanding streams output via
`GET /api/build-logs/id/{id}/stream` with sticky-bottom autoscroll
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
entry with that id pre-expanded.
## ST4TS page (`/stats.html`)
Hive-wide turn statistics, aggregated across every agent's
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
tab — a standalone page reached from the dashboard's pages menu,
minimal chrome (a back-link and a title). Distinct from
each agent's own `/stats` page (which carries the per-agent trend
charts): ST4TS is the swarm-level rollup.
- **Window selector** (`1h`, `4h`, `24h`, `3d`, `7d`, `30d`, `all`) —
a hash-routed tab strip (`#1h` / `#24h` / …) — re-fetches on change.
- **Summary chips**: active agents, turns, total/input/output/cache-read
tokens, and a labelled **est cost**.
- **Busiest agents** table — one row per agent (most turns first):
turns, input / output / cache-read tokens, est cost.
- **Model mix** — turns per model across the swarm, as CSS bars.
- **Favorite tools** — most-run normalised bash-command heads across the
swarm (top 10, as CSS bars), aggregated from each agent's
`bash_commands` table (written by the hive-bash-daemon capture). The
header + list stay hidden until at least one agent has recorded a
command, so the section never shows an empty block on a fresh hive.
Backed by `GET /api/stats-hive?window=<w>` in `hive-c0re`
(`hive_stats.rs`): for every name from
`Coordinator::kept_state_names()` it opens
`agent_harness_dir(name)/hyperhive-turn-stats.sqlite` read-only (with a
500 ms `busy_timeout`, since `turn_stats` is rollback-journal) and rolls
the rows up, skipping missing / unreadable / zero-turn dbs so one
bad db never fails the endpoint. This is a **pull** surface (no SSE):
the page fetches the data on page load and on window change. Rendered
with plain tables + CSS bars — the dashboard bundle ships no chart
library.
The cost figure is a deliberately rough estimate from a per-model
price table (`est_cost_usd`); it drifts with list pricing and the page
labels it accordingly. The table is operator-tunable via the
`services.hyperhive.c0re.modelPrices` nix option — each key is a
model-family short name (matched case-insensitively as a substring of
the model id, longest match wins) mapping to
`{ input, output, cache_read, cache_write }` USD-per-million-token
prices. Models not covered fall back to hive-c0re's built-in estimate.
## Container row
A full-height **square agent icon** (5em, capped) on the left. The
icon is the **selection toggle**: select (or Enter/Space) adds/removes
the agent from the selection set; `aria-pressed` reflects the state;
the tooltip says "select … for bulk actions" or "deselect … (or press
Esc to clear all)." The `<img>` points at `<url>/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`).
<!-- vale write-good.Passive = NO -->
**Icon layout + load strategy:** the `<img>` is absolutely
positioned (`inset: 0`) inside the `.container-icon` wrapper —
the wrapper is the flex child and sizes itself via `width: 5em` +
`aspect-ratio: 1`, the `<img>` is out of flow so its load state
(pending, loaded, broken) can never contribute intrinsic size or
reflow the row. Without that, the row would briefly grow as the
image's natural dimensions arrived, then snap back on
`object-fit: contain`. The load itself is fire-and-forget: the
dashboard doesn't pre-check whether the agent is reachable, it
just lets the `<img>` try and listens for an `error` event. On
failure the handler swaps the `src` to `/favicon.svg` (served by
the dashboard itself, always reachable) and adds the
`icon-unreachable` class for the dimmed look. When the dashboard
already knows the container is stopped (`ContainerView.running = false`)
the fallback fires immediately, skipping the doomed `<url>/icon`
fetch entirely.
<!-- vale write-good.Passive = YES -->
<!-- vale write-good.Passive = NO -->
**Line 1** — agent name (link → new tab), m1nd/ag3nt chip, an
**icon-only nav strip** plus live agent-owned state, all populated
async from a single `GET /api/dashboard-state` call to the agent's
own hive-agent. The response (`DashboardState`) carries: `links` (nav
strip entries — `📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge
profile`, `↳ agent-configs mirror`, plus any agent-declared
`dashboardLinks` extras), `status_text` / `status_set_at` (agent
self-reported status — the `(set N ago)` chip carries a `data-set-at`
stamp and ticks every 30s to stay fresh across the long-lived keyed row
cache), `rate_limited`, `ctx_tokens` / `context_window_tokens`
(context-window badge data). hive-agent is the single source
of truth for all of these. The dashboard resolves each `AgentLink.kind`
against a per-agent base URL depending on whether hive-gateway is in
front (`StateSnapshot.gateway_enabled`, sourced from the
`HIVE_GATEWAY_ENABLED` env). The c0re NixOS module sets that env
unconditionally — the gateway always runs; the flag stays so the
dashboard doesn't have to learn that. Gateway-on: base URL is
`/agent/<name>` (same origin, gateway proxies to the
per-agent harness — TCP or unix-domain depending on the agent's
`HIVE_WEB_SOCKET` opt-in, see
`docs/networking/gateway.md::Per-agent unix-socket upstream`).
Gateway-off is the flag-absent fallback (a dashboard served outside a
hyperhive deploy): base URL is
`http://<host>:<container.port>` (direct TCP). Forge
links resolve against `state.forge_public_url` (sourced from
`services.hyperhive.swarm.forge.publicUrl`) and disappear entirely when
that's unset — never guessed from `<host>:3000`. External links are
already absolute. The same base URL drives the primary agent-name
link + favicon fetch, so the whole row routes through the gateway
as a unit.
<!-- vale write-good.Passive = YES -->
<!-- vale write-good.Passive = NO -->
**When the container is stopped** (`ContainerView.running = false`),
the dashboard skips the async `dashboard-state` fetch entirely (the agent
web server is down), so a single badge replaces the badge chain,
the nav strip is empty, and status text / rate-limited / ctx
badges disappear. The agent icon goes straight to the dimmed
`/favicon.svg` fallback instead of attempting a doomed load from the
container's URL. Static fields — `needs_update`, `deployed_sha`,
`pending_reminders`, `parent`, `config` link — remain visible
regardless of run state.
<!-- vale write-good.Passive = YES -->
**Line 2** — status badges only (no per-card action buttons — actions
live in the **selection bar** or the **per-agent `⋮` menu**, see
below).
### Status badges
**Which single badge:** `ContainerView.failed`
(systemd `ActiveState=failed` — the unit exhausted its bounded
restarts and gave up on its own) draws a red `✖ gave up` badge;
otherwise a plain muted `■ not running` — a container an operator
stopped deliberately. Both states read `running: false`; `failed` is
the orthogonal fact (a fifth one alongside `paused`/`needs_update`/
`needs_login`, same "independent flags, no state machine" shape —
see `ContainerView`'s own doc comment) that tells them apart. An
older hive-c0re without the field serves `failed: undefined`, which
reads falsy — degrades cleanly to the single `not running` badge.
When the container is running, status badges follow — `⊘ rate
limited` (red, while the harness parks itself after a 429), `needs
login`, `needs update` — plus **one `◐ pending-state…` pill per
active transient** (replaces buttons during operator-initiated
start / stop / restart / rebuild / destroy). An agent can carry
**multiple transients at once** — for example a lease-exempt `prebuild`
running alongside a `stop_for_update` on the same agent — and each
renders as its own independent badge instead of collapsing
into one label, matching the existing multi-badge convention this
line already uses for `paused`/`needs_update`/model/ctx.
Any pending badge means the row is actually **running** something
right now (see **Pending-badge derivation** below); every row
carrying ≥1 badge keeps the amber row tint AND draws a **rotating
amber ring** around the agent icon.
**Pending-badge derivation:** transients only (`transientsState`,
keyed `agent -> Map<kind, since_unix>`) — a transient is **derived
from a job-queue node currently `Running`** against that agent, not
declared per request, so its label follows the operation as it
progresses (a rebuild reads `stop_for_update`, then `swap`, then
`reconcile` rather than one constant `rebuilding` for its whole
life). Two consequences for anything rendering it:
- The label vocabulary is **open** — it's the node's own wire tag
(`NodeKind`'s `&str` conversion, the same strings the graph wire's node
labels carry), not a fixed set. Treat it as an opaque display
string; don't switch on specific values.
- It's **not** exclusively operator-initiated, and **not** limited
to rebuild-shaped work — `running_transients()` in hive-c0re is
a status-only test (any `Running` node whose payload names a
non-empty agent lights a pill), so work the operator never
triggered (a meta-update cascade, a crash-recover rebuild, a
lease-exempt `prebuild`) lights the same mechanism.
Ops with no queue node behind them (destroy, migration) supply
their own label directly via `TransientSet`/`TransientCleared`
events carrying no backing node at all.
**Queued (not-yet-started) work shows nothing on the card.** Only
running work gets a per-agent pending badge, by design — the swarm
view stays independent of job-queue internals. The queue-summary
banner below is the only queued-work indicator on this tab, and it
reads the narrow `/api/jobq/rollup` endpoint rather than the full
graph.
**Active model badge** (`model · <name>`, blue) appears when the
container is running and the harness has persisted a model name (the
`active_model` field of `hyperhive-harness.json`, the consolidated
harness state file in the agent's state dir). Read by hive-c0re's
`ContainerView` (`read_active_model`); absent until the agent has
completed at least one turn, and hive-c0re suppresses stale values
for stopped containers.
**`ctx · Nk` chip** shows the agent's last-turn context size,
populated from `DashboardState.ctx_tokens` (absent until the
agent has completed at least one turn). The chip colour (green /
yellow / red) keys off `DashboardState.context_window_tokens`
(the real context window for the model the agent last ran on,
authoritative from the agent side); the badge goes yellow ≥ 50%
and red ≥ 75% of that window, matching the harness compaction
watermarks. When the window value is absent the badge falls back
to fixed 100k / 150k thresholds.
**Per-agent `⋮` overflow menu** — a `⋮` button appears on the right
edge of each container row. Selecting it opens a small dropdown with
per-agent actions and navigation links. Contents:
- `↺ R3ST4RT` (running agents only) / `■ ST0P` (running only) /
`▶ ST4RT` (stopped only) — single-agent run-state toggles.
Identical to the bulk actions on the selection bar but operate
on one agent without requiring a selection.
- `↻ R3BU1LD` — always available; queues a rebuild for this agent.
- `DESTR0Y` / `PURG3` — destructive, each prompts for confirmation
via the themed dialog (see **Themed dialogs** below).
- `deployed:<sha> ↗` — present when the agent has a `deployed_sha`
and the forge is reachable; links the deployed commit on the forge
agent-configs mirror.
`↻ UPD4TE 4LL` button appears above the containers list when any
agent is stale. Banner pulses on each broker SSE event
(`pulseBanner` with a 4s grace timer).
**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
(`@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`
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 }>`,
every lifecycle state present in a fixed order, zero counts included —
rather than the full `/api/jobq/graph` tree: `running` sums the
`Running` and `Finishing` entries' `roots` (`Finishing` = own work
done, subtree still going, still in flight), `queued` reads the
`Pending` entry's `roots`. `roots` specifically, not `nodes` — the
banner means *N whole operations*, not raw steps (one rebuild is ~7
nodes but 1 root); `nodes` exists on the same endpoint for a consumer
that wants step-level counts instead, unused here.
### Themed dialogs
All confirmations and transient error notices use an in-app themed
dialog system (`@hive/shared/modal.js`) rather than the browser's
native `confirm()` / `alert()` chrome, so they match the Catppuccin
palette and the OS can't style them away. Two primitives, both built
on the `openDialog` core:
- `themedConfirm({ message, danger, confirmLabel, checkboxes })`
— a modal confirm that resolves to `null` on cancel or an object
of checkbox states on confirm. Destructive actions pass
`danger: true` (the confirm button turns red and the cancel
button takes focus). Selecting the backdrop and `Esc` both cancel.
- `themedToast(message, { type, ... })` — a non-blocking toast (top-right,
`info` / `error` / `ok`) for transient validation + action
failures, so an error doesn't trap the operator behind a modal.
Single-action errors autodismiss; bulk / partial-failure
summaries are sticky (select to dismiss) so they aren't missed.
Every destructive run-state action (`ST0P`, `R3ST4RT`, `R3BU1LD`,
`DESTR0Y`, `PURG3`) routes through `themedConfirm`, on both
the per-agent `⋮` menu and the bulk selection bar.
**Graceful stop** — the `■ ST0P` confirm dialog (per-agent and
bulk) carries a `stop gracefully — let the agent finish its turn
and flush state before the container stops` checkbox. When ticked,
the action POSTs `/api/kill/<name>?graceful=true` (the bulk path
appends the flag per-agent); unticked is the instant hard stop
(`/api/kill/<name>` with no query). hive-c0re enqueues a
`Signal` → `Drain` job-queue node pair (`NodeKind::Signal` /
`NodeKind::Drain`): `Signal` sets the graceful-stop fence and kicks
the harness so it runs one stop-checkpoint turn (so the agent can
flush `/state`); `Drain` awaits the harness clearing that fence,
bounded by a 3-minute timeout (`GRACEFUL_STOP_TIMEOUT`) that
resolves either way and falls back to the downstream mechanical
stop. The quiescing progress surfaces through the same
rebuild-queue pending-badge mechanism the card already reads for a
rebuild, driven by the `Signal`/`Drain` pair rather than a nix
build. (The `hivectl stop --graceful` CLI flag enqueues the same
`Signal`/`Drain` pair, so the dashboard and CLI paths behave
identically.)
## Selection bar
Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` /
`R3BU1LD` / `DESTR0Y` / `PURG3`) live here rather than as per-card
buttons — see **Container row** above. Selecting an agent's icon
toggles its selection (an in-memory `Set<name>`); `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
tabs don't show the agent cards needed to cross-reference.
When the operator selects one or more agents (via the icon), a sticky
frosted-mauve bar slides up from the bottom of the viewport
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
<!-- vale write-good.Passive = NO -->
- **Count + names** — "N agents selected · name1, name2, …"
- **Bulk action buttons** — only enabled when ALL selected agents
support the action; disabled with a tooltip naming the blockers
when the selection is mixed:
- `↺ R3ST4RT` — running agents only
- `■ ST0P` — running agents only
- `▶ ST4RT` — stopped agents only
- `⏸ P4US3` — unpaused agents only (orthogonal to running/stopped —
see **Container row**'s own pause/resume note)
- `▶ R3SUM3` — paused agents only
- `↻ R3BU1LD` — always available
- `DESTR0Y` / `PURG3` — always available
<!-- vale write-good.Passive = YES -->
That list is the whole set — the bar offers no other bulk action.
- **`✕ clear`** button + `Esc` key clear the entire selection.
Every render prunes stale selections (agents destroyed while
selected) before the bar appears.
## Dashboard endpoints
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
the pages menu's **api docs** entry), with the raw spec at `/api/openapi.json`.
It's a growing supplement, not yet a full replacement for the list
below — some endpoints aren't in it yet.
- `POST /api/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
`destroy` accepts `purge=on` to also wipe state dirs.
- `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs.
- `POST /api/update-all` — rebuild every stale container.
- `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry.
Refuses `Running` / terminal-state entries (in-flight
rebuilds can't be safely interrupted). Always 200; body is
`{"cancelled": true}` on a successful flip or
`{"cancelled": false}` when the entry wasn't in `Queued` state.
- `POST /api/agent/{name}/mark-all-read` — ack all pending broker
messages for `{name}`. Backfills `delivered_at` for rows
not yet delivered and sets `acked_at = now`. Returns
`{ "marked": N }`. Agent name validated against
`[a-z0-9_-]`, 1-63 chars; 400 on bad input.
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
headers for one agent, newest first. Returns
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
`started_at`, `finished_at`, `status` (`"ok"` / `"fail"` /
`null` while in-progress). `limit` defaults to 10, server-side
cap at 50. Agent name validated (`[a-z0-9_-]`, 1-63 chars).
- `GET /api/build-logs/id/{id}` — full build log by id. Returns
`BuildLogFull` (JSON): all header fields plus `stdout` and
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
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
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
build logs.
- `GET /static/marked.js` serves the vendored `marked` bundle used
for markdown previews.
- `GET /api/state-file?path=<host-or-container-path>` — bounded
text read of a file under the per-agent `state/` subtree or
the shared `/var/lib/hyperhive/shared/`. Accepts the
container-view forms (`/agents/<n>/state/...`, `/shared/...`)
and the host form. Canonicalises + verifies the path stays
inside the allow-list, refuses anything but a regular file,
refuses `/agents/<n>/claude` / `config` subtrees, truncates
bodies at 1 MiB. No dashboard page calls it since the flow page
stopped existing.
The broker forwarder still detects path tokens **server-side at
broker-message ingest**: it calls `scan_validated_paths(body)` —
same allow-list helper the read endpoint uses — and attaches
the verified file tokens to the event as `file_refs: Vec<String>`.
No dashboard page reads that list either.
- `GET /api/reminders` — list pending reminders for the
dashboard's queued-reminders panel.
- `GET /api/stats-hive?window=<1h|4h|24h|3d|7d|30d|all>` — hive-wide
turn-stats rollup for the ST4TS tab. Aggregates every agent's
`hyperhive-turn-stats.sqlite` read-only (skips missing / unreadable /
zero-turn dbs); returns swarm totals, a busiest-first per-agent
rollup, swarm model mix, and a labelled `est_cost_usd`. Window
defaults to `24h`.
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
failed (clears the failure state so the scheduler retries).
- `POST /api/meta-update` — `nix flake update` the selected
`meta/flake.lock` inputs, then rebuild the affected agents.
- `GET /api/dashboard/stream` — unified live event channel:
broker `sent` / `delivered`, plus the mutation events listed
below. Each frame carries `seq`.
<!-- vale write-good.Passive = YES -->
## Dashboard event channel
Wire vocabulary on `/api/dashboard/stream` (kind tag is in the JSON
payload):
- `sent` / `delivered` — broker traffic, mirrored from the
intra-process channel by a forwarder task. Both carry `id: i64`
(the broker row id) and `in_reply_to: Option<i64>` for thread
rendering.
- `transient_set` (name, transient_kind, since_unix) /
`transient_cleared` (name) — lifecycle action spinners. The
client ticks the elapsed-seconds badge off `since_unix`
client-side, no polling. `transient_kind` is an **open**
display string (the running node's own tag), not a fixed
enum — render it, don't branch on it.
- `container_state_changed` (container: ContainerView) /
`container_removed` (name) — per-row container mutations,
emitted by `Coordinator::rescan_containers_and_emit` from
the job queue's own node execution (`job_queue::exec`, for example
after a rebuild's stop/swap/start steps or a destroy's teardown
step) — and from the 10s
`crash_watch` poll. Client upserts/removes by name and
reads the pending overlay from `transientsState` since the
payload doesn't carry it.
- `rebuild_queue_changed` (seq) — **payload-free trigger**, fired on
every queue mutation (enqueue, state transition, dedup collapse,
terminal-history trim). Unlike the snapshot events below it ships
no state at all: the client re-fetches `GET /api/jobq/graph`, which
is where it reads the queue from cold too.
- `reminders_changed` (seq, reminders: `Vec<PendingReminder>`) —
full snapshot of all pending reminders. Emitted after every
reminder mutation: agent `remind` calls (`agent_server`),
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
with Reminder kind, and the scheduler tick after each delivery
batch (`reminder_scheduler`).
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. The K3PT ST4T3 tab subscribes.
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
subscribes; same shape as `GET /api/meta-inputs`.
- `meta_update_running` (running: bool) — emitted when a
`nix flake update` ripple starts or completes. The M3T4 1NPUTS
tab uses this to show/hide the "⏳ meta-update running" banner.
<!-- vale write-good.Passive = NO -->
`/api/state` is **only fetched on cold-load and on the few
forms that mutate non-event-derived state** (PURG3 +
meta-update, since tombstones + meta_inputs aren't event-
shaped yet). Every other section — transients,
containers —
derives from `/api/dashboard/stream` after the initial snapshot,
maintaining its own client-side store and applying events on
top.
<!-- vale write-good.Passive = YES -->
Generalised form helper: `form[data-confirm="…"]` pops
`confirm()` before submit.