//! Approval endpoints for the dashboard. //! //! Approve/deny actions plus the orphan-approval GC sweep used by the //! `/api/state` builder. use axum::{ extract::{Form, Path as AxumPath, State}, http::StatusCode, response::{IntoResponse, Response}, }; use hive_sh4re::Approval; use serde::Deserialize; use super::{AppState, error_response}; use crate::actions; use crate::coordinator::Coordinator; pub(super) async fn post_approve( State(state): State, AxumPath(id): AxumPath, ) -> Response { match actions::approve(state.coord.clone(), id).await { // 200 instead of 303 — `actions::approve` fires // `ApprovalResolved` (success path) or the eventual failure // event, both of which the dashboard's derived store applies // live. The matching form carries `data-no-refresh`. Ok(()) => (StatusCode::OK, "ok").into_response(), Err(e) => error_response(&format!("approve {id} failed: {e:#}")), } } #[derive(Deserialize, Default)] pub(super) struct DenyForm { #[serde(default)] note: Option, } pub(super) async fn post_deny( State(state): State, AxumPath(id): AxumPath, Form(form): Form, ) -> Response { let note = form .note .as_deref() .map(str::trim) .filter(|s| !s.is_empty()); match actions::deny(&state.coord, id, note) { Ok(()) => (StatusCode::OK, "ok").into_response(), Err(e) => error_response(&format!("deny {id} failed: {e:#}")), } } /// Filter out approvals whose agent state dir was wiped out from under us /// (e.g. by a test script's cleanup). Marks them failed so they fall out of /// `pending` on next render. pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec) -> Vec { approvals .into_iter() .filter(|a| { // Spawn and InitConfig approvals are for not-yet-existent agents; // the proposed dir is supposed to be missing. if matches!( a.kind, hive_sh4re::ApprovalKind::Spawn | hive_sh4re::ApprovalKind::InitConfig ) { return true; } if Coordinator::agent_proposed_dir(&a.agent).exists() { true } else { let note = "agent state dir missing"; let _ = coord.approvals.mark_failed(a.id, note); tracing::info!(id = a.id, agent = %a.agent, "auto-failed orphan approval"); let sha_short = a .fetched_sha .as_deref() .map(|s| s[..s.len().min(12)].to_owned()); coord.emit_approval_resolved(crate::coordinator::ApprovalResolved { id: a.id, agent: a.agent.as_str(), approval_kind: a.kind.as_str(), sha_short, status: "failed", note: Some(note.to_owned()), description: a.description.clone(), }); false } }) .collect() }