# Sidelinja - Architecture Decision Record & System Overview **Dette dokumentet definerer den overordnede arkitekturen, teknologistacken og datamodellen for Sidelinja-suiten. AI-agenter (som Claude Code) SKAL lese og forstå dette dokumentet før de foreslår endringer, skriver kode eller gjør arkitektoniske valg.** ## 1. Visjon og Konsept Sidelinja er ikke bare en podcast-host; det er et **redaksjonelt operativsystem** og en **kunnskapsgraf**. Målet er å bygge en plattform som sømløst integrerer research, asynkron kommunikasjon (chat), sanntids innspilling (Lyd/Video) og automatisert publisering. Visjonen inkluderer også at plattformen skal fungere som en "live co-host" (virtuell assistent) under innspilling ved å boble opp relevant informasjon fra kunnskapsgrafen i sanntid. Systemet er bygget for full datakontroll, eierskap og minimal bruk av lukkede tredjepartstjenester. ## 2. Infrastruktur og DevOps * **Produksjonsserver:** Hetzner VPS (Ubuntu, 8 vCPU, 16 GB RAM, 320 GB SSD). Kapasiteten er tilstrekkelig for nåværende behov. Ved behov kan VPS-en dobles (16 vCPU, 32 GB). Mest CPU-krevende tjenester er faster-whisper og LiveKit under samtidig bruk — disse bør overvåkes først ved kapasitetsproblemer. * **CPU-ressursstyring:** `faster-whisper` (medium) bruker ~18 min på 30 min lyd og kan stjele CPU fra LiveKit under live-innspilling (risiko for audio glitches). To-lags beskyttelse: 1. **Docker cgroups (harde grenser):** `docker-compose.yml` skal sette `deploy.resources.limits` på worker-containere: maks 4 CPU og 8 GB RAM for Whisper-workers, slik at LiveKit og PostgreSQL alltid har garantert kapasitet. 2. **Applikasjonsnivå (dynamisk):** Rust-workeren implementerer en "Resource Governor" som reduserer Whisper-tråder ytterligere (f.eks. `--threads 2`) når et LiveKit-rom er aktivt. Sjekkes via LiveKit room-status i jobbkøen. * **Diskstrategi:** 320 GB SSD fylles raskt med råopptak og MP3-er. Tre tiltak: 1. **Block Storage:** Mediafiler serveres fra en separat Hetzner Block Storage-volum montert på `/srv/sidelinja/media/`, skalerbart uavhengig av OS-disken. 2. **S3-abstraksjon:** SvelteKit sin filopplasting bør abstrahere lagring bak et S3-kompatibelt grensesnitt (Hetzner Object Storage eller Cloudflare R2), slik at vi kan flytte til ekstern lagring uten å endre applikasjonskode. Caddy kan proxy-e eller redirecte til S3 for servering. 3. **Arkiveringspolicy:** Råopptak eldre enn 6 mnd flyttes automatisk til Object Storage via nattlig jobb. Kun ferdig-redigerte MP3-er beholdes lokalt for rask servering. * **Orkestrering:** Docker / Docker Compose. Alle tjenester kjører i isolerte containere på et internt Docker-nettverk. * **Reverse Proxy & Webserver:** **Caddy**. Håndterer all innkommende trafikk for flere domener, automatisk HTTPS (Let's Encrypt), og ruting til interne containere. Port 80/443 er de *eneste* portene som er eksponert mot internett. * **Domener:** - `sidelinja.org` — Hovedapplikasjon (SvelteKit, media, SpacetimeDB, LiveKit) - `auth.sidelinja.org` — Authentik SSO (felles for alle domener) - `git.sidelinja.org` — Forgejo - `vegard.info` — Separat nettsted, deler SSO med Sidelinja * **Kildekode og CI/CD:** **Forgejo** (Selv-hostet Git). To repos: - `sidelinja/server` — app-kode, infra, arkitektur, config - `sidelinja/sidelinja` — podcastinnhold (transkripsjoner, show notes, research) * **Utvikling og Utrulling:** Kode skrives og testes lokalt i WSL2. Infrastruktur-config (docker-compose, Caddy, Authentik) endres direkte i prod. Deploy: push til Forgejo → SSH pull på server → `docker compose up -d --build`. ### 2.1 Serverstruktur (Produksjon) All persistent data, konfigurasjon og kildekode monteres via Docker Bind Mounts til en fast struktur på vertssystemet, typisk `/srv/sidelinja/`. Dette muliggjør granulert backup. /srv/sidelinja/ ├── docker-compose.yml # Orkestrering ├── .env # Miljøvariabler (IKKE i Git) ├── config/ # Konfigurasjonsfiler (Caddy, Authentik, etc.) ├── data/ # Databaser (Postgres, SpacetimeDB, Forgejo) ├── media/ # Lydfiler (podcast, råopptak) └── logs/ # Caddy access-logger, app-logger ### 2.2 Dataklassifisering og backup-strategi Alle data i Sidelinja faller i én av fire kategorier. Nye komponenter og features MÅ klassifisere sin data etter dette skjemaet. #### Kategori 1: Kritisk — krever backup Data som ikke kan gjenskapes. Tap = permanent informasjonstap. | Data | Lagring | Backup | |---|---|---| | PostgreSQL (kunnskapsgraf, brukere, metadata, episoder) | `data/postgres/` | Daglig pg_dump + WAL-arkivering (PITR) | | Lydfiler (MP3, råopptak) | `media/` | Daglig fil-backup | | `.env` (hemmeligheter) | `/srv/sidelinja/.env` | Manuell kopi, ikke i Git | #### Kategori 2: Gjenskapbar fra Git Data som lever i Forgejo og kan klones/pulles på nytt. | Data | Lagring | Restore | |---|---|---| | Kildekode | Git (Forgejo) | `git clone` | | Transkripsjoner (SRT master) | Git (Forgejo) | `git clone` | | PG-skjema + migrasjoner | Git (Forgejo) | Kjør migrasjoner | | Config-filer (Caddyfile, etc.) | Git (Forgejo) | `git clone` | | Forgejo-data (repos, issues) | `data/forgejo/` | **Daglig backup som sikkerhetsnett** — kan gjenskapes men tidkrevende | #### Kategori 3: Avledet — kan regenereres Data som er deterministisk avledet fra kategori 1 eller 2. Tåler tap — regenereres automatisk. | Data | Kilde | Regenerering | |---|---|---| | Ren tekst (transkripsjoner) | SRT i Git | Rust-worker parser SRT | | Segmenter (tidsstemplet, grafkoblet) | SRT i Git | Rust-worker parser SRT | | Full-text søkeindeks | SRT i Git | Rebuild fra Git | | SRT → PG-cache | SRT i Git | Reimport via webhook | | Redis-cache | Applikasjonsdata | Regenereres automatisk | | Caddy-sertifikater | Let's Encrypt | Regenereres automatisk | #### Kategori 4: Flyktig — tåler tap, TTL-styrt Arbeidsdata med begrenset levetid. Ryddes automatisk. | Data | Lagring | TTL | Formål | |---|---|---|---| | Live-transkripsjonslogg | PostgreSQL | 30 dager | Feilsøking av live-assistent | | Caddy access-logger | `logs/caddy/` | 90 dager | Podcast-statistikk (batch-prosesseres først) | | Jobbkø-historikk (fullførte jobber) | PostgreSQL | 30 dager | Feilsøking | | Whisper-modeller | `.docker-data/` (lokal) | Ingen TTL | Re-download fra HuggingFace ved behov | #### Off-site backup (kritisk) Lokal backup på samme server beskytter kun mot logiske feil (slettet fil, korrupt dump). Ved fysisk diskfeil eller nodefeil hos Hetzner tapes både produksjon og backup. Kategori 1-data **må** pushes ut av serveren: | Data | Mål | Verktøy | Frekvens | |---|---|---|---| | PostgreSQL-dumper | Hetzner Object Storage (S3-kompatibel) | `rclone sync` | Daglig etter pg_dump | | Lydfiler (media/) | Hetzner Object Storage | `rclone sync` (inkrementell) | Daglig | | `.env` | Kryptert kopi i Object Storage | `gpg -c` + `rclone` | Ved endring | **Retensjon off-site:** 90 dager for PG-dumper, ubegrenset for media. Kostnad: ~€5/mnd for 100 GB på Hetzner Object Storage. #### PostgreSQL WAL-arkivering (Point-In-Time Recovery) Daglig pg_dump kl. 03:00 betyr opptil 24 timers datatap ved korrupsjon midt på dagen. For å redusere dette til minutter, settes opp kontinuerlig WAL-arkivering: * **Verktøy:** pgBackRest eller WAL-G (foretrukket for S3-kompatibel lagring) * **Flyt:** PostgreSQL streamer WAL-segmenter kontinuerlig til Hetzner Object Storage. Ved behov kan databasen gjenopprettes til et vilkårlig tidspunkt (PITR). * **Konfigurasjon:** `archive_mode = on`, `archive_command` peker på pgBackRest/WAL-G som pusher til S3. * **Full backup:** Ukentlig full backup via pgBackRest, daglige inkrementelle. WAL-segmenter fyller gapet. * **Recovery:** `pgbackrest restore --target-time="2026-03-15 13:59:00"` gjenoppretter til minuttet før krasjet. * **Kostnad:** Minimal — WAL-segmenter er komprimerte og kompakte. ~1-5 GB/mnd avhengig av skriveaktivitet. #### Retningslinjer for nye komponenter Når en ny feature eller komponent introduserer data: 1. **Klassifiser** — hvilken kategori faller dataen i? 2. **Dokumenter** — legg til i tabellen over 3. **Implementer TTL** for kategori 4 — aldri la flyktig data vokse ubegrenset 4. **Aldri dupliser kilde** — avledede data (kategori 3) skal kunne slettes og regenereres 5. **Aldri backup avledet data** — det er bortkastet plass og skaper falsk trygghet ### 2.3 Lokalt Utviklingsmiljø Det lokale miljøet (WSL2) er et **kodeutviklingsmiljø**, ikke en replika av prod. Infrastruktur-config (docker-compose, Caddy, Authentik) testes direkte i prod. **Komplett oppsett: `docs/setup/lokal.md`.** * **Docker Compose Dev:** `docker-compose.dev.yml` spinner opp PostgreSQL, Redis, SpacetimeDB, Caddy, Whisper og AI Gateway lokalt. Volumene er flyktige (`.docker-data/`, gitignored). * **Docker Compose Prod:** `/srv/sidelinja/docker-compose.yml` kjører PostgreSQL, Redis, Caddy, Authentik, Forgejo og SvelteKit (`web`-container bygget fra `web/Dockerfile`). * **SvelteKit HMR:** Kjøres utenfor Docker lokalt for rask iterasjon. I prod bygges som Docker-container med adapter-node. * **Rust Workers:** Kompileres og kjøres lokalt med `cargo run`. * **AI Gateway / Whisper:** Lokale instanser for eksperimentering og prompt-testing. * **Forgejo/Authentik:** Kjører IKKE lokalt — push direkte til prod-Forgejo. ## 3. Teknologistack Vi følger et "Best tool for the job"-prinsipp, med en sterk preferanse for minnesikkerhet, ytelse og rene grensesnitt. * **Backend/Automasjon:** **Rust**. Brukes som bakgrunnsworkers (jobbkø), logg-parsing og SpacetimeDB-moduler. Rust er *ikke* en API-server — SvelteKit server-side håndterer all HTTP-kommunikasjon og PG-tilgang direkte (se `docs/infra/api_grensesnitt.md`). * **Frontend / UI:** **SvelteKit** (med TypeScript). Bygges som en PWA. Valgt for ytelse og enkel integrasjon med WebRTC og vanilla JS-biblioteker. * **Sanntids Lyd/Video:** **LiveKit** (Selv-hostet). Håndterer WebRTC, fler-bruker videochat og opptak i det virtuelle "studioet". * **AI / Prosessering:** `faster-whisper` (lokal transkripsjon) og **LiteLLM** (AI Gateway — sentralisert ruting til Gemini, Claude, Grok, OpenRouter). All AI-kode peker på `http://ai-gateway:4000/v1`, aldri direkte til leverandører. Se `docs/infra/ai_gateway.md`. * **SSO / Autentisering:** **Authentik** (Selv-hostet). Sentralisert rollestyring. ## 4. Den To-delte Databasestrategien 1. **PostgreSQL (Historikk & Kunnskapsgraf):** Én sentralisert instans i Docker for brukerkontoer, Git-metadata, aggregert statistikk og Kunnskapsgrafen (Artikler, Faktoider). 2. **SpacetimeDB (Sanntidsbuffer):** In-memory database for live chat, status på episoder, og live-oppdateringer i studio. Klienten (Svelte) lytter direkte på SpacetimeDB. SpacetimeDB er en **ren sanntidsbuffer** — all data synkes til PostgreSQL innen ~5 sek. PostgreSQL-skjemaet dekker alle SpacetimeDB-tabeller, slik at SpacetimeDB kan erstattes med PG `LISTEN/NOTIFY` + SSE uten arkitekturendring. Se `docs/infra/synkronisering.md` §1.1–1.2. 3. **Synkronisering:** Event-drevet med ~5 sek forsinkelse. SpacetimeDB er autoritativ for sanntidsdata (chat, kanban), PostgreSQL for persistent data (kunnskapsgraf, metadata). Detaljer i `docs/infra/synkronisering.md`. ### 4.1 Workspace-modellen (Multi-tenancy) Sidelinja er designet for multi-tenancy fra dag én. Hver organisasjon/podcast opererer i sin egen **workspace** — en streng datasilo som sikrer full isolasjon av kunnskap og arbeidsflyt. #### Prinsipp: Ingenting deles på tvers * **PostgreSQL:** Supertabellen `nodes` og `graph_edges` har obligatorisk `workspace_id`. Row-Level Security (RLS) sikrer at spørringer aldri lekker data mellom workspaces. SvelteKit setter workspace-kontekst via `SET LOCAL` i en transaksjon: ```sql BEGIN; SET LOCAL app.current_workspace_id = 'uuid'; SELECT ...; COMMIT; ``` **Viktig:** Bruk alltid `SET LOCAL` (ikke `SET`) — lokal scope forsvinner automatisk når transaksjonen avsluttes. Med vanlig `SET` overlever verdien på tilkoblingen, og ved connection pooling (PgBouncer, driver-pool) kan neste bruker arve feil workspace_id. `SET LOCAL` eliminerer denne risikoen fullstendig. * **SpacetimeDB:** WebSocket-tilkoblinger bærer et `workspace_id`-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. * **Mediefiler:** Lagres i `/srv/sidelinja/media/{workspace_slug}/`. Caddy ruter basert på domene. * **Transkripsjoner:** Ett Forgejo-repo per workspace for SRT-filer. * **Jobbkø:** Alle jobber i `job_queue` merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og isolerer via applikasjonskode. * **AI-prompts:** Whisper `initial_prompt` og LLM system-prompts lagres i `workspaces.settings` (JSONB) per workspace. #### Tilgangsstyring Authentik styrer gruppetilhørighet. SvelteKit mapper innlogget brukers Authentik-grupper til tilgjengelige workspaces via `workspace_members`-tabellen. UI-et har en global "Workspace-switcher" som tømmer lokal state (hard reset) ved bytte for å forhindre visuell datalekkasje. #### Globale ressurser (unntak fra silo) * `relation_types` — deles på tvers (systemdefinerte relasjonstyper). * `users` — Authentik-IDer er globale, workspace-tilhørighet styres via `workspace_members`. * Valgomat-modulen er publikumsrettet og opererer potensielt på tvers av workspaces. Eierskapsmodellen avklares ved implementering (se `docs/concepts/valgomaten.md`). ## 5. Datamodell: Kunnskapsgrafen Systemet er bygget rundt **Temaer** og **Aktører**, ikke episoder. Dette bygger et asynkront research-arkiv. Alle entiteter arver UUID fra en felles `nodes`-supertabell som gir ekte FK-integritet i grafmodellen (detaljer i `docs/features/kunnskapsgraf_og_relasjoner.md`). * **Tema (Saker):** Levende konsepter ("Skolepolitikk"). * **Aktør (Entity):** Personer eller organisasjoner ("Jonas Gahr Støre"). * **Faktoide (Factoid):** En atomisk bit med informasjon koblet til Aktører/Temaer ("Søkte jobb i AP i 2011"). * **Episode:** Et tidsbegrenset prosjekt ("Episode 42") som samler et utvalg av aktuelle *Temaer*. * **Segment:** En tidsavgrenset del av en episode med egen transkripsjon, koblet til Temaer/Aktører i grafen. Muliggjør presise oppslag ("hva sa vi om X i Episode 42?"). * **Research-klipp:** Råtekst renset av AI, koblet til Temaer/Aktører. **Transkripsjoner — eierskapsmodell:** * **Git (Forgejo)** er kilde til sannhet. Master-formatet er **SRT** (SubRip) — et etablert undertekstformat med tidsstempler som er redigerbart, diffbart og lett å parse. Whisper leverer SRT direkte (`response_format=srt`). * **PostgreSQL** lagrer alt avledet fra SRT-kilden: - **Ren tekst** — strippes fra SRT for lesbart publiseringsdokument - **Segmenter** — tidsstemplede utdrag koblet til Aktører/Temaer i kunnskapsgrafen - **Full-text søkeindeks** — for oppslag på tvers av episoder * **Flyt:** Whisper → SRT → Git commit → Forgejo webhook → Rust-worker parser SRT → avledede formater i PG. SvelteKit serverer SRT fra Git og avledet innhold fra PG. ## 6. Podcast Hosting og Distribusjon * **Lagring:** MP3-filer lagres flatt i `/srv/sidelinja/media/`. Ingen lydfiler i databaser. * **Servering (intern):** Caddy serverer media-mappen for intern bruk. MÅ ha `Accept-Ranges: bytes` aktivert for podcast-streaming. * **Servering (publisert):** RSS-feedens ``-URL-er **MÅ** peke på et CDN (Cloudflare eller Hetzner CDN), ikke direkte til Caddy. Ved ny episode vil podcast-aggregatorer (Apple Podcasts, Spotify) og abonnenter laste ned samtidig — 1000 lyttere × 50 MB = 50 GB burst som kveler hele serveren. CDN absorberer trafikken og beskytter den redaksjonelle arbeidsflaten. S3-abstraksjonen (se §2) er grunnlaget — CDN legges foran S3-bucketen. * **RSS-Feed:** Genereres av SvelteKit og leveres statisk eller dynamisk med aggressiv caching. ## 7. Planlagte Funksjoner Detaljerte spesifikasjoner ligger i `docs/concepts/` (brukeropplevelser) og `docs/features/` (byggeklosser). ### Konsepter (brukeropplevelser) * **Studioet:** Podcast-innspilling med LiveKit, live AI faktoid-oppslag og Aha-markør. * **Møterommet:** LiveKit-basert møterom med AI-referent, off-the-record, whiteboard, scratchpad og søkbar historikk. * **Redaksjonen:** Daglig arbeidsflate med trådet chat (channels), Kanban, show notes og AI-behandling av tekst. * **Podcastfabrikken:** Automatisert publiseringspipeline — Whisper, AI-metadata, RSS, cache-busting. * **Kunnskapsgrafen:** Visuell utforsking og redigering av kunnskapsnettverk (D3.js/Vis.js). * **Valgomaten:** Publikumsrettet, crowdsourced valgomat med PCA og Sidelinja Explorer. * **Den Asynkrone Gjesten:** Tidsbegrenset lenke til gjester for asynkrone lydopptak som lander i redaksjonens arbeidsflyt. ### Features (byggeklosser) Chat (channels), Kanban, Kalender, Notater/Scratchpad, Whiteboard, Live transkripsjon, Live AI (faktoid + referent), Visuell graf, Lydmeldinger & Diktering, Podcast-statistikk, Kunnskaps-Bridge (cross-workspace), Prompt-Laboratorium, Graf-vedlikehold (nattlig jobb som finner isolerte noder og foreslår koblinger basert på co-occurrence i transkripsjoner). AI-behandling av tekst (research, oppsummering, fakta-uttrekk) er en universell funksjon i editoren — se `docs/proposals/editor.md`. ## 8. Bygge-rekkefølge (Avhengighetskart) ### Lag 0 — Infrastruktur - [x] Produksjonsserver: PostgreSQL, Caddy, Authentik, Forgejo, Redis - [x] Lokalt utviklingsmiljo: docker-compose.dev.yml (PostgreSQL, Redis, Caddy) - [x] Rust toolchain (lokal + server) - [x] faster-whisper-server (lokal, testet med medium + prompt) ### Lag 1 — Fundament (ingen avhengigheter) - [x] Workspace-modell (workspaces, workspace_members, RLS-policies) - [x] PostgreSQL-skjema (nodes m/workspace_id, graph_edges, job_queue, messages, channels, media_files) - [x] SpacetimeDB grunnoppsett (Docker, Rust WASM-modul, TypeScript-bindings) - [x] SvelteKit skjelett med Authentik-integrasjon + Workspace-switcher - [x] AI Gateway (LiteLLM) oppsett + config - [x] Git-repostruktur for transkripsjoner (ett repo per workspace) — spec i `docs/concepts/podcastfabrikken.md` §5.2 ### Lag 2 — Kjernekomponenter (krever Lag 1) - [ ] **Meldingsboks-migrasjon** (0005): Universell diskusjonsprimitiv som erstatter separate modeller for chat, kanban-kort, kalenderhendelser, faktoider og notater. Se `docs/features/meldingsboks.md`. Migrerer eksisterende data fra `kanban_cards`, `calendar_events`, `factoids`, `notes` til ny `messages`-tabell + view-config-tabeller. **Bør gjøres før videre arbeid på chat/kanban/kalender for å unngå bygge-på-gammel-modell.** - [ ] Jobbkø-worker (Rust) - [ ] Kunnskapsgraf CRUD (SvelteKit server-side) - [ ] pgvector-migrasjon (0006): `CREATE EXTENSION vector;` + embedding-kolonner på nodes — gjøres tidlig for å unngå smertefull migrasjon i Lag 4 - [ ] **RLS Leak Hunter i CI** (se `docs/setup/migration_safety.md`) — **KRITISK, bør være første CI-steg.** En glemt RLS-policy på en ny tabell (f.eks. `guest_tokens`, fremtidige valgomat-tabeller) kan lekke data uten at noen merker det. Automatisér med testcontainers + fullstendig leak-test mot alle tabeller med `workspace_id`. - [~] Chat med channels (PG-adapter + SpacetimeDB hybrid-adapter ferdig — **refaktoreres til meldingsboks-modell**, sync-worker gjenstår) - [~] Kanban (PG-adapter ferdig med drag & drop — **refaktoreres til meldingsboks + kanban_card_view**, SpacetimeDB-sync gjenstår) - [~] Kalender (PG-adapter ferdig med månedsvisning — **refaktoreres til meldingsboks + calendar_event_view**, SpacetimeDB-sync gjenstår) - [~] Notater/Scratchpad (PG-adapter ferdig — **refaktoreres til meldingsboks**, rich text og SpacetimeDB-sync gjenstår) - [ ] **Merge Entities (admin-verktøy):** Sammenslåing av duplikate entiteter (#Jonas, #Støre, #Jonas Gahr Støre → én node). Omdirigerer alle `graph_edges` og `messages`-mentions, legger til navn som alias, sletter duplikaten. Uten dette fragmenteres kunnskapsgrafen raskt og serendipity-effekten dør. - [ ] Lydmeldinger & Diktering (opptak + Whisper + AI-opprydding) - [ ] Prompt-Laboratorium (prompt-testing mot egne data) - [ ] Promptfoo testsett for første jobbtyper (norsk testdata) ### Lag 3 — Features (krever Lag 2) - [ ] Podcastfabrikken (Whisper SRT → Git → PG-avledede formater + episodeside) - [ ] Editor med AI-behandling (universell editor + jobbkø + AI Gateway, se `docs/proposals/editor.md`) - [ ] Podcast-Statistikk (jobbkø + episoder) - [ ] Whiteboard (sanntids frihåndstavle i SpacetimeDB) - [ ] Den Asynkrone Gjesten (gjeste-tokens + lydmeldinger) ### Lag 4 — Avansert (krever Lag 3) - [ ] Studioet: Live AI-Assistent (fylt kunnskapsgraf + LiveKit + Whisper small) - [ ] Møterommet: AI-Referent (LiveKit + Whisper + møte-oppsummering) - [ ] Visuell Kunnskapsgraf (D3.js/Vis.js graf-visning) - [ ] Kunnskaps-Bridge (pgvector, cross-workspace discovery) - [ ] Graf-vedlikehold (nattlig jobb: finn isolerte noder, foreslå koblinger basert på co-occurrence) - [ ] Valgomat (selvstendig, lav prioritet) ## 9. Observabilitet ### 9.1 Helse Alle Docker-containere skal ha `healthcheck` definert i `docker-compose.yml`: - PostgreSQL: `pg_isready` - SpacetimeDB: TCP-sjekk mot intern port - Caddy: `curl -f http://localhost/health` - SvelteKit: `curl -f http://localhost:3000/health` - Rust Workers: Heartbeat-rad i `job_queue` (en `worker_heartbeat`-jobb som re-enqueuer seg selv hvert minutt — fravær betyr død worker) ### 9.2 Logging - **Format:** Strukturert JSON fra alle komponenter (Rust, SvelteKit, Caddy) - **Plassering:** `/srv/sidelinja/logs/` med undermapper per tjeneste - **Rotasjon:** Standard Linux logrotate, daglig rotasjon, 30 dagers retensjon - Caddy podcast-logger behandles separat av statistikk-workeren (se `docs/features/podcast_statistikk.md`) ### 9.3 Jobbkø-overvåking - Admin-visning i SvelteKit som viser `job_queue`-status (pending, running, error-count) - Feilede jobber (`status = 'error'`) poster automatisk en varslingsmelding til et dedikert system-tema i Redaksjonens chat, slik at redaksjonen ser det i sin daglige arbeidsflate ### 9.4 Observability Dashboard SvelteKit-appen inkluderer en intern admin-side (`/admin/observability`) som samler: - **Container-status:** Healthcheck-resultater fra Docker (via `docker compose ps` / Docker socket) - **Jobbkø:** Pending/running/error-count med sparkline-grafer (siste 24t) - **AI Gateway:** Token-bruk per jobbtype, kostnad per workspace, failover-hendelser (fra LiteLLMs innebygde logging). Inkluderer workspace-budsjett status (se `docs/infra/ai_gateway.md` §6). - **Database-ytelse:** `pg_stat_statements`-utvidelse for query-kostnader per workspace/feature. Identifiserer hvilke features som spiser CPU/RAM (f.eks. tunge graf-spørringer, valgomat-PCA). - **Disk/Minne:** Mediamappe-størrelse per workspace, PG-størrelse, SpacetimeDB-minnebruk (med graf over tid) - **Sikkerhet:** Siste secret-rotasjon timestamp (`.env`-endringer), RLS Leak Hunter siste kjøring, antall aktive guest-tokens - **SpacetimeDB:** Minnebruk-graf, `sync_outbox`-størrelse (indikerer sync-etterslep), tilkoblede klienter per workspace Ingen eksterne tjenester (Prometheus, Grafana) — alt bygges som SvelteKit-sider med data hentet server-side fra PG, Docker og LiteLLM. Konsistent med self-hosted-filosofien. ### 9.5 Ekstern helsesjekk (utenfor stacken) Intern overvåking er verdiløs hvis hele serveren er nede. En ekstern uptime-monitor **utenfor** Hetzner-stacken skal polle følgende endepunkter og varsle ved feil: | Endepunkt | Sjekk | Varsel | |---|---|---| | `https://sidelinja.org/api/health` | HTTP 200 | E-post/push ved 2 min nedetid | | `https://auth.sidelinja.org` | HTTP 200 | E-post/push ved 2 min nedetid | | `sidelinja.org:443` | SSL-utløp < 7 dager | E-post | **Implementering:** Bruk en gratis/billig ekstern tjeneste (UptimeRobot, Hetrixtools, eller lignende) — dette er det eneste unntaket fra self-hosted-filosofien, da en helsesjekk per definisjon må leve utenfor systemet den overvåker. ### 9.6 Ingen andre eksterne observability-tjenester Utover ekstern helsesjekk (§9.5) skjer all overvåking og varsling internt i Sidelinja-suiten. Ingen avhengighet til Discord, Slack eller andre tredjepartstjenester. ## 10. Erfaringslogg Mappen `docs/erfaringer/` samler praktiske lærdommer fra implementering — ikke hva vi valgte, men hva vi lærte som ikke er åpenbart fra koden. Formålet er å treffe raskere blink med neste komponent. Nye komponenter BØR legge til erfaringer etter ferdig implementering. Innhold per mars 2025: - `svelte5_reaktivitet.md` — $state-getters, SSR-feller, polling-mønster - `spacetimedb_integrasjon.md` — SDK-konvensjoner, BigInt, Rust borrow-feller - `adapter_moenster.md` — Hybrid PG+SpacetimeDB, anti-patterns, anbefaling for neste komponent - `authentik_oidc.md` — Sub-claim er SHA256, @auth/sveltekit JWT-quirks, redirect URI ## 11. Testing og Utrulling ### 11.1 Teststrategi Sidelinja har tre testnivåer. Alle nye features MÅ dekke de relevante nivåene. | Nivå | Hva | Verktøy | Kjøres | |---|---|---|---| | **Enhetstester** | Rust-logikk (jobbkø, sync, parsing) | `cargo test` | Lokalt + CI | | **Migrasjonstester** | PG-skjema: opp-migrering mot tom DB + seed + spørringer | `psql` + testskript | Lokalt + CI | | **Prompt-regresjon** | LLM-prompts mot norske testsett | Promptfoo | Lokalt + CI (månedlig cron) | **E2E-tester** (SvelteKit → PG → SpacetimeDB) innføres når Lag 2 er stabilt. Inntil da er manuell testing i dev-miljøet tilstrekkelig — kodebasen er liten nok til at det er effektivt. ### 11.2 Migrasjonstesting Migrasjonsfiler i `migrations/` testes automatisk: 1. Spin opp en tom PostgreSQL-container 2. Kjør alle migrasjoner sekvensielt 3. Kjør seed-data (fixture) med testdata 4. Verifiser: RLS-policies blokkerer cross-workspace-tilgang, indekser eksisterer, constraints holder 5. Kjør `pg_dump --schema-only` og diff mot forrige kjente state ### 11.3 Utrullingsprosedyre (Deployment Checklist) ``` 1. [ ] Alle tester grønne lokalt (cargo test, migrasjonstester, promptfoo) 2. [ ] Push til Forgejo (main branch) 3. [ ] SSH til prod: ssh sidelinja@157.180.81.26 4. [ ] cd /srv/sidelinja && git pull 5. [ ] Nye migrasjoner? → Kjør mot prod-PG (manuelt, verifiser først) 6. [ ] docker compose up -d --build 7. [ ] Verifiser healthchecks: docker compose ps (alle "healthy") 8. [ ] Smoke-test: curl https://sidelinja.org/health 9. [ ] Sjekk logs: docker compose logs --tail=50 ``` **Rollback:** `git revert` + ny deploy. Migrasjoner som endrer skjema MÅ ha en tilhørende down-migrering. ### 11.4 Rate Limiting Ressurskrevende endepunkter beskyttes mot overbruk: | Endepunkt | Begrensning | Mekanisme | |---|---|---| | LiveKit (romopprettelse) | Maks 5 aktive rom per workspace | Applikasjonslogikk i SvelteKit | | Whisper (transkripsjon) | Maks 3 samtidige jobber per workspace | Jobbkø-constraint (`max_concurrent_per_workspace`) | | AI Gateway (LLM-kall) | LiteLLMs innebygde rate limiting | `max_parallel_requests` i config.yaml | | Filopplasting (media) | Maks 500 MB per fil, 5 GB per workspace/dag | SvelteKit middleware | Caddy håndterer generell rate limiting (DDoS-beskyttelse) via `rate_limit`-direktivet. ## 12. AI Agent Guidelines (Instrukser for Claude Code) * **Start her:** Når du settes til å bygge en ny komponent, sikre alltid at det lokale utviklingsmiljøet (`docker-compose.dev.yml`) kjører først. * **Dokumentasjonsstandard:** Når du skal implementere en ny funksjon (feature), sjekk ALLTID om det finnes et dokument i `docs/features/.md` først. Oppdater disse dokumentene hvis arkitekturen for funksjonen endres. * **Ingen "gh" CLI:** Vi bruker Forgejo. For Pull Requests/Issues, bruk `tea` CLI. * **Deployment:** Kod og test lokalt i WSL. Push til Forgejo, logg inn via SSH for å pulle kode og restarte containere/tjenester (`docker compose up -d`). * **Asynkron AI:** Tyngre jobber (Whisper, OpenRouter) skal aldri blokkere web-forespørsler. Alle bakgrunnsjobber kjøres via den felles PostgreSQL-baserte jobbkøen (se `docs/infra/jobbkø.md`). * **Sikkerhet:** Forsøk aldri å eksponere databaseporter ut mot internett i Docker Compose-filer (hverken lokalt eller i prod). Port 80/443 (Caddy) er de eneste inngangsportene.