diff --git a/docs/approvals.md b/docs/approvals.md index e587add0..7f5149aa 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -187,49 +187,6 @@ approval id to retry. Because tags are first-class git objects, rejected and failed trees stay browsable forever — `git log --tags` in the applied repo is the audit trail. -### Dispatch via `rebuild_queue` (#441) - -Long-running approval work — `ApplyCommit`, `UpdateMetaInputs`, -`Spawn` — no longer runs inline inside `actions::approve`. Instead -the approval handler enqueues a `QueueEntry` into the global -`rebuild_queue`: - -| `ApprovalKind` | `QueueKind` queued | `QueueSource` | -|---|---|---| -| `ApplyCommit` | `Rebuild` | `Approval` | -| `UpdateMetaInputs` | `MetaUpdate` | `Approval` | -| `Spawn` | `Spawn` | `Approval` | -| `InitConfig` | — runs inline (sub-second git seed) | — | -| `SchedulePrompt` | — runs inline (single sqlite insert) | — | - -Each queue entry carries the originating `approval_id` so the -worker can re-fetch the approval row when it dispatches, run the -kind-specific pipeline (`run_approval_apply_commit` / -`run_approval_update_meta_inputs` / `run_approval_spawn`), and -fire the matching `HelperEvent::*` on completion via -`finish_approval`. - -Two visible consequences: - -- **Operator dashboard**: after clicking APPR0VE the work-in-progress - shows up on the *rebuild queue* card (`POST /api/state.rebuild_queue` - + live `rebuild_queue_changed` events), not on the approvals panel - (which already moved the row to "approved"). A long meta-update - cascade renders as a parent entry with one child per per-agent - rebuild — see `docs/web-ui.md` for the layout. -- **Cancellation**: the dashboard's *× cancel* button on a `Queued` - entry calls `POST /api/rebuild-queue/{id}/cancel`, which flips the - entry to `Cancelled` before the worker dispatches it. Returns - `{"cancelled": true}` on success, `{"cancelled": false}` if the - entry already left `Queued` (running / done / failed) — terminal - states can't be retroactively rewritten. - -`QueueSource::Approval` carries the `approval_id` so a tail-end -build failure surfaces back as a failed approval row, not just a -silent queue entry. `QueueSource::Manual` (dashboard ↻ R3BU1LD) -and `QueueSource::AutoUpdate` (boot-time sweep) use the same -queue but skip the approval row plumbing. - ### Forge mirror When the bundled `hive-forge` container is running — on by diff --git a/docs/gotchas.md b/docs/gotchas.md index 32537c3d..f41c9c90 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -133,8 +133,6 @@ hive-forge assign 42 damocles hive-forge close 42 hive-forge labels 42 add feature hive-forge pr 42 # PR metadata as JSON -hive-forge diff 42 # unified diff (lockfile hunks collapsed by default) -hive-forge diff 42 --full # include unfiltered lockfile hunks hive-forge branches deployed/ # filter branches by pattern hive-forge -r other-org/other-repo pr 7 # target a different repo ``` diff --git a/frontend/packages/dashboard/src/flow.js b/frontend/packages/dashboard/src/flow.js index d4b1c9eb..eefe8710 100644 --- a/frontend/packages/dashboard/src/flow.js +++ b/frontend/packages/dashboard/src/flow.js @@ -187,20 +187,10 @@ import { logEl: flow, pillAnchor: flowMain, historyUrl: '/dashboard/history', - // #408: server-side filter — only the kinds this page actually - // renders or routes (sent/delivered → broker terminal, - // container_state_changed/_removed → local autocomplete cache). - // Backend (#499) pre-parses the allow-list at subscribe time so - // the per-frame hot path is one HashSet::contains and the - // JSON-serialise is skipped entirely on irrelevant kinds. The - // dashboard tabs page (tabs.js) keeps the unfiltered subscribe - // since it routes every mutation kind into its derived stores. - streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed', + streamUrl: '/dashboard/stream', // #448: route through the SharedWorker so this page's SSE shares // a single backend connection with /index.html (and any other - // open hyperhive tab). Worker keys on the full URL (incl. - // query string), so the filtered subscribe is its own upstream - // — won't accidentally share with tabs.js's wider subscribe. + // open hyperhive tab). streamFactory: openStream, renderers: { sent: (ev, api) => renderMsg(ev, api, '→'), diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 86ef4f6d..956f9ae9 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -798,49 +798,14 @@ async fn dashboard_history(State(state): State) -> Response { } } -/// `/dashboard/stream` query string. Today's only field is `kinds` -/// (#408): a comma-separated allow-list of event-`kind` strings. -/// Empty / absent ⇒ no filter (current behaviour, all variants -/// forwarded). Set ⇒ only the named kinds reach the subscriber, -/// non-matches are skipped before the JSON serialise cost. -/// -/// Useful for narrow pages (e.g. `flow.js` only cares about `sent` -/// / `delivered` / `container_state_changed` / `container_removed`) -/// that want to drop the dispatch overhead on every unrelated mutation. -#[derive(Deserialize, Default)] -struct DashboardStreamQuery { - /// Comma-separated event kinds to forward. Each token is - /// trimmed; unknown kinds are silently ignored on lookup - /// (subscriber sees nothing instead of an error). - kinds: Option, -} - async fn dashboard_stream( State(state): State, - axum::extract::Query(q): axum::extract::Query, ) -> Sse>> { let rx = state.coord.dashboard_subscribe(); - // Pre-parse the allow-list once at subscription time, so the - // per-event hot path is just a `HashSet::contains` on a - // `&'static str` — no string churn per frame. - let kind_filter: Option> = q.kinds.and_then(|raw| { - let set: std::collections::HashSet = raw - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_owned) - .collect(); - if set.is_empty() { None } else { Some(set) } - }); - let stream = BroadcastStream::new(rx).filter_map(move |res| { + let stream = BroadcastStream::new(rx).filter_map(|res| { // Drop lagged frames. Browsers reconnect; the seq dedupe on // reconnect skips any frame already reflected in the snapshot. let event = res.ok()?; - if let Some(filter) = kind_filter.as_ref() - && !filter.contains(event.kind_tag()) - { - return None; - } let json = serde_json::to_string(&event).ok()?; Some(Ok(Event::default().data(json))) }); diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index 0ae794a8..fcea9c78 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -218,160 +218,3 @@ pub enum DashboardEvent { queue: Vec, }, } - -impl DashboardEvent { - /// Snake-case identifier matching this variant's serde `tag` - /// (e.g. `Sent` → `"sent"`, `ContainerStateChanged` → - /// `"container_state_changed"`). Lets `/dashboard/stream`'s - /// `?kinds=` filter (#408) decide whether to forward a frame - /// without paying the JSON-serialise cost first. - /// - /// Keep in sync with `#[serde(rename_all = "snake_case", tag = - /// "kind")]` on `DashboardEvent` — if a new variant lands above, - /// add it here too. `cargo test` covers this via the - /// `kind_tag_matches_serde_kind_field` round-trip test. - #[must_use] - pub fn kind_tag(&self) -> &'static str { - match self { - DashboardEvent::Sent { .. } => "sent", - DashboardEvent::Delivered { .. } => "delivered", - DashboardEvent::ApprovalAdded { .. } => "approval_added", - DashboardEvent::ApprovalResolved { .. } => "approval_resolved", - DashboardEvent::QuestionAdded { .. } => "question_added", - DashboardEvent::QuestionResolved { .. } => "question_resolved", - DashboardEvent::TransientSet { .. } => "transient_set", - DashboardEvent::TransientCleared { .. } => "transient_cleared", - DashboardEvent::ContainerStateChanged { .. } => "container_state_changed", - DashboardEvent::ContainerRemoved { .. } => "container_removed", - DashboardEvent::TombstonesChanged { .. } => "tombstones_changed", - DashboardEvent::MetaInputsChanged { .. } => "meta_inputs_changed", - DashboardEvent::MetaUpdateRunning { .. } => "meta_update_running", - DashboardEvent::RebuildQueueChanged { .. } => "rebuild_queue_changed", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Round-trip representative variants through serde and confirm - /// the `kind` JSON field matches `kind_tag()`. The exhaustive - /// `match` in `kind_tag` already provides compile-time variant - /// coverage — this test is the value-side guard against - /// typos in the snake_case strings vs serde's `rename_all` - /// output. `ContainerStateChanged` is omitted from the sample - /// list only because `ContainerView` has no `Default` impl and - /// constructing one inline here is more boilerplate than the - /// test is worth; the variant is still covered by the - /// `kind_tag` match arm. - #[test] - fn kind_tag_matches_serde_kind_field() { - let samples: Vec = vec![ - DashboardEvent::Sent { - seq: 1, - id: 1, - from: "a".into(), - to: "b".into(), - body: String::new(), - at: 0, - in_reply_to: None, - file_refs: Vec::new(), - }, - DashboardEvent::Delivered { - seq: 1, - id: 1, - from: "a".into(), - to: "b".into(), - body: String::new(), - at: 0, - in_reply_to: None, - file_refs: Vec::new(), - }, - DashboardEvent::ApprovalAdded { - seq: 1, - id: 1, - agent: "x".into(), - approval_kind: "apply_commit", - sha_short: None, - diff: None, - description: None, - }, - DashboardEvent::ApprovalResolved { - seq: 1, - id: 1, - agent: "x".into(), - approval_kind: "apply_commit", - sha_short: None, - status: "approved", - resolved_at: 0, - note: None, - description: None, - }, - DashboardEvent::QuestionAdded { - seq: 1, - id: 1, - asker: "a".into(), - question: String::new(), - options: Vec::new(), - multi: false, - asked_at: 0, - deadline_at: None, - target: None, - question_refs: Vec::new(), - }, - DashboardEvent::QuestionResolved { - seq: 1, - id: 1, - answer: String::new(), - answerer: "a".into(), - answered_at: 0, - cancelled: false, - target: None, - answer_refs: Vec::new(), - }, - DashboardEvent::TransientSet { - seq: 1, - name: "x".into(), - transient_kind: "rebuilding", - since_unix: 0, - }, - DashboardEvent::TransientCleared { - seq: 1, - name: "x".into(), - }, - DashboardEvent::ContainerRemoved { - seq: 1, - name: "x".into(), - }, - DashboardEvent::TombstonesChanged { - seq: 1, - tombstones: Vec::new(), - }, - DashboardEvent::MetaInputsChanged { - seq: 1, - inputs: Vec::new(), - }, - DashboardEvent::MetaUpdateRunning { - seq: 1, - running: false, - }, - DashboardEvent::RebuildQueueChanged { - seq: 1, - queue: Vec::new(), - }, - ]; - for ev in samples { - let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise"); - let serde_kind = v - .get("kind") - .and_then(|k| k.as_str()) - .expect("kind field present"); - assert_eq!( - ev.kind_tag(), - serde_kind, - "kind_tag() drift on {ev:?}", - ); - } - } -}