92 lines
3.1 KiB
Rust
92 lines
3.1 KiB
Rust
//! 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<AppState>,
|
|
AxumPath(id): AxumPath<i64>,
|
|
) -> 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<String>,
|
|
}
|
|
|
|
pub(super) async fn post_deny(
|
|
State(state): State<AppState>,
|
|
AxumPath(id): AxumPath<i64>,
|
|
Form(form): Form<DenyForm>,
|
|
) -> 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<Approval>) -> Vec<Approval> {
|
|
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()
|
|
}
|