docs(#727): split docs/web-ui.md into shape / dashboard / agent
docs/web-ui.md (1315 lines) split into three sub-files: - docs/web-ui/shape.md — shared SPA skeleton, SSE multiplexing, Worker-death self-heal, terminal pane, listener bind, relative paths, atomic repaint, side panel - docs/web-ui/dashboard.md — SW4RM/Y3R/SYST3M/SCH3DUL3S/S3TT1NGS tabs, container row, topology tree, selection bar, approval card, dashboard endpoints + event channel - docs/web-ui/agent.md — header, terminal, composer, inbox, live view, slash commands, per-agent endpoints, stats page docs/web-ui.md replaced with a thin index linking all three. Section anchors in docs (gateway.md, gotchas.md), Rust doc comments (hive-ag3nt/src/web_ui.rs), and nix/templates/weston-vnc.nix updated to point at the correct sub-file. README and CLAUDE.md file-map updated with sub-file links. Inline // comments in frontend source left unchanged (they reference the index which redirects to the right sub-file).
This commit is contained in:
parent
e1e5195081
commit
c7360cf0bb
10 changed files with 1373 additions and 1319 deletions
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -298,7 +298,13 @@ nix/
|
||||||
docs/
|
docs/
|
||||||
conventions.md naming, identity=socket, async forms, commit style
|
conventions.md naming, identity=socket, async forms, commit style
|
||||||
gotchas.md NixOS / nspawn quirks and lessons learned
|
gotchas.md NixOS / nspawn quirks and lessons learned
|
||||||
web-ui.md dashboard + per-agent page layouts and endpoints
|
web-ui.md index → web-ui/shape.md (shared skeleton, SSE,
|
||||||
|
terminal, listener bind, relative paths, atomic
|
||||||
|
repaint, side panel), web-ui/dashboard.md (tab
|
||||||
|
contents, container row, topology, selection bar,
|
||||||
|
approval card, endpoints + event channel),
|
||||||
|
web-ui/agent.md (header, terminal, composer,
|
||||||
|
inbox, live view, per-agent endpoints, stats)
|
||||||
turn-loop.md claude invocation, wake prompt, MCP tool surface
|
turn-loop.md claude invocation, wake prompt, MCP tool surface
|
||||||
approvals.md approval flow, manager policy, helper events
|
approvals.md approval flow, manager policy, helper events
|
||||||
persistence.md sqlite dbs, retention, state dir layout
|
persistence.md sqlite dbs, retention, state dir layout
|
||||||
|
|
@ -324,7 +330,10 @@ Pick the doc that matches your task. None depend on the others —
|
||||||
read them à la carte.
|
read them à la carte.
|
||||||
|
|
||||||
- **"What does the dashboard look like?"** →
|
- **"What does the dashboard look like?"** →
|
||||||
[`docs/web-ui.md`](docs/web-ui.md).
|
[`docs/web-ui.md`](docs/web-ui.md) (index; sub-pages:
|
||||||
|
[`shape`](docs/web-ui/shape.md),
|
||||||
|
[`dashboard`](docs/web-ui/dashboard.md),
|
||||||
|
[`agent`](docs/web-ui/agent.md)).
|
||||||
- **"How does the per-agent terminal classify + colour
|
- **"How does the per-agent terminal classify + colour
|
||||||
events?"** → [`docs/terminal-rendering.md`](docs/terminal-rendering.md).
|
events?"** → [`docs/terminal-rendering.md`](docs/terminal-rendering.md).
|
||||||
- **"How does claude get its prompt and what tools does it have?"** →
|
- **"How does claude get its prompt and what tools does it have?"** →
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Depth lives in [`docs/`](docs/) — pick the one matching your task:
|
||||||
|
|
||||||
| reading path | doc |
|
| reading path | doc |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) |
|
| dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) ([shape](docs/web-ui/shape.md) · [dashboard](docs/web-ui/dashboard.md) · [agent](docs/web-ui/agent.md)) |
|
||||||
| claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) |
|
| claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) |
|
||||||
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
|
| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) |
|
||||||
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
|
| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) |
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ flip together: the primary agent-name link, the favicon fetch
|
||||||
`/api/agent/<name>/links`. `forge`-kind nav-strip links still
|
`/api/agent/<name>/links`. `forge`-kind nav-strip links still
|
||||||
resolve against `http://<host>:3000` (separate sub-domain transition
|
resolve against `http://<host>:3000` (separate sub-domain transition
|
||||||
tracked by `forge.behindGateway`); `external`-kind links are
|
tracked by `forge.behindGateway`); `external`-kind links are
|
||||||
already absolute. See `docs/web-ui.md::Container row` for the
|
already absolute. See `docs/web-ui/dashboard.md::Container row` for the
|
||||||
frontend-side derivation.
|
frontend-side derivation.
|
||||||
|
|
||||||
## Self-signed TLS (`selfSignedTls`)
|
## Self-signed TLS (`selfSignedTls`)
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ derivation's `nativeBuildInputs`.
|
||||||
`nix/templates/weston-vnc.nix` adds an optional Weston Wayland
|
`nix/templates/weston-vnc.nix` adds an optional Weston Wayland
|
||||||
compositor with the VNC backend, surfaced as
|
compositor with the VNC backend, surfaced as
|
||||||
`hyperhive.gui.enable = true` per-agent. The harness's
|
`hyperhive.gui.enable = true` per-agent. The harness's
|
||||||
`/screen/ws` WebSocket relay (`docs/web-ui.md::Per-agent endpoints`)
|
`/screen/ws` WebSocket relay (`docs/web-ui/agent.md::Per-agent endpoints`)
|
||||||
connects to the compositor at `127.0.0.1:<vnc_port>`.
|
connects to the compositor at `127.0.0.1:<vnc_port>`.
|
||||||
|
|
||||||
- **Port allocation**: deterministic FNV-1a of the agent name
|
- **Port allocation**: deterministic FNV-1a of the agent name
|
||||||
|
|
|
||||||
1340
docs/web-ui.md
1340
docs/web-ui.md
File diff suppressed because it is too large
Load diff
336
docs/web-ui/agent.md
Normal file
336
docs/web-ui/agent.md
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# Per-agent page
|
||||||
|
|
||||||
|
> Part of [Web UI](../web-ui.md). See also:
|
||||||
|
> [Shape (shared)](shape.md) · [Dashboard layout](dashboard.md)
|
||||||
|
|
||||||
|
|
||||||
|
Three fixed-position layers frame a full-viewport terminal:
|
||||||
|
|
||||||
|
**Fixed-overlay header** (`<header class="agent-header">`): frosted
|
||||||
|
glass — `backdrop-filter: blur` lets scrolled terminal rows show
|
||||||
|
through. Three flex columns:
|
||||||
|
|
||||||
|
- **Agent icon** (`<img class="agent-icon">`): fixed-size square
|
||||||
|
identity anchor — `width: 5em; height: 5em` with explicit pixel
|
||||||
|
sizing so the `<img>`'s intrinsic (large) dimensions don't push
|
||||||
|
the parent flex container open via `align-items: stretch`-driven
|
||||||
|
height feedback. 5em ≈ header content area (header `min-height: 6em`
|
||||||
|
minus `2 × 0.5em` padding). `align-self: flex-start` keeps the
|
||||||
|
icon stuck to the top so a state-row line-wrap doesn't drag it
|
||||||
|
down with it. Falls back to the dimmed hyperhive mark on load
|
||||||
|
error.
|
||||||
|
- **Main column** (`.agent-header-main`): two rows.
|
||||||
|
- Row 1 (`.agent-header-title-row`): title (`<h2 id="title">`) +
|
||||||
|
meta-nav (`<nav id="meta-links">`). Meta-nav renders
|
||||||
|
backend-supplied `StateSnapshot.links` as icon-only anchors — always
|
||||||
|
`📊 stats` (`kind = Container`); `🖥 screen` when VNC is enabled;
|
||||||
|
`⬡ forge` (profile) + `↳ config` (agent-configs mirror) when the
|
||||||
|
agent has a forge account; any `hyperhive.dashboardLinks` extras
|
||||||
|
(`kind = External`). A `↑ dashboard` link is prepended by the JS
|
||||||
|
so the host dashboard is one click away. `GET /api/agent/{name}/links`
|
||||||
|
is the single source of truth. Each `NavLink.kind` resolves
|
||||||
|
differently in the frontend: `Container` → same-origin path
|
||||||
|
(the agent page is itself container-local); `Forge` →
|
||||||
|
`http://<host>:3000<url>`; `External` → already absolute.
|
||||||
|
All anchors are built via `el()` — agent-declared icon /
|
||||||
|
label / url strings never reach `innerHTML` (XSS-safe by
|
||||||
|
construction).
|
||||||
|
- Row 2 (`.agent-state-row`): alive badge + state badge + model chip
|
||||||
|
+ ctx badge + cost badge + last-turn chip + cancel button.
|
||||||
|
- Alive badge: `● alive` (green) / `⊘ rate limited` (red) /
|
||||||
|
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
||||||
|
Driven by `LiveEvent::StatusChanged`.
|
||||||
|
- State badge: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
||||||
|
`○ offline` / `… booting` + age suffix. Driven by
|
||||||
|
`LiveEvent::TurnStateChanged ({ state, since_unix })`.
|
||||||
|
- Model chip: `model · <name>`. Driven by `LiveEvent::ModelChanged`.
|
||||||
|
- Ctx badge: `ctx · 142k` — last inference's prompt size.
|
||||||
|
Tooltip shows % of window when `context_window_tokens` is known.
|
||||||
|
- Cost badge: `cost · 1.3M` — cumulative tokens billed across every
|
||||||
|
inference in the last turn (tool-heavy turns rebill the cached
|
||||||
|
prefix per call — cost signal, not size signal).
|
||||||
|
- Both driven by `LiveEvent::TokenUsageChanged { ctx, cost }` at
|
||||||
|
turn-end.
|
||||||
|
- `■ cancel turn` (visible while thinking) → `POST /api/cancel`.
|
||||||
|
- **Right cluster** (`.agent-header-pills`): flyout pills + overflow.
|
||||||
|
- **Inbox pill** (`📬 inbox · N`): hidden when empty; click opens
|
||||||
|
the inbox flyout in the side panel.
|
||||||
|
- **Loose-ends pill** (`🪢 loose ends · N`): hidden when empty;
|
||||||
|
click opens the loose-ends flyout.
|
||||||
|
- **Overflow button** (`⋯`): always visible. Opens a frosted popover
|
||||||
|
(`#overflow-menu`, positioned outside the header to escape any
|
||||||
|
stacking context) with four rows: `↑ dashboard` (link), `↻ rebuild
|
||||||
|
container` (POST confirm, same action as the dashboard R3BU1LD
|
||||||
|
button), `↻ new claude session` (POST confirm → `POST
|
||||||
|
/api/new-session`; next turn drops `--continue`), `🔓 logout`
|
||||||
|
(POST confirm → `POST /api/logout`; SIGINTs any in-flight turn,
|
||||||
|
wipes OAuth credential files, flips the agent to `needs_login`
|
||||||
|
— session history preserved). All destructive actions require
|
||||||
|
one extra click to acknowledge — rare ops shouldn't live in the
|
||||||
|
primary state strip. The popover's display rules are scoped to
|
||||||
|
`:not([hidden])` so the `[hidden]` HTML attribute's UA `display:
|
||||||
|
none` isn't overridden by the author CSS's `display: flex` —
|
||||||
|
the popover stays hidden until JS removes the attribute.
|
||||||
|
|
||||||
|
`/api/state` is fetched once on cold load (+ while
|
||||||
|
`status === 'needs_login_in_progress'`); all other updates arrive via
|
||||||
|
SSE. Snapshot includes `context_window_tokens` for the ctx badge
|
||||||
|
tooltip, and `qualified_label` — the hive-qualified agent name
|
||||||
|
(`name@domain` form when `HYPERHIVE_HIVE_DOMAIN` is set, otherwise
|
||||||
|
just `name`). The frontend uses `qualified_label` to set the browser
|
||||||
|
tab title so two tabs from different hives are distinguishable; the
|
||||||
|
header `<h2 id="title">` stays short.
|
||||||
|
|
||||||
|
**Main content** (`<main class="agent-main">`): fills the viewport
|
||||||
|
and scrolls behind the fixed header + footer.
|
||||||
|
- `#status` overlay: empty when online; shows the login form / OAuth
|
||||||
|
URL when `status` is `needs_login_*`. The OAuth code input is
|
||||||
|
`type="password"` with a `👁 reveal` toggle that flips it back to
|
||||||
|
`text` on press so the operator can sanity-check the paste before
|
||||||
|
submit — avoids accidental on-screen token exposure to
|
||||||
|
shoulder-surfers or screenshots. `autocomplete="one-time-code"`
|
||||||
|
is the semantic value for OAuth codes (per WHATWG): browsers may
|
||||||
|
silently ignore `autocomplete="off"` on `type="password"`, but
|
||||||
|
`one-time-code` is honoured and suppresses the "save password
|
||||||
|
for this site?" prompt that would otherwise fire on submit.
|
||||||
|
- Terminal-wrap: live event tail (sticky-bottom auto-scroll +
|
||||||
|
`↓ N new` pill when not at bottom). The pill is **anchored in
|
||||||
|
`.agent-main`**, not in `log.parentElement = .terminal-wrap`:
|
||||||
|
`.terminal-wrap` applies `backdrop-filter: blur` for the frost
|
||||||
|
effect, which creates a CSS stacking context — anchoring the
|
||||||
|
pill inside that context would trap its `z-index` below the
|
||||||
|
fixed composer in the root stacking context, and it'd never
|
||||||
|
float. `.agent-main` has no backdrop-filter (no stacking-context
|
||||||
|
creators), so the pill's `z-index` reaches the root and properly
|
||||||
|
composites above the composer. Geometry is unchanged —
|
||||||
|
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
||||||
|
area.
|
||||||
|
|
||||||
|
**Fixed-overlay footer** (`<footer class="agent-composer">`): frosted
|
||||||
|
glass, symmetric with the header. Contains the operator-input
|
||||||
|
textarea (`#term-input`) — multi-line, Enter sends, Shift+Enter
|
||||||
|
newlines, Tab-completes slash commands (see "Terminal-embedded
|
||||||
|
prompt" below).
|
||||||
|
|
||||||
|
**Side panel** (slide-in from right): singleton shared with the
|
||||||
|
dashboard's side panel shape. Carries inbox and loose-ends flyouts
|
||||||
|
(opened via the header pills) as well as long content (file previews,
|
||||||
|
diffs, journald logs). Inbox flyout: last 30 messages addressed to
|
||||||
|
this agent (`AgentRequest::Recent { limit: 30 }`); reply messages
|
||||||
|
indented with `↳ reply ·` in amber. A `✓ mark all read` button
|
||||||
|
appears in the flyout header when the inbox is non-empty;
|
||||||
|
clicking it confirms then POSTs cross-origin to the core
|
||||||
|
dashboard's `POST /api/agent/{name}/mark-all-read` — all pending
|
||||||
|
messages for this agent are acked, the harness won't receive
|
||||||
|
wake-prompts for them. A `{ marked: N }` pill surfaces the count.
|
||||||
|
The displayed message list stays put (it shows the most-recent N
|
||||||
|
regardless of ack state); the unread badge on the next turn-start
|
||||||
|
will reflect zero. Loose-ends flyout: questions,
|
||||||
|
approvals, and reminders pending against this agent (`GET /api/loose-ends`);
|
||||||
|
question rows carry an inline answer form that POSTs cross-origin to
|
||||||
|
the core dashboard's `/answer-question/{id}` so the operator answers
|
||||||
|
*as operator* (see `docs/boundary.md`).
|
||||||
|
|
||||||
|
**Ask → operator inline-answer binding.** When the agent emits
|
||||||
|
`mcp__hyperhive__ask(to: "operator", ...)`, the tool_use renderer
|
||||||
|
mounts an empty slot (`<div class="ask-answer-inline-slot">`)
|
||||||
|
right under the `↳ ask → operator` row in the terminal scrollback
|
||||||
|
and pushes a reference into `pendingAskBinds`. The broker assigns
|
||||||
|
the question id asynchronously, so the slot waits — and the next
|
||||||
|
`/api/loose-ends` refresh runs `reconcileAskBinds()`, which walks
|
||||||
|
the slot list and pairs each unbound slot with the first unclaimed
|
||||||
|
pending operator-bound question whose `question` text matches the
|
||||||
|
slot's stashed `_askQuestion`. On match the slot mounts the
|
||||||
|
`buildAnswerForm` (same form shape as the loose-ends flyout —
|
||||||
|
POSTs to the core's `/answer-question/{id}` cross-origin). Slots
|
||||||
|
stay in the array after binding so the reconciler can flip them
|
||||||
|
to a neutral `[resolved]` tag when the question later disappears
|
||||||
|
from the pending list. Disappearance can mean answered, cancelled
|
||||||
|
by the asker, or TTL-expired — the neutral label avoids
|
||||||
|
mis-asserting "✓" on the cancel / expire paths; full resolution
|
||||||
|
state is visible via the side-panel history. A defensive prune
|
||||||
|
walks the slot list each tick and drops any whose DOM node has
|
||||||
|
been removed (e.g. via a future "clear single row" affordance),
|
||||||
|
so stale references don't accumulate. Slots whose question never
|
||||||
|
arrives (e.g. the agent cancelled the ask, or the question is
|
||||||
|
older than the loose-ends retention window) stay empty — the
|
||||||
|
operator can still answer via the side panel, no regression.
|
||||||
|
|
||||||
|
## Live view
|
||||||
|
|
||||||
|
Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>`
|
||||||
|
plus a sqlite-backed history at `/state/hyperhive-events.sqlite`.
|
||||||
|
The harness emits `TurnStart { from, body, unread }`,
|
||||||
|
`Stream(value)` (one per parsed stream-json line), `Note`,
|
||||||
|
`TurnEnd { ok, note }`. The web UI:
|
||||||
|
|
||||||
|
- fetches `GET /events/history` on page load and replays the last
|
||||||
|
2000 events (oldest first, with `.no-anim` so they don't
|
||||||
|
stagger);
|
||||||
|
- then subscribes to `GET /events/stream` (SSE) for live tail;
|
||||||
|
- shows a granular state badge above the terminal, driven
|
||||||
|
authoritatively from `/api/state.turn_state`. SSE turn_start /
|
||||||
|
turn_end still flip the badge instantly between renders;
|
||||||
|
- sticky-bottom auto-scroll: scrolling up parks the view; new rows
|
||||||
|
surface a "↓ N new" pill instead of yanking;
|
||||||
|
- terminal-themed: phosphor mauve glow, Crust bg,
|
||||||
|
backdrop-filter blur, row fade-in slide-up.
|
||||||
|
|
||||||
|
Per-stream rendering:
|
||||||
|
|
||||||
|
- `Stream` `tool_use` →
|
||||||
|
- `Write` / `Edit`: collapsed `<details>` with a +/- diff body
|
||||||
|
(`-` lines from `input.old_string`, `+` lines from
|
||||||
|
`input.new_string` or every line of `input.content`).
|
||||||
|
Summary carries the path + line counts.
|
||||||
|
- others (`Read /path`, `Bash $ cmd`, `mcp__hyperhive__send →
|
||||||
|
operator: "..."`, etc.): flat one-line per-tool format.
|
||||||
|
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
|
||||||
|
`<details>` `▸ ← Nl · headline` (click to expand full body).
|
||||||
|
- `Stream` `thinking` → text content if claude provided one,
|
||||||
|
otherwise the bare `· thinking …` indicator.
|
||||||
|
- `Stream` `system init`, `result`, `rate_limit_event` are
|
||||||
|
dropped — too noisy.
|
||||||
|
- `Note` → `· text`.
|
||||||
|
- `TurnEnd` → `✓ turn ok` / `✗ turn fail — note`, triggers a
|
||||||
|
`refreshState()`.
|
||||||
|
|
||||||
|
## Terminal-embedded prompt
|
||||||
|
|
||||||
|
The operator input lives *inside* the terminal-wrap as a
|
||||||
|
prompt-style textarea below the live tail: multi-line (Enter
|
||||||
|
sends, Shift+Enter newlines), tab-completes slash commands.
|
||||||
|
|
||||||
|
Slash commands today:
|
||||||
|
|
||||||
|
- `/help` — list commands locally.
|
||||||
|
- `/clear` — wipe the local terminal view (server history kept).
|
||||||
|
- `/cancel` — `POST /api/cancel` → host shellouts `pkill -INT
|
||||||
|
claude`, emits a Note. Also surfaces as a `■ cancel turn`
|
||||||
|
button in the state row while state=thinking.
|
||||||
|
- `/compact` — `POST /api/compact` → host spawns
|
||||||
|
`turn::compact_session` in the background; output streams into
|
||||||
|
the live panel.
|
||||||
|
- `/model <name>` — `POST /api/model` flipping `Bus::set_model`.
|
||||||
|
Takes effect on the next turn; persisted to
|
||||||
|
`/state/hyperhive-model` so the override survives harness
|
||||||
|
restart / rebuild.
|
||||||
|
- `/new-session` — `POST /api/new-session` (confirms first).
|
||||||
|
Arms a one-shot on the Bus; next turn runs without
|
||||||
|
`--continue`, dropping the resume session entirely.
|
||||||
|
- `/logout` — `POST /api/logout` (confirms first). Wipes OAuth
|
||||||
|
credential files, parks the agent in `needs_login`. Session
|
||||||
|
history (`~/.claude/projects/`) is preserved.
|
||||||
|
|
||||||
|
Unknown `/foo` shows an error row instead of being silently sent.
|
||||||
|
|
||||||
|
## Per-agent endpoints
|
||||||
|
|
||||||
|
All POSTs return 200 (no 303 redirects). The matching mutations
|
||||||
|
fire `LiveEvent` variants on the per-agent bus, so the client
|
||||||
|
doesn't refetch `/api/state` on submit — the SSE stream
|
||||||
|
delivers the new state faster anyway. Only the login flow still
|
||||||
|
polls (session output streams in updates that aren't event-
|
||||||
|
shaped).
|
||||||
|
|
||||||
|
- `POST /send` — operator-injected message into this agent's inbox.
|
||||||
|
- `POST /login/{start,code,cancel}` — claude OAuth login flow.
|
||||||
|
Start/cancel emit `LiveEvent::StatusChanged` to flip the
|
||||||
|
badge to/from `needs_login_in_progress`.
|
||||||
|
- `POST /api/cancel` — SIGINT the in-flight claude turn. Emits a
|
||||||
|
`LiveEvent::Note`.
|
||||||
|
- `POST /api/compact` — run `/compact` on the persistent session
|
||||||
|
(same MCP config + system prompt + allowed tools as a normal
|
||||||
|
turn — only the stdin payload differs). Flips state to
|
||||||
|
`Compacting` via `Bus::set_state`, which emits
|
||||||
|
`TurnStateChanged`.
|
||||||
|
- `POST /api/model` (`model=<name>`) — switch the model for
|
||||||
|
future turns. `Bus::set_model` emits `ModelChanged`.
|
||||||
|
- `POST /api/new-session` — arm a one-shot for the next turn to
|
||||||
|
drop `--continue`. Emits a `LiveEvent::Note`.
|
||||||
|
- `POST /api/logout` — three-step teardown that re-uses the
|
||||||
|
existing `wait_for_login` resumption path:
|
||||||
|
1. SIGINT any running claude (matches `/api/cancel`'s pattern —
|
||||||
|
idempotent no-op when nothing is running) so the credential
|
||||||
|
wipe doesn't race a mid-API-call turn.
|
||||||
|
2. Delete only the OAuth credential files
|
||||||
|
(`.credentials.json` + `mcp-needs-auth-cache.json` under
|
||||||
|
`~/.claude/`). **Preserves** session history files
|
||||||
|
(`projects/<hash>/*.jsonl`), sessions, shell-snapshots,
|
||||||
|
plans, settings, telemetry, and the dir itself, so
|
||||||
|
`claude --continue` keeps working after a fresh login.
|
||||||
|
Wholesale `remove_dir_all` of `~/.claude/` was the previous
|
||||||
|
shape and broke session continuity; the narrowed allow-list
|
||||||
|
is the fix.
|
||||||
|
3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note`
|
||||||
|
describing exactly what was wiped, then emit
|
||||||
|
`needs_login_idle`. The turn-loop's next iteration parks
|
||||||
|
into `wait_for_login`, which snapshots the credential dir
|
||||||
|
(now missing the wiped files) and resumes when a fresh
|
||||||
|
credentials file appears via the dashboard's `/login/code`
|
||||||
|
flow (the same mtime-resumption path manual re-login uses).
|
||||||
|
|
||||||
|
Always returns 200 with a body describing what happened —
|
||||||
|
per-file errors are folded into the response + the Note so the
|
||||||
|
operator sees them in the live panel rather than as an HTTP
|
||||||
|
error. Missing files (already logged out) are treated as
|
||||||
|
idempotent.
|
||||||
|
- `GET /events/history` — replay buffer for the terminal.
|
||||||
|
- `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket
|
||||||
|
renderer — deliberately thin, just enough to display the
|
||||||
|
desktop + forward pointer + keyboard. A production-grade viewer
|
||||||
|
would vendor noVNC; this file ships the minimal in-tree variant).
|
||||||
|
Only accessible when `hyperhive.gui.enable = true` in the agent's
|
||||||
|
`agent.nix`; the harness shows a 🖥 screen link in the state row
|
||||||
|
when `gui_vnc_port` is present. Toolbar: `⤢ fit` CSS-downscales
|
||||||
|
the canvas to the window via `relayoutCanvas()` setting explicit
|
||||||
|
pixel dimensions on the canvas — *not* CSS `max-width/max-height`,
|
||||||
|
because a flex item's automatic minimum size (`min-width: auto`
|
||||||
|
resolves to the canvas's intrinsic framebuffer resolution) silently
|
||||||
|
clamps `max-*` back up, making fit mode a no-op that just centred +
|
||||||
|
clipped the oversized canvas. The fit-mode rules pin the canvas
|
||||||
|
with `flex: none; min-width: 0; min-height: 0` so the JS-set size
|
||||||
|
sticks. `⤡ match size` sends an RFB `SetDesktopSize` request so the
|
||||||
|
server (weston) changes its real output resolution to the window
|
||||||
|
dimensions; enabled once the server advertises the
|
||||||
|
`ExtendedDesktopSize` pseudo-encoding (`-308` rect in the header).
|
||||||
|
Fit-mode state persists in `localStorage` (`screen-fit`); default
|
||||||
|
is on. Pointer coordinates are rescaled in `sendPointer` so clicks
|
||||||
|
land on the right pixel regardless of CSS scale.
|
||||||
|
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
||||||
|
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
||||||
|
Transparent to any RFB variant. VNC port comes from
|
||||||
|
`/etc/hyperhive/gui.json` (written by the weston startup
|
||||||
|
script in `weston-vnc.nix`).
|
||||||
|
|
||||||
|
Bus events (new vocabulary on `/events/stream`):
|
||||||
|
|
||||||
|
- `status_changed { status }` — `online` / `rate_limited` /
|
||||||
|
`needs_login_idle` / `needs_login_in_progress`. Drives the
|
||||||
|
alive-badge. `rate_limited` is set when the harness detects a
|
||||||
|
429 response and cleared when the retry sleep expires.
|
||||||
|
- `model_changed { model }` — drives the model chip.
|
||||||
|
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
|
||||||
|
— drives the ctx + cost badges. Emitted from
|
||||||
|
`Bus::record_turn_usage` at turn-end; `ctx` is the last
|
||||||
|
inference's usage (current context size), `cost` is the
|
||||||
|
cumulative across every inference (the `result` event's
|
||||||
|
totals).
|
||||||
|
- `turn_state_changed { state, since_unix }` — drives the
|
||||||
|
state badge (`idle`/`thinking`/`compacting`).
|
||||||
|
|
||||||
|
## Stats page
|
||||||
|
|
||||||
|
`GET /stats` is a separate per-agent page (served by the
|
||||||
|
harness, linked from the per-agent page's `📊 stats →` and from
|
||||||
|
each dashboard container row). Turn analytics, read-only, from
|
||||||
|
`/state/hyperhive-turn-stats.sqlite`. `GET /api/stats?window=
|
||||||
|
24h|7d|30d` returns a time-bucketed `Snapshot`; the page renders
|
||||||
|
it with Chart.js (vendored from a CDN). Charts: turns,
|
||||||
|
duration (p50 · p95 · avg), context tokens, token cost per
|
||||||
|
bucket, a **turns-by-model** stacked bar (model choice drives
|
||||||
|
token cost, so it sits directly under the cost chart), and
|
||||||
|
doughnuts for tool / wake-source / result mix. A summary chip
|
||||||
|
row carries window totals. `stats.rs` opens the sqlite db
|
||||||
|
read-only and degrades to an empty snapshot on any error — the
|
||||||
|
page is decorative, never authoritative.
|
||||||
713
docs/web-ui/dashboard.md
Normal file
713
docs/web-ui/dashboard.md
Normal file
|
|
@ -0,0 +1,713 @@
|
||||||
|
# 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`, `#schedules`) 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 ◆`,
|
||||||
|
`◆ SCH3DUL3S ◆`, `◆ 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); S3TT1NGS has 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}`).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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 `@manager` (the broker recipient string),
|
||||||
|
not `@hm1nd` (the container name).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- 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/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`).
|
||||||
|
|
||||||
270
docs/web-ui/shape.md
Normal file
270
docs/web-ui/shape.md
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
# Shape (shared by both)
|
||||||
|
|
||||||
|
> Part of [Web UI](../web-ui.md). See also:
|
||||||
|
> [Dashboard layout](dashboard.md) · [Per-agent page](agent.md)
|
||||||
|
|
||||||
|
|
||||||
|
- `GET /` → `index.html` from the bundled frontend dist (see
|
||||||
|
`frontend/`). Both binaries' routers declare their dynamic
|
||||||
|
endpoints first and then `fallback_service(ServeDir::new(...))`
|
||||||
|
pointed at `HIVE_STATIC_DIR` — anything not matched by an API or
|
||||||
|
action route is served from the dist. Dashboard dist lives at
|
||||||
|
`${frontend}/dashboard`; per-agent dist is the merged
|
||||||
|
`hyperhive.frontend.mergedDist` (default agent dist + per-agent
|
||||||
|
`extraFiles` overlay).
|
||||||
|
- `GET /static/*` → bundled CSS + JS produced by esbuild
|
||||||
|
(`frontend/packages/{dashboard,agent}/build.mjs`). Both pages
|
||||||
|
pull the shared terminal pane + Catppuccin palette + typography
|
||||||
|
from `@hive/shared` (was `hive-fr0nt`); the CSS bundle inlines
|
||||||
|
`base.css` + `terminal.css` via esbuild's `@import` resolution.
|
||||||
|
`terminal.js` exports `{ create, linkify }` as ES module
|
||||||
|
members (no more `window.HiveTerminal` global outside the
|
||||||
|
back-compat shim the IIFE bodies still use). The dashboard's
|
||||||
|
`#msgflow` and the per-agent `#live` log are both backed by
|
||||||
|
this terminal — sticky-bottom auto-scroll, "↓ N new" pill,
|
||||||
|
history backfill, SSE plumbing all live there. Each page
|
||||||
|
registers a kind→renderer map; unknown kinds fall through to
|
||||||
|
a JSON-dump note row. Bare `http(s)://` URLs in row text are
|
||||||
|
turned into clickable new-tab links by `linkify` (text-node
|
||||||
|
based, no `innerHTML` — XSS-safe); markdown bodies get the
|
||||||
|
same treatment via `marked`'s autolink (npm dep, replacing the
|
||||||
|
vendored UMD bundle), with the rendered `<a>`s rewritten to
|
||||||
|
`target="_blank"`.
|
||||||
|
- `GET /api/state` → JSON snapshot the JS app renders into the
|
||||||
|
DOM. Includes a top-level `seq` (the dashboard event channel's
|
||||||
|
high-water mark at the moment the snapshot was assembled);
|
||||||
|
clients use it to dedupe their buffered SSE traffic against
|
||||||
|
the snapshot (drop frames with `seq <= snapshot.seq`).
|
||||||
|
- `GET /dashboard/stream` (dashboard) / `GET /events/stream`
|
||||||
|
(per-agent) → `text/event-stream` SSE for live updates. The
|
||||||
|
dashboard stream carries broker `Sent` / `Delivered` (mirrored
|
||||||
|
by a forwarder task from the broker's intra-process channel)
|
||||||
|
plus mutation events (`approval_added` / `approval_resolved`,
|
||||||
|
`question_added` / `question_resolved`, `transient_set` /
|
||||||
|
`transient_cleared`). Each frame carries a `seq`. The
|
||||||
|
matching backfill endpoint is `GET /dashboard/history` (last
|
||||||
|
~200 broker messages wrapped in `{ seq, events }`) on the
|
||||||
|
dashboard and `GET /events/history` (last 2000 `LiveEvent`s
|
||||||
|
also wrapped in `{ seq, events }`) on the agent.
|
||||||
|
**SSE multiplexing**: the dashboard uses a
|
||||||
|
`SharedWorker` (`stream-worker.js`) to hold one upstream
|
||||||
|
`EventSource` per URL. All same-origin tabs share this worker
|
||||||
|
— a second dashboard tab joins the existing connection rather
|
||||||
|
than opening a duplicate. The worker fans SSE events out to
|
||||||
|
each subscribed tab via `MessagePort`; on bfcache restore the
|
||||||
|
page re-subscribes (gets a synthetic `open` event immediately
|
||||||
|
if the upstream is already connected). Falls back gracefully
|
||||||
|
when `SharedWorker` is unavailable (e.g. some private-mode
|
||||||
|
browsers). **Worker-death self-heal**: Firefox kills
|
||||||
|
"idle" SharedWorkers under memory pressure with no client-side
|
||||||
|
signal — the port silently goes no-op. The worker now pings
|
||||||
|
every connected port every 30s; the client bumps a
|
||||||
|
last-activity timestamp on every message (incl. pings, which
|
||||||
|
carry no URL — bumped before the URL filter). A
|
||||||
|
visibility-gated watchdog polls every 15s and, if the page is
|
||||||
|
visible AND has active subs AND hasn't heard from the worker
|
||||||
|
in >90s (three missed pings), presumes the worker dead and
|
||||||
|
re-subscribes on a fresh `SharedWorker` port (same code path
|
||||||
|
bfcache-restore uses). Recovery is per-tab; pings are
|
||||||
|
invisible on the healthy path.
|
||||||
|
|
||||||
|
## Shared terminal pane
|
||||||
|
|
||||||
|
Both surfaces' scrollable log streams (`#msgflow` on the dashboard,
|
||||||
|
`#live` on the per-agent page) are backed by the shared terminal
|
||||||
|
factory in `@hive/shared/terminal.js`. The factory wires up
|
||||||
|
sticky-bottom auto-scroll, a "↓ N new" pill, history backfill, and
|
||||||
|
SSE replay. Pages register a `kind → renderer` map; unknown kinds
|
||||||
|
fall through to a JSON-dump note row. The factory ships three row
|
||||||
|
shapes the renderers call:
|
||||||
|
|
||||||
|
- `api.row(cls, text)` — single-line row with an inline `linkify`
|
||||||
|
pass over the text.
|
||||||
|
- `api.details(cls, summary, body)` — collapsible `<details>` with
|
||||||
|
a `<pre>` body (used by long tool-results and stack traces).
|
||||||
|
- `api.detailsDiff(cls, summary, body)` — same shape, splits the
|
||||||
|
body on newlines and tags each line as `diff-add` / `diff-del` /
|
||||||
|
`diff-ctx` so the renderer's diff bodies get coloured without
|
||||||
|
emitting raw HTML.
|
||||||
|
|
||||||
|
**Sticky-bottom + snap animation.** `stickToBottom` is the
|
||||||
|
operator's intent: true means "keep snapping to bottom on every
|
||||||
|
mutation", false means "I scrolled up, leave me alone". The flag
|
||||||
|
flips when a scroll event lands further than `NEAR_BOTTOM_PX = 48`
|
||||||
|
from the bottom. New rows then either snap to bottom (when sticky)
|
||||||
|
or bump the unseen-count and surface the "↓ N new" pill. The snap
|
||||||
|
is a brief 140ms ease-out (`SCROLL_ANIM_MS`) — the browser's
|
||||||
|
default `behavior: 'smooth'` ~500ms reads as "still smooth, but
|
||||||
|
visibly slow"; 140ms feels snap-y while still reading as motion
|
||||||
|
rather than a jump. Distances under `SCROLL_SNAP_PX = 24`
|
||||||
|
short-circuit to instant — animating a 12px nudge would just be
|
||||||
|
jitter. Each new snap cancels the previous `requestAnimationFrame`
|
||||||
|
so a burst of mutations coalesces into one ride to the latest
|
||||||
|
bottom; the per-frame step re-reads `scrollHeight - clientHeight`
|
||||||
|
so mutations landing mid-animation extend the destination smoothly
|
||||||
|
rather than land short.
|
||||||
|
|
||||||
|
**Mid-animation scroll-event guard.** The scroll handler's
|
||||||
|
`isNearBottom` check would flip `stickToBottom` false mid-snap as
|
||||||
|
the smooth animation eases through positions that are technically
|
||||||
|
"not near bottom yet", which would strand the operator partway. A
|
||||||
|
`smoothScrollingUntil` timestamp gates the scroll handler — set to
|
||||||
|
the animation end + ~80ms headroom, re-armed on each fresh snap.
|
||||||
|
Programmatic `scrollTop` writes (the animation's per-frame update)
|
||||||
|
fire scroll events that the gate swallows.
|
||||||
|
|
||||||
|
**Post-append `MutationObserver`.** Renderers commonly call
|
||||||
|
`api.row(cls, text)` to create the row shell then append more
|
||||||
|
children (badges, multi-line bodies, tool result panes) after the
|
||||||
|
factory returned. The initial sticky-snap fires off the row's
|
||||||
|
empty shape; the renderer's later appends grow the row past the
|
||||||
|
visible bottom. A `MutationObserver` on the log subtree fires once
|
||||||
|
per microtask after each batch of synchronous mutations and snaps
|
||||||
|
again when `stickToBottom` is true. Programmatic `scrollTop`
|
||||||
|
writes don't re-trigger the observer (scroll isn't a DOM
|
||||||
|
mutation), so no feedback loop. The pre-append
|
||||||
|
`nearBottomBeforeAppend` snapshot is still useful — it keeps the
|
||||||
|
initial visual lag to one frame instead of one microtask + frame.
|
||||||
|
|
||||||
|
**Backfill + SSE.** Cold load fetches `historyUrl` (replay), then
|
||||||
|
subscribes to `streamUrl` (live tail). Both endpoints return
|
||||||
|
`{ seq, events }` so the client can dedupe — events with
|
||||||
|
`seq <= snapshot.seq` from the SSE stream are dropped silently
|
||||||
|
(the snapshot already covers them). History rows render with a
|
||||||
|
`.no-anim` class so they don't stagger in like live events. The
|
||||||
|
optional `streamFactory(url)` callback lets the dashboard hand
|
||||||
|
the factory a `SharedWorker`-backed `EventSource` facade (so
|
||||||
|
multiple tabs share one upstream connection — see *SSE
|
||||||
|
multiplexing* above); when omitted, the factory falls back to a
|
||||||
|
plain `new EventSource(url)`.
|
||||||
|
|
||||||
|
**`linkify` (text-node based).** Bare `http(s)://` URLs in row
|
||||||
|
text get wrapped in `<a target="_blank" rel="noopener noreferrer">`
|
||||||
|
inside a fresh text node, so the autolinker never touches
|
||||||
|
`innerHTML` and untrusted row content can't smuggle markup. The
|
||||||
|
trailing-punctuation strip keeps `.,;:` outside the link surface.
|
||||||
|
Markdown bodies go through `marked` separately and get the same
|
||||||
|
target rewrite.
|
||||||
|
|
||||||
|
The JS app handles all `form[data-async]` submissions via a delegated
|
||||||
|
listener: read `data-confirm`, swap the button to a spinner, POST
|
||||||
|
`application/x-www-form-urlencoded`, re-enable the button on success
|
||||||
|
(refreshState may keep the form mounted, so we don't rely on a
|
||||||
|
re-render), call `refreshState()`. State shapes live in
|
||||||
|
`dashboard.rs::StateSnapshot` and `web_ui.rs::StateSnapshot` — when
|
||||||
|
adding state fields, plumb through the snapshot struct and the
|
||||||
|
relevant `assets/tabs.js` render function.
|
||||||
|
|
||||||
|
**Focus preservation:** `refreshState` checks whether
|
||||||
|
`document.activeElement` sits inside one of the managed sections
|
||||||
|
and, if so, skips the refresh (defers 2s). The operator never has
|
||||||
|
the form yanked out from under them mid-type; the update lands as
|
||||||
|
soon as they blur.
|
||||||
|
|
||||||
|
**Atomic section repaint:** every managed-section renderer goes
|
||||||
|
through `paintAtomic(liveRoot, build)`: the builder appends into
|
||||||
|
a fresh `DocumentFragment` (off-DOM) and the commit is one
|
||||||
|
`replaceChildren` call. The naive `root.innerHTML = ''; root.append(...)`
|
||||||
|
shape was visibly flashing empty on every poll cycle — on async
|
||||||
|
paths the await yield gave the browser a paint opportunity between
|
||||||
|
the clear and the re-append, and on complex builds (many `el()`
|
||||||
|
allocations) layout could escape the per-task budget even on the
|
||||||
|
synchronous path. The fragment approach keeps the intermediate
|
||||||
|
empty state invisible. Builders receive the fragment as their
|
||||||
|
`root`, so existing renderer code carries over unchanged; early
|
||||||
|
returns inside the builder still commit whatever was appended
|
||||||
|
before they returned.
|
||||||
|
|
||||||
|
**`<details>` open-state preservation:** any collapsible element
|
||||||
|
tagged with `data-restore-key="<stable-key>"` survives the
|
||||||
|
refresh. `snapshotOpenDetails()` walks managed sections before
|
||||||
|
render, `restoreOpenDetails()` re-applies after. Long-content
|
||||||
|
drill-ins (file previews, diffs, journald logs) now open in the
|
||||||
|
**side panel** (see below) rather than expanding inline, so the
|
||||||
|
only restore-keyed `<details>` left is the answered-questions
|
||||||
|
history list.
|
||||||
|
|
||||||
|
**Side panel (dashboard):** long content opens in a drawer that
|
||||||
|
swipes in from the right — a singleton `#side-panel` with a
|
||||||
|
titled header, a close button, and a scrollable body. Closes on
|
||||||
|
the button, a backdrop click, or `Escape`. `Panel.open(title,
|
||||||
|
node)` swaps the body; the JS builders for file previews,
|
||||||
|
approval diffs, and journald logs all render into it. **The
|
||||||
|
drawer width is drag-to-resize**: a thin 6px hit-strip on
|
||||||
|
the left edge captures pointer events, resizes the drawer in
|
||||||
|
real-time (pointer capture keeps dragging even if the cursor
|
||||||
|
outpaces the handle), and persists the chosen width to
|
||||||
|
`localStorage` (key `hyperhive:side-panel-width`) so it
|
||||||
|
survives page reload. Width is clamped to CSS `min-width: 320px`
|
||||||
|
/ `max-width: 96vw`; the viewport-resize handler re-clamps
|
||||||
|
persisted values after a window shrink. File
|
||||||
|
previews are type-aware:
|
||||||
|
|
||||||
|
- **Markdown** (`.md` / `.markdown`) — a `rendered` / `plain`
|
||||||
|
tabbed view: `rendered` (default) is the vendored `marked`
|
||||||
|
bundle (`GET /static/marked.js`), `plain` is the raw source.
|
||||||
|
- **SVG** (`.svg`) — a `rendered` / `source` tabbed view;
|
||||||
|
`rendered` shows the image via an `<img>` `data:` URI (the
|
||||||
|
browser's secure static mode, so an untrusted SVG can't run
|
||||||
|
scripts), `source` shows the raw markup.
|
||||||
|
- **Raster images** (`.png` / `.jpg` / `.gif` / `.webp` /
|
||||||
|
`.bmp` / `.ico` / `.avif`) — render as an `<img>` pointed at
|
||||||
|
`/api/state-file`, which serves them as binary with their
|
||||||
|
real content-type (text files stay UTF-8-lossy `text/plain`).
|
||||||
|
- **Everything else** — raw text in a `<pre>`.
|
||||||
|
|
||||||
|
## Listener bind
|
||||||
|
|
||||||
|
Both bind their TCP listener with `SO_REUSEADDR` via
|
||||||
|
`tokio::net::TcpSocket` plus a retry loop on `AddrInUse`
|
||||||
|
(exponential backoff capped at 2s, **no attempt cap**) so an nspawn
|
||||||
|
restart that races the previous process's socket release resolves
|
||||||
|
itself. The retry is uncapped on purpose: a capped budget once
|
||||||
|
left the harness silently UI-less for the rest of its lifetime
|
||||||
|
when a back-to-back restart held the port longer than the cap
|
||||||
|
allowed. Genuine port collisions are preflighted host-side
|
||||||
|
(`lifecycle::{spawn,rebuild}` refuses with a clear error,
|
||||||
|
surfaced on the dashboard as a banner), so at this layer a
|
||||||
|
persistent `AddrInUse` always reflects a recoverable stale
|
||||||
|
socket — retrying forever is the safe choice. The first 12
|
||||||
|
attempts log at WARN; after that the level drops to INFO so a
|
||||||
|
long-held stale socket doesn't flood the journal.
|
||||||
|
|
||||||
|
The per-agent UI optionally binds a `UnixListener` instead of
|
||||||
|
TCP when `HIVE_WEB_SOCKET` is set — the unix-socket transition
|
||||||
|
mechanics (per-agent `/run/hive-agent/<name>/` bind-mount,
|
||||||
|
`.bound` marker filtering, `agent-sockets.json` consumer on the
|
||||||
|
gateway side) live in [`docs/gateway.md::Per-agent unix-socket
|
||||||
|
upstream`](gateway.md). The env var is opt-in per agent so the
|
||||||
|
two modes coexist while sub-agents transition.
|
||||||
|
|
||||||
|
## Per-agent relative paths
|
||||||
|
|
||||||
|
The per-agent UI uses **document-relative paths everywhere** for
|
||||||
|
assets, API calls, form actions, and the screen WebSocket. Bare
|
||||||
|
references like `static/app.js`, `api/state`, `events/stream`,
|
||||||
|
`screen/ws` resolve against `document.baseURI` — the page's URL
|
||||||
|
without its last path segment.
|
||||||
|
|
||||||
|
That makes the page work under any prefix the agent ends up mounted
|
||||||
|
at without rebuilding the dist. The cases that matter:
|
||||||
|
|
||||||
|
| served at | `api/state` resolves to |
|
||||||
|
|---|---|
|
||||||
|
| `/` (own port, today's shape) | `/api/state` |
|
||||||
|
| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` |
|
||||||
|
| `/agent/iris/stats` (subpage, no trailing slash) | `/agent/iris/api/state` |
|
||||||
|
|
||||||
|
The gateway upstream config strips the prefix before forwarding to
|
||||||
|
the per-agent server, so the agent's Rust routes (`api/state`,
|
||||||
|
`events/stream`, `screen/ws`, `login/start`, …) keep their absolute
|
||||||
|
paths server-side. Only the browser-facing URLs are gated on the
|
||||||
|
mount prefix.
|
||||||
|
|
||||||
|
Subpages (`stats`, `screen`) are served without a trailing slash so
|
||||||
|
the relative-path resolution stays correct: `static/app.js` from
|
||||||
|
`/stats` becomes `/static/app.js` (last segment `stats` gets
|
||||||
|
replaced), not `/stats/static/app.js`. Adding a trailing slash to
|
||||||
|
those routes would break the resolution; either keep them
|
||||||
|
slash-less or use `<base href>` injection at serve time.
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ pub type Flavor = mcp::Flavor;
|
||||||
///
|
///
|
||||||
/// `HIVE_WEB_SOCKET` opt-in selects unix-socket vs TCP binding; the
|
/// `HIVE_WEB_SOCKET` opt-in selects unix-socket vs TCP binding; the
|
||||||
/// dual-mode transition + gateway-side consumer live in
|
/// dual-mode transition + gateway-side consumer live in
|
||||||
/// [`docs/web-ui.md::Listener bind`](../../../docs/web-ui.md) and
|
/// [`docs/web-ui/shape.md::Listener bind`](../../../docs/web-ui/shape.md) and
|
||||||
/// [`docs/gateway.md::Per-agent unix-socket upstream`](../../../docs/gateway.md).
|
/// [`docs/gateway.md::Per-agent unix-socket upstream`](../../../docs/gateway.md).
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -219,7 +219,7 @@ fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> {
|
||||||
/// a long-held stale socket doesn't flood the journal.
|
/// a long-held stale socket doesn't flood the journal.
|
||||||
///
|
///
|
||||||
/// Uncapped retry + dashboard-banner-on-real-collision rationale:
|
/// Uncapped retry + dashboard-banner-on-real-collision rationale:
|
||||||
/// see [`docs/web-ui.md::Listener bind`](../../../docs/web-ui.md).
|
/// see [`docs/web-ui/shape.md::Listener bind`](../../../docs/web-ui/shape.md).
|
||||||
async fn bind_with_retry(addr: SocketAddr, label: &str) -> Result<tokio::net::TcpListener> {
|
async fn bind_with_retry(addr: SocketAddr, label: &str) -> Result<tokio::net::TcpListener> {
|
||||||
let mut delay_ms = 250u64;
|
let mut delay_ms = 250u64;
|
||||||
let mut attempts = 0u32;
|
let mut attempts = 0u32;
|
||||||
|
|
@ -433,7 +433,7 @@ struct StateSnapshot {
|
||||||
/// dashboard card's icon-only nav strip via hive-c0re's
|
/// dashboard card's icon-only nav strip via hive-c0re's
|
||||||
/// `GET /api/agent/{name}/links` same-origin passthrough proxy
|
/// `GET /api/agent/{name}/links` same-origin passthrough proxy
|
||||||
/// — single source of truth, no CORS. Per-agent page also reads
|
/// — single source of truth, no CORS. Per-agent page also reads
|
||||||
/// this directly. See [`docs/web-ui.md::Container row`] for the
|
/// this directly. See [`docs/web-ui/dashboard.md::Container row`] for the
|
||||||
/// frontend resolver + which links appear in which conditions.
|
/// frontend resolver + which links appear in which conditions.
|
||||||
links: Vec<AgentLink>,
|
links: Vec<AgentLink>,
|
||||||
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
|
/// Human name of this hive instance (e.g. `"pr1ma"`). Sourced
|
||||||
|
|
@ -595,7 +595,7 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
||||||
/// for `External`; the frontend resolves each against its `kind`
|
/// for `External`; the frontend resolves each against its `kind`
|
||||||
/// against the right base so the backend never has to guess the
|
/// against the right base so the backend never has to guess the
|
||||||
/// operator's browser host. See
|
/// operator's browser host. See
|
||||||
/// [`docs/web-ui.md::Container row`](../../../docs/web-ui.md) for
|
/// [`docs/web-ui/dashboard.md::Container row`](../../../docs/web-ui/dashboard.md) for
|
||||||
/// the resolver + how `deployed:<sha>` ships alongside.
|
/// the resolver + how `deployed:<sha>` ships alongside.
|
||||||
fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
||||||
let mut links = Vec::new();
|
let mut links = Vec::new();
|
||||||
|
|
@ -950,7 +950,7 @@ async fn post_new_session(State(state): State<AppState>) -> Response {
|
||||||
/// only these (and not the rest of `~/.claude/`) preserves session
|
/// only these (and not the rest of `~/.claude/`) preserves session
|
||||||
/// history so `claude --continue` keeps working after a fresh login.
|
/// history so `claude --continue` keeps working after a fresh login.
|
||||||
/// Rationale + the previous wholesale-wipe shape we replaced live in
|
/// Rationale + the previous wholesale-wipe shape we replaced live in
|
||||||
/// [`docs/web-ui.md::Per-agent endpoints`](../../../docs/web-ui.md)
|
/// [`docs/web-ui/agent.md::Per-agent endpoints`](../../../docs/web-ui/agent.md)
|
||||||
/// (the `/api/logout` bullet).
|
/// (the `/api/logout` bullet).
|
||||||
const CRED_FILE_NAMES: &[&str] = &[".credentials.json", "mcp-needs-auth-cache.json"];
|
const CRED_FILE_NAMES: &[&str] = &[".credentials.json", "mcp-needs-auth-cache.json"];
|
||||||
|
|
||||||
|
|
@ -959,7 +959,7 @@ const CRED_FILE_NAMES: &[&str] = &[".credentials.json", "mcp-needs-auth-cache.js
|
||||||
/// turn loop's next iteration parks into `wait_for_login` which
|
/// turn loop's next iteration parks into `wait_for_login` which
|
||||||
/// resumes when a fresh credentials file appears via `/login/code`.
|
/// resumes when a fresh credentials file appears via `/login/code`.
|
||||||
/// Always returns 200 with a body describing what happened. See
|
/// Always returns 200 with a body describing what happened. See
|
||||||
/// [`docs/web-ui.md::Per-agent endpoints`](../../../docs/web-ui.md)
|
/// [`docs/web-ui/agent.md::Per-agent endpoints`](../../../docs/web-ui/agent.md)
|
||||||
/// (the `/api/logout` bullet) for the three-step rationale +
|
/// (the `/api/logout` bullet) for the three-step rationale +
|
||||||
/// preservation invariants.
|
/// preservation invariants.
|
||||||
async fn post_logout(State(state): State<AppState>) -> Response {
|
async fn post_logout(State(state): State<AppState>) -> Response {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
# Port allocation, weston bind-address quirk, PAM service name, the
|
# Port allocation, weston bind-address quirk, PAM service name, the
|
||||||
# Type=simple choice, idle-time=0: all in
|
# Type=simple choice, idle-time=0: all in
|
||||||
# docs/gotchas.md::Weston VNC compositor.
|
# docs/gotchas.md::Weston VNC compositor.
|
||||||
# Harness-side WebSocket relay shape: docs/web-ui.md::Per-agent
|
# Harness-side WebSocket relay shape: docs/web-ui/agent.md::Per-agent
|
||||||
# endpoints (`/screen` + `/screen/ws`).
|
# endpoints (`/screen` + `/screen/ws`).
|
||||||
|
|
||||||
options.hyperhive.gui.enable = lib.mkOption {
|
options.hyperhive.gui.enable = lib.mkOption {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue