use std::sync::Arc; use axum::extract::{Path, State as AxumState}; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::Json; use tokio::sync::mpsc; use tracing::{error, info}; use crate::config::Config; use crate::life::LifeEvent; use crate::state::AppState; #[derive(Clone)] pub struct HttpState { pub app_state: Arc, pub life_tx: mpsc::Sender, } pub async fn start_http_server( config: &Config, app_state: Arc, life_tx: mpsc::Sender, ) { let port = config .gitea .as_ref() .map(|g| g.webhook_port) .unwrap_or(9880); let state = Arc::new(HttpState { app_state, life_tx, }); let mut app = axum::Router::new() .route("/api/timers", get(list_timers)) .route("/api/timers/{id}/fire", post(fire_timer)) .with_state(state); // merge gitea webhook router if configured if let Some(gitea_config) = &config.gitea { let bot_user = std::env::var("GITEA_ADMIN_USER").unwrap_or_else(|_| "noc".into()); app = app.merge(crate::gitea::webhook_router(gitea_config, bot_user)); } let addr = format!("0.0.0.0:{port}"); info!("http server listening on {addr}"); let listener = tokio::net::TcpListener::bind(&addr) .await .unwrap_or_else(|e| panic!("bind {addr}: {e}")); if let Err(e) = axum::serve(listener, app).await { error!("http server error: {e}"); } } async fn list_timers(AxumState(state): AxumState>) -> impl IntoResponse { let timers = state.app_state.list_timers(None).await; let items: Vec = timers .iter() .map(|(id, chat_id, label, schedule, next_fire, enabled)| { serde_json::json!({ "id": id, "chat_id": chat_id, "label": label, "schedule": schedule, "next_fire": next_fire, "enabled": enabled, }) }) .collect(); Json(serde_json::json!(items)) } async fn fire_timer( AxumState(state): AxumState>, Path(id): Path, ) -> impl IntoResponse { match state.life_tx.send(LifeEvent::FireTimer(id)).await { Ok(_) => { info!(timer_id = id, "timer fire requested via API"); ( StatusCode::OK, Json(serde_json::json!({"status": "fired", "timer_id": id})), ) } Err(e) => { error!(timer_id = id, "failed to send fire event: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "life loop not responding"})), ) } } }