use super::cleanup_workbenches;
use super::create_flag;
use super::create_routine;
use super::delete_routine;
use super::get_lock_status;
use super::get_routine;
use super::health;
use super::list_agents;
use super::list_flags;
use super::list_routine_runs;
use super::list_routines;
use super::lock_routines;
use super::mcp::MoadimMcp;
use super::metrics;
use super::move_routine;
use super::resolve_flag;
use super::restart;
use super::shutdown;
use super::trigger_routine;
use super::unlock_routines;
use super::update_routine;
use crate::error::AppError;
use crate::middlewares;
use crate::routines::{self, RoutineStore};
use crate::utils::time::now_secs;
use axum::{
http::{
header::{CACHE_CONTROL, ETAG, IF_NONE_MATCH},
HeaderMap, StatusCode,
},
middleware,
response::{IntoResponse, Response},
routing::{delete, get, post},
Router,
};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use tower::limit::GlobalConcurrencyLimitLayer;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::compression::CompressionLayer;
use utoipa_swagger_ui::SwaggerUi;
#[allow(
clippy::missing_docs_in_private_items,
reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "build_app_with_shutdown.rs"]
mod build_app_with_shutdown;
pub(crate) use build_app_with_shutdown::*;
const MAX_CONCURRENT_REQUESTS: usize = 64;
pub type ShutdownSignal = Arc<tokio::sync::Notify>;
#[derive(Clone)]
pub struct AppState {
pub routines: RoutineStore,
pub routines_dir: std::path::PathBuf,
pub uptime_start: u64,
pub shutdown: ShutdownSignal,
}
impl axum::extract::FromRef<AppState> for RoutineStore {
fn from_ref(state: &AppState) -> Self {
state.routines.clone()
}
}
const INDEX_HTML: &str = include_str!(concat!(env!("OUT_DIR"), "/index.html"));
static INDEX_ETAG: LazyLock<String> = LazyLock::new(|| etag_for(INDEX_HTML));
fn etag_for(html: &str) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
html.hash(&mut hasher);
format!("\"{:016x}\"", hasher.finish())
}
fn serve_spa(html: &'static str, etag: &'static str, headers: &HeaderMap) -> Response {
let not_modified = headers
.get(IF_NONE_MATCH)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value == etag);
if not_modified {
return (StatusCode::NOT_MODIFIED, [(ETAG, etag)]).into_response();
}
(
[(ETAG, etag), (CACHE_CONTROL, "no-cache")],
axum::response::Html(html),
)
.into_response()
}
#[utoipa::path(get, path = "/",
responses(
(status = 200, description = "Web client HTML", body = str),
(status = 304, description = "Client's cached copy is still current"),
))]
pub async fn index(headers: HeaderMap) -> Response {
serve_spa(INDEX_HTML, INDEX_ETAG.as_str(), &headers)
}
pub async fn redirect_client_to_root(uri: axum::http::Uri) -> axum::response::Redirect {
let path = match uri.path().strip_prefix("/client") {
Some("") | None => "/",
Some(rest) => rest,
};
match uri.query() {
Some(query) => axum::response::Redirect::permanent(&format!("{path}?{query}")),
None => axum::response::Redirect::permanent(path),
}
}
async fn api_not_found() -> AppError {
AppError::NotFound
}
#[path = "http_settings_routes.rs"]
mod http_settings_routes;
include!("build_app.rs");