herdr phase 3: INFRA tier Herdr sessions surface

New INFRA category "Herdr sessions" (purple sparkles icon between
Fleet and Local hardware). Shows a card per online fleet node with:

  - Node name + hostname + IP
  - Per-workspace agent state pills (working / blocked / done /
    idle / unknown), colored dots + pane count
  - "Open" button → renders that node's full Herdr TUI inline via
    xterm.js (same nodeHerdrConnector + WebRTC-with-fallback the
    MissionCanvas Live Pane uses)

Backend:
  - node daemon: herdr_workspaces + herdr_snapshot ops
    (`herdr workspace list`, `herdr api snapshot`)
  - fleet_herdr::snapshot helper on top of hub.call_timeout
  - GET /api/nodes/{id}/herdr/session route

Fetch flow: /api/nodes filtered to status='online' → for each,
/api/nodes/{id}/herdr/session in parallel. Snapshot errors surface
per-card without failing the whole grid.

The "Open" xterm is separate from the MissionCanvas Live Pane —
this one is scoped to the whole node's Herdr TUI (any workspace),
not a specific mission's pane. Operator toggles between nodes via
the buttons.

Verified: cargo check --workspace + tsc --noEmit both green.
This commit is contained in:
Omar Sobh
2026-07-20 11:30:52 -07:00
parent 0d4cb2c2bc
commit bf4af48c80
6 changed files with 390 additions and 2 deletions
+18
View File
@@ -142,6 +142,24 @@ pub async fn sandbox_check(
}
}
/// `GET /api/nodes/{id}/herdr/session` — full Herdr session snapshot
/// for a node (workspaces + tabs + panes + agent states). Used by the
/// INFRA Herdr surface to browse per-node Herdr activity.
pub async fn herdr_session(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<Value>, ApiError> {
let node_id = NodeId::from(id);
nodes::get(&state.pool, node_id, user.workspace_id)
.await?
.ok_or(ApiError::NotFound)?;
match crate::fleet_herdr::snapshot(state.node_hub.clone(), node_id).await {
Ok(snap) => Ok(Json(snap)),
Err(e) => Ok(Json(json!({ "error": e }))),
}
}
/// `DELETE /api/nodes/{id}` — deregister a node (workspace-scoped).
pub async fn remove(
State(state): State<AppState>,