Parser menneskelig scriptspråk og kompilerer til tekniske CLI-kall.
"transkriber lydfilen (stor modell)" → "synops-transcribe --cas-hash {event.cas_hash} --model large"
Kompilatoren:
- Parser nummererte steg, ved_feil-fallbacks, og globale feilhåndterere
- Matcher verb mot cli_tool-noders aliases (case-insensitive)
- Mapper argumenter i parentes via args_hints
- Validerer variabelreferanser ({event.*}) mot kjent liste
- Fuzzy-matching med Levenshtein-avstand for forslag ved feil
- Rust-stil kompileringsrapport med ✓/✗ per linje
Integrert i jobbkøen: orchestrate-jobb kompilerer scriptet og
lagrer pipeline i metadata. Utførelse kommer i oppgave 24.5.
12 unit-tester dekker parser, kompilator, feilhåndtering og fuzzy-matching.
298 lines
13 KiB
Rust
298 lines
13 KiB
Rust
pub mod agent;
|
|
pub mod ai_admin;
|
|
pub mod ai_edges;
|
|
pub mod ai_process;
|
|
pub mod audio;
|
|
pub mod bandwidth;
|
|
mod auth;
|
|
pub mod cas;
|
|
pub mod cli_dispatch;
|
|
mod custom_domain;
|
|
mod intentions;
|
|
pub mod jobs;
|
|
pub mod livekit;
|
|
pub mod maintenance;
|
|
pub mod metrics;
|
|
pub mod pruning;
|
|
mod queries;
|
|
pub mod pg_writes;
|
|
pub mod publishing;
|
|
pub mod health;
|
|
pub mod resource_usage;
|
|
pub mod resources;
|
|
mod rss;
|
|
mod serving;
|
|
pub mod summarize;
|
|
pub mod ws;
|
|
pub mod mixer;
|
|
pub mod orchestration_trigger;
|
|
pub mod script_compiler;
|
|
pub mod tiptap;
|
|
pub mod transcribe;
|
|
pub mod tts;
|
|
pub mod usage_overview;
|
|
pub mod user_usage;
|
|
mod workspace;
|
|
|
|
use axum::{extract::State, http::StatusCode, middleware, routing::{get, post}, Json, Router};
|
|
use serde::Serialize;
|
|
use sqlx::postgres::PgPoolOptions;
|
|
use sqlx::PgPool;
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
|
|
|
use auth::{AuthUser, JwksKeys};
|
|
use cas::CasStore;
|
|
use ws::WsBroadcast;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub db: PgPool,
|
|
pub jwks: JwksKeys,
|
|
pub cas: CasStore,
|
|
pub index_cache: publishing::IndexCache,
|
|
pub dynamic_page_cache: publishing::DynamicPageCache,
|
|
pub maintenance: maintenance::MaintenanceState,
|
|
pub priority_rules: resources::PriorityRules,
|
|
pub metrics: metrics::MetricsCollector,
|
|
pub ws_broadcast: WsBroadcast,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct HealthResponse {
|
|
status: &'static str,
|
|
version: &'static str,
|
|
db: &'static str,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct MeResponse {
|
|
node_id: uuid::Uuid,
|
|
authentik_sub: String,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
// Strukturert logging: LOG_FORMAT=json for maskinlesbart, ellers human-readable.
|
|
let env_filter = EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "maskinrommet=info,tower_http=info".parse().unwrap());
|
|
let log_format = std::env::var("LOG_FORMAT").unwrap_or_default();
|
|
if log_format == "json" {
|
|
tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(tracing_subscriber::fmt::layer().json())
|
|
.init();
|
|
} else {
|
|
tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.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");
|
|
|
|
// 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");
|
|
|
|
// Vedlikeholdstilstand (oppgave 15.2)
|
|
let maintenance = maintenance::MaintenanceState::new();
|
|
|
|
// Last prioritetsregler fra PG (oppgave 15.5)
|
|
let priority_rules = resources::PriorityRules::load(&db)
|
|
.await
|
|
.expect("Kunne ikke laste prioritetsregler fra PG");
|
|
|
|
let index_cache = publishing::new_index_cache();
|
|
|
|
// Start jobbkø-worker i bakgrunnen (med ressursstyring, oppgave 15.5)
|
|
jobs::start_worker(db.clone(), cas.clone(), index_cache.clone(), maintenance.clone(), priority_rules.clone());
|
|
|
|
// Start periodisk CAS-pruning i bakgrunnen
|
|
pruning::start_pruning_loop(db.clone(), cas.clone());
|
|
|
|
// Start disk-overvåking i bakgrunnen (oppgave 15.5)
|
|
resources::start_disk_monitor(db.clone());
|
|
|
|
// Start planlagt publisering-scheduler i bakgrunnen
|
|
publishing::start_publish_scheduler(db.clone());
|
|
|
|
// Start A/B-evaluator i bakgrunnen (oppgave 14.17)
|
|
publishing::start_ab_evaluator(db.clone());
|
|
|
|
// Start nattlig bandwidth-parsing (oppgave 15.7)
|
|
bandwidth::start_bandwidth_parser(db.clone());
|
|
|
|
// Start periodisk CAS tmp-opprydding (oppgave 17.6)
|
|
cas::start_tmp_cleanup_loop(cas.clone());
|
|
let dynamic_page_cache = publishing::new_dynamic_page_cache();
|
|
let metrics = metrics::MetricsCollector::new();
|
|
|
|
// WebSocket broadcast-kanal og PG LISTEN/NOTIFY-lytter (oppgave 22.1)
|
|
let ws_broadcast = WsBroadcast::new();
|
|
ws::start_pg_listener(db.clone(), ws_broadcast.clone());
|
|
|
|
let state = AppState { db, jwks, cas, index_cache, dynamic_page_cache, maintenance, priority_rules, metrics, ws_broadcast };
|
|
|
|
// Ruter: /health er offentlig, /me krever gyldig JWT
|
|
let app = Router::new()
|
|
.route("/health", get(health))
|
|
.route("/me", get(me))
|
|
// WebSocket-endepunkt for sanntid via PG LISTEN/NOTIFY (oppgave 22.1)
|
|
.route("/ws", get(ws::ws_handler))
|
|
.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/delete_edge", post(intentions::delete_edge))
|
|
.route("/intentions/set_slot", post(intentions::set_slot))
|
|
.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/editorial_board", get(queries::query_editorial_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/ai_process", post(intentions::ai_process))
|
|
.route("/intentions/create_ai_preset", post(intentions::create_ai_preset))
|
|
.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/presentation_elements", get(queries::query_presentation_elements))
|
|
.route("/query/transcription_versions", get(queries::query_transcription_versions))
|
|
.route("/query/segments_version", get(queries::query_segments_version))
|
|
.route("/intentions/audio_analyze", post(intentions::audio_analyze))
|
|
.route("/intentions/audio_process", post(intentions::audio_process))
|
|
.route("/intentions/ab_override", post(publishing::ab_override))
|
|
// Systemvarsler (oppgave 15.1)
|
|
.route("/intentions/create_announcement", post(intentions::create_announcement))
|
|
.route("/intentions/expire_announcement", post(intentions::expire_announcement))
|
|
// Vedlikeholdsmodus (oppgave 15.2)
|
|
.route("/intentions/initiate_maintenance", post(intentions::initiate_maintenance))
|
|
.route("/intentions/cancel_maintenance", post(intentions::cancel_maintenance))
|
|
.route("/admin/maintenance_status", get(intentions::maintenance_status))
|
|
// Jobbkø-oversikt (oppgave 15.3)
|
|
.route("/admin/jobs", get(intentions::list_jobs))
|
|
.route("/intentions/retry_job", post(intentions::retry_job))
|
|
.route("/intentions/cancel_job", post(intentions::cancel_job))
|
|
// Ressursstyring (oppgave 15.5)
|
|
.route("/admin/resources", get(intentions::resource_status))
|
|
.route("/admin/resources/disk", get(intentions::resource_disk))
|
|
.route("/admin/resources/update_rule", post(intentions::update_priority_rule))
|
|
// AI Gateway-konfigurasjon (oppgave 15.4)
|
|
.route("/admin/ai", get(ai_admin::ai_overview))
|
|
.route("/admin/ai/usage", get(ai_admin::ai_usage))
|
|
.route("/admin/ai/update_alias", post(ai_admin::update_alias))
|
|
.route("/admin/ai/create_alias", post(ai_admin::create_alias))
|
|
.route("/admin/ai/update_provider", post(ai_admin::update_provider))
|
|
.route("/admin/ai/create_provider", post(ai_admin::create_provider))
|
|
.route("/admin/ai/delete_provider", post(ai_admin::delete_provider))
|
|
.route("/admin/ai/update_routing", post(ai_admin::update_routing))
|
|
.route("/admin/ai/delete_routing", post(ai_admin::delete_routing))
|
|
// Forbruksoversikt (oppgave 15.8)
|
|
.route("/admin/usage", get(usage_overview::usage_overview))
|
|
// Brukersynlig forbruk (oppgave 15.9)
|
|
.route("/my/usage", get(user_usage::my_usage))
|
|
.route("/my/workspace", get(workspace::my_workspace))
|
|
.route("/query/node_usage", get(user_usage::node_usage))
|
|
// Serverhelse-dashboard (oppgave 15.6)
|
|
.route("/admin/health", get(health::health_dashboard))
|
|
.route("/admin/health/logs", get(health::health_logs))
|
|
.route("/query/audio_info", get(intentions::audio_info))
|
|
.route("/query/job_status", get(queries::query_job_status))
|
|
.route("/pub/{slug}/feed.xml", get(rss::generate_feed))
|
|
.route("/pub/{slug}", get(publishing::serve_index))
|
|
// A/B-testing: klikk-sporing (oppgave 14.17)
|
|
.route("/pub/{slug}/t/{article_id}", get(publishing::track_click))
|
|
// Dynamiske sider: kategori, arkiv, søk, om (oppgave 14.15)
|
|
.route("/pub/{slug}/kategori/{tag}", get(publishing::serve_category))
|
|
.route("/pub/{slug}/arkiv", get(publishing::serve_archive))
|
|
.route("/pub/{slug}/sok", get(publishing::serve_search))
|
|
.route("/pub/{slug}/om", get(publishing::serve_about))
|
|
.route("/pub/{slug}/preview/{theme}", get(publishing::preview_theme))
|
|
// NB: {article_id} catch-all må komme etter de spesifikke rutene
|
|
.route("/pub/{slug}/{article_id}", get(publishing::serve_article))
|
|
// Custom domains: Caddy on-demand TLS callback
|
|
.route("/internal/verify-domain", get(custom_domain::verify_domain))
|
|
// Custom domains: domene-basert serving (Caddy proxyer hit)
|
|
.route("/custom-domain/index", get(custom_domain::serve_custom_domain_index))
|
|
.route("/custom-domain/feed.xml", get(custom_domain::serve_custom_domain_feed))
|
|
// Dynamiske sider for custom domains
|
|
.route("/custom-domain/kategori/{tag}", get(custom_domain::serve_custom_domain_category))
|
|
.route("/custom-domain/arkiv", get(custom_domain::serve_custom_domain_archive))
|
|
.route("/custom-domain/sok", get(custom_domain::serve_custom_domain_search))
|
|
.route("/custom-domain/om", get(custom_domain::serve_custom_domain_about))
|
|
.route("/custom-domain/{article_id}", get(custom_domain::serve_custom_domain_article))
|
|
// Mixer-kanaler
|
|
.route("/intentions/create_mixer_channel", post(mixer::create_mixer_channel))
|
|
.route("/intentions/set_gain", post(mixer::set_gain))
|
|
.route("/intentions/set_mute", post(mixer::set_mute))
|
|
.route("/intentions/toggle_effect", post(mixer::toggle_effect))
|
|
.route("/intentions/set_mixer_role", post(mixer::set_mixer_role))
|
|
// Observerbarhet (oppgave 12.1)
|
|
.route("/metrics", get(metrics::metrics_endpoint))
|
|
.layer(middleware::from_fn_with_state(state.clone(), metrics::latency_middleware))
|
|
.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-tilkobling.
|
|
async fn health(State(state): State<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
|
|
sqlx::query("SELECT 1")
|
|
.execute(&state.db)
|
|
.await
|
|
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
Ok(Json(HealthResponse {
|
|
status: "ok",
|
|
version: env!("CARGO_PKG_VERSION"),
|
|
db: "connected",
|
|
}))
|
|
}
|
|
|
|
/// Beskyttet endepunkt — returnerer autentisert brukers node_id.
|
|
/// Brukes for å verifisere at auth-middleware fungerer.
|
|
async fn me(user: AuthUser) -> Json<MeResponse> {
|
|
Json(MeResponse {
|
|
node_id: user.node_id,
|
|
authentik_sub: user.authentik_sub,
|
|
})
|
|
}
|