hivectl: migrate dag_progress to hive-jobq-wire's generic GraphNode

This commit is contained in:
damocles 2026-08-03 18:47:22 +02:00 committed by mara
commit 10b0f640af
8 changed files with 272 additions and 175 deletions

View file

@ -378,6 +378,30 @@ impl JobQueue {
.filter_map(|c| dag_view(&inner, c))
.collect()
}
/// One DAG's container node plus its live subtree, as generic wire
/// nodes — the `QueueNodes` polling surface behind `hivectl`'s
/// wait/progress loop. Sibling of [`Self::snapshot`]
/// (which serves the same graph through the typed `DagView`/`NodeView`
/// projection for the dashboard's `/api/state.rebuild_queue`), this one
/// goes through [`GraphWire::wire_snapshot`] instead — no `Done`-node
/// filtering, no roll-up field (the root's own `state` answers that,
/// see `hive_jobq_wire`'s doc comment).
///
/// Empty when `dag_id` names no DAG container in the graph. Today that
/// only happens for a genuinely unknown id: nothing prunes the graph
/// yet (bounded-prune is a Stage-C follow-up, see [`visible_dags`]), so
/// a *completed* DAG's nodes keep riding here with a terminal `state`
/// rather than disappearing — callers watching for "done" should read
/// the root's `state`, not emptiness.
#[must_use]
pub fn dag_nodes(&self, dag_id: u64) -> Vec<GraphNode> {
let inner = self.lock();
let Some(root) = container(&inner, dag_id) else {
return Vec::new();
};
inner.graph().wire_snapshot([root])
}
}
/// The container node of `dag_id` — the `NodeKind::Dag` root whose id equals

View file

@ -308,6 +308,15 @@ impl hive_jobq_wire::WireNode for NodeKind {
{
data.insert("inputs".to_owned(), inputs.clone().into());
}
// The DAG container's own metadata — nowhere else on the wire, since
// `GraphNode` carries no DAG-level fields (a group root is an
// ordinary node). `hivectl` needs `source` for its progress line;
// `reason` rides along for free rather than adding a second variant
// later for the one field the first pass missed.
if let NodeKind::Dag { source, reason, .. } = self {
data.insert("source".to_owned(), source.as_str().into());
data.insert("reason".to_owned(), reason.clone().into());
}
// Not in the payload at all — the build log is keyed on node identity
// in a side table, which is why `data` is handed the id.
if let Some(log) = crate::build_logs::global().and_then(|h| h.id_for_node(id)) {

View file

@ -160,6 +160,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
.collect();
HostResponse::dags(dags)
}
HostRequest::QueueNodes { id } => HostResponse::nodes(coord.job_queue.dag_nodes(*id)),
HostRequest::List => HostResponse::list(lifecycle::list().await?),
// The agents root is ours and not world-traversable, so this
// question is only answerable on this side of the socket —