docs: full sync ahead of compaction + config-management overhaul

readme: manager mcp surface picks up update; operator-surface
recap mentions /model + last-turn + model chip + the three
collapsibles (inbox / journald / agent.nix).

web-ui.md: details-restore-key story under shape; port-conflict
banner mention on containers; agent.nix viewer alongside journald;
notifications use per-event tags + console.debug log on
block/show; deny endpoint takes note=<reason>; data-prompt /
data-prompt-field generalisation noted.

conventions.md: data-prompt and snapshot/restoreOpenDetails added
to the async-forms section.

persistence.md: operator_questions row picks up deadline_at (ttl)
column with a migration note.

todo.md: new 'Bugs' section captures the manager-question
not-rendering issue with three suspect paths to chase.

claude.md scratchpad rewritten as a clean handoff for the
compaction + the upcoming config-git overhaul. flags the
two-repo (proposed/ + applied/) split as the thing to
reconsider.
This commit is contained in:
müde 2026-05-15 22:12:40 +02:00
parent 6a2ffd521b
commit 75e7faff0c
6 changed files with 120 additions and 35 deletions

View file

@ -30,6 +30,16 @@ and, if so, skips the refresh (defers 2s). The operator never has
the form yanked out from under them mid-type; the update lands as
soon as they blur.
**`<details>` open-state preservation:** any collapsible element
tagged with `data-restore-key="<stable-key>"` survives the
refresh. `snapshotOpenDetails()` walks managed sections before
render, `restoreOpenDetails()` re-applies after. Used today for
the journald viewer (`journal:<container>`), the agent-config
viewer (`agent-config:<name>`), and approval diff blocks
(`approval-diff:<id>`). Setting `.open = true` programmatically
also fires the `toggle` event, so any lazy-fetch wired to it
re-runs cleanly on restore.
Both bind their listeners with `SO_REUSEADDR` via
`tokio::net::TcpSocket` plus a retry loop on `AddrInUse` (12 tries,
exponential backoff capped at 2s) so an nspawn restart that races
@ -42,6 +52,11 @@ the previous process's socket release resolves itself.
inline "unsupported / blocked" message when applicable. Sits
under the banner.
2. **C0NTAINERS** — live containers with their action surface.
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.
3. **K3PT ST4T3** — destroyed-but-state-kept tombstones (size +
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a
Spawn approval; existing state is reused), `PURG3` (wipes
@ -73,12 +88,16 @@ Two-line layout (`assets/app.js::renderContainers`):
on sub-agents, `↺ R3ST4RT` + (sub-agents) `■ ST0P` when running,
`▶ ST4RT` when stopped. Buttons dim + disable while a transient
lifecycle action is in flight.
- Plus a collapsible `↳ logs · <container>` `<details>` block.
Expanding lazy-fetches journald output via `GET
/api/journal/{name}?unit=...&lines=...` (`journalctl -M
<container> -b --no-pager --output=short-iso`). A unit dropdown
switches between the harness service (default) and the full
machine journal; refresh button re-fetches.
- Plus two collapsible `<details>` blocks:
- `↳ logs · <container>` — lazy-fetches journald output via
`GET /api/journal/{name}?unit=...&lines=...` (`journalctl -M
<container> -b --no-pager --output=short-iso`). A unit
dropdown switches between the harness service (default) and
the full machine journal; refresh button re-fetches.
- `↳ agent.nix · <name>` — lazy-fetches the applied config
file via `GET /api/agent-config/{name}` (read-only mirror of
`/var/lib/hyperhive/applied/<name>/agent.nix`). Mutating
this still requires `request_apply_commit` + approval.
`↻ UPD4TE 4LL` button appears above the containers list when any
agent is stale. Banner pulses on each broker SSE event
@ -94,15 +113,27 @@ Pure frontend (`Notification` API). Three signals trigger them:
First `/api/state` after page load seeds "seen" sets without
firing — only items that arrive while the page is open count.
`tag: "hyperhive"` collapses bursts; click focuses the dashboard
tab. localStorage-backed mute toggle silences without revoking
the OS permission. Requires a secure context (HTTPS or
localhost); on other origins the controls hide themselves.
Per-event tags (`hyperhive:approval:<id>`, `hyperhive:question:<id>`,
`hyperhive:msg:<at>:<rand>`) so distinct events stack in the OS
notification center instead of overwriting each other.
`console.debug` logs at every block point (unsupported,
permission ungranted, muted) for in-browser debugging. Click
focuses the dashboard tab. localStorage-backed mute toggle
silences without revoking the OS permission. Requires a secure
context (HTTPS or localhost); on other origins the controls hide
themselves. Browsers typically suppress notifications while the
originating tab is focused — that's a browser-level decision,
not ours.
### Dashboard endpoints
- `POST /{approve,deny}/{id}` — approve/deny a pending approval.
- `POST /approve/{id}` — approve a pending approval.
- `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`.
Dashboard prompts via `window.prompt()` on click.
- `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
`destroy` accepts `purge=on` to also wipe state dirs.
- `POST /purge-tombstone/{name}` — wipe a tombstone's state dirs.
- `POST /answer-question/{id}` — answer a pending operator question.
- `POST /cancel-question/{id}` — cancel a pending question with
@ -111,6 +142,13 @@ localhost); on other origins the controls hide themselves.
- `POST /update-all` — rebuild every stale container.
- `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`.
Generalised form helpers: `form[data-confirm="…"]` pops
`confirm()` before submit; `form[data-prompt="…"]` pops
`prompt()` and stashes the answer in a hidden input named by
`data-prompt-field` (default `note`).
## Per-agent page
@ -134,7 +172,9 @@ Layout, top to bottom:
POSTs `/api/cancel`.
- Inbox `<details>` block (collapsed): `inbox · N` — last 30
messages addressed to this agent, fetched via
`AgentRequest::Recent { limit: 30 }`.
`AgentRequest::Recent { limit: 30 }`. (Separate from
`AgentRequest::Recv { wait_seconds }` which the harness uses
internally to long-poll the broker.)
- Terminal-wrap: live event tail (sticky-bottom auto-scroll +
`↓ N new` pill when not at bottom) followed by an
operator-input textarea acting as a prompt.