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) 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):

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 — the approval queue.

P3NDING APPR0VALS — the queue (see "Approval card" below). 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).

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

◆ PR3F3R3NC3S ◆ — operator-local preferences. State lives in the browser's localStorage — preferences do NOT sync between devices and do NOT survive a profile wipe. One section today (browser notifications); future preferences land here as sibling <h3> blocks in dashboard.html. Was its own S3TT1NGS page/tile (/settings.html) until mara moved it in here — a standalone page for one toggle didn't earn its own tile.

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

The "expand tool output panels" toggle lives on the per-agent page's own settings popover (SettingsMenu, @hive/shared/settings-menu.js) instead — it's agent-terminal-only, unrelated to this section.

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). 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). One action: 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. 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; autorefreshes 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/networking/network.md).

Hive infrastructure services (hive-ci, hive-forge, hive-gateway, hive-matrix) have no dashboard panel — hivectl stop/start/restart is the only control surface, a separate host-admin-socket path with no HTTP route and no agent-facing equivalent.

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 doesn't render the queue itself; it just mounts the component (mountJobqGraph(container, props), since this page has no JSX pipeline) with an onUpdate callback to drive the two things below it that the generic view doesn't show. The component owns fetching, cold and live: GET /api/jobq/graph on mount, and a refetch via the mount handle's .refresh() on every rebuild_queue_changed SSE tick (that event is a bare { seq } trigger — 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 doesn't 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; autorefreshes when rebuild_queue_changed fires. Fetches GET /api/build-logs?limit=30. Renders a scrollable list of build entries; each row is a collapsible button showing status badge (live / ok / fail), agent name, elapsed duration, build kind, age, and the invocation command line. Expanding a row fetches the full stdout+stderr via GET /api/build-logs/id/{id}. A live in-progress build shows a live badge with a ticking elapsed-time chip; expanding streams output via GET /api/build-logs/id/{id}/stream with sticky-bottom autoscroll (suspends on manual scroll-up). Deep-link: ?id=N#buildlogs opens the entry with that id pre-expanded.

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

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:

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's 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/integrations/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

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 selected. 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, executionToolGroup::AGENT_DEFAULT; root agent is seeded to ToolGroup::MANAGER_DEFAULTmessaging, meta, inbox, lifecycle, approvals, scheduling, diagnostics, execution, that is 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. No per-row save buttons exist. 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. Selecting 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:

Per-schedule action column:

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.

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). Select 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.

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.c0re.modelPrices nix option — each key is a model-family short name (matched case-insensitively as a substring of the model id, longest match wins) mapping to { input, output, cache_read, cache_write } USD-per-million-token prices. Models not covered fall back to hive-c0re's built-in estimate.

The FL0W page does NOT host the PR3F3R3NC3S pane — it lives only on the dashboard's Y3R C4LL tab (reach it via the FL0W page's ← home back-link → Dashboard). Notifications still fire on the FL0W page when they're enabled there, 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.deploy.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.

There's no tile for it on the H0M3 hub (removed along with Forge's — see the H0M3 section above). The operator reaches it from the swarm-ui LinksMenu instead (see docs/swarm/ui.md::Quick links), and 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 autodiscovery lives in docs/networking/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 autoscroll + "↓ N new" pill. Below the stream sits a terminal-style compose box: @name picks the recipient (sticky via localStorage; autocomplete from the live container list, Tab/Enter to confirm; @* broadcasts). POST /api/op-send drops {from:"operator", to, body} into the broker; the resulting SSE frame re-renders the terminal row. The 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, Builds, Stats, Settings, Core, Credentials, API — each pointing to their respective surfaces, all unconditionally shown (no gating). The page is a pure portal with no tab-bar or SSE subscriptions. Typography + colours inherit from the shared theme (Catppuccin Mocha via common.css + theme.css). home.js fills the swarm/hive identity line at the top. All dashboard sub-pages include a ← Home back-link for navigation.

No Matrix or Forge tile here — those surfaces (/matrix/, the forge's own public URL) are reached directly, not linked from the dashboard UI.

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, #infra, #system); 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.

INFRA sub-tab — journald viewer for the four hive infrastructure containers (hive-ci, hive-forge, hive-gateway, hive-matrix), a fixed client-side list (INFRA_NAMES in logs.js — no dashboard API exposes just the name list). No unit filter (infra containers don't run the per-agent hive daemons) — always the full machine journal (or, for the gateway, the host journal filtered to its own unit). Fetches GET /api/journal/{name}?lines=500, same "fetched N ago" ticker as AGENT. A ?agent=<name> deep-link routes here instead of AGENT when the name is one of the four infra containers.

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

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 that env unconditionally — the gateway always runs; the flag stays so the dashboard doesn't have to learn that. Gateway-on: base URL is /agent/<name> (same origin, gateway proxies to the per-agent harness — TCP or unix-domain depending on the agent's HIVE_WEB_SOCKET opt-in, see docs/networking/gateway.md::Per-agent unix-socket upstream). Gateway-off is the flag-absent fallback (a dashboard served outside a hyperhive deploy): base URL is http://<host>:<container.port> (direct TCP). Forge links resolve against state.forge_public_url (sourced from services.hyperhive.swarm.forge.publicUrl) and 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 multiple transients at once — for example a lease-exempt prebuild running alongside a stop_for_update on the same agent — and each renders as its own independent badge 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:

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. Selecting it opens a small dropdown with per-agent actions and navigation links. Contents:

↻ 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:

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 SignalDrain 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. Selecting an agent's icon toggles its selection (an in-memory Set<name>); Esc or the bar's ✕ clear button drops everything. The selection persists across tab switches in-memory — the bar just hides on non-SW4RM tabs since other tabs don't show the agent cards needed to cross-reference.

When one or more agents are selected (via the icon), a sticky frosted-mauve bar slides up from the bottom of the viewport (#selection-bar, position: fixed; bottom: 0). It shows:

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:

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). Two signals trigger them:

The toggle controls live in the Y3R C4LL tab's ◆ PR3F3R3NC3S ◆ section; 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: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. Selecting it 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.

Dashboard event channel

Wire vocabulary on /api/dashboard/stream (kind tag is in the JSON payload):

/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, 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).