Per mara's explicit steer on hyperhive#2812 ("also replace the build
queue tab with this component" + "graph fetching should live in the
component, not build.js" + "dont replicate the grouping by dag"):
R3BU1LD QU3U3 is now a mounted <hive-jobq-graph endpoint="/api/jobq/graph">
element. builds.js no longer renders the queue itself, does its own
fetch, or hand-rolls a per-root tree/roll-up/cancel-button — all of
buildNodeTree/topoSort/entryFingerprint/renderQueueEntry/
firstFailedNode/rebuildQueueRowCache/QUEUE_STATE_GLYPH/rollupState is
gone.
builds.js's remaining job is listening for the component's
hive-jobq-graph-update event (added to the component in the prior
commit) to keep a flat jobqNodes array in sync, and using that for the
two things the generic view doesn't render: the count-pill and the
live-log panel. On the rebuild_queue_changed SSE tick, calls the
mounted element's .refresh() instead of doing its own fetch — that
event still carries its own queue payload on the wire (tabs.js/SW4RM
still reads it for the badges, untouched), this page just ignores it
now.
Also removed, now genuinely dead: the two elapsed/finished-time
tickers (nothing produces the .rqe-when spans they targeted anymore),
stateSlug and isoToSecs (no callers left), fmtElapsed import (no
callers left).
New @hive/shared/jobq-graph.js export entry in packages/shared's
package.json, alongside the existing hive-tab-strip.js/hive-menu.js/
etc. pattern.
docs/web-ui/dashboard.md's R3BU1LD QU3U3 section rewritten to match:
mounted-component shape, no source/reason/cancel-button/deep-link on
rows (generic component has none), settled entries show their full
step tree (Done nodes aren't filtered off this wire, unlike the old
DagView projection).
Verified against real production data again (this hive's own live
/api/jobq/graph, now settled — no in-flight build at test time) plus
a synthetic running-build case to exercise findLiveBuild's happy path:
correct live-node detection (build_log_id gate), correct in-flight
root count. Confirmed the built dist bundle actually registers
customElements.define("hive-jobq-graph", ...) — the new shared
package export resolves correctly through esbuild.
Branch reused per mara's explicit "dont rework #3000 - continue
working on _this_ pr [#2996], it already has the component that
replaces 90% of build.js" — this ships as part of PR #2996, not a
separate PR.
1475 lines
79 KiB
Markdown
1475 lines
79 KiB
Markdown
# Dashboard layout
|
||
|
||
> Part of [Web UI](../web-ui.md). See also:
|
||
> [Shape (shared)](shape.md) · [Per-agent page](agent.md)
|
||
|
||
|
||
The dashboard is served at `/dashboard.html` (with the home page at `/`).
|
||
It has a fixed chrome header at the top and a `<main>` that shows exactly
|
||
one tab pane at a time. The URL hash (`#swarm`, `#call`, `#system`,
|
||
`#permissions`, `#schedules`, `#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 `<body>` 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
|
||
`<swarm> / <hive>` 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 `<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*: 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<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
|
||
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@<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 dashboard tab keeps the rebuild-queue *state* for the SW4RM
|
||
card badges without rendering these panels.
|
||
|
||
**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
|
||
`<hive-jobq-graph endpoint="/api/jobq/graph">`** (the shared generic
|
||
graph-viewer component, `@hive/shared/jobq-graph.js`) — `builds.js`
|
||
does not render the queue itself; it just mounts the element and
|
||
listens for its `hive-jobq-graph-update` event 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
|
||
`.refresh()` on every `rebuild_queue_changed` SSE tick (that event
|
||
still carries its own `Vec<QueueEntry>` payload on the wire — the
|
||
SW4RM tab still consumes it for the badges, untouched — this page just
|
||
ignores the payload and treats the tick as a 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, `deps` edges order
|
||
siblings). 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. **No source chip, kind label, cancel button, timing, or
|
||
build-log deep-link on rows** — the generic component has no
|
||
per-node action affordances or entry-level metadata (no equivalent of
|
||
the old `DagView`'s `source`/`reason`/`created_at`, which were
|
||
`NodeKind::Dag`-specific fields the generic wire doesn't carry); per
|
||
mara's steer on hyperhive#2812 ("dont feel constrained by what the ui
|
||
does currently"), the first cut presents what the endpoint actually
|
||
gives rather than reconstructing the old per-row chrome. Settled
|
||
entries render their **full step tree**, not just a bare summary —
|
||
unlike the old `DagView` projection, this wire does not filter `Done`
|
||
nodes out.
|
||
|
||
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
|
||
`hive-jobq-graph-update` event'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 `<hive-jobq-graph>`'s own re-renders never disturb the
|
||
open poll; it hides when nothing is building.
|
||
|
||
**M3T4 1NPUTS** — inputs in `meta/flake.lock` the operator can
|
||
selectively `nix flake update`, rendered as an indented tree:
|
||
every fetched input at every depth (`hyperhive`,
|
||
`hyperhive/nixpkgs`, `agent-<n>`, `agent-<n>/mcp-<x>`, …), each
|
||
shown once at its shallowest path. `read_meta_inputs` walks the
|
||
lock graph with a `visited` set — `follows` aliases and rev-less
|
||
nodes are skipped. A `select all / select none`
|
||
control sits above the tree. Checking inputs + submitting bumps
|
||
the lock in `/meta/` and rebuilds the selected agents in
|
||
sequence; each outcome reaches the root agent as a `rebuilt`
|
||
system event. `POST /meta-update`. While a lock-bump ripple runs,
|
||
the panel shows a pulsing "⏳ meta-update running" banner and the
|
||
update button is disabled (snapshot field `meta_update_running`,
|
||
live event `meta_update_running`).
|
||
|
||
**BUILD L0GS** — all-agents build log history (moved from `/logs.html`).
|
||
Lazy-loaded on first tab activation; auto-refreshes when
|
||
`rebuild_queue_changed` fires. Fetches `GET /api/build-logs?limit=30`.
|
||
Renders a scrollable list of build entries; each row is a collapsible
|
||
button showing status badge (`live` / `ok` / `fail`), agent name,
|
||
elapsed duration, build kind, age, and the invocation command line.
|
||
Expanding a row fetches the full stdout+stderr via
|
||
`GET /api/build-logs/id/{id}`. A live in-progress build shows a `live`
|
||
badge with a ticking elapsed-time chip; expanding streams output via
|
||
`GET /api/build-logs/id/{id}/stream` with sticky-bottom auto-scroll
|
||
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
|
||
entry with that id pre-expanded.
|
||
|
||
## CR3D3NTIALS page (`/credentials.html`)
|
||
|
||
Operator surface to provision per-agent credentials without editing the
|
||
agent's config repo. Standalone page reached from the **Credentials** tile
|
||
on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link
|
||
+ a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather
|
||
than `/core.html`'s plain title. Its own esbuild bundle
|
||
(`credentials.js`); no SSE — it reads `/api/state` once for the (shared)
|
||
agent picker and otherwise works off purpose-built endpoints per tab.
|
||
Three sub-tabs:
|
||
|
||
### MATRIX tab
|
||
|
||
Provision / log in a per-agent **external** matrix account and store its
|
||
access token (this half is unchanged from the old `/matrix-accounts.html`
|
||
page it replaces — only the URL and surrounding chrome moved).
|
||
|
||
An agent picker (populated from `state.containers`, the live roster) drives a list of that
|
||
agent's accounts — name, homeserver, user id, and a status dot —
|
||
read from `GET /api/matrix-accounts?agent=<name>` →
|
||
`{ accounts: [ { name, homeserver, token_present, live, user_id } ], as_of_unix }`.
|
||
`token_present` is whether a token is **stored**; `live`, `homeserver`,
|
||
and `user_id` are backfilled from the matrix daemon's
|
||
`matrix-accounts.json` snapshot — a host-visible file the daemon
|
||
**force-rewrites every ~30s** (a heartbeat), so `as_of_unix` (the
|
||
snapshot mtime) advances while the daemon is alive and a *stalled* value
|
||
genuinely means "stopped publishing", not just "old snapshot". An account
|
||
with a token but absent from the snapshot reports `live: false`.
|
||
|
||
The status dot renders these states:
|
||
|
||
- **green** — `live` and the container is running: online.
|
||
- **dim green** — `live` but `as_of_unix` hasn't advanced in > ~90s (3
|
||
missed heartbeats) while the container is *not* down: the daemon stopped
|
||
publishing, so the snapshot's `live` is no longer trustworthy (likely
|
||
dead/wedged). Labelled "online · no heartbeat".
|
||
- **amber** — `live` but the container is **down** (a stopped container
|
||
⟹ a dead daemon, so the snapshot is stale); also the `token_present &&
|
||
!live` "provisioned but offline" case.
|
||
- **grey** — no token (not provisioned).
|
||
|
||
### GITHUB tab
|
||
|
||
Provision a single per-agent GitHub personal access token (see
|
||
[`docs/github.md`](../github.md) for the injection + `gh`/git-push
|
||
mechanics). No login flow — the operator pastes an existing PAT for a
|
||
dedicated bot account, with a security-warning banner (dedicated account +
|
||
minimally-scoped token) and a link to
|
||
[github.com/settings/tokens](https://github.com/settings/tokens).
|
||
|
||
Status reads `GET /api/github-account?agent=<name>` →
|
||
`{ present: bool }` — whether the agent's `github-token` file exists.
|
||
There's no live/heartbeat concept for a static PAT, so this is just a
|
||
"token stored ✓" / "not set" line, unlike MATRIX's status-dot taxonomy.
|
||
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
|
||
`token`) → `200 { ok: true }` on success, or the same `error_response`
|
||
shape `/api/matrix-account-login` uses on failure. The token is never
|
||
echoed back in either direction.
|
||
|
||
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.
|
||
|
||
### 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-gateway`, `hive-forge`); 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. The
|
||
checkboxes reflect the *effective* set (explicit grant or role default),
|
||
so a default-perms agent shows its real grants rather than blank; absent
|
||
agents in the assignment map have no extra capabilities.
|
||
|
||
**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`; root agent: all
|
||
groups). 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`, `execution`, `web_tools`. All
|
||
listed in `ToolGroup::ALL` in `hive-sh4re`. The `web_tools` group is
|
||
special: it carries no MCP tools; instead it adds Claude's built-in
|
||
`WebFetch` and `WebSearch` to `--tools` / `--allowedTools` for that
|
||
agent session.
|
||
|
||
Both tables share the same visual shape: `.cap-table-wrap` /
|
||
`.tg-table-wrap` outer scroll container, `thead` with a label column
|
||
(`.cap-agent-col` / `.tg-agent-col`) + one column per permission
|
||
(`.cap-col` / `.tg-group-col`). Each tbody row is one agent: a name cell
|
||
and its checkbox cells, where each checkbox carries `data-baseline` (its
|
||
render-time state) and the row carries `data-agent` for dirty-tracking.
|
||
|
||
**Saving — one button for the whole page.** There are no per-row save
|
||
buttons. A single page-level `.perm-save-bar` with a **save all (N
|
||
agents)** button sits at the bottom of the pane, enabled only when some
|
||
checkbox diverges from its baseline. Clicking it diffs every checkbox
|
||
across *both* matrices and POSTs one batch to `POST /api/permissions` as
|
||
`{ changes: [ { agent, tool_groups?, capabilities? } ] }` — only the
|
||
perm-types that actually changed for each agent are included (an omitted
|
||
field leaves that file untouched; an included array fully replaces it).
|
||
The backend coalesces an agent's capabilities + tool-groups into a
|
||
single rebuild, so changing both for one agent is one rebuild, not two.
|
||
The batch is atomic: it validates every change first and on any error
|
||
rejects the whole POST (`{error}`, nothing applied); a clean 200 (`ok`)
|
||
flips the bar to a queued→rebuilding state and re-fetches both tables.
|
||
Live `capabilities_changed` / `tool_groups_changed` events re-render the
|
||
matrices unless the section has unsaved edits, so an in-progress edit set
|
||
isn't clobbered.
|
||
|
||
## SCH3DUL3S tab
|
||
|
||
Anything that fires at a future time. Operator-set schedules
|
||
are created inline in the table (last row); agent self-paced
|
||
reminders surface at the bottom as a sibling list — they
|
||
share enough conceptual ground to live together.
|
||
|
||
**N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed
|
||
scheduled prompts. **Single-table layout**: each schedule is
|
||
one `<tr>`; columns are
|
||
`# | src | next | every | owner | body | …agents… | actions`.
|
||
Agent columns are dynamic — `operator` + `root` + every
|
||
live container + any extra name that appears as a target on
|
||
some schedule but isn't a current container (same
|
||
`buildTargetChips` membership rule the new/edit forms use,
|
||
so table and forms agree on what's addressable). Column
|
||
headers tilt -45° via CSS so each column reads as a narrow
|
||
~28px strip; per-agent cells render as:
|
||
|
||
- **active target** → `<button>✓</button>` that cancels just
|
||
that one target on click
|
||
- **cancelled target** → muted `✕` glyph (no button —
|
||
re-adding goes through the edit form's targets multi-select)
|
||
- **not a target** → empty cell
|
||
|
||
Per-schedule action column:
|
||
|
||
- **`↯ fire now`** — sends an out-of-band manual pulse to every
|
||
active target (recurring schedules keep their cadence; one-shots
|
||
are consumed after the manual fire). Disabled for paused schedules.
|
||
- **`⏸` / `▶` pause/resume toggle** — only on recurring schedules
|
||
(one-shots may also be paused to delay a one-time fire, so the
|
||
button always appears). Paused rows show a `⏸ paused` badge in the
|
||
`next` column and are sorted to the middle bucket (after active,
|
||
before cancelled). A paused schedule does not fire regardless of
|
||
`next_fire_at`; resuming (`▶`) clears the pause without adjusting
|
||
the next-fire time. POSTs to `/api/schedules/{id}/pause` or
|
||
`/api/schedules/{id}/resume`.
|
||
- **`✎ edit`** — expands an inline edit form as a colspan'd row
|
||
directly under the schedule's row (body / description / interval /
|
||
next-fire / targets all editable; targets are a multi-select diff'd
|
||
against the original active set so unchecked-was-active =
|
||
`targets_remove`, checked-not-originally-active = `targets_add`;
|
||
submit PATCHes `/api/schedules/{id}`).
|
||
- **`✕`** — cancels the whole schedule
|
||
(`POST /api/schedules/{id}/cancel`).
|
||
|
||
The `next` column cell (`.sched-due`) carries a `data-due-at`
|
||
Unix timestamp attribute; a shared 1s ticker rewrites it
|
||
in-place showing `fmtDuration` while in the future and
|
||
`overdue X ago` once the fire time has passed — same
|
||
zero-re-render pattern as the reminder due-at labels and the
|
||
question TTL chip.
|
||
|
||
The table's last row is a permanent inline creation row:
|
||
inputs live directly in table cells (targets as checkboxes,
|
||
body textarea that expands on focus, datetime-local pre-filled
|
||
to 5 minutes from now, mini d/h/m/s number inputs (blank or
|
||
all-zero = one-shot), description). Click `+` to POST to
|
||
`/api/schedules` as JSON (or `⌫` to clear the half-filled row);
|
||
carry-state preserves partially-typed inputs across re-renders. The tab pill shows the count of active
|
||
schedules (at least one live target not yet cancelled).
|
||
Refreshed on tab activation and after each submit/cancel. Backed by
|
||
`GET /api/schedules`. No backend changes for the table layout
|
||
— it renders entirely from existing `schedulesState` +
|
||
`containersState`.
|
||
|
||
**QU3U3D R3M1ND3RS** — reminders agents have scheduled for
|
||
themselves (via the `remind` tool) but not yet delivered.
|
||
Each row shows the owner, due time, and message; a `CANC3L`
|
||
button hard-deletes (`POST /api/cancel-reminder/{id}`) and a
|
||
`R3TRY` button re-arms one whose delivery failed
|
||
(`POST /api/retry-reminder/{id}`). Backed by `GET /api/reminders`.
|
||
Lives in the SCH3DUL3S tab alongside operator schedules so the
|
||
operator has one place for everything time-fired. The due-time
|
||
label (`.reminder-due`) carries a `data-due-at` Unix timestamp
|
||
attribute; a shared 1s ticker rewrites it in-place — showing
|
||
`in Xm Ys` while the reminder is in the future and
|
||
`overdue X ago` once the deadline passes — without triggering a
|
||
full re-render of the list.
|
||
|
||
## ST4TS tab
|
||
|
||
Hive-wide turn statistics, aggregated across every agent's
|
||
`hyperhive-turn-stats.sqlite` for the selected window. Distinct from
|
||
each agent's own `/stats` page (which carries the per-agent trend
|
||
charts): ST4TS is the swarm-level rollup.
|
||
|
||
- **Window selector** (`1h`–`30d`) re-fetches on change.
|
||
- **Summary chips**: active agents, turns, total/input/output/cache-read
|
||
tokens, and a labelled **est cost**.
|
||
- **Busiest agents** table — one row per agent (most turns first):
|
||
turns, input / output / cache-read tokens, est cost.
|
||
- **Model mix** — turns per model across the swarm, as CSS bars.
|
||
- **Favorite tools** — most-run normalised bash-command heads across the
|
||
swarm (top 10, as CSS bars), aggregated from each agent's
|
||
`bash_commands` table (written by the hive-bash-daemon capture). The
|
||
header + list stay hidden until at least one agent has recorded a
|
||
command, so the section never shows an empty block on a fresh hive.
|
||
|
||
Backed by `GET /api/stats-hive?window=<w>` in `hive-c0re`
|
||
(`hive_stats.rs`): for every name from
|
||
`Coordinator::kept_state_names()` it opens
|
||
`agent_harness_dir(name)/hyperhive-turn-stats.sqlite` read-only (with a
|
||
500 ms `busy_timeout`, since `turn_stats` is rollback-journal) and rolls
|
||
the rows up — missing / unreadable / zero-turn dbs are skipped so one
|
||
bad db never fails the endpoint. This is a **pull** surface (no SSE):
|
||
the data is fetched on tab activation 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.
|
||
|
||
## P33RS tab
|
||
|
||
Peer hives in this swarm. The tab is hidden when the
|
||
`state.peer_hives` array from `/api/state` is empty (i.e. no
|
||
`services.hyperhive.swarm.peers` are configured). When at least
|
||
one peer is present the `hidden` attribute is removed and the tab
|
||
becomes active.
|
||
|
||
**P33R H1V3S** — each peer renders as a card row: a hexagon icon
|
||
(`⬡`), the peer's DNS domain as the primary name, and the peer
|
||
dashboard HTTPS URL as a clickable secondary link. Clicking the
|
||
URL opens the peer hive's dashboard in a new tab.
|
||
|
||
### Backend wiring
|
||
|
||
The host daemon reads `services.hyperhive.swarm.peers` from the
|
||
nix config (an attrset keyed by peer domain), serialises each
|
||
entry as `{ name, url }` into `state.peer_hives: Vec<PeerHiveView>`,
|
||
and includes the field in the `/api/state` snapshot. `tabs.js`
|
||
reads `state.peer_hives` on every `refreshState` call and calls
|
||
`swarm.js::renderPeerHives(peers)`, which rebuilds the `#peers-section`
|
||
div from scratch.
|
||
|
||
The `name` field is the peer's DNS domain (the attrset key); `url`
|
||
is `https://{domain}/`. Both are derived from the env var
|
||
`HYPERHIVE_PEERS` (a JSON array of `{ domain, cert_fingerprint }`
|
||
objects) that the nix module writes into the c0re container
|
||
environment. `cert_fingerprint` is null for CA-trusted (e.g.
|
||
Let's Encrypt) peers and non-null to pin a self-signed cert.
|
||
`parse_peer_hives()` in `dashboard.rs` converts each entry to the
|
||
`PeerHiveView { name: domain, url: "https://domain/" }` shape the
|
||
frontend reads.
|
||
|
||
## 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.matrix.gui.package`) served by the hive-gateway
|
||
nginx container at `/matrix/` when
|
||
`services.hyperhive.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://matrix.${hyperhive.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 the body always gets the full row width down to
|
||
the content edge — long timestamps + agent names used to push the body ~30ch
|
||
in and force awkward narrow-column wraps. `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. Dashboard is now served at `/dashboard.html` (route swap
|
||
completed in #1464 step 2); the home page at `/` replaces the old dashboard
|
||
root. 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
|
||
muted `■ not running` 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.
|
||
When the container is running, status badges follow — `⊘ rate
|
||
limited` (red, while the harness is parked after a 429), `needs
|
||
login`, `needs update` — in-flight `◐ pending-state…` pill
|
||
(replaces buttons during operator-initiated start / stop /
|
||
restart / rebuild / destroy). Additionally, when a rebuild-queue
|
||
entry for this agent is `queued` or `running` but no
|
||
operator-initiated transient is set, the card surfaces a
|
||
`building…` / `meta-updating…` / `starting…` / `stopping…` badge
|
||
(per queue `kind` — e.g. a `start` entry shows `starting…` /
|
||
`start queued`, `stop` and `graceful_stop` both show `stopping…` /
|
||
`stop queued`) sourced from `rebuildQueueState` — so the SW4RM tab
|
||
shows the same progress visible on the BU1LDS page's R3BU1LD QU3U3.
|
||
The row visual splits queued vs running: a **queued** entry shows
|
||
only the pending-state pill (no row tint, so a long queue doesn't
|
||
paint half the tab amber); a **running** entry keeps the amber
|
||
row tint AND draws a **rotating amber ring** around the agent
|
||
icon, so it's obvious at a glance which container is actually
|
||
moving.
|
||
**Pending-state derivation:** the pill is sourced from two
|
||
separate stores in priority order. (1) The **transient**
|
||
(`transientsState`) — covers the create-and-start window where the
|
||
container literally isn't up yet, before any backend state event
|
||
has fired.
|
||
|
||
A transient is **derived from the 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 `NodeView.kind` carries),
|
||
not a fixed set. Treat it as an opaque display string; do not
|
||
switch on specific values. `restarting` in particular no longer
|
||
exists, because no node kind is unique to a restart.
|
||
- It is **not** exclusively operator-initiated. Work the operator
|
||
never clicked (a meta-update cascade, a crash-recover rebuild)
|
||
lights the same pill, since it is the running node that sets it.
|
||
|
||
Ops with no queue node behind them (destroy, migration) supply
|
||
their own label directly. (2) If no transient is set, the **rebuild-queue
|
||
entry** for this agent is consulted (`rebuildQueueState`); this
|
||
covers worker-driven ops — meta-update cascades, crash-recover
|
||
rebuilds, approval-driven rebuilds — that the operator didn't
|
||
click. `ContainerStateChanged` carries neither signal, so the
|
||
dashboard reads from the two snapshots directly. The
|
||
`opRunning` flag (driving the `pending-running` row class +
|
||
spinner) is true when (1) is set OR (2) is in `running` state;
|
||
queued entries leave `opRunning` false.
|
||
An **active model badge** (`model · <name>`, blue) appears when the
|
||
container is running and the harness has persisted a model name
|
||
(`harness/hyperhive-model`). 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.
|
||
A `ctx · Nk` chip showing 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.
|
||
- Line 2: status badges only (no per-card action buttons — actions
|
||
moved to the **selection bar** or the **per-agent `⋮` menu**, see
|
||
below).
|
||
|
||
**Per-agent `⋮` overflow menu** — a `⋮` button appears on the right
|
||
edge of each container row. Clicking it opens a small dropdown with
|
||
per-agent actions and navigation links. Contents:
|
||
|
||
- `↺ R3ST4RT` (running agents only) / `■ ST0P` (running only) /
|
||
`▶ ST4RT` (stopped only) — single-agent run-state toggles.
|
||
Identical to the bulk actions on the selection bar but operate
|
||
on one agent without requiring a selection click.
|
||
- `↻ R3BU1LD` — always available; queues a rebuild for this agent.
|
||
- `journal logs →` — opens `/logs.html#agent?agent=<name>` so the
|
||
operator lands directly in the AGENT log tab pre-filtered to this
|
||
container, without having to pick an agent from the dropdown.
|
||
- `DESTR0Y` / `PURG3` — destructive, each prompts for confirmation
|
||
via the themed dialog (see **Themed dialogs** below).
|
||
- `deployed:<sha> ↗` — present when the agent has a `deployed_sha`
|
||
and the forge is reachable; links the deployed commit on the forge
|
||
agent-configs mirror.
|
||
|
||
`↻ UPD4TE 4LL` button appears above the containers list when any
|
||
agent is stale. Banner pulses on each broker SSE event
|
||
(`pulseBanner` with a 4s grace timer).
|
||
|
||
**Build-queue summary banner** — when the rebuild queue has any
|
||
`queued` / `running` entries, 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). It replaces the
|
||
old per-transient spinner list; the actual running node for each
|
||
agent is already shown on its card (transient + in-flight-queue
|
||
badges), so the top of the tab only needs the at-a-glance summary.
|
||
|
||
### Themed dialogs
|
||
|
||
All confirmations, prompts, and transient error notices use an
|
||
in-app themed dialog system (`assets/modal.js`) rather than the
|
||
browser's native `confirm()` / `prompt()` / `alert()` chrome, so
|
||
they match the Catppuccin palette and can't be styled away by the
|
||
OS. Three primitives, all built on the `openDialog` core:
|
||
|
||
- `themedConfirm({ message, danger, confirmLabel, checkboxes })`
|
||
— a modal confirm that resolves to `null` on cancel or an object
|
||
of checkbox states on confirm. Destructive actions pass
|
||
`danger: true` (the confirm button turns red and the cancel
|
||
button takes focus). Backdrop click and `Esc` both cancel.
|
||
- `themedPrompt(...)` — a modal with a text input, resolving to the
|
||
entered string or `null`.
|
||
- `themedToast({ 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
|
||
`GracefulStop` rebuild-queue transient: the harness runs one
|
||
stop-checkpoint turn (so the agent can flush `/state`) and then
|
||
exits, with a 3-minute timeout that falls back to a hard stop. The
|
||
quiescing progress surfaces through the same rebuild-queue
|
||
transient + build log the card already reads for a rebuild. (The
|
||
`hivectl --graceful` CLI flag enqueues the same `GracefulStop`, so
|
||
the dashboard and CLI paths behave identically.)
|
||
|
||
### Topology tree
|
||
|
||
Container rows render as a forest, not a flat list — each agent
|
||
sits indented under its declared parent. `swarm.js::buildAgentTree`
|
||
walks `ContainerView.parent` for every container in the snapshot
|
||
and produces a render order with per-row depth + sibling-position
|
||
info:
|
||
|
||
- Top-level rows are agents with `parent = null` OR a parent that
|
||
doesn't appear in the container map (orphans get hoisted to root
|
||
so they're still visible).
|
||
- Within each level children sort alphabetically by name; roots
|
||
likewise.
|
||
- Cycle safety: any container not reached during the root-walk is
|
||
appended at the end as a root, so no agent ever silently
|
||
disappears from the list when the topology JSON is malformed.
|
||
- The pre-topology rendering shape (every container at depth 0,
|
||
flat list) collapses to the same visual today when no parent
|
||
field is set — bit-identical fallback path.
|
||
|
||
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; using text box-drawing characters
|
||
(`├─`, `└─`, `│ `) only paints one text-line tall and leaves
|
||
visible breaks between the taller-than-one-line container cards.
|
||
The bars come in two flavours: continuation (the ancestor's
|
||
subtree extends below this row → vertical line top→bottom) or
|
||
blank (ancestor was the last sibling at its level → no line
|
||
needed). The joint at the row's own depth column is `├` (more
|
||
siblings below) or `└` (last sibling at this depth — vertical
|
||
stops at the row's icon midline).
|
||
|
||
**Indent + lane geometry.** Each depth level shifts the row right
|
||
by `1.8em` (the lane width). The per-depth ladders are hardcoded
|
||
for six levels — enough for any plausible hive topology, and the
|
||
typed `attr()` function from CSS Values 5 that would collapse
|
||
this to one rule is still partial-support (Chromium-only as of
|
||
2026). The `.tree-prefix` span sits absolutely positioned with
|
||
`left: -<depth>*1.8em` so its right edge meets the row content
|
||
(the icon) and its leftmost lane lines up with top-level rows'
|
||
icons at `x = 0`. Each `.tree-lane` is `flex: 0 0 1.8em` so all
|
||
lanes have equal width. Continuation bars are drawn at lane
|
||
center (`left: 0.6em`, `border-left: 1px solid currentColor`,
|
||
`top: 0; bottom: 0`) and extend through `.containers { gap: 0.4em }`
|
||
into the next sibling's prefix (`bottom: -0.4em` on the prefix
|
||
span itself) so adjacent ancestor lines visually merge into one
|
||
unbroken vertical line. The horizontal stub at a row's own joint
|
||
lands at the icon midline so the L/T meets the icon edge cleanly.
|
||
When every container has `parent = null` (pre-topology state) the
|
||
`[data-depth]` attribute is absent on every row and these rules
|
||
are no-ops — the layout reads exactly like the legacy flat list.
|
||
|
||
## Selection bar
|
||
|
||
Per-card action buttons (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` /
|
||
`DESTR0Y` / `PURG3`) used to live on each container row; the
|
||
operator picked the bulk-bar model instead. Clicking an agent's
|
||
icon toggles its selection (an in-memory `Set<name>`); `Esc` or
|
||
the bar's `✕ clear` button drops everything. The selection
|
||
persists across tab switches in-memory — the bar just hides on
|
||
non-SW4RM tabs since other tabs don't show the agent cards needed
|
||
to cross-reference.
|
||
|
||
When one or more agents are selected (via icon click), a sticky
|
||
frosted-mauve bar slides up from the bottom of the viewport
|
||
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
|
||
|
||
- **Count + names** — "N agents selected · name1, name2, …"
|
||
- **Bulk action buttons** — only enabled when ALL selected agents
|
||
support the action; disabled with a tooltip naming the blockers
|
||
when the selection is mixed:
|
||
- `↺ R3ST4RT` — running agents only
|
||
- `■ ST0P` — running agents only
|
||
- `▶ ST4RT` — stopped agents only
|
||
- `↻ R3BU1LD` — always available
|
||
- `DESTR0Y` / `PURG3` — always available
|
||
- `⇡ M0V3 → ROOT` — promote selected agents to top-level
|
||
(parent = null); disabled when all selected are already at root.
|
||
Backend `topology::set_parent` refuses moves it can't satisfy
|
||
(e.g. a move that would create a cycle) and the refusal surfaces
|
||
in the failure roll-up.
|
||
- `⇢ M0V3 → [select]` — inline picker available for any
|
||
selection size. The dropdown lists every container that isn't IN
|
||
the selection itself nor a descendant of any selected agent
|
||
(client-side BFS cycle prevention across the whole batch; the
|
||
backend re-checks per-agent). On submit:
|
||
- **single agent** → `POST /api/topology/set-parent`
|
||
(form-encoded `child=<name>&new_parent=<target>`)
|
||
- **multiple agents** → `POST /api/topology/set-parent-bulk`
|
||
(JSON `[{ child, new_parent }]`; all moves land in a single
|
||
`topology.json` commit instead of one per agent)
|
||
|
||
Both write `topology.json` and re-emit a container snapshot so
|
||
the tree repaints without a page reload.
|
||
- **`✕ clear`** button + `Esc` key clear the entire selection.
|
||
|
||
Stale selections (agents destroyed while selected) are pruned on
|
||
every render before the bar appears.
|
||
|
||
## Approval card
|
||
|
||
Each pending approval renders as a card (`assets/call.js::
|
||
renderApprovals`) with three stacked sections:
|
||
|
||
- **identity header** — glyph, `#id`, agent, kind chip, and a
|
||
right-aligned `requested <N> ago` relative time from
|
||
`ApprovalView.requested_at`. Glyph and chip vary by kind:
|
||
|
||
| kind | glyph | chip | sha shown |
|
||
|---|---|---|---|
|
||
| `merge_config_pr` | `⇒` | `merge-pr` | PR-head sha (`sha_short`) |
|
||
| `update_meta_inputs` | `↻` | `meta-update` | — |
|
||
| `schedule_prompt` | `⏱` | `schedule` | — |
|
||
| `init_config` | `⊕` | `init` | — |
|
||
| `spawn` | `⊕` | `spawn` | — |
|
||
|
||
The chip ticks live every second via a `data-requested-at`
|
||
attribute + client-side interval (no re-render). Turns amber once
|
||
the request has been pending ≥ 1h so a stale approval stands out;
|
||
the `.stale` class flips precisely at the 3600s boundary rather
|
||
than at the next `renderApprovals` call.
|
||
- **what-changed body** — the submitting agent's description, then
|
||
kind-specific drill-in triggers:
|
||
- `merge_config_pr`: `↳ review PR on forge ↗` deep-links the
|
||
config PR into `agent-configs/<agent>/pulls/<pr_number>` (shown
|
||
only when `forge_present` and `pr_number` is set). The config diff
|
||
lives on the forge PR itself — no inline diff side-panel.
|
||
- `init_config` / `spawn`: a one-line "container will be created"
|
||
note instead.
|
||
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
||
`prompt()` for an optional reason carried to the submitting agent as
|
||
`HelperEvent::ApprovalResolved.note`.
|
||
|
||
A `pending · N` / `history · N` tab pair switches the section
|
||
between the live queue and the last 30 resolved approvals.
|
||
|
||
## Browser notifications
|
||
|
||
Pure frontend (`Notification` API). Three signals trigger them:
|
||
|
||
- new pending approval (per id, delta on `/api/state`)
|
||
- new pending operator question (per id)
|
||
- new broker message sent `to: "operator"` (live via SSE)
|
||
|
||
The toggle controls live in the `S3TT1NGS` tab (`#settings`); see
|
||
that section above for the user-facing shape. Dispatch logic lives
|
||
in `common.js::NOTIF`.
|
||
|
||
First `/api/state` after page load seeds "seen" sets without
|
||
firing — only items that arrive while the page is open count.
|
||
Per-event tags (`hyperhive:approval:<id>`, `hyperhive:question:<id>`,
|
||
`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
|
||
notification center instead of overwriting each other.
|
||
`console.debug` logs at every block point (unsupported,
|
||
permission ungranted, muted) for in-browser debugging. Click
|
||
focuses the dashboard tab. The localStorage key
|
||
`hyperhive.notify.muted` (`"1"` = muted, absent = unmuted) backs
|
||
the toggle and silences dispatch without revoking the OS
|
||
permission. Requires a secure context (HTTPS or localhost); on
|
||
other origins the controls hide themselves. Browsers typically
|
||
suppress notifications while the originating tab is focused —
|
||
that's a browser-level decision, not ours.
|
||
|
||
## Dashboard endpoints
|
||
|
||
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
|
||
the H0M3 hub's **API** tile), with the raw spec at `/api/openapi.json`.
|
||
It's a growing supplement, not yet a full replacement for the list
|
||
below — some endpoints aren't in it yet.
|
||
|
||
- `POST /api/approve/{id}` — approve a pending approval. Fires
|
||
`ApprovalResolved` on the dashboard event channel; client
|
||
updates derived approvals state from the event.
|
||
- `POST /api/deny/{id}` (`note=<reason>`, optional) — deny a pending
|
||
approval with an optional operator-supplied reason. The reason
|
||
travels to the submitting agent as `HelperEvent::ApprovalResolved.note`
|
||
and also rides on the dashboard's `ApprovalResolved` event.
|
||
Dashboard prompts via the themed `themedPrompt()` dialog on
|
||
click — a resizable `<textarea>` where Enter submits and
|
||
Shift+Enter inserts a newline (so multi-sentence rejection
|
||
notes are possible).
|
||
- `POST /api/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
|
||
`destroy` accepts `purge=on` to also wipe state dirs.
|
||
- `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs.
|
||
- `POST /api/answer-question/{id}` — answer a pending operator question.
|
||
- `POST /api/cancel-question/{id}` — cancel a pending question with
|
||
the sentinel `[cancelled]`. Same code path as a real answer.
|
||
- `POST /api/request-spawn` — queue a Spawn approval.
|
||
- `POST /api/update-all` — rebuild every stale container.
|
||
- `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry.
|
||
Refuses `Running` / terminal-state entries (in-flight
|
||
rebuilds can't be safely interrupted). Always 200; body is
|
||
`{"cancelled": true}` on a successful flip or
|
||
`{"cancelled": false}` when the entry was not in `Queued` state.
|
||
- `POST /api/agent/{name}/mark-all-read` — ack all pending broker
|
||
messages for `{name}`. Backfills `delivered_at` for rows
|
||
not yet delivered and sets `acked_at = now`. Returns
|
||
`{ "marked": N }`. Agent name validated against
|
||
`[a-z0-9_-]`, 1-63 chars; 400 on bad input.
|
||
- `GET /api/operator-inbox` — list unread messages addressed to
|
||
`to="operator"` (broker rows with `acked_at = NULL`). Cold-loaded
|
||
for the Y3R C4LL tab's ◆ 1NB0X ◆ section on page load + tab activation;
|
||
live updates fed from the broker `sent` stream. Returns
|
||
`{ messages: [{ id, from, body, at, in_reply_to, file_refs }, …] }`,
|
||
newest-first. Reuses `/api/agent/operator/mark-all-read` to ack (filters
|
||
are identical so every listed row is exactly what mark-read clears).
|
||
- `POST /api/op-send` (`to=<name>`, `body=<text>`) — drop an
|
||
operator-authored message into `<name>`'s inbox. `to=*` fans
|
||
out to every registered agent. Returns 200; the broker
|
||
`Sent` event re-renders the message-flow terminal without a
|
||
snapshot refetch. Used by the compose textbox under MESS4GE FL0W.
|
||
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
||
headers for one agent, newest first. Returns
|
||
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
|
||
`started_at`, `finished_at`, `status` (`"ok"` / `"fail"` /
|
||
`null` while in-progress). `limit` defaults to 10, server-side
|
||
cap at 50. Agent name validated (`[a-z0-9_-]`, 1-63 chars).
|
||
- `GET /api/build-logs/id/{id}` — full build log by id. Returns
|
||
`BuildLogFull` (JSON): all header fields plus `stdout` and
|
||
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
|
||
404 when the row is missing (vacuum-reaped or stale id).
|
||
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of
|
||
stdout+stderr for an in-progress build (newline-delimited JSON
|
||
frames). Used by the expanding live-row view in BU1LDS. The
|
||
stream closes when the build finishes or errors.
|
||
- `GET /api/build-logs/id/{id}/raw` — raw concatenated
|
||
stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download
|
||
raw` links in BU1LDS. Works on both finished and in-progress
|
||
build logs.
|
||
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
|
||
a managed container; rendered in the side panel.
|
||
- `GET /api/audit-log` — agent-initiated privileged-action audit
|
||
trail. Returns `{ entries, total }`: `entries` is a `Vec<AuditEntry>`
|
||
(`id`, `ts_unix` as RFC 3339, `agent`, `action`, `target`, `outcome`
|
||
`"ok"`/`"err"`, `detail` nullable), newest first, server-clamped to
|
||
500; `total` is the full row count for a "latest 500 of N" header.
|
||
Backs the LOGS page AUDIT sub-tab.
|
||
- `GET /static/marked.js` serves the vendored `marked` bundle used
|
||
for markdown previews.
|
||
- `GET /api/state-file?path=<host-or-container-path>` — bounded
|
||
text read of a file under the per-agent `state/` subtree or
|
||
the shared `/var/lib/hyperhive/shared/`. Accepts the
|
||
container-view forms (`/agents/<n>/state/...`, `/shared/...`)
|
||
and the host form. Canonicalises + verifies the path stays
|
||
inside the allow-list, refuses anything but a regular file,
|
||
refuses `/agents/<n>/claude` / `config` subtrees, truncates
|
||
bodies at 1 MiB. Click-time backing for the inline path-link
|
||
preview.
|
||
|
||
Detection of which tokens *are* path links is done
|
||
**server-side at broker-message ingest**, not client-side:
|
||
the broker forwarder calls `scan_validated_paths(body)` —
|
||
same allow-list helper the read endpoint uses — and attaches
|
||
the verified file tokens to the event as `file_refs: Vec<String>`.
|
||
The client trusts that list and linkifies only those tokens,
|
||
so directories, missing files, and forbidden subtrees never
|
||
become anchors. No probe endpoint, no client-side regex
|
||
heuristics. Historical messages get the same treatment on
|
||
`/api/dashboard/history` backfill.
|
||
- `GET /api/reminders` — list pending reminders for the
|
||
dashboard's queued-reminders panel.
|
||
- `GET /api/stats-hive?window=<1h|4h|24h|3d|7d|30d|all>` — hive-wide
|
||
turn-stats rollup for the ST4TS tab. Aggregates every agent's
|
||
`hyperhive-turn-stats.sqlite` read-only (skips missing / unreadable /
|
||
zero-turn dbs); returns swarm totals, a busiest-first per-agent
|
||
rollup, swarm model mix, and a labelled `est_cost_usd`. Window
|
||
defaults to `24h`.
|
||
- `GET /api/container-resources` — live per-agent-container CPU +
|
||
memory from cgroup v2 (C0R3 › C0NT41N3R L04D panel). Returns one
|
||
row per running agent (`name`, `cpu_pct`, `mem_current_bytes`,
|
||
`mem_peak_bytes`, `mem_max_bytes`, `disk_bytes`); samples CPU over
|
||
~200 ms so the call briefly awaits. Skips non-running agents (no scope
|
||
dir). No network field — agents share the host netns. `disk_bytes` is
|
||
the last value from a **separate slow sampler** (not this hot path):
|
||
a background `du -sxb` of the agent's state dir + container writable
|
||
rootfs every ~5 min, `-x` excluding the shared read-only nix store.
|
||
`null` until the first sample lands.
|
||
- `POST /api/infra-container/{name}/{action}` — start / stop / restart a
|
||
hive infra container (C0R3 › 1NFR4 panel). `name` parses into the
|
||
`InfraContainer` allowlist (`hive-ci`/`hive-forge`/`hive-gateway`/
|
||
`hive-matrix`, 400 on unknown), `action` ∈ `start|stop|restart`. Calls
|
||
the same `priv_client::control_infra_container` helper the
|
||
`infra_admin` agent path uses; records an `audit_log` entry
|
||
(`start_infra`/`stop_infra`/`restart_infra`, actor `"operator"`) either
|
||
way.
|
||
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
||
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
||
failed (clears the failure state so the scheduler retries).
|
||
- `GET /api/tool-groups` — returns `{ groups: Vec<String>,
|
||
assignments: BTreeMap<String, Vec<String>>,
|
||
descriptions: BTreeMap<String, String> }`. `groups` is the
|
||
ordered list of all known tool-group names (drives the column
|
||
headers in the P3RM1SS10NS tab); `assignments` is the per-agent
|
||
override map (absent agents use the role default); `descriptions`
|
||
maps each group name to a short human-readable string surfaced as
|
||
a column-header tooltip on hover.
|
||
- `POST /api/tool-groups/{agent}` — body `{ groups: ["name", …] }`.
|
||
Writes the tool-group set for `{agent}` to
|
||
`/var/lib/hyperhive/meta/tool-groups.json` and queues a rebuild so
|
||
`HIVE_TOOL_GROUPS` takes effect. Agent name validated;
|
||
`guard_agent_name` applied.
|
||
- `GET /api/capabilities` — returns `{ caps: Vec<String>,
|
||
assignments: BTreeMap<String, Vec<String>>,
|
||
descriptions: BTreeMap<String, String> }`. `caps` is the
|
||
ordered list of all known capability names; `assignments` is the
|
||
per-agent grant map (absent agents have no extra capabilities);
|
||
`descriptions` maps each capability name to a short human-readable
|
||
string surfaced as a column-header tooltip on hover.
|
||
- `POST /api/capabilities/{agent}` — body `{ caps: ["name", …] }`.
|
||
Writes the capability set for `{agent}` to
|
||
`/var/lib/hyperhive/meta/capabilities.json` and queues a rebuild so
|
||
`HIVE_CAPABILITIES` takes effect. Agent name validated;
|
||
unknown capability strings are rejected (400). `guard_agent_name`
|
||
applied.
|
||
- `POST /api/permissions` — batch perm apply for the save-all
|
||
permissions button. Body
|
||
`{ changes: [{ agent, tool_groups?: ["name", …], capabilities?: ["name", …] }] }`.
|
||
Sparse per agent: an omitted field leaves that perm-type untouched,
|
||
an empty array clears it, a populated array fully replaces it (same
|
||
replace semantics as the per-agent endpoints above). Each affected
|
||
agent gets ONE combined `PermChange` queue entry, so changing both
|
||
an agent's tool-groups and capabilities triggers a single rebuild,
|
||
not two. **Atomic**: every change is validated first (agent names via
|
||
`guard_agent_name`, group + capability names) and on any validation
|
||
error nothing is written or enqueued (non-2xx `{ error }`); rows with
|
||
both fields omitted are skipped, not errors. Returns `200 "ok"` on
|
||
success.
|
||
- `GET /api/schedules` — list all schedules (active and
|
||
recently cancelled) for the SCH3DUL3S scheduled-prompts panel.
|
||
- `POST /api/schedules` — operator-direct schedule create:
|
||
`{ targets, body, first_fire_at_unix, interval_seconds?, description? }`.
|
||
Agent-initiated schedules go through the approval queue instead
|
||
(via the `request_schedule_prompt` MCP tool).
|
||
- `PATCH /api/schedules/{id}` — partial edit. JSON body
|
||
`{ body?, description?, interval_seconds?, next_fire_at_unix?,
|
||
targets_add?, targets_remove? }`.
|
||
Missing key = "leave alone"; explicit `null` on
|
||
`description` / `interval_seconds` clears the field (so a
|
||
recurring schedule flips to one-shot when `interval_seconds`
|
||
is sent as `null`). `targets_add` is replace-on-conflict:
|
||
re-adding a previously-cancelled target drops the tombstone
|
||
and the target starts fresh (operator intent on re-add =
|
||
"this target is active again"). `targets_remove` delegates
|
||
to the same path as `cancel_targets` — tombstones preserve
|
||
audit, parent schedule auto-cancels when no active targets
|
||
remain. Refuses cancelled rows; returns the updated
|
||
`WireSchedule` on success.
|
||
- `POST /api/schedules/{id}/cancel` — cancel a schedule. Body
|
||
`{ targets?: ["name", …] }` cancels just those recipients;
|
||
absent or empty body cancels the whole schedule.
|
||
- `POST /api/schedules/{id}/fire-now` — out-of-band manual
|
||
pulse. Fires the schedule body once immediately to
|
||
every active target. Recurring schedules: `next_fire_at_unix`
|
||
is untouched; the regular cadence continues. One-shots: the
|
||
schedule is consumed (cancelled) after the manual fan-out.
|
||
Per-target `last_result` is annotated as a manual fire so
|
||
the audit trail distinguishes scheduled fires from operator-
|
||
triggered ones.
|
||
- `POST /api/schedules/{id}/pause` — pause a recurring or
|
||
one-shot schedule. A paused schedule does not fire regardless
|
||
of `next_fire_at`; the row gains a `⏸ paused` badge.
|
||
- `POST /api/schedules/{id}/resume` — resume a paused schedule.
|
||
Clears the pause flag without adjusting `next_fire_at`.
|
||
- `POST /api/meta-update` — `nix flake update` the selected
|
||
`meta/flake.lock` inputs, then rebuild the affected agents.
|
||
- `GET /api/dashboard/stream` — unified live event channel:
|
||
broker `sent` / `delivered`, plus the mutation events listed
|
||
below. Each frame carries `seq`.
|
||
- `GET /api/dashboard/history` — last ~200 broker messages
|
||
(wrapped as `{ seq, events }`) for the message-flow
|
||
terminal's backfill on page load.
|
||
|
||
## Dashboard event channel
|
||
|
||
Wire vocabulary on `/api/dashboard/stream` (kind tag is in the JSON
|
||
payload):
|
||
|
||
- `sent` / `delivered` — broker traffic, mirrored from the
|
||
intra-process channel by a forwarder task. Both carry `id: i64`
|
||
(the broker row id) and `in_reply_to: Option<i64>` for thread
|
||
rendering. The dashboard message-flow terminal renders reply
|
||
rows with a `↳ reply` tag that scroll-highlights the parent
|
||
row on click. Used by the message-flow terminal renderer and
|
||
the operator-inbox derived state.
|
||
- `approval_added` (id, agent, approval_kind, sha_short, diff,
|
||
description) / `approval_resolved` (id, agent, approval_kind,
|
||
sha_short, status, resolved_at, note, description) — pending
|
||
queue + history mutations. Client mutates a derived store and
|
||
re-renders only the approvals section.
|
||
- `question_added` (id, asker, question, options, multi,
|
||
asked_at, deadline_at, target) / `question_resolved` (id,
|
||
answer, answerer, answered_at, cancelled, target) — both
|
||
operator-targeted and peer (agent-to-agent) threads fire
|
||
these. The dashboard's questions pane surfaces both, with
|
||
filter chips (all / @operator / @peer / per-participant) and
|
||
an `0V3RR1D3` button on peer rows so the operator can
|
||
answer when an agent is stuck. The ttl watchdog fires
|
||
`question_resolved` with `answerer = "ttl-watchdog"` on
|
||
expiry.
|
||
- `transient_set` (name, transient_kind, since_unix) /
|
||
`transient_cleared` (name) — lifecycle action spinners. The
|
||
client ticks the elapsed-seconds badge off `since_unix`
|
||
client-side, no polling. `transient_kind` is an **open**
|
||
display string (the running node's own tag), not a fixed
|
||
enum — render it, don't branch on it.
|
||
- `container_state_changed` (container: ContainerView) /
|
||
`container_removed` (name) — per-row container mutations,
|
||
emitted by `Coordinator::rescan_containers_and_emit` from
|
||
every mutation site (`actions::approve` post-spawn,
|
||
`actions::destroy`, the lifecycle_action wrapper,
|
||
`auto_update::rebuild_agent`) and from the 10s
|
||
`crash_watch` poll. Client upserts/removes by name; the
|
||
pending overlay is read from `transientsState` since the
|
||
payload doesn't carry it.
|
||
- `rebuild_queue_changed` (seq, queue: `Vec<QueueEntry>`) —
|
||
full snapshot of the rebuild queue on every mutation (enqueue,
|
||
state transition, dedup collapse, terminal-history trim).
|
||
Same snapshot-over-diff rationale as `tombstones_changed` /
|
||
`meta_inputs_changed`: the list is small and the client renders
|
||
each DAG's multi-agent shape from its own `nodes` (no cross-DAG
|
||
grouping). Cold-loaded from `/api/state.rebuild_queue`.
|
||
- `schedules_changed` (seq, schedules: `Vec<WireSchedule>`) —
|
||
full snapshot of all scheduled prompts. Emitted after every
|
||
operator mutation via the `/api/schedules` surface (new /
|
||
edit / cancel / fire-now) and after the worker fires or
|
||
rearms a row. Same snapshot-shape rationale as
|
||
`rebuild_queue_changed`. The SCH3DUL3S tab subscribes and
|
||
re-renders `schedulesState` on receipt; tab activation still
|
||
re-fetches as a safety net for approval-path inserts and
|
||
disconnect windows.
|
||
- `reminders_changed` (seq, reminders: `Vec<PendingReminder>`) —
|
||
full snapshot of all pending reminders. Emitted after every
|
||
reminder mutation: agent `remind` calls (`agent_server`),
|
||
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
|
||
with Reminder kind, and the scheduler tick after each delivery
|
||
batch (`reminder_scheduler`). The SCH3DUL3S tab's reminders section
|
||
subscribes and calls `renderReminders` on receipt, so the list
|
||
updates live without polling.
|
||
- `capabilities_changed` (seq, caps: `Vec<str>`, descriptions: map,
|
||
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
|
||
capability grants. Emitted from the rebuild-queue worker after a
|
||
`PermChange` / Capabilities entry commits the JSON file. Payload
|
||
matches `GET /api/capabilities` shape so `renderCapabilities` can
|
||
be called directly. P3RM1SS10NS tab subscribes; activation
|
||
re-fetch still runs as a safety net.
|
||
- `tool_groups_changed` (seq, groups: `Vec<str>`, descriptions: map,
|
||
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
|
||
tool-group assignments. Emitted from the rebuild-queue worker after
|
||
a `PermChange` / ToolGroups entry commits the JSON file. Same
|
||
shape as `GET /api/tool-groups`; P3RM1SS10NS tab subscribes.
|
||
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
||
full snapshot of destroyed-but-state-kept agents. Emitted on
|
||
destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes.
|
||
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
|
||
snapshot of `meta/flake.lock` inputs. BU1LDS M3T4 1NPUTS tab
|
||
subscribes; same shape as `GET /api/meta-inputs`.
|
||
- `meta_update_running` (running: bool) — emitted when a
|
||
`nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS
|
||
tab uses this to show/hide the "⏳ meta-update running" banner.
|
||
- `audit_entry_added` (flattened `AuditEntry` fields: id, ts_unix,
|
||
agent, action, target, outcome, detail) — a single new audit-log
|
||
row. L0GS AUDIT sub-tab live-prepends the row and bumps the
|
||
"latest N of M" count, de-duped by id against the cold fetch.
|
||
|
||
`/api/state` is **only fetched on cold-load and on the few
|
||
forms that mutate non-event-derived state** (PURG3 +
|
||
meta-update, since tombstones + meta_inputs aren't event-
|
||
shaped yet). Every other section — approvals, questions,
|
||
transients, containers, operator inbox, message flow —
|
||
derives from `/api/dashboard/stream` after the initial snapshot,
|
||
maintaining its own client-side store and applying events on
|
||
top. The 5s periodic poll is gone.
|
||
|
||
Generalised form helpers: `form[data-confirm="…"]` pops
|
||
`confirm()` before submit; `form[data-prompt="…"]` pops
|
||
`prompt()` and stashes the answer in a hidden input named by
|
||
`data-prompt-field` (default `note`).
|
||
|