use std::sync::Arc;
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use orca_ai::context::ClusterContext;
use orca_ai::monitor::ContextProvider;
use orca_core::types::AlertConversation;
use crate::alerts::{SharedAlertEngine, StateContextProvider};
use crate::state::AppState;
#[derive(Debug, Deserialize, Default)]
pub(crate) struct ListQuery {
#[serde(default)]
all: bool,
}
#[derive(Serialize)]
pub(crate) struct ListResponse {
alerts: Vec<AlertConversation>,
}
#[derive(Deserialize)]
pub(crate) struct ReplyBody {
message: String,
}
fn unconfigured() -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": "AI alerts are not configured; add an [ai] block to cluster.toml"
})),
)
}
fn not_found(id: Uuid) -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": format!("alert {id} not found") })),
)
}
fn engine(state: &Arc<AppState>) -> Option<SharedAlertEngine> {
state.alerts.clone()
}
async fn current_context(state: &Arc<AppState>) -> ClusterContext {
let provider = StateContextProvider::for_state(state.clone());
provider
.snapshot()
.await
.unwrap_or_else(|_| ClusterContext {
cluster_name: state.cluster_config.cluster.name.clone(),
nodes: Vec::new(),
services: Vec::new(),
recent_events: Vec::new(),
active_alerts: Vec::new(),
})
}
pub(crate) async fn list(
State(state): State<Arc<AppState>>,
Query(query): Query<ListQuery>,
) -> impl IntoResponse {
let Some(engine) = engine(&state) else {
return unconfigured().into_response();
};
let guard = engine.read().await;
let alerts: Vec<AlertConversation> = if query.all {
guard.all_conversations().to_vec()
} else {
guard.active_conversations().into_iter().cloned().collect()
};
Json(ListResponse { alerts }).into_response()
}
pub(crate) async fn view(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
let Some(engine) = engine(&state) else {
return unconfigured().into_response();
};
let guard = engine.read().await;
match guard.get_conversation(id) {
Some(c) => Json(c.clone()).into_response(),
None => not_found(id).into_response(),
}
}
pub(crate) async fn reply(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
Json(body): Json<ReplyBody>,
) -> impl IntoResponse {
let Some(engine) = engine(&state) else {
return unconfigured().into_response();
};
let ctx = current_context(&state).await;
let mut guard = engine.write().await;
match guard.operator_reply(id, &body.message, &ctx).await {
Ok(c) => Json(c.clone()).into_response(),
Err(e) => {
let msg = format!("{e}");
if msg.contains("conversation not found") {
not_found(id).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": msg })),
)
.into_response()
}
}
}
}
pub(crate) async fn dismiss(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
inline_reply(state, id, "dismiss").await
}
pub(crate) async fn resolve(
State(state): State<Arc<AppState>>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
inline_reply(state, id, "resolve").await
}
async fn inline_reply(state: Arc<AppState>, id: Uuid, marker: &str) -> axum::response::Response {
let Some(engine) = engine(&state) else {
return unconfigured().into_response();
};
let ctx = current_context(&state).await;
let mut guard = engine.write().await;
match guard.operator_reply(id, marker, &ctx).await {
Ok(c) => Json(c.clone()).into_response(),
Err(e) => {
let msg = format!("{e}");
if msg.contains("conversation not found") {
not_found(id).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": msg })),
)
.into_response()
}
}
}
}