server/docs/arkitektur.md
vegard 74110e842c Dokumentasjon: oppdatert arkitektur, nye proposals og konsepter
Oppdatert basert på ekstern tilbakemelding. Nye proposals for
kildevern, podcasting 2.0, web clipper, waveforms, editor,
tekst-primitiv og avisvisning. Oppdatert meldingsboks med
slette-semantikk, entity resolution i kunnskapsgrafen, og
AI gateway med kildevern-modus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:45:24 +01:00

354 lines
No EOL
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.11.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 `<enclosure>`-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 <tjeneste>
```
**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/<feature-navn>.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.