docs: dashboard event channel, hive-fr0nt crate, mutation events, seq dedupe

This commit is contained in:
müde 2026-05-17 14:24:47 +02:00
parent 62784d4933
commit d8d393da6d
3 changed files with 167 additions and 30 deletions

View file

@ -19,8 +19,19 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
src/client.rs admin-socket client src/client.rs admin-socket client
src/manager_server.rs manager-privileged socket (ManagerRequest) src/manager_server.rs manager-privileged socket (ManagerRequest)
src/agent_server.rs per-sub-agent socket listener (long-poll Recv) src/agent_server.rs per-sub-agent socket listener (long-poll Recv)
src/broker.rs sqlite Message store + broadcast channel for SSE + src/broker.rs sqlite Message store + intra-process broadcast
hourly vacuum of delivered>30d channel (`MessageEvent`) for `recv_blocking` +
the dashboard forwarder; hourly vacuum of
delivered>30d
src/dashboard_events.rs unified wire-facing event channel feeding
`/dashboard/stream`. Carries broker `Sent` /
`Delivered` (mirrored by the forwarder task
in main.rs) + mutation events
(`ApprovalAdded` / `ApprovalResolved`,
`QuestionAdded` / `QuestionResolved`,
`TransientSet` / `TransientCleared`). Each
frame carries a monotonic per-process `seq`
clients use to dedupe against snapshot reads.
src/approvals.rs sqlite Approval queue + kinds src/approvals.rs sqlite Approval queue + kinds
src/operator_questions.rs sqlite question queue backing `ask` / src/operator_questions.rs sqlite question queue backing `ask` /
`answer` (both operator + agent-to-agent) `answer` (both operator + agent-to-agent)
@ -49,9 +60,26 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
(idempotent, marker-guarded phase 4) (idempotent, marker-guarded phase 4)
src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions
+ journald viewer + bind-with-retry (SO_REUSEADDR) + journald viewer + bind-with-retry (SO_REUSEADDR)
+ deployed_sha chip per container + deployed_sha chip per container +
/dashboard/{stream,history} subscribing to the
unified DashboardEvent channel
assets/ index.html, dashboard.css, app.js (include_str!) assets/ index.html, dashboard.css, app.js (include_str!)
hive-fr0nt/ shared frontend-assets crate (browser only).
src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS
re-exports; both binaries `include_str!` them
and prepend to their per-page serving routes.
assets/base.css Catppuccin palette + body typography (one source
of truth, no per-page redeclaration).
assets/terminal.css `.terminal-wrap` + `.live` + `.tail-pill` +
`.row` / `details.row` styling for both
pages' lit log panes.
assets/terminal.js `window.HiveTerminal.create(opts)`: scroll-
sticky log + "↓ N new" pill + history
backfill + SSE subscribe-buffer-snapshot-
dedupe dance. Pages register a kind→renderer
map; the terminal owns the lifecycle.
hive-ag3nt/ in-container harness crate; produces TWO binaries hive-ag3nt/ in-container harness crate; produces TWO binaries
src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT
src/client.rs generic JSON-line request/response over unix socket src/client.rs generic JSON-line request/response over unix socket
@ -165,6 +193,37 @@ Prune freely.
domain tooling — the agent flake's `inputs` block pulls domain tooling — the agent flake's `inputs` block pulls
the external flake, `agent.nix` references it via the external flake, `agent.nix` references it via
`flakeInputs.<name>.packages.${pkgs.system}.default`. `flakeInputs.<name>.packages.${pkgs.system}.default`.
- **Just landed:** dashboard event refactor. New `hive-fr0nt`
workspace crate hosts shared frontend assets (palette + terminal
CSS + `window.HiveTerminal.create` JS) so both the dashboard and
the per-agent web UI render their live panes through the same
code; the dashboard's `#msgflow` now feels like the agent's
terminal (sticky-bottom + pill + lit chrome). New unified
`DashboardEvent` channel on `Coordinator` (replaces the
broker-only `/messages/stream`); a background forwarder mirrors
broker traffic onto it as `Sent` / `Delivered` variants, and
the mutation-event variants
(`ApprovalAdded` / `ApprovalResolved`, `QuestionAdded` /
`QuestionResolved`, `TransientSet` / `TransientCleared`) cover
every in-process state change the dashboard cares about. Each
frame carries a monotonic per-process `seq`; snapshot endpoints
return their seq alongside the state, and the terminal's
open-buffer-then-fetch-history dance drops any buffered frame
with `seq <= history_seq` so an event landing between subscribe
and history-fetch is neither shown twice nor lost. Operator
inbox + approvals + questions + transients are now derived
client-side from the event stream (cold-loaded from
`/api/state` for first paint, mutated live from SSE
thereafter); `/op-send` + per-agent `/send` return 200 instead
of 303-and-refetch. Container-list events still pending —
`ContainerView` is sourced from external `nixos-container list`,
so the 5s `/api/state` poll continues to drive the containers
section. Approval diffs are now raw unified-diff text on the
wire (per-line classification happens in JS) so they fit in
SSE payloads without HTML escaping. Bug fix: `LiveEvent::Note`
was a newtype variant that serde silently failed to serialize
— converted to `Note { text: String }` (wire shape matches what
the JS already read).
- **Just landed:** `ask_operator``ask` rename + optional - **Just landed:** `ask_operator``ask` rename + optional
`to: <agent>` param for agent-to-agent structured Q&A. `to: <agent>` param for agent-to-agent structured Q&A.
Recipient defaults to the operator (dashboard); peer Recipient defaults to the operator (dashboard); peer

View file

@ -24,9 +24,12 @@ admin socket.
## Wire protocol ## Wire protocol
JSON line-delimited over unix sockets in both directions (host admin JSON line-delimited over unix sockets in both directions (host admin
/ manager / agent). SSE streams (`/messages/stream`, / manager / agent). SSE streams (`/dashboard/stream` on hive-c0re,
`/events/stream`) are `text/event-stream`. Request/response types `/events/stream` on the per-agent web UIs) are `text/event-stream`;
live in `hive-sh4re` — change them in one place. each frame carries a `seq` field for the snapshot-dedupe dance
(see `docs/web-ui.md`). Request/response types live in `hive-sh4re`
— change them in one place. The dashboard event vocabulary lives
in `hive-c0re::dashboard_events::DashboardEvent`.
## Async forms ## Async forms

View file

@ -10,10 +10,31 @@ and the per-agent UIs (manager on :8000, sub-agents on a hashed
- `GET /``assets/index.html` (placeholders for state-driven - `GET /``assets/index.html` (placeholders for state-driven
sections, shipped via `include_str!` so the binary has no runtime sections, shipped via `include_str!` so the binary has no runtime
file dependency). file dependency).
- `GET /static/*.css` + `GET /static/*.js` → static assets. - `GET /static/*.css` + `GET /static/*.js` → static assets. Both
- `GET /api/state` → JSON snapshot the JS app renders into the DOM. pages prepend `hive_fr0nt::BASE_CSS` + `TERMINAL_CSS` to their
- `GET /events/stream` (per-agent) / `GET /messages/stream` per-page stylesheet, and `GET /static/hive-fr0nt.js` serves the
(dashboard) → `text/event-stream` SSE for live updates. shared `window.HiveTerminal.create` runtime. The dashboard's
`#msgflow` and the per-agent `#live` log are both backed by
this terminal — sticky-bottom auto-scroll, "↓ N new" pill,
history backfill, SSE plumbing all live there. Each page
registers a kind→renderer map; unknown kinds fall through to
a JSON-dump note row.
- `GET /api/state` → JSON snapshot the JS app renders into the
DOM. Includes a top-level `seq` (the dashboard event channel's
high-water mark at the moment the snapshot was assembled);
clients use it to dedupe their buffered SSE traffic against
the snapshot (drop frames with `seq <= snapshot.seq`).
- `GET /dashboard/stream` (dashboard) / `GET /events/stream`
(per-agent) → `text/event-stream` SSE for live updates. The
dashboard stream carries broker `Sent` / `Delivered` (mirrored
by a forwarder task from the broker's intra-process channel)
plus mutation events (`approval_added` / `approval_resolved`,
`question_added` / `question_resolved`, `transient_set` /
`transient_cleared`). Each frame carries a `seq`. The
matching backfill endpoint is `GET /dashboard/history` (last
~200 broker messages wrapped in `{ seq, events }`) on the
dashboard and `GET /events/history` (last 2000 `LiveEvent`s
also wrapped in `{ seq, events }`) on the agent.
The JS app handles all `form[data-async]` submissions via a delegated The JS app handles all `form[data-async]` submissions via a delegated
listener: read `data-confirm`, swap the button to a spinner, POST listener: read `data-confirm`, swap the button to a spinner, POST
@ -71,26 +92,37 @@ the previous process's socket release resolves itself.
with `[cancelled]`. Questions with a `ttl_seconds` show a with `[cancelled]`. Questions with a `ttl_seconds` show a
`⏳ MM:SS` chip; the host-side watchdog auto-cancels with `⏳ MM:SS` chip; the host-side watchdog auto-cancels with
`[expired]` when the deadline fires. `[expired]` when the deadline fires.
5. **0PER4T0R 1NB0X** — recent messages addressed to `operator` 5. **0PER4T0R 1NB0X** — recent messages addressed to `operator`,
(last 50, from the broker). derived client-side from the dashboard event stream (no longer
a snapshot field). Cold load seeds from
`/dashboard/history`'s 200-message backfill; subsequent
`sent` events with `to == "operator"` are appended live. Cap
50, newest-first.
6. **P3NDING APPR0VALS** — the queue. The R3QU3ST SP4WN form 6. **P3NDING APPR0VALS** — the queue. The R3QU3ST SP4WN form
lives at the top of this section since submitting it lives at the top of this section since submitting it
immediately queues an approval that lands directly below. immediately queues an approval that lands directly below.
7. **MESS4GE FL0W** — live broker SSE tail (newest-first). 7. **MESS4GE FL0W** — live broker tail wrapped in a
Each row is one broker event — `sent` or `delivered` — with `.terminal-wrap` (same chrome as the per-agent terminal).
`from → to: body`; per-agent thinking / tool calls / claude Cold load backfills the last ~200 messages from
chatter stay out of this view, only what passes through `/dashboard/history`; live frames arrive on
hive-c0re's broker. Below the stream sits a terminal-style `/dashboard/stream` and dispatch through
compose box: `@name` picks the recipient (sticky across `HiveTerminal.create`. Each row is one broker event —
sends via localStorage; auto-complete from the live `sent` or `delivered` — with `from → to: body`; per-agent
container list, Tab/Enter to confirm), starting a message thinking / tool calls / claude chatter stay out of this
with `@<name> body` retargets in one stroke, plain text view, only what passes through hive-c0re's broker. Sticky-
sends to the sticky recipient. `POST /op-send` drops bottom auto-scroll + "↓ N new" pill match the per-agent
`{from:"operator", to, body}` into the broker — same shape page. Below the stream sits a terminal-style compose box:
any sub-agent sees as a regular inbox message. Manager is `@name` picks the recipient (sticky across sends via
addressed as `@manager` (the broker recipient string), not localStorage; auto-complete from the live container list,
`@hm1nd` (the container name); the auto-complete swaps Tab/Enter to confirm; `@*` broadcasts to every registered
automatically. agent), starting a message with `@<name> body` retargets
in one stroke, plain text sends to the sticky recipient.
`POST /op-send` drops `{from:"operator", to, body}` into
the broker and returns 200; the resulting SSE frame
re-renders both the terminal row and the inbox section
(no `/api/state` refetch). Manager is addressed as
`@manager` (the broker recipient string), not `@hm1nd`
(the container name); the auto-complete swaps automatically.
### Container row ### Container row
@ -143,10 +175,13 @@ not ours.
### Dashboard endpoints ### Dashboard endpoints
- `POST /approve/{id}` — approve a pending approval. - `POST /approve/{id}` — approve a pending approval. Fires
`ApprovalResolved` on the dashboard event channel; client
updates derived approvals state from the event.
- `POST /deny/{id}` (`note=<reason>`, optional) — deny a pending - `POST /deny/{id}` (`note=<reason>`, optional) — deny a pending
approval with an optional operator-supplied reason. The reason approval with an optional operator-supplied reason. The reason
travels to the manager as `HelperEvent::ApprovalResolved.note`. travels to the manager as `HelperEvent::ApprovalResolved.note`
and also rides on the dashboard's `ApprovalResolved` event.
Dashboard prompts via `window.prompt()` on click. Dashboard prompts via `window.prompt()` on click.
- `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle. - `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
`destroy` accepts `purge=on` to also wipe state dirs. `destroy` accepts `purge=on` to also wipe state dirs.
@ -157,12 +192,52 @@ not ours.
- `POST /request-spawn` — queue a Spawn approval. - `POST /request-spawn` — queue a Spawn approval.
- `POST /update-all` — rebuild every stale container. - `POST /update-all` — rebuild every stale container.
- `POST /op-send` (`to=<name>`, `body=<text>`) — drop an - `POST /op-send` (`to=<name>`, `body=<text>`) — drop an
operator-authored message into `<name>`'s inbox. Used by the operator-authored message into `<name>`'s inbox. `to=*` fans
out to every registered agent. Returns 200; the broker
`Sent` event re-renders both the message-flow terminal and
the operator inbox without a snapshot refetch. Used by the
compose textbox under MESS4GE FL0W. compose textbox under MESS4GE FL0W.
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for - `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
a managed container. a managed container.
- `GET /api/agent-config/{name}` — read-only view of the applied - `GET /api/agent-config/{name}` — read-only view of the applied
`agent.nix`. `agent.nix`.
- `GET /dashboard/stream` — unified live event channel:
broker `sent` / `delivered`, plus the mutation events listed
below. Each frame carries `seq`.
- `GET /dashboard/history` — last ~200 broker messages
(wrapped as `{ seq, events }`) for the message-flow
terminal's backfill on page load.
### Dashboard event channel
Wire vocabulary on `/dashboard/stream` (kind tag is in the JSON
payload):
- `sent` / `delivered` — broker traffic, mirrored from the
intra-process channel by a forwarder task. Used by the
message-flow terminal renderer and the operator-inbox
derived state.
- `approval_added` (id, agent, approval_kind, sha_short, diff,
description) / `approval_resolved` (id, agent, approval_kind,
sha_short, status, resolved_at, note, description) — pending
queue + history mutations. Client mutates a derived store and
re-renders only the approvals section.
- `question_added` (id, asker, question, options, multi,
asked_at, deadline_at) / `question_resolved` (id, answer,
answerer, answered_at, cancelled) — operator-targeted
questions only (peer-to-peer questions never fire these). The
ttl watchdog fires `question_resolved` with
`answerer = "ttl-watchdog"` on expiry.
- `transient_set` (name, transient_kind, since_unix) /
`transient_cleared` (name) — lifecycle action spinners. The
client ticks the elapsed-seconds badge off `since_unix`
client-side, no polling.
`/api/state` still serves `approvals` / `approval_history` /
`questions` / `question_history` / `transients` for cold-start
on first page load and as a safety-net resync from the 5s poll;
the client maintains the same arrays in derived stores and
applies the events on top.
Generalised form helpers: `form[data-confirm="…"]` pops Generalised form helpers: `form[data-confirm="…"]` pops
`confirm()` before submit; `form[data-prompt="…"]` pops `confirm()` before submit; `form[data-prompt="…"]` pops