Fleet: per-node dev-tool version cards + nightly latest-check (Phase 1, read-only)
Each node card now shows installed versions of Docker / Claude Code / Kimi / GLM /
Ollama (conditional per node) under the ssh card, with an "update available" badge.
- daemon: probe_tools() finds docker/claude/kimi-cli/ollama across candidate bin dirs,
extracts semver from --version, reports {"t":"node_tools",...} on connect + every 15m.
- migration node_tools + tool_latest; cm-db repo node_tools (upsert/list/latest).
- cm-api: fleet.rs NodeTools uplink → upsert; tool_versions.rs spawn_latest_checker
(24h, npm/pypi/github; docker display-only); GET /api/nodes/{id}/tools (glm mirrors
claude). Spawned in clawmates-server.
- frontend: NodeTools cards on each HostCard with the ↑latest badge.
Phase 2 (one-click update execution) intentionally deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
69c64ad676
commit
9263418fcb
@@ -174,6 +174,51 @@ pub async fn agent_ws(
|
||||
upgrade.on_upgrade(move |socket| run_channel(pool, hub, node_id, socket))
|
||||
}
|
||||
|
||||
/// `GET /api/nodes/{id}/tools` — installed dev-tool versions for a node, the
|
||||
/// latest upstream version, and an update flag (Phase 1: read-only). `glm` is
|
||||
/// derived from `claude` (it runs Claude Code with a z.ai config).
|
||||
pub async fn tools(
|
||||
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)?;
|
||||
let installed: std::collections::HashMap<String, String> =
|
||||
cm_db::repo::node_tools::list(&state.pool, node_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let latest = cm_db::repo::node_tools::all_latest(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// (probe key, display name, ui key) in display order; `glm` mirrors `claude`.
|
||||
let order = [
|
||||
("docker", "Docker", "docker"),
|
||||
("claude", "Claude Code", "claude"),
|
||||
("kimi-cli", "Kimi", "kimi"),
|
||||
("glm", "GLM (Claude Code)", "glm"),
|
||||
("ollama", "Ollama", "ollama"),
|
||||
];
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for (probe, name, key) in order {
|
||||
let src = if probe == "glm" { "claude" } else { probe };
|
||||
if let Some(inst) = installed.get(src) {
|
||||
let lat = latest.get(src).cloned();
|
||||
let update = lat.as_ref().map(|l| l != inst).unwrap_or(false);
|
||||
out.push(json!({
|
||||
"name": name, "key": key, "installed": inst,
|
||||
"latest": lat, "updateAvailable": update,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(Json(json!({ "tools": out })))
|
||||
}
|
||||
|
||||
/// `POST /api/nodes/{id}/terminal/ticket` — mint a single-use terminal ticket
|
||||
/// (the browser WS handshake can't carry a bearer header).
|
||||
pub async fn terminal_ticket(
|
||||
|
||||
Reference in New Issue
Block a user