Initial commit: arkitekturdokumentasjon og feature-specs
This commit is contained in:
commit
290a1e398d
14 changed files with 1170 additions and 0 deletions
133
ARCHITECTURE.md
Normal file
133
ARCHITECTURE.md
Normal 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
36
CLAUDE.md
Normal 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
|
||||
19
docs/features/ai_research_klipper.md
Normal file
19
docs/features/ai_research_klipper.md
Normal 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".
|
||||
73
docs/features/api_grensesnitt.md
Normal file
73
docs/features/api_grensesnitt.md
Normal 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
87
docs/features/jobbkø.md
Normal 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
|
||||
134
docs/features/kunnskapsgraf_og_relasjoner.md
Normal file
134
docs/features/kunnskapsgraf_og_relasjoner.md
Normal 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.
|
||||
19
docs/features/live_ai_assistent.md
Normal file
19
docs/features/live_ai_assistent.md
Normal 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.
|
||||
19
docs/features/podcast_statistikk.md
Normal file
19
docs/features/podcast_statistikk.md
Normal 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.
|
||||
33
docs/features/podcastfabrikken.md
Normal file
33
docs/features/podcastfabrikken.md
Normal 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.
|
||||
22
docs/features/produktivitetssuite.md
Normal file
22
docs/features/produktivitetssuite.md
Normal 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.
|
||||
63
docs/features/synkronisering.md
Normal file
63
docs/features/synkronisering.md
Normal 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
16
docs/features/valgomat.md
Normal 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
224
docs/setup/lokal.md
Normal 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
292
docs/setup/produksjon.md
Normal 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`
|
||||
Loading…
Add table
Reference in a new issue