# Dashboard layout > Part of [Web UI](../web-ui.md). See also: > [Shape (shared)](shape.md) · [Per-agent page](agent.md) The dashboard is served at `/dashboard.html` (with the home page at `/`). It has a fixed chrome header at the top and a `
` that shows exactly one tab pane at a time. The URL hash (`#swarm`, `#call`, `#system`, `#permissions`, `#schedules`, `#peers`, `#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). Peer hives render as a headline under SW4RM rather than a tab. Count pills on SW4RM (container count), Y3R C4LL (pending approvals + questions + unread operator messages), and SCH3DUL3S (active schedules); P33RS and S3TT1NGS have 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 + the stand-alone FL0W / L0GS / H0M3 pages), injected at the top of `` by `renderServerWarnings` in `common.js`. Driven by `state.server_warnings` — a list of `{ kind, level, message }` from hive-c0re's `host_stats::server_warnings` — and coloured by `level` (`warn` amber / `crit` red). The backend owns the threshold + message, so adding a new system warning needs no frontend change. The only producer today is the host disk-pressure check (a `statvfs` probe of `/nix`: ≥85% used → `warn`, ≥95% → `crit`, e.g. `⚠ host nix store N% full (G GiB free) — garbage-collect …`). Hidden when there are no warnings. - **Browser tab title** — `hive / c0re` by default; updated to ` / ` once `hive_name` / `swarm_name` arrive in the state snapshot. When there are pending approvals or unanswered questions, a `(N)` prefix is prepended — `(3) pr1ma / hive-c0re` — so the operator can see the call count in an unfocused browser tab without opening the dashboard. The prefix is set on the initial `/api/state` cold-load and updated live by `approval_added` / `approval_resolved` / `question_added` / `question_resolved` SSE events; it's preserved when `hive_name` / `swarm_name` later replace the raw title. The FL0W and L0GS pages use a slim header (a `← home` back-link + the page title) rather than the dashboard tab strip — they're standalone surfaces, not tab panes. ## SW4RM tab **C0NTAINERS** — live containers rendered as a depth-first tree using `ContainerView.parent` (populated by `topology.rs`). Each container's row is prefixed with ASCII tree glyphs (`├─`, `└─`, `│ ` continuation columns) showing the agent parent/child hierarchy. When every container has `parent = null` (flat topology) the tree collapses to a plain list with no glyphs. Children are sorted alphabetically within each parent; roots likewise. Cycles in the parent graph are tolerated — orphaned containers (not reachable from any root) are appended as roots so no agent disappears. Pulsing red banner at the top of this section if any two sub-agents hash to the same port (`port_conflicts` from `/api/state`): the operator must rename one of them and rebuild. `lifecycle::{spawn,rebuild}` also preflight this and refuse with a clear error message naming the conflicting agent. `↻ UPD4TE 4LL` button appears above the containers list when any agent is stale. ## Y3R C4LL tab Things blocked on operator decision — approvals and questions share a tab because they're the same concept ("something is waiting on you"). **P3NDING APPR0VALS** — the queue (see "Approval card" below). The R3QU3ST SP4WN form lives at the top of this section. A `pending · N` / `history · N` tab pair switches between the live queue and the last 30 resolved approvals (see "Approval card" for the history row shape). **M1ND H4S QU3STI0NS** — pending `ask` calls waiting on the operator, with amber pulsing border. Anatomy of each card: - **Filter chips** — `all · N`, `@operator · N`, `@peer · N`, plus one chip per participant name (`@asker · N` / `@target · N`). Every chip shows its own count so the operator can see the distribution at a glance. Clicking a chip narrows the visible list; selection persists in localStorage so a tab switch doesn't lose the filter. - **Question card** — timestamp · asker → target · body text (with file-path links). Operator-targeted questions (`target = null`) show `▸ ANSW3R`; peer-targeted questions (`target = agent`) show `⤿ 0V3RR1D3` so the operator can unblock an agent-to-agent exchange. Questions with a `ttl_seconds` show a `⏳ MM:SS` live countdown chip; the host-side watchdog resolves with `answerer = "ttl-watchdog"` on expiry. - **Answer form** — free-text textarea (Enter = submit, Shift+Enter = newline) + optional option list (radio for single-select, checkboxes for `multi=true`). Submit merges selected options + free text comma-joined into a single `answer` field. `✗ CANC3L` is a separate form so the submit merge handler doesn't interfere. - **◆ answ3red (N)** — collapsible `
` 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*: destroyed-but-state-kept agents (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}`). *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.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 containers (`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`) directly from the dashboard, without needing an `infra_admin` agent's `restart` MCP tool. One row per container: 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@.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 dashboard tab keeps the rebuild-queue *state* for the SW4RM card badges without rendering these panels. **R3BU1LD QU3U3** — pending and recently-completed container operations: rebuilds, meta-update cascades, and first-spawns. One operation runs at a time; the worker drains FIFO. Each row shows a state glyph (`⏸` queued / `▶` running / `✔` done / `✖` failed / `⊘` cancelled), kind glyph + verb (`↻ rebuild`, `◆ meta_update`, `✨ spawn`, `🗑 destroy`, `↺ restart`, `⚡ boot`, `🔑 perm_change`, `⏹ graceful_stop`, `▶ start`, `■ stop`), agent name, source chip (`manual | meta_update | auto_update | crash_recover | approval` — green for operator-approved config changes), timing, and an optional reason / error. A multi-step op is a single DAG: its per-agent subgraphs and sub-steps (a meta-update cascade's per-agent rebuilds, a `Reconcile`'s deferred `Start`/`Stop`) render as nodes within the one entry — split into subgraphs by the backend `deps` edges — not as nested child entries. Dedup: re-enqueueing a still-queued op for the same agent collapses into the existing entry. All timing labels stay live: running entries tick elapsed seconds every second; queued and terminal ("done N ago" / "failed N ago") labels tick every 30s so keyed rows never show stale timestamps as they persist across `rebuild_queue_changed` snapshots. The in-flight phase is the running **node's kind** in the node chain — there is no separate sub-step label (the old cyan `↳ ` sub-line went away with the DAG queue: each phase is its own node now). Queued entries carry a `✗` cancel button on the right edge; running / done / failed / cancelled entries don't show it — the backend refuses cancellation for non-`Queued` rows anyway (`POST /api/rebuild-queue/{id}/cancel`). Successful cancel flips the row to `⊘ cancelled` via the next `rebuild_queue_changed` snapshot. Cold-loaded from `/api/state.rebuild_queue`; live updates via `rebuild_queue_changed` snapshot event. Below the queue, a **live build-log panel** (`#rebuild-live-log`, `renderRebuildLiveLog`) streams the currently-running rebuild's output inline — collapsible, with a live/ok/fail badge and a `↓ raw` download. It's keyed to the running entry's `build_log_id` and opens one `EventSource` to `GET /api/build-logs/id/{id}/stream` (the same stream the BUILD L0GS tab uses; the stream replays accumulated output on connect). It lives in its own container outside `#rebuild-queue-section` so the queue's per-row re-render (rows rebuild as nodes advance) never tears down the open stream; it hides when nothing is building and each row keeps its `logs →` link out to the full build log history. **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-`, `agent-/mcp-`, …), each shown once at its shallowest path. `read_meta_inputs` walks the lock graph with a `visited` set — `follows` aliases and rev-less nodes are skipped. A `select all / select none` control sits above the tree. Checking inputs + submitting bumps the lock in `/meta/` and rebuilds the selected agents in sequence; each outcome reaches the root agent as a `rebuilt` system event. `POST /meta-update`. While a lock-bump ripple runs, the panel shows a pulsing "⏳ meta-update running" banner and the update button is disabled (snapshot field `meta_update_running`, live event `meta_update_running`). **BUILD L0GS** — all-agents build log history (moved from `/logs.html`). Lazy-loaded on first tab activation; auto-refreshes when `rebuild_queue_changed` fires. Fetches `GET /api/build-logs?limit=30`. Renders a scrollable list of build entries; each row is a collapsible button showing status badge (`live` / `ok` / `fail`), agent name, elapsed duration, build kind, age, and the invocation command line. Expanding a row fetches the full stdout+stderr via `GET /api/build-logs/id/{id}`. A live in-progress build shows a `live` badge with a ticking elapsed-time chip; expanding streams output via `GET /api/build-logs/id/{id}/stream` with sticky-bottom auto-scroll (suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the entry with that id pre-expanded. ## CR3D3NTIALS page (`/credentials.html`) Operator surface to provision per-agent credentials without editing the agent's config repo. Standalone page reached from the **Credentials** tile on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link + a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather than `/core.html`'s plain title. Its own esbuild bundle (`credentials.js`); no SSE — it reads `/api/state` once for the (shared) agent picker and otherwise works off purpose-built endpoints per tab. Three sub-tabs: ### MATRIX tab Provision / log in a per-agent **external** matrix account and store its access token (this half is unchanged from the old `/matrix-accounts.html` page it replaces — only the URL and surrounding chrome moved). An agent picker (populated from `state.containers`, the live roster) drives a list of that agent's accounts — name, homeserver, user id, and a status dot — read from `GET /api/matrix-accounts?agent=` → `{ accounts: [ { name, homeserver, token_present, live, user_id } ], as_of_unix }`. `token_present` is whether a token is **stored**; `live`, `homeserver`, and `user_id` are backfilled from the matrix daemon's `matrix-accounts.json` snapshot — a host-visible file the daemon **force-rewrites every ~30s** (a heartbeat), so `as_of_unix` (the snapshot mtime) advances while the daemon is alive and a *stalled* value genuinely means "stopped publishing", not just "old snapshot". An account with a token but absent from the snapshot reports `live: false`. The status dot renders these states: - **green** — `live` and the container is running: online. - **dim green** — `live` but `as_of_unix` hasn't advanced in > ~90s (3 missed heartbeats) while the container is *not* down: the daemon stopped publishing, so the snapshot's `live` is no longer trustworthy (likely dead/wedged). Labelled "online · no heartbeat". - **amber** — `live` but the container is **down** (a stopped container ⟹ a dead daemon, so the snapshot is stale); also the `token_present && !live` "provisioned but offline" case. - **grey** — no token (not provisioned). ### GITHUB tab Provision a single per-agent GitHub personal access token (see [`docs/github.md`](../github.md) for the injection + `gh`/git-push mechanics). No login flow — the operator pastes an existing PAT for a dedicated bot account, with a security-warning banner (dedicated account + minimally-scoped token) and a link to [github.com/settings/tokens](https://github.com/settings/tokens). Status reads `GET /api/github-account?agent=` → `{ 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. 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..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. ### 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=` → `{ forges: [{ label, base_url }] }`, derived by scanning the agent's state dir for `forge-