From 52dd9ede2366b260a52ce5a1f839d33d484f8eb8 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 23 Jul 2026 15:24:04 +0200 Subject: [PATCH] feat(#2591): add GET /api/build-log/ {,/raw} query endpoints The raw-graph wire drops the inline build_log_id; the client fetches a node's captured output on demand. Two handlers resolve node id -> log-row id (JobQueue::build_log_id_of, now keyed by the wire u64) then delegate to the existing get_full / raw handlers: /api/build-log/ serves the BuildLogFull JSON ({stdout, stderr} + header), /raw serves text/plain. 404 when the node has no linked log. --- hive-c0re/src/dashboard/build_logs.rs | 36 +++++++++++++++++++++++++++ hive-c0re/src/dashboard/mod.rs | 8 ++++++ hive-c0re/src/job_queue/mod.rs | 16 +++++++----- hive-c0re/src/job_queue/tests.rs | 2 +- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/hive-c0re/src/dashboard/build_logs.rs b/hive-c0re/src/dashboard/build_logs.rs index b0771608..657557b5 100644 --- a/hive-c0re/src/dashboard/build_logs.rs +++ b/hive-c0re/src/dashboard/build_logs.rs @@ -90,6 +90,42 @@ pub(super) async fn get_build_log_full( } } +/// `GET /api/build-log/{node_id}` — the build log for a **queue node**, +/// resolved node id → log-row id → full log. Same `BuildLogFull` JSON +/// (`stdout` / `stderr` + header) as `get_build_log_full`; HTTP 404 when the +/// node has no linked log (the client gates the request on `NodeView.has_log`, +/// but a vacuum race can still 404). This is the on-demand log fetch the +/// raw-graph dashboard uses instead of an inline `build_log_id` on the wire. +pub(super) async fn get_build_log_for_node( + State(state): State, + AxumPath(node_id): AxumPath, +) -> Response { + match state.coord.job_queue.build_log_id_of(node_id) { + Some(log_id) => get_build_log_full(State(state), AxumPath(log_id)).await, + None => ( + StatusCode::NOT_FOUND, + format!("node #{node_id} has no build log"), + ) + .into_response(), + } +} + +/// `GET /api/build-log/{node_id}/raw` — the node's build log as `text/plain` +/// for download (delegates to `get_build_log_raw` after resolving the node id). +pub(super) async fn get_build_log_raw_for_node( + State(state): State, + AxumPath(node_id): AxumPath, +) -> Response { + match state.coord.job_queue.build_log_id_of(node_id) { + Some(log_id) => get_build_log_raw(State(state), AxumPath(log_id)).await, + None => ( + StatusCode::NOT_FOUND, + format!("node #{node_id} has no build log"), + ) + .into_response(), + } +} + /// JSON frame sent on the `/api/build-logs/id/{id}/stream` SSE channel. /// `stdout_append` / `stderr_append` carry only the new bytes since the /// last frame; `done = true` means the build finished and the stream diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index 580feeba..7334a7d5 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -100,6 +100,14 @@ pub async fn serve( ) .route("/api/audit-log", get(misc_api::api_audit_log)) .route("/api/build-logs", get(build_logs::get_build_logs_all)) + .route( + "/api/build-log/{node_id}", + get(build_logs::get_build_log_for_node), + ) + .route( + "/api/build-log/{node_id}/raw", + get(build_logs::get_build_log_raw_for_node), + ) .route( "/api/build-logs/{agent}", get(build_logs::get_build_logs_agent), diff --git a/hive-c0re/src/job_queue/mod.rs b/hive-c0re/src/job_queue/mod.rs index 304243c9..b549d8d2 100644 --- a/hive-c0re/src/job_queue/mod.rs +++ b/hive-c0re/src/job_queue/mod.rs @@ -511,15 +511,19 @@ impl JobQueue { true } - /// The `build_logs` row id linked to `node_id`, if any — the lookup behind - /// the `GET /api/build-log/` query endpoint (the client fetches a - /// node's captured build output on demand rather than receiving it inline). + /// The `build_logs` row id linked to the wire node id `node_id`, if any — + /// the lookup behind the `GET /api/build-log/` query endpoint (the + /// client fetches a node's captured build output on demand rather than + /// receiving it inline). Takes the raw wire `u64` (the endpoint's path + /// param); `node_rt` is keyed by the opaque `NodeId`, so this scans for the + /// matching id — the map is small (live + recently-terminal nodes). #[must_use] - pub fn build_log_id_of(&self, node_id: NodeId) -> Option { + pub fn build_log_id_of(&self, node_id: u64) -> Option { self.lock() .node_rt - .get(&node_id) - .and_then(|r| r.build_log_id) + .iter() + .find(|(nid, _)| nid.get() == node_id) + .and_then(|(_, rt)| rt.build_log_id) } /// A DAG's terminal roll-up summary, computed on demand from its container. diff --git a/hive-c0re/src/job_queue/tests.rs b/hive-c0re/src/job_queue/tests.rs index 21c584e1..6c8a0199 100644 --- a/hive-c0re/src/job_queue/tests.rs +++ b/hive-c0re/src/job_queue/tests.rs @@ -896,7 +896,7 @@ fn set_build_log_id_links_running_node() { // The log id is fetched by node id (the `GET /api/build-log/` lookup), // not carried on the wire — it survives completion in the node runtime. assert_eq!( - q.build_log_id_of(c.node_id), + q.build_log_id_of(c.node_id.get()), Some(43), "log id survives completion" );