diff --git a/CLAUDE.md b/CLAUDE.md index 0b2bf157..c7db0ed7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,19 +19,8 @@ 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 + 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/broker.rs sqlite Message store + broadcast channel for SSE + + hourly vacuum of delivered>30d src/approvals.rs sqlite Approval queue + kinds src/operator_questions.rs sqlite question queue backing `ask` / `answer` (both operator + agent-to-agent) @@ -60,26 +49,9 @@ 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 + - /dashboard/{stream,history} subscribing to the - unified DashboardEvent channel + + deployed_sha chip per container 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 @@ -193,37 +165,6 @@ Prune freely. domain tooling — the agent flake's `inputs` block pulls the external flake, `agent.nix` references it via `flakeInputs..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: ` param for agent-to-agent structured Q&A. Recipient defaults to the operator (dashboard); peer diff --git a/TODO.md b/TODO.md index 54d5442a..7a44fdc4 100644 --- a/TODO.md +++ b/TODO.md @@ -8,11 +8,18 @@ - **Broadcast messaging**: allow sending messages with recipient "*" to all agents; deliver with hint "this was a broadcast and may not need any action from you" - **Multi-agent restart coordination**: when rebuilding all agents, manager should start first so it can coordinate post-restart confusion (notify agents, suppress unnecessary retries, etc) - **Shared docs/skills repo (RO)**: a single repo on the hive forge that every agent has read-only access to — common references, prompts, runbooks, "skills" the operator wants every agent to inherit without baking into the system prompt or `/shared`. Implementation likely: seed an `org-shared/docs` repo on first hive-forge boot, grant every per-agent user a read membership in the org. Agents `git clone` it (or use the API) to read; only the manager + operator can push. +- ~~**Rename `ask_operator` → `ask` with optional `to` param**~~ ✓ done — `Ask { question, options, multi, ttl_seconds, to: Option }` on both `AgentRequest` + `ManagerRequest`. `to = None` (or `Some("operator")`) = dashboard path; `to = Some()` pushes `HelperEvent::QuestionAsked` into the target's inbox. New `Answer { id, answer }` request on both surfaces — target answers via `mcp__hyperhive__answer`; answer flows back to the asker as `HelperEvent::QuestionAnswered { id, question, answer, answerer }` (renamed from `OperatorAnswered`; carries who answered so the asker can distinguish operator vs peer vs `ttl-watchdog`). Authorisation: only the question's `target` agent or the operator can answer; self-ask is rejected. DB gets a nullable `target` column (NULL = operator path, back-compat). Dashboard's `pending()` / `recent_answered()` filter on `target IS NULL` so peer questions never leak into the operator's queue. Shared dispatch lives in `hive-c0re/src/questions.rs` so both surfaces stay aligned. - **Loose-ends tracker + `get_open_threads` tool**: hive-c0re already knows about pending approvals + unanswered questions; soon will also know about open PRs on hive-forge. Aggregate these into a per-agent "open threads" view (e.g. `[{kind: "approval", id: 7, summary: "spawn alice"}, {kind: "question", id: 12, asker: "alice", summary: "deploy now?"}]`). New MCP tool `mcp__hyperhive__get_open_threads` returns the list so an agent can see what's still pending against it without rebuilding context from inbox history. Manager's version includes hive-wide threads. **Also surface this list on the per-agent web UI** so the operator can see at a glance what each agent has hanging open — same data source as the MCP tool, just rendered into the existing per-agent dashboard page (next to inbox view / model chip / etc). ## Reminder Tool +- ~~Handle text overflow → suggest file_path option for long messages~~ ✓ fixed — Remind dispatch rejects `message.len() > 4096` (when no `file_path` was supplied) with an error pointing at the `file_path` escape hatch. - Per-agent reminder limits (burst capacity, rate limiting) +- ~~**Expose `remind` MCP tool**~~ ✓ fixed — `mcp__hyperhive__remind` now on `AgentServer`; takes `message`, exactly one of `delay_seconds` / `at_unix_timestamp`, optional `file_path`. Manager surface still missing (no `ManagerRequest::Remind` variant) — separate item below. +- ~~**Manager-side `remind`**~~ ✓ fixed — `ManagerRequest::Remind` variant added, dispatch reuses `agent_server::store_remind` helper (shared across both surfaces), `mcp__hyperhive__remind` now on `ManagerServer` (auto-file lands at `/state/reminders/auto-.md` — manager's legacy state mount). +- ~~**File path delivery**~~ ✓ fixed — scheduler now writes the reminder body to the requested `file_path` (mapped from container `/agents//state/...` to host `/var/lib/hyperhive/agents//state/...`) and delivers a short pointer message in its place. Path-traversal + foreign-agent-state writes are rejected; on rejection or write failure the body falls back to inline delivery with a noted warning. New module `hive-c0re/src/reminder_scheduler.rs` (extracted from main.rs). +- ~~**Orphan reminders**~~ ✓ fixed — `Broker::deliver_reminder` wraps the inbox INSERT + reminders UPDATE in one sqlite transaction; partial failure can no longer cause duplicate delivery on the next tick. +- ~~**Unbounded batches**~~ ✓ fixed — scheduler now calls `get_due_reminders(REMINDER_BATCH_LIMIT)` (cap = 100/tick); overflow stays due and gets picked up next cycle. - **Scheduler shutdown**: add graceful shutdown signal when coordinator is destroyed (currently runs forever) - **DB lock contention**: under high reminder volume, the broker's `Mutex` serializes every delivery transaction. Consider batching multiple deliveries into one tx, or moving reminders onto a separate sqlite connection. @@ -24,8 +31,10 @@ - Per-agent reminder status (pending, delivered) - Reminder query interface for debugging - Display reminder delivery errors (failed sends, mark failures) -- **Phase 6 leftovers** — event-covered endpoints (`/approve`, `/deny`, `/answer-question`, `/cancel-question`, `/request-spawn`) now return 200 (f559441); the matching forms carry `data-no-refresh` so the post-submit `/api/state` refetch is skipped (the SSE event delivers the update). Container-lifecycle endpoints (`/restart`, `/destroy`, `/kill`, `/rebuild`, `/start`, `/api/{cancel,compact,model,new-session}`, `/meta-update`, `/purge-tombstone`) still need a `ContainerListChanged` event before their redirects can drop — `ContainerView` is currently sourced from external `nixos-container list`, so the 5s poll continues to drive that section. +- ~~**Phase 5b: per-domain mutation event types + client derived state**~~ ✓ landed across 56d615b (approvals), 1879b2f (questions), 7956e1c (transients). `DashboardEvent` now carries `ApprovalAdded` / `ApprovalResolved`, `QuestionAdded` / `QuestionResolved`, `TransientSet` / `TransientCleared`; emit sites cover `actions::approve`/`deny`/`finish_approval`, dashboard's orphan-approval GC, manager-socket `request_spawn` + `request_apply_commit` (success + git_fetch failure), `questions::handle_ask`/`handle_answer` (operator-targeted only), dashboard's `/answer-question` + `/cancel-question`, ttl-watchdog, `Coordinator::set_transient`/`clear_transient`. `/api/state` still serves these arrays for cold-start; live updates flow through the events. Container-list events still deferred — `ContainerView` is sourced from external `nixos-container list`, so the 5s poll continues to drive `/containers-section`. Phase 6 remaining redirect conversions (`/approve`, `/deny`, `/restart`, `/destroy`, `/kill`, `/rebuild`, `/api/cancel`, `/api/compact`, `/api/model`, `/api/new-session`, `/request-spawn`, `/answer-question`, `/cancel-question`, `/meta-update`, `/purge-tombstone`) are now unblocked for the event-covered domains; container-lifecycle ones still need either container-list events or to live with the 5s poll-refresh delay. ## Bugs +- ~~**Pending message wake-up**~~ ✓ fixed (e423d57) — subscribe-before-check race in `broker.recv_blocking` meant a send landing between the initial `recv()` and `subscribe()` was missed; agent then sat on the 180s long-poll until another, unrelated message woke it. Now subscribe first. - **Post-rebuild system-message missed wake**: at 09:13:14 the dashboard showed `system → damocles container rebuilt` as ✓ delivered, but the agent harness never ran a turn for it (no claude invocation, no operator-visible activity). A subsequent `recv()` from inside the agent returned `(empty)`, confirming the message was popped + marked delivered server-side — yet drove no turn. Most likely cause: the agent_server `serve_agent_stdio` task is up and answering MCP/socket calls, but the `hive-ag3nt::serve` long-poll loop that drives `drive_turn` either died silently during rebuild or never restarted. Investigate: (a) does hive-ag3nt's serve loop survive `nixos-container update` cleanly, or does its tokio runtime get torn down mid-loop? (b) is there an early-exit path on a transient socket error during rebuild that drops the serve task without notifying the manager? (c) compare timeline with manager's own post-rebuild wake to see if this is rebuilt-agents-only or universal. Could be related to the `recv_blocking` fix in `e423d57` if the rebuild restarts the broker mid-subscribe. +- ~~**`LiveEvent::Note(String)` never reaches the browser**~~ ✓ fixed — converted to struct variant `Note { text: String }`; wire shape `{"kind":"note","text":"..."}` matches what the JS already reads via `ev.text`. Historical sqlite rows persisted as the literal string `"null"` (from when serialization silently failed) get filtered out by the `rows.flatten().flatten()` pipeline in `EventStore::recent`, so replay tolerates them. diff --git a/docs/conventions.md b/docs/conventions.md index a72c6e34..8a709ec4 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -24,12 +24,9 @@ admin socket. ## Wire protocol JSON line-delimited over unix sockets in both directions (host admin -/ 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`. +/ manager / agent). SSE streams (`/messages/stream`, +`/events/stream`) are `text/event-stream`. Request/response types +live in `hive-sh4re` — change them in one place. ## Async forms diff --git a/docs/web-ui.md b/docs/web-ui.md index 79c9ca2f..c00f5483 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -10,31 +10,10 @@ 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. 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. +- `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. The JS app handles all `form[data-async]` submissions via a delegated listener: read `data-confirm`, swap the button to a spinner, POST @@ -92,37 +71,26 @@ 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`, - 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. +5. **0PER4T0R 1NB0X** — recent messages addressed to `operator` + (last 50, from the broker). 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 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 `@ 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. +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 `@ 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. ### Container row @@ -175,13 +143,10 @@ not ours. ### Dashboard endpoints -- `POST /approve/{id}` — approve a pending approval. Fires - `ApprovalResolved` on the dashboard event channel; client - updates derived approvals state from the event. +- `POST /approve/{id}` — approve a pending approval. - `POST /deny/{id}` (`note=`, optional) — deny a pending approval with an optional operator-supplied reason. The reason - travels to the manager as `HelperEvent::ApprovalResolved.note` - and also rides on the dashboard's `ApprovalResolved` event. + 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. @@ -192,52 +157,12 @@ not ours. - `POST /request-spawn` — queue a Spawn approval. - `POST /update-all` — rebuild every stale container. - `POST /op-send` (`to=`, `body=`) — drop an - operator-authored message into ``'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 + operator-authored message into ``'s inbox. 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 diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 4d247a73..5fd00305 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -22,14 +22,10 @@ } return e; }; - const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = {}) => { + const form = (action, btnClass, btnLabel, confirmMsg, extra = {}) => { const f = el('form', { method: 'POST', action, class: 'inline', 'data-async': '', ...(confirmMsg ? { 'data-confirm': confirmMsg } : {}), - // Endpoints whose mutation fires a DashboardEvent (and whose - // derived store applies it live) opt out of the post-submit - // /api/state refetch. See the async-form handler. - ...(opts.noRefresh ? { 'data-no-refresh': '' } : {}), }); for (const [name, value] of Object.entries(extra)) { f.append(el('input', { type: 'hidden', name, value })); @@ -199,15 +195,7 @@ if (btn) { btn.disabled = false; btn.innerHTML = original; } // Clear text inputs whose value was just submitted. f.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((i) => { i.value = ''; }); - // Forms whose endpoint already emits a DashboardEvent that - // updates the derived store can opt out of the post-submit - // /api/state refetch (the event delivers the new row faster - // than the snapshot poll anyway). Container-lifecycle forms - // still rely on the refresh since `ContainerView` isn't yet - // event-derivable. - if (!f.hasAttribute('data-no-refresh')) { - refreshState(); - } + refreshState(); } catch (err) { alert('action failed: ' + err); if (btn) { btn.disabled = false; btn.innerHTML = original; } @@ -599,7 +587,7 @@ li.append(head, el('div', { class: 'q-body' }, q.question)); const f = el('form', { method: 'POST', action: '/answer-question/' + q.id, - class: 'qform', 'data-async': '', 'data-no-refresh': '', + class: 'qform', 'data-async': '', }); const hasOptions = q.options && q.options.length; const isMulti = !!q.multi && hasOptions; @@ -650,7 +638,7 @@ // merge-on-submit handler attached to the main form. const cancelForm = el('form', { method: 'POST', action: '/cancel-question/' + q.id, - class: 'qform-cancel', 'data-async': '', 'data-no-refresh': '', + class: 'qform-cancel', 'data-async': '', 'data-confirm': 'cancel this question? manager will see ' + '"[cancelled]" as the answer.', }); @@ -780,7 +768,7 @@ // the containers list (the agent doesn't exist yet). const spawn = el('form', { method: 'POST', action: '/request-spawn', - class: 'spawnform', 'data-async': '', 'data-no-refresh': '', + class: 'spawnform', 'data-async': '', }); spawn.append( el('input', { @@ -863,13 +851,13 @@ // HelperEvent::ApprovalResolved { note }. const denyForm = el('form', { method: 'POST', action: '/deny/' + a.id, - class: 'inline', 'data-async': '', 'data-no-refresh': '', + class: 'inline', 'data-async': '', 'data-prompt': 'reason for denying (optional, sent to manager):', }); denyForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, 'DENY')); row.append( ' ', - form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE', null, {}, { noRefresh: true }), + form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE'), ' ', denyForm, ); diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 8054fff6..f5720e91 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -783,11 +783,7 @@ async fn dashboard_stream( async fn post_approve(State(state): State, AxumPath(id): AxumPath) -> Response { match actions::approve(state.coord.clone(), id).await { - // 200 instead of 303 — `actions::approve` fires - // `ApprovalResolved` (success path) or the eventual failure - // event, both of which the dashboard's derived store applies - // live. The matching form carries `data-no-refresh`. - Ok(()) => (StatusCode::OK, "ok").into_response(), + Ok(()) => Redirect::to("/").into_response(), Err(e) => error_response(&format!("approve {id} failed: {e:#}")), } } @@ -809,7 +805,7 @@ async fn post_deny( .map(str::trim) .filter(|s| !s.is_empty()); match actions::deny(&state.coord, id, note).await { - Ok(()) => (StatusCode::OK, "ok").into_response(), + Ok(()) => Redirect::to("/").into_response(), Err(e) => error_response(&format!("deny {id} failed: {e:#}")), } } @@ -857,7 +853,7 @@ async fn post_answer_question( false, ); } - (StatusCode::OK, "ok").into_response() + Redirect::to("/").into_response() } Err(e) => error_response(&format!("answer {id} failed: {e:#}")), } @@ -898,7 +894,7 @@ async fn post_cancel_question( answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), }, ); - (StatusCode::OK, "ok").into_response() + Redirect::to("/").into_response() } Err(e) => error_response(&format!("cancel-question {id} failed: {e:#}")), } @@ -1203,7 +1199,7 @@ async fn post_request_spawn( state .coord .emit_approval_added(id, &name, "spawn", None, None, None); - (StatusCode::OK, "ok").into_response() + Redirect::to("/").into_response() } Err(e) => error_response(&format!("request-spawn {name} failed: {e:#}")), }