c0re: serve the jobq graph generically at /api/jobq/graph

`graph_node` projects a `hive_jobq::Node` onto the wire type from the
parent commit: everything the crate records, with hive-c0re's own fields
(`agent`, `approval_id`, `inputs`, `build_log_id`) collected into the
opaque payload slot instead of standing as named columns. A node with
nothing domain-specific to say serialises no `data` key at all, so the
slot costs nothing when it is unused.

`graph_dep` turns a `DepWhen` into the set of outcomes that satisfy the
edge by asking it about each of the four terminal states, rather than
leaking the bitflags representation onto the wire.

`Resource::wire_name` gives the resource vocabulary a string form —
hive-jobq is generic over the resource type, so a viewer that renders any
graph cannot be handed this enum. The `agent:` prefix keeps per-agent
leases from colliding with a global resource sharing an agent's name.

`graph_snapshot` deliberately reuses `visible_dags` for retention: every
live group plus the newest terminal ones. Serving the raw graph would
grow without bound — evicted groups' nodes linger until bounded pruning
lands. Within a retained group nothing is filtered: group roots ride as
ordinary nodes, and `Done` nodes stay, which is the projection defect
behind the "rebuild shows a single node" report.

The endpoint lands in the same commit rather than after it. Without a
consumer the whole projection is dead code, and a wire type nobody
produces cannot be reviewed for whether it says the right things.

Its OpenAPI body is `serde_json::Value`, matching `api_state`: no type in
`hive-host-sock` derives `ToSchema`, and that crate stays dependency-lean
on purpose.
This commit is contained in:
atlas 2026-08-02 22:59:44 +02:00 committed by mara
commit f357f98867
4 changed files with 145 additions and 1 deletions

View file

@ -205,6 +205,7 @@ pub async fn serve(
.routes(routes!(build_logs::get_build_log_stream))
.routes(routes!(state_snapshot::dashboard_stream))
.routes(routes!(state_snapshot::dashboard_history))
.routes(routes!(state_snapshot::jobq_graph))
.split_for_parts();
// Just the JSON, not the UI — Swagger UI itself is nginx-hosted from
// the nix store (see the module doc comment above `ApiDoc`). `api`
@ -448,6 +449,7 @@ mod router_build_probe {
.routes(routes!(meta_inputs::post_meta_update))
.routes(routes!(build_logs::get_build_log_stream))
.routes(routes!(state_snapshot::dashboard_stream))
.routes(routes!(state_snapshot::dashboard_history));
.routes(routes!(state_snapshot::dashboard_history))
.routes(routes!(state_snapshot::jobq_graph));
}
}

View file

@ -651,6 +651,27 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
out
}
#[utoipa::path(
get,
path = "/api/jobq/graph",
responses(
(status = 200, description = "every node of every retained job group, \
as generic `hive_jobq` graph nodes: identity, the parent tree, \
dependency edges with their accepted-outcome sets, lifecycle, and \
one opaque per-node payload. Group roots ride as ordinary nodes \
(`parent: null`) and `Done` nodes are not filtered a consumer \
renders the graph without knowing what any node means. Each element \
is a `hive_host_sock::graph::GraphNode`",
body = serde_json::Value),
),
tag = "state_snapshot"
)]
pub(super) async fn jobq_graph(
State(state): State<AppState>,
) -> axum::Json<Vec<hive_host_sock::graph::GraphNode>> {
axum::Json(state.coord.job_queue.graph_snapshot())
}
#[utoipa::path(
get,
path = "/api/dashboard/history",