refactor(#2808): the wire state enum is the scheduler's own

`hive_host_sock::jobs::State` was a hand-maintained copy of
`hive_jobq::State` — five variants spelled the same in both, kept in sync
by whoever remembered. Adding `Skipped` last week meant adding it twice.
The wire crate now re-exports the scheduler's enum and `to_wire_state` is
gone.

Two states that were hidden now reach clients. `to_wire_state` renamed
`Pending` to `Queued` and folded `Finishing` into `Running`, so the
dashboard could not distinguish a node waiting on its dependencies from
one whose own work is done while its sub-nodes still run. Both are now
visible, and consumers say which they mean.

Every consumer had to move with it, and only the Rust ones said so: the
exhaustive matches in `hivectl` and `DagView::rollup_state` failed to
compile, while the dashboard's fourteen string comparisons would have
gone quietly wrong — a `finishing` node no longer counting as running,
a `pending` node no longer as queued.

The frontend also builds CSS class names out of the state string
(`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all
lowercase. Those go through a `stateSlug` helper now; comparisons use the
wire spelling, presentation lowercases. Without that split every queue
entry and node chip would have silently lost its styling.

Dropping the `State as JobState` alias in hive-c0re falls out of this:
the alias only existed to tell two `State` types apart, and there is one
now.
This commit is contained in:
atlas 2026-07-27 21:50:24 +02:00
commit b9aab7e923
8 changed files with 72 additions and 93 deletions

View file

@ -43,7 +43,7 @@ use chrono::{DateTime, Utc};
use hive_host_sock::jobs::NodeView;
use hive_jobq::resources::ResourceTable;
use hive_jobq::scheduler::{Outcome, Scheduler};
use hive_jobq::{Dep, Graph, NodeId, State as JobState};
use hive_jobq::{Dep, Graph, NodeId};
use hive_sh4re::wire_time::now_unix;
use tokio::sync::Notify;
@ -133,19 +133,6 @@ impl Default for JobQueue {
}
}
/// Map a crate node state onto the wire state (`Pending` ↔ `Queued`;
/// `Finishing` — own logic done, sub-nodes still running — reads as `Running`).
fn to_wire_state(state: JobState) -> State {
match state {
JobState::Pending => State::Queued,
JobState::Running | JobState::Finishing => State::Running,
JobState::Done => State::Done,
JobState::Failed => State::Failed,
JobState::Cancelled => State::Cancelled,
JobState::Skipped => State::Skipped,
}
}
/// Insert `nodes` into the shared graph, honouring the spec's explicit **parent
/// axis**: a node with `parent = None` is a top-level group root (re-parented to
/// `group_parent`, which is `None` for `submit` and the emitting node for
@ -476,7 +463,7 @@ impl QueueInner {
self.sched
.graph()
.node(id)
.is_some_and(|n| n.state == JobState::Running)
.is_some_and(|n| n.state == State::Running)
}
/// The container node of `dag_id` — the `NodeKind::Dag` root whose id equals
@ -548,10 +535,10 @@ impl QueueInner {
// `Done` nodes drop off the wire — a finished step isn't
// interesting. `Skipped` ones stay: which branch a run *didn't*
// take is the readable half of an outcome-branched DAG.
if matches!(node.state, JobState::Done) {
if matches!(node.state, State::Done) {
continue;
}
any_unsettled |= !matches!(node.state, JobState::Skipped);
any_unsettled |= !matches!(node.state, State::Skipped);
let deps: Vec<u64> = node
.deps
.iter()
@ -587,7 +574,7 @@ impl QueueInner {
agent: node.payload.agent().to_owned(),
kind: node.payload.as_str().to_owned(),
deps,
state: to_wire_state(node.state),
state: node.state,
started_at: node.started_at,
finished_at: node.finished_at,
error: node.error.clone(),