mod config; mod event_handler; mod polling; use anyhow::Result; use application::DataProjection; use config_sqlite::SqliteConfigStore; use domain::ConfigRepository; use http_api::AppState; use kframe_auth::{Argon2Hasher, AuthConfig, JwtAuthService}; use secret_store::AesSecretStore; use std::sync::Arc; use tcp_server::{ClientTracker, TcpBroadcaster, TcpEventBus, run_tcp_server}; use tracing::{error, info, warn}; #[tokio::main] async fn main() -> Result<()> { dotenvy::dotenv().ok(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info,sqlx=warn".into()), ) .init(); let cfg = config::ServerConfig::from_env(); let auth_config = AuthConfig::from_env().map_err(|e| anyhow::anyhow!(e))?; let secrets = AesSecretStore::from_env().map_err(|e| anyhow::anyhow!(e))?; info!(db = %cfg.database_url, "connecting to database"); let secrets = Arc::new(secrets); let config_store = Arc::new(SqliteConfigStore::with_secrets(&cfg.database_url, Some(secrets.clone())).await?); let event_bus = Arc::new(TcpEventBus::new(64)); let broadcaster = Arc::new(TcpBroadcaster::new(64)); let projection = Arc::new(DataProjection::new()); let tracker = Arc::new(ClientTracker::new()); let auth = Arc::new(JwtAuthService::new(auth_config)); let hasher = Arc::new(Argon2Hasher); match config_store.load_widget_states().await { Ok(states) if !states.is_empty() => { info!(count = states.len(), "loaded cached widget states"); projection.seed(states).await; } Ok(_) => {} Err(e) => warn!(error = %e, "failed to load cached widget states"), } let tcp_addr = cfg.tcp_addr.clone(); let tcp_bc = broadcaster.clone(); let tcp_tracker = tracker.clone(); let tcp_config = config_store.clone(); let tcp_proj = projection.clone(); tokio::spawn(async move { if let Err(e) = run_tcp_server(&tcp_addr, tcp_bc, tcp_tracker, tcp_config, tcp_proj).await { error!(error = %e, "tcp server failed"); } }); info!(addr = %cfg.tcp_addr, "TCP server started"); let http_addr = cfg.http_addr.clone(); let http_state = AppState { config: config_store.clone(), events: event_bus.clone(), widget_states: projection.clone(), broadcaster: broadcaster.clone(), clients: tracker.clone(), auth: auth.clone(), hasher: hasher.clone(), spa_dir: cfg.spa_dir, }; tokio::spawn(async move { if let Err(e) = http_api::serve(&http_addr, http_state).await { error!(error = %e, "HTTP API failed"); } }); info!(addr = %cfg.http_addr, "HTTP API started"); info!("K-Frame server running"); let ev_bus = event_bus.clone(); let ev_config = config_store.clone(); let ev_bc = broadcaster.clone(); let ev_proj = projection.clone(); tokio::spawn(async move { event_handler::run(ev_bus, ev_config, ev_bc, ev_proj).await; }); polling::run( config_store, broadcaster, projection, cfg.poll_interval_secs, ) .await }