feat(#1005): capabilities UI — per-agent tool-group table in SYST3M tab
Backend (hive-c0re/src/dashboard.rs):
GET /api/tool-groups — returns { groups: [...], assignments: {...} };
groups list comes from ToolGroup::ALL so the UI needs no change when
a new group is added (satisfies the 'no extend ui' requirement)
POST /api/tool-groups/{agent} — accepts { groups: [...] }, calls
set_groups() then enqueues a rebuild so the new HIVE_TOOL_GROUPS
env var takes effect immediately
hive-sh4re/src/lib.rs:
Added ToolGroup::ALL const (ordered slice of every group)
Added ToolGroup::as_str() — snake_case wire name, matches serde
Frontend:
SYST3M tab: new C4P4B1L1T13S section above K3PT ST4T3 with
#capabilities-section placeholder
tabs.js: fetchAndRenderCapabilities() + renderCapabilities() —
columns are built from the groups array returned by the API;
each row has one checkbox per group and a save button that POSTs
and re-fetches after 800ms; agents without explicit assignments
show a (default) label; triggered on each SYST3M tab activation
dashboard.css: .cap-table-wrap/.cap-table/.cap-row/.cap-agent-*
styles for the scrollable matrix table
This commit is contained in:
parent
81607aca26
commit
86a1591cfc
5 changed files with 277 additions and 0 deletions
|
|
@ -78,6 +78,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/retry-reminder/{id}", post(post_retry_reminder))
|
||||
.route("/request-spawn", post(post_request_spawn))
|
||||
.route("/api/topology/set-parent", post(post_set_parent))
|
||||
.route("/api/tool-groups", get(get_tool_groups))
|
||||
.route("/api/tool-groups/{agent}", post(post_tool_groups))
|
||||
.route("/op-send", post(post_op_send))
|
||||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
|
||||
|
|
@ -2475,6 +2477,57 @@ async fn post_set_parent(
|
|||
}
|
||||
}
|
||||
|
||||
// ── tool-group (capabilities) endpoints ──────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolGroupsSnapshot {
|
||||
/// Ordered list of all known tool-group names. Drives the column
|
||||
/// headers in the capabilities table — the UI does not hard-code them.
|
||||
groups: Vec<&'static str>,
|
||||
/// Per-agent assignment map. Absent agents use the role default
|
||||
/// (agents: messaging+meta+inbox+execution; manager: all groups).
|
||||
assignments: std::collections::BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
async fn get_tool_groups(State(_state): State<AppState>) -> axum::Json<ToolGroupsSnapshot> {
|
||||
let groups = hive_sh4re::ToolGroup::ALL
|
||||
.iter()
|
||||
.map(|g| g.as_str())
|
||||
.collect();
|
||||
let assignments = crate::tool_groups::read();
|
||||
axum::Json(ToolGroupsSnapshot { groups, assignments })
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetToolGroupsBody {
|
||||
groups: Vec<String>,
|
||||
}
|
||||
|
||||
async fn post_tool_groups(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(name): AxumPath<String>,
|
||||
axum::Json(body): axum::Json<SetToolGroupsBody>,
|
||||
) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
return reject;
|
||||
}
|
||||
if let Err(e) = crate::tool_groups::set_groups(&logical, &body.groups) {
|
||||
return error_response(&format!("set tool-groups for {logical}: {e}"));
|
||||
}
|
||||
// Trigger a rebuild so the new HIVE_TOOL_GROUPS env var takes effect.
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
crate::rebuild_queue::QueueKind::Rebuild,
|
||||
logical.clone(),
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"tool-group change via capabilities UI".to_owned(),
|
||||
None,
|
||||
);
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
tracing::info!(agent = %logical, groups = ?body.groups, "operator: set tool-groups via dashboard");
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
|
|
|
|||
Loading…
Reference in a new issue