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:
parent
fef4e537fd
commit
024a91e1b3
10 changed files with 400 additions and 15 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
41
docs/proposals/audience_voice_memo.md
Normal file
41
docs/proposals/audience_voice_memo.md
Normal 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?
|
||||
39
docs/proposals/auto_highlight_reel.md
Normal file
39
docs/proposals/auto_highlight_reel.md
Normal 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)?
|
||||
41
docs/proposals/contradiction_detector.md
Normal file
41
docs/proposals/contradiction_detector.md
Normal 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
|
||||
|
|
@ -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).**
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue