Dokumentasjon: arkitekturvurdering — sikkerhet, backup, kostnad, nye forslag

Oppdaterer dokumentasjon basert på tre eksterne arkitekturvurderinger:

- RLS Leak Hunter med CI-test og audit-trigger (migration_safety.md)
- pgvector-migrasjon flyttet til Lag 2, WAL-arkivering med pgBackRest (ARCHITECTURE.md, produksjon.md)
- Off-site backup med rclone, Docker cgroups for workers (ARCHITECTURE.md, produksjon.md)
- Kostnadskontroll i AI Gateway: workspace-budsjett, auto-fallback (ai_gateway.md)
- Gjeste-token sikkerhetsdybde: ClamAV, rate limiting, auto-revoke (den_asynkrone_gjesten.md)
- SpacetimeDB fase 1-vurdering: PG LISTEN/NOTIFY som mellomsteg (synkronisering.md)
- Kritiske events (Aha-markører) flushes umiddelbart (synkronisering.md)
- Ekstern helsesjekk, observability-utvidelser (ARCHITECTURE.md)
- Tre nye forslag: Contradiction Detector, Auto-Highlight Reel, Audience Voice Memo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-15 03:56:21 +01:00
parent fef4e537fd
commit 024a91e1b3
10 changed files with 400 additions and 15 deletions

View file

@ -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. 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 ## 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. * **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. * **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:** * **Domener:**
@ -38,7 +45,7 @@ Data som ikke kan gjenskapes. Tap = permanent informasjonstap.
| Data | Lagring | Backup | | 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 | | Lydfiler (MP3, råopptak) | `media/` | Daglig fil-backup |
| `.env` (hemmeligheter) | `/srv/sidelinja/.env` | Manuell kopi, ikke i Git | | `.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 | | Jobbkø-historikk (fullførte jobber) | PostgreSQL | 30 dager | Feilsøking |
| Whisper-modeller | `.docker-data/` (lokal) | Ingen TTL | Re-download fra HuggingFace ved behov | | 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 #### Retningslinjer for nye komponenter
Når en ny feature eller komponent introduserer data: Når en ny feature eller komponent introduserer data:
1. **Klassifiser** — hvilken kategori faller dataen i? 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. * **Den Asynkrone Gjesten:** Tidsbegrenset lenke til gjester for asynkrone lydopptak som lander i redaksjonens arbeidsflyt.
### Features (byggeklosser) ### 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) ## 8. Bygge-rekkefølge (Avhengighetskart)
@ -182,6 +210,8 @@ Chat (channels), Kanban, Kalender, Notater/Scratchpad, Whiteboard, Live transkri
### Lag 2 — Kjernekomponenter (krever Lag 1) ### Lag 2 — Kjernekomponenter (krever Lag 1)
- [ ] Jobbkø-worker (Rust) - [ ] Jobbkø-worker (Rust)
- [ ] Kunnskapsgraf CRUD (SvelteKit server-side) - [ ] 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) - [~] 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) - [~] 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) - [~] 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) - [ ] Møterommet: AI-Referent (LiveKit + Whisper + møte-oppsummering)
- [ ] Visuell Kunnskapsgraf (D3.js/Vis.js graf-visning) - [ ] Visuell Kunnskapsgraf (D3.js/Vis.js graf-visning)
- [ ] Kunnskaps-Bridge (pgvector, cross-workspace discovery) - [ ] Kunnskaps-Bridge (pgvector, cross-workspace discovery)
- [ ] Graf-vedlikehold (nattlig jobb: finn isolerte noder, foreslå koblinger basert på co-occurrence)
- [ ] Valgomat (selvstendig, lav prioritet) - [ ] Valgomat (selvstendig, lav prioritet)
## 9. Observabilitet ## 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: SvelteKit-appen inkluderer en intern admin-side (`/admin/observability`) som samler:
- **Container-status:** Healthcheck-resultater fra Docker (via `docker compose ps` / Docker socket) - **Container-status:** Healthcheck-resultater fra Docker (via `docker compose ps` / Docker socket)
- **Jobbkø:** Pending/running/error-count med sparkline-grafer (siste 24t) - **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) - **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 - **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. 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 ### 9.5 Ekstern helsesjekk (utenfor stacken)
All overvåking og varsling skjer internt i Sidelinja-suiten. Ingen avhengighet til Discord, Slack eller andre tredjepartstjenester. 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 ## 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. 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.

View file

@ -63,6 +63,30 @@ guest_tokens (
- Ingen tilgang til andre channels, workspaces eller funksjoner. - Ingen tilgang til andre channels, workspaces eller funksjoner.
- Tokenet kan revokeres manuelt av redaksjonen. - 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) ### 4.3 Flyt (teknisk)
``` ```
Gjest åpner URL med token Gjest åpner URL med token

View file

@ -151,7 +151,37 @@ tests/prompts/
└── dataset.json └── 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 | | Data | Kategori | Detaljer |
|---|---|---| |---|---|---|
@ -161,7 +191,7 @@ tests/prompts/
| Promptfoo testsett | Gjenskapbar (Git) | `tests/prompts/` — versjonskontrollert | | Promptfoo testsett | Gjenskapbar (Git) | `tests/prompts/` — versjonskontrollert |
| Promptfoo testresultater | Flyktig (lokal) | Kjøres on-demand, ikke lagret permanent | | 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 * 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 * 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 * API-nøkler i `.env`, aldri i config-filer eller kode

View file

@ -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. **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 ## 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. - **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. - **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 ## 10. Instruks for Claude Code
- `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel - `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 - 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

View file

@ -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 | | [Artikkel-publisering](artikkel_publisering.md) | Middels | Høy | Kunnskapsgraf, Caddy, jobbkø, AI Gateway |
| [Sosial publisering](social_posting.md) | LavMiddels | Høy | Chat, jobbkø, workspace settings | | [Sosial publisering](social_posting.md) | LavMiddels | Høy | Chat, jobbkø, workspace settings |
| [Komponerbare sider](komponerbare_sider.md) | Lav (Fase 1) | MiddelsHøy | Workspace-modell, SvelteKit, alle feature-komponenter | | [Komponerbare sider](komponerbare_sider.md) | Lav (Fase 1) | MiddelsHø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 ## Format
Forslagsfiler er lette — ingen streng mal. Minimum: Forslagsfiler er lette — ingen streng mal. Minimum:

View file

@ -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]: <transkribert tekst>" + 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?

View file

@ -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)?

View file

@ -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

View file

@ -62,5 +62,87 @@ WHERE tc.table_schema = 'public'
ORDER BY tc.table_name; 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 ## 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).**

View file

@ -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) # - Alle tjenester på samme interne nettverk (sidelinja-net)
# - Volumer bruker bind mounts til /srv/sidelinja/ # - Volumer bruker bind mounts til /srv/sidelinja/
# - .env-filen lastes automatisk av Docker Compose # - .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: networks:
sidelinja-net: 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. 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 ```bash
# pg_dump er konsistent selv under last — ingen nedetid # pg_dump er konsistent selv under last — ingen nedetid
docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \ 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 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) ### 11.2 Media-filer (daglig, 03:30)
```bash ```bash
# Inkrementell med rsync til lokal backup-disk eller ekstern lagring # 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_* 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 ```bash
# /etc/cron.d/sidelinja-backup # /etc/cron.d/sidelinja-backup
0 3 * * * sidelinja /srv/sidelinja/scripts/backup-pg.sh 0 3 * * * sidelinja /srv/sidelinja/scripts/backup-pg.sh
30 3 * * * sidelinja /srv/sidelinja/scripts/backup-media.sh 30 3 * * * sidelinja /srv/sidelinja/scripts/backup-media.sh
0 4 * * * sidelinja /srv/sidelinja/scripts/backup-forgejo.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 - **Redis** — cache, regenereres automatisk
- **Caddy-data** — sertifikater regenereres av Let's Encrypt - **Caddy-data** — sertifikater regenereres av Let's Encrypt
- **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git - **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 - **Whisper-modeller** — re-download fra HuggingFace
- **SpacetimeDB** — sanntidsdata synkes til PG, in-memory state er flyktig - **SpacetimeDB** — sanntidsdata synkes til PG, in-memory state er flyktig
### 11.7 Restore-prosedyre ### 11.8 Restore-prosedyre
```bash ```bash
# 1. PostgreSQL # 1. PostgreSQL
docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \ docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \