docs: the hive dashboard and agent page shrink to overviews
The hive dashboard and the per-agent page lose their control-by-control UI description and endpoint tables from the handbook. The full text moves to 4-implementation/web-ui/dashboard.md and agent-page.md, and the handbook pages keep what an operator needs to find and use them. Code comments that named the old sections point at the new pages.
This commit is contained in:
parent
ce0db01937
commit
2acde41e54
25 changed files with 1028 additions and 955 deletions
|
|
@ -1,421 +1,24 @@
|
||||||
# Per-agent page
|
# Per-agent page
|
||||||
|
|
||||||
> Part of [Agents](README.md). See also:
|
> Part of [Agents](README.md). See also: [Dashboard layout](../hives/dashboard.md)
|
||||||
> [Shape (shared)](../../4-implementation/web-ui/shape.md) · [Dashboard layout](../hives/dashboard.md)
|
|
||||||
|
|
||||||
|
Every agent's harness serves a page of its own, reached from the agent's row on
|
||||||
|
the hive dashboard. It shows a live terminal view of what the agent is doing: its
|
||||||
|
turns, tool calls and results as they happen, with the recent history replayed
|
||||||
|
when you open it. The page has no message box; to write to an agent, use the
|
||||||
|
terminal in the [swarm UI](../swarm/ui.md). Like the hive dashboard, it
|
||||||
|
keeps shrinking, bit by bit.
|
||||||
|
|
||||||
Two fixed-position layers frame a full-viewport terminal: a header and
|
- **Header**: the agent's icon and name, a state badge (idle, thinking, needs
|
||||||
scrollable main content — plus a slide-in side panel for flyouts and long
|
login), and pills for its inbox and todos. Selecting a pill opens the list in
|
||||||
content. The page has no message box: to write to an agent, use the swarm
|
a side panel. The inbox panel has **mark all read**, and the todos panel lets
|
||||||
UI's terminal.
|
you dismiss todos.
|
||||||
|
- **State badge menu**: **cancel turn** stops the turn in flight, **new session**
|
||||||
|
starts the next turn without the resume session, and **log out** wipes the
|
||||||
|
agent's claude login (it then waits in `needs_login`).
|
||||||
|
- **Stats page**: the agent's turn statistics. The same figures are in Grafana's
|
||||||
|
`agents` board.
|
||||||
|
|
||||||
## Header
|
The header's parts, the live view, the endpoints the page and the harness serve,
|
||||||
|
and the stats page are in
|
||||||
Preact component tree (`Header.tsx` + `StatusChips.tsx` +
|
[the agent page reference](../../4-implementation/web-ui/agent-page.md).
|
||||||
`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:
|
|
||||||
- **Status badge**, one badge for liveness and turn state: `● alive`
|
|
||||||
(green) / `⊘ rate limited` (red) / `◌ needs login` / `◌ logging in` /
|
|
||||||
`○ offline` / `… connecting` / `💤 idle` / `🧠 thinking` /
|
|
||||||
`📦 compacting` / `… booting` + age suffix, or paused. Selecting it
|
|
||||||
opens a menu: `⏸ pause` / `▶ resume` (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), `■ cancel turn` (only while a turn is running),
|
|
||||||
`↻ new session` and `🔓 logout`. Cancel turn, new session and logout
|
|
||||||
need a second selection to confirm.
|
|
||||||
- **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).
|
|
||||||
- **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, 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; `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 `/harness/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__matrix__send_dm →
|
|
||||||
iris: "..."`, 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
|
|
||||||
(`/api/todos/mark-done` with no todo ids), **500** only for genuine
|
|
||||||
server/transport failures. `/compact` never conflicts: it queues
|
|
||||||
and runs at the end of the current turn. 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.
|
|
||||||
|
|
||||||
The agent has no login flow of its own. An agent without credentials
|
|
||||||
reports `needs_login_idle`, the operator logs in from the swarm UI, and the
|
|
||||||
credentials arrive from the swarm secret store.
|
|
||||||
|
|
||||||
- `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 (a login from the swarm UI stores one,
|
|
||||||
and the agent installs it).
|
|
||||||
|
|
||||||
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. 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`. 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.
|
|
||||||
|
|
|
||||||
|
|
@ -99,15 +99,16 @@ For the code, these jump straight to the relevant section:
|
||||||
- **"How does the dashboard SPA stay live without polling?"** →
|
- **"How does the dashboard SPA stay live without polling?"** →
|
||||||
[`shape.md`](../../4-implementation/web-ui/shape.md) (SSE multiplexing,
|
[`shape.md`](../../4-implementation/web-ui/shape.md) (SSE multiplexing,
|
||||||
Worker-death self-heal, atomic repaint).
|
Worker-death self-heal, atomic repaint).
|
||||||
- **"What does a container row contain?"** → [`dashboard.md`](dashboard.md)
|
- **"What does a container row contain?"** →
|
||||||
(Container row, Selection bar).
|
[`dashboard.md`](../../4-implementation/web-ui/dashboard.md) (Container row,
|
||||||
|
Selection bar).
|
||||||
- **"What endpoints does the dashboard expose?"** →
|
- **"What endpoints does the dashboard expose?"** →
|
||||||
[`dashboard.md`](dashboard.md) (Dashboard endpoints, Dashboard event
|
[`dashboard.md`](../../4-implementation/web-ui/dashboard.md) (Dashboard endpoints,
|
||||||
channel).
|
Dashboard event channel).
|
||||||
- **"How does the per-agent terminal render tool calls?"** →
|
- **"How does the per-agent terminal render tool calls?"** →
|
||||||
[`terminal-rendering.md`](../../4-implementation/web-ui/terminal-rendering.md)
|
[`terminal-rendering.md`](../../4-implementation/web-ui/terminal-rendering.md)
|
||||||
(the full row taxonomy and dispatch walkthrough); for a summary see
|
(the full row taxonomy and dispatch walkthrough); for a summary see
|
||||||
[`web-page.md`](../agents/web-page.md) (Per-stream rendering).
|
[`agent-page.md`](../../4-implementation/web-ui/agent-page.md) (Per-stream rendering).
|
||||||
- **"Which CSS variable to use, and where are the colours defined?"** →
|
- **"Which CSS variable to use, and where are the colours defined?"** →
|
||||||
[`css-vars.md`](../../4-implementation/web-ui/css-vars.md) (Palette, the
|
[`css-vars.md`](../../4-implementation/web-ui/css-vars.md) (Palette, the
|
||||||
single-source `base.css` rule).
|
single-source `base.css` rule).
|
||||||
|
|
|
||||||
|
|
@ -1,498 +1,33 @@
|
||||||
# Dashboard layout
|
# Dashboard layout
|
||||||
|
|
||||||
> Part of [Hives](README.md). See also:
|
> Part of [Hives](README.md). See also: [Per-agent page](../agents/web-page.md)
|
||||||
> [Shape (shared)](../../4-implementation/web-ui/shape.md) · [Per-agent page](../agents/web-page.md)
|
|
||||||
|
|
||||||
This page covers the per-hive UI that hive-c0re serves. Swarm-wide
|
The hive dashboard is the per-hive UI that hive-c0re serves at `/`. The
|
||||||
surfaces — the agent roster, agent creation, linking external forge and
|
[swarm UI](../swarm/ui.md) holds everything swarm-wide (the agent roster, agent
|
||||||
matrix accounts, the controller's job graph — are on the
|
creation, linking external forge and matrix accounts, the controller's job
|
||||||
[swarm UI](../swarm/ui.md).
|
graph) and is where you do day-to-day work. The hive dashboard keeps shrinking:
|
||||||
|
parts of it go, bit by bit.
|
||||||
|
|
||||||
hive-c0re serves the dashboard at `/`.
|
A fixed header holds the tab strip, a **pages** menu with links to the API docs
|
||||||
It has a fixed chrome header at the top and a `<main>` that shows exactly
|
and the swarm UI, and a profile menu with your authelia settings and sign-out.
|
||||||
one tab pane at a time. The URL hash (`#agents`, `#rebuild`,
|
The URL hash (`#agents`, `#rebuild`, `#kept`) picks the tab.
|
||||||
`#kept`) drives which pane is
|
|
||||||
active; hash changes don't reload the page.
|
|
||||||
|
|
||||||
**Chrome header** (fixed, overlays the active tab pane):
|
- **4G3NTS** lists the hive's containers, one row each, sorted by name, with
|
||||||
- **Pages menu**: a link icon at the right end of the chrome, beside the
|
the agent's status and the actions on it: rebuild, start, stop, destroy. A red
|
||||||
profile avatar (`links-menu.js`, `@hive/shared`'s `LinksMenu`, the same
|
banner at the top says two agents hash to the same port. Rename one and
|
||||||
component the swarm UI uses for its service links), opening a popover with links to **api docs**
|
rebuild.
|
||||||
(`/api/docs/`) and **swarm** (the swarm
|
- **R3BU1LD QU3U3** shows the rebuild queue and each build's log.
|
||||||
UI at the swarm apex domain, opened in a new tab). The swarm entry
|
- **K3PT ST4T3** lists agents with kept state and no container, with their size
|
||||||
comes from `state.swarm_public_url`, which hive-c0re reads from
|
and age. **PURG3** wipes one's state directory. The list also shows an agent
|
||||||
`HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`);
|
that's part-way through a spawn, because both look the same on disk: the row
|
||||||
with no domain set there is no swarm entry. The standalone pages link back to `/`.
|
says `offline` and the tab carries a warning.
|
||||||
- **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
|
Per-container CPU, memory and disk figures aren't shown here. They go out as
|
||||||
|
metrics and appear in Grafana, and you set an agent's CPU and memory caps per
|
||||||
**C0NTAINERS** — live containers as a flat list, one row per
|
agent in the swarm UI. The dashboard shows no hive warnings either; they show up
|
||||||
container, sorted alphabetically by agent name; every container the
|
as metrics and Grafana alerts (see
|
||||||
rescan reports gets a row. Pulsing red banner at the top
|
[observability](../../4-implementation/scheduler/observability.md#hive-scoped-metrics-hive-c0re)).
|
||||||
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, 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`. 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). 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 and the rate-limited badge 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`.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
**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,pause,resume}/{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?limit=N` — most-recent build log headers across
|
|
||||||
all agents (`limit` defaults to 30).
|
|
||||||
- `GET /api/build-log/{node_id}` and `/api/build-log/{node_id}/raw` — the
|
|
||||||
build log of one job-queue node, as JSON and as `text/plain`.
|
|
||||||
- `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/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.
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
|
The container row, status badges, dialogs, the selection bar, the API endpoints
|
||||||
|
and the event channel are in
|
||||||
|
[the dashboard reference](../../4-implementation/web-ui/dashboard.md).
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ each `git` call inline, never writing it into a repo's config, so
|
||||||
no stored token sits in a repo an agent might one day read.
|
no stored token sits in a repo an agent might one day read.
|
||||||
|
|
||||||
The dashboard deep-links into this org — a `config repo` link
|
The dashboard deep-links into this org — a `config repo` link
|
||||||
per container row. See `docs/2-handbook/hives/dashboard.md`.
|
per container row. See `docs/4-implementation/web-ui/dashboard.md`.
|
||||||
|
|
||||||
### Submitting agent's view of config repos
|
### Submitting agent's view of config repos
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ Under `/var/lib/hyperhive/agents/<name>/`:
|
||||||
has a value.
|
has a value.
|
||||||
- `hyperhive-harness.json` — rate-limited / needs-login flags read
|
- `hyperhive-harness.json` — rate-limited / needs-login flags read
|
||||||
by the dashboard's async container-state fetch. See
|
by the dashboard's async container-state fetch. See
|
||||||
`docs/2-handbook/hives/dashboard.md::Container row`.
|
`docs/4-implementation/web-ui/dashboard.md::Container row`.
|
||||||
|
|
||||||
The agent writes its own instructions to `AGENTS.md` here; claude and
|
The agent writes its own instructions to `AGENTS.md` here; claude and
|
||||||
opencode load it automatically from their cwd, `state/`. At boot,
|
opencode load it automatically from their cwd, `state/`. At boot,
|
||||||
|
|
|
||||||
|
|
@ -448,7 +448,7 @@ when `StateSnapshot.gateway_enabled` is true, and direct
|
||||||
every hive, so the direct shape is a fallback for the variable being unset.
|
every hive, so the direct shape is a fallback for the variable being unset.
|
||||||
Three render sites follow it: the agent-name link, the favicon fetch
|
Three render sites follow it: the agent-name link, the favicon fetch
|
||||||
(`<url>/icon`), and the nav-strip `container`-kind links from
|
(`<url>/icon`), and the nav-strip `container`-kind links from
|
||||||
`DashboardState.links` (`GET /api/dashboard-state`). See `docs/2-handbook/hives/dashboard.md::Container row` for the frontend side.
|
`DashboardState.links` (`GET /api/dashboard-state`). See `docs/4-implementation/web-ui/dashboard.md::Container row` for the frontend side.
|
||||||
|
|
||||||
### Dialing another vhost by name (`verifiedProxyTo`)
|
### Dialing another vhost by name (`verifiedProxyTo`)
|
||||||
|
|
||||||
|
|
|
||||||
424
docs/4-implementation/web-ui/agent-page.md
Normal file
424
docs/4-implementation/web-ui/agent-page.md
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
# Per-agent page: header, live view and endpoints
|
||||||
|
|
||||||
|
The page each agent's harness serves: header, live view, side panel, actions,
|
||||||
|
its endpoints and the stats page. The operator's overview is
|
||||||
|
[the agent page](../../2-handbook/agents/web-page.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:
|
||||||
|
- **Status badge**, one badge for liveness and turn state: `● alive`
|
||||||
|
(green) / `⊘ rate limited` (red) / `◌ needs login` / `◌ logging in` /
|
||||||
|
`○ offline` / `… connecting` / `💤 idle` / `🧠 thinking` /
|
||||||
|
`📦 compacting` / `… booting` + age suffix, or paused. Selecting it
|
||||||
|
opens a menu: `⏸ pause` / `▶ resume` (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), `■ cancel turn` (only while a turn is running),
|
||||||
|
`↻ new session` and `🔓 logout`. Cancel turn, new session and logout
|
||||||
|
need a second selection to confirm.
|
||||||
|
- **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`](../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).
|
||||||
|
- **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, 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; `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 `/harness/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](../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`](../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__matrix__send_dm →
|
||||||
|
iris: "..."`, 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
|
||||||
|
(`/api/todos/mark-done` with no todo ids), **500** only for genuine
|
||||||
|
server/transport failures. `/compact` never conflicts: it queues
|
||||||
|
and runs at the end of the current turn. 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.
|
||||||
|
|
||||||
|
The agent has no login flow of its own. An agent without credentials
|
||||||
|
reports `needs_login_idle`, the operator logs in from the swarm UI, and the
|
||||||
|
credentials arrive from the swarm secret store.
|
||||||
|
|
||||||
|
- `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 (a login from the swarm UI stores one,
|
||||||
|
and the agent installs it).
|
||||||
|
|
||||||
|
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. 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`. 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.
|
||||||
509
docs/4-implementation/web-ui/dashboard.md
Normal file
509
docs/4-implementation/web-ui/dashboard.md
Normal file
|
|
@ -0,0 +1,509 @@
|
||||||
|
# Hive dashboard: tabs, container rows and endpoints
|
||||||
|
|
||||||
|
The hive UI that hive-c0re serves at `/`, tab by tab, with its endpoints and
|
||||||
|
event channel. The operator's overview is
|
||||||
|
[the dashboard page](../../2-handbook/hives/dashboard.md); the swarm UI is
|
||||||
|
where day-to-day work happens.
|
||||||
|
|
||||||
|
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](../../2-handbook/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`,
|
||||||
|
`#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](../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](../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, 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`. 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). 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 and the rate-limited badge 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`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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,pause,resume}/{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?limit=N` — most-recent build log headers across
|
||||||
|
all agents (`limit` defaults to 30).
|
||||||
|
- `GET /api/build-log/{node_id}` and `/api/build-log/{node_id}/raw` — the
|
||||||
|
build log of one job-queue node, as JSON and as `text/plain`.
|
||||||
|
- `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/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.
|
||||||
|
- `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.
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shape (shared by both)
|
# Shape (shared by both)
|
||||||
|
|
||||||
> Part of [Hives](../../2-handbook/hives/README.md). See also:
|
> Part of [Hives](../../2-handbook/hives/README.md). See also:
|
||||||
> [Dashboard layout](../../2-handbook/hives/dashboard.md) · [Per-agent page](../../2-handbook/agents/web-page.md)
|
> [Dashboard reference](dashboard.md) · [Per-agent page reference](agent-page.md)
|
||||||
|
|
||||||
## Shared routes
|
## Shared routes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ Never use raw `curl` for forge access.
|
||||||
`nix/agent-modules/weston-vnc.nix` adds an optional Weston Wayland
|
`nix/agent-modules/weston-vnc.nix` adds an optional Weston Wayland
|
||||||
compositor with the `vnc-backend` plugin, surfaced as
|
compositor with the `vnc-backend` plugin, surfaced as
|
||||||
`services.hyperhive.agent.gui.enable = true` per-agent. The harness's
|
`services.hyperhive.agent.gui.enable = true` per-agent. The harness's
|
||||||
`/screen/ws` WebSocket relay (`docs/2-handbook/agents/web-page.md::Per-agent endpoints`)
|
`/screen/ws` WebSocket relay (`docs/4-implementation/web-ui/agent-page.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**: a **fixed** port (`services.hyperhive.agent.gui.vncPort`,
|
- **Port allocation**: a **fixed** port (`services.hyperhive.agent.gui.vncPort`,
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ The host a hive runs on and the operator's tools for it. Start at
|
||||||
|
|
||||||
- **What does the dashboard look like, and how to use it?** →
|
- **What does the dashboard look like, and how to use it?** →
|
||||||
[`2-handbook/hives/`](2-handbook/hives/README.md), with the full tab-by-tab
|
[`2-handbook/hives/`](2-handbook/hives/README.md), with the full tab-by-tab
|
||||||
reference in [`dashboard`](2-handbook/hives/dashboard.md).
|
reference in [`dashboard`](4-implementation/web-ui/dashboard.md).
|
||||||
- **What does `hivectl` do? Provisioning, gateway users, container
|
- **What does `hivectl` do? Provisioning, gateway users, container
|
||||||
shells?** → [`2-handbook/hives/hivectl.md`](2-handbook/hives/hivectl.md) (the curated guide);
|
shells?** → [`2-handbook/hives/hivectl.md`](2-handbook/hives/hivectl.md) (the curated guide);
|
||||||
[`2-handbook/hives/hivectl-cli.md`](2-handbook/hives/hivectl-cli.md) for the exhaustive,
|
[`2-handbook/hives/hivectl-cli.md`](2-handbook/hives/hivectl-cli.md) for the exhaustive,
|
||||||
|
|
@ -98,7 +98,8 @@ One agent: its roster entry, state, config and page. Start at
|
||||||
- **Who can do what to whom — the agent roster and privilege?** →
|
- **Who can do what to whom — the agent roster and privilege?** →
|
||||||
[`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md).
|
[`2-handbook/agents/agent-roster.md`](2-handbook/agents/agent-roster.md).
|
||||||
- **What does an agent's own page show?** →
|
- **What does an agent's own page show?** →
|
||||||
[`2-handbook/agents/web-page.md`](2-handbook/agents/web-page.md).
|
[`2-handbook/agents/web-page.md`](2-handbook/agents/web-page.md); the full
|
||||||
|
reference is [`agent-page`](4-implementation/web-ui/agent-page.md).
|
||||||
- **Agent trust model, prompt-injection threat model, credential
|
- **Agent trust model, prompt-injection threat model, credential
|
||||||
isolation?** → [`2-handbook/agents/security.md`](2-handbook/agents/security.md).
|
isolation?** → [`2-handbook/agents/security.md`](2-handbook/agents/security.md).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
/* ─── full-screen layout overrides ─────────────────────────────────
|
/* ─── full-screen layout overrides ─────────────────────────────────
|
||||||
The agent page mounts a full-viewport terminal under a fixed
|
The agent page mounts a full-viewport terminal under a fixed
|
||||||
frosted-glass header. See docs/2-handbook/agents/web-page.md::Per-agent
|
frosted-glass header. See docs/4-implementation/web-ui/agent-page.md::Per-agent
|
||||||
page for the layer / pill / side-panel structure; rules below
|
page for the layer / pill / side-panel structure; rules below
|
||||||
override the in-page defaults from @hive/shared. */
|
override the in-page defaults from @hive/shared. */
|
||||||
|
|
||||||
|
|
@ -184,7 +184,7 @@ h3 {
|
||||||
}
|
}
|
||||||
.agent-icon {
|
.agent-icon {
|
||||||
/* Square identity anchor — explicit 5em sizing + align-self.
|
/* Square identity anchor — explicit 5em sizing + align-self.
|
||||||
See docs/2-handbook/agents/web-page.md::Per-agent page (Agent icon) for the
|
See docs/4-implementation/web-ui/agent-page.md::Per-agent page (Agent icon) for the
|
||||||
intrinsic-dim-pushes-parent-flex-open + sticks-to-top
|
intrinsic-dim-pushes-parent-flex-open + sticks-to-top
|
||||||
rationale. */
|
rationale. */
|
||||||
width: 5em;
|
width: 5em;
|
||||||
|
|
@ -594,7 +594,7 @@ pre.diff {
|
||||||
/* Tail pill (↓ N new): z-index bumped above the floating chrome so the
|
/* Tail pill (↓ N new): z-index bumped above the floating chrome so the
|
||||||
pill sits on the top layer instead of being clipped by it. (Pill is anchored in
|
pill sits on the top layer instead of being clipped by it. (Pill is anchored in
|
||||||
`.agent-main` rather than `.terminal-wrap` — see
|
`.agent-main` rather than `.terminal-wrap` — see
|
||||||
docs/2-handbook/agents/web-page.md::Per-agent page Terminal-wrap paragraph for the
|
docs/4-implementation/web-ui/agent-page.md::Per-agent page Terminal-wrap paragraph for the
|
||||||
backdrop-filter stacking-context rationale.) */
|
backdrop-filter stacking-context rationale.) */
|
||||||
.agent-main .tail-pill {
|
.agent-main .tail-pill {
|
||||||
bottom: 0.6em;
|
bottom: 0.6em;
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export interface HeaderProps {
|
||||||
* a variable pill count) grows to fit badges+pills together, keeping
|
* a variable pill count) grows to fit badges+pills together, keeping
|
||||||
* the header a stable height even on widescreen. Meta-nav
|
* the header a stable height even on widescreen. Meta-nav
|
||||||
* (`<MetaNav>`) lives here too, as a fixed-size trigger, not an
|
* (`<MetaNav>`) lives here too, as a fixed-size trigger, not an
|
||||||
* inline link list in the title row (docs/2-handbook/agents/web-page.md). */
|
* inline link list in the title row (docs/4-implementation/web-ui/agent-page.md). */
|
||||||
pills?: ComponentChildren;
|
pills?: ComponentChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<body class="agent-shell">
|
<body class="agent-shell">
|
||||||
<!-- The Preact rewrite owns everything below — header, terminal,
|
<!-- The Preact rewrite owns everything below — header, terminal,
|
||||||
side panel. See main.tsx/Root.tsx for the component
|
side panel. See main.tsx/Root.tsx for the component
|
||||||
tree; docs/2-handbook/agents/web-page.md for the design. -->
|
tree; docs/4-implementation/web-ui/agent-page.md for the design. -->
|
||||||
<div id="preact-root"></div>
|
<div id="preact-root"></div>
|
||||||
|
|
||||||
<script type="module" src="static/main.js" defer></script>
|
<script type="module" src="static/main.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<script>
|
<script>
|
||||||
// Minimal RFB-over-WebSocket renderer. Connects to `screen/ws` on
|
// Minimal RFB-over-WebSocket renderer. Connects to `screen/ws` on
|
||||||
// the same host; the harness relays raw RFB bytes to the VNC server
|
// the same host; the harness relays raw RFB bytes to the VNC server
|
||||||
// running inside the container. See docs/2-handbook/agents/web-page.md (GET /screen)
|
// running inside the container. See docs/4-implementation/web-ui/agent-page.md (GET /screen)
|
||||||
// for the design rationale + canvas-sizing implementation notes.
|
// for the design rationale + canvas-sizing implementation notes.
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Fit-to-window toggle ---
|
// --- Fit-to-window toggle ---
|
||||||
// See docs/2-handbook/agents/web-page.md::Per-agent endpoints (GET /screen) for the
|
// See docs/4-implementation/web-ui/agent-page.md::Per-agent endpoints (GET /screen) for the
|
||||||
// localStorage persistence + canvas-intrinsic-resolution +
|
// localStorage persistence + canvas-intrinsic-resolution +
|
||||||
// pointer-rescale model.
|
// pointer-rescale model.
|
||||||
let fitMode = localStorage.getItem("screen-fit") !== "off";
|
let fitMode = localStorage.getItem("screen-fit") !== "off";
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
// Sends an RFB SetDesktopSize request so the VNC server (weston)
|
// Sends an RFB SetDesktopSize request so the VNC server (weston)
|
||||||
// changes its actual output resolution to match the browser
|
// changes its actual output resolution to match the browser
|
||||||
// viewport — sharper than fit-mode's CSS downscale. Gated on the
|
// viewport — sharper than fit-mode's CSS downscale. Gated on the
|
||||||
// server's ExtendedDesktopSize advert; see docs/2-handbook/agents/web-page.md::Per-agent
|
// server's ExtendedDesktopSize advert; see docs/4-implementation/web-ui/agent-page.md::Per-agent
|
||||||
// endpoints (GET /screen).
|
// endpoints (GET /screen).
|
||||||
let extDesktopSupported = false;
|
let extDesktopSupported = false;
|
||||||
let screenId = 1; // captured from the server's ExtendedDesktopSize advert
|
let screenId = 1; // captured from the server's ExtendedDesktopSize advert
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export interface AgentState {
|
||||||
// in the agent page header (stats/screen + any
|
// in the agent page header (stats/screen + any
|
||||||
// `services.hyperhive.agent.dashboardLinks` extras). `agent_links()` on the backend
|
// `services.hyperhive.agent.dashboardLinks` extras). `agent_links()` on the backend
|
||||||
// is the single source of truth for what links an agent exposes; see
|
// is the single source of truth for what links an agent exposes; see
|
||||||
// docs/2-handbook/agents/web-page.md::Header for the kind → URL resolution table.
|
// docs/4-implementation/web-ui/agent-page.md::Header for the kind → URL resolution table.
|
||||||
export interface AgentLink {
|
export interface AgentLink {
|
||||||
url: string;
|
url: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// 4G3NTS (containers) domain — extracted from tabs.js.
|
// 4G3NTS (containers) domain — extracted from tabs.js.
|
||||||
// Agent topology, container-row rendering, selection bar, and all
|
// Agent topology, container-row rendering, selection bar, and all
|
||||||
// live-update apply handlers for container-state, transient, and
|
// live-update apply handlers for container-state, transient, and
|
||||||
// job-queue-rollup ops. See docs/2-handbook/hives/dashboard.md::Container row for the
|
// job-queue-rollup ops. See docs/4-implementation/web-ui/dashboard.md::Container row for the
|
||||||
// rendering contract.
|
// rendering contract.
|
||||||
|
|
||||||
import { $, form, fmtAgeSecs } from "./common.js";
|
import { $, form, fmtAgeSecs } from "./common.js";
|
||||||
|
|
@ -26,14 +26,14 @@ const containerRowCache = new Map();
|
||||||
// by agent name -> Map<transient_kind, since_unix>: an agent can hold
|
// by agent name -> Map<transient_kind, since_unix>: an agent can hold
|
||||||
// several pills at once (every *running* node naming the agent lights
|
// several pills at once (every *running* node naming the agent lights
|
||||||
// one now, not just a curated "worth surfacing" subset — see
|
// one now, not just a curated "worth surfacing" subset — see
|
||||||
// docs/2-handbook/hives/dashboard.md::Container row), keyed the same way the backend's
|
// docs/4-implementation/web-ui/dashboard.md::Container row), keyed the same way the backend's
|
||||||
// own tombstone map is, `(agent, label)`, so a clear only removes the
|
// own tombstone map is, `(agent, label)`, so a clear only removes the
|
||||||
// specific pill it names rather than guessing. `since_unix` is
|
// specific pill it names rather than guessing. `since_unix` is
|
||||||
// wall-clock so the elapsed-seconds badge ticks without polling.
|
// wall-clock so the elapsed-seconds badge ticks without polling.
|
||||||
const transientsState = new Map();
|
const transientsState = new Map();
|
||||||
|
|
||||||
// In-memory set of selected agent logical names backing the sticky
|
// In-memory set of selected agent logical names backing the sticky
|
||||||
// #selection-bar. See docs/2-handbook/hives/dashboard.md::Selection bar for the
|
// #selection-bar. See docs/4-implementation/web-ui/dashboard.md::Selection bar for the
|
||||||
// interaction model (icon-click toggle, Esc/clear button drop,
|
// interaction model (icon-click toggle, Esc/clear button drop,
|
||||||
// tab-gated visibility).
|
// tab-gated visibility).
|
||||||
const selectionState = new Set();
|
const selectionState = new Set();
|
||||||
|
|
@ -214,7 +214,7 @@ function derivePortConflicts(containers) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── container ordering ──────────────────────────────────────────────────────
|
// ─── container ordering ──────────────────────────────────────────────────────
|
||||||
// Flat alphabetical sort — see docs/2-handbook/hives/dashboard.md::C0NTAINERS.
|
// Flat alphabetical sort — see docs/4-implementation/web-ui/dashboard.md::C0NTAINERS.
|
||||||
|
|
||||||
function sortedContainerRows(containers) {
|
function sortedContainerRows(containers) {
|
||||||
// Close any open context menu before replacing the DOM tree — the
|
// Close any open context menu before replacing the DOM tree — the
|
||||||
|
|
@ -272,7 +272,7 @@ function buildContainerLi(c, opts) {
|
||||||
// Agent icon: 5em square wrapper with an absolutely-positioned
|
// Agent icon: 5em square wrapper with an absolutely-positioned
|
||||||
// <img> + fire-and-forget load with /favicon.svg fallback. The
|
// <img> + fire-and-forget load with /favicon.svg fallback. The
|
||||||
// wrapper doubles as the selection toggle (click / keyboard).
|
// wrapper doubles as the selection toggle (click / keyboard).
|
||||||
// See docs/2-handbook/hives/dashboard.md::Container row for the layout + load-strategy
|
// See docs/4-implementation/web-ui/dashboard.md::Container row for the layout + load-strategy
|
||||||
// rationale.
|
// rationale.
|
||||||
const iconImg = el("img", { class: "container-icon-img", alt: "" });
|
const iconImg = el("img", { class: "container-icon-img", alt: "" });
|
||||||
const icon = el(
|
const icon = el(
|
||||||
|
|
@ -436,7 +436,7 @@ function buildContainerLi(c, opts) {
|
||||||
// dashboard-state fetch above.
|
// dashboard-state fetch above.
|
||||||
if (pending.length) {
|
if (pending.length) {
|
||||||
// One badge per pill — an agent can carry several transients at
|
// One badge per pill — an agent can carry several transients at
|
||||||
// once now (see docs/2-handbook/hives/dashboard.md::Container row), each rendered
|
// once now (see docs/4-implementation/web-ui/dashboard.md::Container row), each rendered
|
||||||
// independently rather than collapsed into one label.
|
// independently rather than collapsed into one label.
|
||||||
for (const label of pending) {
|
for (const label of pending) {
|
||||||
head.append(
|
head.append(
|
||||||
|
|
@ -506,7 +506,7 @@ function buildContainerLi(c, opts) {
|
||||||
|
|
||||||
// Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
|
// Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
|
||||||
// DESTR0Y / PURG3) live on the selection bar, not here — see
|
// DESTR0Y / PURG3) live on the selection bar, not here — see
|
||||||
// docs/2-handbook/hives/dashboard.md::Selection bar. The contextual `needs update ↻`
|
// docs/4-implementation/web-ui/dashboard.md::Selection bar. The contextual `needs update ↻`
|
||||||
// chip in the head row stays — it's a state-hint, not an
|
// chip in the head row stays — it's a state-hint, not an
|
||||||
// action button.
|
// action button.
|
||||||
|
|
||||||
|
|
@ -577,7 +577,7 @@ export function renderContainers(s) {
|
||||||
// TCP fallback — the gateway proxies the prefix to the per-agent
|
// TCP fallback — the gateway proxies the prefix to the per-agent
|
||||||
// harness (unix-domain via `agent-sockets.json`, or a computed TCP
|
// harness (unix-domain via `agent-sockets.json`, or a computed TCP
|
||||||
// loopback port while the socket marker is absent). See
|
// loopback port while the socket marker is absent). See
|
||||||
// `docs/2-handbook/hives/dashboard.md::Container row` + `docs/4-implementation/networking/gateway.md::Vhost map`.
|
// `docs/4-implementation/web-ui/dashboard.md::Container row` + `docs/4-implementation/networking/gateway.md::Vhost map`.
|
||||||
const gatewayLinks = !!(s && s.gateway_enabled);
|
const gatewayLinks = !!(s && s.gateway_enabled);
|
||||||
const ul = existingUl ?? el("ul", { class: "containers" });
|
const ul = existingUl ?? el("ul", { class: "containers" });
|
||||||
const rows = sortedContainerRows(containers);
|
const rows = sortedContainerRows(containers);
|
||||||
|
|
@ -597,7 +597,7 @@ export function renderContainers(s) {
|
||||||
// full stop (mara, on review: "remove the per agent pending stuff -
|
// full stop (mara, on review: "remove the per agent pending stuff -
|
||||||
// only show what is running"; rendered one badge per pill — mara,
|
// only show what is running"; rendered one badge per pill — mara,
|
||||||
// earlier: "show all running nodes that name the agent"). See
|
// earlier: "show all running nodes that name the agent"). See
|
||||||
// docs/2-handbook/hives/dashboard.md::Container row.
|
// docs/4-implementation/web-ui/dashboard.md::Container row.
|
||||||
const transientKindsMap = transientsState.get(c.name);
|
const transientKindsMap = transientsState.get(c.name);
|
||||||
// Sorted for stable badge order across renders — Map iteration
|
// Sorted for stable badge order across renders — Map iteration
|
||||||
// order is insertion order, which shifts as pills clear/re-add.
|
// order is insertion order, which shifts as pills clear/re-add.
|
||||||
|
|
@ -663,7 +663,7 @@ export function renderContainers(s) {
|
||||||
|
|
||||||
// ─── selection bar ───────────────────────────────────────────────────────────
|
// ─── selection bar ───────────────────────────────────────────────────────────
|
||||||
// Sticky-bottom strip; visible when >=1 agent selected on the 4G3NTS
|
// Sticky-bottom strip; visible when >=1 agent selected on the 4G3NTS
|
||||||
// tab. See docs/2-handbook/hives/dashboard.md::Selection bar for the interaction model
|
// tab. See docs/4-implementation/web-ui/dashboard.md::Selection bar for the interaction model
|
||||||
// and the per-action availability rules (disabled-with-tooltip for
|
// and the per-action availability rules (disabled-with-tooltip for
|
||||||
// actions that don't apply to the full selection). Actions POST per
|
// actions that don't apply to the full selection). Actions POST per
|
||||||
// agent in a loop (endpoints are individually idempotent /
|
// agent in a loop (endpoints are individually idempotent /
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ code {
|
||||||
}
|
}
|
||||||
/* M0V3 affordance (selection bar) — mauve reads as "structural
|
/* M0V3 affordance (selection bar) — mauve reads as "structural
|
||||||
change" rather than the destructive red / amber chrome of
|
change" rather than the destructive red / amber chrome of
|
||||||
destroy / rebuild. See docs/2-handbook/hives/dashboard.md::Selection bar. */
|
destroy / rebuild. See docs/4-implementation/web-ui/dashboard.md::Selection bar. */
|
||||||
.btn-move {
|
.btn-move {
|
||||||
color: var(--purple);
|
color: var(--purple);
|
||||||
border-color: var(--purple);
|
border-color: var(--purple);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
||||||
Top-of-page sticky header with banner + tab strip. SSE stays
|
Top-of-page sticky header with banner + tab strip. SSE stays
|
||||||
alive across tab switches. See docs/2-handbook/hives/dashboard.md::Chrome header + Tab strip for the
|
alive across tab switches. See docs/4-implementation/web-ui/dashboard.md::Chrome header + Tab strip for the
|
||||||
routing model. */
|
routing model. */
|
||||||
|
|
||||||
body.dashboard-shell {
|
body.dashboard-shell {
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
|
|
||||||
<!-- Selection action bar. Sticky-bottom strip that slides into
|
<!-- Selection action bar. Sticky-bottom strip that slides into
|
||||||
view when one or more agent cards is selected (click the icon
|
view when one or more agent cards is selected (click the icon
|
||||||
to toggle). See docs/2-handbook/hives/dashboard.md::Selection bar for the bulk
|
to toggle). See docs/4-implementation/web-ui/dashboard.md::Selection bar for the bulk
|
||||||
action gating + clear semantics. -->
|
action gating + clear semantics. -->
|
||||||
<div
|
<div
|
||||||
id="selection-bar"
|
id="selection-bar"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue