diff --git a/CLAUDE.md b/CLAUDE.md index 4cf65892..ff3e1e61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,8 @@ hive-c0re/ host daemon + sibling operator CLI (lib + 2 bins) `QuestionAdded` / `QuestionResolved`, `TransientSet` / `TransientCleared`, `RebuildQueueChanged`, `SchedulesChanged`, - `RemindersChanged`). + `RemindersChanged`, `CapabilitiesChanged`, + `ToolGroupsChanged`). Each frame carries a monotonic per-process `seq` clients use to dedupe against snapshot reads. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index f02b23c0..232232fa 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -857,9 +857,21 @@ payload): reminder mutation: agent `remind` calls (`agent_server`), operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end` with Reminder kind, and the scheduler tick after each delivery - batch (`reminder_scheduler`). The SYST3M tab's reminders section + batch (`reminder_scheduler`). The SCH3DUL3S tab's reminders section subscribes and calls `renderReminders` on receipt, so the list updates live without polling. +- `capabilities_changed` (seq, caps: `Vec`, descriptions: map, + assignments: `BTreeMap>`) — full snapshot of + capability grants. Emitted from the rebuild-queue worker after a + `PermChange` / Capabilities entry commits the JSON file. Payload + matches `GET /api/capabilities` shape so `renderCapabilities` can + be called directly. P3RM1SS10NS tab subscribes; activation + re-fetch still runs as a safety net. +- `tool_groups_changed` (seq, groups: `Vec`, descriptions: map, + assignments: `BTreeMap>`) — full snapshot of + tool-group assignments. Emitted from the rebuild-queue worker after + a `PermChange` / ToolGroups entry commits the JSON file. Same + shape as `GET /api/tool-groups`; P3RM1SS10NS tab subscribes. `/api/state` is **only fetched on cold-load and on the few forms that mutate non-event-derived state** (PURG3 + diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index db49837f..2efbe319 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -1227,7 +1227,19 @@ window.marked = marked; // ── tool-groups (permissions) table ───────────────────────────────────── // Fetched from GET /api/tool-groups on system tab activation and after // each save. Groups (columns) come from the backend so the UI doesn't - // need updating when a new group is added. + // need updating when a new group is added. Live updates via + // `capabilities_changed` / `tool_groups_changed` SSE events fired + // after the rebuild-queue worker commits the perm JSON file. + function applyCapabilitiesChanged(ev) { + const root = $('capabilities-section'); + if (!root) return; + renderCapabilities(root, ev); + } + function applyToolGroupsChanged(ev) { + const root = $('tool-groups-section'); + if (!root) return; + renderToolGroups(root, ev); + } async function fetchAndRenderCapabilities() { const root = $('capabilities-section'); @@ -3607,6 +3619,8 @@ window.marked = marked; rebuild_queue_changed: applyRebuildQueueChanged, schedules_changed: applySchedulesChanged, reminders_changed: applyRemindersChanged, + capabilities_changed: applyCapabilitiesChanged, + tool_groups_changed: applyToolGroupsChanged, }; (function bindDashboardStream() { // Route through the SharedWorker so all open hyperhive tabs share @@ -3667,8 +3681,10 @@ window.marked = marked; // re-fetch reminders on SCH3DUL3S activation since both sections // live on the same tab. if (target === 'schedules') { refreshSchedules(); refreshReminders(); } - // Permissions tables (capabilities + tool-groups) have no SSE channel; - // fetch both on each activation so the operator sees fresh data. + // Permissions tables: SSE covers worker-applied changes + // (capabilities_changed / tool_groups_changed); re-fetch on + // activation as a safety net for any gap between SSE events and + // the cold-load snapshot. if (target === 'permissions') { fetchAndRenderCapabilities(); fetchAndRenderToolGroups(); diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 6a13c03c..c8260098 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -407,6 +407,44 @@ impl Coordinator { }); } + /// Emit a `CapabilitiesChanged` snapshot event. Called from the + /// rebuild-queue worker after a `PermChange` / Capabilities entry + /// commits the JSON file, so the P3RM1SS10NS tab updates live. + pub fn emit_capabilities_snapshot(self: &Arc) { + use hive_sh4re::Capability; + let caps = Capability::ALL.iter().map(|c| c.as_str()).collect(); + let descriptions = Capability::ALL + .iter() + .map(|c| (c.as_str(), c.description())) + .collect(); + let assignments = crate::capabilities::read(); + self.emit_dashboard_event(DashboardEvent::CapabilitiesChanged { + seq: self.next_seq(), + caps, + descriptions, + assignments, + }); + } + + /// Emit a `ToolGroupsChanged` snapshot event. Called from the + /// rebuild-queue worker after a `PermChange` / ToolGroups entry + /// commits the JSON file, so the P3RM1SS10NS tab updates live. + pub fn emit_tool_groups_snapshot(self: &Arc) { + use hive_sh4re::ToolGroup; + let groups = ToolGroup::ALL.iter().map(|g| g.as_str()).collect(); + let descriptions = ToolGroup::ALL + .iter() + .map(|g| (g.as_str(), g.description())) + .collect(); + let assignments = crate::tool_groups::read(); + self.emit_dashboard_event(DashboardEvent::ToolGroupsChanged { + seq: self.next_seq(), + groups, + descriptions, + assignments, + }); + } + /// Update the `step` label on a running queue entry and (if it /// actually changed) re-emit the queue snapshot so the dashboard /// renders the new phase. Returns `true` when the label was new diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index 818f452d..52bbf3c2 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -210,6 +210,32 @@ pub enum DashboardEvent { seq: u64, reminders: Vec, }, + /// Full snapshot of capability grants (per-agent `Vec`). + /// Emitted from the rebuild-queue worker after a `PermChange` + /// `Capabilities` entry commits the JSON file. Lets the P3RM1SS10NS + /// tab update live when the worker applies a queued change. + CapabilitiesChanged { + seq: u64, + /// Ordered list of all known capability names (column headers). + caps: Vec<&'static str>, + /// Short description for each capability name (tooltip). + descriptions: std::collections::BTreeMap<&'static str, &'static str>, + /// Per-agent capability grant map; absent agents have no extra caps. + assignments: std::collections::BTreeMap>, + }, + /// Full snapshot of tool-group assignments (per-agent `Vec`). + /// Emitted from the rebuild-queue worker after a `PermChange` + /// `ToolGroups` entry commits the JSON file. Lets the P3RM1SS10NS + /// tab update live when the worker applies a queued change. + ToolGroupsChanged { + seq: u64, + /// Ordered list of all known tool-group names (column headers). + groups: Vec<&'static str>, + /// Short description for each group name (tooltip). + descriptions: std::collections::BTreeMap<&'static str, &'static str>, + /// Per-agent assignment map; absent agents use the role default. + assignments: std::collections::BTreeMap>, + }, } impl DashboardEvent { @@ -242,6 +268,8 @@ impl DashboardEvent { DashboardEvent::RebuildQueueChanged { .. } => "rebuild_queue_changed", DashboardEvent::SchedulesChanged { .. } => "schedules_changed", DashboardEvent::RemindersChanged { .. } => "reminders_changed", + DashboardEvent::CapabilitiesChanged { .. } => "capabilities_changed", + DashboardEvent::ToolGroupsChanged { .. } => "tool_groups_changed", } } } @@ -368,6 +396,18 @@ mod tests { seq: 1, reminders: Vec::new(), }, + DashboardEvent::CapabilitiesChanged { + seq: 1, + caps: Vec::new(), + descriptions: std::collections::BTreeMap::new(), + assignments: std::collections::BTreeMap::new(), + }, + DashboardEvent::ToolGroupsChanged { + seq: 1, + groups: Vec::new(), + descriptions: std::collections::BTreeMap::new(), + assignments: std::collections::BTreeMap::new(), + }, ]; for ev in samples { let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise"); diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index e299b243..f415ac1d 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -770,11 +770,16 @@ async fn dispatch( crate::meta::commit_tool_groups(name, groups) .await .with_context(|| format!("commit tool-groups for {name}"))?; + // Emit after the commit so the P3RM1SS10NS tab + // reflects the new assignment without the operator + // needing to navigate away and back. + coord.emit_tool_groups_snapshot(); } Some(PermPayload::Capabilities { caps }) => { crate::meta::commit_capabilities(name, caps) .await .with_context(|| format!("commit capabilities for {name}"))?; + coord.emit_capabilities_snapshot(); } None => { anyhow::bail!(