//! AI Weekly Synth — Rust/Axum backend entry point. //! //! Loads configuration, connects to Postgres, runs migrations, //! and starts the HTTP server. Also supports the `create-admin` CLI subcommand. mod cli; mod logging; use anyhow::Context; use clap::Parser; use sqlx::postgres::PgPoolOptions; use tracing_subscriber::{fmt, EnvFilter}; use ai_synth_backend::app_state; use ai_synth_backend::config::AppConfig; use ai_synth_backend::db; use ai_synth_backend::models::user::UserRole; use ai_synth_backend::router; use ai_synth_backend::services::scraper; use crate::cli::{Cli, Commands}; #[tokio::main] async fn main() -> anyhow::Result<()> { // Load .env file if present (not an error if missing) dotenvy::dotenv().ok(); // Initialize tracing let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info,ai_synth_backend=debug")); fmt() .with_env_filter(filter) .event_format(logging::ErrorLocationFormat) .init(); let cli = Cli::parse(); // Load and validate configuration let config = AppConfig::from_env().map_err(|e| anyhow::anyhow!(e))?; config.validate().map_err(|e| anyhow::anyhow!(e))?; tracing::info!("Configuration loaded successfully"); // Create database connection pool let pool = PgPoolOptions::new() .max_connections(10) .connect(&config.database_url) .await .context("Failed to connect to Postgres")?; tracing::info!("Connected to Postgres"); // Run migrations run_migrations(&pool).await?; match cli.command.unwrap_or(Commands::Serve) { Commands::Serve => { let http_client = scraper::build_scraper_client()?; let state = app_state::AppState::new(config.clone(), pool, http_client); // Load provider rate limits from DB into in-memory limiter if let Err(e) = state.provider_rate_limiter.reload_from_db(&state.pool).await { tracing::warn!("Failed to load provider rate limits from DB: {:?}. Using defaults.", e); } let shutdown_pool = state.pool.clone(); let app = router::build_router(state, &config); let addr = format!("0.0.0.0:{}", config.port); tracing::info!("Starting server on {}", addr); let listener = tokio::net::TcpListener::bind(&addr) .await .context("Failed to bind to address")?; axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await .context("Server error")?; tracing::info!("Server shut down, closing database pool..."); shutdown_pool.close().await; tracing::info!("Shutdown complete."); } Commands::CreateAdmin { email } => { tracing::info!("Creating admin user: {}", email); let existing = db::users::find_by_email(&pool, &email).await?; match existing { Some(user) => { db::users::update_role(&pool, user.id, UserRole::Admin).await?; tracing::info!("User {} promoted to admin.", email); println!("User {} promoted to admin.", email); } None => { db::users::create(&pool, &email, None, UserRole::Admin).await?; tracing::info!("Admin user {} created.", email); println!("Admin user {} created.", email); } } } } Ok(()) } /// Run database migrations at startup. async fn run_migrations(pool: &sqlx::PgPool) -> anyhow::Result<()> { tracing::info!("Running database migrations..."); sqlx::migrate!("./migrations") .run(pool) .await .context("Failed to run database migrations")?; tracing::info!("Migrations complete."); Ok(()) } /// Wait for SIGTERM or Ctrl+C, then return to trigger graceful shutdown. 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 SIGTERM handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => tracing::info!("Received Ctrl+C, starting graceful shutdown..."), _ = terminate => tracing::info!("Received SIGTERM, starting graceful shutdown..."), } }