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/manager_server.rs manager-privileged socket (ManagerRequest)
src/agent_server.rs per-sub-agent socket listener (long-poll Recv)
src/broker.rs sqlite Message store + broadcast channel for SSE +
hourly vacuum of delivered>30d
src/broker.rs sqlite Message store + intra-process broadcast
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/operator_questions.rs sqlite question queue backing `ask` /
`answer` (both operator + agent-to-agent)
@ -49,9 +60,26 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
(idempotent, marker-guarded phase 4)
src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions
+ 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!)
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
src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT
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
the external flake, `agent.nix` references it via
`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
`to: <agent>` param for agent-to-agent structured Q&A.
Recipient defaults to the operator (dashboard); peer

View file

@ -24,9 +24,12 @@ admin socket.
## Wire protocol
JSON line-delimited over unix sockets in both directions (host admin
/ manager / agent). SSE streams (`/messages/stream`,
`/events/stream`) are `text/event-stream`. Request/response types
live in `hive-sh4re` — change them in one place.
/ manager / agent). SSE streams (`/dashboard/stream` on hive-c0re,
`/events/stream` on the per-agent web UIs) are `text/event-stream`;
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

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
sections, shipped via `include_str!` so the binary has no runtime
file dependency).
- `GET /static/*.css` + `GET /static/*.js` → static assets.
- `GET /api/state` → JSON snapshot the JS app renders into the DOM.
- `GET /events/stream` (per-agent) / `GET /messages/stream`
(dashboard) → `text/event-stream` SSE for live updates.
- `GET /static/*.css` + `GET /static/*.js` → static assets. Both
pages prepend `hive_fr0nt::BASE_CSS` + `TERMINAL_CSS` to their
per-page stylesheet, and `GET /static/hive-fr0nt.js` serves the
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
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
`⏳ MM:SS` chip; the host-side watchdog auto-cancels with
`[expired]` when the deadline fires.
5. **0PER4T0R 1NB0X** — recent messages addressed to `operator`
(last 50, from the broker).
5. **0PER4T0R 1NB0X** — recent messages addressed to `operator`,
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
lives at the top of this section since submitting it
immediately queues an approval that lands directly below.
7. **MESS4GE FL0W** — live broker SSE tail (newest-first).
Each row is one broker event — `sent` or `delivered` — with
`from → to: body`; per-agent thinking / tool calls / claude
chatter stay out of this view, only what passes through
hive-c0re's broker. Below the stream sits a terminal-style
compose box: `@name` picks the recipient (sticky across
sends via localStorage; auto-complete from the live
container list, Tab/Enter to confirm), 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 — same shape
any sub-agent sees as a regular inbox message. Manager is
addressed as `@manager` (the broker recipient string), not
`@hm1nd` (the container name); the auto-complete swaps
automatically.
7. **MESS4GE FL0W** — live broker tail wrapped in a
`.terminal-wrap` (same chrome as the per-agent terminal).
Cold load backfills the last ~200 messages from
`/dashboard/history`; live frames arrive on
`/dashboard/stream` and dispatch through
`HiveTerminal.create`. Each row is one broker event —
`sent` or `delivered` — with `from → to: body`; per-agent
thinking / tool calls / claude chatter stay out of this
view, only what passes through hive-c0re's broker. Sticky-
bottom auto-scroll + "↓ N new" pill match the per-agent
page. Below the stream sits a terminal-style compose box:
`@name` picks the recipient (sticky across sends via
localStorage; auto-complete from the live container list,
Tab/Enter to confirm; `@*` broadcasts to every registered
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
@ -143,10 +175,13 @@ not ours.
### 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
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.
- `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
`destroy` accepts `purge=on` to also wipe state dirs.
@ -157,12 +192,52 @@ not ours.
- `POST /request-spawn` — queue a Spawn approval.
- `POST /update-all` — rebuild every stale container.
- `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.
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
a managed container.
- `GET /api/agent-config/{name}` — read-only view of the applied
`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
`confirm()` before submit; `form[data-prompt="…"]` pops