Fleet: per-node dev-tool version cards + nightly latest-check (Phase 1, read-only)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 23s
ci / e2e (push) Has been skipped

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:
Omar Sobh
2026-06-27 06:01:51 -07:00
co-authored by Claude Opus 4.8
parent 69c64ad676
commit 9263418fcb
10 changed files with 322 additions and 0 deletions
+1
View File
@@ -11,6 +11,7 @@ pub mod fleet_tailscale;
pub mod messages;
pub mod node_metrics;
pub mod node_rules;
pub mod node_tools;
pub mod nodes;
pub mod orgs;
pub mod outbox;
+64
View File
@@ -0,0 +1,64 @@
//! Per-node installed dev-tool versions (probed by the daemon) and the latest
//! upstream version per tool (filled by a nightly checker). Drives the fleet
//! UI's "update available" badges.
use std::collections::HashMap;
use cm_domain::NodeId;
use sqlx::{PgPool, Row};
use crate::DbError;
/// Replace a node's reported tool versions with `tools` (tool, version).
pub async fn upsert(pool: &PgPool, node_id: NodeId, tools: &[(String, String)]) -> Result<(), DbError> {
for (tool, version) in tools {
sqlx::query(
"INSERT INTO node_tools (node_id, tool, version, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (node_id, tool) DO UPDATE SET version = excluded.version, updated_at = now()",
)
.bind(node_id.as_uuid())
.bind(tool)
.bind(version)
.execute(pool)
.await?;
}
Ok(())
}
/// A node's installed tool versions as `(tool, version)`.
pub async fn list(pool: &PgPool, node_id: NodeId) -> Result<Vec<(String, String)>, DbError> {
let rows = sqlx::query("SELECT tool, version FROM node_tools WHERE node_id = $1")
.bind(node_id.as_uuid())
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| (r.get::<String, _>("tool"), r.get::<String, _>("version")))
.collect())
}
/// Record the latest upstream version for a tool.
pub async fn set_latest(pool: &PgPool, tool: &str, latest: &str) -> Result<(), DbError> {
sqlx::query(
"INSERT INTO tool_latest (tool, latest_version, checked_at)
VALUES ($1, $2, now())
ON CONFLICT (tool) DO UPDATE SET latest_version = excluded.latest_version, checked_at = now()",
)
.bind(tool)
.bind(latest)
.execute(pool)
.await?;
Ok(())
}
/// All known latest versions, keyed by tool.
pub async fn all_latest(pool: &PgPool) -> Result<HashMap<String, String>, DbError> {
let rows = sqlx::query("SELECT tool, latest_version FROM tool_latest")
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| (r.get::<String, _>("tool"), r.get::<String, _>("latest_version")))
.collect())
}