Initial commit: arkitekturdokumentasjon og feature-specs

This commit is contained in:
vegard 2026-03-13 04:51:26 +01:00
commit 290a1e398d
14 changed files with 1170 additions and 0 deletions

133
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,133 @@
# Sidelinja - Architecture Decision Record & System Overview
**Dette dokumentet definerer den overordnede arkitekturen, teknologistacken og datamodellen for Sidelinja-suiten. AI-agenter (som Claude Code) SKAL lese og forstå dette dokumentet før de foreslår endringer, skriver kode eller gjør arkitektoniske valg.**
## 1. Visjon og Konsept
Sidelinja er ikke bare en podcast-host; det er et **redaksjonelt operativsystem** og en **kunnskapsgraf**. Målet er å bygge en plattform som sømløst integrerer research, asynkron kommunikasjon (chat), sanntids innspilling (Lyd/Video) og automatisert publisering. Visjonen inkluderer også at plattformen skal fungere som en "live co-host" (virtuell assistent) under innspilling ved å boble opp relevant informasjon fra kunnskapsgrafen i sanntid. Systemet er bygget for full datakontroll, eierskap og minimal bruk av lukkede tredjepartstjenester.
## 2. Infrastruktur og DevOps
* **Produksjonsserver:** Hetzner VPS (Ubuntu, 8 vCPU, 16 GB RAM). 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.
* **Orkestrering:** Docker / Docker Compose. Alle tjenester kjører i isolerte containere på et internt Docker-nettverk.
* **Reverse Proxy & Webserver:** **Caddy**. Håndterer all innkommende trafikk for flere domener, automatisk HTTPS (Let's Encrypt), og ruting til interne containere. Port 80/443 er de *eneste* portene som er eksponert mot internett.
* **Domener:**
- `sidelinja.org` — Hovedapplikasjon (SvelteKit, media, SpacetimeDB, LiveKit)
- `auth.sidelinja.org` — Authentik SSO (felles for alle domener)
- `git.sidelinja.org` — Forgejo
- `vegard.info` — Separat nettsted, deler SSO med Sidelinja
* **Kildekode og CI/CD:** **Forgejo** (Selv-hostet Git). All kode og konfigurasjon lever her.
* **Utvikling og Utrulling (Claude Code Workflow):** All prototyping og koding skjer lokalt i **WSL2 (Ubuntu)** på en lokal Windows 11-maskin. Når koden er testet og klar, pusher AI-agenten (Claude Code) til Forgejo. Deretter logger agenten seg på produksjonsserveren via SSH for å hente koden, trigge kompilering og starte tjenestene på nytt. Regelen "ikke programmere i produksjon" betyr utelukkende at redigering av kildekode og "prøving og feiling" hører hjemme lokalt, ikke live på serveren.
### 2.1 Serverstruktur og Backup-strategi (Produksjon)
All persistent data, konfigurasjon og kildekode monteres via Docker Bind Mounts til en fast struktur på vertssystemet, typisk `/srv/sidelinja/`. Dette muliggjør granulert backup.
/srv/sidelinja/
├── docker-compose.yml # Orkestrering
├── .env # Miljøvariabler (IKKE i Git)
├── config/ # Konfigurasjonsfiler (Caddy, Authentik, etc.)
├── data/ # Databaser (Postgres, SpacetimeDB, Forgejo) -> KRITISK BACKUP
├── media/ # Lydfiler (podcast, råopptak) -> KRITISK BACKUP
└── logs/ # Caddy access-logger, app-logger -> SEPARAT/LANGTIDS BACKUP
*Målrettet backup:* Mappen `logs/` ekskluderes fra den daglige snapshot-backupen for å spare plass, men rulleres og arkiveres separat for fremtidig dataanalyse.
### 2.2 Lokalt Utviklingsmiljø (Dev Replika)
For å sikre smidig lokal utvikling i WSL2, bygger vi en nøyaktig replika av produksjonsmiljøet, men optimalisert for utviklingshastighet (Hot Reloading). **Komplett steg-for-steg oppsett finnes i `docs/setup/lokal.md`, produksjonsoppsett i `docs/setup/produksjon.md`.**
* **Docker Compose Dev:** Vi bruker en egen `docker-compose.dev.yml` som spinner opp lokale instanser av databasene (PostgreSQL, SpacetimeDB) og LiveKit. Volumene for disse er lokale og flyktige/seedede.
* **SvelteKit HMR:** SvelteKit-klienten kjøres *utenfor* Docker under aktiv utvikling (ved bruk av `npm run dev` i WSL2). Dette sikrer at Hot Module Replacement (HMR) fungerer lynraskt når kode endres.
* **Lokal Ruting:** En lokal Caddy-instans ruter trafikk fra `localhost` til SvelteKit, SpacetimeDB og LiveKit, med self-signed sertifikater (`local_certs`) for sikker kontekst (WebRTC).
* **Forgejo:** Kjører *ikke* lokalt. Push direkte til produksjons-Forgejo fra WSL2.
## 3. Teknologistack
Vi følger et "Best tool for the job"-prinsipp, med en sterk preferanse for minnesikkerhet, ytelse og rene grensesnitt.
* **Backend/Automasjon:** **Rust**. Brukes som bakgrunnsworkers (jobbkø), logg-parsing og SpacetimeDB-moduler. Rust er *ikke* en API-server — SvelteKit server-side håndterer all HTTP-kommunikasjon og PG-tilgang direkte (se `docs/features/api_grensesnitt.md`).
* **Frontend / UI:** **SvelteKit** (med TypeScript). Bygges som en PWA. Valgt for ytelse og enkel integrasjon med WebRTC og vanilla JS-biblioteker.
* **Sanntids Lyd/Video:** **LiveKit** (Selv-hostet). Håndterer WebRTC, fler-bruker videochat og opptak i det virtuelle "studioet".
* **AI / Prosessering:** `faster-whisper` (lokal transkripsjon) og OpenRouter (Claude-modeller for tekstanalyse).
* **SSO / Autentisering:** **Authentik** (Selv-hostet). Sentralisert rollestyring.
## 4. Den To-delte Databasestrategien
1. **PostgreSQL (Historikk & Kunnskapsgraf):** Én sentralisert instans i Docker for brukerkontoer, Git-metadata, aggregert statistikk og Kunnskapsgrafen (Artikler, Faktoider).
2. **SpacetimeDB (Sanntid & Arbeidsflyt):** In-memory database for live chat, status på episoder, og live-oppdateringer i studio. Klienten (Svelte) lytter direkte på SpacetimeDB. Strategisk avhengighet, men all persistent data synkes til PostgreSQL — ved eventuelt bortfall kan sanntidslaget erstattes uten tap av data.
3. **Synkronisering:** Event-drevet med ~5 sek forsinkelse. SpacetimeDB er autoritativ for sanntidsdata (chat, kanban), PostgreSQL for persistent data (kunnskapsgraf, metadata). Detaljer i `docs/features/synkronisering.md`.
## 5. Datamodell: Kunnskapsgrafen
Systemet er bygget rundt **Temaer** og **Aktører**, ikke episoder. Dette bygger et asynkront research-arkiv. Alle entiteter arver UUID fra en felles `nodes`-supertabell som gir ekte FK-integritet i grafmodellen (detaljer i `docs/features/kunnskapsgraf_og_relasjoner.md`).
* **Tema (Saker):** Levende konsepter ("Skolepolitikk").
* **Aktør (Entity):** Personer eller organisasjoner ("Jonas Gahr Støre").
* **Faktoide (Factoid):** En atomisk bit med informasjon koblet til Aktører/Temaer ("Søkte jobb i AP i 2011").
* **Episode:** Et tidsbegrenset prosjekt ("Episode 42") som samler et utvalg av aktuelle *Temaer*.
* **Segment:** En tidsavgrenset del av en episode med egen transkripsjon, koblet til Temaer/Aktører i grafen. Muliggjør presise oppslag ("hva sa vi om X i Episode 42?").
* **Research-klipp:** Råtekst renset av AI, koblet til Temaer/Aktører.
**Transkripsjoner:** Git (Forgejo) er kilde til sannhet (redigerbar, sporbar). PostgreSQL er søkeindeks (full-text, koblet til grafen). Endringer i Git reimporteres automatisk via Forgejo webhook.
## 6. Podcast Hosting og Distribusjon
* **Lagring:** MP3-filer lagres flatt i `/srv/sidelinja/media/`. Ingen lydfiler i databaser.
* **Servering:** Caddy serverer media-mappen. MÅ ha `Accept-Ranges: bytes` aktivert for podcast-streaming.
* **RSS-Feed:** Genereres av SvelteKit og leveres statisk eller dynamisk med aggressiv caching.
## 7. Planlagte Funksjoner (Feature Ideas)
Dette er hovedkonseptene plattformen skal støtte. **Merk: Detaljerte tekniske spesifikasjoner, flytskjemaer og datastrukturer for hver av disse ligger i mappen `docs/features/`.**
* **Live AI-Assistent i Studio:** Sanntidstranskripsjon via mikrofonene som lytter etter nøkkelord (Named Entity Recognition). Gjør asynkrone oppslag i PostgreSQL og dytter relevante "Faktoider" live til Svelte-grensesnittet via SpacetimeDB mens programlederne snakker.
* **AI Research-Klipper ("Ctrl+A workflow"):** Et verktøy der redaksjonen limer inn rotete nyhetsartikler. AI-en (OpenRouter) renser, oppsummerer, og trekker ut Aktører og Faktoider som lagres i Kunnskapsgrafen.
* **Produktivitetssuiten:** En Svelte/SpacetimeDB-basert flate for Kanban-styring av episoder, trådet chat knyttet til Temaer, og kollaborative show notes.
* **Valgomat:** En publikumsrettet, avansert og interaktiv valgomat drevet av SpacetimeDB for umiddelbar respons og vekting av svar.
* **Podcast-Statistikk (Privacy First):** Batch-prosessering i Rust som tygger Caddy JSON-logger, dedupliserer lyttere, fjerner bots og lagrer ferdig statistikk i PostgreSQL.
* **Podcastfabrikken:** System for automatisk og manuell publisering (Whisper transkripsjon, metadata via OpenRouter) og versjonshåndtering/cache-busting ved oppdatering av eksisterende episoder.
## 8. Bygge-rekkefølge (Avhengighetskart)
```
Lag 1 — Fundament (ingen avhengigheter):
├── PostgreSQL-skjema (nodes, graph_edges, job_queue)
├── SpacetimeDB grunnoppsett
└── SvelteKit skjelett med Authentik-integrasjon
Lag 2 — Kjernekomponenter (krever Lag 1):
├── Jobbkø-worker (Rust)
├── Kunnskapsgraf CRUD (SvelteKit server-side)
└── Produktivitetssuiten: Chat + Kanban (SpacetimeDB ↔ PG synk)
Lag 3 — Features (krever Lag 2):
├── AI Research-Klipper (kunnskapsgraf + jobbkø)
├── Podcastfabrikken (jobbkø + episoder/segmenter)
└── Podcast-Statistikk (jobbkø + episoder)
Lag 4 — Avansert (krever Lag 3):
├── Live AI-Assistent (fylt kunnskapsgraf + LiveKit + Whisper)
└── Valgomat (selvstendig, lav prioritet)
```
## 9. Observabilitet
### 9.1 Helse
Alle Docker-containere skal ha `healthcheck` definert i `docker-compose.yml`:
- PostgreSQL: `pg_isready`
- SpacetimeDB: TCP-sjekk mot intern port
- Caddy: `curl -f http://localhost/health`
- SvelteKit: `curl -f http://localhost:3000/health`
- Rust Workers: Heartbeat-rad i `job_queue` (en `worker_heartbeat`-jobb som re-enqueuer seg selv hvert minutt — fravær betyr død worker)
### 9.2 Logging
- **Format:** Strukturert JSON fra alle komponenter (Rust, SvelteKit, Caddy)
- **Plassering:** `/srv/sidelinja/logs/` med undermapper per tjeneste
- **Rotasjon:** Standard Linux logrotate, daglig rotasjon, 30 dagers retensjon
- Caddy podcast-logger behandles separat av statistikk-workeren (se `docs/features/podcast_statistikk.md`)
### 9.3 Jobbkø-overvåking
- Admin-visning i SvelteKit som viser `job_queue`-status (pending, running, error-count)
- Feilede jobber (`status = 'error'`) poster automatisk en varslingsmelding til et dedikert system-tema i Produktivitetssuiten (intern chat), slik at redaksjonen ser det i sin daglige arbeidsflate
### 9.4 Ingen eksterne tjenester
All overvåking og varsling skjer internt i Sidelinja-suiten. Ingen avhengighet til Discord, Slack eller andre tredjepartstjenester.
## 10. AI Agent Guidelines (Instrukser for Claude Code)
* **Start her:** Når du settes til å bygge en ny komponent, sikre alltid at det lokale utviklingsmiljøet (`docker-compose.dev.yml`) kjører først.
* **Dokumentasjonsstandard:** Når du skal implementere en ny funksjon (feature), sjekk ALLTID om det finnes et dokument i `docs/features/<feature-navn>.md` først. Oppdater disse dokumentene hvis arkitekturen for funksjonen endres.
* **Ingen "gh" CLI:** Vi bruker Forgejo. For Pull Requests/Issues, bruk `tea` CLI.
* **Deployment:** Kod og test lokalt i WSL. Push til Forgejo, logg inn via SSH for å pulle kode og restarte containere/tjenester (`docker compose up -d`).
* **Asynkron AI:** Tyngre jobber (Whisper, OpenRouter) skal aldri blokkere web-forespørsler. Alle bakgrunnsjobber kjøres via den felles PostgreSQL-baserte jobbkøen (se `docs/features/jobbkø.md`).
* **Sikkerhet:** Forsøk aldri å eksponere databaseporter ut mot internett i Docker Compose-filer (hverken lokalt eller i prod). Port 80/443 (Caddy) er de eneste inngangsportene.

36
CLAUDE.md Normal file
View file

@ -0,0 +1,36 @@
# Sidelinja - Claude Code Prosjektguide
## Prosjektoversikt
Sidelinja er et redaksjonelt operativsystem og kunnskapsgraf for podcast-produksjon.
Self-hosted på Hetzner VPS med full datakontroll.
## Nøkkelfiler
- `ARCHITECTURE.md` — Overordnet arkitektur, stack, datamodell og infrastruktur
- `docs/setup/produksjon.md` — Steg-for-steg oppsett av Hetzner VPS fra scratch
- `docs/setup/lokal.md` — Steg-for-steg oppsett av lokalt WSL2 utviklingsmiljø
- `docs/features/` — Detaljerte feature-spesifikasjoner:
- `kunnskapsgraf_og_relasjoner.md` — Nodes & Edges-modell i PostgreSQL
- `ai_research_klipper.md` — AI-drevet research-inntak til kunnskapsgrafen
- `live_ai_assistent.md` — Sanntids faktoid-oppslag under innspilling
- `produktivitetssuite.md` — Kanban, chat, show notes (SpacetimeDB-tung)
- `podcastfabrikken.md` — Publiseringspipeline (Whisper + OpenRouter + RSS)
- `podcast_statistikk.md` — IAB-kompatibel lytterstatistikk fra Caddy-logger
- `valgomat.md` — Publikumsrettet valgomat (SpacetimeDB)
- `jobbkø.md` — Felles PostgreSQL-basert køsystem for alle bakgrunnsjobber
- `synkronisering.md` — PostgreSQL ↔ SpacetimeDB dataflyt og eierskapsmodell
- `api_grensesnitt.md` — Kommunikasjonskart: SvelteKit er web-API, Rust er worker
## Stack
- **Backend/Automasjon:** Rust
- **Frontend:** SvelteKit (TypeScript, PWA)
- **Sanntid:** SpacetimeDB (arbeidsflyt/state) + LiveKit (lyd/video)
- **Database:** PostgreSQL (persistent/kunnskapsgraf) + SpacetimeDB (in-memory/sanntid)
- **AI:** faster-whisper (transkripsjon), OpenRouter (Claude-modeller)
- **Infra:** Docker Compose, Caddy, Authentik (SSO), Forgejo (Git)
## Viktige regler
- Aldri eksponere databaseporter mot internett (kun port 80/443 via Caddy)
- Bruk `tea` CLI, ikke `gh` (vi bruker Forgejo, ikke GitHub)
- Tunge AI-jobber (Whisper, OpenRouter) skal aldri blokkere web-requests
- Kod og test lokalt i WSL2, deploy via push til Forgejo + SSH pull
- Sjekk alltid `docs/features/<navn>.md` før du implementerer en feature

View file

@ -0,0 +1,19 @@
# Feature Spec: AI Research-Klipper ("Ctrl+A Workflow")
**Filsti:** `docs/features/ai_research_klipper.md`
## 1. Konsept
Et internt redaksjonelt verktøy for å samle inn research fra nettet. Programlederne limer inn uformatert tekst (ofte med menyer, annonser og støy fra "Ctrl+A"-kopiering), og en AI renser teksten og trekker ut strukturert kunnskap.
## 2. Arkitektur & Dataflyt
1. **Input (SvelteKit):** En modal i grensesnittet der brukeren limer inn råtekst og valgfri kilde-URL, og knytter det til et *Tema* (f.eks. "Skolepolitikk").
2. **Prosessering (Jobbkø + OpenRouter):**
* Backend mottar teksten og oppretter en `research_clip`-jobb i jobbkøen (se `docs/features/jobbkø.md`). Rust-workeren plukker opp jobben og sender request til OpenRouter (Claude-modell).
* **System Prompt:** Skal instruere AI-en til å returnere JSON med følgende struktur:
`{ "title": "...", "summary": ["..."], "cleaned_text": "...", "actors": ["..."], "factoids": ["..."] }`
3. **Lagring (PostgreSQL):** Backend lagrer resultatet relasjonelt i Kunnskapsgrafen. *Aktører* som ikke finnes opprettes. *Faktoider* kobles til aktørene. Selve artikkelen knyttes til det valgte *Temaet*.
4. **Broadcast (SpacetimeDB):**
Når lagringen er ferdig, sendes et signal via SpacetimeDB slik at chatten/tema-visningen oppdateres hos alle innloggede brukere med et "Kort" som viser det nye sammendraget.
## 3. Instruks for Claude Code
* Sørg for at OpenRouter API-kallet forventer og validerer streng JSON-struktur.
* Lagringen i PostgreSQL må håndtere "upserts" for Aktører elegant, slik at vi ikke får duplikater av f.eks. "Arbeiderpartiet".

View file

@ -0,0 +1,73 @@
# Feature Spec: API-grensesnitt og Tjenesteansvar
**Filsti:** `docs/features/api_grensesnitt.md`
## 1. Konsept
Definerer hvordan SvelteKit-frontenden kommuniserer med backend-tjenestene. Prinsippet er: **SvelteKit er web-serveren, Rust er workeren.** Ingen separat Rust HTTP API.
## 2. Kommunikasjonskart
```
┌─────────────────────────────────────────────────────────────┐
│ Brukerens nettleser (SvelteKit klient) │
└──────────┬──────────────────────┬───────────────────────────┘
│ │
│ WebSocket │ HTTP (forms, fetch)
▼ ▼
┌──────────────────┐ ┌─────────────────────────────────────┐
│ SpacetimeDB │ │ SvelteKit Server │
│ │ │ (load functions, form actions, │
│ - Chat │ │ API routes) │
│ - Kanban │ │ │
│ - Live events │ │ Ansvar: │
│ - Autocomplete │ │ - Les/skriv PostgreSQL direkte │
│ - Studio- │ │ - Opprett jobber i job_queue │
│ markører │ │ - Filopplasting (streaming) │
│ │ │ - RSS-generering │
│ │ │ - Kunnskapsgraf-spørringer │
└──────────────────┘ └──────────────┬───────────────────────┘
│ SQL
┌──────────────────────────┐
│ PostgreSQL │
│ │
│ - Kunnskapsgraf │
│ - Episodemetadata │
│ - Statistikk │
│ - Jobbkø (job_queue) │
│ - Brukerdata │
└──────────────┬────────────┘
│ Poll (SELECT FOR UPDATE)
┌──────────────────────────┐
│ Rust Workers │
│ │
│ - whisper_transcribe │
│ - openrouter_analyze │
│ - research_clip │
│ - stats_parse │
│ - sync_to_pg (SpaceDB→PG)│
└──────────────────────────┘
```
## 3. Ansvarsfordeling
| Komponent | Rolle | Snakker med |
|---|---|---|
| **SvelteKit (klient)** | UI, brukerinteraksjon | SpacetimeDB (WS), SvelteKit server (HTTP) |
| **SvelteKit (server)** | Web-API, PG-tilgang, jobb-trigger | PostgreSQL (SQL) |
| **SpacetimeDB** | Sanntids state, push til klienter | Klienter (WS), sync-worker (intern) |
| **Rust Workers** | Tunge bakgrunnsjobber, synk | PostgreSQL (SQL), SpacetimeDB, OpenRouter, faster-whisper |
## 4. Viktige avklaringer
- **Rust er ikke en API-server.** Rust kjører kun som workers/prosessorer som poller jobbkøen
- **SvelteKit server-side er trygt.** Load functions og form actions kjører på serveren og kan snakke direkte med PG uten sikkerhetsproblemer
- **Filopplasting** håndteres av SvelteKit (streaming for store filer), som lagrer filen på disk og oppretter en jobb i køen
- **SpacetimeDB nås aldri via SvelteKit server** — kun direkte fra klienten via WebSocket
## 5. Instruks for Claude Code
- Ikke opprett et separat Rust HTTP API/webserver-prosjekt
- Bruk SvelteKit `+server.ts` (API routes) eller `+page.server.ts` (form actions/load) for all HTTP-kommunikasjon
- Rust-kode skal struktureres som worker-binærer som konsumerer fra `job_queue`
- For PG-tilgang i SvelteKit, bruk et bibliotek som `postgres.js` eller `drizzle-orm`

87
docs/features/jobbkø.md Normal file
View file

@ -0,0 +1,87 @@
# Feature Spec: Jobbkø (PostgreSQL-basert)
**Filsti:** `docs/features/jobbkø.md`
## 1. Konsept
Et felles, sentralisert køsystem for alle asynkrone bakgrunnsjobber i Sidelinja. Bygget som en enkel tabell i PostgreSQL med Rust-workers som konsumerer jobber. Ingen ekstern message broker — PostgreSQL er køen.
## 2. Hvorfor PostgreSQL?
- Allerede i stacken, ingen ny infrastruktur å drifte
- Transaksjonell garanti: jobben og resultatet kan committes sammen med dataendringer
- Lavt volum (titalls jobber/time) gjør polling neglisjerbart
- Enkel feilsøking via SQL (`SELECT * FROM job_queue WHERE status = 'error'`)
- `SELECT ... FOR UPDATE SKIP LOCKED` gir trygg concurrent polling uten låsekonflikt
## 3. Datastruktur
```sql
CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry');
CREATE TABLE job_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_type TEXT NOT NULL, -- 'whisper_transcribe', 'openrouter_analyze', 'stats_parse', 'research_clip'
payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.)
status job_status NOT NULL DEFAULT 'pending',
priority SMALLINT NOT NULL DEFAULT 0, -- Høyere = viktigere
result JSONB, -- Resultatet ved fullført jobb
error_msg TEXT, -- Feilmelding ved error
attempts SMALLINT NOT NULL DEFAULT 0,
max_attempts SMALLINT NOT NULL DEFAULT 3,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now() -- For utsatte jobber / retry med backoff
);
CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC)
WHERE status IN ('pending', 'retry');
```
## 4. Worker-arkitektur (Rust)
```
┌─────────────────────────────────────────────┐
│ Rust Worker-prosess (én per jobbtype) │
│ │
│ Loop: │
│ 1. SELECT ... FOR UPDATE SKIP LOCKED │
│ WHERE status IN ('pending','retry') │
│ AND job_type = $type │
│ AND scheduled_for <= now() │
│ ORDER BY priority DESC, scheduled_for │
│ LIMIT 1 │
│ │
│ 2. UPDATE status = 'running' │
│ 3. Utfør jobben │
│ 4a. OK: UPDATE status = 'completed' │
│ 4b. Feil: attempts += 1 │
│ Hvis attempts < max_attempts:
│ status = 'retry' │
│ scheduled_for = now() │
│ + backoff(attempts) │
│ Ellers: status = 'error' │
│ │
│ Poll-intervall: 1 sekund (konfigurerbart) │
└─────────────────────────────────────────────┘
```
**Backoff-strategi:** Eksponentiell: `30s × 2^(attempts-1)` (30s, 60s, 120s).
## 5. Jobbtyper
| `job_type` | Konsument | Beskrivelse |
|---|---|---|
| `whisper_transcribe` | Podcastfabrikken | Transkriber MP3 via faster-whisper |
| `openrouter_analyze` | Podcastfabrikken | Metadata-uttrekk fra transkripsjon |
| `research_clip` | AI Research-Klipper | Rens og strukturer innlimt tekst |
| `stats_parse` | Podcast-Statistikk | Batch-prosesser Caddy-logger |
## 6. Observabilitet
- Jobber med `status = 'error'` skal være synlige i Produktivitetssuiten (enkel admin-visning)
- Valgfritt: SpacetimeDB-event ved statusendring slik at UI kan vise fremdrift i sanntid (f.eks. "Transkriberer... 2/3 forsøk")
## 7. Instruks for Claude Code
- Implementer worker-logikken som et Rust-bibliotek (`sidelinja-jobs`) som de ulike binærene kan bruke
- Hver jobbtype får sin egen handler-funksjon, men deler polling-loopen
- Unngå å spinne opp mange tråder — én tokio-task per jobbtype er tilstrekkelig
- Aldri lagre lydfiler i `payload` — bruk filstier
- Ved `stats_parse`: denne erstatter den frittstående cronjobben beskrevet i podcast_statistikk.md — bruk jobbkøen med `scheduled_for` for periodisk kjøring

View file

@ -0,0 +1,134 @@
# Feature Spec: Kunnskapsgraf og Relasjoner (Logseq-modell)
**Filsti:** `docs/features/kunnskapsgraf_og_relasjoner.md`
## 1. Konsept
Inspirert av verktøy som Logseq og Obsidian, bygger vi databasen som en toveis-lenket graf. Målet er å skape "serendipity" (lykketreff) i research-fasen ved å synliggjøre uventede forbindelser. Hvis Aktør A og Aktør B begge er nevnt i samme chat-tråd eller knyttet til samme Tema over tid, skal systemet kunne visualisere denne røde tråden for programlederne.
## 2. Arkitektur og Teknologivalg
Vi unngår tunge, dedikerte grafdatabaser (som Neo4j) for å holde infrastrukturen og ressursbruken (RAM) minimal.
* **Valgt teknologi:** Vanilla PostgreSQL.
* **Mekanisme:** En "Nodes and Edges" (Noder og Kanter) tabellstruktur kombinert med Recursive CTEs (Common Table Expressions) i SQL for å traversere grafen. Dette er mer enn raskt nok for redaksjonelle datamengder (100k+ noder).
## 3. Datastruktur
### 3.1 Supertabell: `nodes`
Alle entiteter i systemet arver sin UUID fra én sentral tabell. Dette gir ekte Foreign Key-integritet på `graph_edges` uten applikasjonslogikk-hacks.
```sql
CREATE TYPE node_type AS ENUM (
'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding'
);
CREATE TABLE nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_type node_type NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```
### 3.2 Detailtabeller
Hver nodetype har sin egen tabell med FK til `nodes`. Eksempler:
```sql
CREATE TABLE actors (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT -- 'person', 'organisasjon', etc.
);
CREATE TABLE topics (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL
);
CREATE TABLE episodes (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
title TEXT NOT NULL,
published_at TIMESTAMPTZ,
guid TEXT UNIQUE NOT NULL -- RSS <guid>, aldri endres
);
CREATE TABLE segments (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
start_time INTERVAL NOT NULL,
end_time INTERVAL NOT NULL,
transcript TEXT, -- Segmentets transkripsjon
CONSTRAINT valid_timerange CHECK (end_time > start_time)
);
CREATE INDEX idx_segments_episode ON segments(episode_id);
CREATE INDEX idx_segments_transcript_fts ON segments USING GIN (to_tsvector('norwegian', transcript));
```
### 3.3 Kantene: `graph_edges`
All kobling skjer i én sentral tabell med ekte FK-integritet:
```sql
CREATE TABLE graph_edges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL, -- 'MENTIONS', 'CONTRADICTS', 'WORKS_FOR', 'PART_OF', 'DISCUSSED_IN'
context_id UUID REFERENCES nodes(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT no_self_reference CHECK (source_id != target_id)
);
CREATE INDEX idx_edges_source ON graph_edges(source_id);
CREATE INDEX idx_edges_target ON graph_edges(target_id);
CREATE INDEX idx_edges_relation ON graph_edges(relation_type);
```
## 4. Segmenter og Transkripsjoner
### 4.1 Segment som grafnode
Episoder deles i **segmenter** — tidsavgrensede deler med egen transkripsjon. Hvert segment er en node i grafen og kan kobles til Temaer, Aktører og Faktoider. Dette muliggjør presise oppslag som: "I Episode 42, fra 14:23 til 21:07, diskuterte dere Skolepolitikk og sa følgende..."
```
Episode 42 (node: episode)
├── Segment 00:00-14:22 (node: segment) ──DISCUSSED_IN──► Tema: Mediepolitikk
├── Segment 14:23-21:07 (node: segment) ──DISCUSSED_IN──► Tema: Skolepolitikk
│ ──MENTIONS──────► Aktør: Støre
└── Segment 21:08-45:00 (node: segment) ──DISCUSSED_IN──► Tema: Kommuneøkonomi
```
Kapitler i RSS-feeden genereres fra segmentene, men er et eget konsern (se `docs/features/podcastfabrikken.md`).
### 4.2 Transkripsjoner: Git som master, PG som søkeindeks
Transkripsjoner lever i **to steder** med klart eierskap:
| Sted | Rolle | Format |
|---|---|---|
| **Git (Forgejo)** | Kilde til sannhet. Redigerbar, sporbar, diffbar | Markdown med tidsstempler |
| **PostgreSQL** | Søkeindeks. Full-text search, koblet til grafen | Segmentert i `segments`-tabellen |
**Flyt:**
```
Whisper → Git (rå transkripsjon med tidsstempler)
→ Redaksjonen korrigerer manuelt ved behov
→ Push til Forgejo
→ Forgejo webhook trigger 'transcript_reimport'-jobb i jobbkøen
→ Rust-worker parser filen, splitter i segmenter
→ DELETE + INSERT i én PG-transaksjon (idempotent reimport)
→ Grafkoblinger bevares (segment-UUID deterministisk fra episode-UUID + tidsstempel)
```
**Deterministisk UUID for segmenter:** `UUID = uuid_v5(episode_uuid, start_time_ms)`. Dette sikrer at samme segment alltid får samme UUID, selv ved reimport. Grafkoblinger som peker på segmentet overlever dermed en full reimport.
## 5. Arbeidsflyt: Hvordan grafen vokser
Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten:
1. **Chat & Notater:** En bruker skriver: *"Apropos #Hans_Petter_Sjøli, hva var greia med #Arbeiderpartiet?"*
2. **Parsing (Svelte/Rust):** Systemet fanger opp de to `#`-taggene (som allerede har UUIDs i `Aktør`-tabellen).
3. **Edge Creation:** SvelteKit server-side oppretter automatisk to nye oppføringer i `graph_edges`-tabellen:
* [Melding UUID] -> `MENTIONS` -> [Sjøli UUID]
* [Melding UUID] -> `MENTIONS` -> [Arbeiderpartiet UUID]
4. **Indirekte relasjon:** Fordi begge aktørene nå deler samme `context_id` (meldingen), vet Kunnskapsgrafen at det finnes en tematisk kobling mellom Sjøli og Ap.
5. **Publisering:** Når en episode publiseres, kobles segmentene automatisk til relevante Temaer og Aktører basert på AI-analyse av transkripsjonen.
## 6. Instruks for Claude Code
* **`nodes`-tabellen er obligatorisk.** Opprett alltid en rad i `nodes` før du inserter i en detailtabell. Bruk en hjelpefunksjon som gjør begge i én transaksjon.
* **Graf-spørringer:** Bruk `WITH RECURSIVE` i PostgreSQL når du bygger endepunkter som skal hente ut "Linked Mentions" eller nettverket rundt en spesifikk Aktør opp til 2-3 ledd ut.
* **Fremtidssikring for UI:** Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (som D3.js eller Vis.js) i Svelte-frontenden. Formatet bør være `{ "nodes": [...], "edges": [...] }`.
* **Transkripsjon-reimport:** Workeren må være idempotent. Bruk `uuid_v5(episode_uuid, start_time_ms)` for deterministiske segment-UUIDs. Slett og gjenopprett segmenter i én transaksjon, men **ikke** slett edges som peker til segmentene — de overlever fordi UUID-en er stabil.
* **Full-text search:** Bruk `to_tsvector('norwegian', transcript)` for norsk språkstøtte i søk.

View file

@ -0,0 +1,19 @@
# Feature Spec: Live AI-Assistent i Studio
**Filsti:** `docs/features/live_ai_assistent.md`
## 1. Konsept
En "virtuell co-host" som lytter på innspillingen i sanntid. Når programlederne nevner spesifikke personer eller organisasjoner, slår systemet opp i Kunnskapsgrafen og dytter relevante "Faktoider" til skjermen deres umiddelbart.
## 2. Arkitektur & Dataflyt
Denne funksjonen krever lav forsinkelse og asynkron prosessering.
1. **Lydkilde (SvelteKit + LiveKit):** SvelteKit-appen bruker `livekit-client`. I tillegg til å sende høykvalitetslyd til de andre deltakerne, rutes en komprimert lydstrøm (via WebSockets eller LiveKit sine egne server-side hooks) til en lokal Rust-tjeneste.
2. **Transkripsjon (Rust + Whisper):** Rust-tjenesten mater lyden inn i `faster-whisper` (eller et tilsvarende raskt API) i små chunks. Den spytter ut en kontinuerlig tekststrøm.
3. **Entity Extraction & Oppslag (Rust + PostgreSQL):** Rust-skriptet analyserer tekststrømmen for egennavn (Named Entity Recognition). Den gjør et lynraskt asynkront oppslag i PostgreSQL: `SELECT * FROM factoids JOIN actors... WHERE actor.name = $1`.
4. **Sanntids-Push (SpacetimeDB):** Hvis et treff finnes, dytter Rust-skriptet faktoiden inn i SpacetimeDB som et event: `LiveFactoidEvent`.
5. **Visning (SvelteKit):** Studio-grensesnittet lytter på SpacetimeDB. Når `LiveFactoidEvent` inntreffer, popper faktoiden lydløst opp i en egen boks på skjermen.
## 3. Utviklingsfaser (For Claude Code)
* **Fase 1:** Ikke bygg live-lyd enda. Bygg funksjonaliteten der grensesnittet lytter på SpacetimeDB, og lag et dummy-script i Rust som dytter test-faktoider inn i SpacetimeDB for å verifisere UI-et.
* **Fase 2:** Koble Whisper til et offline lydopptak og kjør NER/oppslag mot PostgreSQL.
* **Fase 3:** Koble sammen LiveKit-strømmen og Whisper.

View file

@ -0,0 +1,19 @@
# Feature Spec: Podcast-Statistikk
**Filsti:** `docs/features/podcast_statistikk.md`
## 1. Konsept
IAB-kompatibel lytterstatistikk bygget fra bunnen av. Vi fanger all rådata via Caddy, og bruker asynkron batch-prosessering for å bygge grafer og tall uten å belaste webserveren eller databasen med sanntids-skriving.
## 2. Arkitektur & Dataflyt
1. **Rådata (Caddy):** Caddy konfigureres til å skrive access-logs for stien `/media/podcast/*.mp3` til en formatert JSON-fil (f.eks. `/srv/sidelinja/logs/caddy/podcast_access.log`).
2. **Logrotate:** Standard Linux logrotate arkiverer loggene nattlig.
3. **Rust Batch Processor (Jobbkø):** Statistikkparseren kjøres som en `stats_parse`-jobb i den felles jobbkøen (se `docs/features/jobbkø.md`), med `scheduled_for` satt 1 time frem for periodisk kjøring. Workeren re-enqueuer seg selv ved fullføring.
* **Steg A (Filtrering):** Leser JSON-loggen. Fjerner treff fra kjente bots ved å krysjekke `User-Agent` mot OPAWG (Open Podcast Analytics Working Group) sine åpne bot-lister.
* **Steg B (Deduplisering):** Slår sammen byte-range forespørsler. Hvis samme IP og User-Agent har lastet ned deler av samme fil innenfor et 24-timers vindu, telles det som KUN én (1) nedlasting.
* **Steg C (Geografi/Klient):** Mapper User-Agent til Podcast-klient (Spotify, Apple) basert på OPAWG-regler.
4. **Lagring (PostgreSQL):** Rust-programmet skriver det aggregerte resultatet inn i PostgreSQL (`episode_stats` tabell med felter for `date`, `episode_id`, `client_name`, `unique_downloads`).
## 3. Instruks for Claude Code
* Bruk Rust-biblioteket `serde_json` for rask parsing av Caddy-loggene.
* Dette programmet må skrives robust med tanke på at filer kan være låst av Caddy. Det bør tåle å avbrytes, og må holde styr på hvilken linje i loggfilen det prosesserte sist (f.eks. via en liten cursor-fil).
* Rålogger skal ALDRI lagres i PostgreSQL.

View file

@ -0,0 +1,33 @@
# Feature Spec: Podcastfabrikken (Lyd & Publiserings-Pipeline)
**Filsti:** `docs/features/podcastfabrikken.md`
## 1. Konsept
Den automatiserte "samlebåndet" som tar over når en ferdigklippet episode er klar, samt verktøyet for å **oppdatere eksisterende episoder** (f.eks. en rullerende intro-episode). Målet er at maskinen gjør 90 % av grovarbeidet (transkripsjon, metadata, kapittelinndeling), men at redaksjonen alltid kan overstyre resultatet manuelt før publisering.
## 2. Arkitektur & Dataflyt
Dette er en asynkron arbeidsflyt som kombinerer filsystem, AI, databaser og CI/CD.
1. **Trigger (Opplasting/Oppdatering):** Brukeren laster opp en `.mp3`-fil via SvelteKit-grensesnittet. Dette rutes enten som en *ny* episode (`INSERT`), eller en *oppdatering* av en eksisterende (`UPDATE`).
2. **Kø-system (PostgreSQL jobbkø):** Siden lydprosessering tar tid (CPU-intensivt), legges oppgaven i den felles jobbkøen (se `docs/features/jobbkø.md`). Opplastingen oppretter to jobber i sekvens: først `whisper_transcribe`, deretter `openrouter_analyze` (som trigges automatisk ved fullført transkripsjon).
3. **Transkripsjon (faster-whisper):** Rust-worker trigger `faster-whisper` lokalt på serveren og genererer rå tekst med tidsstempler.
4. **AI-Analyse (OpenRouter):** Transkripsjonen sendes til OpenRouter (Claude-modell) for uttrekk av forslag til tittel, sammendrag, show notes og kapittler.
5. **Manuell Godkjenning & Fletting (SvelteKit):**
* *For nye episoder:* Presenteres som et ferskt utkast.
* *For oppdateringer:* Viser AI-ens nye forslag side-om-side med eksisterende metadata. Redaksjonen kan da velge hva som skal beholdes eller flettes (merge).
6. **Publisering (PostgreSQL):** Ved "Godkjenn" lagres metadataene permanent i databasen.
7. **RSS-Generering:** SvelteKit-appen genererer en oppdatert `/feed.xml`.
## 3. Spesialhåndtering: Oppdatering av eksisterende episoder (Cache-busting)
Podcast-apper (Apple, Spotify) og CDN-er cacher innhold aggressivt. For at en endring i f.eks. "Introepisoden" skal slå gjennom hos lytterne, MÅ følgende tekniske regler følges:
* **Filnavn-versjonering (Viktigst!):** Den nye lydfilen skal *aldri* overskrive det gamle filnavnet på disken. Systemet må legge til en hash, UUID eller et tidsstempel (f.eks. `intro_v2_1710289000.mp3`). Dette tvinger appene til å laste ned filen på nytt.
* **RSS `<guid>` (Global Unique Identifier):** Denne taggen MÅ forbli 100% statisk/uendret fra originalepisoden. Den forteller appene at "Dette er fortsatt samme episode, ikke lag en duplikat".
* **RSS `<enclosure>`:** URL-en i `enclosure`-taggen (som peker på `.mp3`-filen) oppdateres i databasen til å reflektere det *nye* filnavnet.
* **RSS `<pubDate>`:** SvelteKit-grensesnittet skal gi redaksjonen en toggle-knapp ved oppdatering:
* Alternativ A: "Behold opprinnelig dato" (Episoden oppdateres i det stille for nye lyttere).
* Alternativ B: "Sett dato til NÅ" (Episoden spretter til toppen av feeden som en ny utgivelse).
## 4. Instruks for Claude Code
* **Lydfiler:** Håndter filopplasting i SvelteKit strømmende (streaming) for filer >100MB for å unngå minne-lekkasjer.
* **Feilhåndtering:** Hvis OpenRouter timer ut eller Whisper feiler, må oppgaven flagges med status `error` i databasen slik at brukeren kan trigge jobben på nytt manuelt via UI.
* **Opprydding (Disk):** Når en fil oppdateres vellykket, skal den gamle/foreldede `.mp3`-filen enten slettes fra Hetzner-serveren automatisk, eller flyttes til en `/archive/`-mappe basert på en miljøvariabel.

View file

@ -0,0 +1,22 @@
# Feature Spec: Produktivitetssuiten
**Filsti:** `docs/features/produktivitetssuite.md`
## 1. Konsept
En asynkron og synkron arbeidsflate der Kunnskapsgrafen møter prosjektstyring. **Temaet** er hovedobjektet, ikke episoden.
## 2. Datastrukturer og Ansvarsfordeling
Dette systemet bruker SpacetimeDB tungt for sanntidsopplevelsen.
* **Tema-bassenget (PostgreSQL + SpacetimeDB):** Alle pågående "Saker". PostgreSQL er kilden til sannhet (langtidslagring), mens SpacetimeDB holder de aktive temaene i minnet slik at chat og oppdateringer skjer uten page-reloads.
* **Trådet Chat (SpacetimeDB):** Meldinger sendes via SpacetimeDB. Hver melding tilhører et `Tema`. Meldinger kan ha en `parent_message_id` for å skape tråder.
* **Sanntids Autocomplete & Mentions (Mobil-optimalisert):**
* Siden SpacetimeDB synkroniserer data til klienten, skal Svelte-grensesnittet ha umiddelbar autocomplete på tekstfeltet.
* Trigger-tegn: `/` (for kommandoer som `/oppgave`), `@` (for brukere/redaksjonsmedlemmer), og feks `#` (for Temaer/Aktører fra Kunnskapsgrafen).
* Skriver man `#Ha...` filtrerer Svelte-klienten umiddelbart den lokale SpacetimeDB-cachen og viser en klikkbar/tappbar liste (f.eks. "Hans Petter Sjøli", "Høyre"). Ved trykk settes hele navnet inn, og det lenkes automatisk opp i databasen.
* **Kanban / Kjøreplan (SpacetimeDB):** Opprettelse av en `Episode` fungerer som en container. Brukerne drar *Temaer* fra Tema-bassenget og inn i en Episodes Kjøreplan (Drag and Drop i SvelteKit). Posisjon/Rekkefølge synkroniseres til alle klienter via SpacetimeDB.
* **Kollaborative Show Notes:** Et tekstfelt koblet til et Tema. Enkle "Operational Transformation"-aktige oppdateringer (eller felt-låsing) håndteres i Rust-modulen til SpacetimeDB.
* **Live Studio-Markører ("Blooper-knapp"):** En funksjon i Svelte-studioet der brukere kan trykke på en knapp under innspilling. Dette fanger opp gjeldende opptakstid (timer/min/sek) og lagrer det i SpacetimeDB som et "Klippepunkt" koblet til episoden.
## 3. Instruks for Claude Code
* Bruk SvelteKit for Drag-and-Drop grensesnitt. Unngå tunge biblioteker hvis native HTML5 Drag and Drop er tilstrekkelig.
* SpacetimeDB skal fungere som "State Manager". Frontend bør ikke ha kompleks lokal state (f.eks. Redux); den skal speile SpacetimeDB sin tilstand.

View file

@ -0,0 +1,63 @@
# Feature Spec: PostgreSQL ↔ SpacetimeDB Synkronisering
**Filsti:** `docs/features/synkronisering.md`
## 1. Konsept
SpacetimeDB gir sanntidsopplevelsen, PostgreSQL er langtidsminnet. Denne spec-en definerer hvordan data flyter mellom dem, hvem som eier sannheten, og hva som skjer ved feil.
## 2. Strategi: Event-drevet med kort forsinkelse
SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. En Rust-worker konsumerer disse og skriver til PostgreSQL, batched med ~5 sekunders vindu.
**Akseptabelt datatap:** Maks 5 sekunder ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes.
## 3. Dataflyt
```
┌──────────────┐ events ┌──────────────┐ batch write ┌──────────────┐
│ SpacetimeDB │ ──────────────► │ Rust Worker │ ────────────────► │ PostgreSQL │
│ (sanntid) │ │ (sync_to_pg) │ │ (persistent)│
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ oppvarming (oppstart / reconnect) ┌──────────────┐
│ PostgreSQL │ ──────────────────────────────────────► │ SpacetimeDB │
└──────────────┘ └──────────────┘
```
## 4. Eierskapsmodell
| Data | Autoritativ kilde | Synkretning | Merknad |
|---|---|---|---|
| Chatmeldinger | SpacetimeDB | → PG (event, batched) | |
| Kanban-posisjon | SpacetimeDB | → PG (event) | |
| Show notes | SpacetimeDB | → PG (event) | |
| Live studio-markører | SpacetimeDB | → PG (event) | |
| Kunnskapsgraf | PostgreSQL | → SpacetimeDB (oppvarming) | Read-only i SpacetimeDB |
| Episodemetadata | PostgreSQL | Ingen synk | |
| Brukerkontoer | PostgreSQL (Authentik) | Ingen synk | |
| Statistikk | PostgreSQL | Ingen synk | |
| Valgomat | TBD | TBD | Konseptet må modnes. Mulig PG-autoritativ med SpacetimeDB som serveringslag |
## 5. Mekanisme
### 5.1 SpacetimeDB → PostgreSQL (persistering)
- SpacetimeDB-modulene kaller en intern `emit_sync_event()`-funksjon ved relevante dataendringer
- Events bufres i en SpacetimeDB-tabell (`sync_outbox`) med tidsstempel og payload
- Rust-workeren poller `sync_outbox` hvert ~5 sekund, leser alle usynkede events, skriver til PostgreSQL i én transaksjon, og markerer dem som synket
- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører
### 5.2 PostgreSQL → SpacetimeDB (oppvarming)
- Ved oppstart (eller reconnect) av SpacetimeDB laster Rust-workeren aktive data fra PG:
- Aktive temaer med siste N chatmeldinger
- Kanban-state for pågående episoder
- Aktør/Tema-navn for autocomplete (read-only cache)
- Dette er en enveis-last, ikke kontinuerlig synk. Kunnskapsgrafen oppdateres i SpacetimeDB kun ved oppstart eller eksplisitt refresh
## 6. Feilhåndtering
- **SpacetimeDB krasjer:** Data siden siste synk (~5 sek) tapes. Ved restart oppvarmes fra PG
- **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk
- **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering)
## 7. 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/features/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen
- Hold sync-payloaden enkel: `{ "table": "chat_messages", "action": "insert", "data": {...} }` — workeren mapper dette til riktig PG-tabell
- Ikke optimaliser for store datamengder ennå. Enkle INSERTs er bra nok til volumet stabiliserer seg

16
docs/features/valgomat.md Normal file
View file

@ -0,0 +1,16 @@
# Feature Spec: Valgomat
**Filsti:** `docs/features/valgomat.md`
## 1. Konsept
En publikumsrettet web-applikasjon for å hjelpe velgere med å finne partimatch. Må være lynrask, tåle høy trafikk (Spike-trafikk etter publisering av episode), og føles interaktiv ("gamified").
## 2. Arkitektur
For å gi en "instant" følelse, bypasses tradisjonell database-arkitektur under selve gjennomføringen.
* **Logikk og Vekting (SpacetimeDB / Rust):** SpacetimeDB egner seg perfekt for Valgomaten. Databasen holder reglene (spørsmål, vekting per parti) i minnet. Rust-koden inne i SpacetimeDB (modulen) beregner resultatet umiddelbart når en bruker sender inn et svar.
* **Frontend (SvelteKit):** En ren, responsiv SvelteKit-klient (PWA) som kobler seg til SpacetimeDB via WebSockets. Svar klikkes, animasjoner vises, og neste spørsmål lastes uten noe nettverks-forsinkelse (fordi tilkoblingen holdes åpen).
* **Statistikk (PostgreSQL):** Aggregerte resultater ("70% av de under 30 i Oslo fikk SV") lagres periodisk over til PostgreSQL for langsiktig analyse og grafer til podcastepisodene.
## 3. Instruks for Claude Code
* Implementer vektingen (algoritmen) som en Rust-funksjon (Reducer) inne i SpacetimeDB-modulen.
* Sørg for at brukere ikke må logge inn for å ta valgomaten (Anonym tilgang støttes i SpacetimeDB).

224
docs/setup/lokal.md Normal file
View file

@ -0,0 +1,224 @@
# Oppsett: Lokalt Utviklingsmiljø (WSL2)
**Filsti:** `docs/setup/lokal.md`
Denne oppskriften setter opp en lokal utviklingsreplika av Sidelinja i WSL2. Målet er at all utvikling og testing skjer her — aldri på produksjonsserveren.
## 0. Forutsetninger
- Windows 11 med WSL2 (Ubuntu 24.04 LTS)
- Docker Desktop for Windows med WSL2-integrasjon aktivert
- Node.js 20+ (via nvm i WSL2)
- Rust toolchain (via rustup i WSL2)
- Git konfigurert med SSH-nøkkel mot produksjons-Forgejo
## 1. Installer verktøy i WSL2
```bash
# Node.js via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# tea CLI (Forgejo)
# Installer fra: https://gitea.com/gitea/tea/releases
```
## 2. Klon prosjektet
```bash
mkdir -p ~/server
cd ~/server
git clone ssh://git@sidelinja.no:222/sidelinja/sidelinja.git .
# Eller om repo allerede finnes lokalt, sett opp remote:
git remote add forgejo ssh://git@sidelinja.no:222/sidelinja/sidelinja.git
```
## 3. Lokal mappestruktur
```bash
# Docker-volumer for databaser (flyktige, ikke backupes)
mkdir -p .docker-data/{postgres,spacetimedb}
mkdir -p .docker-data/media/podcast
mkdir -p .docker-data/logs/caddy
```
## 4. Miljøvariabler (.env.local)
```bash
cat > .env.local << 'EOF'
# === Lokalt utviklingsmiljø ===
DOMAIN=localhost
COMPOSE_PROJECT_NAME=sidelinja-dev
# === PostgreSQL (lokale verdier, ikke hemmelige) ===
POSTGRES_USER=sidelinja
POSTGRES_PASSWORD=localdev
POSTGRES_DB=sidelinja
# === SpacetimeDB ===
# Lokale defaults
# === LiveKit (lokale test-nøkler) ===
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=devsecret
# === OpenRouter (bruk egen nøkkel for AI-testing) ===
OPENROUTER_API_KEY=<din personlige nøkkel>
EOF
```
## 5. docker-compose.dev.yml
Spinner opp kun infrastruktur-tjenestene. SvelteKit kjøres utenfor Docker for HMR.
```yaml
# Fullstendig docker-compose.dev.yml bygges ut ved implementering.
# Struktur:
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./.docker-data/postgres:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432" # KUN localhost, aldri 0.0.0.0
networks:
- sidelinja-dev
spacetimedb:
image: clockworklabs/spacetimedb
volumes:
- ./.docker-data/spacetimedb:/var/lib/spacetimedb
ports:
- "127.0.0.1:3001:3000" # KUN localhost
networks:
- sidelinja-dev
livekit:
image: livekit/livekit-server
ports:
- "127.0.0.1:7880:7880" # KUN localhost
networks:
- sidelinja-dev
caddy:
image: caddy:2
ports:
- "127.0.0.1:443:443"
- "127.0.0.1:80:80"
volumes:
- ./config/caddy/Caddyfile.dev:/etc/caddy/Caddyfile
- ./.docker-data/logs/caddy:/var/log/caddy
- ./.docker-data/media:/srv/media
networks:
- sidelinja-dev
networks:
sidelinja-dev:
driver: bridge
```
## 6. Start infrastruktur
```bash
# Start Docker-tjenestene
docker compose -f docker-compose.dev.yml --env-file .env.local up -d
# Verifiser
docker compose -f docker-compose.dev.yml ps
docker compose -f docker-compose.dev.yml exec postgres pg_isready
```
## 7. Start SvelteKit (utenfor Docker, for HMR)
```bash
cd sveltekit/ # eller der SvelteKit-prosjektet lever
npm install
npm run dev -- --host # Tilgjengelig på https://localhost:5173
```
## 8. Start Rust Workers (under utvikling)
```bash
cd workers/ # eller der Rust worker-prosjektet lever
cargo run --bin sidelinja-worker
```
## 9. Database-seeding (valgfritt)
```bash
# Kjør SQL-seed mot lokal PostgreSQL for testdata
psql -h localhost -U sidelinja -d sidelinja -f db/seed.sql
```
## 10. Lokal Caddy (HTTPS for WebRTC)
WebRTC krever sikker kontekst. Lokal Caddy genererer self-signed cert:
```caddyfile
# config/caddy/Caddyfile.dev
{
local_certs
}
localhost {
# SvelteKit dev server
reverse_proxy host.docker.internal:5173
# SpacetimeDB
handle_path /spacetime/* {
reverse_proxy spacetimedb:3000
}
# LiveKit
handle_path /livekit/* {
reverse_proxy livekit:7880
}
# Media
handle_path /media/* {
root * /srv/media
file_server
}
}
```
## 11. Daglig utviklingsflyt
```bash
# 1. Start infrastruktur (om ikke allerede kjørende)
docker compose -f docker-compose.dev.yml --env-file .env.local up -d
# 2. Start SvelteKit
cd sveltekit && npm run dev
# 3. Gjør endringer, test lokalt
# 4. Commit og push til Forgejo
git add <filer>
git commit -m "beskrivelse"
git push forgejo main
# 5. Deploy til prod (via SSH)
ssh sidelinja@<server-ip> "cd /srv/sidelinja && git pull && docker compose up -d --build"
```
## 12. Forskjeller fra produksjon
| Aspekt | Lokalt | Produksjon |
|---|---|---|
| SvelteKit | `npm run dev` (HMR) | Docker container (bygget) |
| Porter | Localhost-bundet (`127.0.0.1`) | Kun 80/443 via Caddy |
| HTTPS | Self-signed (Caddy `local_certs`) | Let's Encrypt |
| Forgejo | Ikke installert, push direkte til prod | Docker container |
| Authentik | Ikke installert (bypass auth lokalt) | Docker container |
| DB-data | Flyktig (`.docker-data/`, gitignored) | Persistent (`/srv/sidelinja/data/`) |
| Whisper | Valgfritt, kan mockes | Alltid tilgjengelig |

292
docs/setup/produksjon.md Normal file
View file

@ -0,0 +1,292 @@
# Oppsett: Produksjonsserver (Hetzner VPS)
**Filsti:** `docs/setup/produksjon.md`
Denne oppskriften tar en fersk Ubuntu VPS fra null til en komplett Sidelinja-installasjon. Hvert steg er sekvensielt — ikke hopp over noe.
## 0. Forutsetninger
- Hetzner VPS med Ubuntu 24.04 LTS (8 vCPU, 16 GB RAM minimum)
- DNS A-records som peker til VPS-ens IP:
- `sidelinja.org` + `*.sidelinja.org`
- `vegard.info` + `*.vegard.info`
- SSH-tilgang med nøkkelpar (passordautentisering deaktiveres i steg 1)
## 1. Grunnsikring av VPS
```bash
# Oppdater systemet
apt update && apt upgrade -y
# Opprett tjenestebruker (ikke kjør alt som root)
adduser sidelinja
usermod -aG sudo sidelinja
# Kopier SSH-nøkkel til ny bruker
mkdir -p /home/sidelinja/.ssh
cp ~/.ssh/authorized_keys /home/sidelinja/.ssh/
chown -R sidelinja:sidelinja /home/sidelinja/.ssh
# Deaktiver passordautentisering og root-login
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart sshd
# Brannmur: kun SSH, HTTP, HTTPS
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
```
**Logg ut og logg inn som `sidelinja` fra nå av.**
## 2. Installer Docker
```bash
# Docker Engine (offisiell repo)
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Kjør Docker uten sudo
sudo usermod -aG docker sidelinja
newgrp docker
```
## 3. Opprett mappestruktur
```bash
sudo mkdir -p /srv/sidelinja/{config,data,media,logs}
sudo mkdir -p /srv/sidelinja/config/{caddy,authentik}
sudo mkdir -p /srv/sidelinja/data/{postgres,spacetimedb,forgejo,authentik}
sudo mkdir -p /srv/sidelinja/media/podcast
sudo mkdir -p /srv/sidelinja/logs/caddy
sudo chown -R sidelinja:sidelinja /srv/sidelinja
```
Resultat:
```
/srv/sidelinja/
├── docker-compose.yml
├── .env
├── config/
│ ├── caddy/Caddyfile
│ └── authentik/
├── data/
│ ├── postgres/
│ ├── spacetimedb/
│ ├── forgejo/
│ └── authentik/
├── media/
│ └── podcast/
└── logs/
└── caddy/
```
## 4. Miljøvariabler (.env)
```bash
cat > /srv/sidelinja/.env << 'EOF'
# === Domener ===
DOMAIN_SIDELINJA=sidelinja.org
DOMAIN_VEGARD=vegard.info
DOMAIN_AUTH=auth.sidelinja.org
COMPOSE_PROJECT_NAME=sidelinja
# === PostgreSQL ===
POSTGRES_USER=sidelinja
POSTGRES_PASSWORD=<generer med: openssl rand -hex 32>
POSTGRES_DB=sidelinja
# === Authentik ===
AUTHENTIK_SECRET_KEY=<generer med: openssl rand -hex 64>
AUTHENTIK_POSTGRESQL_PASSWORD=<generer med: openssl rand -hex 32>
# Authentik bruker sin egen database i samme PostgreSQL-instans
AUTHENTIK_POSTGRESQL_HOST=postgres
AUTHENTIK_POSTGRESQL_USER=authentik
AUTHENTIK_POSTGRESQL_NAME=authentik
# === Forgejo ===
FORGEJO_DB_PASSWD=<generer med: openssl rand -hex 32>
# === LiveKit ===
LIVEKIT_API_KEY=<generer>
LIVEKIT_API_SECRET=<generer med: openssl rand -hex 32>
# === OpenRouter ===
OPENROUTER_API_KEY=<fra openrouter.ai>
# === Intern ===
# Ingen porter eksponeres utenom 80/443. Alt rutes internt via Docker-nettverket.
EOF
chmod 600 /srv/sidelinja/.env
```
## 5. Tjeneste-installasjon (rekkefølge)
Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i `docker-compose.yml`, men vi verifiserer hvert lag før vi går videre.
### Lag A: Fundament (ingen avhengigheter mellom seg)
1. **Docker-nettverk:** Opprett internt nettverk `sidelinja-net`
2. **PostgreSQL:** Start, opprett databaser for Authentik og Forgejo, verifiser (`pg_isready`)
3. **Caddy:** Start med Caddyfile for alle domener, verifiser at HTTPS fungerer
4. **Authentik:** Start, gjennomfør initial setup via `https://auth.sidelinja.org`
5. **Forgejo:** Start med Authentik som OAuth2-provider, opprett organisasjon og repo
### Lag B: Sanntid (krever nettverk)
6. **SpacetimeDB:** Start, verifiser tilkobling
7. **LiveKit:** Start, verifiser at WebRTC fungerer
### Lag C: Applikasjon (krever alt over)
8. **SvelteKit:** Bygg og start container, verifiser at frontenden laster
9. **Rust Workers:** Bygg og start container(e), verifiser at jobbkøen polles
## 6. docker-compose.yml (skjelett)
```yaml
# Fullstendig docker-compose.yml bygges ut når tjenestene implementeres.
# Denne seksjonen dokumenterer strukturen og viktige regler.
# REGLER:
# - Ingen "ports:" mot host UTENOM Caddy (80, 443)
# - Alle tjenester på samme interne nettverk (sidelinja-net)
# - Volumer bruker bind mounts til /srv/sidelinja/
# - .env-filen lastes automatisk av Docker Compose
networks:
sidelinja-net:
driver: bridge
services:
caddy: # Eneste tjeneste med eksponerte porter (80, 443)
postgres: # data:/srv/sidelinja/data/postgres
authentik: # SSO for alle domener, på auth.sidelinja.org
forgejo: # data:/srv/sidelinja/data/forgejo, på git.sidelinja.org
spacetimedb: # data:/srv/sidelinja/data/spacetimedb
livekit: # Intern port, proxyet via Caddy
sveltekit: # Intern port, proxyet via Caddy
workers: # Rust job workers, ingen porter
```
## 7. Caddy (Caddyfile grunnstruktur)
```caddyfile
# === SSO (felles for alle domener) ===
auth.sidelinja.org {
reverse_proxy authentik:9000
}
# === Sidelinja (hovedapplikasjon) ===
sidelinja.org {
# SvelteKit (frontend + API)
reverse_proxy sveltekit:3000
# LiveKit (WebSocket upgrade)
handle_path /livekit/* {
reverse_proxy livekit:7880
}
# SpacetimeDB (WebSocket)
handle_path /spacetime/* {
reverse_proxy spacetimedb:3000
}
# Podcast media (statiske filer med byte-range support)
handle_path /media/* {
root * /srv/sidelinja/media
file_server
}
# Podcast access log (kun media-forespørsler)
log {
output file /srv/sidelinja/logs/caddy/podcast_access.log
format json
}
}
# === Forgejo (Git) ===
git.sidelinja.org {
reverse_proxy forgejo:3000
}
# === Vegard.info ===
vegard.info {
# Konfigureres når innhold er klart
respond "Under construction" 200
}
```
## 8. PostgreSQL: Initielle databaser
Ved første oppstart må det opprettes separate databaser og brukere for Authentik og Forgejo:
```sql
-- Kjøres mot PostgreSQL etter første start
-- (eller via init-script montert til /docker-entrypoint-initdb.d/)
CREATE USER authentik WITH PASSWORD '<AUTHENTIK_POSTGRESQL_PASSWORD>';
CREATE DATABASE authentik OWNER authentik;
CREATE USER forgejo WITH PASSWORD '<FORGEJO_DB_PASSWD>';
CREATE DATABASE forgejo OWNER forgejo;
```
## 9. Authentik: Initial konfigurasjon
Etter oppstart, gå til `https://auth.sidelinja.org/if/flow/initial-setup/`:
1. Opprett admin-konto
2. Opprett OAuth2/OpenID Connect-provider for Forgejo
3. Opprett OAuth2/OpenID Connect-provider for SvelteKit (senere)
4. Konfigurer brukergrupper etter behov (redaksjon, admin)
## 10. Forgejo: Koble til Authentik
Forgejo konfigureres med Authentik som OAuth2-kilde:
- Authentication Source: OAuth2
- Provider: OpenID Connect
- Discovery URL: `https://auth.sidelinja.org/application/o/<slug>/.well-known/openid-configuration`
- Etter oppsett: opprett organisasjon `sidelinja`, opprett repo `sidelinja`
## 11. Backup-strategi
```bash
# Daglig snapshot (cron, 03:00)
# Inkluderer: data/, media/, config/, docker-compose.yml
# Ekskluderer: logs/ (arkiveres separat månedlig)
# Eksempel med restic eller borgbackup:
# borg create /backup/sidelinja::{now} /srv/sidelinja --exclude /srv/sidelinja/logs
```
## 12. Deploy-workflow (etter initial setup)
Etter at serveren er satt opp, er dette den daglige deploy-flyten:
```bash
# Fra lokal maskin (WSL2):
git push forgejo main
# SSH inn til server:
ssh sidelinja@<server-ip>
cd /srv/sidelinja
git pull
docker compose build --no-cache <tjeneste>
docker compose up -d <tjeneste>
```
## 13. Verifisering etter oppsett
### Lag A (minimum fungerende server)
- [ ] `https://auth.sidelinja.org` viser Authentik login
- [ ] `https://git.sidelinja.org` viser Forgejo, innlogging via Authentik fungerer
- [ ] PostgreSQL: `docker compose exec postgres pg_isready` returnerer OK
- [ ] SSH-push fra lokal WSL2 til Forgejo fungerer
### Lag B-C (når implementert)
- [ ] `https://sidelinja.org` laster SvelteKit-appen
- [ ] `https://vegard.info` svarer
- [ ] SpacetimeDB: WebSocket-tilkobling fra nettleser fungerer
- [ ] LiveKit: Test-rom med video/lyd fungerer
- [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes`