refactor(#2591): slim the job-queue wire types to a raw-graph projection

WIP (hive-sh4re only; hive-c0re construction follows). Reshapes the
dashboard/hivectl queue wire per mara's redesign (#2637):

- DagView is now a thin projection: { id, source, reason, created_at, nodes }.
  Dropped the rolled-up kind (Template)/state/started_at/finished_at/inputs/
  approval_id — the client derives label + roll-up state + DAG timestamps
  from the node set.
- NodeView drops step + inline build_log_id; started_at/finished_at are
  chrono DateTime<Utc> (off the hive_jobq Node); non-derivable per-node
  payload rides the owning node (approval_id on the approval node, inputs
  on the meta_lock node).
- Deleted the Template enum entirely.

Done nodes are excluded from the wire (a fully-done DAG disappears; a
failed DAG lingers until the history cap). Build logs move to an on-demand
GET /api/build-log/<node_id> query (next commits).
This commit is contained in:
atlas 2026-07-23 14:25:00 +02:00 committed by mara
commit 54d144b647

View file

@ -6,68 +6,9 @@
//! live in `hive-c0re::job_queue`; these are the serialized views it
//! produces. Semantics: `docs/coordinator.md::Job queue`.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// What a DAG *means* — the request-level shape. Wire strings match
/// the pre-DAG queue's `kind` values so dashboards key off the same
/// tags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Template {
/// Rebuild one agent's container (prebuild → stop → profile-swap →
/// reconcile).
Rebuild,
/// Bump meta flake locks; grows a rebuild subgraph per affected
/// agent into the same DAG on completion.
MetaUpdate,
/// First-deploy spawn (approval-driven).
Spawn,
/// Reserved for a future destroy integration.
Destroy,
/// Mechanical stop + converge to `wanted = Up` (a restart).
Restart,
/// Signal → drain → mechanical stop → converge to `wanted = Up` — a
/// graceful restart as one atomic DAG (drains the harness before the
/// stop, same as `GracefulStop`, but then reconciles back up instead
/// of staying down).
GracefulRestart,
/// Perm-file commit followed by the rebuild subgraph.
PermChange,
/// Quiesce the harness, drain, then stop (`wanted = Offline`).
GracefulStop,
/// Converge to `wanted = Up`.
Start,
/// Converge to `wanted = Offline`.
Stop,
/// Bare converge of observed power state to the persisted intent
/// (boot reconcile).
Reconcile,
/// Boot-time config sweep as one DAG: a hyperhive lock bump that grows
/// a rebuild subgraph per stale agent, plus a `Reconcile` per drifted
/// agent — all in a single DAG (no anchor node, no child DAGs).
Boot,
}
impl Template {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Template::Rebuild => "rebuild",
Template::MetaUpdate => "meta_update",
Template::Spawn => "spawn",
Template::Destroy => "destroy",
Template::Restart => "restart",
Template::GracefulRestart => "graceful_restart",
Template::PermChange => "perm_change",
Template::GracefulStop => "graceful_stop",
Template::Start => "start",
Template::Stop => "stop",
Template::Reconcile => "reconcile",
Template::Boot => "boot",
}
}
}
/// Where the submit request originated — drives the "why" chip on the
/// dashboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -140,9 +81,12 @@ pub enum PermPayload {
/// so the widening from the old dag-local `u32` is transparent.
pub type NodeId = u64;
/// One node of a queued DAG, as serialized. Step labels, build-log
/// links, errors, and timestamps are per-node; the DAG-level `state`
/// is a roll-up.
/// One node of a queued DAG, serialized near-raw from the scheduler
/// graph. Lifecycle (`state` / `started_at` / `finished_at` / `error`)
/// comes straight off the `hive_jobq::Node`. The client derives DAG-level
/// roll-ups (label, state, timestamps) from the node set — nothing is
/// rolled up host-side. Build logs are fetched on demand by node id
/// (`GET /api/build-log/<id>`), not carried inline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeView {
pub id: NodeId,
@ -156,44 +100,45 @@ pub struct NodeView {
/// `"drain"`, `"write_dropin"`, `"write_perm_file"`,
/// `"approval_deploy"`.
pub kind: String,
/// Ids of the nodes this one waits for.
/// Ids of the nodes this one waits for. May reference an already-`Done`
/// node that's been filtered out of the wire — the client treats a dep
/// on an absent node as satisfied.
#[serde(default)]
pub deps: Vec<NodeId>,
pub state: State,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step: Option<String>,
pub started_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_log_id: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
pub finished_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// Approval-queue row id — present only on the `approval_deploy` node.
/// The client links a DAG to its pending approval through this (it is
/// not derivable from the graph, so it rides the node that owns it).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_id: Option<i64>,
/// Meta-flake inputs being bumped — present only on the `meta_lock`
/// node. Display-only payload, not derivable from the graph.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>,
}
/// A queued/running/recent DAG. `kind` = template string, roll-up
/// `state`; everything per-node appears exactly once, inside `nodes`.
/// There is no DAG-level `agent` — a DAG can span agents, so agent lives
/// on each [`NodeView`]; consumers group nodes by `NodeView::agent`.
/// A queued / running / failed DAG — a thin projection of one container
/// node plus its (non-`Done`) subtree from the scheduler graph. Only
/// non-derivable facts live here: `id`, `source`, `reason`, `created_at`,
/// and the node set. The client derives the card label, roll-up state, and
/// DAG timestamps from `nodes` (per-node `kind` + lifecycle) — nothing is
/// rolled up host-side. There is no DAG-level `agent`: a DAG can span
/// agents, so agent is per-[`NodeView`]; consumers group by `NodeView::agent`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagView {
pub id: u64,
/// Template wire string — same values the old `kind` field used.
pub kind: Template,
/// Roll-up: `failed` if any node failed, else `running` /
/// `queued` / `cancelled` / `done`.
pub state: State,
pub source: Source,
pub reason: String,
pub enqueued_at: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_id: Option<i64>,
/// When the DAG was enqueued.
pub created_at: DateTime<Utc>,
/// Nodes of this DAG with `Done` ones excluded. A DAG whose nodes are
/// all `Done` is omitted from the snapshot entirely; a `Failed` DAG
/// lingers until aged out by the history cap.
pub nodes: Vec<NodeView>,
}