//! Rebuild-queue cancel endpoint for the dashboard. use axum::{ extract::{Path as AxumPath, State}, response::{IntoResponse, Response}, }; use super::AppState; #[derive(serde::Serialize, utoipa::ToSchema)] pub(super) struct CancelResultBody { cancelled: bool, } /// Drop still-queued work from the job /// queue. /// /// `id` is a **node** id. A DAG's root cancels the whole group (the scheduler /// cascades to pending descendants), which is what the dashboard's cancel /// button sends today — a DAG id *is* its root node's id. An interior node /// cancels just that branch. /// /// Refuses `Running` / terminal nodes: an /// in-flight node owns the agent's nix store + nixos-container update /// lock and can't be safely interrupted from the queue side. Always /// returns 200; the body is `{"cancelled": true}` on a successful /// flip to Cancelled, `{"cancelled": false}` when the DAG was /// Running / terminal / gone. On success a fresh `RebuildQueueChanged` /// snapshot fires so the state flip surfaces live. #[utoipa::path( post, path = "/api/rebuild-queue/{id}/cancel", params(("id" = u64, Path, description = "job-queue node id (a DAG's root cancels the group)")), responses((status = 200, description = "whether the DAG was cancelled", body = CancelResultBody)), tag = "rebuild_queue" )] pub(super) async fn post_rebuild_queue_cancel( State(state): State, AxumPath(id): AxumPath, ) -> Response { if state.coord.job_queue.cancel(id) { // Any terminal side effect is the DAG's own spared tail node, which the // scheduler picks up on its next pass — nothing to fire from here. state.coord.emit_rebuild_queue_snapshot(); axum::Json(CancelResultBody { cancelled: true }).into_response() } else { axum::Json(CancelResultBody { cancelled: false }).into_response() } }