pub mod agent; pub mod ai_edges; mod auth; pub mod cas; mod intentions; pub mod jobs; pub mod livekit; pub mod pruning; mod queries; mod rss; mod serving; mod stdb; pub mod summarize; pub mod transcribe; pub mod tts; mod warmup; use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router}; use serde::Serialize; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use auth::{AuthUser, JwksKeys}; use cas::CasStore; use stdb::StdbClient; #[derive(Clone)] pub struct AppState { pub db: PgPool, pub jwks: JwksKeys, pub stdb: StdbClient, pub cas: CasStore, } #[derive(Serialize)] struct HealthResponse { status: &'static str, version: &'static str, db: &'static str, stdb: &'static str, } #[derive(Serialize)] struct MeResponse { node_id: uuid::Uuid, authentik_sub: String, } #[tokio::main] async fn main() { tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "maskinrommet=debug,tower_http=debug".parse().unwrap()), ) .with(tracing_subscriber::fmt::layer()) .init(); // Database let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://sidelinja:sidelinja@localhost:5432/synops".to_string()); let db = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Kunne ikke koble til PostgreSQL"); tracing::info!("Koblet til PostgreSQL"); // JWKS — hent nøkler fra Authentik ved oppstart let issuer = std::env::var("AUTHENTIK_ISSUER") .unwrap_or_else(|_| "https://auth.sidelinja.org/application/o/sidelinja/".to_string()); let client_id = std::env::var("AUTHENTIK_CLIENT_ID") .expect("AUTHENTIK_CLIENT_ID må være satt"); let jwks = JwksKeys::fetch(&issuer, &client_id) .await .expect("Kunne ikke hente JWKS fra Authentik"); // SpacetimeDB-klient let stdb_url = std::env::var("SPACETIMEDB_URL") .unwrap_or_else(|_| "http://spacetimedb:3000".to_string()); let stdb_database = std::env::var("SPACETIMEDB_DATABASE") .unwrap_or_else(|_| "synops".to_string()); // Hent token fra miljøvariabel, eller opprett ny identitet let stdb_token = match std::env::var("SPACETIMEDB_TOKEN") { Ok(token) if !token.is_empty() => { tracing::info!("Bruker konfigurert STDB-token"); token } _ => { tracing::info!("Ingen STDB-token konfigurert, oppretter ny identitet"); let (identity, token) = StdbClient::create_identity(&stdb_url) .await .expect("Kunne ikke opprette STDB-identitet"); tracing::info!("Opprettet STDB-identitet: {identity}"); token } }; let stdb = StdbClient::new(&stdb_url, &stdb_database, &stdb_token); // Warmup: last hele grafen fra PG til SpacetimeDB match warmup::run(&db, &stdb).await { Ok(stats) => { tracing::info!( "Warmup fullført: {} noder, {} edges, {} access", stats.nodes, stats.edges, stats.access ); } Err(e) => { tracing::error!("Warmup feilet: {e}"); // Fortsett likevel — STDB kan være midlertidig utilgjengelig } } // CAS — content-addressable store for binærfiler let cas_root = std::env::var("CAS_ROOT") .unwrap_or_else(|_| "/srv/synops/media/cas".to_string()); let cas = CasStore::new(&cas_root) .await .expect("Kunne ikke opprette CAS-katalog"); tracing::info!(root = %cas_root, "CAS initialisert"); // Start jobbkø-worker i bakgrunnen jobs::start_worker(db.clone(), stdb.clone(), cas.clone()); // Start periodisk CAS-pruning i bakgrunnen pruning::start_pruning_loop(db.clone(), cas.clone()); let state = AppState { db, jwks, stdb, cas }; // Ruter: /health er offentlig, /me krever gyldig JWT let app = Router::new() .route("/health", get(health)) .route("/me", get(me)) .route("/intentions/create_node", post(intentions::create_node)) .route("/intentions/create_edge", post(intentions::create_edge)) .route("/intentions/update_node", post(intentions::update_node)) .route("/intentions/delete_node", post(intentions::delete_node)) .route("/intentions/update_edge", post(intentions::update_edge)) .route("/intentions/create_communication", post(intentions::create_communication)) .route("/intentions/upload_media", post(intentions::upload_media)) .route("/cas/{hash}", get(serving::get_cas_file)) .route("/query/nodes", get(queries::query_nodes)) .route("/query/board", get(queries::query_board)) .route("/query/segments", get(queries::query_segments)) .route("/query/segments/srt", get(queries::export_srt)) .route("/intentions/create_alias", post(intentions::create_alias)) .route("/intentions/update_segment", post(intentions::update_segment)) .route("/intentions/retranscribe", post(intentions::retranscribe)) .route("/intentions/resolve_retranscription", post(intentions::resolve_retranscription)) .route("/intentions/summarize", post(intentions::summarize)) .route("/intentions/generate_tts", post(intentions::generate_tts)) .route("/intentions/join_communication", post(intentions::join_communication)) .route("/intentions/leave_communication", post(intentions::leave_communication)) .route("/intentions/close_communication", post(intentions::close_communication)) .route("/query/aliases", get(queries::query_aliases)) .route("/query/graph", get(queries::query_graph)) .route("/query/transcription_versions", get(queries::query_transcription_versions)) .route("/query/segments_version", get(queries::query_segments_version)) .route("/pub/{slug}/feed.xml", get(rss::generate_feed)) .layer(TraceLayer::new_for_http()) .with_state(state); let bind = std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:3100".to_string()); let listener = tokio::net::TcpListener::bind(&bind).await.unwrap(); tracing::info!("Maskinrommet lytter på {bind}"); axum::serve(listener, app).await.unwrap(); } /// Offentlig helsesjekk — verifiserer PG- og STDB-tilkobling. async fn health(State(state): State) -> Result, StatusCode> { sqlx::query("SELECT 1") .execute(&state.db) .await .map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?; // STDB helsesjekk: prøv å slette en ikke-eksisterende node. // Kallet når STDB og returnerer ok (noop), men feiler ved nettverksfeil. let stdb_status = match state.stdb.delete_node("__healthcheck_nonexistent__").await { Ok(()) => "connected", Err(_) => "unavailable", }; Ok(Json(HealthResponse { status: "ok", version: env!("CARGO_PKG_VERSION"), db: "connected", stdb: stdb_status, })) } /// Beskyttet endepunkt — returnerer autentisert brukers node_id. /// Brukes for å verifisere at auth-middleware fungerer. async fn me(user: AuthUser) -> Json { Json(MeResponse { node_id: user.node_id, authentik_sub: user.authentik_sub, }) }