diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index efdc6d6..e8f4362 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,7 +6,14 @@ 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). 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. +* **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:** @@ -38,7 +45,7 @@ Data som ikke kan gjenskapes. Tap = permanent informasjonstap. | Data | Lagring | Backup | |---|---|---| -| PostgreSQL (kunnskapsgraf, brukere, metadata, episoder) | `data/postgres/` | Daglig pg_dump + fil-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 | @@ -75,6 +82,27 @@ Arbeidsdata med begrenset levetid. Ryddes automatisk. | 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? @@ -161,7 +189,7 @@ Detaljerte spesifikasjoner ligger i `docs/concepts/` (brukeropplevelser) og `doc * **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, AI Research-Klipper, Lydmeldinger & Diktering, Podcast-statistikk, Kunnskaps-Bridge (cross-workspace), Prompt-Laboratorium. +Chat (channels), Kanban, Kalender, Notater/Scratchpad, Whiteboard, Live transkripsjon, Live AI (faktoid + referent), Visuell graf, AI Research-Klipper, 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). ## 8. Bygge-rekkefølge (Avhengighetskart) @@ -182,6 +210,8 @@ Chat (channels), Kanban, Kalender, Notater/Scratchpad, Whiteboard, Live transkri ### Lag 2 — Kjernekomponenter (krever Lag 1) - [ ] Jobbkø-worker (Rust) - [ ] Kunnskapsgraf CRUD (SvelteKit server-side) +- [ ] pgvector-migrasjon (0005): `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`) - [~] Chat med channels (PG-adapter + SpacetimeDB hybrid-adapter ferdig, sync-worker gjenstår) - [~] Kanban (PG-adapter ferdig med drag & drop, redigeringsmodal, CRUD API. SpacetimeDB-sync gjenstår) - [~] Kalender (PG-adapter ferdig med månedsvisning, fargekoder, heldags/tidshendelser. SpacetimeDB-sync gjenstår) @@ -202,6 +232,7 @@ Chat (channels), Kanban, Kalender, Notater/Scratchpad, Whiteboard, Live transkri - [ ] 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 @@ -228,13 +259,26 @@ Alle Docker-containere skal ha `healthcheck` definert i `docker-compose.yml`: 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) -- **Disk/Minne:** Mediamappe-størrelse per workspace, PG-størrelse, SpacetimeDB-minnebruk +- **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). +- **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 Ingen eksterne observability-tjenester -All overvåking og varsling skjer internt i Sidelinja-suiten. Ingen avhengighet til Discord, Slack eller andre tredjepartstjenester. +### 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. diff --git a/docs/concepts/den_asynkrone_gjesten.md b/docs/concepts/den_asynkrone_gjesten.md index 1a51e32..29d3802 100644 --- a/docs/concepts/den_asynkrone_gjesten.md +++ b/docs/concepts/den_asynkrone_gjesten.md @@ -63,6 +63,30 @@ guest_tokens ( - Ingen tilgang til andre channels, workspaces eller funksjoner. - Tokenet kan revokeres manuelt av redaksjonen. +### 4.2b Sikkerhetsdybde (mot token-lekkasje og misbruk) +Et lekket gjeste-token gir direkte filopplasting uten autentisering — dette er høyrisiko. Følgende tiltak begrenser skadepotensialet: + +| Tiltak | Implementering | Formål | +|---|---|---| +| **Rate limiting per token** | SvelteKit middleware: maks 1 opplasting per 30 sek per token | Forhindrer spam/flooding | +| **Filtype-validering** | SvelteKit: kun `audio/*` MIME-typer aksepteres, filstørrelse maks 50 MB | Blokkerer malware-opplasting | +| **Malware-scanning** | ClamAV sidecar-container scanner opplastede filer før de lagres | Fanger kjent malware | +| **Auto-revoke** | Token deaktiveres automatisk når `recordings_count >= max_recordings` | Begrenser eksponering | +| **IP-logging** | Logger klient-IP per opplasting i `guest_token_usage`-tabell | Sporbarhet ved misbruk | +| **Geo-begrensning** (valgfritt) | Caddy-nivå: blokker requests fra uventede geolokasjoner | Reduserer angrepsflate | + +**ClamAV Docker-oppsett:** +```yaml +clamav: + image: clamav/clamav:latest + restart: unless-stopped + volumes: + - /srv/sidelinja/media:/scan:ro + networks: + - sidelinja-net +``` +SvelteKit kaller ClamAV via `clamdscan` (socket) etter filopplasting, før filen flyttes til endelig plassering. Infiserte filer slettes umiddelbart og tokenet flagges for manuell gjennomgang. + ### 4.3 Flyt (teknisk) ``` Gjest åpner URL med token diff --git a/docs/infra/ai_gateway.md b/docs/infra/ai_gateway.md index db01473..d917086 100644 --- a/docs/infra/ai_gateway.md +++ b/docs/infra/ai_gateway.md @@ -151,7 +151,37 @@ tests/prompts/ └── dataset.json ``` -## 6. Dataklassifisering (ref. ARCHITECTURE.md 2.2) +## 6. Kostnadskontroll + +LiteLLM har innebygd logging, men mangler workspace-nivå budsjettering. For å forhindre kostnadssprekk: + +### 6.1 Workspace-budsjett +Hver workspace har et månedlig AI-budsjett lagret i `workspaces.settings` (JSONB): + +```json +{ + "ai_budget": { + "monthly_limit_usd": 50, + "alert_threshold_pct": 80, + "auto_fallback": true + } +} +``` + +- **Sporing:** SvelteKit logger token-bruk per AI-kall med workspace_id og jobbtype i `ai_usage_log`-tabellen (flyktig, TTL 90 dager). +- **Alert:** Når 80 % av budsjettet er brukt, postes varsel i workspace-chat (system-channel). +- **Auto-fallback:** Når budsjettet er nådd og `auto_fallback: true`, rutes alle kall til `sidelinja/rutine` (billigste modell). Ellers blokkeres AI-kall med feilmelding. + +### 6.2 Per-episode maks-kostnad +Podcastfabrikken-jobber (whisper + metadata + oppsummering) kan estimere totalkostnad basert på lydlengde. Jobben avbrytes med varsel hvis estimert kostnad overstiger `max_cost_per_episode` (default: $5). + +### 6.3 Modell-nedgradering +Jobbkøen støtter automatisk modell-nedgradering ved kostnadsmål: +1. Prøv `sidelinja/resonering` (Claude) +2. Ved budsjett-nær: fall tilbake til `sidelinja/rutine` (Gemini gratis) +3. Ved budsjett-nådd: sett jobb i `paused`-status med varsel + +## 7. Dataklassifisering (ref. ARCHITECTURE.md 2.2) | Data | Kategori | Detaljer | |---|---|---| @@ -161,7 +191,7 @@ tests/prompts/ | Promptfoo testsett | Gjenskapbar (Git) | `tests/prompts/` — versjonskontrollert | | Promptfoo testresultater | Flyktig (lokal) | Kjøres on-demand, ikke lagret permanent | -## 6. Instruks for Claude Code +## 8. Instruks for Claude Code * All AI-kode skal peke på `http://ai-gateway:4000/v1` — aldri direkte til leverandør * Bruk modellaliaser (`sidelinja/rutine`, `sidelinja/resonering`) — aldri hardkod leverandør-spesifikke modellnavn i applikasjonskode * API-nøkler i `.env`, aldri i config-filer eller kode diff --git a/docs/infra/synkronisering.md b/docs/infra/synkronisering.md index 00efbf7..ba95584 100644 --- a/docs/infra/synkronisering.md +++ b/docs/infra/synkronisering.md @@ -24,6 +24,8 @@ SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. E **Akseptabelt datatap:** Maks 5 sekunder ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes. +**Unntak — kritiske events:** Aha-markører fra studioet (live-innspilling) er tidssensitive og vanskelige å gjenskape. Disse bør flushes til PG umiddelbart (ikke batched) via en dedikert `sync_critical()`-funksjon som skriver direkte til PG i stedet for via `sync_outbox`. Alternativt kan SpacetimeDB-modulen skrive kritiske events til sin egen WAL/disk umiddelbart. Hvilke event-typer som er "kritiske" defineres per workspace i `workspaces.settings`. + ## 3. Dataflyt ``` @@ -113,6 +115,13 @@ Meldinger er append-only. Redigering av egne meldinger er last-write-wins — ak - **Graceful degradation:** SpacetimeDB-tilkoblingsfeil faller stille tilbake til PG. Brukeren ser ingen feilmelding — PG-data beholdes. - **Adapter-mønster:** `ChatConnection`-interface med to implementasjoner (PG og SpacetimeDB hybrid). Factory velger basert på env-variabel. Gjør det trivielt å teste hver adapter isolert. +### Åpent spørsmål: SpacetimeDB i fase 1? +PG-polling (3 sek) fungerer godt nok for chat og kanban med nåværende brukertall. SpacetimeDB + sync-worker innfører betydelig kompleksitet (outbox, oppvarming, workspace-partisjonering, feilhåndtering) som ennå ikke gir målbar gevinst. + +**Alternativ:** Bruk PostgreSQL `LISTEN/NOTIFY` → SvelteKit SSE (Server-Sent Events) som neste steg fra polling. Dette gir sub-sekund sanntid uten ny infrastruktur-avhengighet. SpacetimeDB introduseres først når vi har et konkret behov det ikke dekker (f.eks. LiveKit-studio med høyfrekvent state-sync mellom mange klienter). + +**Beslutning:** Utsatt. PG-adapter med polling er "god nok" for Lag 2. SpacetimeDB-koden beholdes men aktiveres ikke i prod før behovet er bevist. Adapter-mønsteret gjør at vi kan bytte uten frontend-endring. + ## 10. Instruks for Claude Code - `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel - Workeren skal bruke jobbkø-infrastrukturen (se `docs/infra/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen diff --git a/docs/proposals/README.md b/docs/proposals/README.md index 3370140..00767ca 100644 --- a/docs/proposals/README.md +++ b/docs/proposals/README.md @@ -28,8 +28,11 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe | [Artikkel-publisering](artikkel_publisering.md) | Middels | Høy | Kunnskapsgraf, Caddy, jobbkø, AI Gateway | | [Sosial publisering](social_posting.md) | Lav–Middels | Høy | Chat, jobbkø, workspace settings | | [Komponerbare sider](komponerbare_sider.md) | Lav (Fase 1) | Middels–Høy | Workspace-modell, SvelteKit, alle feature-komponenter | +| [Contradiction Detector](contradiction_detector.md) | Middels | Høy | Live AI, kunnskapsgraf, pgvector, segmenter | +| [Auto-Highlight Reel](auto_highlight_reel.md) | Middels | Høy | Podcastfabrikken, jobbkø, AI Gateway, Caddy byte-range | +| [Audience Voice Memo](audience_voice_memo.md) | Lav | Høy | Den Asynkrone Gjesten, Live transkripsjon, Live AI | -**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator. +**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator, Audience Voice Memo. ## Format Forslagsfiler er lette — ingen streng mal. Minimum: diff --git a/docs/proposals/audience_voice_memo.md b/docs/proposals/audience_voice_memo.md new file mode 100644 index 0000000..5e94a33 --- /dev/null +++ b/docs/proposals/audience_voice_memo.md @@ -0,0 +1,41 @@ +# Forslag: Audience Voice Memo (Live publikums-innspill) +**Innsats:** Lav | **Wow-faktor:** Høy + +## Idé +Under live-innspilling vises en QR-kode (eller kort-URL) som publikum kan skanne. Den åpner en minimal nettside (gjenbruker Den Asynkrone Gjestens tech) der de kan sende voice memos. Memoene dukker opp i studio-chatten som `voice_memo`-meldinger, transkriberes live, og AI matcher innholdet til kunnskapsgrafen: + +*"Lytter 'Kari fra Bergen' spør om vindkraft — du har 3 faktoider om dette fra Episode 12 og 17."* + +## Hvorfor +- Gjør live-innspilling interaktiv uten at publikum trenger app eller konto +- Gjenbruker nesten alt fra Den Asynkrone Gjesten (guest_tokens, lydopplasting, Whisper) +- Kombinert med Live AI gir det programlederen kontekst på publikums-spørsmål i sanntid +- Viralt: "Send oss en voice memo LIVE mens vi spiller inn" + +## Bygger på +- **Den Asynkrone Gjesten** (guest_tokens, `/guest/[token]`-rute, lydopplasting) +- **Live transkripsjon** (Whisper transkriberer voice memos via jobbkø) +- **Live AI** (matcher transkriberte memos mot kunnskapsgraf) +- **SpacetimeDB / PG-polling** (memos dukker opp i studio-chat i sanntid) + +## Forskjell fra Den Asynkrone Gjesten +- **Asynkron gjest:** Én person, navngitt, forberedte spørsmål, tidsbegrenset +- **Audience Voice Memo:** Mange anonyme/pseudonyme lyttere, fritt innhold, kun aktivt under innspilling + +## Teknisk skisse +1. Redaksjonen oppretter en "Live Q&A-sesjon" (spesiell guest_token med `type: 'audience'`) +2. QR-kode genereres med kort-URL → `/live/[token]` +3. Publikum åpner, skriver inn kallenavn, tar opp voice memo (maks 30 sek) +4. Voice memo lastes opp, Whisper transkriberer, AI matcher mot graf +5. Studio-chatten viser: "[Kari fra Bergen]: " + AI-kontekst + +## Dataklassifisering +- Audience voice memos: Flyktig (TTL 7 dager) — kun relevant rundt innspilling +- Transkripsjoner av memos: Flyktig (TTL 7 dager) +- Kuraterte memos (valgt ut av redaksjonen): Kritisk (flyttes til workspace media/) + +## Åpne spørsmål +- Moderering: skal alle memos dukke opp automatisk, eller må en produsent godkjenne først? +- Skalering: hva om 100+ lyttere sender memos samtidig? Whisper-kø kan bli overbelastet +- Kan dette kombineres med Live Audience Q&A-forslaget (stemmegiving på spørsmål)? +- Personvern: skal lytterne akseptere at memoet kan brukes i podcasten? diff --git a/docs/proposals/auto_highlight_reel.md b/docs/proposals/auto_highlight_reel.md new file mode 100644 index 0000000..a76bea2 --- /dev/null +++ b/docs/proposals/auto_highlight_reel.md @@ -0,0 +1,39 @@ +# Forslag: Auto-Highlight Reel (Post-innspilling) +**Innsats:** Middels | **Wow-faktor:** Høy + +## Idé +Etter innspilling analyserer Podcastfabrikken transkripsjonen for humor, emosjonelle topper, sterke meninger og "punchlines". AI genererer automatisk 5-10 klipp (15-45 sek) med: +- Tidsstempler (start/slutt) i originalt opptak +- Foreslått teksting (fra transkripsjon, formatert for sosiale medier) +- Auto-generert thumbnail-tekst (det sterkeste sitatet) +- Foreslått hashtags basert på kunnskapsgraf-tags + +Klippene havner i en "Highlights"-channel i workspace-chatten for review, med ett-klikk godkjenning og auto-posting via sosial publisering. + +## Hvorfor +- Podcast-klipp er den viktigste vekstmotoren, men manuell klipping er tidkrevende +- Bygger på eksisterende Whisper-transkripsjon + jobbkø + AI Gateway +- Kombinert med sosial publisering-forslaget gir dette en komplett "innspilling → distribusjon"-pipeline +- Differensiator: ingen annen podcast-plattform gjør dette automatisk med kvalitetskontroll + +## Bygger på +- **Podcastfabrikken** (Whisper SRT + AI-metadata — allerede spesifisert) +- **Auto-Clipper** (eksisterende forslag — dette er post-innspilling-versjonen) +- **Jobbkø** (`highlight_extract`-jobb, kjøres etter `whisper_postprocess`) +- **AI Gateway** (`sidelinja/resonering` for klipp-vurdering) +- **Caddy byte-range** (klipp serveres som range-requests mot original MP3) +- **Sosial publisering** (eksisterende forslag — ett-klikk posting) + +## Forskjell fra Auto-Clipper +Auto-Clipper kjører *live* under innspilling og fanger øyeblikk i sanntid. Auto-Highlight Reel kjører *etter* innspilling og har tilgang til hele transkripsjonen — kan dermed finne narrative buer og tematiske høydepunkter som bare er synlige i kontekst. + +## Dataklassifisering +- Klipp-metadata (tidsstempler, teksting, score): Kritisk (PG) +- Klipp-lydfiler: Avledet (kategori 3) — genereres on-demand fra original MP3 + tidsstempler +- Highlight-forslag (før godkjenning): Flyktig (TTL 30 dager) + +## Åpne spørsmål +- Scoring: hva gjør et øyeblikk "klippverdig"? Humor, nyhet, kontrovers, emosjon? +- Videostøtte: trenger vi waveform-video med teksting for TikTok/Shorts, eller holder lyd + bilde? +- Skal AI-en foreslå rekkefølge/gruppering av klipp til en "highlight reel" (2-3 min sammenklipp)? +- Kan den lære av hvilke klipp redaksjonen godkjenner over tid (feedback loop)? diff --git a/docs/proposals/contradiction_detector.md b/docs/proposals/contradiction_detector.md new file mode 100644 index 0000000..a144430 --- /dev/null +++ b/docs/proposals/contradiction_detector.md @@ -0,0 +1,41 @@ +# Forslag: Contradiction Detector (Live i Studioet) +**Innsats:** Middels | **Wow-faktor:** Høy + +## Idé +Under live-innspilling matcher Live AI nye utsagn mot eksisterende `CONTRADICTS`-edges og gamle segmenter i kunnskapsgrafen. Når en selvmotsigelse oppdages, popper det opp et diskret varsel i studio-UI: + +*"Du sa akkurat «vi må kutte støtte til vindkraft» — men i Episode 17 (segment 3, 14:22) sa du «vindkraft er fremtiden». Vil du adressere det?"* + +Programlederen kan: +1. Ignorere (ingen handling) +2. Markere for oppfølging (Aha-markør) +3. Spille inn et 12-sekunders "correction clip" på stedet + +## Hvorfor +- Den ultimate "live co-host"-funksjonen — AI som faktisk gjør programlederen bedre +- Bygger direkte på eksisterende infrastruktur (Live AI + segmenter + kunnskapsgraf) +- Øker troverdigheten til podcasten (selvkorreksjon er sterkere enn å bli tatt i feil) +- Viralt potensial: "Denne podcasten har en AI som fanger selvmotsigelser i sanntid" + +## Bygger på +- **Live transkripsjon** (Whisper-chunks i sanntid) +- **Live AI** (eksisterende faktoid-oppslag-pipeline) +- **Kunnskapsgraf** (segmenter med NER-tags, `CONTRADICTS`-edges) +- **pgvector** (semantisk matching for "lignende men motstridende" utsagn) +- **Caddy byte-range** (for å hente originalt lydklipp fra gammel episode) + +## Teknisk skisse +1. Whisper-chunk → NER-uttrekk (aktører, temaer, påstander) +2. Søk i kunnskapsgrafen: finnes det segmenter med samme aktør/tema men motstridende innhold? +3. pgvector cosine similarity for semantisk matching + LLM-vurdering via `sidelinja/resonering` +4. Resultat med confidence score > terskel → push til studio-UI via SpacetimeDB + +## Dataklassifisering +- Contradiction-alerts: Flyktig (TTL 24t) — kun relevant under/etter innspilling +- Godkjente contradictions → nye `CONTRADICTS`-edges i kunnskapsgrafen (kritisk) + +## Åpne spørsmål +- Terskel for confidence: for lav = støy under innspilling, for høy = misser reelle motstridelser +- Skal den kun matche mot egne episoder, eller også mot eksterne faktoider? +- Kan dette kombineres med Ghost Host for å "lese opp" motstridelsen? +- Latens-krav: må fungere innen 10-15 sek etter utsagnet for å være nyttig live diff --git a/docs/setup/migration_safety.md b/docs/setup/migration_safety.md index bd4af3f..f477915 100644 --- a/docs/setup/migration_safety.md +++ b/docs/setup/migration_safety.md @@ -62,5 +62,87 @@ WHERE tc.table_schema = 'public' ORDER BY tc.table_name; ``` +## RLS Leak Hunter (CI-test) + +`SET app.current_workspace_id` er en skjult single point of failure — en glemt SET i en ny feature, en feil i connection-pool, eller en ny tjeneste som kobler til PG uten middleware kan føre til cross-workspace datalekkasje. Denne testen fanger det opp. + +### Automatisk CI-test (to-workspace leak detection) +Kjøres i migrasjonstester og som egen CI-steg: + +```sql +-- Opprett to test-workspaces +INSERT INTO workspaces (id, name, slug) VALUES + ('aaaaaaaa-0000-0000-0000-000000000001', 'Workspace A', 'ws-a'), + ('aaaaaaaa-0000-0000-0000-000000000002', 'Workspace B', 'ws-b'); + +-- Seed testdata i begge +INSERT INTO nodes (id, node_type, workspace_id) VALUES + ('bbbbbbbb-0000-0000-0000-000000000001', 'tema', 'aaaaaaaa-0000-0000-0000-000000000001'), + ('bbbbbbbb-0000-0000-0000-000000000002', 'tema', 'aaaaaaaa-0000-0000-0000-000000000002'); + +-- TEST 1: Sett workspace A, forsøk å lese workspace B +SET app.current_workspace_id = 'aaaaaaaa-0000-0000-0000-000000000001'; +DO $$ +BEGIN + IF (SELECT count(*) FROM nodes WHERE workspace_id = 'aaaaaaaa-0000-0000-0000-000000000002') > 0 THEN + RAISE EXCEPTION 'RLS LEAK: Workspace A kan lese Workspace B sine noder!'; + END IF; +END $$; + +-- TEST 2: Uten SET (tom current_setting) skal returnere 0 rader +RESET app.current_workspace_id; +DO $$ +BEGIN + -- For vanlig bruker (ikke superuser) bør dette returnere 0 + IF (SELECT count(*) FROM nodes) > 0 AND current_setting('is_superuser') = 'off' THEN + RAISE EXCEPTION 'RLS LEAK: Uautentisert tilkobling kan lese data!'; + END IF; +END $$; +``` + +### Audit-trigger (produksjon) +Valgfri trigger som logger mistenkelige queries i prod: + +```sql +-- Tabell for RLS-audit +CREATE TABLE IF NOT EXISTS rls_audit_log ( + id BIGSERIAL PRIMARY KEY, + table_name TEXT NOT NULL, + operation TEXT NOT NULL, + current_workspace TEXT, + session_user TEXT NOT NULL, + query_timestamp TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Funksjon som logger når current_workspace_id ikke er satt +CREATE OR REPLACE FUNCTION audit_rls_context() RETURNS TRIGGER AS $$ +BEGIN + IF current_setting('app.current_workspace_id', true) IS NULL + OR current_setting('app.current_workspace_id', true) = '' THEN + IF current_setting('is_superuser') = 'off' THEN + INSERT INTO rls_audit_log (table_name, operation, current_workspace, session_user) + VALUES (TG_TABLE_NAME, TG_OP, current_setting('app.current_workspace_id', true), session_user); + END IF; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +``` + +**Kjør leak hunter mot ALLE tabeller med workspace_id — ikke bare de som er listet over.** Nye tabeller legges til i listen automatisk via introspeksjon: + +```sql +-- Finn alle tabeller med workspace_id-kolonne (bør alle ha RLS) +SELECT t.tablename +FROM pg_tables t +JOIN information_schema.columns c ON c.table_name = t.tablename +WHERE c.column_name = 'workspace_id' + AND t.schemaname = 'public' + AND NOT EXISTS ( + SELECT 1 FROM pg_policies p WHERE p.tablename = t.tablename + ); +-- Forventet: 0 rader. Enhver rad her = tabell med workspace_id UTEN RLS-policy. +``` + ## Automatisering -Disse sjekkene kjøres automatisk i migrasjonstestene (se `ARCHITECTURE.md` §10.2). Manuell kjøring er kun nødvendig ved prod-migrasjoner til automatiserte tester er på plass. +Disse sjekkene kjøres automatisk i migrasjonstestene (se `ARCHITECTURE.md` §10.2). Manuell kjøring er kun nødvendig ved prod-migrasjoner til automatiserte tester er på plass. **RLS Leak Hunter bør prioriteres som første CI-steg — den beskytter mot den mest alvorlige feilkategorien (cross-workspace datalekkasje).** diff --git a/docs/setup/produksjon.md b/docs/setup/produksjon.md index 445152b..1708d49 100644 --- a/docs/setup/produksjon.md +++ b/docs/setup/produksjon.md @@ -155,6 +155,9 @@ Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i # - Alle tjenester på samme interne nettverk (sidelinja-net) # - Volumer bruker bind mounts til /srv/sidelinja/ # - .env-filen lastes automatisk av Docker Compose +# - RESSURSGRENSER: Worker-containere (Whisper) MÅ ha deploy.resources.limits +# for å forhindre at de sultefôrer LiveKit og PostgreSQL. +# Eksempel: workers: deploy: resources: limits: cpus: '4' memory: 8G networks: sidelinja-net: @@ -254,7 +257,7 @@ Forgejo konfigureres med Authentik som OAuth2-kilde: Se `ARCHITECTURE.md` seksjon 2.2 for full dataklassifisering. Kun kategori 1 (kritisk) og Forgejo-data backupes. -### 11.1 PostgreSQL (daglig, 03:00) +### 11.1 PostgreSQL (daglig dump, 03:00) ```bash # pg_dump er konsistent selv under last — ingen nedetid docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \ @@ -264,6 +267,43 @@ docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \ find /srv/sidelinja/backup/pg/ -name "*.dump" -mtime +30 -delete ``` +### 11.1b PostgreSQL WAL-arkivering (kontinuerlig, PITR) +Daglig dump gir opptil 24 timers datatap. WAL-arkivering muliggjør Point-In-Time Recovery til minuttet. + +```bash +# Installer pgBackRest (i PostgreSQL Docker-containeren eller som sidecar) +# Alternativt: WAL-G for enklere S3-oppsett + +# postgresql.conf (legg til i Docker-volumet eller via environment) +archive_mode = on +archive_command = 'pgbackrest --stanza=sidelinja archive-push %p' +wal_level = replica + +# pgbackrest.conf +[sidelinja] +pg1-path=/var/lib/postgresql/data + +[global] +repo1-type=s3 +repo1-s3-bucket=sidelinja-backup +repo1-s3-endpoint=fsn1.your-objectstorage.com +repo1-s3-region=fsn1 +repo1-path=/pgbackrest +repo1-retention-full=4 +repo1-retention-diff=14 + +# Ukentlig full backup (søndag kl. 02:00) +# 0 2 * * 0 sidelinja pgbackrest --stanza=sidelinja --type=full backup +# Daglig differensiell (man-lør kl. 02:00) +# 0 2 * * 1-6 sidelinja pgbackrest --stanza=sidelinja --type=diff backup + +# Recovery-eksempel (gjenopprett til spesifikt tidspunkt): +# pgbackrest --stanza=sidelinja --target="2026-03-15 13:59:00" \ +# --target-action=promote restore +``` + +**Merk:** WAL-arkivering erstatter IKKE daglig pg_dump — dumpen er en enkel, portabel backup som fungerer uavhengig av pgBackRest. WAL-arkivering er et tillegg for finkornet recovery. + ### 11.2 Media-filer (daglig, 03:30) ```bash # Inkrementell med rsync til lokal backup-disk eller ekstern lagring @@ -284,15 +324,47 @@ cp /srv/sidelinja/.env /srv/sidelinja/backup/env_$(date +%Y%m%d) chmod 600 /srv/sidelinja/backup/env_* ``` -### 11.5 Cron-oppsett +### 11.5 Off-site backup (rclone → Hetzner Object Storage) + +Lokal backup beskytter kun mot logiske feil. Ved fysisk nodefeil tapes alt. Kategori 1-data pushes daglig til Hetzner Object Storage via `rclone`. + +```bash +# Installer og konfigurer rclone +curl https://rclone.org/install.sh | sudo bash +rclone config +# Opprett remote "hetzner-s3" med Hetzner Object Storage credentials +# (S3-kompatibelt, endpoint: fsn1.your-objectstorage.com eller nbg1) + +# /srv/sidelinja/scripts/backup-offsite.sh +#!/bin/bash +set -euo pipefail +BUCKET="s3:hetzner-s3/sidelinja-backup" + +# PG-dump (siste lokale dump) +LATEST_DUMP=$(ls -t /srv/sidelinja/backup/pg/*.dump 2>/dev/null | head -1) +if [ -n "$LATEST_DUMP" ]; then + rclone copy "$LATEST_DUMP" "$BUCKET/pg/" +fi + +# Media (inkrementell sync) +rclone sync /srv/sidelinja/media/ "$BUCKET/media/" --transfers 4 + +# Behold 90 dager PG-dumper off-site +rclone delete "$BUCKET/pg/" --min-age 90d + +echo "$(date): Off-site backup ferdig" >> /srv/sidelinja/logs/backup-offsite.log +``` + +### 11.6 Cron-oppsett ```bash # /etc/cron.d/sidelinja-backup 0 3 * * * sidelinja /srv/sidelinja/scripts/backup-pg.sh 30 3 * * * sidelinja /srv/sidelinja/scripts/backup-media.sh 0 4 * * * sidelinja /srv/sidelinja/scripts/backup-forgejo.sh +30 4 * * * sidelinja /srv/sidelinja/scripts/backup-offsite.sh ``` -### 11.6 Hva som IKKE backupes (bevisst) +### 11.7 Hva som IKKE backupes (bevisst) - **Redis** — cache, regenereres automatisk - **Caddy-data** — sertifikater regenereres av Let's Encrypt - **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git @@ -300,7 +372,7 @@ chmod 600 /srv/sidelinja/backup/env_* - **Whisper-modeller** — re-download fra HuggingFace - **SpacetimeDB** — sanntidsdata synkes til PG, in-memory state er flyktig -### 11.7 Restore-prosedyre +### 11.8 Restore-prosedyre ```bash # 1. PostgreSQL docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \