use std::sync::Arc; use tokio::net::TcpListener; use tower_http::limit::RequestBodyLimitLayer; use tower_http::trace::TraceLayer; use tracing::{info, warn}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; use doctate_server::analyze; use doctate_server::config::Config; use doctate_server::transcribe; use doctate_server::AppState; #[tokio::main] async fn main() { dotenvy::dotenv().ok(); let config = Arc::new(Config::from_env()); // Logging: stdout + daily rotating log files. let env_filter = EnvFilter::try_new(&config.log_level) .unwrap_or_else(|_| EnvFilter::new("info")); let file_appender = tracing_appender::rolling::daily(&config.log_path, "recorder.log"); let (file_writer, _guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::registry() .with(env_filter) .with( tracing_subscriber::fmt::layer() .with_target(false) .with_writer(std::io::stdout), ) .with( tracing_subscriber::fmt::layer() .with_target(false) .with_ansi(false) .with_writer(file_writer), ) .init(); // Surface configuration gaps that silently disable features. if !config.llm_configured() { warn!( "LLM not configured — 'Fall abschließen' is disabled. \ Set LLM_URL, LLM_API_KEY and LLM_MODEL in .env to enable." ); } { let custom = std::env::var("LLM_SYSTEM_PROMPT") .map(|v| !v.is_empty()) .unwrap_or(false); info!( prompt_source = if custom { "env" } else { "default" }, prompt_chars = config.llm_system_prompt.chars().count(), "system prompt loaded" ); } // Shared HTTP client for both downstream pipelines. let http_client = reqwest::Client::builder() .build() .expect("reqwest client build failed"); // Transcription pipeline: channel + worker + recovery scan. let (transcribe_tx, transcribe_rx) = transcribe::channel(); tokio::spawn(transcribe::worker::run( transcribe_rx, config.clone(), http_client.clone(), )); { let tx = transcribe_tx.clone(); let data_path = config.data_path.clone(); tokio::spawn(async move { transcribe::recovery::scan_and_enqueue(&data_path, &tx).await; }); } // Analyze pipeline: channel + worker + recovery scan. let (analyze_tx, analyze_rx) = analyze::channel(); tokio::spawn(analyze::worker::run( analyze_rx, config.clone(), http_client.clone(), )); { let tx = analyze_tx.clone(); let data_path = config.data_path.clone(); tokio::spawn(async move { analyze::recovery::scan_and_enqueue(&data_path, &tx).await; }); } let state = AppState { config: config.clone(), transcribe_tx, analyze_tx, session_store: doctate_server::web_session::new_store(), }; let addr = format!("0.0.0.0:{}", config.server_port); info!(port = config.server_port, "Server starting"); let app = doctate_server::create_router_with_state(state) .layer(TraceLayer::new_for_http()) .layer(RequestBodyLimitLayer::new(50 * 1024 * 1024)); // 50 MB let listener = TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); }