Remove web_tools from the C4P4B1L1T13S table in dashboard.md and add it to the T00L GR0UPS section with a note that it gates Claude built-ins rather than MCP tools. Fix turn-loop.md to say 'tool-group-gated' and 'web_tools tool group' throughout.
860 lines
44 KiB
Markdown
860 lines
44 KiB
Markdown
# Dashboard layout
|
||
|
||
> Part of [Web UI](../web-ui.md). See also:
|
||
> [Shape (shared)](shape.md) · [Per-agent page](agent.md)
|
||
|
||
|
||
The dashboard (`/`) 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 is a separate full-page terminal at `/flow.html` — its
|
||
tab-strip entry is a cross-page link (`◆ FL0W ◆ →`), not a pane
|
||
swap.
|
||
|
||
**Chrome header** (fixed, overlays the active tab pane):
|
||
- **Tab strip**: `◆ SW4RM ◆`, `◆ Y3R C4LL ◆`, `◆ SYST3M ◆`,
|
||
`◆ P3RM1SS10NS ◆`, `◆ SCH3DUL3S ◆`, `◆ P33RS ◆` (hidden when `swarm.peers` is empty),
|
||
`◆ M4TR1X ◆ →` (optional page link, see below), `◆ FL0W ◆ →`
|
||
(page link), and `◆ S3TT1NGS ◆`. Count pills on SW4RM (container
|
||
count), Y3R C4LL (pending approvals + questions), and SCH3DUL3S
|
||
(active schedules); FL0W pill mirrors the operator inbox length
|
||
(hidden when zero); P33RS and S3TT1NGS have no count. The M4TR1X →
|
||
entry is hidden when `services.hyperhive.matrix.gui.enable` is off
|
||
(defaults to `matrix.enable`) so operators without the matrix GUI
|
||
on don't see a dead link — tabs.js gates the `hidden` attribute on
|
||
`state.matrix_gui_enabled` from `/api/state`.
|
||
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
|
||
— sits below the tab strip.
|
||
|
||
The FL0W page reuses the same chrome strip but its `◆ S3TT1NGS ◆ →`
|
||
entry is a cross-page link back to the dashboard
|
||
(`/#settings`) since the settings pane only lives there.
|
||
|
||
## 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.
|
||
|
||
**M1ND H4S QU3STI0NS** — pending operator-targeted `ask`
|
||
questions (amber pulsing border). Free-text fallback always
|
||
rendered alongside any option list; `multi=true` renders options
|
||
as checkboxes; submit merges selections + free text
|
||
comma-joined. Each row has a `✗ CANC3L` button. Questions with
|
||
a `ttl_seconds` show a `⏳ MM:SS` chip; the host-side watchdog
|
||
auto-cancels with `[expired]` when the deadline fires.
|
||
|
||
## SYST3M tab
|
||
|
||
Passive / rare-interaction state.
|
||
|
||
**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. Running entries tick elapsed
|
||
seconds live, and 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.
|
||
|
||
**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}`).
|
||
|
||
## 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.
|
||
|
||
Fetches fire on tab activation (not page-load) to avoid unnecessary
|
||
work when the operator never visits this tab.
|
||
|
||
**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 changes only the in-browser state; the `S4V3` button on the
|
||
right edge POSTs the full capability set for that agent to
|
||
`POST /api/capabilities/{agent}` as `{ caps: ["name", …] }` and
|
||
queues a rebuild. 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 and saving changes which groups are
|
||
active for the agent. Backed by `GET /api/tool-groups` (columns) and
|
||
`POST /api/tool-groups/{agent}` (save). A rebuild is queued after
|
||
each save 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`) + a save column (`.cap-save-col` /
|
||
`.tg-save-col`). Each tbody row is one agent: a name cell, checkbox
|
||
cells, and the `S4V3` button.
|
||
|
||
## 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 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.
|
||
|
||
## 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,
|
||
inbox-pill threshold, 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 — its tab-strip
|
||
`◆ S3TT1NGS ◆ →` entry is a cross-page link to the dashboard's
|
||
`#settings` route. 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 M4TR1X → strip entry, 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). Reuses the same `<header class="dashboard-chrome">` chrome
|
||
as the dashboard so the tab strip remains visible; SW4RM / Y3R
|
||
C4LL / SYST3M / SCH3DUL3S are cross-page links back to `/#<tab>`,
|
||
and the FL0W entry is marked active (`aria-current="page"`).
|
||
|
||
**0PER4T0R 1NB0X** — recent messages addressed to `operator`,
|
||
derived client-side from the dashboard event stream. Cold load
|
||
seeds from `/dashboard/history`'s 200-message backfill; subsequent
|
||
`sent` events with `to == "operator"` are appended live. Cap 50,
|
||
newest-first.
|
||
|
||
**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`. 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 both the terminal row and the inbox section.
|
||
Manager is addressed as `@root`.
|
||
|
||
## L0GS page (`/logs.html`)
|
||
|
||
A dedicated log-viewer page (not a tab pane — a separate HTML page),
|
||
linked from the SETTINGS tab's `◆ L0GS ◆ →` entry. Minimal chrome:
|
||
a `← dashboard` back link and a three-item sub-tab strip. Tab
|
||
routing is hash-based (`#build`, `#agent`, `#system`); 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 and a growing `…` duration chip; when complete, the row
|
||
re-renders with the final status. `status === null` = still running.
|
||
|
||
**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 refresh. Output
|
||
rendered as a `<pre>` block.
|
||
|
||
**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. Available to the
|
||
operator unconditionally (not capability-gated — the endpoint lives
|
||
on the hive-c0re dashboard, behind the gateway).
|
||
|
||
## Container row
|
||
|
||
A full-height **square agent icon** (5em, capped) on the left. The
|
||
icon is the **selection toggle**: click (or Enter/Space) adds/removes
|
||
the agent from the selection set; `aria-pressed` reflects the state;
|
||
the tooltip says "select … for bulk actions" or "deselect … (or press
|
||
Esc to clear all)". The `<img>` points at `<url>/icon`; load failure
|
||
falls back to the dimmed hyperhive mark (`/favicon.svg`). The card
|
||
body sits to the right with three stacked lines
|
||
(`assets/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** populated async from the agent backend
|
||
(`📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge profile`,
|
||
`↳ agent-configs mirror`, plus any agent-declared
|
||
`dashboardLinks` extras). The dashboard JS fetches
|
||
`GET /api/agent/{name}/links`, a same-origin passthrough proxy
|
||
that forwards the agent's own link list; the agent backend is
|
||
the single source of truth. The frontend resolves each
|
||
`AgentLink.kind` against a per-agent base URL that depends 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): `container` → `/agent/<name>/<url>` (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):
|
||
`container` → `http://<host>:<container.port>/<url>` (direct TCP
|
||
fallback). Forge links resolve against `http://<host>:3000`,
|
||
external links are already absolute. The same flag drives the
|
||
primary agent-name link + favicon fetch (`<url>/icon`), so the
|
||
whole row routes through the gateway as a unit.
|
||
**When the container is stopped** (`ContainerView.running = false`),
|
||
the host clears live-only fields before emitting the state, so
|
||
the dashboard never renders stale data: the badge chain is
|
||
replaced by a single muted `■ not running` badge, the nav-strip
|
||
fetch is skipped (the agent web server is down), and the
|
||
self-reported status text is 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…` badge sourced from
|
||
`rebuildQueueState` — so the SW4RM tab shows the same
|
||
rebuild progress visible on the SYST3M tab'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.
|
||
Container name + port, and a `ctx · Nk` chip showing the
|
||
agent's last-turn context size (from `ContainerView.ctx_tokens`,
|
||
read from the turn-stats sqlite on each `build_all` sweep;
|
||
absent until the first turn). The chip colour (green / yellow /
|
||
red) is keyed off the model's real context window: `build_all`
|
||
resolves the last turn's model against the host's per-model
|
||
`contextWindowTokens` config and exposes it as
|
||
`ContainerView.context_window_tokens`; the badge goes yellow
|
||
≥ 50% and red ≥ 75% of that window (the harness compaction
|
||
watermarks). When the window can't be resolved 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**, see below).
|
||
- Line 3: drill-in triggers —
|
||
- `↳ logs · <container>` — opens the side panel and lazy-
|
||
fetches journald via `GET /api/journal/{name}?unit=&lines=`
|
||
(`journalctl -M <container> -b --no-pager --output=short-iso`).
|
||
A unit dropdown (harness service / full machine journal) and
|
||
a refresh button live in the panel. The panel uses a column-flex
|
||
layout so the `<pre>` log surface fills the full remaining panel
|
||
height; scroll happens inside the `<pre>`, not the side
|
||
panel body.
|
||
- `↳ build logs · <agent>` — opens the side panel and fetches
|
||
the last 10 build-log headers via
|
||
`GET /api/build-logs/{agent}` (status chip + kind + age +
|
||
truncated cmdline per row). Clicking a row lazy-fetches its
|
||
full stdout+stderr from `GET /api/build-logs/id/{id}` and
|
||
expands it inline as a scrollable `<pre>`. A refresh button
|
||
re-fetches the header list. Backed by the `build_logs.sqlite`
|
||
store that `lifecycle::run` and `lifecycle::prebuild_toplevel`
|
||
write into.
|
||
- Plain navigation links (config repo, forge profile,
|
||
`dashboardLinks` extras) now live in the icon-only nav strip
|
||
on Line 1 — see above. The agent's `config` link
|
||
goes to the repo root; the deployed sha shows separately on
|
||
Line 1 as the `deployed:<sha>` chip, since the agent harness
|
||
can't know its own deployed commit.
|
||
|
||
`↻ 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).
|
||
|
||
### 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` — amber once the request has been
|
||
pending ≥ 1h so a stale approval stands out.
|
||
- **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 `window.prompt()` on click.
|
||
- `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.
|
||
- `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 both the message-flow terminal and
|
||
the operator inbox 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/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.
|
||
- `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>> }`. `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).
|
||
- `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>> }`. `caps` is the
|
||
ordered list of all known capability names; `assignments` is the
|
||
per-agent grant map (absent agents have no extra capabilities).
|
||
- `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.
|
||
- `GET /api/schedules` — list all schedules (active and
|
||
recently cancelled) for the SYST3M 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`.
|
||
|
||
`/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`).
|
||
|