//! Web server module for the makima audio API.
pub mod auth;
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::{api_keys, chat, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_ws, users, versions};
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))
// Version history endpoints
.route("/files/{id}/versions", get(versions::list_versions))
.route("/files/{id}/versions/{version}", get(versions::get_version))
.route("/files/{id}/versions/restore", post(versions::restore_version))
// Mesh/task orchestration endpoints
.route(
"/mesh/tasks",
get(mesh::list_tasks).post(mesh::create_task),
)
.route(
"/mesh/tasks/{id}",
get(mesh::get_task)
.put(mesh::update_task)
.delete(mesh::delete_task),
)
.route("/mesh/tasks/{id}/subtasks", get(mesh::list_subtasks))
.route("/mesh/tasks/{id}/events", get(mesh::list_task_events))
.route("/mesh/tasks/{id}/output", get(mesh::get_task_output))
.route("/mesh/tasks/{id}/start", post(mesh::start_task))
.route("/mesh/tasks/{id}/stop", post(mesh::stop_task))
.route("/mesh/tasks/{id}/message", post(mesh::send_message))
.route("/mesh/tasks/{id}/retry-completion", post(mesh::retry_completion_action))
.route("/mesh/tasks/{id}/clone", post(mesh::clone_worktree))
.route("/mesh/tasks/{id}/check-target", post(mesh::check_target_exists))
.route("/mesh/chat", post(mesh_chat::mesh_toplevel_chat_handler))
.route(
"/mesh/chat/history",
get(mesh_chat::get_chat_history).delete(mesh_chat::clear_chat_history),
)
.route("/mesh/tasks/{id}/chat", post(mesh_chat::mesh_chat_handler))
.route("/mesh/daemons", get(mesh::list_daemons))
.route("/mesh/daemons/directories", get(mesh::get_daemon_directories))
.route("/mesh/daemons/{id}", get(mesh::get_daemon))
// Merge endpoints for orchestrators
.route("/mesh/tasks/{id}/branches", get(mesh_merge::list_branches))
.route("/mesh/tasks/{id}/merge/start", post(mesh_merge::merge_start))
.route("/mesh/tasks/{id}/merge/status", get(mesh_merge::merge_status))
.route("/mesh/tasks/{id}/merge/resolve", post(mesh_merge::merge_resolve))
.route("/mesh/tasks/{id}/merge/commit", post(mesh_merge::merge_commit))
.route("/mesh/tasks/{id}/merge/abort", post(mesh_merge::merge_abort))
.route("/mesh/tasks/{id}/merge/skip", post(mesh_merge::merge_skip))
.route("/mesh/tasks/{id}/merge/check", get(mesh_merge::merge_check))
// Mesh WebSocket endpoints
.route("/mesh/tasks/subscribe", get(mesh_ws::task_subscription_handler))
.route("/mesh/daemons/connect", get(mesh_daemon::daemon_handler))
// API key management endpoints
.route(
"/auth/api-keys",
post(api_keys::create_api_key_handler)
.get(api_keys::get_api_key_handler)
.delete(api_keys::revoke_api_key_handler),
)
.route("/auth/api-keys/refresh", post(api_keys::refresh_api_key_handler))
// User account management endpoints
.route(
"/users/me",
axum::routing::delete(users::delete_account_handler),
)
.route("/users/me/password", axum::routing::put(users::change_password_handler))
.route("/users/me/email", axum::routing::put(users::change_email_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");
}