dashboard: deployed sha chip per container
ContainerView grows deployed_sha (first 12 chars of the rev that /var/lib/hyperhive/meta/flake.lock currently has locked for agent-<name>). renderContainers appends a 'deployed:<sha12>' 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.
This commit is contained in:
parent
691057d2d3
commit
2f6ecc4dc0
2 changed files with 43 additions and 0 deletions
|
|
@ -279,6 +279,11 @@
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
head.append(el('span', { class: 'meta' }, `${c.container} :${c.port}`));
|
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);
|
li.append(head);
|
||||||
|
|
||||||
// ── line 2: action buttons ───────────────────────────────────
|
// ── line 2: action buttons ───────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,12 @@ struct ContainerView {
|
||||||
/// disable other buttons.
|
/// disable other buttons.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pending: Option<&'static str>,
|
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/<n>/main` only between
|
||||||
|
/// `meta::prepare_deploy` and `finalize_deploy` (≤ build duration).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
deployed_sha: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -281,6 +287,7 @@ async fn build_container_views(
|
||||||
) -> (Vec<ContainerView>, bool) {
|
) -> (Vec<ContainerView>, bool) {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut any_stale = false;
|
let mut any_stale = false;
|
||||||
|
let locked = read_meta_locked_revs();
|
||||||
for c in raw_containers {
|
for c in raw_containers {
|
||||||
let (logical, is_manager) = if c == MANAGER_NAME {
|
let (logical, is_manager) = if c == MANAGER_NAME {
|
||||||
(MANAGER_NAME.to_owned(), true)
|
(MANAGER_NAME.to_owned(), true)
|
||||||
|
|
@ -299,6 +306,9 @@ async fn build_container_views(
|
||||||
let pending = transient_snapshot
|
let pending = transient_snapshot
|
||||||
.get(&logical)
|
.get(&logical)
|
||||||
.map(|st| transient_label(st.kind));
|
.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 {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running: lifecycle::is_running(&logical).await,
|
running: lifecycle::is_running(&logical).await,
|
||||||
|
|
@ -308,11 +318,39 @@ async fn build_container_views(
|
||||||
needs_update,
|
needs_update,
|
||||||
needs_login,
|
needs_login,
|
||||||
pending,
|
pending,
|
||||||
|
deployed_sha,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(out, any_stale)
|
(out, any_stale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse `/var/lib/hyperhive/meta/flake.lock` into a map of node name
|
||||||
|
/// (`agent-<n>`, `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<String, String> {
|
||||||
|
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::<serde_json::Value>(&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
|
/// Transient state for agents whose container does NOT yet exist
|
||||||
/// (`Spawning`). Lifecycle ops on existing containers surface as
|
/// (`Spawning`). Lifecycle ops on existing containers surface as
|
||||||
/// `ContainerView.pending` inline; this list only catches pre-creation.
|
/// `ContainerView.pending` inline; this list only catches pre-creation.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue