From 2f6ecc4dc027bfee6b84e1f3084c738db839f5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 00:36:52 +0200 Subject: [PATCH] dashboard: deployed sha chip per container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContainerView grows deployed_sha (first 12 chars of the rev that /var/lib/hyperhive/meta/flake.lock currently has locked for agent-). renderContainers appends a 'deployed:' chip next to the container name + port — title attribute explains it's the meta-lock sha. degrades gracefully when the meta repo isn't seeded yet (missing / unparsable lock = empty map = no chip). new read_meta_locked_revs helper does the JSON parsing without unwraps. --- hive-c0re/assets/app.js | 5 +++++ hive-c0re/src/dashboard.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index d000976..58ed989 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -279,6 +279,11 @@ )); } head.append(el('span', { class: 'meta' }, `${c.container} :${c.port}`)); + if (c.deployed_sha) { + head.append(el('span', + { class: 'meta', title: 'sha currently locked in /meta/flake.lock' }, + `deployed:${c.deployed_sha}`)); + } li.append(head); // ── line 2: action buttons ─────────────────────────────────── diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 298ba3e..210b976 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -187,6 +187,12 @@ struct ContainerView { /// disable other buttons. #[serde(skip_serializing_if = "Option::is_none")] pending: Option<&'static str>, + /// First 12 chars of the sha the meta flake currently has locked + /// for this agent's input. Reflects what's actually deployed; can + /// differ from `applied//main` only between + /// `meta::prepare_deploy` and `finalize_deploy` (≤ build duration). + #[serde(skip_serializing_if = "Option::is_none")] + deployed_sha: Option, } #[derive(Serialize)] @@ -281,6 +287,7 @@ async fn build_container_views( ) -> (Vec, bool) { let mut out = Vec::new(); let mut any_stale = false; + let locked = read_meta_locked_revs(); for c in raw_containers { let (logical, is_manager) = if c == MANAGER_NAME { (MANAGER_NAME.to_owned(), true) @@ -299,6 +306,9 @@ async fn build_container_views( let pending = transient_snapshot .get(&logical) .map(|st| transient_label(st.kind)); + let deployed_sha = locked + .get(&format!("agent-{logical}")) + .map(|s| s[..s.len().min(12)].to_owned()); out.push(ContainerView { port: lifecycle::agent_web_port(&logical), running: lifecycle::is_running(&logical).await, @@ -308,11 +318,39 @@ async fn build_container_views( needs_update, needs_login, pending, + deployed_sha, }); } (out, any_stale) } +/// Parse `/var/lib/hyperhive/meta/flake.lock` into a map of node name +/// (`agent-`, `hyperhive`) → locked sha. Missing / unparsable lock +/// yields an empty map so the dashboard degrades gracefully when the +/// meta repo hasn't been seeded yet. +fn read_meta_locked_revs() -> std::collections::HashMap { + let mut out = std::collections::HashMap::new(); + let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else { + return out; + }; + let Ok(json) = serde_json::from_str::(&raw) else { + return out; + }; + let Some(nodes) = json.get("nodes").and_then(|v| v.as_object()) else { + return out; + }; + for (name, node) in nodes { + if let Some(rev) = node + .get("locked") + .and_then(|v| v.get("rev")) + .and_then(|v| v.as_str()) + { + out.insert(name.clone(), rev.to_owned()); + } + } + out +} + /// Transient state for agents whose container does NOT yet exist /// (`Spawning`). Lifecycle ops on existing containers surface as /// `ContainerView.pending` inline; this list only catches pre-creation.