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
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user