The M4TR1X ACC0UNTS section described the pre-heartbeat state: a
snapshot 'written at startup / rewritten each daemon (re)start' with an
'ambiguous' as_of, and the live-status dot as a 'dashboard-side
follow-up'. All three shipped since:
- the daemon now force-rewrites the snapshot every ~30s (heartbeat), so
as_of advances while alive and a stalled value is an honest dead-daemon
signal — documented, with the full dot state table (green / dim-green
'no heartbeat' age case / amber container-down + offline / grey);
- the login endpoint moved to /api/matrix-account-login and its failure
body is RFC 9457 application/problem+json (message in detail), not the
old 4xx { error } — corrected, with the 400/500 status scheme.
1285 lines
69 KiB
Markdown
1285 lines
69 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 (**R3BU1LD QU3U3** default,
|
||
then **M3T4 1NPUTS**, **K3PT ST4T3**, **C0NT41N3R L04D**). The page is its
|
||
own esbuild bundle (`core.js`) that cold-loads `/api/state` and subscribes
|
||
to `/dashboard/stream` for the same live events as the dashboard
|
||
(`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`,
|
||
`tombstones_changed`). The dashboard keeps the rebuild-queue *state* (it
|
||
drives the "building…" badges on SW4RM agent cards) but no longer renders
|
||
these panels.
|
||
|
||
**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 manager 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`).
|
||
|
||
**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`), agent name, source
|
||
chip (`manual | meta_update | auto_update | crash_recover | approval`
|
||
— green for operator-approved config changes), timing, and an
|
||
optional reason / error. Meta-update cascade rebuilds nest under
|
||
their parent entry (`parent_id` grouping; `rqe-child` CSS class).
|
||
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. When the worker has annotated the current phase
|
||
a cyan `↳ <step>` sub-line appears under the main row showing the
|
||
in-flight step name (e.g. `↳ meta prepare_deploy` → `↳ nixos-container update`
|
||
→ `↳ finalize deploy`). Terminal transitions clear `step` on the
|
||
backend so Done / Failed rows don't render stale labels.
|
||
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 L0GS page BUILD 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 the `step` advances)
|
||
never tears down the open stream; it hides when nothing is building and
|
||
each row keeps its `logs →` link out to the full L0GS history.
|
||
|
||
**K3PT ST4T3** — destroyed-but-state-kept tombstones (size +
|
||
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a
|
||
Spawn approval; existing state is reused), `PURG3` (wipes
|
||
state + applied dirs; `POST /purge-tombstone/{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`).
|
||
|
||
## M4TR1X ACC0UNTS page (`/matrix-accounts.html`)
|
||
|
||
Operator surface to provision / log in a per-agent **external** matrix
|
||
account and store its access token, without editing the agent's config
|
||
repo. Standalone page reached from the **Matrix accounts** tile on the
|
||
H0M3 hub, same minimal chrome as `/core.html` (a `← home` back-link +
|
||
title). Its own esbuild bundle (`matrix-accounts.js`); no SSE — it reads
|
||
`/api/state` once for the agent picker and otherwise works off two
|
||
purpose-built endpoints.
|
||
|
||
An agent picker (populated from `state.containers`, the live roster) drives a list of that
|
||
agent's accounts — name, homeserver, user id, and a status dot —
|
||
read from `GET /api/matrix-accounts?agent=<name>` →
|
||
`{ accounts: [ { name, homeserver, token_present, live, user_id } ], as_of_unix }`.
|
||
`token_present` is whether a token is **stored**; `live`, `homeserver`,
|
||
and `user_id` are backfilled from the matrix daemon's
|
||
`matrix-accounts.json` snapshot — a host-visible file the daemon
|
||
**force-rewrites every ~30s** (a heartbeat), so `as_of_unix` (the
|
||
snapshot mtime) advances while the daemon is alive and a *stalled* value
|
||
genuinely means "stopped publishing", not just "old snapshot". An account
|
||
with a token but absent from the snapshot reports `live: false`.
|
||
|
||
The status dot renders these states:
|
||
|
||
- **green** — `live` and the container is running: online.
|
||
- **dim green** — `live` but `as_of_unix` hasn't advanced in > ~90s (3
|
||
missed heartbeats) while the container is *not* down: the daemon stopped
|
||
publishing, so the snapshot's `live` is no longer trustworthy (likely
|
||
dead/wedged). Labelled "online · no heartbeat".
|
||
- **amber** — `live` but the container is **down** (a stopped container
|
||
⟹ a dead daemon, so the snapshot is stale); also the `token_present &&
|
||
!live` "provisioned but offline" case.
|
||
- **grey** — no token (not provisioned).
|
||
|
||
The container-down cross-reference (`/api/state`) takes precedence over
|
||
the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout
|
||
so freshness is always legible. When `live` is absent (an older backend
|
||
without the snapshot) the dot falls back to a token-present rendering.
|
||
|
||
The provision form (account name, homeserver, login method) posts
|
||
`POST /api/matrix-account-login` (`x-www-form-urlencoded`, operator-auth):
|
||
fields `agent, account, homeserver, mode=password|token, user_id?,
|
||
password?, token?` → `200 { ok, user_id }` on success. Failures come back
|
||
as RFC 9457 `application/problem+json` (`{ type, title, status, detail }`)
|
||
with the human-readable message in `detail` and the status code reflecting
|
||
the cause (400 for a validation error, 500 for a login / `whoami` /
|
||
internal failure); the page reads `detail` for display. The host coordinator performs the login
|
||
(password) or validates the token (`whoami`) and writes the bearer to
|
||
the agent's `matrixAccounts.<account>.tokenFile` via the same
|
||
privileged write path as the hive-internal `matrix-token`; the token is
|
||
**never** echoed back, and the page clears the secret inputs on submit
|
||
regardless of outcome. The account list reflects what is *provisioned*
|
||
(an account with a stored token), so a config-declared-but-unprovisioned
|
||
account appears only once it has been provisioned through the form.
|
||
|
||
## 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 manager |
|
||
| `read_host_journal` | unlocks `get_host_journal` to read journald from inside a container |
|
||
| `query_agent_state` | allows `get_loose_ends(agent: "<name>")` calls targeting other agents |
|
||
|
||
Each row is one agent. Columns are the capability names returned by
|
||
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
|
||
boxes only stages the change in-browser; nothing is written until the
|
||
page-level **save all** button (described below) is 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
|
||
(agents: `messaging`, `meta`, `inbox`, `execution`; manager: 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` + `manager` + 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: a `↯ fire now` button sends an
|
||
out-of-band manual pulse to every active target (recurring
|
||
schedules keep their cadence; one-shots are consumed after
|
||
the manual fire), an `✎ edit` button 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}`), and a `✕` button 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 /cancel-reminder/{id}`) and a
|
||
`R3TRY` button re-arms one whose delivery failed
|
||
(`POST /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-mcp 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
|
||
`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. Today the tab holds one section
|
||
(browser notifications); future preferences (theme, density,
|
||
etc.) land here as sibling `<h3>` blocks
|
||
under the same `<section id="tab-pane-settings">`.
|
||
|
||
**◇ browser notifications** — `🔔 enable notifications` button when
|
||
permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted
|
||
(mute silences the dispatch without revoking the OS-level
|
||
permission). On unsupported origins (non-secure context, or
|
||
browsers without the `Notification` API) the controls hide and a
|
||
single status line explains why. See `### Browser notifications`
|
||
below for the dispatch model + the three signals the dashboard
|
||
emits OS notifications on.
|
||
|
||
The 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 `/dashboard/history`;
|
||
live frames arrive on `/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 /op-send` drops
|
||
`{from:"operator", to, body}` into the broker; the resulting SSE frame
|
||
re-renders the terminal row. Manager 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) — each pointing to their respective surfaces. 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
|
||
`state.forge_present` and fills the href from `state.forge_public_url` (the
|
||
gateway-served public URL when `services.hyperhive.forge.behindGateway=true`)
|
||
or falls back to the direct `:3000` port. Operators without matrix or forge
|
||
enabled never see dead links. `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 four-item sub-tab strip. Tab
|
||
routing is hash-based (`#build`, `#agent`, `#system`, `#audit`); default is
|
||
`#build`.
|
||
|
||
**BUILD sub-tab** — all-agents build log history. Fetches
|
||
`GET /api/build-logs?limit=30` on load and on `↻ refresh`. 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 an elapsed-time
|
||
chip that ticks every second (updated by a `setInterval` on the
|
||
row; cleared when the build finishes or the stream errors). Expanding
|
||
a live row streams its output via `GET /api/build-logs/stream/{id}`
|
||
(newline-delimited JSON frames) with **sticky-bottom auto-scroll**:
|
||
the stream scrolls to keep the latest output visible as long as the
|
||
operator hasn't scrolled up manually; once the operator scrolls up,
|
||
new lines append silently at the bottom without jumping.
|
||
|
||
The build list auto-refreshes when a `rebuild_queue_changed` SSE
|
||
event fires while the BUILD tab is active (2s debounce to let the
|
||
backend commit the new row). The `↻ refresh` button triggers an
|
||
immediate re-fetch.
|
||
|
||
**AGENT sub-tab** — per-container journald viewer. Two selects: agent
|
||
name (populated from `GET /api/state`) and unit filter
|
||
(`hive-ag3nt.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 (currently
|
||
only `hive-c0re.service`). Fetches
|
||
`GET /api/journal-host?unit=hive-c0re.service&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 unix seconds; 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 `/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/tabs.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 `http://<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 C0R3 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 operator-initiated
|
||
**transient** (`transientsState`) is set on the dashboard the
|
||
moment the operator clicks start / stop / restart / rebuild /
|
||
destroy / spawn — covers the create-and-start window where the
|
||
container literally isn't up yet, before any backend state event
|
||
has fired. (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.
|
||
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 C0R3 page's R3BU1LD QU3U3). It replaces the
|
||
old per-transient spinner list; the actual running step 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 `/kill/<name>?graceful=1` (the bulk path appends
|
||
the flag per-agent); unticked is the instant hard stop
|
||
(`/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. `tabs.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` — sub-agents only (disabled if manager selected)
|
||
- `⇡ 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. moving the manager) 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 POSTs to
|
||
`/api/topology/set-parent` (form-encoded `child=<name>&new_parent=<target>`)
|
||
once per selected agent, which writes `topology.json` and re-emits
|
||
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/tabs.js::
|
||
renderApprovals`) with three stacked sections:
|
||
|
||
- **identity header** — glyph, `#id`, agent, kind chip, (for
|
||
`apply_commit`) the short proposal sha as `<code>`, and a
|
||
right-aligned `requested <N> ago` relative time from
|
||
`ApprovalView.requested_at`. 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 manager's description, then
|
||
drill-in triggers: `↳ view diff` opens the diff in the side
|
||
panel; `↳ commit on forge ↗` deep-links the proposal commit
|
||
into `agent-configs/<agent>` (shown only when `forge_present`).
|
||
Spawn approvals show 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 manager as
|
||
`HelperEvent::ApprovalResolved.note`.
|
||
|
||
The diff panel has a 3-way base toggle — **vs applied** (the
|
||
running tree, served instantly from the diff already on the
|
||
approval), **vs last-approved**, **vs previous proposal** — the
|
||
latter two fetched on click from `GET /api/approval-diff/{id}
|
||
?base=approved|previous`. Each line is classified client-side
|
||
(`+` / `-` / `@@` / `--- ` / `+++ ` → add / del / hunk / file).
|
||
|
||
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
|
||
|
||
- `POST /approve/{id}` — approve a pending approval. Fires
|
||
`ApprovalResolved` on the dashboard event channel; client
|
||
updates derived approvals state from the event.
|
||
- `POST /deny/{id}` (`note=<reason>`, optional) — deny a pending
|
||
approval with an optional operator-supplied reason. The reason
|
||
travels to the manager 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 /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
|
||
`destroy` accepts `purge=on` to also wipe state dirs.
|
||
- `POST /purge-tombstone/{name}` — wipe a tombstone's state dirs.
|
||
- `POST /answer-question/{id}` — answer a pending operator question.
|
||
- `POST /cancel-question/{id}` — cancel a pending question with
|
||
the sentinel `[cancelled]`. Same code path as a real answer.
|
||
- `POST /request-spawn` — queue a Spawn approval.
|
||
- `POST /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 /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/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` in seconds, `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 /api/approval-diff/{id}?base=applied|approved|previous` —
|
||
on-demand unified diff for an `ApplyCommit` approval against
|
||
the chosen base (running tree / last approved proposal /
|
||
previous queued proposal). Raw diff text, classified
|
||
client-side. `GET /static/marked.js` serves the vendored
|
||
`marked` bundle the side panel uses 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
|
||
`/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>` — 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 /cancel-reminder/{id}` — hard-delete a pending reminder.
|
||
- `POST /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
|
||
(manager MCP `request_schedule_prompt`).
|
||
- `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 /meta-update` — `nix flake update` the selected
|
||
`meta/flake.lock` inputs, then rebuild the affected agents.
|
||
- `GET /dashboard/stream` — unified live event channel:
|
||
broker `sent` / `delivered`, plus the mutation events listed
|
||
below. Each frame carries `seq`.
|
||
- `GET /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 `/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.
|
||
- `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's
|
||
`parent_id` grouping is most naturally re-derived from the
|
||
full list. 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.
|
||
|
||
`/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 `/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`).
|
||
|