//! Web server module for the makima audio API. pub mod handlers; pub mod messages; pub mod openapi; pub mod state; use axum::{ http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde::Serialize; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use crate::server::handlers::{chat, file_ws, files, listen}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; #[derive(Serialize)] struct HealthResponse { status: &'static str, version: &'static str, } /// Health check endpoint for load balancers and orchestrators. async fn health_check() -> impl IntoResponse { ( StatusCode::OK, Json(HealthResponse { status: "healthy", version: env!("CARGO_PKG_VERSION"), }), ) } /// Create the axum Router with all routes configured. pub fn make_router(state: SharedState) -> Router { // API v1 routes let api_v1 = Router::new() .route("/listen", get(listen::websocket_handler)) .route("/files/subscribe", get(file_ws::file_subscription_handler)) .route("/files", get(files::list_files).post(files::create_file)) .route( "/files/{id}", get(files::get_file) .put(files::update_file) .delete(files::delete_file), ) .route("/files/{id}/chat", post(chat::chat_handler)) .with_state(state); let swagger = SwaggerUi::new("/swagger-ui") .url("/api-docs/openapi.json", ApiDoc::openapi()); Router::new() .route("/api/v1/healthcheck", get(health_check)) .nest("/api/v1", api_v1) .merge(swagger) .layer( CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), ) .layer(TraceLayer::new_for_http()) } /// Run the HTTP server with graceful shutdown support. /// /// # Arguments /// * `state` - Shared application state containing ML models /// * `addr` - Address to bind to (e.g., "0.0.0.0:8080") pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> { let app = make_router(state); let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!("Server listening on {}", addr); tracing::info!("Swagger UI available at http://{}/swagger-ui", addr); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; Ok(()) } /// Wait for shutdown signals (Ctrl+C or SIGTERM). async fn shutdown_signal() { let ctrl_c = async { tokio::signal::ctrl_c() .await .expect("Failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("Failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } tracing::info!("Shutdown signal received, starting graceful shutdown"); }