diff --git a/hive-c0re/src/dashboard/approvals.rs b/hive-c0re/src/dashboard/approvals.rs index c523f728..8604483c 100644 --- a/hive-c0re/src/dashboard/approvals.rs +++ b/hive-c0re/src/dashboard/approvals.rs @@ -16,7 +16,7 @@ use axum::{ use hive_sh4re::Approval; use serde::Deserialize; -use super::{AppState, error_response}; +use super::{AppState, error_response, problem_response}; use crate::actions; use crate::coordinator::Coordinator; use crate::lifecycle; @@ -209,7 +209,12 @@ pub(super) async fn get_approval_diff( .max() .map(|n| format!("refs/tags/proposal/{n}")) } - other => return error_response(&format!("unknown diff base {other:?}")), + other => { + return problem_response( + StatusCode::BAD_REQUEST, + &format!("unknown diff base {other:?}"), + ); + } }; let Some(base_ref) = base_ref else { return plain_text(match base { diff --git a/hive-c0re/src/dashboard/journal.rs b/hive-c0re/src/dashboard/journal.rs index cdcba9fb..5a661d51 100644 --- a/hive-c0re/src/dashboard/journal.rs +++ b/hive-c0re/src/dashboard/journal.rs @@ -13,7 +13,7 @@ use axum::{ }; use serde::Deserialize; -use super::{error_response, strip_container_prefix, validate_agent_name}; +use super::{error_response, problem_response, strip_container_prefix, validate_agent_name}; use crate::lifecycle; #[derive(Deserialize)] @@ -48,7 +48,10 @@ pub(super) async fn get_journal( let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX); let live = lifecycle::list().await.unwrap_or_default(); if !live.iter().any(|c| c == &prefixed) { - return error_response(&format!("journal: no managed container {prefixed:?}")); + return problem_response( + StatusCode::NOT_FOUND, + &format!("journal: no managed container {prefixed:?}"), + ); } let lines = q.lines.unwrap_or(500).min(5000); let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) { @@ -61,7 +64,10 @@ pub(super) async fn get_journal( format!("{u}.service") }; if !allowed.contains(&unit.as_str()) { - return error_response(&format!("journal: unknown unit {unit:?}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("journal: unknown unit {unit:?}"), + ); } Some(unit) } @@ -121,7 +127,10 @@ pub(super) async fn get_journal_host( format!("{u}.service") }; if !allowed.contains(&unit.as_str()) { - return error_response(&format!("journal-host: unknown unit {unit:?}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("journal-host: unknown unit {unit:?}"), + ); } cmd.args(["-u", &unit]); } diff --git a/hive-c0re/src/dashboard/permissions.rs b/hive-c0re/src/dashboard/permissions.rs index 60044b4b..ff379188 100644 --- a/hive-c0re/src/dashboard/permissions.rs +++ b/hive-c0re/src/dashboard/permissions.rs @@ -12,7 +12,7 @@ use axum::{ }; use serde::{Deserialize, Serialize}; -use super::{AppState, error_response, guard_agent_name, strip_container_prefix}; +use super::{AppState, guard_agent_name, problem_response, strip_container_prefix}; #[derive(Serialize)] pub(super) struct ToolGroupsSnapshot { @@ -120,7 +120,10 @@ pub(super) async fn post_tool_groups( // Validate group names before queuing — fail fast so the operator // sees the error immediately rather than waiting for the worker. if let Err(e) = crate::tool_groups::validate_groups(&body.groups) { - return error_response(&format!("invalid tool-groups for {logical}: {e}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("invalid tool-groups for {logical}: {e}"), + ); } // Enqueue a PermChange so the JSON file write is serialised through // the FIFO worker. Prevents concurrent batch-apply actions for @@ -204,7 +207,10 @@ pub(super) async fn post_capabilities( .collect(); for cap in &body.caps { if !known.contains(&cap.as_str()) { - return error_response(&format!("unknown capability: {cap}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("unknown capability: {cap}"), + ); } } // Enqueue a PermChange so the JSON file write is serialised through @@ -272,12 +278,18 @@ pub(super) async fn post_permissions( if let Some(groups) = &change.tool_groups && let Err(e) = crate::tool_groups::validate_groups(groups) { - return error_response(&format!("invalid tool-groups for {logical}: {e}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("invalid tool-groups for {logical}: {e}"), + ); } if let Some(caps) = &change.capabilities { for cap in caps { if !known_caps.contains(&cap.as_str()) { - return error_response(&format!("unknown capability for {logical}: {cap}")); + return problem_response( + StatusCode::BAD_REQUEST, + &format!("unknown capability for {logical}: {cap}"), + ); } } } diff --git a/hive-c0re/src/dashboard/questions.rs b/hive-c0re/src/dashboard/questions.rs index 5d6530e0..ef50aff8 100644 --- a/hive-c0re/src/dashboard/questions.rs +++ b/hive-c0re/src/dashboard/questions.rs @@ -13,7 +13,7 @@ use axum::{ }; use serde::Deserialize; -use super::{AppState, error_response}; +use super::{AppState, error_response, problem_response}; #[derive(Deserialize)] pub(super) struct AnswerForm { @@ -41,7 +41,10 @@ pub(super) async fn post_answer_question( ) -> Response { let answer = form.answer.trim(); if answer.is_empty() { - return with_cors(error_response("answer: required")); + return with_cors(problem_response( + StatusCode::BAD_REQUEST, + "answer: required", + )); } let resp = match state .coord diff --git a/hive-c0re/src/dashboard/reminders.rs b/hive-c0re/src/dashboard/reminders.rs index e29e885c..75789b0f 100644 --- a/hive-c0re/src/dashboard/reminders.rs +++ b/hive-c0re/src/dashboard/reminders.rs @@ -10,7 +10,7 @@ use axum::{ response::{IntoResponse, Response}, }; -use super::{AppState, error_response}; +use super::{AppState, error_response, problem_response}; pub(super) async fn api_reminders(State(state): State) -> Response { match state.coord.broker.list_pending_reminders() { @@ -24,7 +24,10 @@ pub(super) async fn post_cancel_reminder( AxumPath(id): AxumPath, ) -> Response { match state.coord.broker.cancel_reminder(id) { - Ok(0) => error_response(&format!("reminder {id} not pending (already delivered?)")), + Ok(0) => problem_response( + StatusCode::NOT_FOUND, + &format!("reminder {id} not pending (already delivered?)"), + ), Ok(_) => { tracing::info!(%id, "operator cancelled reminder"); state.coord.emit_reminders_snapshot(); @@ -44,7 +47,10 @@ pub(super) async fn post_retry_reminder( AxumPath(id): AxumPath, ) -> Response { match state.coord.broker.reset_reminder_failure(id) { - Ok(0) => error_response(&format!("reminder {id} not pending (already delivered?)")), + Ok(0) => problem_response( + StatusCode::NOT_FOUND, + &format!("reminder {id} not pending (already delivered?)"), + ), Ok(_) => { tracing::info!(%id, "operator reset reminder failure for retry"); state.coord.emit_reminders_snapshot(); diff --git a/hive-c0re/src/dashboard/schedules.rs b/hive-c0re/src/dashboard/schedules.rs index 535ae9c0..ccabe878 100644 --- a/hive-c0re/src/dashboard/schedules.rs +++ b/hive-c0re/src/dashboard/schedules.rs @@ -11,7 +11,7 @@ use axum::{ response::{IntoResponse, Response}, }; -use super::{AppState, error_response}; +use super::{AppState, error_response, problem_response}; /// `GET /api/schedules` — snapshot of every schedule for the /// scheduled-prompts tab. Returns the wire shape directly @@ -51,13 +51,19 @@ pub(super) async fn post_schedule_new( axum::Json(payload): axum::Json, ) -> Response { if payload.targets.is_empty() { - return error_response("schedule must have at least one target"); + return problem_response( + StatusCode::BAD_REQUEST, + "schedule must have at least one target", + ); } if payload.body.trim().is_empty() { - return error_response("schedule body must be non-empty"); + return problem_response(StatusCode::BAD_REQUEST, "schedule body must be non-empty"); } if let Some(0) = payload.interval_seconds { - return error_response("interval_seconds must be > 0 (use None for one-shot)"); + return problem_response( + StatusCode::BAD_REQUEST, + "interval_seconds must be > 0 (use None for one-shot)", + ); } let new = crate::scheduled_prompts::NewSchedule { owner: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), diff --git a/hive-c0re/src/dashboard/topology.rs b/hive-c0re/src/dashboard/topology.rs index 7482a846..5751d9a0 100644 --- a/hive-c0re/src/dashboard/topology.rs +++ b/hive-c0re/src/dashboard/topology.rs @@ -13,7 +13,7 @@ use axum::{ }; use serde::Deserialize; -use super::{AppState, error_response}; +use super::{AppState, error_response, problem_response}; /// `POST /api/topology/set-parent` body. `child` is required. /// `new_parent` may be: @@ -53,7 +53,7 @@ pub(super) async fn post_set_parent( ) -> Response { let child = form.child.trim().to_owned(); if child.is_empty() { - return error_response("set-parent: `child` required"); + return problem_response(StatusCode::BAD_REQUEST, "set-parent: `child` required"); } // Empty / whitespace-only `new_parent` ⇒ promote to root. Web // forms submit the empty string for a "no value" radio button,