mod frontend_api; mod frontend_render; mod manifest; mod static_serve; mod summary_proxy; use crate::frontend_api::create_frontend_api; use crate::frontend_render::{new_frontend_render_router, FrontendConfig}; use crate::manifest::handle_manifest; use crate::static_serve::{static_serve, static_serve_svg, static_serve_sw}; use crate::summary_proxy::generate_summary; use axum::headers::CacheControl; use axum::routing::{any, get}; use axum::{Router, TypedHeader}; use dotenvy::dotenv; use hyper::StatusCode; use magnetar_common::config::MagnetarConfig; use miette::{miette, IntoDiagnostic}; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tera::Tera; use thiserror::Error; use tokio::signal; use tower_http::services::ServeFile; use tower_http::trace::TraceLayer; use tracing::error; use tracing::log::info; use tracing_subscriber::EnvFilter; #[derive(Debug, Error)] pub enum RouterSetupError { #[error("Failed to load template: {0}")] TemplateLoadError(#[from] tera::Error), } fn new_calckey_fe_router(config: &'static MagnetarConfig) -> Result { let tera = Tera::new("fe_calckey/frontend/assets-be/template/**/*.html")?; let frontend_renderer_config = FrontendConfig { magnetar_config: config, templater: Arc::new(tera), }; Ok(Router::new() .nest_service( "/favicon.ico", ServeFile::new("fe_calckey/frontend/assets/favicon.ico"), ) .nest_service( "/favicon.png", ServeFile::new("fe_calckey/frontend/assets/favicon.png"), ) .nest_service( "/favicon.svg", ServeFile::new("fe_calckey/frontend/assets/favicon.svg"), ) .route("/manifest.json", get(handle_manifest).with_state(config)) .nest( "/sw.js", static_serve_sw("fe_calckey/frontend/built/_sw_dist_/sw.js"), ) .nest("/static-assets", static_serve("fe_calckey/frontend/assets")) .nest( "/client-assets", static_serve("fe_calckey/frontend/client/assets"), ) .nest( "/assets", static_serve("fe_calckey/frontend/built/_client_dist_"), ) .nest( "/twemoji", static_serve_svg("fe_calckey/frontend/assets-be/twemoji"), ) .nest("/fe-api", create_frontend_api("fe_calckey/frontend/assets")) .route("/url", get(generate_summary)) .route( "/streaming", any(|| async { ( StatusCode::SERVICE_UNAVAILABLE, TypedHeader( CacheControl::new() .with_max_age(Duration::from_secs(0)) .with_private(), ), ) }), ) .nest("", new_frontend_render_router(frontend_renderer_config))) } #[tokio::main] async fn main() -> miette::Result<()> { dotenv().ok(); let filter_layer = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new("info")) .unwrap(); tracing_subscriber::fmt() .with_env_filter(filter_layer) .with_test_writer() .init(); let config = &*Box::leak::<'static>(Box::new( magnetar_common::config::load_config().map_err(|e| miette!(e))?, )); info!("Loaded configuration: {config:#?}"); let app = Router::new() .layer(TraceLayer::new_for_http()) .nest("", new_calckey_fe_router(config).into_diagnostic()?); let addr = SocketAddr::from(( config.calckey_frontend.bind_addr, config.calckey_frontend.port, )); info!("Serving on: {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await .map_err(|e| miette!("Error running server: {}", e)) } async fn shutdown_signal() { let ctrl_c = async { if let Err(e) = signal::ctrl_c().await { error!("Ctrl+C signal handler error: {}", e); } }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("SIGTERM handler error") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } info!("Shutting down..."); }