docs: sort the top-level directories into five sections by what the reader wants
Every docs directory moves under 1-getting-started, 2-handbook, 4-implementation or 5-project; the options reference (section 3) comes from the website and has no directory here. trust-boundary stays whole under the handbook for now. web-ui, swarm and tools split between handbook and implementation, and the command-line references move to 2-handbook/reference. Links, path references in code and nix, and the docs index follow the moves; no page content changes.
This commit is contained in:
parent
53ccbcb4b3
commit
6739bb903d
3 changed files with 736 additions and 701 deletions
115
docs/2-handbook/web-ui/README.md
Normal file
115
docs/2-handbook/web-ui/README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Dashboard & Web UI
|
||||
|
||||
The per-hive dashboard: container state and the rebuild
|
||||
queue for one hive. For swarm-wide day to day, see
|
||||
[the swarm UI](../swarm/ui.md) instead. If you want implementation
|
||||
detail — wire formats, DOM structure, event plumbing — see [Dashboard
|
||||
layout](dashboard.md), [Per-agent page](agent.md), [Shape](../../4-implementation/web-ui/shape.md),
|
||||
and [CSS theme variables](../../4-implementation/web-ui/css-vars.md) below; this page only covers
|
||||
what you actually do here.
|
||||
|
||||
## Where things are
|
||||
|
||||
Everything starts at the **dashboard**, served at `/`. A pages menu
|
||||
(the link icon beside the profile avatar, at the right of the tab strip) links to the other
|
||||
surfaces — the API docs and the swarm UI.
|
||||
|
||||
The dashboard is where you'll spend most of your time. It's a single
|
||||
page with four tabs:
|
||||
|
||||
- **4G3NTS** — every agent, live. This is the default tab and the one
|
||||
you'll check most.
|
||||
- **R3BU1LD QU3U3** — the rebuild queue. A build's output and history are on the
|
||||
swarm's `builds` Grafana board.
|
||||
- **K3PT ST4T3** — kept state from agents with no container
|
||||
(tombstones), with a purge button per entry.
|
||||
|
||||
Each agent also has its own page — a full terminal view of that
|
||||
agent's session, reachable by selecting its name anywhere in the
|
||||
dashboard, or directly at `/agent/<name>/` (or `http://<host>:<port>/`
|
||||
if the gateway isn't in front).
|
||||
|
||||
## The things you'll actually do
|
||||
|
||||
**Check on an agent.** 4G3NTS shows every container as a row: name,
|
||||
whether it's running, what it's currently doing (a live status pill —
|
||||
`rebuilding…`, `starting…`, or similar — while something's in flight),
|
||||
and quick links (stats, screen, forge profile). Select the name to open
|
||||
its terminal and watch it work in real time.
|
||||
|
||||
**Review a config change.** Agent config changes (new packages, env
|
||||
vars, MCP servers) are pull requests on the agent's `agent-configs`
|
||||
repo. Review and merge them on the forge; the merge deploys the change.
|
||||
See [`config-changes.md`](../agent-lifecycle/config-changes.md#config-changes).
|
||||
|
||||
**Start, stop, restart, or rebuild an agent.** Select one or more
|
||||
agents on 4G3NTS (select the icon) and use the selection bar, or use the
|
||||
per-agent `⋮` menu on a single row. Rebuilding downloads and applies the closure the swarm
|
||||
declares for the agent again. The hive applies a changed declaration itself,
|
||||
so use it to retry one that failed.
|
||||
|
||||
**Watch a build.** The dashboard's R3BU1LD QU3U3 tab shows the rebuild queue live, plus a
|
||||
streaming log of whatever's currently building. Useful right after
|
||||
merging a config change or bumping a flake input.
|
||||
|
||||
**Grant or revoke a permission.** Not on the hive dashboard: an agent's
|
||||
permissions live in the swarm. Set them with `swarmctl agent permissions`,
|
||||
or on the swarm UI's create-agent screen when creating the agent; see
|
||||
[`process/conventions.md`](../../5-project/process/conventions.md#permissions).
|
||||
|
||||
**Read an agent's logs.** Logs aren't in the hive dashboard. Every
|
||||
hive ships its agents', daemons' and infra containers' journals to the
|
||||
swarm's VictoriaLogs; read them in Grafana's **Explore** — see
|
||||
[`swarm/services.md`](../swarm/services.md#logs-victorialogs).
|
||||
|
||||
**Set up a schedule.** Scheduled prompts are swarm-level: set them with
|
||||
`swarmctl schedule` ([`swarm/schedules.md`](../swarm/schedules.md)) or
|
||||
in the schedules card on the swarm UI's messages tab ([`swarm/ui.md`](../swarm/ui.md)).
|
||||
|
||||
## More depth
|
||||
|
||||
Both the dashboard and the per-agent pages are SPAs sharing one
|
||||
skeleton: `GET /` returns a static shell, `/api/state` returns JSON,
|
||||
JS renders — no full-page reloads.
|
||||
|
||||
- **[Dashboard layout](dashboard.md)** — every tab and standalone page,
|
||||
in full implementation detail: endpoint shapes, event wiring, exact
|
||||
badge-derivation rules.
|
||||
- **[Per-agent page](agent.md)** — the per-agent terminal,
|
||||
side panel, actions, and per-agent endpoints.
|
||||
- **[Shape (shared by both)](../../4-implementation/web-ui/shape.md)** — the SPA skeleton, SSE
|
||||
multiplexing, terminal pane, listener bind, per-agent relative
|
||||
paths, `data-async` form pattern, atomic repaint.
|
||||
- **[CSS theme variables](../../4-implementation/web-ui/css-vars.md)** — the Catppuccin Mocha custom
|
||||
properties declared once in `base.css` and the rule that per-page
|
||||
stylesheets reference (never redeclare) them.
|
||||
- **[swarm-ui design guide](../../5-project/web-ui/design-guide.md)** — visual language,
|
||||
motion, theming, error-UX, and component-first principles for the
|
||||
swarm-level Preact app specifically (not this page's dashboard/agent
|
||||
UIs). `/components` on a running swarm-ui is the companion living
|
||||
demo of every building block it references.
|
||||
|
||||
### Implementation reading paths
|
||||
|
||||
Task-oriented jumps straight to the relevant section, for when you're
|
||||
touching the code rather than using the UI:
|
||||
|
||||
- **"How does the dashboard SPA stay live without polling?"** →
|
||||
[`shape.md`](../../4-implementation/web-ui/shape.md) (SSE multiplexing, Worker-death self-heal,
|
||||
atomic repaint).
|
||||
- **"What does a container row contain?"** →
|
||||
[`dashboard.md`](dashboard.md) (Container row, Selection bar).
|
||||
- **"What endpoints does the dashboard expose?"** →
|
||||
[`dashboard.md`](dashboard.md) (Dashboard endpoints, Dashboard event
|
||||
channel).
|
||||
- **"How does the per-agent terminal render tool calls?"** →
|
||||
[`terminal-rendering.md`](../../4-implementation/web-ui/terminal-rendering.md) (full row taxonomy
|
||||
and dispatch walkthrough); for a high-level summary see
|
||||
[`agent.md`](agent.md) (Per-stream rendering).
|
||||
- **"What actions does the agent page offer?"** →
|
||||
[`agent.md`](agent.md) (Actions).
|
||||
- **"What are the per-agent HTTP endpoints?"** →
|
||||
[`agent.md`](agent.md) (Per-agent endpoints).
|
||||
- **"Which CSS variable to use / where are colours defined?"** →
|
||||
[`css-vars.md`](../../4-implementation/web-ui/css-vars.md) (Palette, single-source `base.css`
|
||||
rule).
|
||||
425
docs/2-handbook/web-ui/agent.md
Normal file
425
docs/2-handbook/web-ui/agent.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# Per-agent page
|
||||
|
||||
> Part of [Web UI](README.md). See also:
|
||||
> [Shape (shared)](../../4-implementation/web-ui/shape.md) · [Dashboard layout](dashboard.md)
|
||||
|
||||
|
||||
Two fixed-position layers frame a full-viewport terminal: a header and
|
||||
scrollable main content — plus a slide-in side panel for flyouts and long
|
||||
content. The page has no message box: to write to an agent, use the swarm
|
||||
UI's terminal.
|
||||
|
||||
## Header
|
||||
|
||||
Preact component tree (`Header.tsx` + `StatusChips.tsx` +
|
||||
`MetaNav.tsx` + `HeaderPill.tsx`, wired together in `Root.tsx`) — see
|
||||
`frontend/packages/agent/src/components/`. This section describes the
|
||||
rendered component tree: every element is Preact component output.
|
||||
|
||||
**Fixed-overlay header** (`<header class="agent-header">`): frosted
|
||||
glass — `backdrop-filter: blur` lets scrolled terminal rows show
|
||||
through. Measures its own rendered height via `ResizeObserver`
|
||||
(`Header.tsx`) and writes it to a CSS custom property the content
|
||||
below reads for its offset, so content never overlaps the header
|
||||
regardless of how many rows of badges/pills wrap onto an extra line
|
||||
at a given viewport width. Two columns:
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
- **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. Falls back to the dimmed hyperhive mark
|
||||
(`/favicon.svg`) on load error (`/icon` 404s when the agent has no
|
||||
`services.hyperhive.agent.icon` override — there is no server-side default image).
|
||||
- **Main column** (`.agent-header-main`, `Header.tsx`): two rows —
|
||||
title (`◆ <label> ◆`) and, when set, the "swarm / hive" identity
|
||||
line. Nothing else lives here; both rows are short, fixed-shape text
|
||||
that doesn't wrap in practice, so this column's height stays stable
|
||||
regardless of how many badges or pills are showing (a design
|
||||
correction mid-review — the first pass put the variable-width
|
||||
content here instead, which is exactly the kind of thing the
|
||||
`ResizeObserver` measurement above exists to catch even when a
|
||||
layout choice reintroduces it).
|
||||
- **Pills cluster** (`.agent-header-pills`, right-aligned): status
|
||||
badges (`StatusChips.tsx`) and flyout triggers together in one row,
|
||||
so the header reads as one identity zone + one status/actions zone:
|
||||
- **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) /
|
||||
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
||||
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
||||
`○ offline` / `… booting` + age suffix.
|
||||
- **Model and effort badges** (`model · <name>`, `effort · <level>`):
|
||||
display only. Each tooltip says where the value comes from
|
||||
(`state.model_source` / `state.effort_source`: set on the swarm,
|
||||
agent.nix, an existing `hyperhive-model` / `hyperhive-effort` file, or
|
||||
the built-in default); the model's also says what the last turn
|
||||
resolved it to. Both are set on the swarm: the swarm UI's agents page,
|
||||
or `swarmctl agent inference set`
|
||||
([`inference.md`](../../4-implementation/swarm/inference.md)).
|
||||
- On an ACP agent each badge shows the session's own value when it
|
||||
doesn't offer the requested one. The effort badge is hidden
|
||||
(`state.effort` is `null`) until the first turn has attached a
|
||||
session, when the model offers no effort levels, and while a newly
|
||||
set model waits for the next turn, since the levels come with the
|
||||
model.
|
||||
- **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size,
|
||||
tooltip shows % of context window) / `cost · 1.3M` (cumulative
|
||||
tokens billed across every inference in the last turn).
|
||||
- **Pause badge** (`⏸ pause` / `▶ resume`): toggles via the same
|
||||
hive-c0re-owned `/api/pause/<name>` / `/api/resume/<name>`
|
||||
endpoints the dashboard uses (pausing needs a write this
|
||||
unprivileged process can't make directly).
|
||||
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
||||
when empty; selecting it opens the matching flyout in the side panel.
|
||||
- **Profile avatar**: the signed-in user's initial, opening a popover with
|
||||
their name and links to authelia's settings and sign-out pages, as on the
|
||||
dashboard (`AgentUserMenu.tsx`). It shows only behind the gateway, where
|
||||
the page shares the dashboard's origin and session; opened on the agent's
|
||||
own port there is no sign-in and no avatar. The name comes from the
|
||||
dashboard's `/api/whoami`, authelia's URL from the dashboard's
|
||||
`/api/state`.
|
||||
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
|
||||
(back-link to the host dashboard, `${dashboardBase}`)
|
||||
followed by this agent's `agent_links()`-sourced navigation (stats,
|
||||
screen when VNC is enabled, forge profile + config-repo mirror when
|
||||
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`
|
||||
extras) — all as real `<a>` elements, not a `Dropdown`-style
|
||||
command list, so ctrl/middle-click and "copy link address" keep
|
||||
working. Each `AgentLink.kind` resolves differently: `container` →
|
||||
same-origin path; `forge` → `state.forge_public_url + url`, and
|
||||
hive-agent drops the link entirely when that's unset (never guessed from
|
||||
`<host>:3000`); `external` → already absolute. Same source
|
||||
(`GET /api/state`'s `links` field) also feeds
|
||||
`DashboardState.links` for the dashboard card's icon strip —
|
||||
`agent_links()` in hive-agent is the single source of truth for
|
||||
both. The dashboard link lives in this 🔗 popover. `cancel turn`,
|
||||
`new session` and `log out` are rows of the state badge's menu, each
|
||||
with a click-again confirm (see Actions below).
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
Values throughout come from `GET /api/state`'s cold-load snapshot,
|
||||
kept in sync afterwards by the SSE stream (see Live view below) —
|
||||
`context_window_tokens` for the ctx badge tooltip, `qualified_label`
|
||||
(the hive-qualified `name@domain` form, used for the browser tab title
|
||||
so two tabs from different hives are distinguishable; the header's own
|
||||
`◆ <label> ◆` stays short).
|
||||
|
||||
## Main content
|
||||
|
||||
**Main content** (`<main class="agent-main">`): fills the viewport
|
||||
and scrolls behind the fixed header + footer.
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
- `#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
|
||||
browsers honour `one-time-code` and suppress the "save password
|
||||
for this site?" prompt that would otherwise fire on submit.
|
||||
- Terminal-wrap: live event tail (sticky-bottom autoscroll +
|
||||
`↓ 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 confine its `z-index` below the
|
||||
fixed header 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 header. Geometry is unchanged —
|
||||
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
||||
area.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
## Side panel
|
||||
|
||||
**Side panel** (slide-in from right): singleton shared with the
|
||||
dashboard's side panel shape. Carries inbox and todos flyouts (opened
|
||||
via the header pills) as well as long content (file previews, diffs,
|
||||
journald logs). Inbox flyout: unread messages addressed to this agent
|
||||
(`acked_at IS NULL`, newest-first, up to 30); reply messages indented
|
||||
with `↳ reply ·` in amber. A `✓ mark all read` button appears in the
|
||||
flyout header when the inbox is non-empty; selecting it confirms then
|
||||
POSTs cross-origin to the core dashboard's `POST
|
||||
/api/agent/{name}/mark-all-read`, which acks all pending messages for
|
||||
this agent so the harness won't receive wake-prompts for them. A `{
|
||||
marked: N }` pill surfaces the count. After the drain the inbox list
|
||||
empties on reload (the filter is `acked_at IS NULL`, so drained
|
||||
messages disappear). Todos flyout ("loose-ends v2"): the harness-local
|
||||
todos other subsystems push at this agent (`GET /api/todos`) — matrix,
|
||||
forge, and bash are the built-in producers, but any user-configured MCP
|
||||
server can push its own via the same in-agent socket. Each row shows the
|
||||
producing subsystem, an
|
||||
optional source label, a summary, and age. A checkbox per row plus a
|
||||
select-all / select-none / `✓ mark done` bulk row above the list POSTs
|
||||
the checked ids to `POST /api/todos/mark-done`, which dismisses them
|
||||
from the harness-local store (same effect as `cancel_loose_end(kind:
|
||||
"todo")`, just from the web UI instead of the agent's own tool calls).
|
||||
If the todo store errors or doesn't answer for any id, the endpoint returns
|
||||
500 naming how many ids failed, and the panel shows `failed: …` instead of
|
||||
`✓ marked done`.
|
||||
|
||||
## 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 }`. Each event also carries a `ts` (unix
|
||||
seconds) — a flattened sibling of the event tag on both the live
|
||||
SSE frame and the replayed history rows. The web UI:
|
||||
|
||||
- fetches `GET /events/history` on page load and replays the last
|
||||
2000 events (oldest first);
|
||||
- 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;
|
||||
- terminal-themed: phosphor mauve glow, Crust bg,
|
||||
backdrop-filter blur, row fade-in slide-up.
|
||||
|
||||
The backfill/live-tail dedupe, sticky-bottom autoscroll, and "↓ N
|
||||
new" pill are the shared terminal-pane mechanics described in
|
||||
[Shape](../../4-implementation/web-ui/shape.md#shared-terminal-pane) — this page's log is one
|
||||
instance of that same factory.
|
||||
|
||||
Per-stream rendering (see [`docs/4-implementation/web-ui/terminal-rendering.md`](../../4-implementation/web-ui/terminal-rendering.md) for
|
||||
the full row taxonomy and dispatch logic):
|
||||
|
||||
- `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 with
|
||||
per-tool salient-arg extraction (`fmtToolUse`).
|
||||
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
|
||||
`<details>` `▸ ← Nl · headline` (select to expand full body).
|
||||
- `Stream` `thinking` → `.thinking` row with a `💭 thinking …`
|
||||
indicator.
|
||||
- `Stream` `system` → handled by subtype: `plugin_install` and
|
||||
`compact_boundary` emit muted notes; `commands_changed` emits an
|
||||
expandable details row listing slash commands; `thinking_tokens`
|
||||
updates a single in-place `🧠` counter; `init`, `result`, and
|
||||
`rate_limit_event` drop silently (noise / used elsewhere); other
|
||||
subtypes → muted `⚙ <subtype>` note.
|
||||
- `Note` → `· text`.
|
||||
- `TurnStart` → `◆ TURN ← <from>` with the wake-prompt body; a
|
||||
muted `· HH:MM:SS` time suffix from the event `ts`.
|
||||
- `TurnEnd` → `✓ turn ok` / `✗ turn fail — note`, with a
|
||||
`· HH:MM:SS · <duration>` suffix (duration = end − the paired
|
||||
turn-start), triggers a `refreshState()`. The time suffix is
|
||||
guarded on a numeric `ts`, so rows degrade cleanly when absent.
|
||||
|
||||
## Actions
|
||||
|
||||
The state badge's menu carries the actions on the agent's claude:
|
||||
|
||||
- `cancel turn` — `POST /api/cancel` → SIGINT to the turn's claude and every
|
||||
command it started (its process group), emits a Note. On an ACP agent
|
||||
it sends the agent `session/cancel` instead, and SIGINT if the turn
|
||||
hasn't ended 10s later. Offered while the state is `thinking`.
|
||||
- `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.
|
||||
- `log out` — `POST /api/logout` (confirms first). Wipes OAuth
|
||||
credential files, parks the agent in `needs_login`, and preserves
|
||||
session history (`~/.claude/projects/`).
|
||||
|
||||
`POST /api/compact` sets the deferred `Bus::request_compact()` flag and
|
||||
returns immediately. The harness runs the `/compact` at the next turn
|
||||
boundary (end of the in-flight turn, or `turn::run_pending_compact` when
|
||||
idle); output streams into the live panel, which ends with a `/compact done`
|
||||
or `/compact failed: …` note. It works mid-turn, not only when idle. Nothing
|
||||
on this page calls it now.
|
||||
|
||||
## Per-agent endpoints
|
||||
|
||||
Successful POSTs return 200 (no 303 redirects). Error responses
|
||||
use semantic status codes: **400** for missing/malformed input
|
||||
(`body` required, unknown model name, unrecognised effort level),
|
||||
**409** for retryable state conflicts (turn in flight when
|
||||
`/compact` fires), **500** only for genuine server/transport
|
||||
failures. 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 /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 and the
|
||||
commands it started. 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/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.
|
||||
⚠️ Deleting the whole `~/.claude/` directory instead breaks
|
||||
session continuity — logout must stay narrowed to just the
|
||||
credential files.
|
||||
3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note`
|
||||
naming the files it just deleted, 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 — it
|
||||
folds per-file errors 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) count as
|
||||
idempotent.
|
||||
- `GET /api/state` — cold-load snapshot (`StateSnapshot`) consumed by
|
||||
the frontend (`useAgentState.ts`) on page load and, on a poll, while
|
||||
`status === 'needs_login_in_progress'`. Includes `turn_state`,
|
||||
`context_window_tokens`, `qualified_label`,
|
||||
`model`, `model_source`, `effort`, `effort_source`, `links`, and
|
||||
other fields described inline throughout this document. All
|
||||
subsequent state updates arrive via SSE.
|
||||
- `GET /api/dashboard-state` — lean snapshot of agent-owned fields
|
||||
fetched once per running agent by the dashboard's container row to
|
||||
get fresh values without relying on hive-c0re's periodic file-reads.
|
||||
Returns `{ status_text?, status_set_at?, ctx_tokens?, context_window_tokens,
|
||||
rate_limited, links }`. Only called when the container is running;
|
||||
skipped (muted badges) when stopped. Also accessible via the gateway
|
||||
at `/agent/<name>/api/dashboard-state`.
|
||||
- `GET /api/todos` — harness-local todos snapshot (loose-ends v2)
|
||||
consumed by the todos flyout (`refreshTodos` / `buildTodosList`).
|
||||
See the todos-flyout paragraph above for the producer/subsystem
|
||||
model and the bulk mark-done row.
|
||||
- `GET /api/stats?window=24h|7d|30d|all` — time-bucketed turn analytics
|
||||
`Snapshot` consumed by the `/stats` page. `all` ranges from the
|
||||
earliest recorded turn with an adaptive bucket width.
|
||||
- `GET /icon` — agent's icon as `image/svg+xml`. Returns
|
||||
`/etc/hyperhive/icon.svg` (set via `services.hyperhive.agent.icon` in `agent.nix`)
|
||||
when present, otherwise **404** — there is no server-side default.
|
||||
Consumers (dashboard container row, this page's own header icon)
|
||||
hit `/icon` optimistically and fall back client-side on load failure
|
||||
to the frontend-bundled `/favicon.svg` rather than probing first.
|
||||
- `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 `services.hyperhive.agent.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. A `← agent` back-link in the page
|
||||
header returns to the main per-agent page. Controls: `⤢ 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. `sendPointer` rescales pointer coordinates so presses
|
||||
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 the
|
||||
`HIVE_GUI_VNC_PORT` env var (a fixed port set on the harness
|
||||
service when `services.hyperhive.agent.gui.enable`; see `weston-vnc.nix`).
|
||||
- `GET|POST /extra/<name>/…` — **extra web proxies** declared via
|
||||
`services.hyperhive.agent.extraWebProxies` in `agent.nix` (serialised to the
|
||||
`HIVE_EXTRA_WEB_PROXIES` env var as a JSON object
|
||||
`{"<name>": "<upstream_url>"}`). Each entry mounts a transparent
|
||||
reverse-proxy at `/extra/<name>/` that forwards every request (method,
|
||||
headers, body) to the configured upstream, strips hop-by-hop headers
|
||||
on both sides (repeated response headers such as two `Set-Cookie` lines
|
||||
are all forwarded), and buffers the full response body (MVP — SSE
|
||||
connections will appear as one large response rather than streaming).
|
||||
The `/extra/` namespace ensures user-declared proxies can never
|
||||
conflict with native agent endpoints. Upstream values are either an
|
||||
`http(s)://` URL (forwarded via `reqwest`) or a Unix domain socket,
|
||||
spelled `unix:<path>` (for example `unix:/run/myapp/http.sock`) — dialed
|
||||
directly with a raw HTTP/1.1 client per request, since `reqwest` has
|
||||
no UDS transport. Implemented in `web_ui/proxy.rs`.
|
||||
|
||||
Bus events (new vocabulary on `/events/stream`):
|
||||
|
||||
- `status_changed { status }` — `online` / `rate_limited` /
|
||||
`needs_login_idle` / `needs_login_in_progress`. Drives the
|
||||
alive-badge. The harness sets `rate_limited` when it detects a
|
||||
429 response and clears it when the retry sleep expires.
|
||||
- `model_changed { model }` — drives the model chip. The harness emits it
|
||||
when it applies or clears a swarm value.
|
||||
- `effort_changed { effort }` — drives the effort chip. The harness
|
||||
emits it when it applies or clears a swarm value.
|
||||
- `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). A `← live` back-link in the page
|
||||
header returns to the main per-agent page. Turn analytics, read-only, from
|
||||
`/state/hyperhive-turn-stats.sqlite`. `GET /api/stats?window=
|
||||
24h|7d|30d|all` returns a time-bucketed `Snapshot`; the page renders
|
||||
it with Chart.js (bundled into `stats.js` via esbuild — no CDN
|
||||
dependency). 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), doughnuts for tool / wake-source / result mix, and a
|
||||
**result-trend** stacked bar — per-bucket `result_counts` so
|
||||
error / rate-limit / compaction outcomes are visible over time
|
||||
(the doughnut shows only the window total).
|
||||
A **favorite tools** doughnut shows the most-run shell commands —
|
||||
normalised `bash_commands` heads written per bash task by the
|
||||
hive-bash-daemon capture: the basename of the *first real command*,
|
||||
looking past `cd repo &&` prefixes, env-assignments, and
|
||||
prefix-runners like `sudo` / `env` (so `cd /repo && cargo build`
|
||||
records `cargo`, not `cd`). Read via `bash_breakdown`; the card
|
||||
stays hidden until the agent has run a bash command (a missing
|
||||
`bash_commands` table degrades to an empty list), so it never
|
||||
renders an empty chart.
|
||||
A summary chip row carries window totals, plus two
|
||||
token-efficiency chips derived from the bucket sums: **cache
|
||||
hit-rate** (`cache_read` over all input-side tokens) and
|
||||
**tokens/turn**. When `reminder_stats` is present (fetched via
|
||||
`ReminderRollup` RPC and merged into the snapshot in
|
||||
`web_ui/stats.rs::api_stats`) three more chips appear: **reminders
|
||||
scheduled / delivered / pending** for the window. When the
|
||||
per-session capture has data, a **first-turn ctx** chip shows the
|
||||
input tokens of the most recent fresh claude session's first turn —
|
||||
a proxy for system-prompt + AGENTS.md sprawl (a fresh session's first
|
||||
turn pays the full static prefix uncached). It's derived in `stats.rs`
|
||||
(`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of
|
||||
the latest `sessions` row in the window) and stays out of the JSON
|
||||
until the `sessions` / `turn_stats.session_id` capture has rows — so
|
||||
the chip stays hidden on older dbs (inert-until-capture).
|
||||
`stats.rs` opens the sqlite db read-only and degrades to an
|
||||
empty snapshot on any error — the page is decorative, never
|
||||
authoritative.
|
||||
528
docs/2-handbook/web-ui/dashboard.md
Normal file
528
docs/2-handbook/web-ui/dashboard.md
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
# Dashboard layout
|
||||
|
||||
> Part of [Web UI](README.md). See also:
|
||||
> [Shape (shared)](../../4-implementation/web-ui/shape.md) · [Per-agent page](agent.md)
|
||||
|
||||
This page covers the per-hive UI that hive-c0re serves. Swarm-wide
|
||||
surfaces — the agent roster, agent creation, linking external forge and
|
||||
matrix accounts, the controller's job graph — are on the
|
||||
[swarm UI](../swarm/ui.md).
|
||||
|
||||
hive-c0re serves the dashboard at `/`.
|
||||
It has a fixed chrome header at the top and a `<main>` that shows exactly
|
||||
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
|
||||
`#kept`) drives which pane is
|
||||
active; hash changes don't reload the page.
|
||||
|
||||
**Chrome header** (fixed, overlays the active tab pane):
|
||||
- **Pages menu**: a link icon at the right end of the chrome, beside the
|
||||
profile avatar (`links-menu.js`, `@hive/shared`'s `LinksMenu`, the same
|
||||
component the swarm UI uses for its service links), opening a popover with links to **api docs**
|
||||
(`/api/docs/`) and **swarm** (the swarm
|
||||
UI at the swarm apex domain, opened in a new tab). The swarm entry
|
||||
comes from `state.swarm_public_url`, which hive-c0re reads from
|
||||
`HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`);
|
||||
with no domain set there is no swarm entry. The standalone pages link back to `/`.
|
||||
- **Profile menu**: the signed-in user's avatar at the right end of the
|
||||
chrome (`user-menu.js`, the same component as the swarm UI's and the agent
|
||||
page's). It opens a popover with the user's name and links to authelia's
|
||||
settings and sign-out pages. The name comes from `/api/whoami`, which the
|
||||
dashboard's nginx vhost proxies to authelia, and the links from
|
||||
`state.authelia_public_url`, which hive-c0re reads from
|
||||
`HIVE_AUTHELIA_PUBLIC_URL` (set from `services.hyperhive.swarm.authelia.url`).
|
||||
If the name doesn't load the avatar shows a question mark, and without the URL the
|
||||
popover has no links.
|
||||
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`,
|
||||
`◆ K3PT ST4T3 ◆`.
|
||||
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
|
||||
— sits below the tab strip.
|
||||
- **No warning banner.** No page shows hive-level warnings (host disk
|
||||
pressure, failing background sweeps, forge boot failures, agents that
|
||||
need a claude login). They live in a push-based registry
|
||||
(`hive-c0re`'s `warnings.rs`): any subsystem raises or clears its own
|
||||
entry through an RAII guard, and each raise logs one
|
||||
`hive warning raised` line. The operator sees the registry as metrics and
|
||||
Grafana alerts: see
|
||||
[Hive-scoped metrics](../../4-implementation/scheduler/observability.md#hive-scoped-metrics-hive-c0re).
|
||||
A container crash is a swarm-level Grafana alert on the crash watcher's
|
||||
own log line, so a hive with no otel export has no operator-visible
|
||||
warning signal beyond `/health/ready`.
|
||||
- **Browser tab title** — `hyperhive // h1ve-c0re` by default; updated
|
||||
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
|
||||
arrive in the state snapshot.
|
||||
|
||||
## 4G3NTS tab
|
||||
|
||||
**C0NTAINERS** — live containers as a flat list, one row per
|
||||
container, sorted alphabetically by agent name; every container the
|
||||
rescan reports gets a row. 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.
|
||||
|
||||
## K3PT ST4T3 tab
|
||||
|
||||
Passive / rare-interaction state. `kept.js` renders it from the
|
||||
`/api/state` snapshot and keeps it live from the `tombstones_changed`
|
||||
stream event. (The rebuild queue and build logs are dashboard tabs too —
|
||||
see below.)
|
||||
|
||||
Per-container CPU, memory and disk load isn't shown in the hive UI. The
|
||||
same figures go out as OTEL metrics and appear in Grafana (see
|
||||
[observability](../../4-implementation/scheduler/observability.md)). An agent's CPU and memory
|
||||
caps live per agent in the swarm UI.
|
||||
|
||||
**K3PT ST4T3** — agents with kept state and **no container** (size + age +
|
||||
claude-creds badge). One action: `PURG3` (wipes the state
|
||||
dir; `POST /api/purge-tombstone/{name}`).
|
||||
|
||||
⚠️ **Not only *destroyed* agents.** Nothing records a destroy: `Provision`
|
||||
writes every definition-side artifact (state subvolume,
|
||||
meta registration, topology entry) *before* the container exists,
|
||||
and that artifact set survives `lifecycle::destroy`. An agent
|
||||
part-way through a spawn is byte-identical on disk to a tombstone, and
|
||||
the pane lists both. The pane carries a standing warning to that effect, and
|
||||
the row badge says `offline` rather than `destroyed` — the absence of a
|
||||
container is all hive-c0re can actually prove. Fixing it properly
|
||||
needs a recorded destroy — **deferred** to the swarm-controller /
|
||||
snapshot-storage rework where the problem changes shape.
|
||||
|
||||
## Build tab (`#rebuild`)
|
||||
|
||||
The rebuild queue is the **R3BU1LD QU3U3** tab (`#rebuild`). It lives in `builds.js`, which `tabs.js` imports: the
|
||||
dashboard's one tab strip, `/api/state` snapshot and
|
||||
`/api/dashboard/stream` subscription serve them, with
|
||||
`rebuild_queue_changed` on the stream alongside the dashboard's other
|
||||
kinds.
|
||||
The 4G3NTS tab does **not** read the graph endpoint, by design — the swarm
|
||||
view stays independent of job-queue internals. Its per-agent pending
|
||||
badges are transient-only, and its queue-summary banner reads the much
|
||||
narrower `GET /api/jobq/rollup` instead (see Container row, below) — a
|
||||
handful of pre-tallied counts, not the graph. The banner's "view queue →"
|
||||
link goes to the `#rebuild` tab.
|
||||
|
||||
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
|
||||
operations: rebuilds and first-spawns. One
|
||||
operation runs at a time; the worker drains FIFO. **Is a mounted
|
||||
`JobqGraph`** (the shared generic graph-viewer Preact component,
|
||||
`@hive/shared/jobq-graph.js` — the same one swarm-ui uses) —
|
||||
`builds.js` doesn't render the queue itself; it just mounts the component
|
||||
(`mountJobqGraph(container, props)`, since this page has no JSX
|
||||
pipeline) with an `onUpdate` callback to drive the two things below it
|
||||
that the generic view doesn't show. The component owns fetching, cold
|
||||
and live: `GET /api/jobq/graph` on mount, and a refetch via the mount
|
||||
handle's `.refresh()` on every `rebuild_queue_changed` SSE tick (that
|
||||
event is a bare `{ seq }` trigger carrying no queue payload; both
|
||||
dashboard pages treat the tick as a pure refetch trigger).
|
||||
|
||||
Each row is one root graph node (`parent: null`); a multi-step op's
|
||||
per-agent subgraphs and sub-steps render as nodes within that one
|
||||
entry (structural `parent` edges define the tree; sibling order is
|
||||
array order, unchanged from the wire). A row shows a state glyph (`⏸`
|
||||
pending / `▶` running / `◐` finishing — own work done, a sub-node
|
||||
still running / `✔` done / `✖` failed / `⊘` cancelled / `·` skipped)
|
||||
and each step's own label/agent. A `Node`-kind dep on a sibling shows
|
||||
as a plain "waits on: `<label>`" text line under the row rather than a
|
||||
gutter rail — a rail breaks visually whenever a nested subtree sits
|
||||
between the two related rows, since a text line needs no continuous
|
||||
vertical space to draw.
|
||||
`builds.js` mounts the component with `cancellable` set, which turns on
|
||||
a per-node cancel button (`✕`) on any non-terminal row — the button
|
||||
calls the mount's `onCancel(id)` callback, and the page does the actual
|
||||
`POST /api/rebuild-queue/{id}/cancel`, matching that callback to its
|
||||
own domain concept (the component knows nothing about that endpoint).
|
||||
**Rows carry no source chip, kind label, timing, or build-log
|
||||
deep-link** — the generic graph wire doesn't carry those fields, and
|
||||
rows deliberately present exactly what the endpoint provides rather
|
||||
than reconstructing chrome hive-c0re doesn't send. Settled entries
|
||||
render their **full step tree**, not just a bare summary — the wire
|
||||
doesn't filter `Done` nodes out.
|
||||
|
||||
**State filter.** A row of per-state checkboxes above
|
||||
the tree — one per lifecycle state, matching the row glyphs — lets the
|
||||
operator narrow which root groups render; unchecking a state re-fetches
|
||||
`GET /api/jobq/graph?states=<checked, comma-joined>` rather than
|
||||
hiding rows client-side, so the `onUpdate` callback's node list (and
|
||||
everything downstream of it) only
|
||||
ever sees what's actually shown. Filtering is by a **root's own**
|
||||
state, which is already its subtree's rolled-up answer, so the filter
|
||||
keeps or drops a whole group, never splitting mid-tree. Default selection is
|
||||
every state **except** `Done`/`Skipped` — a fresh queue view leads
|
||||
with what's still moving or needs attention, not the settled tail;
|
||||
builds.js omits the `states` param entirely (identical request to
|
||||
before this filter existed) when the operator has checked every state. Server-side: the query
|
||||
narrows [`Queue::graph_snapshot`]'s already-bounded (`MAX_HISTORY_DAGS`)
|
||||
root set — the history cap and the state filter are independent
|
||||
concerns, so a narrow filter never reaches further back in time to
|
||||
compensate.
|
||||
|
||||
A build's output and history aren't on this page. hive-c0re logs every container build
|
||||
to the journal, and the swarm's `builds` Grafana board reads those lines: the builds with
|
||||
their outcome and runtime, and the output of the one you pick.
|
||||
|
||||
## Container row
|
||||
|
||||
A full-height **square agent icon** (5em, capped) on the left. The
|
||||
icon is the **selection toggle**: select (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/agents.js::renderContainers`).
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
**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 dashboard
|
||||
already knows the container is stopped (`ContainerView.running = false`)
|
||||
the fallback fires immediately, skipping the doomed `<url>/icon`
|
||||
fetch entirely.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
**Line 1** — agent name (link → new tab), m1nd/ag3nt chip, an
|
||||
**icon-only nav strip** plus live agent-owned state, all populated
|
||||
async from a single `GET /api/dashboard-state` call to the agent's
|
||||
own hive-agent. The response (`DashboardState`) carries: `links` (nav
|
||||
strip entries — `📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge
|
||||
profile`, `↳ agent-configs mirror`, plus any agent-declared
|
||||
`dashboardLinks` extras), `status_text` / `status_set_at` (agent
|
||||
self-reported status — the `(set N ago)` chip carries a `data-set-at`
|
||||
stamp and ticks every 30s to stay fresh across the long-lived keyed row
|
||||
cache), `rate_limited`, `ctx_tokens` / `context_window_tokens`
|
||||
(context-window badge data). hive-agent is the single source
|
||||
of truth for all of these. The dashboard resolves each `AgentLink.kind`
|
||||
against a per-agent base URL depending on whether hive-gateway is in
|
||||
front (`StateSnapshot.gateway_enabled`, sourced from the
|
||||
`HIVE_GATEWAY_ENABLED` env). The c0re NixOS module sets that env
|
||||
unconditionally — the gateway always runs; the flag stays so the
|
||||
dashboard doesn't have to learn that. Gateway-on: base URL is
|
||||
`/agent/<name>` (same origin, gateway proxies to the
|
||||
per-agent harness — TCP or unix-domain depending on the agent's
|
||||
`HIVE_WEB_SOCKET` opt-in, see
|
||||
`docs/4-implementation/networking/gateway.md::Per-agent unix-socket upstream`).
|
||||
Gateway-off is the flag-absent fallback (a dashboard served outside a
|
||||
hyperhive deploy): base URL is
|
||||
`http://<host>:<container.port>` (direct TCP). Forge
|
||||
links resolve against `state.forge_public_url` (sourced from
|
||||
`services.hyperhive.swarm.forge.publicUrl`) and disappear entirely when
|
||||
that's unset — never guessed from `<host>:3000`. External links are
|
||||
already absolute. The same base URL drives the primary agent-name
|
||||
link + favicon fetch, so the whole row routes through the gateway
|
||||
as a unit.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
**When the container is stopped** (`ContainerView.running = false`),
|
||||
the dashboard skips the async `dashboard-state` fetch entirely (the agent
|
||||
web server is down), so a single badge replaces the badge chain,
|
||||
the nav strip is empty, and status text / rate-limited / ctx
|
||||
badges disappear. The agent icon goes straight to the dimmed
|
||||
`/favicon.svg` fallback instead of attempting a doomed load from the
|
||||
container's URL. Static fields — `pending_reminders`, `parent`, `config` link — remain visible
|
||||
regardless of run state.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
**Line 2** — status badges only (no per-card action buttons — actions
|
||||
live in the **selection bar** or the **per-agent `⋮` menu**, see
|
||||
below).
|
||||
|
||||
### Status badges
|
||||
|
||||
**Which single badge:** `ContainerView.failed`
|
||||
(systemd `ActiveState=failed` — the unit exhausted its bounded
|
||||
restarts and gave up on its own) draws a red `✖ gave up` badge;
|
||||
otherwise a plain muted `■ not running` — a container an operator
|
||||
stopped deliberately. Both states read `running: false`; `failed` is
|
||||
the orthogonal fact (a fourth one alongside `paused`/
|
||||
`needs_login`, same "independent flags, no state machine" shape —
|
||||
see `ContainerView`'s own doc comment) that tells them apart. An
|
||||
older hive-c0re without the field serves `failed: undefined`, which
|
||||
reads falsy — degrades cleanly to the single `not running` badge.
|
||||
|
||||
When the container is running, status badges follow — `⊘ rate
|
||||
limited` (red, while the harness parks itself after a 429), `needs
|
||||
login` — plus **one `◐ pending-state…` pill per
|
||||
active transient** (replaces buttons during operator-initiated
|
||||
start / stop / restart / rebuild / destroy). An agent can carry
|
||||
**multiple transients at once** — for example a lease-exempt `prebuild`
|
||||
running alongside a `stop_for_update` on the same agent — and each
|
||||
renders as its own independent badge instead of collapsing
|
||||
into one label, matching the existing multi-badge convention this
|
||||
line already uses for `paused`/model/ctx.
|
||||
|
||||
Any pending badge means the row is actually **running** something
|
||||
right now (see **Pending-badge derivation** below); every row
|
||||
carrying ≥1 badge keeps the amber row tint AND draws a **rotating
|
||||
amber ring** around the agent icon.
|
||||
|
||||
**Pending-badge derivation:** transients only (`transientsState`,
|
||||
keyed `agent -> Map<kind, since_unix>`) — a transient is **derived
|
||||
from a job-queue node currently `Running`** against that agent, not
|
||||
declared per request, so its label follows the operation as it
|
||||
progresses (a rebuild reads `stop_for_update`, then `swap`, then
|
||||
`reconcile` rather than one constant `rebuilding` for its whole
|
||||
life). Two consequences for anything rendering it:
|
||||
|
||||
- The label vocabulary is **open** — it's the node's own wire tag
|
||||
(`NodeKind`'s `&str` conversion, the same strings the graph wire's node
|
||||
labels carry), not a fixed set. Treat it as an opaque display
|
||||
string; don't switch on specific values.
|
||||
- It's **not** exclusively operator-initiated, and **not** limited
|
||||
to rebuild-shaped work — `running_transients()` in hive-c0re is
|
||||
a status-only test (any `Running` node whose payload names a
|
||||
non-empty agent lights a pill), so work the operator never
|
||||
triggered (a crash-recover rebuild, a boot-sweep rebuild, a
|
||||
lease-exempt `prebuild`) lights the same mechanism.
|
||||
|
||||
Ops with no queue node behind them (destroy, migration) supply
|
||||
their own label directly via `TransientSet`/`TransientCleared`
|
||||
events carrying no backing node at all.
|
||||
|
||||
**Queued (not-yet-started) work shows nothing on the card.** Only
|
||||
running work gets a per-agent pending badge, by design — the swarm
|
||||
view stays independent of job-queue internals. The queue-summary
|
||||
banner below is the only queued-work indicator on this tab, and it
|
||||
reads the narrow `/api/jobq/rollup` endpoint rather than the full
|
||||
graph.
|
||||
|
||||
**Active model badge** (`model · <name>`, blue) appears when the
|
||||
container is running and the harness has persisted a model name (the
|
||||
`active_model` field of `hyperhive-harness.json`, the consolidated
|
||||
harness state file in the agent's state dir). Read by hive-c0re's
|
||||
`ContainerView` (`read_active_model`); absent until the agent has
|
||||
completed at least one turn, and hive-c0re suppresses stale values
|
||||
for stopped containers.
|
||||
|
||||
**`ctx · Nk` chip** shows the agent's last-turn context size,
|
||||
populated from `DashboardState.ctx_tokens` (absent until the
|
||||
agent has completed at least one turn). The chip colour (green /
|
||||
yellow / red) keys off `DashboardState.context_window_tokens`
|
||||
(the real context window for the model the agent last ran on,
|
||||
authoritative from the agent side); the badge goes yellow ≥ 50%
|
||||
and red ≥ 75% of that window, matching the harness compaction
|
||||
watermarks. When the window value is absent the badge falls back
|
||||
to fixed 100k / 150k thresholds.
|
||||
|
||||
**Per-agent `⋮` overflow menu** — a `⋮` button appears on the right
|
||||
edge of each container row. Selecting it opens a small dropdown with
|
||||
per-agent actions and navigation links. Contents:
|
||||
|
||||
- `↺ R3ST4RT` (running agents only) / `■ ST0P` (running only) /
|
||||
`▶ ST4RT` (stopped only) — single-agent run-state toggles.
|
||||
Identical to the bulk actions on the selection bar but operate
|
||||
on one agent without requiring a selection.
|
||||
- `↻ R3BU1LD` — always available; queues a rebuild for this agent.
|
||||
- `DESTR0Y` / `PURG3` — destructive, each prompts for confirmation
|
||||
via the themed dialog (see **Themed dialogs** below).
|
||||
|
||||
**Build-queue summary banner** — when the job queue has any active
|
||||
work, a compact amber banner sits above the container list: `◐ build
|
||||
queue — N running · M queued — view queue →` (the link goes to the
|
||||
R3BU1LD QU3U3 tab). The shared `JobqRollup` Preact component
|
||||
(`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
|
||||
mounts, pointed at swarm-controller's own rollup endpoint instead),
|
||||
mounted once into `#jobq-rollup-section` by `agents.js::initJobqRollup`
|
||||
and refreshed via its own handle rather than being re-rendered by
|
||||
`renderContainers`. Reads `GET /api/jobq/rollup`
|
||||
(`hive-jobq-wire::state_rollup`) — `Vec<{ state, nodes, roots }>`,
|
||||
every lifecycle state present in a fixed order, zero counts included —
|
||||
rather than the full `/api/jobq/graph` tree: `running` sums the
|
||||
`Running` and `Finishing` entries' `roots` (`Finishing` = own work
|
||||
done, subtree still going, still in flight), `queued` reads the
|
||||
`Pending` entry's `roots`. `roots` specifically, not `nodes` — the
|
||||
banner means *N whole operations*, not raw steps (one rebuild is ~7
|
||||
nodes but 1 root); `nodes` exists on the same endpoint for a consumer
|
||||
that wants step-level counts instead, unused here.
|
||||
|
||||
### Themed dialogs
|
||||
|
||||
All confirmations and transient error notices use an in-app themed
|
||||
dialog system (`@hive/shared/modal.js`) rather than the browser's
|
||||
native `confirm()` / `alert()` chrome, so they match the Catppuccin
|
||||
palette and the OS can't style them away. Two primitives, both built
|
||||
on the `openDialog` core:
|
||||
|
||||
- `themedConfirm({ message, danger, confirmLabel, checkboxes })`
|
||||
— a modal confirm that resolves to `null` on cancel or an object
|
||||
of checkbox states on confirm. Destructive actions pass
|
||||
`danger: true` (the confirm button turns red and the cancel
|
||||
button takes focus). Selecting the backdrop and `Esc` both cancel.
|
||||
- `themedToast(message, { type, ... })` — a non-blocking toast (top-right,
|
||||
`info` / `error` / `ok`) for transient validation + action
|
||||
failures, so an error doesn't lock the operator behind a modal.
|
||||
Single-action errors autodismiss; bulk / partial-failure
|
||||
summaries are sticky (select to dismiss) so they aren't missed.
|
||||
|
||||
Every destructive run-state action (`ST0P`, `R3ST4RT`, `R3BU1LD`,
|
||||
`DESTR0Y`, `PURG3`) routes through `themedConfirm`, on both
|
||||
the per-agent `⋮` menu and the bulk selection bar.
|
||||
|
||||
**Graceful stop** — the `■ ST0P` confirm dialog (per-agent and
|
||||
bulk) carries a `stop gracefully — let the agent finish its turn
|
||||
and flush state before the container stops` checkbox. When ticked,
|
||||
the action POSTs `/api/kill/<name>?graceful=true` (the bulk path
|
||||
appends the flag per-agent); unticked is the instant hard stop
|
||||
(`/api/kill/<name>` with no query). hive-c0re enqueues a
|
||||
`Signal` → `Drain` job-queue node pair (`NodeKind::Signal` /
|
||||
`NodeKind::Drain`): `Signal` sets the graceful-stop fence and kicks
|
||||
the harness so it runs one stop-checkpoint turn (so the agent can
|
||||
flush `/state`); `Drain` awaits the harness clearing that fence,
|
||||
bounded by a 3-minute timeout (`GRACEFUL_STOP_TIMEOUT`) that
|
||||
resolves either way and falls back to the downstream mechanical
|
||||
stop. The quiescing progress surfaces through the same
|
||||
rebuild-queue pending-badge mechanism the card already reads for a
|
||||
rebuild, driven by the `Signal`/`Drain` pair rather than a nix
|
||||
build. (The `hivectl stop --graceful` CLI flag enqueues the same
|
||||
`Signal`/`Drain` pair, so the dashboard and CLI paths behave
|
||||
identically.)
|
||||
|
||||
## Selection bar
|
||||
|
||||
Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` /
|
||||
`R3BU1LD` / `DESTR0Y` / `PURG3`) live here rather than as per-card
|
||||
buttons — see **Container row** above. Selecting 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-4G3NTS tabs since other
|
||||
tabs don't show the agent cards needed to cross-reference.
|
||||
|
||||
When the operator selects one or more agents (via the icon), a sticky
|
||||
frosted-mauve bar slides up from the bottom of the viewport
|
||||
(`#selection-bar`, `position: fixed; bottom: 0`). It shows:
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
- **Count + names** — "N agents selected · name1, name2, …"
|
||||
- **Bulk action buttons** — only enabled when ALL selected agents
|
||||
support the action; greyed out with a tooltip naming the blockers
|
||||
when the selection is mixed:
|
||||
- `↺ R3ST4RT` — running agents only
|
||||
- `■ ST0P` — running agents only
|
||||
- `▶ ST4RT` — stopped agents only
|
||||
- `⏸ P4US3` — unpaused agents only (orthogonal to running/stopped —
|
||||
see **Container row**'s own pause/resume note)
|
||||
- `▶ R3SUM3` — paused agents only
|
||||
- `↻ R3BU1LD` — always available
|
||||
- `DESTR0Y` / `PURG3` — always available
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
That list is the whole set — the bar offers no other bulk action.
|
||||
- **`✕ clear`** button + `Esc` key clear the entire selection.
|
||||
|
||||
Every render prunes stale selections (agents destroyed while
|
||||
selected) before the bar appears.
|
||||
|
||||
## Dashboard endpoints
|
||||
|
||||
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
|
||||
the pages menu's **api docs** entry), with the raw spec at `/api/openapi.json`.
|
||||
It's a growing supplement, not yet a full replacement for the list
|
||||
below — some endpoints aren't in it yet.
|
||||
|
||||
- `POST /api/{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
|
||||
`destroy` accepts `purge=on` to also wipe state dirs.
|
||||
- `POST /api/purge-tombstone/{name}` — wipe a tombstone's state dirs.
|
||||
- `POST /api/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 wasn't in `Queued` state.
|
||||
- `POST /api/agent/{name}/mark-all-read` — ack all pending broker
|
||||
messages for `{name}`. Backfills `delivered_at` for rows
|
||||
not yet delivered and sets `acked_at = now`. Returns
|
||||
`{ "marked": N }`. Agent name validated against
|
||||
`[a-z0-9_-]`, 1-63 chars; 400 on bad input.
|
||||
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
|
||||
headers for one agent, newest first. Returns
|
||||
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
|
||||
`started_at`, `finished_at`, `status` (`"ok"` / `"fail"` /
|
||||
`null` while in-progress). `limit` defaults to 10, server-side
|
||||
cap at 50. Agent name validated (`[a-z0-9_-]`, 1-63 chars).
|
||||
- `GET /api/build-logs/id/{id}` — full build log by id. Returns
|
||||
`BuildLogFull` (JSON): all header fields plus `stdout` and
|
||||
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
|
||||
404 when the row is missing (vacuum-reaped or stale id).
|
||||
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of
|
||||
stdout+stderr for an in-progress build (newline-delimited JSON
|
||||
frames). The
|
||||
stream closes when the build finishes or errors.
|
||||
- `GET /api/build-logs/id/{id}/raw` — raw concatenated
|
||||
stdout+stderr as `text/plain`. Works on both finished and in-progress
|
||||
build logs.
|
||||
- `GET /api/reminders` — list pending reminders for the
|
||||
dashboard's queued-reminders panel.
|
||||
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
||||
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
||||
failed (clears the failure state so the scheduler retries).
|
||||
|
||||
- `GET /api/dashboard/stream` — unified live event channel:
|
||||
broker `sent` / `delivered`, plus the mutation events listed
|
||||
below. Each frame carries `seq`.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
## Dashboard event channel
|
||||
|
||||
Wire vocabulary on `/api/dashboard/stream` (kind tag is in the JSON
|
||||
payload):
|
||||
|
||||
- `sent` / `delivered` — broker traffic, mirrored from the
|
||||
intra-process channel by a forwarder task. Both carry `id: i64`
|
||||
(the broker row id) and `in_reply_to: Option<i64>` for thread
|
||||
rendering.
|
||||
- `transient_set` (name, transient_kind, since_unix) /
|
||||
`transient_cleared` (name) — lifecycle action spinners. The
|
||||
client ticks the elapsed-seconds badge off `since_unix`
|
||||
client-side, no polling. `transient_kind` is an **open**
|
||||
display string (the running node's own tag), not a fixed
|
||||
enum — render it, don't branch on it.
|
||||
- `container_state_changed` (container: ContainerView) /
|
||||
`container_removed` (name) — per-row container mutations,
|
||||
emitted by `Coordinator::rescan_containers_and_emit` from
|
||||
the job queue's own node execution (`job_queue::exec`, for example
|
||||
after a rebuild's stop/swap/start steps or a destroy's teardown
|
||||
step) — and from the 10s
|
||||
`crash_watch` poll. Client upserts/removes by name and
|
||||
reads the pending overlay from `transientsState` since the
|
||||
payload doesn't carry it.
|
||||
- `rebuild_queue_changed` (seq) — **payload-free trigger**, fired on
|
||||
every queue mutation (enqueue, state transition, dedup collapse,
|
||||
terminal-history trim). Unlike the snapshot events below it ships
|
||||
no state at all: the client re-fetches `GET /api/jobq/graph`, which
|
||||
is where it reads the queue from cold too.
|
||||
- `reminders_changed` (seq, reminders: `Vec<PendingReminder>`) —
|
||||
full snapshot of all pending reminders. Emitted after every
|
||||
reminder mutation: agent `remind` calls (`agent_server`),
|
||||
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
|
||||
with Reminder kind, and the scheduler tick after each delivery
|
||||
batch (`reminder_scheduler`).
|
||||
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
||||
full snapshot of destroyed-but-state-kept agents. Emitted on
|
||||
destroy, purge, and revive. The K3PT ST4T3 tab subscribes.
|
||||
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
`/api/state` is **only fetched on cold-load and on the few
|
||||
forms that mutate non-event-derived state** (PURG3, since
|
||||
tombstones aren't event-shaped yet). Every other section — transients,
|
||||
containers —
|
||||
derives from `/api/dashboard/stream` after the initial snapshot,
|
||||
maintaining its own client-side store and applying events on
|
||||
top.
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
Generalised form helper: `form[data-confirm="…"]` pops
|
||||
`confirm()` before submit.
|
||||
|
||||
Loading…
Reference in a new issue