feat: live SSE updates for P3RM1SS10NS tab (capabilities + tool groups)

Add CapabilitiesChanged and ToolGroupsChanged DashboardEvent variants
so the P3RM1SS10NS tab reflects perm changes without the operator
navigating away and back.

Backend:
- DashboardEvent::CapabilitiesChanged { seq, caps, descriptions,
  assignments } — same payload shape as GET /api/capabilities
- DashboardEvent::ToolGroupsChanged { seq, groups, descriptions,
  assignments } — same payload shape as GET /api/tool-groups
- Coordinator::emit_capabilities_snapshot() and
  emit_tool_groups_snapshot() — read from the JSON files and broadcast
- rebuild_queue.rs PermChange worker: emit after each successful
  commit_capabilities / commit_tool_groups call

Frontend:
- applyCapabilitiesChanged(ev): calls renderCapabilities(root, ev)
- applyToolGroupsChanged(ev): calls renderToolGroups(root, ev)
- Both registered in MUTATION_HANDLERS
- activateTab comment updated (SSE now covers perm changes)

Docs: dashboard.md and CLAUDE.md updated.

This completes SSE coverage for all dashboard sections: SW4RM,
Y3R C4LL, SYST3M, SCH3DUL3S/reminders, and P3RM1SS10NS all
derive live updates from /dashboard/stream.
This commit is contained in:
iris 2026-06-05 10:21:37 +02:00 committed by mara
commit 2080fd3866
6 changed files with 117 additions and 5 deletions

View file

@ -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<Self>) {
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<Self>) {
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

View file

@ -210,6 +210,32 @@ pub enum DashboardEvent {
seq: u64,
reminders: Vec<crate::broker::PendingReminder>,
},
/// Full snapshot of capability grants (per-agent `Vec<cap_name>`).
/// 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<String, Vec<String>>,
},
/// Full snapshot of tool-group assignments (per-agent `Vec<group_name>`).
/// 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<String, Vec<String>>,
},
}
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");

View file

@ -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!(