Mechanical addition to the existing bulk-action framework (renderSelectionBar/addBulkButton, swarm.js) -- two per-agent actions already exist (POST /api/pause/, /api/resume/, see hive-agent-menu.js), this just gives the selection bar the same all-or-nothing enablement rule the other six bulk buttons already use (pause enabled only when none of the selection is already paused, and vice versa for resume). .btn-pause reuses .badge-paused's yellow so the trigger and the resulting state pill read as one colour; .btn-resume is green like .btn-start (both are "go" actions). Docs updated to list both in the Selection bar reference.
81 KiB
Dashboard layout
Part of Web UI. See also: Shape (shared) · Per-agent page
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>byrenderServerWarningsincommon.js. Driven bystate.server_warnings— a list of{ kind, level, message }— and coloured bylevel(warnamber /critred). The backend owns the threshold + message, so adding a new system warning needs no frontend change. Warnings are a push-based registry (hive-c0re'swarnings.rs): any subsystem raises/clears its own entry via an RAII guard, andhost_stats::server_warnings()is just a cheap snapshot of that registry. Producers today include the host disk-pressure check (astatvfsprobe 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-c0reby default; updated to<swarm> / <hive> // h1ve-c0reoncehive_name/swarm_namearrive 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/statecold-load and updated live byapproval_added/approval_resolved/question_added/question_resolvedSSE events; it's preserved whenhive_name/swarm_namelater 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⤿ 0V3RR1D3so the operator can unblock an agent-to-agent exchange. Questions with attl_secondsshow a⏳ MM:SSlive countdown chip; the host-side watchdog resolves withanswerer = "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 singleanswerfield.✗ CANC3Lis 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.jstab strip) rather than/core.html's plain title. Its own esbuild bundle (credentials.js); no SSE — it reads/api/stateonce 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 —
liveand the container is running: online. - dim green —
livebutas_of_unixhasn't advanced in > ~90s (3 missed heartbeats) while the container is not down: the daemon stopped publishing, so the snapshot'sliveis no longer trustworthy (likely dead/wedged). Labelled "online · no heartbeat". - amber —
livebut the container is down (a stopped container ⟹ a dead daemon, so the snapshot is stale); also thetoken_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 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.
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⏸ pausedbadge in thenextcolumn and are sorted to the middle bucket (after active, before cancelled). A paused schedule does not fire regardless ofnext_fire_at; resuming (▶) clears the pause without adjusting the next-fire time. POSTs to/api/schedules/{id}/pauseor/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_commandstable (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.restartingin 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 (anyRunningnode 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-exemptprebuild) 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 adeployed_shaand 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 tonullon cancel or an object of checkbox states on confirm. Destructive actions passdanger: true(the confirm button turns red and the cancel button takes focus). Backdrop click andEscboth cancel.themedPrompt(...)— a modal with a text input, resolving to the entered string ornull.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 / P4US3 / R3SUM3 /
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 -
⏸ 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 -
⇡ M0V3 → ROOT— promote selected agents to top-level (parent = null); disabled when all selected are already at root. Backendtopology::set_parentrefuses 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-encodedchild=<name>&new_parent=<target>) - multiple agents →
POST /api/topology/set-parent-bulk(JSON[{ child, new_parent }]; all moves land in a singletopology.jsoncommit instead of one per agent)
Both write
topology.jsonand re-emit a container snapshot so the tree repaints without a page reload. - single agent →
-
✕ clearbutton +Esckey 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-alignedrequested <N> agorelative time fromApprovalView.requested_at. Glyph and chip vary by kind:kind glyph chip sha shown merge_config_pr⇒merge-prPR-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-atattribute + client-side interval (no re-render). Turns amber once the request has been pending ≥ 1h so a stale approval stands out; the.staleclass flips precisely at the 3600s boundary rather than at the nextrenderApprovalscall. -
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 intoagent-configs/<agent>/pulls/<pr_number>(shown only whenforge_presentandpr_numberis 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 —
◆ APPR0VEandDENY. Deny pops aprompt()for an optional reason carried to the submitting agent asHelperEvent::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. FiresApprovalResolvedon 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 asHelperEvent::ApprovalResolved.noteand also rides on the dashboard'sApprovalResolvedevent. Dashboard prompts via the themedthemedPrompt()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.destroyacceptspurge=onto 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 aQueuedentry. RefusesRunning/ 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 inQueuedstate. -
POST /api/agent/{name}/mark-all-read— ack all pending broker messages for{name}. Backfillsdelivered_atfor rows not yet delivered and setsacked_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 toto="operator"(broker rows withacked_at = NULL). Cold-loaded for the Y3R C4LL tab's ◆ 1NB0X ◆ section on page load + tab activation; live updates fed from the brokersentstream. Returns{ messages: [{ id, from, body, at, in_reply_to, file_refs }, …] }, newest-first. Reuses/api/agent/operator/mark-all-readto 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 brokerSentevent 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. ReturnsVec<BuildLogHeader>(JSON):id,agent,kind,cmdline,started_at,finished_at,status("ok"/"fail"/nullwhile in-progress).limitdefaults 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. ReturnsBuildLogFull(JSON): all header fields plusstdoutandstderras 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 astext/plain. Used by the↓ raw/↓ download rawlinks 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 }:entriesis aVec<AuditEntry>(id,ts_unixas RFC 3339,agent,action,target,outcome"ok"/"err",detailnullable), newest first, server-clamped to 500;totalis the full row count for a "latest 500 of N" header. Backs the LOGS page AUDIT sub-tab. -
GET /static/marked.jsserves the vendoredmarkedbundle used for markdown previews. -
GET /api/state-file?path=<host-or-container-path>— bounded text read of a file under the per-agentstate/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/configsubtrees, 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 asfile_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/historybackfill. -
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'shyperhive-turn-stats.sqliteread-only (skips missing / unreadable / zero-turn dbs); returns swarm totals, a busiest-first per-agent rollup, swarm model mix, and a labelledest_cost_usd. Window defaults to24h. -
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_bytesis the last value from a separate slow sampler (not this hot path): a backgrounddu -sxbof the agent's state dir + container writable rootfs every ~5 min,-xexcluding the shared read-only nix store.nulluntil the first sample lands. -
POST /api/infra-container/{name}/{action}— start / stop / restart a hive infra service (C0R3 › 1NFR4 panel).nameparses into theInfraContainerallowlist (hive-ci/hive-forge/hive-gateway/hive-matrix, 400 on unknown), and the variant decides the unit —container@<name>.service, ornginx.servicefor the gateway.action∈start|stop|restart. Calls the samepriv_client::control_infra_containerhelper theinfra_adminagent path uses; records anaudit_logentry (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> }.groupsis the ordered list of all known tool-group names (drives the column headers in the P3RM1SS10NS tab);assignmentsis the per-agent override map (absent agents use the role default);descriptionsmaps 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.jsonand queues a rebuild soHIVE_TOOL_GROUPStakes effect. Agent name validated;guard_agent_nameapplied. -
GET /api/capabilities— returns{ caps: Vec<String>, assignments: BTreeMap<String, Vec<String>>, descriptions: BTreeMap<String, String> }.capsis the ordered list of all known capability names;assignmentsis the per-agent grant map (absent agents have no extra capabilities);descriptionsmaps 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.jsonand queues a rebuild soHIVE_CAPABILITIEStakes effect. Agent name validated; unknown capability strings are rejected (400).guard_agent_nameapplied. -
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 combinedPermChangequeue 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 viaguard_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. Returns200 "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 therequest_schedule_promptMCP 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"; explicitnullondescription/interval_secondsclears the field (so a recurring schedule flips to one-shot wheninterval_secondsis sent asnull).targets_addis 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_removedelegates to the same path ascancel_targets— tombstones preserve audit, parent schedule auto-cancels when no active targets remain. Refuses cancelled rows; returns the updatedWireScheduleon 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_unixis untouched; the regular cadence continues. One-shots: the schedule is consumed (cancelled) after the manual fan-out. Per-targetlast_resultis 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 ofnext_fire_at; the row gains a⏸ pausedbadge. -
POST /api/schedules/{id}/resume— resume a paused schedule. Clears the pause flag without adjustingnext_fire_at. -
POST /api/meta-update—nix flake updatethe selectedmeta/flake.lockinputs, then rebuild the affected agents. -
GET /api/dashboard/stream— unified live event channel: brokersent/delivered, plus the mutation events listed below. Each frame carriesseq. -
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 carryid: i64(the broker row id) andin_reply_to: Option<i64>for thread rendering. The dashboard message-flow terminal renders reply rows with a↳ replytag 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 an0V3RR1D3button on peer rows so the operator can answer when an agent is stuck. The ttl watchdog firesquestion_resolvedwithanswerer = "ttl-watchdog"on expiry.transient_set(name, transient_kind, since_unix) /transient_cleared(name) — lifecycle action spinners. The client ticks the elapsed-seconds badge offsince_unixclient-side, no polling.transient_kindis 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 byCoordinator::rescan_containers_and_emitfrom 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 10scrash_watchpoll. Client upserts/removes by name; the pending overlay is read fromtransientsStatesince 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-fetchesGET /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/schedulessurface (new / edit / cancel / fire-now) and after the worker fires or rearms a row. Same snapshot-shape rationale astombstones_changed/meta_inputs_changed(small list, no add/remove races). The SCH3DUL3S tab subscribes and re-rendersschedulesStateon 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: agentremindcalls (agent_server), operator cancel / retry (/api/system/reminders/*),cancel_loose_endwith Reminder kind, and the scheduler tick after each delivery batch (reminder_scheduler). The SCH3DUL3S tab's reminders section subscribes and callsrenderReminderson 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 aPermChange/ Capabilities entry commits the JSON file. Payload matchesGET /api/capabilitiesshape sorenderCapabilitiescan 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 aPermChange/ ToolGroups entry commits the JSON file. Same shape asGET /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 ofmeta/flake.lockinputs. BU1LDS M3T4 1NPUTS tab subscribes; same shape asGET /api/meta-inputs.meta_update_running(running: bool) — emitted when anix flake updateripple starts or completes. BU1LDS M3T4 1NPUTS tab uses this to show/hide the "⏳ meta-update running" banner.audit_entry_added(flattenedAuditEntryfields: 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).