The dashboard's top-of-page warning banner is gone. The warning registry
stays, because /health/ready, the swarm status view and the OTEL gauges
read it. What it feeds is now alerted on in Grafana:
- hyperhive.hive.warning{kind,level}: one series per raised warning, so an
alert names the cause. Raising a warning also logs one `hive warning
raised` line with the message.
- hyperhive.hive.nix_store.utilization: the used fraction of the nix
store's filesystem, probed in the gauge callback.
- Alerts: hh-hive-degraded on the existing degraded gauge, hh-hive-warning
per kind (disk excluded), hh-host-disk-high at 85% and
hh-host-disk-critical at 95%.
- pending_logins is now a registry warning, refreshed from the container
rescan, so it reaches the per-kind gauge.
Frontend: server_warnings leaves the state snapshot, and the dashboard,
flow and stats pages keep only the sticky chrome wrapper. The shared
hive-warn and WarnBanner components stay, since other views use them.
A module-eval case checks each alert's metric name against the exporter's
source, so a rename on either side fails the build.
707 lines
38 KiB
Markdown
707 lines
38 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. FL0W and ST4TS
|
|
are standalone pages 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 **flow** (`/flow.html`), **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 — FL0W / ST4TS
|
|
live on their own pages, 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.
|
|
|
|
The FL0W page uses a slim header (a `← dashboard` back-link + the page
|
|
title) rather than the dashboard tab strip — it's a standalone surface,
|
|
not a tab pane.
|
|
|
|
## 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,
|
|
same minimal chrome as `/flow.html`. 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.
|
|
|
|
## FL0W page (`/flow.html`)
|
|
|
|
A dedicated full-page terminal (not a tab pane — a separate HTML
|
|
page). Slim chrome: a `← dashboard` back-link, the FL0W
|
|
title, and the agent-filter select (see below). No dashboard tab
|
|
strip — FL0W is a standalone surface reached from the pages menu.
|
|
|
|
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
|
|
Cold load backfills the last ~200 messages from `/api/dashboard/history`;
|
|
live frames arrive on `/api/dashboard/stream`. Each row is one broker
|
|
event — `sent` or `delivered` — with `from → to: body`. When a `sent`
|
|
and `delivered` event for the same message arrive within 3 seconds
|
|
(immediate delivery to a live recipient), the page upgrades the row in
|
|
place (arrow becomes green ✓, title reads "sent + delivered") instead of
|
|
rendering two near-identical lines — genuine delivery latency (recipient
|
|
was busy) still appears as a second row. Each row carries `data-from` /
|
|
`data-to` attributes; an **agent filter select in the FL0W header** narrows
|
|
the timeline to messages involving the chosen agent (matched on `from` OR
|
|
`to`), with non-matching rows hidden (`.flow-hidden` class). The selection
|
|
persists in localStorage across reloads; new rows pick up the active filter
|
|
at render time. The dropdown populates from the live container list and
|
|
stays current on add/remove; a saved selection survives even if that agent
|
|
isn't currently listed.
|
|
|
|
The row is a `flex-wrap: wrap` container holding ts / arrow / from / sep
|
|
/ to chips inline; the **body wraps to its own full-width line below** the
|
|
chips (`flex: 1 1 100%`) so long timestamps and agent names never push the
|
|
body into a narrow trailing column. `min-width: 0` keeps
|
|
`word-break: break-word` effective so the body doesn't force the row wider
|
|
than its container. Sticky-bottom autoscroll + "↓ N new" pill. Below the
|
|
stream sits a terminal-style compose box: `@name` picks the recipient
|
|
(sticky via localStorage; autocomplete from the live container list,
|
|
Tab/Enter to confirm; `@*` broadcasts). `POST /api/op-send` drops
|
|
`{from:"operator", to, body}` into the broker; the resulting SSE frame
|
|
re-renders the terminal row. The operator addresses the root agent as `@root`.
|
|
|
|
## 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.
|
|
- `POST /api/op-send` (`to=<name>`, `body=<text>`) — drop an
|
|
operator-authored message into `<name>`'s inbox. `to=*` fans
|
|
out to every registered agent. Returns 200; the broker
|
|
`Sent` event re-renders the message-flow terminal without a
|
|
snapshot refetch. Used by the compose textbox under MESS4GE FL0W.
|
|
- `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. Selection-time backing for the inline path-link
|
|
preview.
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
The broker forwarder detects which tokens *are* path links
|
|
**server-side at broker-message ingest**, not client-side:
|
|
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>`.
|
|
The client trusts that list and linkifies only those tokens,
|
|
so directories, missing files, and forbidden subtrees never
|
|
become anchors. No probe endpoint, no client-side regex
|
|
heuristics. Historical messages get the same treatment on
|
|
`/api/dashboard/history` backfill.
|
|
- `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`.
|
|
- `GET /api/dashboard/history` — last ~200 broker messages
|
|
(wrapped as `{ seq, events }`) for the message-flow
|
|
terminal's backfill on page load.
|
|
<!-- 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. The dashboard message-flow terminal renders reply
|
|
rows with a `↳ reply` tag that scroll-highlights the parent
|
|
row on selection. Used by the message-flow terminal renderer.
|
|
- `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, message flow —
|
|
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.
|
|
|