New @hive/shared/jobq-rollup.js (JobqRollup.tsx + jobq-rollup.css), mirroring JobqGraph's shape exactly: JSX use plus an imperative mountJobqRollup(container, props) for a plain-.js call site. Fetches Vec<hive_jobq_wire::StateCount> off `endpoint`, sums Running+Finishing roots as "running" and Pending roots as "queued", renders nothing when both are zero. Optional `queueHref` adds a "view queue -> " link. Swapped dashboard's hand-rolled queue-summary banner (swarm.js) over to this component instead of keeping two parallel implementations - same "one shared component" pattern JobqGraph already set for the rebuild queue tree view. Mounted once into a new #jobq-rollup-section, kept as a sibling of (not inside) #containers-section since that section gets replaceChildren()-wiped on every container-state render, which would tear down and remount a Preact tree on every tick. Refreshed via the mount handle's .refresh() on rebuild_queue_changed, same as builds.js's JobqGraph handle. Also mounted in swarm-ui's /jobs page, above JobqGraph, with no queueHref (a link back to the page you're already on is noise) - the literal ask on hyperhive#3364. Verified: npm run build (whole workspace) and swarm-ui typecheck both clean, comment-block + issue-ref lints run manually, headless-chromium screenshots of both the dashboard SW4RM tab and swarm-ui's /jobs page against mocked /api/jobq/rollup payloads - banner renders identically in both, with and without the queue link as expected.
1498 lines
81 KiB
Markdown
1498 lines
81 KiB
Markdown
# Dashboard layout
|
||
|
||
> Part of [Web UI](../web-ui.md). See also:
|
||
> [Shape (shared)](shape.md) · [Per-agent page](agent.md)
|
||
|
||
|
||
The dashboard is served at `/dashboard.html` (with the home page 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 (`#swarm`, `#call`, `#system`,
|
||
`#permissions`, `#schedules`, `#settings`) drives which pane is
|
||
active; hash changes don't reload the page. FL0W, L0GS, and the optional
|
||
M4TR1X client are separate pages reachable from the H0M3 hub at `/`, not
|
||
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**: `◆ SW4RM ◆`, `◆ Y3R C4LL ◆`, `◆ P3RM1SS10NS ◆`,
|
||
`◆ SCH3DUL3S ◆`. In-page tabs only — the SYST3M panels moved to the
|
||
standalone **C0R3** page (`/core.html`), and FL0W / L0GS / ST4TS /
|
||
S3TT1NGS / M4TR1X live on their own pages too, all reachable from the
|
||
H0M3 hub (not the tab strip). Count pills on SW4RM
|
||
(container count), Y3R C4LL (pending approvals + questions + unread
|
||
operator messages), and SCH3DUL3S (active schedules); S3TT1NGS has no
|
||
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, L0GS,
|
||
H0M3, C0R3, BU1LDS, CR3D3NTIALS, ST4TS, S3TT1NGS), injected at the top
|
||
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
|
||
`state.server_warnings` — a list of `{ kind, level, message }` — and
|
||
coloured by `level` (`warn` amber / `crit` red). The backend owns the
|
||
threshold + message, so adding a new system warning needs no frontend
|
||
change. Warnings are a push-based registry (`hive-c0re`'s
|
||
`warnings.rs`): any subsystem raises/clears its own entry via an RAII
|
||
guard, and `host_stats::server_warnings()` is just a cheap snapshot of
|
||
that registry. Producers today include the host disk-pressure check
|
||
(a `statvfs` probe of `/nix`: ≥85% used → `warn`, ≥95% → `crit`, e.g.
|
||
`⚠ host nix store N% full (G GiB free) — garbage-collect …`), forge
|
||
provisioning/CI-runner boot failures, and agent-state warnings
|
||
(`pending_logins`, `agents_crashing`) computed alongside the container
|
||
snapshot. Hidden when there are no warnings.
|
||
- **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated
|
||
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
|
||
arrive in the state snapshot. When there are pending approvals or
|
||
unanswered questions, a `(N)` prefix is prepended — `(3) pr1ma //
|
||
h1ve-c0re` — so the operator can see the call count in an unfocused
|
||
browser tab without opening the dashboard. The prefix is set on the
|
||
initial `/api/state` cold-load and updated live by
|
||
`approval_added` / `approval_resolved` / `question_added` /
|
||
`question_resolved` SSE events; it's preserved when
|
||
`hive_name` / `swarm_name` later replace the raw title.
|
||
|
||
The FL0W and L0GS pages use a slim header (a `← home` back-link + the
|
||
page title) rather than the dashboard tab strip — they're standalone
|
||
surfaces, not tab panes.
|
||
|
||
## SW4RM tab
|
||
|
||
**C0NTAINERS** — live containers rendered as a depth-first
|
||
tree using `ContainerView.parent` (populated by
|
||
`hive-c0re/src/agent_config/topology.rs` — not to be confused with
|
||
`hive-c0re/src/dashboard/topology.rs`, which only holds the
|
||
set-parent endpoints).
|
||
Each container's row is prefixed with ASCII tree glyphs (`├─`,
|
||
`└─`, `│ ` continuation columns) showing the agent
|
||
parent/child hierarchy. When every container has `parent = null`
|
||
(flat topology) the tree collapses to a plain list with no
|
||
glyphs. Children are sorted alphabetically within each parent;
|
||
roots likewise. Cycles in the parent graph are tolerated —
|
||
orphaned containers (not reachable from any root) are appended
|
||
as roots so no agent disappears. 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.
|
||
|
||
## Y3R C4LL tab
|
||
|
||
Things blocked on operator decision — approvals and questions
|
||
share a tab because they're the same concept ("something is
|
||
waiting on you").
|
||
|
||
**P3NDING APPR0VALS** — the queue (see "Approval card" below).
|
||
The R3QU3ST SP4WN form lives at the top of this section. A
|
||
`pending · N` / `history · N` tab pair switches between the live
|
||
queue and the last 30 resolved approvals (see "Approval card" for
|
||
the history row shape).
|
||
|
||
**M1ND H4S QU3STI0NS** — pending `ask` calls waiting on the
|
||
operator, with amber pulsing border. Anatomy of each card:
|
||
|
||
- **Filter chips** — `all · N`, `@operator · N`, `@peer · N`,
|
||
plus one chip per participant name (`@asker · N` / `@target · N`).
|
||
Every chip shows its own count so the operator can see the
|
||
distribution at a glance. Clicking a chip narrows the visible
|
||
list; selection persists in localStorage so a tab switch doesn't
|
||
lose the filter.
|
||
- **Question card** — timestamp · asker → target · body text
|
||
(with file-path links). Operator-targeted questions (`target =
|
||
null`) show `▸ ANSW3R`; peer-targeted questions (`target =
|
||
agent`) show `⤿ 0V3RR1D3` so the operator can unblock an
|
||
agent-to-agent exchange. Questions with a `ttl_seconds` show a
|
||
`⏳ MM:SS` live countdown chip; the host-side watchdog resolves
|
||
with `answerer = "ttl-watchdog"` on expiry.
|
||
- **Answer form** — free-text textarea (Enter = submit,
|
||
Shift+Enter = newline) + optional option list (radio for
|
||
single-select, checkboxes for `multi=true`). Submit merges
|
||
selected options + free text comma-joined into a single
|
||
`answer` field. `✗ CANC3L` is a separate form so the submit
|
||
merge handler doesn't interfere.
|
||
- **◆ answ3red (N)** — collapsible `<details>` below the pending
|
||
list; shows the last 20 resolved questions with their answers.
|
||
|
||
**0PER4T0R 1NB0X** — messages agents have sent to `to="operator"` but
|
||
the operator hasn't read yet. Cold-loaded from `/api/operator-inbox` on
|
||
tab activation + page load; appended live from the broker `sent` stream
|
||
(deduped on row id). Each row shows sender · timestamp · body (with
|
||
file-path linkification). A `✓ mark all read` button on the right acks
|
||
all rows via `POST /api/agent/operator/mark-all-read` (reuses the existing
|
||
mark-read endpoint). Unread count folds into the Y3R C4LL tab pill so
|
||
messages are visible from any tab even while inactive. Backed by
|
||
`GET /api/operator-inbox` → `{ messages: [...] }` (id, from, body, at,
|
||
in_reply_to, file_refs).
|
||
|
||
## C0R3 page (`/core.html`)
|
||
|
||
Passive / rare-interaction state. No longer a dashboard tab — it's a
|
||
standalone page reached from the **Core** tile on the H0M3 hub (served at
|
||
`/core.html`), with the same minimal chrome as `/logs.html`: a `← home`
|
||
back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default,
|
||
then **C0NT41N3R L04D**, then **1NFR4**). 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 have moved to the BU1LDS
|
||
page — see below.)
|
||
|
||
**K3PT ST4T3** — two sub-sections on one pane:
|
||
|
||
*Tombstones*: agents with kept state and **no container** (size + age +
|
||
claude-creds badge). Two actions: `⊕ R3V1V3` (queues a Spawn
|
||
approval; existing state is reused), `PURG3` (wipes state + applied
|
||
dirs; `POST /api/purge-tombstone/{name}`).
|
||
|
||
⚠️ **Not only *destroyed* agents.** Nothing records a destroy: every
|
||
definition-side artifact (state subvolume, proposed + applied repos,
|
||
meta registration, topology entry) is written by `Provision` *before*
|
||
the container exists and survives `lifecycle::destroy`. So an agent
|
||
part-way through a spawn is byte-identical on disk to a tombstone, and
|
||
both are listed. 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 the backend can actually prove. Fixing it properly
|
||
needs a recorded destroy: **#3020**, deferred to the swarm-controller /
|
||
snapshot-storage rework where the problem changes shape.
|
||
|
||
*Stale permission entries*: agents with explicit capability or
|
||
tool-group JSON entries but no live container — typically renamed or
|
||
deleted agents whose entries persisted in `capabilities.json` /
|
||
`tool-groups.json`. Lazy-loaded on first K3PT ST4T3 tab activation;
|
||
auto-refreshes on `capabilities_changed` + `tool_groups_changed` SSE
|
||
events. Each ghost agent gets a `✕ clear perms` button
|
||
(`DELETE /api/permissions/{name}`).
|
||
|
||
**C0NT41N3R L04D** — live CPU + memory per agent container, read
|
||
straight from cgroup v2 on the host (`cpu.stat`, `memory.current`,
|
||
`memory.peak`, `memory.max` under
|
||
`/sys/fs/cgroup/machine.slice/machine-h\x2d<name>.scope/`). CPU is a
|
||
host-normalised percentage (0..100 across all cores) sampled over a
|
||
short (~200 ms) two-read interval; memory shows current + peak with a
|
||
bar against the `memory.max` quota. Backed by
|
||
`GET /api/container-resources` (`container_stats.rs`), which reads the
|
||
files read-only (world-readable; no `hive-priv`) and skips agents whose
|
||
scope dir is absent (= not running). Pull-only: `core.js` polls every
|
||
5 s **only while the C0NT41N3R L04D sub-tab is active** (CPU needs a fresh
|
||
sample each refresh), and stops on sub-tab change. Disk size (`disk_bytes`)
|
||
rides the same row but is fed by a separate ~5 min background `du` sampler
|
||
(state dir + container writable rootfs, shared nix store excluded via
|
||
`du -x`), so the 5 s poll stays cheap cgroup-only reads; the row carries the
|
||
last-sampled value (`null` until the first sample). Network is intentionally
|
||
omitted — agents share the host netns, so there is no per-container net
|
||
counter (per-agent network needs the netns-isolation roadmap in
|
||
`docs/network.md`).
|
||
|
||
**1NFR4** — start / stop / restart the four hive infrastructure services
|
||
(`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`) directly from the
|
||
dashboard, without needing an `infra_admin` agent's `restart` MCP tool.
|
||
Three are containers; `hive-gateway` is the host's `nginx.service`, and is
|
||
the one an agent may **not** restart — this panel is the way it gets
|
||
bounced. One row per service: name, a `badge-ok`/`badge-fail`
|
||
running/stopped dot, and `↺ R3ST4RT` + `■ ST0P` (running) or `▶ ST4RT`
|
||
(stopped) buttons, same themed-confirm pattern as the K3PT ST4T3
|
||
tombstone actions. Backed by
|
||
`POST /api/infra-container/{name}/{action}` (`action` ∈
|
||
`start|stop|restart`), which calls the same
|
||
`priv_client::control_infra_container` helper the agent-facing
|
||
`infra_admin` path uses — no new privileged-helper surface, no
|
||
capability check (the dashboard is already operator-authenticated).
|
||
Every attempt is written to the audit log (actor `"operator"`, action
|
||
`start_infra`/`stop_infra`/`restart_infra`) alongside agent-driven infra
|
||
restarts. Status rows ride the `infra_containers` field on
|
||
`GET /api/state`'s `StateSnapshot` (`{name, running}`, live
|
||
`systemctl is-active container@<unit>.service` read); `core.js` polls
|
||
`/api/state` every 5 s only while the 1NFR4 sub-tab is active, same
|
||
cadence/lifecycle as C0NT41N3R L04D's polling.
|
||
|
||
## BU1LDS page (`/builds.html`)
|
||
|
||
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
|
||
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` does not 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 — it carried a typed queue snapshot
|
||
until every consumer had moved to the generic endpoint, and now carries
|
||
none; 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 are meant to present exactly what the endpoint provides rather
|
||
than reconstruct chrome the backend no longer sends. Settled entries
|
||
render their **full step tree**, not just a bare summary — the wire
|
||
does not filter `Done` nodes out.
|
||
|
||
**State filter (hyperhive#2606).** 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 a group is
|
||
kept or dropped whole, never split 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; the
|
||
`states` param is omitted entirely (identical request to before this
|
||
filter existed) when every state is checked. 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` is set — 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 — `follows` aliases and rev-less
|
||
nodes are skipped. 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 the
|
||
update button is disabled (snapshot field `meta_update_running`,
|
||
live event `meta_update_running`).
|
||
|
||
**BUILD L0GS** — all-agents build log history (moved from `/logs.html`).
|
||
Lazy-loaded on first tab activation; auto-refreshes 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 auto-scroll
|
||
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
|
||
entry with that id pre-expanded.
|
||
|
||
## CR3D3NTIALS page (`/credentials.html`)
|
||
|
||
Operator surface to provision per-agent credentials without editing the
|
||
agent's config repo. Standalone page reached from the **Credentials** tile
|
||
on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link
|
||
+ a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather
|
||
than `/core.html`'s plain title. Its own esbuild bundle
|
||
(`credentials.js`); no SSE — it reads `/api/state` once for the (shared)
|
||
agent picker and otherwise works off purpose-built endpoints per tab.
|
||
Three sub-tabs:
|
||
|
||
### MATRIX tab
|
||
|
||
Provision / log in a per-agent **external** matrix account and store its
|
||
access token (this half is unchanged from the old `/matrix-accounts.html`
|
||
page it replaces — only the URL and surrounding chrome moved).
|
||
|
||
An agent picker (populated from `state.containers`, the live roster) drives a list of that
|
||
agent's accounts — name, homeserver, user id, and a status dot —
|
||
read from `GET /api/matrix-accounts?agent=<name>` →
|
||
`{ accounts: [ { name, homeserver, token_present, live, user_id } ], as_of_unix }`.
|
||
`token_present` is whether a token is **stored**; `live`, `homeserver`,
|
||
and `user_id` are backfilled from the matrix daemon's
|
||
`matrix-accounts.json` snapshot — a host-visible file the daemon
|
||
**force-rewrites every ~30s** (a heartbeat), so `as_of_unix` (the
|
||
snapshot mtime) advances while the daemon is alive and a *stalled* value
|
||
genuinely means "stopped publishing", not just "old snapshot". An account
|
||
with a token but absent from the snapshot reports `live: false`.
|
||
|
||
The status dot renders these states:
|
||
|
||
- **green** — `live` and the container is running: online.
|
||
- **dim green** — `live` but `as_of_unix` hasn't advanced in > ~90s (3
|
||
missed heartbeats) while the container is *not* down: the daemon stopped
|
||
publishing, so the snapshot's `live` is no longer trustworthy (likely
|
||
dead/wedged). Labelled "online · no heartbeat".
|
||
- **amber** — `live` but the container is **down** (a stopped container
|
||
⟹ a dead daemon, so the snapshot is stale); also the `token_present &&
|
||
!live` "provisioned but offline" case.
|
||
- **grey** — no token (not provisioned).
|
||
|
||
The container-down cross-reference (`/api/state`) takes precedence over
|
||
the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout
|
||
so freshness is always legible. When `live` is absent (an older backend
|
||
without the snapshot) the dot falls back to a token-present rendering.
|
||
|
||
The provision form (account name, homeserver, login method) posts
|
||
`POST /api/matrix-account-login` (`x-www-form-urlencoded`, operator-auth):
|
||
fields `agent, account, homeserver, mode=password|token, user_id?,
|
||
password?, token?` → `200 { ok, user_id }` on success. Failures come back
|
||
as RFC 9457 `application/problem+json` (`{ type, title, status, detail }`)
|
||
with the human-readable message in `detail` and the status code reflecting
|
||
the cause (400 for a validation error, 500 for a login / `whoami` /
|
||
internal failure); the page reads `detail` for display. The host coordinator performs the login
|
||
(password) or validates the token (`whoami`) and writes the bearer to
|
||
the agent's `matrixAccounts.<account>.tokenFile` via the same
|
||
privileged write path as the hive-internal `matrix-token`; the token is
|
||
**never** echoed back, and the page clears the secret inputs on submit
|
||
regardless of outcome. The account list reflects what is *provisioned*
|
||
(an account with a stored token), so a config-declared-but-unprovisioned
|
||
account appears only once it has been provisioned through the form.
|
||
|
||
### GITHUB tab
|
||
|
||
Provision a single per-agent GitHub personal access token (see
|
||
[`docs/github.md`](../github.md) for the injection + `gh`/git-push
|
||
mechanics). No login flow — the operator pastes an existing PAT for a
|
||
dedicated bot account, with a security-warning banner (dedicated account +
|
||
minimally-scoped token) and a link to
|
||
[github.com/settings/tokens](https://github.com/settings/tokens).
|
||
|
||
Status reads `GET /api/github-account?agent=<name>` →
|
||
`{ present: bool }` — whether the agent's `github-token` file exists.
|
||
There's no live/heartbeat concept for a static PAT, so this is just a
|
||
"token stored ✓" / "not set" line, unlike MATRIX's status-dot taxonomy.
|
||
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
|
||
`token`) → `200 { ok: true }` on success, or the same `error_response`
|
||
shape `/api/matrix-account-login` uses on failure. The token is never
|
||
echoed back in either direction.
|
||
|
||
### FORGES tab
|
||
|
||
Store a **label + base URL + access token** for an external Forgejo/Gitea/
|
||
Codeberg-compatible forge, per agent. Entirely dashboard-provisioned —
|
||
there is no host-side nix config for this (no `services.hyperhive.
|
||
extraForges` option). The operator creates the token on the external forge
|
||
themselves (however that forge lets them — PAT UI, a teammate with admin,
|
||
whatever) and pastes label/URL/token into the form; hive-c0re never talks
|
||
to the external forge's API and never creates an account there.
|
||
|
||
The selected agent's stored forges come from `GET /api/extra-forges?
|
||
agent=<name>` → `{ forges: [{ label, base_url }] }`, derived by scanning
|
||
the agent's state dir for `forge-<label>-token` files (mirrors the MATRIX
|
||
tab's filename-scan listing) with `base_url` backfilled from a sibling
|
||
`forge-<label>.json` sidecar. Submitting the add form posts `POST /api/
|
||
extra-forge-account` (form-encoded `agent, label, base_url, token,
|
||
action=add`) → `200 { ok: true }`, which writes both files through the
|
||
same privileged write path as the other tabs. Each row's `remove` button
|
||
opens a themed confirm dialog, then posts the same endpoint with
|
||
`action=remove`, deleting both local files — nothing changes on the
|
||
remote forge. The token is never echoed back in either direction.
|
||
|
||
A per-forge `hive-forge --forge <label>` CLI selector (to make `hive-forge`
|
||
target one of these accounts instead of the internal forge) is a
|
||
deliberate non-goal of this tab — tracked separately.
|
||
|
||
## P3RM1SS10NS tab
|
||
|
||
Per-agent permission configuration. Two sections, each rendered as a
|
||
column-driven checkbox matrix: rows are agents, columns are the
|
||
permission names fetched from the backend. The column list is
|
||
authoritative — adding a new tool-group or capability to the backend
|
||
requires no UI change; the new column appears automatically.
|
||
|
||
The snapshot carries `agents` (the full manageable roster — live
|
||
containers ∪ agents with an explicit entry) and `effective` (per-agent
|
||
explicit-or-role-default values) alongside the explicit `assignments`
|
||
map. Rows come from `agents` so **agents on defaults always appear**
|
||
(not just those with an explicit entry), and checkboxes reflect the
|
||
`effective` values so a default agent shows the groups it actually runs
|
||
with rather than blank — which also means saving it won't silently
|
||
strip those defaults. The `(default)` badge keys off absence from
|
||
`assignments` (no explicit entry).
|
||
|
||
Fetches fire on tab activation (not page-load) to avoid unnecessary
|
||
work when the operator never visits this tab. Live mutations from the
|
||
rebuild-queue worker are also pushed via the `capabilities_changed` /
|
||
`tool_groups_changed` SSE events (same payload shape as the GET
|
||
endpoints), so an open P3RM1SS10NS tab reflects worker-applied changes
|
||
without requiring navigation. Tab-activation re-fetches remain as a
|
||
safety net for reconnect windows.
|
||
|
||
**C4P4B1L1T13S** — per-agent capability grants. Capabilities unlock
|
||
gated MCP tools and system-level access beyond the default agent
|
||
surface. A saving POST queues a rebuild for the affected agent so the
|
||
new `HIVE_CAPABILITIES` env var takes effect in the next session.
|
||
|
||
The current capabilities are:
|
||
|
||
| Name | Effect |
|
||
|------|--------|
|
||
| `manage_root_agent` | allows the `set_status` / lifecycle tools on the root agent |
|
||
| `read_host_journal` | unlocks `get_host_journal` to read journald from inside a container |
|
||
| `query_agent_state` | allows `get_loose_ends(agent: "<name>")` calls targeting other agents |
|
||
| `infra_admin` | allows `restart` on hive infrastructure containers (`hive-ci`, `hive-forge`, `hive-matrix`; the gateway is operator-only); each restart is logged to the AUDIT trail |
|
||
|
||
Each row is one agent. Columns are the capability names returned by
|
||
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
|
||
boxes only stages the change in-browser; nothing is written until the
|
||
page-level **save all** button (described below) is clicked. Row
|
||
values follow the `effective`/`assignments` rule described above.
|
||
|
||
**T00L GR0UPS** — per-agent tool-group permissions. Tool groups are
|
||
named buckets of MCP tools; each agent starts with a role default
|
||
(sub-agents: `messaging`, `meta`, `inbox`, `execution` —
|
||
`ToolGroup::AGENT_DEFAULT`; root agent is seeded to
|
||
`ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`,
|
||
`lifecycle`, `approvals`, `scheduling`, `diagnostics`, `execution`,
|
||
i.e. every group except `forge` and `web_tools`). Checking /
|
||
unchecking stages which groups are active for the agent; the
|
||
page-level **save all** button (below) commits it. Columns come from
|
||
`GET /api/tool-groups`. A rebuild is queued so `HIVE_TOOL_GROUPS`
|
||
takes effect.
|
||
|
||
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
|
||
`approvals`, `scheduling`, `diagnostics`, `forge`, `execution`,
|
||
`web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
|
||
`web_tools` group is special: it carries no MCP tools; instead it adds
|
||
Claude's built-in `WebFetch` and `WebSearch` to `--tools` /
|
||
`--allowedTools` for that agent session.
|
||
|
||
Both tables share the same visual shape: `.cap-table-wrap` /
|
||
`.tg-table-wrap` outer scroll container, `thead` with a label column
|
||
(`.cap-agent-col` / `.tg-agent-col`) + one column per permission
|
||
(`.cap-col` / `.tg-group-col`). Each tbody row is one agent: a name cell
|
||
and its checkbox cells, where each checkbox carries `data-baseline` (its
|
||
render-time state) and the row carries `data-agent` for dirty-tracking.
|
||
|
||
**Saving — one button for the whole page.** There are no per-row save
|
||
buttons. A single page-level `.perm-save-bar` with a **save all (N
|
||
agents)** button sits at the bottom of the pane, enabled only when some
|
||
checkbox diverges from its baseline. Clicking it diffs every checkbox
|
||
across *both* matrices and POSTs one batch to `POST /api/permissions` as
|
||
`{ changes: [ { agent, tool_groups?, capabilities? } ] }` — only the
|
||
perm-types that actually changed for each agent are included (an omitted
|
||
field leaves that file untouched; an included array fully replaces it).
|
||
The backend coalesces an agent's capabilities + tool-groups into a
|
||
single rebuild, so changing both for one agent is one rebuild, not two.
|
||
The batch is atomic: it validates every change first and on any error
|
||
rejects the whole POST (`{error}`, nothing applied); a clean 200 (`ok`)
|
||
flips the bar to a queued→rebuilding state and re-fetches both tables.
|
||
Live `capabilities_changed` / `tool_groups_changed` events re-render the
|
||
matrices unless the section has unsaved edits, so an in-progress edit set
|
||
isn't clobbered.
|
||
|
||
## SCH3DUL3S tab
|
||
|
||
Anything that fires at a future time. Operator-set schedules
|
||
are created inline in the table (last row); agent self-paced
|
||
reminders surface at the bottom as a sibling list — they
|
||
share enough conceptual ground to live together.
|
||
|
||
**N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed
|
||
scheduled prompts. **Single-table layout**: each schedule is
|
||
one `<tr>`; columns are
|
||
`# | src | next | every | owner | body | …agents… | actions`.
|
||
Agent columns are dynamic — `operator` + `root` + every
|
||
live container + any extra name that appears as a target on
|
||
some schedule but isn't a current container (same
|
||
`buildTargetChips` membership rule the new/edit forms use,
|
||
so table and forms agree on what's addressable). Column
|
||
headers tilt -45° via CSS so each column reads as a narrow
|
||
~28px strip; per-agent cells render as:
|
||
|
||
- **active target** → `<button>✓</button>` that cancels just
|
||
that one target on click
|
||
- **cancelled target** → muted `✕` glyph (no button —
|
||
re-adding goes through the edit form's targets multi-select)
|
||
- **not a target** → empty cell
|
||
|
||
Per-schedule action column:
|
||
|
||
- **`↯ fire now`** — sends an out-of-band manual pulse to every
|
||
active target (recurring schedules keep their cadence; one-shots
|
||
are consumed after the manual fire). Disabled for paused schedules.
|
||
- **`⏸` / `▶` pause/resume toggle** — only on recurring schedules
|
||
(one-shots may also be paused to delay a one-time fire, so the
|
||
button always appears). Paused rows show a `⏸ paused` badge in the
|
||
`next` column and are sorted to the middle bucket (after active,
|
||
before cancelled). A paused schedule does not fire regardless of
|
||
`next_fire_at`; resuming (`▶`) clears the pause without adjusting
|
||
the next-fire time. POSTs to `/api/schedules/{id}/pause` or
|
||
`/api/schedules/{id}/resume`.
|
||
- **`✎ edit`** — expands an inline edit form as a colspan'd row
|
||
directly under the schedule's row (body / description / interval /
|
||
next-fire / targets all editable; targets are a multi-select diff'd
|
||
against the original active set so unchecked-was-active =
|
||
`targets_remove`, checked-not-originally-active = `targets_add`;
|
||
submit PATCHes `/api/schedules/{id}`).
|
||
- **`✕`** — cancels the whole schedule
|
||
(`POST /api/schedules/{id}/cancel`).
|
||
|
||
The `next` column cell (`.sched-due`) carries a `data-due-at`
|
||
Unix timestamp attribute; a shared 1s ticker rewrites it
|
||
in-place showing `fmtDuration` while in the future and
|
||
`overdue X ago` once the fire time has passed — same
|
||
zero-re-render pattern as the reminder due-at labels and the
|
||
question TTL chip.
|
||
|
||
The table's last row is a permanent inline creation row:
|
||
inputs live directly in table cells (targets as checkboxes,
|
||
body textarea that expands on focus, datetime-local pre-filled
|
||
to 5 minutes from now, mini d/h/m/s number inputs (blank or
|
||
all-zero = one-shot), description). Click `+` to POST to
|
||
`/api/schedules` as JSON (or `⌫` to clear the half-filled row);
|
||
carry-state preserves partially-typed inputs across re-renders. The tab pill shows the count of active
|
||
schedules (at least one live target not yet cancelled).
|
||
Refreshed on tab activation and after each submit/cancel. Backed by
|
||
`GET /api/schedules`. No backend changes for the table layout
|
||
— it renders entirely from existing `schedulesState` +
|
||
`containersState`.
|
||
|
||
**QU3U3D R3M1ND3RS** — reminders agents have scheduled for
|
||
themselves (via the `remind` tool) but not yet delivered.
|
||
Each row shows the owner, due time, and message; a `CANC3L`
|
||
button hard-deletes (`POST /api/cancel-reminder/{id}`) and a
|
||
`R3TRY` button re-arms one whose delivery failed
|
||
(`POST /api/retry-reminder/{id}`). Backed by `GET /api/reminders`.
|
||
Lives in the SCH3DUL3S tab alongside operator schedules so the
|
||
operator has one place for everything time-fired. The due-time
|
||
label (`.reminder-due`) carries a `data-due-at` Unix timestamp
|
||
attribute; a shared 1s ticker rewrites it in-place — showing
|
||
`in Xm Ys` while the reminder is in the future and
|
||
`overdue X ago` once the deadline passes — without triggering a
|
||
full re-render of the list.
|
||
|
||
## 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 **Stats** tile on the H0M3
|
||
hub, same minimal chrome as `/flow.html` / `/logs.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 — missing / unreadable / zero-turn dbs are skipped so one
|
||
bad db never fails the endpoint. This is a **pull** surface (no SSE):
|
||
the data is fetched 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 is
|
||
labelled accordingly. The table is operator-tunable via the
|
||
`services.hyperhive.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.
|
||
|
||
## S3TT1NGS tab
|
||
|
||
Operator-local preferences. State lives in the browser's
|
||
`localStorage` — preferences do NOT sync between devices and
|
||
do NOT survive a profile wipe. Two sections today (browser
|
||
notifications, agent terminal); future preferences (theme, density,
|
||
etc.) land here as sibling `<h3>` blocks in `settings.html`.
|
||
|
||
**◇ browser notifications** — `🔔 enable notifications` button when
|
||
permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted
|
||
(mute silences the dispatch without revoking the OS-level
|
||
permission). On unsupported origins (non-secure context, or
|
||
browsers without the `Notification` API) the controls hide and a
|
||
single status line explains why. See `### Browser notifications`
|
||
below for the dispatch model + the three signals the dashboard
|
||
emits OS notifications on.
|
||
|
||
**◇ agent terminal** — a single `☐/☑ expand tool output panels`
|
||
toggle button (`role="switch"`, live `aria-checked`). Controls whether
|
||
every per-agent page's terminal defaults otherwise-collapsed
|
||
`<details>` rows (long tool-results, Write/Edit diffs, …) open; rows
|
||
that already default open regardless (send/ask/answer/recv) are
|
||
unaffected either way. Pure client-side — the key + get/set live in
|
||
`@hive/shared/prefs.js` (`getExpandDetailsPref`/`setExpandDetailsPref`)
|
||
so this page and every agent page's `app.js` read/write the exact same
|
||
`localStorage` key without a backend field; the shared terminal
|
||
factory reads it live via its `expandDetails` option (see
|
||
docs/web-ui/shape.md::Shared terminal pane), so a change here applies
|
||
to any already-open agent tab's next rendered row without a reload.
|
||
|
||
The FL0W page does NOT host this pane — settings live only on the
|
||
dashboard's S3TT1NGS tab (reach it via the FL0W page's `← home`
|
||
back-link → Dashboard). Notifications still fire on the FL0W page when
|
||
they're enabled here, because `NOTIF.show()` in
|
||
`common.js` depends on `Notification.permission` + the
|
||
`hyperhive.notify.muted` localStorage key, not on the buttons
|
||
existing in the page DOM.
|
||
|
||
## M4TR1X page (`/matrix/`, optional)
|
||
|
||
A static matrix web client (default `pkgs.fluffychat-web` rebuilt
|
||
with `--base-href /matrix/`, swappable via
|
||
`services.hyperhive.swarm.matrix.gui.package`) served by the hive-gateway
|
||
nginx container at `/matrix/` when
|
||
`services.hyperhive.swarm.matrix.gui.enable` is on (defaults to
|
||
`matrix.enable`). c0re signals availability via the
|
||
`HIVE_MATRIX_GUI_ENABLED` env var → `state.matrix_gui_enabled` in
|
||
`/api/state`; the gateway does the actual static serving.
|
||
|
||
The operator opens `/matrix/` from the Matrix tile on the H0M3 hub, logs
|
||
in once with the in-host tuwunel homeserver URL
|
||
(`http://localhost:8008` or whatever the matrix module exposes).
|
||
|
||
The unified nginx-front re-root to
|
||
`https://chat.${hyperhive.swarm.domain}` + `.well-known/matrix/client`
|
||
auto-discovery lives in `docs/gateway.md` (atlas's lane).
|
||
|
||
## FL0W page (`/flow.html`)
|
||
|
||
A dedicated full-page terminal (not a tab pane — a separate HTML
|
||
page). Slim chrome: a `← home` back-link to the H0M3 hub, the FL0W
|
||
title, and the agent-filter select (see below). No dashboard tab
|
||
strip — FL0W is a standalone surface reached from the hub.
|
||
|
||
The operator inbox is **not** on this page — it lives on the
|
||
dashboard's Y3R C4LL tab (◆ 1NB0X ◆ section, with per-message and
|
||
mark-all read). FL0W stays the pure event firehose.
|
||
|
||
**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 row is upgraded 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 auto-scroll + "↓ N new" pill. Below the
|
||
stream sits a terminal-style compose box: `@name` picks the recipient
|
||
(sticky via localStorage; auto-complete 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 root agent is addressed as `@root`.
|
||
|
||
## H0M3 page (`/`)
|
||
|
||
The H0M3 hub is the primary landing page (served at `/` by default). A
|
||
responsive grid of link tiles — Dashboard, Flow, Logs, Matrix (when enabled),
|
||
Forge (when enabled), among others (Builds, Stats, Settings, Core,
|
||
Credentials, API) — each pointing to their respective surfaces. The **API**
|
||
tile always shows (unlike Matrix/Forge, see below) and links to the
|
||
Swagger UI at `/api/docs`. The page
|
||
is a pure portal with no tab-bar or SSE subscriptions. Typography + colours
|
||
inherit from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
|
||
Optional tiles are hidden until `home.js` confirms their availability:
|
||
Matrix is hidden until `home.js` confirms `matrix_gui_enabled` (same gating as
|
||
the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms both
|
||
`state.forge_present` **and** `state.forge_public_url` (sourced from
|
||
`services.hyperhive.forge.publicUrl`, which defaults to the gateway vhost URL
|
||
when `behindGateway=true` and is `null` otherwise) and fills the href from the
|
||
latter — never guessed from the operator's browser hostname + a container
|
||
port, which is only right by accident off plain localhost. Operators without
|
||
matrix or forge enabled — or with forge on but no public URL configured —
|
||
never see a dead or wrong link. `home.js` also fills the swarm/hive identity
|
||
line at the top. All dashboard sub-pages include a `← Home` back-link for
|
||
navigation.
|
||
|
||
## L0GS page (`/logs.html`)
|
||
|
||
A dedicated log-viewer page (not a tab pane — a separate HTML page),
|
||
reachable from the Logs tile on the H0M3 hub. Minimal chrome:
|
||
a `← home` back link and a three-item sub-tab strip. Tab
|
||
routing is hash-based (`#agent`, `#system`, `#audit`); default is
|
||
`#agent`. (Build log history has moved to the BU1LDS page — see above.)
|
||
|
||
**AGENT sub-tab** — per-container journald viewer. Two selects: agent
|
||
name (populated from `GET /api/state`) and unit filter
|
||
(`hive-agent.service` / `hive-mcp-http.service` /
|
||
`hive-bash-daemon.service` / `hive-matrix-daemon.service` /
|
||
`(full machine journal)`). Fetches
|
||
`GET /api/journal/{name}?unit=<unit>&lines=500` on selection change
|
||
or `↻ refresh`. Output rendered as a `<pre>` block. A `?agent=<name>`
|
||
and/or `?unit=<svc>` URL param pre-selects the agent + unit on page
|
||
load — the per-agent `⋮` menu's **journal logs →** entry uses this to
|
||
deep-link directly to a specific agent's journal. A "fetched N ago"
|
||
chip appears after the `↻ refresh` button following each successful
|
||
fetch and ticks every 30 s.
|
||
|
||
**SYSTEM sub-tab** — host-side service logs. Unit selector
|
||
(`hive-c0re.service` / `hive-priv.service`). Fetches
|
||
`GET /api/journal-host?unit=<unit>&lines=500` on activation
|
||
and on `↻ refresh`. Rendered as a `<pre>` block. A "fetched N ago"
|
||
chip ticks every 30 s. Available to the operator unconditionally (not
|
||
capability-gated — the endpoint lives on the hive-c0re dashboard,
|
||
behind the gateway).
|
||
|
||
**AUDIT sub-tab** — operator-visible trail of agent-initiated
|
||
privileged actions (e.g. infra-container restarts via `infra_admin`).
|
||
Lazy-fetched on tab show (like SYSTEM) from `GET /api/audit-log`, which
|
||
returns `{ entries, total }` — `entries` newest-first, server-clamped to
|
||
the latest 500; `total` drives a "latest 500 of N" count so the clamp is
|
||
never silent. Rendered as a filterable table (when / agent / action /
|
||
target / outcome / detail); the filter box is a client-side substring
|
||
match over the cached rows. The outcome badge colours `ok` green and
|
||
`err` red, with an `err` whose `detail` starts `denied:` (a capability
|
||
refusal) shown amber and labelled `denied` so it reads apart from an
|
||
execution failure. `ts_unix` is an RFC 3339 string; a 30 s ticker keeps the
|
||
relative "ago" column honest while the tab is in view. The backing
|
||
`audit_log` store records every privileged-action attempt (ok / err /
|
||
denied). New entries live-append without a refresh: an `audit_entry_added`
|
||
event on `/api/dashboard/stream` (the flattened row) is prepended to the table
|
||
and the "latest N of M" count bumped, de-duped by id against the cold
|
||
fetch.
|
||
|
||
## Container row
|
||
|
||
A full-height **square agent icon** (5em, capped) on the left. The
|
||
icon is the **selection toggle**: click (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/swarm.js::renderContainers`).
|
||
|
||
**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 container
|
||
is known stopped up front (`ContainerView.running = false`) the
|
||
fallback fires immediately, skipping the doomed `<url>/icon`
|
||
fetch entirely.
|
||
|
||
**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 backend. 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 is stamped `data-set-at`
|
||
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). The agent backend 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 when
|
||
`services.hyperhive.gateway.enable = true`). Gateway-on (default):
|
||
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/gateway.md::Per-agent unix-socket upstream`).
|
||
Gateway-off (legacy / local dev): base URL is
|
||
`http://<host>:<container.port>` (direct TCP fallback). Forge
|
||
links resolve against `state.forge_public_url` (sourced from
|
||
`services.hyperhive.forge.publicUrl`) and are omitted 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.
|
||
|
||
**When the container is stopped** (`ContainerView.running = false`),
|
||
the async `dashboard-state` fetch is skipped entirely (the agent
|
||
web server is down), so the badge chain is replaced by a single
|
||
badge, the nav strip is empty, and status text / rate-limited / ctx
|
||
badges are suppressed. 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.
|
||
|
||
**Line 2** — status badges only (no per-card action buttons — actions
|
||
moved to the **selection bar** or the **per-agent `⋮` menu**, see
|
||
below).
|
||
|
||
### Status badges
|
||
|
||
**Which single badge (hyperhive#3139):** `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 backend 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 is parked 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
|
||
**several transients at once** — e.g. a lease-exempt `prebuild`
|
||
running alongside a `stop_for_update` on the same agent — and each
|
||
renders as its own independent badge rather than being collapsed
|
||
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 — there is no separate queued-but-not-started row state
|
||
to visually distinguish it from (see **Pending-badge derivation**
|
||
below), so 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 is the node's own wire tag
|
||
(`NodeKind::as_str`, the same strings the graph wire's node
|
||
labels carry), not a fixed set. Treat it as an opaque display
|
||
string; do not switch on specific values. `restarting` in
|
||
particular no longer exists, because no node kind is unique to a
|
||
restart.
|
||
- It is **not** exclusively operator-initiated, and **not** limited
|
||
to rebuild-shaped work — `running_transients()` on the backend is
|
||
a status-only test (any `Running` node whose payload names a
|
||
non-empty agent lights a pill), so work the operator never
|
||
clicked (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, there is no
|
||
fallback badge for work that's merely `Pending` in the queue, since
|
||
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 stale values are suppressed 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) is keyed 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. Clicking 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 click.
|
||
- `↻ R3BU1LD` — always available; queues a rebuild for this agent.
|
||
- `journal logs →` — opens `/logs.html#agent?agent=<name>` so the
|
||
operator lands directly in the AGENT log tab pre-filtered to this
|
||
container, without having to pick an agent from the dropdown.
|
||
- `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
|
||
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 `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 }>`,
|
||
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, prompts, and transient error notices use an
|
||
in-app themed dialog system (`@hive/shared/modal.js`) rather than the
|
||
browser's native `confirm()` / `prompt()` / `alert()` chrome, so
|
||
they match the Catppuccin palette and can't be styled away by the
|
||
OS. Three primitives, all 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). Backdrop click and `Esc` both cancel.
|
||
- `themedPrompt(...)` — a modal with a text input, resolving to the
|
||
entered string or `null`.
|
||
- `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 auto-dismiss; bulk / partial-failure
|
||
summaries are sticky (click to dismiss) so they aren't missed.
|
||
|
||
Every destructive run-state action (`ST0P`, `R3ST4RT`, `R3BU1LD`,
|
||
`DESTR0Y`, `PURG3`, `M0V3`) 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). The backend 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 — there's no build log, since a graceful stop runs no nix
|
||
build. (The `hivectl stop --graceful` CLI flag enqueues the same
|
||
`Signal`/`Drain` pair, so the dashboard and CLI paths behave
|
||
identically.)
|
||
|
||
### Topology tree
|
||
|
||
See **SW4RM tab** above for the parent/child derivation, sibling
|
||
sort order, and cycle-safety rules (`swarm.js::buildAgentTree` walks
|
||
`ContainerView.parent`) — this section covers only how the tree is
|
||
*drawn*.
|
||
|
||
The per-row prefix column (`.tree-prefix`) is **DOM-painted, not
|
||
text-glyph-painted**: each indent lane is its own positioned `<span>`
|
||
so CSS can draw full-height vertical bars that bridge the gap between
|
||
sibling rows. Plain text box-drawing characters (`├─`, `└─`, `│ `)
|
||
would only paint one text-line tall and leave visible breaks between
|
||
the taller-than-one-line container cards, so the bars are drawn as
|
||
CSS borders instead: a continuation bar runs the full height of an
|
||
ancestor's still-open subtree, and the joint at a row's own depth is
|
||
`├` (more siblings below) or `└` (last sibling — the line stops at
|
||
the row's icon midline). Exact lane widths and positioning live in
|
||
`swarm.js`'s tree-prefix rendering and its paired CSS rules — not
|
||
reproduced here since they're tuned in pixel units and will drift.
|
||
When every container is at depth 0 (no `parent` set) these rules are
|
||
all no-ops and the layout reads like a plain flat list.
|
||
|
||
## Selection bar
|
||
|
||
Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` / `DESTR0Y` /
|
||
`PURG3`) live here rather than as per-card buttons — see **Container
|
||
row** above. Clicking 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-SW4RM tabs since other tabs don't show the
|
||
agent cards needed to cross-reference.
|
||
|
||
When one or more agents are selected (via icon click), a sticky
|
||
frosted-mauve bar slides up from the bottom of the viewport
|
||
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
|
||
|
||
- **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
|
||
- `↻ R3BU1LD` — always available
|
||
- `DESTR0Y` / `PURG3` — always available
|
||
- `⇡ M0V3 → ROOT` — promote selected agents to top-level
|
||
(parent = null); disabled when all selected are already at root.
|
||
Backend `topology::set_parent` refuses moves it can't satisfy
|
||
(e.g. a move that would create a cycle) and the refusal surfaces
|
||
in the failure roll-up.
|
||
- `⇢ M0V3 → [select]` — inline picker available for any
|
||
selection size. The dropdown lists every container that isn't IN
|
||
the selection itself nor a descendant of any selected agent
|
||
(client-side BFS cycle prevention across the whole batch; the
|
||
backend re-checks per-agent). On submit:
|
||
- **single agent** → `POST /api/topology/set-parent`
|
||
(form-encoded `child=<name>&new_parent=<target>`)
|
||
- **multiple agents** → `POST /api/topology/set-parent-bulk`
|
||
(JSON `[{ child, new_parent }]`; all moves land in a single
|
||
`topology.json` commit instead of one per agent)
|
||
|
||
Both write `topology.json` and re-emit a container snapshot so
|
||
the tree repaints without a page reload.
|
||
- **`✕ clear`** button + `Esc` key clear the entire selection.
|
||
|
||
Stale selections (agents destroyed while selected) are pruned on
|
||
every render before the bar appears.
|
||
|
||
## Approval card
|
||
|
||
Each pending approval renders as a card (`assets/call.js::
|
||
renderApprovals`) with three stacked sections:
|
||
|
||
- **identity header** — glyph, `#id`, agent, kind chip, and a
|
||
right-aligned `requested <N> ago` relative time from
|
||
`ApprovalView.requested_at`. Glyph and chip vary by kind:
|
||
|
||
| kind | glyph | chip | sha shown |
|
||
|---|---|---|---|
|
||
| `merge_config_pr` | `⇒` | `merge-pr` | PR-head sha (`sha_short`) |
|
||
| `update_meta_inputs` | `↻` | `meta-update` | — |
|
||
| `schedule_prompt` | `⏱` | `schedule` | — |
|
||
| `init_config` | `⊕` | `init` | — |
|
||
| `spawn` | `⊕` | `spawn` | — |
|
||
|
||
The chip ticks live every second via a `data-requested-at`
|
||
attribute + client-side interval (no re-render). Turns amber once
|
||
the request has been pending ≥ 1h so a stale approval stands out;
|
||
the `.stale` class flips precisely at the 3600s boundary rather
|
||
than at the next `renderApprovals` call.
|
||
- **what-changed body** — the submitting agent's description, then
|
||
kind-specific drill-in triggers:
|
||
- `merge_config_pr`: `↳ review PR on forge ↗` deep-links the
|
||
config PR into `agent-configs/<agent>/pulls/<pr_number>` (shown
|
||
only when `forge_present` and `pr_number` is set). The config diff
|
||
lives on the forge PR itself — no inline diff side-panel.
|
||
- `init_config` / `spawn`: a one-line "container will be created"
|
||
note instead.
|
||
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
||
`prompt()` for an optional reason carried to the submitting agent as
|
||
`HelperEvent::ApprovalResolved.note`.
|
||
|
||
A `pending · N` / `history · N` tab pair switches the section
|
||
between the live queue and the last 30 resolved approvals.
|
||
|
||
## Browser notifications
|
||
|
||
Pure frontend (`Notification` API). Three signals trigger them:
|
||
|
||
- new pending approval (per id, delta on `/api/state`)
|
||
- new pending operator question (per id)
|
||
- new broker message sent `to: "operator"` (live via SSE)
|
||
|
||
The toggle controls live in the `S3TT1NGS` tab (`#settings`); see
|
||
that section above for the user-facing shape. Dispatch logic lives
|
||
in `common.js::NOTIF`.
|
||
|
||
First `/api/state` after page load seeds "seen" sets without
|
||
firing — only items that arrive while the page is open count.
|
||
Per-event tags (`hyperhive:approval:<id>`, `hyperhive:question:<id>`,
|
||
`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
|
||
notification center instead of overwriting each other.
|
||
`console.debug` logs at every block point (unsupported,
|
||
permission ungranted, muted) for in-browser debugging. Click
|
||
focuses the dashboard tab. The localStorage key
|
||
`hyperhive.notify.muted` (`"1"` = muted, absent = unmuted) backs
|
||
the toggle and silences dispatch without revoking the OS
|
||
permission. Requires a secure context (HTTPS or localhost); on
|
||
other origins the controls hide themselves. Browsers typically
|
||
suppress notifications while the originating tab is focused —
|
||
that's a browser-level decision, not ours.
|
||
|
||
## Dashboard endpoints
|
||
|
||
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
|
||
the H0M3 hub's **API** tile), 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/approve/{id}` — approve a pending approval. Fires
|
||
`ApprovalResolved` on the dashboard event channel; client
|
||
updates derived approvals state from the event.
|
||
- `POST /api/deny/{id}` (`note=<reason>`, optional) — deny a pending
|
||
approval with an optional operator-supplied reason. The reason
|
||
travels to the submitting agent as `HelperEvent::ApprovalResolved.note`
|
||
and also rides on the dashboard's `ApprovalResolved` event.
|
||
Dashboard prompts via the themed `themedPrompt()` dialog on
|
||
click — a resizable `<textarea>` where Enter submits and
|
||
Shift+Enter inserts a newline (so multi-sentence rejection
|
||
notes are possible).
|
||
- `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/answer-question/{id}` — answer a pending operator question.
|
||
- `POST /api/cancel-question/{id}` — cancel a pending question with
|
||
the sentinel `[cancelled]`. Same code path as a real answer.
|
||
- `POST /api/request-spawn` — queue a Spawn approval.
|
||
- `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 was not 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/operator-inbox` — list unread messages addressed to
|
||
`to="operator"` (broker rows with `acked_at = NULL`). Cold-loaded
|
||
for the Y3R C4LL tab's ◆ 1NB0X ◆ section on page load + tab activation;
|
||
live updates fed from the broker `sent` stream. Returns
|
||
`{ messages: [{ id, from, body, at, in_reply_to, file_refs }, …] }`,
|
||
newest-first. Reuses `/api/agent/operator/mark-all-read` to ack (filters
|
||
are identical so every listed row is exactly what mark-read clears).
|
||
- `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 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 BU1LDS. Works on both finished and in-progress
|
||
build logs.
|
||
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
|
||
a managed container; rendered in the side panel.
|
||
- `GET /api/audit-log` — agent-initiated privileged-action audit
|
||
trail. Returns `{ entries, total }`: `entries` is a `Vec<AuditEntry>`
|
||
(`id`, `ts_unix` as RFC 3339, `agent`, `action`, `target`, `outcome`
|
||
`"ok"`/`"err"`, `detail` nullable), newest first, server-clamped to
|
||
500; `total` is the full row count for a "latest 500 of N" header.
|
||
Backs the LOGS page AUDIT sub-tab.
|
||
- `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. Click-time backing for the inline path-link
|
||
preview.
|
||
|
||
Detection of which tokens *are* path links is done
|
||
**server-side at broker-message ingest**, not client-side:
|
||
the broker forwarder 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`.
|
||
- `GET /api/container-resources` — live per-agent-container CPU +
|
||
memory from cgroup v2 (C0R3 › C0NT41N3R L04D panel). Returns one
|
||
row per running agent (`name`, `cpu_pct`, `mem_current_bytes`,
|
||
`mem_peak_bytes`, `mem_max_bytes`, `disk_bytes`); samples CPU over
|
||
~200 ms so the call briefly awaits. Skips non-running agents (no scope
|
||
dir). No network field — agents share the host netns. `disk_bytes` is
|
||
the last value from a **separate slow sampler** (not this hot path):
|
||
a background `du -sxb` of the agent's state dir + container writable
|
||
rootfs every ~5 min, `-x` excluding the shared read-only nix store.
|
||
`null` until the first sample lands.
|
||
- `POST /api/infra-container/{name}/{action}` — start / stop / restart a
|
||
hive infra service (C0R3 › 1NFR4 panel). `name` parses into the
|
||
`InfraContainer` allowlist (`hive-ci`/`hive-forge`/`hive-gateway`/
|
||
`hive-matrix`, 400 on unknown), and the variant decides the unit —
|
||
`container@<name>.service`, or `nginx.service` for the gateway.
|
||
`action` ∈ `start|stop|restart`. Calls
|
||
the same `priv_client::control_infra_container` helper the
|
||
`infra_admin` agent path uses; records an `audit_log` entry
|
||
(`start_infra`/`stop_infra`/`restart_infra`, actor `"operator"`) either
|
||
way.
|
||
- `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).
|
||
- `GET /api/tool-groups` — returns `{ groups: Vec<String>,
|
||
assignments: BTreeMap<String, Vec<String>>,
|
||
descriptions: BTreeMap<String, String> }`. `groups` is the
|
||
ordered list of all known tool-group names (drives the column
|
||
headers in the P3RM1SS10NS tab); `assignments` is the per-agent
|
||
override map (absent agents use the role default); `descriptions`
|
||
maps each group name to a short human-readable string surfaced as
|
||
a column-header tooltip on hover.
|
||
- `POST /api/tool-groups/{agent}` — body `{ groups: ["name", …] }`.
|
||
Writes the tool-group set for `{agent}` to
|
||
`/var/lib/hyperhive/meta/tool-groups.json` and queues a rebuild so
|
||
`HIVE_TOOL_GROUPS` takes effect. Agent name validated;
|
||
`guard_agent_name` applied.
|
||
- `GET /api/capabilities` — returns `{ caps: Vec<String>,
|
||
assignments: BTreeMap<String, Vec<String>>,
|
||
descriptions: BTreeMap<String, String> }`. `caps` is the
|
||
ordered list of all known capability names; `assignments` is the
|
||
per-agent grant map (absent agents have no extra capabilities);
|
||
`descriptions` maps each capability name to a short human-readable
|
||
string surfaced as a column-header tooltip on hover.
|
||
- `POST /api/capabilities/{agent}` — body `{ caps: ["name", …] }`.
|
||
Writes the capability set for `{agent}` to
|
||
`/var/lib/hyperhive/meta/capabilities.json` and queues a rebuild so
|
||
`HIVE_CAPABILITIES` takes effect. Agent name validated;
|
||
unknown capability strings are rejected (400). `guard_agent_name`
|
||
applied.
|
||
- `POST /api/permissions` — batch perm apply for the save-all
|
||
permissions button. Body
|
||
`{ changes: [{ agent, tool_groups?: ["name", …], capabilities?: ["name", …] }] }`.
|
||
Sparse per agent: an omitted field leaves that perm-type untouched,
|
||
an empty array clears it, a populated array fully replaces it (same
|
||
replace semantics as the per-agent endpoints above). Each affected
|
||
agent gets ONE combined `PermChange` queue entry, so changing both
|
||
an agent's tool-groups and capabilities triggers a single rebuild,
|
||
not two. **Atomic**: every change is validated first (agent names via
|
||
`guard_agent_name`, group + capability names) and on any validation
|
||
error nothing is written or enqueued (non-2xx `{ error }`); rows with
|
||
both fields omitted are skipped, not errors. Returns `200 "ok"` on
|
||
success.
|
||
- `GET /api/schedules` — list all schedules (active and
|
||
recently cancelled) for the SCH3DUL3S scheduled-prompts panel.
|
||
- `POST /api/schedules` — operator-direct schedule create:
|
||
`{ targets, body, first_fire_at_unix, interval_seconds?, description? }`.
|
||
Agent-initiated schedules go through the approval queue instead
|
||
(via the `request_schedule_prompt` MCP tool).
|
||
- `PATCH /api/schedules/{id}` — partial edit. JSON body
|
||
`{ body?, description?, interval_seconds?, next_fire_at_unix?,
|
||
targets_add?, targets_remove? }`.
|
||
Missing key = "leave alone"; explicit `null` on
|
||
`description` / `interval_seconds` clears the field (so a
|
||
recurring schedule flips to one-shot when `interval_seconds`
|
||
is sent as `null`). `targets_add` is replace-on-conflict:
|
||
re-adding a previously-cancelled target drops the tombstone
|
||
and the target starts fresh (operator intent on re-add =
|
||
"this target is active again"). `targets_remove` delegates
|
||
to the same path as `cancel_targets` — tombstones preserve
|
||
audit, parent schedule auto-cancels when no active targets
|
||
remain. Refuses cancelled rows; returns the updated
|
||
`WireSchedule` on success.
|
||
- `POST /api/schedules/{id}/cancel` — cancel a schedule. Body
|
||
`{ targets?: ["name", …] }` cancels just those recipients;
|
||
absent or empty body cancels the whole schedule.
|
||
- `POST /api/schedules/{id}/fire-now` — out-of-band manual
|
||
pulse. Fires the schedule body once immediately to
|
||
every active target. Recurring schedules: `next_fire_at_unix`
|
||
is untouched; the regular cadence continues. One-shots: the
|
||
schedule is consumed (cancelled) after the manual fan-out.
|
||
Per-target `last_result` is annotated as a manual fire so
|
||
the audit trail distinguishes scheduled fires from operator-
|
||
triggered ones.
|
||
- `POST /api/schedules/{id}/pause` — pause a recurring or
|
||
one-shot schedule. A paused schedule does not fire regardless
|
||
of `next_fire_at`; the row gains a `⏸ paused` badge.
|
||
- `POST /api/schedules/{id}/resume` — resume a paused schedule.
|
||
Clears the pause flag without adjusting `next_fire_at`.
|
||
- `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.
|
||
|
||
## 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 click. Used by the message-flow terminal renderer and
|
||
the operator-inbox derived state.
|
||
- `approval_added` (id, agent, approval_kind, sha_short, diff,
|
||
description) / `approval_resolved` (id, agent, approval_kind,
|
||
sha_short, status, resolved_at, note, description) — pending
|
||
queue + history mutations. Client mutates a derived store and
|
||
re-renders only the approvals section.
|
||
- `question_added` (id, asker, question, options, multi,
|
||
asked_at, deadline_at, target) / `question_resolved` (id,
|
||
answer, answerer, answered_at, cancelled, target) — both
|
||
operator-targeted and peer (agent-to-agent) threads fire
|
||
these. The dashboard's questions pane surfaces both, with
|
||
filter chips (all / @operator / @peer / per-participant) and
|
||
an `0V3RR1D3` button on peer rows so the operator can
|
||
answer when an agent is stuck. The ttl watchdog fires
|
||
`question_resolved` with `answerer = "ttl-watchdog"` on
|
||
expiry.
|
||
- `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
|
||
many mutation sites — post-spawn approval bookkeeping
|
||
(`actions::approve`), the job queue's own node execution
|
||
(`job_queue::exec`, e.g. 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; the
|
||
pending overlay is read 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. There is no
|
||
`/api/state.rebuild_queue` — it went with the typed projection.
|
||
- `schedules_changed` (seq, schedules: `Vec<WireSchedule>`) —
|
||
full snapshot of all scheduled prompts. Emitted after every
|
||
operator mutation via the `/api/schedules` surface (new /
|
||
edit / cancel / fire-now) and after the worker fires or
|
||
rearms a row. Same snapshot-shape rationale as
|
||
`tombstones_changed` / `meta_inputs_changed` (small list, no
|
||
add/remove races). The SCH3DUL3S tab subscribes and
|
||
re-renders `schedulesState` on receipt; tab activation still
|
||
re-fetches as a safety net for approval-path inserts and
|
||
disconnect windows.
|
||
- `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`). The SCH3DUL3S tab's reminders section
|
||
subscribes and calls `renderReminders` on receipt, so the list
|
||
updates live without polling.
|
||
- `capabilities_changed` (seq, caps: `Vec<str>`, descriptions: map,
|
||
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
|
||
capability grants. Emitted from the rebuild-queue worker after a
|
||
`PermChange` / Capabilities entry commits the JSON file. Payload
|
||
matches `GET /api/capabilities` shape so `renderCapabilities` can
|
||
be called directly. P3RM1SS10NS tab subscribes; activation
|
||
re-fetch still runs as a safety net.
|
||
- `tool_groups_changed` (seq, groups: `Vec<str>`, descriptions: map,
|
||
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
|
||
tool-group assignments. Emitted from the rebuild-queue worker after
|
||
a `PermChange` / ToolGroups entry commits the JSON file. Same
|
||
shape as `GET /api/tool-groups`; P3RM1SS10NS tab subscribes.
|
||
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
||
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<MetaInput>`) — full
|
||
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. BU1LDS M3T4 1NPUTS
|
||
tab uses this to show/hide the "⏳ meta-update running" banner.
|
||
- `audit_entry_added` (flattened `AuditEntry` fields: id, ts_unix,
|
||
agent, action, target, outcome, detail) — a single new audit-log
|
||
row. L0GS AUDIT sub-tab live-prepends the row and bumps the
|
||
"latest N of M" count, de-duped by id against the cold fetch.
|
||
|
||
`/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 — approvals, questions,
|
||
transients, containers, operator inbox, message flow —
|
||
derives from `/api/dashboard/stream` after the initial snapshot,
|
||
maintaining its own client-side store and applying events on
|
||
top. The 5s periodic poll is gone.
|
||
|
||
Generalised form helpers: `form[data-confirm="…"]` pops
|
||
`confirm()` before submit; `form[data-prompt="…"]` pops
|
||
`prompt()` and stashes the answer in a hidden input named by
|
||
`data-prompt-field` (default `note`).
|
||
|