widget states cached to SQLite, loaded on startup to seed DataProjection so server restart preserves last-known data for reconnecting clients. polling: first poll runs immediately, widget list cached per-task with 30s refresh, static text polled once inline instead of looping. poll failures propagate WidgetError::SourceUnavailable to clients. render engine prepends [offline] prefix in accent color, stale data preserved below.
101 lines
3.2 KiB
Rust
101 lines
3.2 KiB
Rust
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
|
|
}
|