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:
parent
d0b038e17d
commit
2080fd3866
6 changed files with 117 additions and 5 deletions
|
|
@ -83,7 +83,8 @@ hive-c0re/ host daemon + sibling operator CLI (lib + 2 bins)
|
||||||
`QuestionAdded` / `QuestionResolved`,
|
`QuestionAdded` / `QuestionResolved`,
|
||||||
`TransientSet` / `TransientCleared`,
|
`TransientSet` / `TransientCleared`,
|
||||||
`RebuildQueueChanged`, `SchedulesChanged`,
|
`RebuildQueueChanged`, `SchedulesChanged`,
|
||||||
`RemindersChanged`).
|
`RemindersChanged`, `CapabilitiesChanged`,
|
||||||
|
`ToolGroupsChanged`).
|
||||||
Each frame carries a
|
Each frame carries a
|
||||||
monotonic per-process `seq` clients use to
|
monotonic per-process `seq` clients use to
|
||||||
dedupe against snapshot reads.
|
dedupe against snapshot reads.
|
||||||
|
|
|
||||||
|
|
@ -857,9 +857,21 @@ payload):
|
||||||
reminder mutation: agent `remind` calls (`agent_server`),
|
reminder mutation: agent `remind` calls (`agent_server`),
|
||||||
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
|
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
|
||||||
with Reminder kind, and the scheduler tick after each delivery
|
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
|
subscribes and calls `renderReminders` on receipt, so the list
|
||||||
updates live without polling.
|
updates live without polling.
|
||||||
|
- `capabilities_changed` (seq, caps: `Vec<str>`, descriptions: map,
|
||||||
|
assignments: `BTreeMap<String, Vec<String>>`) — 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<str>`, descriptions: map,
|
||||||
|
assignments: `BTreeMap<String, Vec<String>>`) — 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
|
`/api/state` is **only fetched on cold-load and on the few
|
||||||
forms that mutate non-event-derived state** (PURG3 +
|
forms that mutate non-event-derived state** (PURG3 +
|
||||||
|
|
|
||||||
|
|
@ -1227,7 +1227,19 @@ window.marked = marked;
|
||||||
// ── tool-groups (permissions) table ─────────────────────────────────────
|
// ── tool-groups (permissions) table ─────────────────────────────────────
|
||||||
// Fetched from GET /api/tool-groups on system tab activation and after
|
// 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
|
// 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() {
|
async function fetchAndRenderCapabilities() {
|
||||||
const root = $('capabilities-section');
|
const root = $('capabilities-section');
|
||||||
|
|
@ -3607,6 +3619,8 @@ window.marked = marked;
|
||||||
rebuild_queue_changed: applyRebuildQueueChanged,
|
rebuild_queue_changed: applyRebuildQueueChanged,
|
||||||
schedules_changed: applySchedulesChanged,
|
schedules_changed: applySchedulesChanged,
|
||||||
reminders_changed: applyRemindersChanged,
|
reminders_changed: applyRemindersChanged,
|
||||||
|
capabilities_changed: applyCapabilitiesChanged,
|
||||||
|
tool_groups_changed: applyToolGroupsChanged,
|
||||||
};
|
};
|
||||||
(function bindDashboardStream() {
|
(function bindDashboardStream() {
|
||||||
// Route through the SharedWorker so all open hyperhive tabs share
|
// 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
|
// re-fetch reminders on SCH3DUL3S activation since both sections
|
||||||
// live on the same tab.
|
// live on the same tab.
|
||||||
if (target === 'schedules') { refreshSchedules(); refreshReminders(); }
|
if (target === 'schedules') { refreshSchedules(); refreshReminders(); }
|
||||||
// Permissions tables (capabilities + tool-groups) have no SSE channel;
|
// Permissions tables: SSE covers worker-applied changes
|
||||||
// fetch both on each activation so the operator sees fresh data.
|
// (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') {
|
if (target === 'permissions') {
|
||||||
fetchAndRenderCapabilities();
|
fetchAndRenderCapabilities();
|
||||||
fetchAndRenderToolGroups();
|
fetchAndRenderToolGroups();
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// Update the `step` label on a running queue entry and (if it
|
||||||
/// actually changed) re-emit the queue snapshot so the dashboard
|
/// actually changed) re-emit the queue snapshot so the dashboard
|
||||||
/// renders the new phase. Returns `true` when the label was new
|
/// renders the new phase. Returns `true` when the label was new
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,32 @@ pub enum DashboardEvent {
|
||||||
seq: u64,
|
seq: u64,
|
||||||
reminders: Vec<crate::broker::PendingReminder>,
|
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 {
|
impl DashboardEvent {
|
||||||
|
|
@ -242,6 +268,8 @@ impl DashboardEvent {
|
||||||
DashboardEvent::RebuildQueueChanged { .. } => "rebuild_queue_changed",
|
DashboardEvent::RebuildQueueChanged { .. } => "rebuild_queue_changed",
|
||||||
DashboardEvent::SchedulesChanged { .. } => "schedules_changed",
|
DashboardEvent::SchedulesChanged { .. } => "schedules_changed",
|
||||||
DashboardEvent::RemindersChanged { .. } => "reminders_changed",
|
DashboardEvent::RemindersChanged { .. } => "reminders_changed",
|
||||||
|
DashboardEvent::CapabilitiesChanged { .. } => "capabilities_changed",
|
||||||
|
DashboardEvent::ToolGroupsChanged { .. } => "tool_groups_changed",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +396,18 @@ mod tests {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
reminders: Vec::new(),
|
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 {
|
for ev in samples {
|
||||||
let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise");
|
let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise");
|
||||||
|
|
|
||||||
|
|
@ -770,11 +770,16 @@ async fn dispatch(
|
||||||
crate::meta::commit_tool_groups(name, groups)
|
crate::meta::commit_tool_groups(name, groups)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("commit tool-groups for {name}"))?;
|
.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 }) => {
|
Some(PermPayload::Capabilities { caps }) => {
|
||||||
crate::meta::commit_capabilities(name, caps)
|
crate::meta::commit_capabilities(name, caps)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("commit capabilities for {name}"))?;
|
.with_context(|| format!("commit capabilities for {name}"))?;
|
||||||
|
coord.emit_capabilities_snapshot();
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue