diff --git a/hive-sh4re/src/jobs.rs b/hive-sh4re/src/jobs.rs index a3075977..fc854fa0 100644 --- a/hive-sh4re/src/jobs.rs +++ b/hive-sh4re/src/jobs.rs @@ -192,4 +192,32 @@ impl DagView { State::Done } } + + /// A short human label for the DAG, derived from its node kinds — the + /// shared replacement for the old `Template` wire string now that the kind + /// isn't sent. Priority-ordered so the most distinctive node wins (an + /// approval deploy reads as "deploy" even though it also rebuilds). Purely + /// cosmetic (progress/queue display); consumers that need exactness inspect + /// the node kinds directly. + #[must_use] + pub fn label(&self) -> &'static str { + let has = |k: &str| self.nodes.iter().any(|n| n.kind == k); + if has("approval_deploy") { + "deploy" + } else if has("meta_lock") { + "meta-update" + } else if has("create") || has("provision") { + "spawn" + } else if has("swap") || has("prebuild") || has("post_swap") { + "rebuild" + } else if has("write_perm_file") { + "perm-change" + } else if has("signal") || has("drain") { + "graceful" + } else if has("set_wanted") || has("stop_for_update") { + "power" + } else { + "reconcile" + } + } } diff --git a/hivectl/src/dag_progress.rs b/hivectl/src/dag_progress.rs index c726773f..f92cf433 100644 --- a/hivectl/src/dag_progress.rs +++ b/hivectl/src/dag_progress.rs @@ -66,8 +66,8 @@ async fn wait_for_dags_plain(socket: &Path, ids: Vec) -> Result<()> { // may still be running — keep watching so the operator // sees whether the agent came back. if d.nodes.iter().all(|n| n.state.is_terminal()) { - if d.state == hive_sh4re::jobs::State::Failed { - failed.push(format!("{} {}", d.kind.as_str(), dag_agents(d))); + if d.rollup_state() == hive_sh4re::jobs::State::Failed { + failed.push(format!("{} {}", d.label(), dag_agents(d))); } } else { all_terminal = false; @@ -134,8 +134,8 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec) -> Result<()> { }); hdr.set_message(format!( "{} {} {} · {}", - state_glyph(d.state), - d.kind.as_str(), + state_glyph(d.rollup_state()), + d.label(), dag_agents(d), fmt_dur(dag_elapsed(d, now)), )); @@ -163,8 +163,8 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec) -> Result<()> { } } if d.nodes.iter().all(|n| n.state.is_terminal()) { - if d.state == hive_sh4re::jobs::State::Failed { - failed.push(format!("{} {}", d.kind.as_str(), dag_agents(d))); + if d.rollup_state() == hive_sh4re::jobs::State::Failed { + failed.push(format!("{} {}", d.label(), dag_agents(d))); } } else { all_terminal = false; @@ -224,18 +224,21 @@ fn now_unix() -> i64 { .unwrap_or(0) } -/// Elapsed seconds for a DAG: `started_at` (falling back to `enqueued_at`) -/// through `finished_at` or `now`. +/// Elapsed seconds for a DAG: `started_at` (falling back to `created_at`) +/// through `finished_at` or `now`. The wire carries these as RFC3339 +/// `DateTime`; compare in unix seconds against `now`. fn dag_elapsed(d: &hive_sh4re::jobs::DagView, now: i64) -> i64 { - let start = d.started_at.unwrap_or(d.enqueued_at); - (d.finished_at.unwrap_or(now) - start).max(0) + let start = d + .started_at + .map_or_else(|| d.created_at.timestamp(), |t| t.timestamp()); + (d.finished_at.map_or(now, |t| t.timestamp()) - start).max(0) } /// Elapsed seconds for a node: `started_at` → `finished_at`/`now`, or 0 /// when it hasn't started. fn node_elapsed(n: &hive_sh4re::jobs::NodeView, now: i64) -> i64 { match n.started_at { - Some(start) => (n.finished_at.unwrap_or(now) - start).max(0), + Some(start) => (n.finished_at.map_or(now, |t| t.timestamp()) - start.timestamp()).max(0), None => 0, } } @@ -250,16 +253,11 @@ fn fmt_dur(secs: i64) -> String { } } -/// One animated node line: kind, live step, an `(after …)` marker for a -/// fan-in node (>1 dep), its elapsed timer, and a truncated error tail. +/// One animated node line: kind, an `(after …)` marker for a fan-in node +/// (>1 dep), its elapsed timer, and a truncated error tail. fn node_line(d: &hive_sh4re::jobs::DagView, n: &hive_sh4re::jobs::NodeView, now: i64) -> String { use std::fmt::Write as _; let mut s = n.kind.clone(); - if n.state == hive_sh4re::jobs::State::Running - && let Some(step) = &n.step - { - let _ = write!(s, " ({step})"); - } if n.deps.len() > 1 { let after: Vec<&str> = n .deps @@ -292,25 +290,21 @@ fn state_glyph(state: hive_sh4re::jobs::State) -> &'static str { } } -/// One progress line for a DAG: roll-up glyph, template, agent, then -/// the node chain with the running node's live step label — the CLI -/// twin of the dashboard's queue card. Used by the plain (non-TTY) path. +/// One progress line for a DAG: roll-up glyph, derived label, agents, then +/// the node chain — the CLI twin of the dashboard's queue card. Roll-up state +/// + label are derived from the node set (the wire no longer carries them). +/// Used by the plain (non-TTY) path. fn render_dag_line(d: &hive_sh4re::jobs::DagView) -> String { use std::fmt::Write as _; let mut out = format!( "{} {} {:<12}", - state_glyph(d.state), - d.kind.as_str(), + state_glyph(d.rollup_state()), + d.label(), dag_agents(d) ); for (i, n) in d.nodes.iter().enumerate() { let sep = if i == 0 { " " } else { " → " }; let _ = write!(out, "{sep}{} {}", state_glyph(n.state), n.kind); - if n.state == hive_sh4re::jobs::State::Running - && let Some(step) = &n.step - { - let _ = write!(out, " ({step})"); - } } if let Some(err) = d.nodes.iter().find_map(|n| n.error.as_deref()) { let short: String = err.chars().take(120).collect(); @@ -321,76 +315,65 @@ fn render_dag_line(d: &hive_sh4re::jobs::DagView) -> String { #[cfg(test)] mod tests { - use hive_sh4re::jobs::{DagView, NodeView, Source, State, Template}; + use hive_sh4re::jobs::{DagView, NodeView, Source, State}; + use hive_sh4re::wire_time::from_secs; use super::render_dag_line; - fn node(id: u64, agent: &str, kind: &str, state: State, step: Option<&str>) -> NodeView { + fn node(id: u64, agent: &str, kind: &str, state: State) -> NodeView { NodeView { id, agent: agent.to_owned(), kind: kind.to_owned(), deps: if id == 0 { vec![] } else { vec![id - 1] }, state, - step: step.map(str::to_owned), - build_log_id: None, started_at: None, finished_at: None, error: None, + approval_id: None, + inputs: vec![], + has_log: false, } } #[test] - fn render_dag_line_shows_chain_and_running_step() { + fn render_dag_line_shows_chain_with_derived_label_and_state() { + // Roll-up state (Running) + label ("rebuild") are derived from the node + // set — the wire no longer carries them. (`Done` nodes are included + // here to exercise glyph rendering; production filters them off.) let dag = DagView { id: 7, - kind: Template::Rebuild, - state: State::Running, source: Source::Manual, reason: "manual".to_owned(), - enqueued_at: 0, - started_at: Some(1), + created_at: from_secs(0), + started_at: Some(from_secs(1)), finished_at: None, - inputs: vec![], - approval_id: None, nodes: vec![ - node(0, "alice", "prebuild", State::Done, None), - node(1, "alice", "stop_for_update", State::Done, None), - node( - 2, - "alice", - "swap", - State::Running, - Some("nixos-container update"), - ), - node(3, "alice", "reconcile", State::Queued, None), + node(0, "alice", "prebuild", State::Done), + node(1, "alice", "stop_for_update", State::Done), + node(2, "alice", "swap", State::Running), + node(3, "alice", "reconcile", State::Queued), ], }; let line = render_dag_line(&dag); assert!(line.starts_with("▶ rebuild alice"), "{line}"); assert!( - line.contains( - "✔ prebuild → ✔ stop_for_update → ▶ swap (nixos-container update) → ⏸ reconcile" - ), + line.contains("✔ prebuild → ✔ stop_for_update → ▶ swap → ⏸ reconcile"), "{line}" ); } #[test] fn render_dag_line_surfaces_first_node_error() { - let mut failed = node(0, "bob", "prebuild", State::Failed, None); + let mut failed = node(0, "bob", "prebuild", State::Failed); failed.error = Some("nix build exploded".to_owned()); let dag = DagView { id: 8, - kind: Template::Rebuild, - state: State::Failed, source: Source::Manual, reason: "manual".to_owned(), - enqueued_at: 0, - started_at: Some(1), - finished_at: Some(2), - inputs: vec![], - approval_id: None, + created_at: from_secs(0), + started_at: Some(from_secs(1)), + finished_at: Some(from_secs(2)), nodes: vec![failed], }; let line = render_dag_line(&dag);