From a5985ef3f85c9f8ad8e4a33c3b0d4b56dc0f2692 Mon Sep 17 00:00:00 2001 From: vegard Date: Sun, 15 Mar 2026 01:40:14 +0100 Subject: [PATCH] Dokumentasjon, erfaringslogg, migrasjoner og infra-oppdateringer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Omorganiser docs/: konsepter, features, infra og proposals i egne mapper - Ny docs/erfaringer/ med lærdommer fra chat-implementering (Svelte 5, SpacetimeDB, adapter-mønster) - Oppdater ARCHITECTURE.md: Lag 1 status, ny §10 Erfaringslogg, SpacetimeDB i lokal dev - Oppdater synkronisering.md med implementeringsstatus og designvalg - Oppdater lokal.md med SpacetimeDB og AI Gateway - Utvid PG-skjema med channels, messages, media_files, message_revisions - Legg til seed_dev.sql, migration_safety.md, .env.example - Nye feature-specs: chat, kanban, whiteboard, live_ai, lydmeldinger m.fl. - Nye konsept-specs: studioet, møterommet, redaksjonen, den asynkrone gjesten m.fl. - SpacetimeDB og AI Gateway i docker-compose.dev.yml - collect-docs.sh inkluderer erfaringer/ Co-Authored-By: Claude Opus 4.6 --- .env.example | 43 ++++ ARCHITECTURE.md | 156 ++++++++++--- CLAUDE.md | 37 ++- docs/concepts/den_asynkrone_gjesten.md | 93 ++++++++ docs/concepts/kunnskapsgrafen.md | 36 +++ docs/concepts/møterommet.md | 40 ++++ .../podcastfabrikken.md | 30 ++- docs/concepts/redaksjonen.md | 36 +++ docs/concepts/studioet.md | 32 +++ docs/concepts/valgomaten.md | 97 ++++++++ docs/erfaringer/README.md | 20 ++ docs/erfaringer/adapter_moenster.md | 65 ++++++ docs/erfaringer/spacetimedb_integrasjon.md | 78 +++++++ docs/erfaringer/svelte5_reaktivitet.md | 73 ++++++ docs/features/ai_research_klipper.md | 2 +- docs/features/chat.md | 139 ++++++++++++ docs/features/kalender.md | 134 +++++++++++ docs/features/kanban.md | 22 ++ docs/features/kunnskaps_bridge.md | 78 +++++++ docs/features/kunnskapsgraf_og_relasjoner.md | 20 +- docs/features/live_ai.md | 66 ++++++ docs/features/live_ai_assistent.md | 38 ---- docs/features/live_transkripsjon.md | 40 ++++ docs/features/lydmeldinger.md | 137 ++++++++++++ docs/features/podcast_statistikk.md | 19 +- docs/features/produktivitetssuite.md | 22 -- docs/features/prompt_lab.md | 101 +++++++++ docs/features/synkronisering.md | 63 ------ docs/features/valgomat.md | 16 -- docs/features/visuell_graf.md | 20 ++ docs/features/whiteboard.md | 34 +++ docs/{features => infra}/ai_gateway.md | 6 +- docs/{features => infra}/api_grensesnitt.md | 4 +- docs/{features => infra}/jobbkø.md | 46 ++-- docs/infra/synkronisering.md | 121 ++++++++++ docs/proposals/README.md | 40 ++++ docs/proposals/artikkel_publisering.md | 128 +++++++++++ docs/proposals/auto_clipper.md | 26 +++ docs/proposals/debate_club.md | 24 ++ docs/proposals/ghost_host_tts.md | 29 +++ docs/proposals/graph_health_monitor.md | 39 ++++ docs/proposals/guest_prep_simulator.md | 29 +++ docs/proposals/komponerbare_sider.md | 69 ++++++ docs/proposals/live_audience_qa.md | 52 +++++ docs/proposals/meme_generator.md | 22 ++ docs/proposals/podcast_time_machine.md | 23 ++ docs/proposals/serendipity_roulette.md | 22 ++ docs/proposals/social_posting.md | 150 +++++++++++++ docs/proposals/valgomat_roast.md | 21 ++ docs/setup/lokal.md | 21 ++ docs/setup/migration_safety.md | 66 ++++++ migrations/0001_initial_schema.sql | 211 +++++++++++++++--- migrations/seed_dev.sql | 65 ++++++ scripts/collect-docs.sh | 20 ++ 54 files changed, 2777 insertions(+), 244 deletions(-) create mode 100644 .env.example create mode 100644 docs/concepts/den_asynkrone_gjesten.md create mode 100644 docs/concepts/kunnskapsgrafen.md create mode 100644 docs/concepts/møterommet.md rename docs/{features => concepts}/podcastfabrikken.md (78%) create mode 100644 docs/concepts/redaksjonen.md create mode 100644 docs/concepts/studioet.md create mode 100644 docs/concepts/valgomaten.md create mode 100644 docs/erfaringer/README.md create mode 100644 docs/erfaringer/adapter_moenster.md create mode 100644 docs/erfaringer/spacetimedb_integrasjon.md create mode 100644 docs/erfaringer/svelte5_reaktivitet.md create mode 100644 docs/features/chat.md create mode 100644 docs/features/kalender.md create mode 100644 docs/features/kanban.md create mode 100644 docs/features/kunnskaps_bridge.md create mode 100644 docs/features/live_ai.md delete mode 100644 docs/features/live_ai_assistent.md create mode 100644 docs/features/live_transkripsjon.md create mode 100644 docs/features/lydmeldinger.md delete mode 100644 docs/features/produktivitetssuite.md create mode 100644 docs/features/prompt_lab.md delete mode 100644 docs/features/synkronisering.md delete mode 100644 docs/features/valgomat.md create mode 100644 docs/features/visuell_graf.md create mode 100644 docs/features/whiteboard.md rename docs/{features => infra}/ai_gateway.md (93%) rename docs/{features => infra}/api_grensesnitt.md (96%) rename docs/{features => infra}/jobbkø.md (61%) create mode 100644 docs/infra/synkronisering.md create mode 100644 docs/proposals/README.md create mode 100644 docs/proposals/artikkel_publisering.md create mode 100644 docs/proposals/auto_clipper.md create mode 100644 docs/proposals/debate_club.md create mode 100644 docs/proposals/ghost_host_tts.md create mode 100644 docs/proposals/graph_health_monitor.md create mode 100644 docs/proposals/guest_prep_simulator.md create mode 100644 docs/proposals/komponerbare_sider.md create mode 100644 docs/proposals/live_audience_qa.md create mode 100644 docs/proposals/meme_generator.md create mode 100644 docs/proposals/podcast_time_machine.md create mode 100644 docs/proposals/serendipity_roulette.md create mode 100644 docs/proposals/social_posting.md create mode 100644 docs/proposals/valgomat_roast.md create mode 100644 docs/setup/migration_safety.md create mode 100644 migrations/seed_dev.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..923d150 --- /dev/null +++ b/.env.example @@ -0,0 +1,43 @@ +# === Lokalt utviklingsmiljo === +# Kopier denne filen til .env.local og fyll inn dine egne verdier. +# cp .env.example .env.local +# +# .env.local er gitignored og skal ALDRI committes. + +DOMAIN=localhost +COMPOSE_PROJECT_NAME=sidelinja-dev + +# === PostgreSQL === +POSTGRES_USER=sidelinja +POSTGRES_PASSWORD=localdev +POSTGRES_DB=sidelinja + +# === Redis === +# Ingen ekstra config nodvendig lokalt + +# === LiveKit (lokale test-nokler, brukes senere) === +LIVEKIT_API_KEY=devkey +LIVEKIT_API_SECRET=devsecret + +# === AI Gateway (LiteLLM) === +LITELLM_MASTER_KEY=sk-sidelinja-dev-1234 + +# === AI-leverandorer (sett dine egne nokler) === +GEMINI_API_KEY= +OPENROUTER_API_KEY= +# ANTHROPIC_API_KEY= +# XAI_API_KEY= + +# === SpacetimeDB === +# Sett denne for å aktivere sanntids-chat via SpacetimeDB. +# Uten den brukes PG-polling som fallback. +VITE_SPACETIMEDB_URL=ws://localhost:3000 + +# === SvelteKit === +DATABASE_URL=postgres://sidelinja:localdev@localhost:5432/sidelinja +AUTH_SECRET= # openssl rand -base64 33 + +# === Authentik OIDC === +AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/ +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 04bdf5d..7ee7fcc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -86,7 +86,7 @@ Når en ny feature eller komponent introduserer data: ### 2.3 Lokalt Utviklingsmiljø Det lokale miljøet (WSL2) er et **kodeutviklingsmiljø**, ikke en replika av prod. Infrastruktur-config (docker-compose, Caddy, Authentik) testes direkte i prod. **Komplett oppsett: `docs/setup/lokal.md`.** -* **Docker Compose Dev:** `docker-compose.dev.yml` spinner opp PostgreSQL, Redis, Caddy og Whisper lokalt. Volumene er flyktige (`.docker-data/`, gitignored). +* **Docker Compose Dev:** `docker-compose.dev.yml` spinner opp PostgreSQL, Redis, SpacetimeDB, Caddy, Whisper og AI Gateway lokalt. Volumene er flyktige (`.docker-data/`, gitignored). * **SvelteKit HMR:** Kjøres utenfor Docker for rask iterasjon. * **Rust Workers:** Kompileres og kjøres lokalt med `cargo run`. * **AI Gateway / Whisper:** Lokale instanser for eksperimentering og prompt-testing. @@ -95,16 +95,35 @@ Det lokale miljøet (WSL2) er et **kodeutviklingsmiljø**, ikke en replika av pr ## 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`). +* **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/infra/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 **LiteLLM** (AI Gateway — sentralisert ruting til Gemini, Claude, Grok, OpenRouter). All AI-kode peker på `http://ai-gateway:4000/v1`, aldri direkte til leverandører. Se `docs/features/ai_gateway.md`. +* **AI / Prosessering:** `faster-whisper` (lokal transkripsjon) og **LiteLLM** (AI Gateway — sentralisert ruting til Gemini, Claude, Grok, OpenRouter). All AI-kode peker på `http://ai-gateway:4000/v1`, aldri direkte til leverandører. Se `docs/infra/ai_gateway.md`. * **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`. +2. **SpacetimeDB (Sanntidsbuffer):** In-memory database for live chat, status på episoder, og live-oppdateringer i studio. Klienten (Svelte) lytter direkte på SpacetimeDB. SpacetimeDB er en **ren sanntidsbuffer** — all data synkes til PostgreSQL innen ~5 sek. PostgreSQL-skjemaet dekker alle SpacetimeDB-tabeller, slik at SpacetimeDB kan erstattes med PG `LISTEN/NOTIFY` + SSE uten arkitekturendring. Se `docs/infra/synkronisering.md` §1.1–1.2. +3. **Synkronisering:** Event-drevet med ~5 sek forsinkelse. SpacetimeDB er autoritativ for sanntidsdata (chat, kanban), PostgreSQL for persistent data (kunnskapsgraf, metadata). Detaljer i `docs/infra/synkronisering.md`. + +### 4.1 Workspace-modellen (Multi-tenancy) +Sidelinja er designet for multi-tenancy fra dag én. Hver organisasjon/podcast opererer i sin egen **workspace** — en streng datasilo som sikrer full isolasjon av kunnskap og arbeidsflyt. + +#### Prinsipp: Ingenting deles på tvers +* **PostgreSQL:** Supertabellen `nodes` og `graph_edges` har obligatorisk `workspace_id`. Row-Level Security (RLS) sikrer at spørringer aldri lekker data mellom workspaces. SvelteKit setter `SET app.current_workspace_id` ved tilkobling. +* **SpacetimeDB:** WebSocket-tilkoblinger bærer et `workspace_id`-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. +* **Mediefiler:** Lagres i `/srv/sidelinja/media/{workspace_slug}/`. Caddy ruter basert på domene. +* **Transkripsjoner:** Ett Forgejo-repo per workspace for SRT-filer. +* **Jobbkø:** Alle jobber i `job_queue` merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og isolerer via applikasjonskode. +* **AI-prompts:** Whisper `initial_prompt` og LLM system-prompts lagres i `workspaces.settings` (JSONB) per workspace. + +#### Tilgangsstyring +Authentik styrer gruppetilhørighet. SvelteKit mapper innlogget brukers Authentik-grupper til tilgjengelige workspaces via `workspace_members`-tabellen. UI-et har en global "Workspace-switcher" som tømmer lokal state (hard reset) ved bytte for å forhindre visuell datalekkasje. + +#### Globale ressurser (unntak fra silo) +* `relation_types` — deles på tvers (systemdefinerte relasjonstyper). +* `users` — Authentik-IDer er globale, workspace-tilhørighet styres via `workspace_members`. +* Valgomat-modulen er publikumsrettet og opererer potensielt på tvers av workspaces. Eierskapsmodellen avklares ved implementering (se `docs/concepts/valgomaten.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`). @@ -128,15 +147,20 @@ Systemet er bygget rundt **Temaer** og **Aktører**, ikke episoder. Dette bygger * **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/`.** +## 7. Planlagte Funksjoner +Detaljerte spesifikasjoner ligger i `docs/concepts/` (brukeropplevelser) og `docs/features/` (byggeklosser). -* **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. +### Konsepter (brukeropplevelser) +* **Studioet:** Podcast-innspilling med LiveKit, live AI faktoid-oppslag og Aha-markør. +* **Møterommet:** LiveKit-basert møterom med AI-referent, off-the-record, whiteboard, scratchpad og søkbar historikk. +* **Redaksjonen:** Daglig arbeidsflate med trådet chat (channels), Kanban, show notes og AI research-klipper. +* **Podcastfabrikken:** Automatisert publiseringspipeline — Whisper, AI-metadata, RSS, cache-busting. +* **Kunnskapsgrafen:** Visuell utforsking og redigering av kunnskapsnettverk (D3.js/Vis.js). +* **Valgomaten:** Publikumsrettet, crowdsourced valgomat med PCA og Sidelinja Explorer. +* **Den Asynkrone Gjesten:** Tidsbegrenset lenke til gjester for asynkrone lydopptak som lander i redaksjonens arbeidsflyt. + +### Features (byggeklosser) +Chat (channels), Kanban, Whiteboard, Live transkripsjon, Live AI (faktoid + referent), Visuell graf, AI Research-Klipper, Lydmeldinger & Diktering, Podcast-statistikk, Kunnskaps-Bridge (cross-workspace), Prompt-Laboratorium. ## 8. Bygge-rekkefølge (Avhengighetskart) @@ -147,25 +171,34 @@ Dette er hovedkonseptene plattformen skal støtte. **Merk: Detaljerte tekniske s - [x] faster-whisper-server (lokal, testet med medium + prompt) ### Lag 1 — Fundament (ingen avhengigheter) -- [ ] PostgreSQL-skjema (nodes, graph_edges, job_queue) -- [ ] SpacetimeDB grunnoppsett -- [ ] SvelteKit skjelett med Authentik-integrasjon -- [ ] AI Gateway (LiteLLM) oppsett + config -- [ ] Git-repostruktur for transkripsjoner (SRT-filer) +- [x] Workspace-modell (workspaces, workspace_members, RLS-policies) +- [x] PostgreSQL-skjema (nodes m/workspace_id, graph_edges, job_queue, messages, channels, media_files) +- [x] SpacetimeDB grunnoppsett (Docker, Rust WASM-modul, TypeScript-bindings) +- [ ] SvelteKit skjelett med Authentik-integrasjon + Workspace-switcher +- [x] AI Gateway (LiteLLM) oppsett + config +- [ ] Git-repostruktur for transkripsjoner (ett repo per workspace) ### Lag 2 — Kjernekomponenter (krever Lag 1) -- [ ] Jobbko-worker (Rust) +- [ ] Jobbkø-worker (Rust) - [ ] Kunnskapsgraf CRUD (SvelteKit server-side) -- [ ] Produktivitetssuiten: Chat + Kanban (SpacetimeDB <-> PG synk) -- [ ] Promptfoo testsett for forste jobbtyper (norsk testdata) +- [~] Chat med channels (PG-adapter + SpacetimeDB hybrid-adapter ferdig, sync-worker gjenstår) +- [ ] Kanban (SpacetimeDB ↔ PG synk) +- [ ] Lydmeldinger & Diktering (opptak + Whisper + AI-opprydding) +- [ ] Prompt-Laboratorium (prompt-testing mot egne data) +- [ ] Promptfoo testsett for første jobbtyper (norsk testdata) ### Lag 3 — Features (krever Lag 2) -- [ ] Podcastfabrikken (Whisper SRT -> Git -> PG-avledede formater + episodeside) -- [ ] AI Research-Klipper (kunnskapsgraf + jobbko + AI Gateway) -- [ ] Podcast-Statistikk (jobbko + episoder) +- [ ] Podcastfabrikken (Whisper SRT → Git → PG-avledede formater + episodeside) +- [ ] AI Research-Klipper (kunnskapsgraf + jobbkø + AI Gateway) +- [ ] Podcast-Statistikk (jobbkø + episoder) +- [ ] Whiteboard (sanntids frihåndstavle i SpacetimeDB) +- [ ] Den Asynkrone Gjesten (gjeste-tokens + lydmeldinger) ### Lag 4 — Avansert (krever Lag 3) -- [ ] Live AI-Assistent (fylt kunnskapsgraf + LiveKit + Whisper small) +- [ ] Studioet: Live AI-Assistent (fylt kunnskapsgraf + LiveKit + Whisper small) +- [ ] Møterommet: AI-Referent (LiveKit + Whisper + møte-oppsummering) +- [ ] Visuell Kunnskapsgraf (D3.js/Vis.js graf-visning) +- [ ] Kunnskaps-Bridge (pgvector, cross-workspace discovery) - [ ] Valgomat (selvstendig, lav prioritet) ## 9. Observabilitet @@ -186,15 +219,80 @@ Alle Docker-containere skal ha `healthcheck` definert i `docker-compose.yml`: ### 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 +- Feilede jobber (`status = 'error'`) poster automatisk en varslingsmelding til et dedikert system-tema i Redaksjonens chat, slik at redaksjonen ser det i sin daglige arbeidsflate -### 9.4 Ingen eksterne tjenester +### 9.4 Observability Dashboard +SvelteKit-appen inkluderer en intern admin-side (`/admin/observability`) som samler: +- **Container-status:** Healthcheck-resultater fra Docker (via `docker compose ps` / Docker socket) +- **Jobbkø:** Pending/running/error-count med sparkline-grafer (siste 24t) +- **AI Gateway:** Token-bruk per jobbtype, kostnad per workspace, failover-hendelser (fra LiteLLMs innebygde logging) +- **Disk/Minne:** Mediamappe-størrelse per workspace, PG-størrelse, SpacetimeDB-minnebruk + +Ingen eksterne tjenester (Prometheus, Grafana) — alt bygges som SvelteKit-sider med data hentet server-side fra PG, Docker og LiteLLM. Konsistent med self-hosted-filosofien. + +### 9.5 Ingen eksterne observability-tjenester All overvåking og varsling skjer internt i Sidelinja-suiten. Ingen avhengighet til Discord, Slack eller andre tredjepartstjenester. -## 10. AI Agent Guidelines (Instrukser for Claude Code) +## 10. Erfaringslogg +Mappen `docs/erfaringer/` samler praktiske lærdommer fra implementering — ikke hva vi valgte, men hva vi lærte som ikke er åpenbart fra koden. Formålet er å treffe raskere blink med neste komponent. Nye komponenter BØR legge til erfaringer etter ferdig implementering. + +Innhold per mars 2025: +- `svelte5_reaktivitet.md` — $state-getters, SSR-feller, polling-mønster +- `spacetimedb_integrasjon.md` — SDK-konvensjoner, BigInt, Rust borrow-feller +- `adapter_moenster.md` — Hybrid PG+SpacetimeDB, anti-patterns, anbefaling for neste komponent + +## 11. Testing og Utrulling + +### 11.1 Teststrategi +Sidelinja har tre testnivåer. Alle nye features MÅ dekke de relevante nivåene. + +| Nivå | Hva | Verktøy | Kjøres | +|---|---|---|---| +| **Enhetstester** | Rust-logikk (jobbkø, sync, parsing) | `cargo test` | Lokalt + CI | +| **Migrasjonstester** | PG-skjema: opp-migrering mot tom DB + seed + spørringer | `psql` + testskript | Lokalt + CI | +| **Prompt-regresjon** | LLM-prompts mot norske testsett | Promptfoo | Lokalt + CI (månedlig cron) | + +**E2E-tester** (SvelteKit → PG → SpacetimeDB) innføres når Lag 2 er stabilt. Inntil da er manuell testing i dev-miljøet tilstrekkelig — kodebasen er liten nok til at det er effektivt. + +### 11.2 Migrasjonstesting +Migrasjonsfiler i `migrations/` testes automatisk: +1. Spin opp en tom PostgreSQL-container +2. Kjør alle migrasjoner sekvensielt +3. Kjør seed-data (fixture) med testdata +4. Verifiser: RLS-policies blokkerer cross-workspace-tilgang, indekser eksisterer, constraints holder +5. Kjør `pg_dump --schema-only` og diff mot forrige kjente state + +### 11.3 Utrullingsprosedyre (Deployment Checklist) +``` +1. [ ] Alle tester grønne lokalt (cargo test, migrasjonstester, promptfoo) +2. [ ] Push til Forgejo (main branch) +3. [ ] SSH til prod: ssh sidelinja@157.180.81.26 +4. [ ] cd /srv/sidelinja && git pull +5. [ ] Nye migrasjoner? → Kjør mot prod-PG (manuelt, verifiser først) +6. [ ] docker compose up -d --build +7. [ ] Verifiser healthchecks: docker compose ps (alle "healthy") +8. [ ] Smoke-test: curl https://sidelinja.org/health +9. [ ] Sjekk logs: docker compose logs --tail=50 +``` + +**Rollback:** `git revert` + ny deploy. Migrasjoner som endrer skjema MÅ ha en tilhørende down-migrering. + +### 11.4 Rate Limiting +Ressurskrevende endepunkter beskyttes mot overbruk: + +| Endepunkt | Begrensning | Mekanisme | +|---|---|---| +| LiveKit (romopprettelse) | Maks 5 aktive rom per workspace | Applikasjonslogikk i SvelteKit | +| Whisper (transkripsjon) | Maks 3 samtidige jobber per workspace | Jobbkø-constraint (`max_concurrent_per_workspace`) | +| AI Gateway (LLM-kall) | LiteLLMs innebygde rate limiting | `max_parallel_requests` i config.yaml | +| Filopplasting (media) | Maks 500 MB per fil, 5 GB per workspace/dag | SvelteKit middleware | + +Caddy håndterer generell rate limiting (DDoS-beskyttelse) via `rate_limit`-direktivet. + +## 12. 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/.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`). +* **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/infra/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. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 2d1c813..9b83da1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,18 +8,39 @@ Self-hosted på Hetzner VPS med full datakontroll. - `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 +- `docs/setup/migration_safety.md` — Sjekkliste for PostgreSQL-migrasjoner (RLS-verifisering) +- `docs/concepts/` — Brukeropplevelser (integrerte produkter): + - `studioet.md` — Podcast-innspilling (LiveKit + Live AI + Aha-markør) + - `møterommet.md` — Interne møter (LiveKit + AI-referent + Whiteboard) + - `redaksjonen.md` — Daglig redaksjonelt arbeid (Chat + Kanban + Research) + - `podcastfabrikken.md` — Publiseringspipeline (Whisper + AI + RSS) + - `kunnskapsgrafen.md` — Utforsking og redigering av kunnskapsnettverk + - `valgomaten.md` — Publikumsrettet crowdsourced valgomat + - `den_asynkrone_gjesten.md` — Asynkrone gjestebidrag via tidsbegrenset lenke +- `docs/features/` — Tekniske byggeklosser (brukes av flere konsepter): + - `kunnskapsgraf_og_relasjoner.md` — Nodes & Edges datamodell i PostgreSQL + - `chat.md` — Trådet chat med mentions og autocomplete (SpacetimeDB) + - `kanban.md` — Drag-and-drop planlegging + - `whiteboard.md` — Sanntids frihåndstavle (møterom, chat, solo) + - `live_transkripsjon.md` — Whisper-pipeline (felles motor for studio/møter/fabrikk) + - `live_ai.md` — Live AI: faktoid-oppslag (studio) + referent (møter) + - `visuell_graf.md` — Interaktiv graf-visning (D3.js/Vis.js) - `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) + - `lydmeldinger.md` — Lydmeldinger, diktering og tale-til-tekst - `podcast_statistikk.md` — IAB-kompatibel lytterstatistikk fra Caddy-logger - - `valgomat.md` — Publikumsrettet valgomat (SpacetimeDB) + - `kunnskaps_bridge.md` — Cross-workspace discovery via vector embeddings + - `prompt_lab.md` — Internt verktøy for testing og deploy av LLM-prompts + - `kalender.md` — Redaksjonell kalender med abonnementsmodell og ICS-eksport +- `docs/infra/` — Infrastruktur (ikke brukersynlig): - `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 - `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + OpenRouter fallback) +- `docs/proposals/` — Halvtenkte idéer og kreative innfall (se `README.md` for oversikt) +- `docs/erfaringer/` — Lærdommer fra implementering (feller, anti-patterns, løsninger): + - `svelte5_reaktivitet.md` — $state-getters, SSR-feller, polling-mønster + - `spacetimedb_integrasjon.md` — SDK-konvensjoner, BigInt, Rust borrow-feller + - `adapter_moenster.md` — Hybrid PG+SpacetimeDB, anti-patterns, anbefaling for neste komponent ## Stack - **Backend/Automasjon:** Rust @@ -45,4 +66,6 @@ Self-hosted på Hetzner VPS med full datakontroll. - Tunge AI-jobber (Whisper, LLM-kall) skal aldri blokkere web-requests - All AI-kode peker på `http://ai-gateway:4000/v1` — aldri direkte til leverandør-APIer - Kod og test lokalt i WSL2, deploy via push til Forgejo + SSH pull -- Sjekk alltid `docs/features/.md` før du implementerer en feature +- Sjekk alltid relevant doc i `docs/concepts/`, `docs/features/` eller `docs/infra/` før du implementerer +- Sjekk `docs/erfaringer/` for kjente feller før du implementerer med Svelte 5, SpacetimeDB eller adapter-mønsteret +- Etter ferdig implementering av en komponent: dokumenter lærdommer i `docs/erfaringer/` diff --git a/docs/concepts/den_asynkrone_gjesten.md b/docs/concepts/den_asynkrone_gjesten.md new file mode 100644 index 0000000..1a51e32 --- /dev/null +++ b/docs/concepts/den_asynkrone_gjesten.md @@ -0,0 +1,93 @@ +# Konsept: Den Asynkrone Gjesten +**Filsti:** `docs/concepts/den_asynkrone_gjesten.md` + +## 1. Konsept +Mange interessante gjester har ikke tid til å stille i studio. Den Asynkrone Gjesten lar redaksjonen sende en unik lenke til en gjest som kan svare på spørsmål via tale — fra mobilen, når det passer dem. Svarene lander direkte i redaksjonens arbeidsflyt, transkriberes automatisk, og kan brukes i podcasten. + +## 2. Brukeropplevelse + +### 2.1 Redaksjonens side +1. Redaksjonen oppretter en "Gjestesesjon" knyttet til et Tema. +2. Legger inn spørsmål (tekst) som gjesten skal svare på. +3. Systemet genererer en unik, tidsbegrenset URL. +4. URL-en sendes til gjesten via e-post, SMS eller chat. +5. Gjestens svar (lydmeldinger) dukker opp i Tema-chatten som `voice_memo`-meldinger, automatisk transkribert. +6. Redaksjonen triagerer svarene — kan tagge, klippe inn i episode, eller bruke som research. + +### 2.2 Gjestens side +1. Gjesten åpner lenken i mobilnettleseren. Ingen app, ingen konto, ingen registrering. +2. Ser en enkel, ren flate: podcast-logo, spørsmålene fra redaksjonen, og en opptaksknapp per spørsmål. +3. Trykker record, snakker, trykker stopp. Kan lytte tilbake og ta om igjen. +4. Ved innsending lastes lydfilene opp og gjesten ser en bekreftelse. +5. Lenken utløper etter gitt tid eller antall besøk. + +### 2.3 Minimal friksjon +- Ingen Authentik-innlogging — tilgang via signert token +- Ingen app — ren PWA/nettleser +- Ingen redigering — gjesten snakker bare +- Responsivt, mobil-first design + +## 3. Komponenter + +| Feature | Rolle | +|---|---| +| Lydmeldinger | Opptakskomponent gjenbrukes (se `docs/features/lydmeldinger.md`) | +| Chat (channels) | Svarene lander i en channel knyttet til Temaet | +| Live transkripsjon | Whisper transkriberer via jobbkø (se `docs/features/live_transkripsjon.md`) | +| Podcastfabrikken | Lydklipp kan trekkes inn som segment (se `docs/concepts/podcastfabrikken.md`) | + +## 4. Autentisering: Gjeste-tokens + +### 4.1 Datamodell + +```sql +guest_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + guest_name TEXT NOT NULL, -- Visningsnavn ("Erna Solberg") + questions JSONB NOT NULL, -- [{ "sort": 1, "text": "Hva tenker du om...?" }] + token TEXT UNIQUE NOT NULL, -- Kryptografisk sikker, URL-safe token + expires_at TIMESTAMPTZ NOT NULL, + max_recordings SMALLINT DEFAULT 10, -- Maks antall opptak + recordings_count SMALLINT DEFAULT 0, + created_by TEXT NOT NULL REFERENCES users(authentik_id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +) +``` + +### 4.2 Sikkerhet +- Token er kryptografisk tilfeldig (256-bit, URL-safe base64). +- SvelteKit validerer token ved hvert request: sjekker expiry, recordings_count < max_recordings, og workspace-tilhørighet. +- Gjestens meldinger merkes med `author_id = NULL` og `metadata.guest_name` + `metadata.guest_token_id` for sporbarhet. +- Ingen tilgang til andre channels, workspaces eller funksjoner. +- Tokenet kan revokeres manuelt av redaksjonen. + +### 4.3 Flyt (teknisk) +``` +Gjest åpner URL med token + → SvelteKit validerer token + → Viser spørsmål + opptaksknapp + → Gjest tar opp svar + → SvelteKit streamer lydfil til media/{workspace_slug}/voice/ + → Oppretter message (voice_memo) i channelen + → Oppretter whisper_transcribe-jobb i jobbkøen + → Inkrementerer recordings_count + → Redaksjonen ser svaret i Tema-chatten +``` + +## 5. Dataklassifisering + +| Data | Kategori | Detaljer | +|---|---|---| +| Gjestens lydopptak | Kritisk (backup) | Unikt innhold | +| Guest tokens | Flyktig (TTL) | Utløper automatisk, slett expired tokens periodisk | +| Spørsmål (JSONB) | Kritisk (PG) | Redaksjonelt innhold | + +## 6. Instruks for Claude Code +* `guest_tokens`-tabellen er **ikke** en node i grafen — den er ren tilgangsstyring. +* Gjeste-UI er en egen SvelteKit-rute (`/guest/[token]`) med minimal layout (ingen navbar, ingen workspace-switcher). +* Gjenbruk lydmeldinger-komponenten — ikke bygg en egen opptaksflyt. +* Meldinger fra gjester har `author_id = NULL`. Frontend må håndtere dette gracefully (vis `guest_name` i stedet). +* Tokenet skal **aldri** gi tilgang til å lese andre meldinger i channelen — gjesten kan kun skrive. +* Alt er workspace-scopet. Token bærer workspace_id eksplisitt. diff --git a/docs/concepts/kunnskapsgrafen.md b/docs/concepts/kunnskapsgrafen.md new file mode 100644 index 0000000..6d000df --- /dev/null +++ b/docs/concepts/kunnskapsgrafen.md @@ -0,0 +1,36 @@ +# Konsept: Kunnskapsgrafen (Utforsking og redigering) +**Filsti:** `docs/concepts/kunnskapsgrafen.md` + +## 1. Konsept +Kunnskapsgrafen er Sidelinjas kjerne — et levende nettverk av Temaer, Aktører, Faktoider, Episoder og Segmenter. Inspirert av Logseq og Obsidian bygger den seg opp organisk gjennom daglig bruk og skaper "serendipity" (lykketreff) i research-fasen ved å synliggjøre uventede forbindelser. + +## 2. Brukeropplevelse + +### 2.1 Organisk vekst +Grafen vokser gjennom daglig bruk av Sidelinja: +1. **Chat-meldinger** med `#`-tags oppretter automatisk `MENTIONS`-relasjoner i grafen. +2. **AI Research-Klipperen** trekker ut aktører og faktoider fra innlimt tekst. +3. **Podcastfabrikken** kobler episode-segmenter til temaer og aktører. +4. **Møtereferater** trådes automatisk mot temaer og aktører av AI-referenten. + +### 2.2 Visuell utforsking +En interaktiv graf-visning (se `docs/features/visuell_graf.md`) lar redaksjonen: +- Navigere nettverket rundt en Aktør eller et Tema (2-3 ledd ut) +- Dra streker mellom noder for å opprette nye relasjoner +- Filtrere etter nodetype, relasjonstype, tidsperiode eller fritekst +- Bruke `PART_OF`-hierarkier for fleksibel prosjektorganisering uten stive mappestrukturer + +### 2.3 Søk +Full-text search på norsk (`to_tsvector('norwegian', ...)`) gjør det mulig å søke på tvers av alle episoder, segmenter og faktoider. + +## 3. Komponenter + +| Feature | Rolle i Kunnskapsgrafen | +|---|---| +| Kunnskapsgraf datamodell | Nodes/edges i PostgreSQL (se `docs/features/kunnskapsgraf_og_relasjoner.md`) | +| Visuell graf | Interaktiv D3.js/Vis.js-visning (se `docs/features/visuell_graf.md`) | +| Chat | Mentions (`#`/`@`) oppretter edges automatisk (se `docs/features/chat.md`) | +| AI Research-Klipper | Trekker ut aktører/faktoider til grafen (se `docs/features/ai_research_klipper.md`) | + +## 4. Datamodell +Den tekniske datamodellen (nodes-supertabell, graph_edges, detailtabeller, deterministiske UUIDs, workspace-isolasjon) er dokumentert i `docs/features/kunnskapsgraf_og_relasjoner.md`. diff --git a/docs/concepts/møterommet.md b/docs/concepts/møterommet.md new file mode 100644 index 0000000..c2ec9eb --- /dev/null +++ b/docs/concepts/møterommet.md @@ -0,0 +1,40 @@ +# Konsept: Møterommet (Interne redaksjonsmøter) +**Filsti:** `docs/concepts/møterommet.md` + +## 1. Konsept +Et fullverdig virtuelt møterom for Sidelinjas redaksjon. Bygget på LiveKit for lyd/video, med AI-referent som automatisk genererer referat og action points, delt whiteboard for visuell brainstorming, og søkbar møtehistorikk i Kunnskapsgrafen. + +## 2. Brukeropplevelse +1. En bruker oppretter et møterom i SvelteKit. Alle deltakere kobler seg til via LiveKit (WebRTC). +2. Under møtet er følgende verktøy tilgjengelige: + - **Video/lyd** — LiveKit-strøm mellom deltakere + - **Whiteboard** — Frihåndstavle for skisser (se `docs/features/whiteboard.md`) + - **Delt scratchpad** — SpacetimeDB-drevet tekstfelt for kjappe notater + - **Aha-markør** — Markerer viktige øyeblikk med tidsstempel + - **Off-the-record** — Pauser AI-lytting og opptak +3. Whisper transkriberer møtet via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`). +4. Ved møteslutt genererer AI-referenten (se `docs/features/live_ai.md`, møte-modus): + - Strukturert referat (Markdown) + - Action points → foreslåtte Kanban-kort (se `docs/features/kanban.md`) + - Identifiserte `#Temaer` og `@Aktører` → automatisk tråding i Kunnskapsgrafen + +## 3. Komponenter + +| Feature | Rolle i Møterommet | +|---|---| +| LiveKit | WebRTC lyd/video | +| Live transkripsjon | Whisper-pipeline for møtetranskripsjon (se `docs/features/live_transkripsjon.md`) | +| Live AI (møte-modus) | Referat, action points, tråding (se `docs/features/live_ai.md`) | +| Whiteboard | Frihåndstavle for visuell brainstorming (se `docs/features/whiteboard.md`) | +| Chat | Scratchpad / tekstnotater (se `docs/features/chat.md`) | +| Kanban | Mottaker av foreslåtte action-point-kort (se `docs/features/kanban.md`) | + +## 4. Off-the-record +Når off-the-record er aktivt: +- Whisper-strømmen stopper — ingen data lagres +- Visuell indikator i alle deltakeres grensesnitt +- Transkripsjonen får et gap i tidslinja +- Whiteboard forblir aktivt (visuelt, ikke tale) + +## 5. Søkbar møtehistorikk +Møter transkriberes i segmenter (samme modell som episode-segmenter) og indekseres i PostgreSQL med full-text search. Møtereferater lenkes til Temaer og Aktører via `graph_edges`, slik at intern møtehistorikk blir søkbar i Kunnskapsgrafen. diff --git a/docs/features/podcastfabrikken.md b/docs/concepts/podcastfabrikken.md similarity index 78% rename from docs/features/podcastfabrikken.md rename to docs/concepts/podcastfabrikken.md index 76f23b8..e47209d 100644 --- a/docs/features/podcastfabrikken.md +++ b/docs/concepts/podcastfabrikken.md @@ -1,5 +1,5 @@ -# Feature Spec: Podcastfabrikken (Lyd & Publiserings-Pipeline) -**Filsti:** `docs/features/podcastfabrikken.md` +# Konsept: Podcastfabrikken (Lyd & Publiserings-Pipeline) +**Filsti:** `docs/concepts/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. @@ -8,7 +8,7 @@ Den automatiserte "samlebåndet" som tar over når en ferdigklippet episode er k 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). +2. **Kø-system (PostgreSQL jobbkø):** Siden lydprosessering tar tid (CPU-intensivt), legges oppgaven i den felles jobbkøen (se `docs/infra/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 kaller faster-whisper-server (OpenAI-kompatibelt API, `POST /v1/audio/transcriptions`) med `response_format=srt` og mottar SRT direkte. Modell: `Systran/faster-whisper-medium` med `initial_prompt` (navneliste). 4. **Lagring av transkripsjon (Git):** Rust-worker committer SRT-filen til Forgejo. SRT er master-formatet — redigerbart, tidsstemplet, og et etablert standardformat. Git gir diff, historikk og sporbarhet. Redaksjonen kan redigere SRT direkte. 5. **Avledede formater (PostgreSQL):** Ved commit (via Forgejo webhook) parser en Rust-worker SRT-filen og genererer: @@ -69,8 +69,28 @@ Sidelinja podcast med Vegard Nøtnæs, Trond Sørensen, Arne Eidshagen, Peter Hagen, Nicolai Buzatu, Bjørn Einar Drag, Øystein Sjølie ``` -## 5. Instruks for Claude Code +## 5. Workspace-spesifikk konfigurasjon +Hver workspace har sin egen podcast-konfigurasjon, lagret i `workspaces.settings` (JSONB): + +### 5.1 Mediefiler +Lydfiler lagres i undermapper per workspace: `/srv/sidelinja/media/{workspace_slug}/`. Caddy ruter trafikk basert på domene (fra `workspaces.domain`) til riktig undermappe. + +### 5.2 Transkripsjoner +Det opprettes **ett Forgejo-repo per workspace** for SRT-filer, slik at historikk og redigering ikke blandes på tvers av podcaster. + +### 5.3 AI-prompts +* **Whisper `initial_prompt`:** Navnelister og kontekst lagres per workspace i `settings.whisper_prompt`. Rust-worker bygger prompten fra statisk liste + aktører i workspace-ets kunnskapsgraf. +* **LLM system-prompts:** OpenRouter-prompts for metadata-uttrekk lagres i `settings.llm_prompts` slik at AI-en kjenner konteksten og vertene for akkurat den podcasten. + +### 5.4 RSS-feed +SvelteKit genererer `/feed.xml` dynamisk basert på domenet forespørselen kommer fra (matcher `workspaces.domain`), eller workspace-slug som fallback. + +### 5.5 Statistikk +Rust-workeren `stats_parse` knytter nedlastingstall fra Caddy-logger til riktig `workspace_id` basert på filsti i loggen. + +## 6. 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. -* **Transkripsjoner:** Master-kopi alltid i Git. Aldri rediger avledede formater direkte i PG — de regenereres fra Git-kilden. \ No newline at end of file +* **Transkripsjoner:** Master-kopi alltid i Git. Aldri rediger avledede formater direkte i PG — de regenereres fra Git-kilden. +* **Workspace:** Alle jobber, mediefiler og metadata opprettes med riktig `workspace_id`. Hent workspace-config (prompts, domene) fra `workspaces.settings`. \ No newline at end of file diff --git a/docs/concepts/redaksjonen.md b/docs/concepts/redaksjonen.md new file mode 100644 index 0000000..39881ad --- /dev/null +++ b/docs/concepts/redaksjonen.md @@ -0,0 +1,36 @@ +# Konsept: Redaksjonen (Daglig redaksjonelt arbeid) +**Filsti:** `docs/concepts/redaksjonen.md` + +## 1. Konsept +Redaksjonen er den daglige arbeidsflaten for Sidelinjas team. Her planlegges episoder, diskuteres temaer, samles research og skrives show notes. **Temaet** er hovedobjektet — ikke episoden. + +## 2. Brukeropplevelse + +### 2.1 Tema-bassenget +Alle pågående "Saker" vises i en oversikt. PostgreSQL er kilden til sannhet, SpacetimeDB holder aktive temaer i minnet for sanntidsoppdateringer. + +### 2.2 Trådet Chat +Hver melding tilhører et Tema. Meldinger støtter tråder (svar-på-svar) og rike mentions med `#`-tags og `@`-mentions. Se `docs/features/chat.md` for teknisk spesifikasjon. + +### 2.3 Kanban / Kjøreplan +Episoder fungerer som containere. Brukerne drar Temaer fra bassenget inn i en episodes kjøreplan med drag-and-drop. Se `docs/features/kanban.md` for teknisk spesifikasjon. + +### 2.4 Show Notes +Et kollaborativt tekstfelt koblet til et Tema. Enkle "Operational Transformation"-aktige oppdateringer (eller felt-låsing) håndteres i SpacetimeDB-modulen. Synkes til PostgreSQL for persistens. + +### 2.5 AI Research-Klipper +Brukere limer inn uformatert tekst fra nettet. AI-en renser, oppsummerer og trekker ut aktører og faktoider til Kunnskapsgrafen. Se `docs/features/ai_research_klipper.md`. + +## 3. Komponenter + +| Feature | Rolle i Redaksjonen | +|---|---| +| Chat | Trådet diskusjon per Tema (se `docs/features/chat.md`) | +| Kanban | Episodeplanlegging (se `docs/features/kanban.md`) | +| AI Research-Klipper | Research-inntak (se `docs/features/ai_research_klipper.md`) | +| Whiteboard | Kan åpnes fra chat for visuell brainstorming (se `docs/features/whiteboard.md`) | + +## 4. Instruks for Claude Code +* Bruk SvelteKit for Drag-and-Drop. Unngå tunge biblioteker hvis native HTML5 Drag and Drop er tilstrekkelig. +* SpacetimeDB er "State Manager". Frontend speiler SpacetimeDB sin tilstand — ikke bygg kompleks lokal state. +* Alle data er workspace-scopet. SpacetimeDB-tilkoblinger bærer `workspace_id`. diff --git a/docs/concepts/studioet.md b/docs/concepts/studioet.md new file mode 100644 index 0000000..806269e --- /dev/null +++ b/docs/concepts/studioet.md @@ -0,0 +1,32 @@ +# Konsept: Studioet (Podcast-innspilling) +**Filsti:** `docs/concepts/studioet.md` + +## 1. Konsept +Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndterer WebRTC for flerbruker lyd/video, mens AI-assistenten lytter med og dytter relevante faktoider til programlederne i sanntid. + +## 2. Brukeropplevelse +1. Programlederne åpner studioet i SvelteKit (PWA) og kobler seg til et LiveKit-rom. +2. Høykvalitetslyd streames mellom deltakerne via WebRTC. +3. I bakgrunnen transkriberer Whisper lydstrømmen i chunks (~5 sek) via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`). +4. AI-assistenten analyserer transkripsjonen for entiteter (NER) og slår opp i Kunnskapsgrafen. Relevante faktoider popper lydløst opp på skjermen (se `docs/features/live_ai.md`, studio-modus). +5. Programlederne kan trykke **Aha-markør** for å markere viktige øyeblikk. Tidsstempelet lagres i SpacetimeDB, koblet til episoden. +6. Etter innspilling skyves lydfilen inn i Podcastfabrikken for full transkripsjon og publisering (se `docs/concepts/podcastfabrikken.md`). + +## 3. Komponenter + +| Feature | Rolle i Studioet | +|---|---| +| LiveKit | WebRTC lyd/video mellom deltakere | +| Live transkripsjon | Whisper `small` for lav latens, ~1s forsinkelse (se `docs/features/live_transkripsjon.md`) | +| Live AI (studio-modus) | NER + faktoid-oppslag fra Kunnskapsgrafen (se `docs/features/live_ai.md`) | +| Aha-markør | Manuell markering av viktige øyeblikk, lagres i SpacetimeDB | + +## 4. Avgrensning +- Studioet er for **innspilling**, ikke redigering. Klipping/postproduksjon skjer utenfor Sidelinja. +- Live-transkripsjonen her er **flyktig** (TTL 30 dager) — den endelige transkripsjonen lages via Podcastfabrikken med `medium` + `initial_prompt`. +- Aha-markøren deles med Møterommet (se `docs/concepts/møterommet.md`), men i studio-konteksten brukes den primært til klippepunkter. + +## 5. Utviklingsfaser +1. Bygg SpacetimeDB-lytter i frontend + dummy faktoid-push for å verifisere UI. +2. Koble Whisper til et offline lydopptak, kjør NER/oppslag mot PostgreSQL. +3. Koble LiveKit-strømmen til Whisper for sanntid. diff --git a/docs/concepts/valgomaten.md b/docs/concepts/valgomaten.md new file mode 100644 index 0000000..1776d8c --- /dev/null +++ b/docs/concepts/valgomaten.md @@ -0,0 +1,97 @@ +# Konsept: Valgomaten (Crowdsourced & Datadrevet) +**Filsti:** `docs/concepts/valgomaten.md` + +## 1. Konsept & Kjernefilosofi +Neste generasjons valgomat tar et oppgjør med den redaktørstyrte, endimensjonale modellen. Istedenfor forhåndsdefinerte akser styrer brukerne innholdet "bottom-up" gjennom et marked for akser. Valgomaten kombinerer friksjonsfri "Tinder-swiping" for folk flest, med et dyptgående redaksjonelt verktøy for nerdene, alt bygget på toppen av Sidelinjas eksisterende Kunnskapsgraf og sanntidsinfrastruktur. + +### 1.1 Markedsmekanisme +Brukere kan fritt opprette nye politiske akser og spørsmål. Dimensjoner som engasjerer (får mange svar) "bobler opp" og blir standardakser, mens irrelevante forsvinner. Ingen begrensning på antall akser — systemet støtter komplekse skillelinjer i samfunnet. + +### 1.2 Teoretisk fundament (Default-akser) +For å unngå "Blank Canvas"-syndromet startes plattformen med anerkjente rammeverk som fungerer som ankere: +* **John Haidts Moral Foundations Theory:** Omsorg, Rettferdighet, Lojalitet, Autoritet, Renhet. +* **Rokkans skillelinjer:** Sentrum/periferi, by/land, religiøs/sekulær. +* **Nanny State Index:** Formynderstat vs. personlig frihet og ansvar. +* **Intensjon vs. Resultat:** Støttes politikk på bakgrunn av teoretisk målsetting eller empirisk utfall? + +## 2. Brukeropplevelse (Trakten) +Valgomaten er bygget som en to-trinns trakt for å maksimere viral spredning samtidig som dataintegriteten bevares. + +### 2.1 Forsiden: Friksjonsfri & Anonym +* **Mekanikk:** Ingen registrering kreves for å starte. SvelteKit genererer en anonym UUID som lagres i `localStorage` og brukes mot SpacetimeDB. +* **UX:** Et rent kortstokk-grensesnitt (Tinder-stil swipe). Påstand på forsiden, brukeren velger Enig/Uenig. +* **Tre-trinns kalibrering (OKCupid-inspirert):** + 1. Hva mener du? + 2. Hva ønsker du at kandidaten skal mene? + 3. Hvor viktig er dette for deg? +* **Dealbreakers (Negative Veto):** Brukere kan sette absolutte grenser (f.eks. "Uansett hvor enige vi er om skatt, matcher jeg aldri med en som er for/mot vindkraft"). +* **Sanntid:** SpacetimeDB beregner PCA (Prinsipalkomponentanalyse) og oppdaterer brukerens posisjon lynraskt i minnet for hvert swipe. + +### 2.2 Baksiden: "Snu kortet" (Krever innlogging) +For avansert interaksjon må brukeren logge inn via Authentik (SSO). Den anonyme sesjonen flettes da automatisk med brukerprofilen. På baksiden av kortet finner man: +* **Folkets Redaksjonsmøte:** Mulighet til å se andres kommentarer til spørsmålet, foreslå endringer, og stemme opp/ned forbedringsforslag. +* **Podcast-snarveien:** Hvis spørsmålet er knyttet til et *Tema* i Kunnskapsgrafen, vises en "Play"-knapp. Caddy (`Accept-Ranges: bytes`) streamer tidsstemplede lydsegmenter fra tidligere Sidelinja-episoder der dette temaet ble diskutert. +* **Multi-akse vekting:** Innsikt i hvilke akser spørsmålet påvirker (f.eks. 80% Klima, 20% Sentrum/Periferi), med mulighet for å foreslå nye akse-koblinger. + +## 3. Matching & Resultat + +### 3.1 Individfokus +Matcher velgeren mot spesifikke lokalpolitikere og listekandidater, ikke bare mot det sentrale partiprogrammet. + +### 3.2 PCA (Prinsipalkomponentanalyse) +Matematisk reduksjon av kompleksitet. Algoritmen koker ned brukerens svar på tvers av 50+ variabler til de 2-3 hovedfaktorene som faktisk styrer vedkommendes politiske kompass, og visualiserer disse spesifikt for brukeren. + +## 4. Crowdsourcing & Dataintegritet + +### 4.1 Uforanderlig Historikk (Versjonering) +Et spørsmål i PostgreSQL kan *aldri* endres (`UPDATE`) etter at folk har begynt å svare på det. +* Hvis et forbedringsforslag stemmes frem (eller godkjennes av redaksjonen), opprettes en ny versjon (f.eks. `Spørsmål 42 (v2)`). +* Brukere beholder sine resultater knyttet til den nøyaktige teksten de leste. Algoritmen vet at v1 og v2 tilhører samme politiske konsept. + +### 4.2 Opprettelse av nye spørsmål og akser (Sandkassen) +* **Inkubator:** Innloggede brukere kan opprette nye spørsmål eller definere helt nye akser (ved å angi to motpoler). Disse havner i en "Sandkasse" og vises ikke på forsiden før de har fått nok organisk engasjement fra andre innloggede brukere. +* **Vekting via Graph Edges:** Koblingen mellom et spørsmål og en akse lagres som en relasjon i `graph_edges`. Når brukere "stemmer opp" at et spørsmål tilhører en spesifikk akse, øker feltet `confidence`. SpacetimeDB bruker denne `confidence`-scoren som multiplikator i match-algoritmen. + +## 5. Visualisering & Sidelinja Explorer + +### 5.1 Innhenting av referansedata +For å bygge nøyaktige velgerkart, introduseres et valgfritt spørsmål før resultatet vises: *"For å kalibrere landskapet: Hva stemte du ved forrige valg?"*. Dette knytter den anonyme eller innloggede sesjonen mot en parti-`aktør` i Kunnskapsgrafen. + +### 5.2 Sidelinja Explorer (Offentlig Graf) +En egen SvelteKit-side der lekfolk og journalister kan analysere dataene. +* **Heatmaps:** Viser klynger ("skyer") av velgermasser basert på partipreferanse på to valgfrie akser. +* **Varder i landskapet:** Historiske figurer (Churchill, Stalin), nåværende politikere og Sidelinjas egne verter ligger inne som faste referansepunkter i grafen. AI-estimerte profiler gir pedagogisk (og underholdende) kontekst. + +## 6. AI & Asynkron Prosessering +Av kostnads- og ytelseshensyn skjer all AI-bruk asynkront i backend via jobbkøen og `ai-gateway` (LiteLLM). Ingen live AI-kall i klienten. + +* **AI-Kandidater (`valgomat_generate_profile`):** En bakgrunnsjobb analyserer partiprogrammer via `sidelinja/rutine` (Gemini) og genererer "syntetiske" referanseprofiler for listekandidater og historiske figurer. Kandidater kan senere logge inn (Authentik) og overstyre AI-ens svar manuelt. +* **Semantisk deduplisering (`valgomat_moderation`):** En asynkron jobb overvåker nye brukerskapte akser og spørsmål, slår sammen duplikater (f.eks. "Skattetrykk" og "Skattenivå"), og flagger emosjonelt ladede spørsmål for Sidelinja-redaksjonen i Redaksjonens chat. + +## 7. Arkitektur & Ansvarsfordeling + +| Komponent | Rolle / Ansvar | +|---|---| +| **SvelteKit (Klient)** | UX, Anonym UUID-håndtering i `localStorage`, kortstokk-swipe-grensesnitt, "lazy loading" av baksiden ved innlogging, visning av Heatmaps via Sidelinja Explorer. | +| **Authentik** | SSO for brukere, kandidater og redaksjon. Sammenfletting av anonym UUID med ekte bruker-ID. | +| **SpacetimeDB** | Sanntids match-kalkulering (Rust Reducers), swiping-logikk, PCA-beregning, og "Multiplayer"-rom ("Sofagruppa"). Holder kun aktive sesjoner i minnet. | +| **PostgreSQL** | Kunnskapsgrafen (`nodes`, `graph_edges`). Permanent lagring av spørsmål, akser, versjonshistorikk og aggregerte data for Sidelinja Explorer. | +| **Rust Worker (Sync)** | Synkroniserer batcher av svar fra SpacetimeDB over til PostgreSQL-lagringen via standard sync-mekanismen (se `synkronisering.md`). | +| **Rust Worker (AI)** | Kjører `valgomat_generate_profile` og `valgomat_moderation` via jobbkøen. | + +## 8. Dataklassifisering (ref. ARCHITECTURE.md 2.2) + +| Data | Kategori | Detaljer | +|---|---|---| +| Spørsmål, akser, versjonshistorikk | Kritisk (PG) | Brukergenerert innhold, krever backup | +| Individuelle svar (aggregert) | Kritisk (PG) | Synket fra SpacetimeDB | +| Aktive sesjoner, live PCA-state | Flyktig (SpacetimeDB) | Tåler tap — bruker svarer på nytt | +| AI-genererte kandidatprofiler | Avledet (PG) | Kan regenereres fra partiprogrammer | + +## 9. Instruks for Claude Code +1. **Datamodell:** Utvid enum `node_type` i PostgreSQL med `valgomat_question` og `valgomat_axis`. Bruk `graph_edges` med `relation_type = 'AFFECTS_AXIS'` og oppdater `confidence`-feltet for å håndtere crowdsourcet vekting av spørsmål opp mot ulike akser. +2. **SpacetimeDB Reducers:** Implementer innkommende events som `SubmitSwipe`, `SuggestAxis`, og match-algoritmen i Rust inne i SpacetimeDB. Pass på at reducere støtter anonym `session_id`. +3. **State Management:** SvelteKit skal ikke kreve innlogging for forsiden. Implementer Auth-guards slik at opprettelse av spørsmål, stemmegivning på andres forslag og visning av kommentarer gir `403 Forbidden` for uautoriserte og trigger Authentik-flyten. +4. **Explorer API:** Bygg aggregerte Materialized Views i PostgreSQL for Sidelinja Explorer for å unngå tung on-the-fly kalkulering av Heatmaps over millioner av rader. +5. **Jobbtyper:** Registrer `valgomat_generate_profile` og `valgomat_moderation` som jobbtyper i jobbkøen (se `jobbkø.md`). Bruk `sidelinja/rutine` som modellalias. +6. **Versjonering:** Spørsmål er append-only. Nye versjoner er nye rader med referanse til forgjengeren — aldri `UPDATE` på tekst som har mottatt svar. diff --git a/docs/erfaringer/README.md b/docs/erfaringer/README.md new file mode 100644 index 0000000..2381e8a --- /dev/null +++ b/docs/erfaringer/README.md @@ -0,0 +1,20 @@ +# Erfaringer — Ting vi lærte av å feile + +Denne mappen samler **praktiske lærdommer** fra implementering — ikke hva vi valgte, men hva vi lærte som ikke er åpenbart fra koden eller arkitekturdokumentene. + +Formålet er å treffe raskere blink med neste komponent. Hver fil dekker én teknologi eller ett mønster og inneholder konkrete feller, anti-patterns og løsninger vi landet på. + +## Innhold + +| Fil | Tema | +|---|---| +| `svelte5_reaktivitet.md` | Svelte 5 $state, SSR, reaktivitet gjennom funksjoner | +| `spacetimedb_integrasjon.md` | SDK-konvensjoner, TypeScript-bindings, BigInt, tilkobling | +| `adapter_moenster.md` | Adapter/factory for PG↔SpacetimeDB, hybrid-tilnærming | + +## Retningslinjer + +- **Kort og konkret.** Maks 1–2 sider per fil. Fellen først, forklaring etter. +- **Bare ting som ikke er åpenbare.** Ikke dokumenter at `npm install` installerer pakker. +- **Oppdater fremfor å legge til.** Hvis en erfaring utdypes, oppdater eksisterende fil. +- **Kodereferanser.** Vis til filer der mønsteret er implementert, så man kan lese koden. diff --git a/docs/erfaringer/adapter_moenster.md b/docs/erfaringer/adapter_moenster.md new file mode 100644 index 0000000..8936d60 --- /dev/null +++ b/docs/erfaringer/adapter_moenster.md @@ -0,0 +1,65 @@ +# Erfaring: Adapter-mønster for PG ↔ SpacetimeDB + +## 1. Mønsteret som fungerte + +Et felles interface (`ChatConnection`) med to implementasjoner: +- **PG-adapter** — polling hvert 3 sek, full-fetch, ingen ekstern avhengighet utover REST API +- **SpacetimeDB hybrid-adapter** — PG for historikk + SpacetimeDB WebSocket for sanntidspush + +Factory-funksjon velger adapter basert på miljøvariabel (`VITE_SPACETIMEDB_URL`). + +``` +ChatBlock.svelte → createChat() → PG-adapter (fallback) + → SpacetimeDB hybrid (hvis URL er satt) +``` + +**Fordeler:** +- Kan teste PG-adapter isolert uten Docker/SpacetimeDB +- Fallback er trivielt — fjern env-variabelen +- Komponenten vet ingenting om hvilken adapter som brukes + +**Referanse:** `web/src/lib/chat/` — hele mappen er organisert etter dette mønsteret. + +## 2. Anti-pattern: Lazy wrapper som bytter adapter + +Vi prøvde først en "lazy wrapper" som startet med PG-adapter og byttet til SpacetimeDB-adapter når tilkoblingen var klar. Problemet: + +- En plain `let activeConnection` i wrapperen er **ikke reaktiv** i Svelte 5 +- Når wrapperen byttet adapter, forsvant meldingene — ny adapter startet med tom liste +- Svelte-komponentene så aldri byttet fordi proxy-referansen ikke oppdaterte seg + +**Lærdom:** Ikke bytt adapter runtime. Velg én ved oppstart. Hybrid-adapteren løser problemet bedre — den bruker begge kilder samtidig i stedet for å bytte mellom dem. + +## 3. Hybrid fremfor ren SpacetimeDB + +Ren SpacetimeDB-tilnærming krever "oppvarming" — lasting av historikk fra PG inn i SpacetimeDB ved oppstart. Dette skaper: +- Kompleksitet i Rust-modulen (load_messages-reducer) +- Konfliktrisiko under oppvarming (§8.1 i synkronisering.md) +- Tom chat inntil oppvarming er ferdig + +**Hybrid-løsningen:** Frontend henter PG-historikk via REST (umiddelbart tilgjengelig) og lytter på SpacetimeDB kun for *nye* meldinger. Deduplisering skjer i klienten (sjekk mot eksisterende IDer). + +**Fordeler:** +- Ingen oppvarming nødvendig +- Historikk er alltid tilgjengelig, selv om SpacetimeDB er nede +- SpacetimeDB-modulen trenger bare håndtere nye meldinger, ikke historikk + +## 4. Graceful degradation — stille feilhåndtering + +SpacetimeDB-adaptere bør **aldri** vise feilmelding til brukeren ved tilkoblingsproblemer. PG-data er allerede lastet — brukeren har en fungerende chat. + +```typescript +.onConnectError((_ctx, err) => { + console.warn('[spacetime] connection error, PG-data beholdes:', err); + // Ingen error-state til UI — PG-data er intakt +}) +``` + +## 5. Anbefaling for neste komponent + +Når Kanban eller Whiteboard skal bygges med SpacetimeDB: + +1. **Start med PG-adapter.** Få hele flyten til å fungere med REST/polling først. +2. **Lag SpacetimeDB hybrid-adapter.** PG for historikk, SpacetimeDB for sanntid. +3. **Bruk samme factory-mønster.** Felles interface, env-variabel for valg. +4. **Test begge adaptere uavhengig** før du integrerer i UI-komponenten. diff --git a/docs/erfaringer/spacetimedb_integrasjon.md b/docs/erfaringer/spacetimedb_integrasjon.md new file mode 100644 index 0000000..f390251 --- /dev/null +++ b/docs/erfaringer/spacetimedb_integrasjon.md @@ -0,0 +1,78 @@ +# Erfaring: SpacetimeDB-integrasjon + +## 1. Genererte bindings — navnekonvensjoner + +SpacetimeDB sin `spacetime generate --lang typescript` produserer bindings med inkonsistente konvensjoner. Sjekk **alltid** de genererte filene i stedet for å gjette. + +| Hva | Konvensjon | Eksempel | +|---|---|---| +| Tabell-accessor på `conn.db` | snake_case | `conn.db.chat_message` | +| Reducer-accessor på `conn.reducers` | camelCase | `conn.reducers.sendMessage()` | +| Felt-navn i tabellrader | camelCase | `row.channelId`, `row.authorName` | +| Reducer-parametere | Enkelt objekt | `sendMessage({ id, channelId, body, ... })` | + +**Felle:** Man forventer at tabell-accessorer er camelCase (`chatMessage`), men de er snake_case. + +## 2. `DbConnection.builder()` — API-detaljer (SDK v2) + +```typescript +DbConnection.builder() + .withUri(spacetimeUrl) + .withDatabaseName(moduleName) // IKKE withModuleName + .withToken(token) + .onConnect((connection) => { // første param er DbConnection, ikke EventContext + connection.subscriptionBuilder() + .subscribe([`SELECT * FROM chat_message WHERE channel_id = '${id}'`]); + }) + .build(); +``` + +**Feller:** +- `withModuleName()` finnes ikke — bruk `withDatabaseName()` +- `onConnect`-callback mottar `DbConnection`, ikke `EventContext` +- `onConnectError`-callback har signatur `(ctx, errMessage)` der `errMessage` er en string + +## 3. BigInt-tidsstempler + +SpacetimeDB `Timestamp`-type bruker `microsSinceEpoch` som er `BigInt`. JavaScript kan ikke blande BigInt med Number: + +```typescript +// FEIL — TypeError: Cannot mix BigInt and other types +const ms = micros / 1000; + +// RIKTIG — sjekk type, bruk BigInt-divisjon +const ms = typeof micros === 'bigint' + ? Number(micros / 1000n) + : Number(micros) / 1000; +``` + +**Referanse:** `web/src/lib/chat/spacetime.svelte.ts` — `spacetimeRowToMessage()`. + +## 4. Rust-modul — borrow checker med SpacetimeDB-makroer + +SpacetimeDB-makroer genererer kode som tar eierskap over struct-felter. Bruk verdier **før** du flytter dem inn i structs: + +```rust +// FEIL — channel_id er moved inn i ChatMessage, kan ikke bruke i log!() etterpå +let msg = ChatMessage { channel_id, body, ... }; +log::info!("Melding i kanal {}", channel_id); // borrow after move + +// RIKTIG — bruk verdien før struct-opprettelse +let log_msg = format!("Melding i kanal {}", channel_id); +let msg = ChatMessage { channel_id, body, ... }; +log::info!("{}", log_msg); +``` + +## 5. Publisering og lokal testing + +```bash +# Publiser modul mot lokal SpacetimeDB (må kjøre i Docker først) +cd spacetimedb +spacetime publish sidelinja-realtime --server http://localhost:3000 + +# Generer TypeScript-bindings +spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindings \ + --project-path . +``` + +**Merk:** Docker-containeren (`clockworklabs/spacetime:latest`) må kjøre før publisering. Modulen overlever container-restart (data i volum `.docker-data/spacetimedb`). diff --git a/docs/erfaringer/svelte5_reaktivitet.md b/docs/erfaringer/svelte5_reaktivitet.md new file mode 100644 index 0000000..195acfc --- /dev/null +++ b/docs/erfaringer/svelte5_reaktivitet.md @@ -0,0 +1,73 @@ +# Erfaring: Svelte 5 Reaktivitet + +## 1. `$state` i `.svelte.ts` krever getters + +Svelte 5 sin `$state` lager reaktive proxyer. Når en funksjon returnerer et objekt med `$state`-verdier, **mister man reaktiviteten** hvis man returnerer verdien direkte: + +```typescript +// FEIL — mister reaktivitet, verdien fryses ved retur +function createThing() { + let count = $state(0); + return { count }; // snapshot, ikke reaktiv +} + +// RIKTIG — getter bevarer proxy-tilgang +function createThing() { + let count = $state(0); + return { + get count() { return count; } + }; +} +``` + +**Referanse:** `web/src/lib/chat/pg.svelte.ts` — alle returnerte verdier bruker getters. + +## 2. SSR kjører alt utenfor `onMount` + +SvelteKit server-renderer kjører komponent-script ved SSR. Alt som bruker browser-APIer (WebSocket, `fetch` til relative URLer, `setInterval`, `sessionStorage`) **krasjer på serveren** hvis det ikke er beskyttet. + +```typescript +// FEIL — krasjer ved SSR +let chat = createChat(channelId); // kjøres på server + +// RIKTIG — kun i browser +import { onMount } from 'svelte'; +let chat = $state(null); +onMount(() => { + chat = createChat(channelId); + return () => chat?.destroy(); +}); +``` + +Alternativt kan factory-funksjonen selv sjekke: +```typescript +import { browser } from '$app/environment'; +if (browser) { /* ... */ } +``` + +**Referanse:** `web/src/lib/chat/create.svelte.ts` — `browser`-guard i factory, `web/src/lib/blocks/ChatBlock.svelte` — `onMount` for opprettelse. + +## 3. `$derived` og `$effect` med null-initialisert state + +Når en `$state`-variabel starter som `null` (fordi den settes i `onMount`), må `$derived` og `$effect` håndtere null-tilfellet: + +```typescript +let chat = $state(null); +let messages = $derived(chat?.messages ?? []); // fallback til tom liste + +$effect(() => { + const count = messages.length; // trygt, alltid array + if (count > prevCount) scrollToBottom(); + prevCount = count; +}); +``` + +**Referanse:** `web/src/lib/blocks/ChatBlock.svelte` — `$derived` med optional chaining. + +## 4. Polling: full-fetch slår inkrementell akkumulering + +Vi prøvde først å bruke en `latestTimestamp`-cursor for å hente kun nye meldinger og appende dem. Dette ga duplikater — telleren vokste mens man tastet (polling i bakgrunnen). + +**Løsning:** Enkel `refresh()` som alltid henter full liste og erstatter `messages` i sin helhet. For et lite volum meldinger er dette enklere og tryggere enn inkrementell logikk. + +**Referanse:** `web/src/lib/chat/pg.svelte.ts` — `refresh()` gjør full fetch, ingen akkumulering. diff --git a/docs/features/ai_research_klipper.md b/docs/features/ai_research_klipper.md index 73619cf..c09ee6b 100644 --- a/docs/features/ai_research_klipper.md +++ b/docs/features/ai_research_klipper.md @@ -7,7 +7,7 @@ Et internt redaksjonelt verktøy for å samle inn research fra nettet. Programle ## 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). + * Backend mottar teksten og oppretter en `research_clip`-jobb i jobbkøen (se `docs/infra/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*. diff --git a/docs/features/chat.md b/docs/features/chat.md new file mode 100644 index 0000000..9668b43 --- /dev/null +++ b/docs/features/chat.md @@ -0,0 +1,139 @@ +# Feature: Chat (Channels & Meldinger) +**Filsti:** `docs/features/chat.md` + +## 1. Konsept +En universell, sanntids meldingskomponent bygget på SpacetimeDB. Chat er ikke bundet til én kontekst — den kan knyttes til enhver node i Kunnskapsgrafen via **channels**. Ulike konsepter bruker chat med ulik konfigurasjon, men all infrastruktur er delt. + +## 2. Channels-modellen +En **channel** er en meldingsstrøm knyttet til en vilkårlig node (parent) i grafen. Channelen er selv en node (`node_type = 'channel'`), noe som betyr at den deltar i grafen og arver workspace-isolasjon. + +``` +┌─────────────┐ parent_id ┌─────────────┐ +│ Channel │ ──────────────────► │ Vilkårlig │ +│ (node) │ │ node │ +└──────┬──────┘ └─────────────┘ + │ + │ channel_id + ▼ +┌─────────────┐ +│ Meldinger │ +│ (nodes) │ +└─────────────┘ +``` + +En node kan ha **flere channels**. Eksempler: +- Et Tema har "Diskusjon" (default) + "Research-dump" +- En Episode har "Redaksjonelt" (intern kommentartråd) +- Et Møte har "Scratchpad" (flyktig, TTL) +- En Aktør har "Notater" (valgfri) + +### 2.1 Channel-konfigurasjon +Hver channel har en `config` (JSONB) som styrer hvilke capabilities den støtter: + +```json +{ + "threads": true, // reply-to-tråder + "mentions": true, // #/@ parsing + automatiske graph_edges + "attachments": true, // filopplasting + "research_clips": true, // research_clip meldingstype (AI-prosessert) + "ttl_days": null // null = permanent, tall = auto-slett etter N dager +} +``` + +### 2.2 Standard channel-presets per konsept + +| Konsept | Channel-navn | Config | +|---|---|---| +| **Redaksjonen** (Tema) | "Diskusjon" | `threads: true, mentions: true, attachments: true, research_clips: true, ttl_days: null` | +| **Redaksjonen** (Tema) | "Research" (valgfri) | `threads: false, mentions: true, attachments: true, research_clips: true, ttl_days: null` | +| **Møterommet** | "Scratchpad" | `threads: false, mentions: false, attachments: false, research_clips: false, ttl_days: 90` | +| **Studioet** | "Studio-chat" | `threads: false, mentions: false, attachments: false, research_clips: false, ttl_days: 30` | +| **Episode** | "Redaksjonelt" | `threads: true, mentions: true, attachments: true, research_clips: false, ttl_days: null` | + +Presets er kun defaults — en workspace-admin kan justere config per channel. + +### 2.3 Automatisk opprettelse +Når en node opprettes som forventes å ha chat (Tema, Episode, Møte), oppretter systemet automatisk en default-channel. Dette skjer i SvelteKit server-side som en del av node-opprettelsestransaksjonen. + +## 3. Meldinger + +### 3.1 Datamodell (PostgreSQL) +Meldinger er noder i Kunnskapsgrafen (`node_type = 'melding'`): + +```sql +messages ( + id UUID PK → nodes(id), + channel_id UUID NOT NULL → channels(id), + reply_to UUID → messages(id), -- tråder (hvis config.threads = true) + author_id TEXT NOT NULL → users, + message_type message_type, -- 'text', 'research_clip', 'factoid', 'system' + body TEXT NOT NULL, + metadata JSONB, -- ekstra data (research-klipp AI-resultat, etc.) + edited_at TIMESTAMPTZ, + created_at TIMESTAMPTZ +) +``` + +### 3.2 SpacetimeDB (sanntid) +SpacetimeDB holder aktive channels' meldinger i minnet. Nye meldinger sendes via SpacetimeDB og kringkastes til alle tilkoblede klienter i samme workspace og channel. Synkes til PostgreSQL med ~5 sek forsinkelse (se `docs/infra/synkronisering.md`). + +## 4. Mentions & Autocomplete +Kun aktive når `config.mentions = true`. + +* **Trigger-tegn:** `#` (Temaer/Aktører fra Kunnskapsgrafen), `@` (brukere/redaksjonsmedlemmer), `/` (kommandoer). +* **Filtrering:** Svelte-klienten filtrerer den lokale SpacetimeDB-cachen umiddelbart. Skriver man `#Ha...` vises en klikkbar liste ("Hans Petter Sjøli", "Høyre"). +* **Grafkobling:** Ved `#`-mention opprettes automatisk `MENTIONS`-edges i `graph_edges` mellom meldingen og den nevnte noden. +* **Mobil-optimalisert:** Autocomplete-listen er tappbar og tilpasset mindre skjermer. + +## 5. Tråder +Kun aktive når `config.threads = true`. Meldinger kan ha en `reply_to`-referanse. Frontend viser tråder som innrykk eller ekspanderbare grupper. + +## 6. Vedlegg +Kun aktive når `config.attachments = true`. Meldinger kan ha vedlegg via `message_attachments` → `media_files`. Whiteboard-eksport kan knyttes som vedlegg. + +## 7. Versjonshistorikk +Alle meldinger støtter redigering med full historikk via `message_revisions`. Original tekst bevares alltid. + +## 8. Tale-til-tekst (Voice-to-text) +Mobilvennlig diktering for situasjoner der tastatur er upraktisk. Brukeren trykker en mikrofon-knapp, snakker, og får teksten tilbake som en vanlig melding klar til redigering og sending. + +### 8.1 Flyt +1. Bruker trykker og holder (eller toggler) mikrofon-knappen i chat-feltet. +2. Nettleseren fanger lyd via `MediaRecorder` API (WebM/Opus). +3. Lydklippet sendes til Whisper (`POST /v1/audio/transcriptions`, `response_format=text`, `language=no`) via SvelteKit server-side. +4. Transkripsjonen settes inn i meldingsfeltet — brukeren kan redigere før sending. +5. Ingen lagring av lydfilen — den kastes etter transkripsjon. + +### 8.2 Avgrensning +* Dette er **ikke** en lydmelding-feature (à la WhatsApp). Lyden er et transportmiddel for tekst. Kun teksten lagres. +* Whisper-kallet er kort (<30 sek tale) og kan rutes direkte til Whisper-serveren uten jobbkø. +* Bruk `small`-modellen for lav latens. Navnenøyaktighet er mindre viktig for korte chatmeldinger. + +## 9. TTL (automatisk opprydding) +Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk slettet av en nattlig jobbkø-jobb. Brukes for flyktige kontekster (scratchpads, studio-chat). + +## 10. Implementeringsstatus + +### Ferdig (mars 2025) +- **ChatBlock.svelte:** Refaktorert til adapter-mønster via `createChat()` factory +- **PG-adapter (`pg.svelte.ts`):** Polling hvert 3. sek, full-fetch (ingen akkumulering). Fungerer som selvstendig fallback. +- **SpacetimeDB hybrid-adapter (`spacetime.svelte.ts`):** Henter historikk fra PG via REST + lytter på SpacetimeDB WebSocket for sanntidspush. Dedupliserer mot PG-data. Faller tilbake til PG REST ved sending hvis SpacetimeDB er nede. +- **Factory (`create.svelte.ts`):** Velger adapter basert på `VITE_SPACETIMEDB_URL`. SSR-safe med `browser`-guard. +- **Shared types (`types.ts`):** `Message` og `ChatConnection` interface brukt av begge adaptere. +- **SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`):** `ChatMessage`-tabell, `SyncOutbox`, `send_message`-reducer. Publisert som `sidelinja-realtime`. +- **TypeScript-bindings:** Generert fra SpacetimeDB-modulen, camelCase-properties. +- **Autoscroll:** `$effect` som scroller ned ved nye meldinger. +- **Datogruppering:** Visuell datoseparator ("I dag", "I går", dato). + +### Gjenstår +- **Sync-worker (SpacetimeDB → PG):** Rust-worker som poller `sync_outbox` og persisterer til PostgreSQL. Se `docs/infra/synkronisering.md` §5.1. +- **Tråder, mentions, vedlegg, TTL** — avventer Kunnskapsgraf CRUD (Lag 2). +- **Autentisering:** `author_name`/`author_id` settes ikke automatisk ennå (avventer Authentik-integrasjon). + +## 11. Instruks for Claude Code +* **Opprettelsesrekkefølge:** Opprett `nodes`-rad → `channels`-rad → (for meldinger) `nodes`-rad → `messages`-rad. Alt i én transaksjon med riktig `workspace_id`. +* **Channel-opprettelse:** Når en Tema, Episode eller Møte opprettes, opprett alltid en default-channel i samme transaksjon. +* **Mentions-parsing:** Skjer i SvelteKit server-side ved mottak av meldingen. Parse `#`- og `@`-tags, valider mot noder i workspace-et, og opprett `graph_edges`. +* **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer (tråd-knapp, vedlegg-knapp, mention-autocomplete) basert på config. +* **SpacetimeDB er autoritativ** for aktive meldinger, PG for historikk. +* **Alt er workspace-scopet.** Channels arver workspace via `nodes.workspace_id`. diff --git a/docs/features/kalender.md b/docs/features/kalender.md new file mode 100644 index 0000000..937fc46 --- /dev/null +++ b/docs/features/kalender.md @@ -0,0 +1,134 @@ +# Kalender + +## Oversikt +Kalender er en workspace-blokk for redaksjonell planlegging og tidslinjevisning. Hendelser er nodes i kunnskapsgrafen og kan kobles til episoder, temaer, aktører og channels. Kalendere kan fritt abonnere på hverandre — på tvers av brukere og workspaces. + +## Konsept + +### Kalendere som grafnoder +Hver kalender er en node (`node_type: 'calendar'`). Hendelser er også nodes (`node_type: 'event'`) som tilhører en kalender via en `PART_OF`-relasjon. Dette gir: +- Full workspace-isolasjon via eksisterende RLS +- Relasjoner mellom hendelser og resten av kunnskapsgrafen (episoder, aktører, temaer) +- Channels knyttet til hendelser (diskusjonstråd per hendelse) + +### Typer kalendere + +| Type | Eier | Synlighet | Eksempel | +|------|------|-----------|----------| +| **Workspace** | Workspace (admin) | Alle medlemmer | "Sidelinja Produksjonskalender" | +| **Personlig** | Bruker | Kun eier (+ eksplisitt deling) | "Vegards arbeidskalender" | +| **Offentlig** | Workspace (admin) | Alle som abonnerer | "Sidelinja Publiseringsplan" | + +### Abonnementsmodell +Kalendere kan abonnere på andre kalendere via en `SUBSCRIBES_TO`-edge i grafen. Abonnenten ser hendelser read-only — bare kildekalenderen kan redigere. + +``` +Vegards kalender ──SUBSCRIBES_TO──▶ Sidelinja Produksjon +Vegards kalender ──SUBSCRIBES_TO──▶ Sidelinja Publisering +Ekstern partner ──SUBSCRIBES_TO──▶ Sidelinja Publisering (offentlig) +``` + +**Prinsipp:** Enhver kalender kan abonnere på enhver annen kalender den har tilgang til. Tilgang styres av: +- Samme workspace → alltid tilgang til workspace-kalendere +- Cross-workspace → kun til kalendere merket "offentlig" +- Personlige → kun eier + eksplisitt inviterte + +### Samlet visning +UI-et viser én samlet kalendervisning per bruker: alle hendelser fra egne + abonnerte kalendere, fargekodet per kilde. Brukeren kan toggle synlighet per kalender. + +## Datamodell + +### Nye node_types +Krever at `node_type` enum utvides med `'calendar'` og `'event'`. + +### events-tabell (detalj for event-nodes) +```sql +CREATE TABLE events ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + calendar_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + title TEXT NOT NULL, + description TEXT, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ, + all_day BOOLEAN NOT NULL DEFAULT false, + recurrence JSONB, -- iCal RRULE som JSON (valgfritt) + location TEXT, + color TEXT, -- hex fargekode for UI + created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL +); +``` + +### calendars-tabell (detalj for calendar-nodes) +```sql +CREATE TABLE calendars ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#3b82f6', + visibility TEXT NOT NULL DEFAULT 'workspace' + CHECK (visibility IN ('workspace', 'personal', 'public')), + owner_user TEXT REFERENCES users(authentik_id) ON DELETE SET NULL + -- owner_user er NULL for workspace-kalendere (eid av workspace via nodes.workspace_id) +); +``` + +### Abonnementer via graph_edges +```sql +-- Kalender A abonnerer på Kalender B +INSERT INTO graph_edges (workspace_id, source_id, target_id, relation_type) +VALUES (:ws, :kalender_a, :kalender_b, 'SUBSCRIBES_TO'); +``` + +Krever ny relasjonstype: +```sql +INSERT INTO relation_types (name, label, description, system) +VALUES ('SUBSCRIBES_TO', 'Abonnerer på', 'Kalender følger en annen kalender', true); +``` + +## ICS/CalDAV-eksport + +### Read-only ICS-feed +Hver kalender med `visibility = 'public'` (eller workspace-kalendere for innloggede) får en ICS-URL: +``` +https://sidelinja.org/cal/{workspace_slug}/{calendar_id}.ics +``` + +SvelteKit genererer ICS-feeden server-side. Brukere kan legge denne inn i Google Calendar, Apple Calendar, Thunderbird osv. + +### Ingen import +Sidelinja importerer ikke fra eksterne kalendere i Fase 1. Sidelinja er kilden til sannhet for sine egne hendelser. Eventuell CalDAV-synk (toveis) er en fremtidig utvidelse. + +## UI-komponenter + +### CalendarBlock (workspace-blokk) +Integreres i det komponerbare side-systemet som blokk-type `calendar`: +- Månedsvisning (default), ukevisning, listevisning +- Fargekodet per kalender-kilde +- Klikk hendelse → åpne detalj med grafkoblinger og channel +- Dra for å opprette ny hendelse (desktop) +- Toggle synlighet per abonnert kalender i sidebar-filter + +### Hendelsesdetalj +- Tittel, tid, beskrivelse, lokasjon +- Grafkoblinger: koble til episode, tema, aktør +- Channel: diskusjonstråd (opprettes on-demand) +- Deltakere: workspace-medlemmer + +## Redaksjonell verdi +Kalenderen er ikke generisk møtebooking — den er **redaksjonell planlegging**: +- **Publiseringsdatoer**: Når skal Episode 43 ut? Synlig for hele redaksjonen +- **Innspillingsslots**: Bookede tider i Studioet, koblet til episode-node +- **Gjeste-intervjuer**: Hendelse koblet til aktør-node, med forberedelseskanal +- **Research-deadlines**: Koblet til tema-node +- **Sesongoversikt**: Alle episoder i en sesong som hendelser på tidslinje + +## Bygger på +- Kunnskapsgraf (nodes, graph_edges) +- Workspace-modell og RLS +- Komponerbare sider (blokk-type `calendar`) +- Channels (diskusjon per hendelse) + +## Åpne spørsmål +- **Recurring events**: RRULE er komplekst. Fase 1 kan starte uten recurrence og legge det til som utvidelse. +- **Tidssoner**: Alle tider lagres som TIMESTAMPTZ (UTC i PG). Frontend viser i brukerens lokale tidssone. Eksplisitt tidssone per hendelse trengs bare ved reise/eksterne gjester. +- **Varsler/påminnelser**: Kan kobles til jobbkøen — en `reminder`-jobb som poster i hendelsens channel X minutter før start. Utsettes til Fase 2. +- **Drag-and-drop i kalender**: Flytte/resize hendelser. Nyttig men ikke kritisk for Fase 1 — enkel klikk-for-å-redigere holder. diff --git a/docs/features/kanban.md b/docs/features/kanban.md new file mode 100644 index 0000000..052d1c8 --- /dev/null +++ b/docs/features/kanban.md @@ -0,0 +1,22 @@ +# Feature: Kanban (Planlegging) +**Filsti:** `docs/features/kanban.md` + +## 1. Konsept +Et drag-and-drop Kanban-brett for planlegging. Primært brukt til episodeplanlegging i Redaksjonen, men også mottaker av AI-genererte action points fra Møterommet. + +## 2. Datamodell +* **Episode** er en container (node i Kunnskapsgrafen) som samler et utvalg Temaer. +* **Posisjon/rekkefølge** lagres i SpacetimeDB for sanntid, synkes til PostgreSQL. +* **Status** på et Tema innenfor en episode (f.eks. "Forslag", "Bekreftet", "Innspilt") håndteres som et felt i SpacetimeDB. + +## 3. Brukes av + +| Konsept | Bruk | +|---|---| +| Redaksjonen | Episodeplanlegging — dra Temaer inn i Kjøreplanen | +| Møterommet | AI-referenten foreslår nye kort basert på action points | + +## 4. Instruks for Claude Code +* Bruk native HTML5 Drag and Drop i SvelteKit, unngå tunge biblioteker. +* SpacetimeDB er autoritativ for posisjon/rekkefølge — frontend speiler SpacetimeDB-state. +* Alt er workspace-scopet. diff --git a/docs/features/kunnskaps_bridge.md b/docs/features/kunnskaps_bridge.md new file mode 100644 index 0000000..8d00305 --- /dev/null +++ b/docs/features/kunnskaps_bridge.md @@ -0,0 +1,78 @@ +# Feature: Kunnskaps-Bridge (Cross-Workspace Discovery) +**Filsti:** `docs/features/kunnskaps_bridge.md` + +## 1. Konsept +En valgfri, opt-in feature som lar brukere oppdage semantisk beslektet kunnskap på tvers av workspaces de har tilgang til. Bryter ikke workspace-isolasjonen — resultatet er en *peker* ("dette finnes i Podcast B"), ikke selve innholdet. Brukeren må ha tilgang til begge workspaces for å se treffet. + +## 2. Avgrensning: Hva dette IKKE er +- **Ikke datadeling.** Ingen data kopieres mellom workspaces. Bridge viser kun at en relevant node *finnes*. +- **Ikke automatisk.** Begge workspaces må ha Bridge eksplisitt aktivert av en admin. +- **Ikke synlig for gjester.** Kun for workspace-medlemmer med tilgang til begge sider. +- **Ikke et søk i andres data.** Du ser bare treff i workspaces du allerede er medlem av. + +## 3. Teknisk arkitektur + +### 3.1 Vector Embeddings (pgvector) +Krever `pgvector`-extension i PostgreSQL: + +```sql +CREATE EXTENSION IF NOT EXISTS vector; + +ALTER TABLE actors ADD COLUMN embedding vector(768); +ALTER TABLE topics ADD COLUMN embedding vector(768); +ALTER TABLE factoids ADD COLUMN embedding vector(768); + +CREATE INDEX idx_actors_embedding ON actors USING ivfflat (embedding vector_cosine_ops); +CREATE INDEX idx_topics_embedding ON topics USING ivfflat (embedding vector_cosine_ops); +CREATE INDEX idx_factoids_embedding ON factoids USING ivfflat (embedding vector_cosine_ops); +``` + +### 3.2 Embedding-generering (`generate_embeddings`) +En jobbkø-jobb som genererer embeddings for nye/endrede noder: +1. Rust-worker plukker opp jobben fra jobbkøen. +2. Bygger en tekst-representasjon av noden (navn, body, tilknyttede faktoider). +3. Sender til AI Gateway (`sidelinja/rutine`) for embedding-generering. +4. Lagrer vektoren i pgvector-kolonnen. +5. Re-genereres ved vesentlige endringer av noden. + +### 3.3 Cross-workspace søk +Når en bruker utforsker en node (f.eks. Tema "Skolepolitikk"): +1. SvelteKit server-side henter brukerens tilgjengelige workspaces fra `workspace_members`. +2. Kjører et similarity-søk med `<=>` (cosine distance) **som superuser** (bypasser RLS) — men filtrerer eksplisitt mot brukerens workspace-liste: + ```sql + SELECT n.workspace_id, t.name, t.embedding <=> $target_embedding AS distance + FROM topics t + JOIN nodes n ON t.id = n.id + WHERE n.workspace_id = ANY($user_workspace_ids) + AND n.workspace_id != $current_workspace_id + AND t.embedding <=> $target_embedding < 0.3 + ORDER BY distance + LIMIT 10; + ``` +3. Resultatet vises som en diskret "Finnes også i..."-seksjon i UI-et. + +### 3.4 Workspace-config +Bridge aktiveres per workspace i `workspaces.settings`: +```json +{ + "bridge_enabled": true, + "bridge_discoverable": true +} +``` +- `bridge_enabled` — workspace kan søke i andre workspaces +- `bridge_discoverable` — andre workspaces kan finne noder i dette workspace-et + +Begge må være `true` for at en kobling skal vises. + +## 4. Dataklassifisering + +| Data | Kategori | Detaljer | +|---|---|---| +| Embedding-vektorer | Avledet (PG) | Kan regenereres fra nodeinnhold | + +## 5. Instruks for Claude Code +* pgvector er en ny avhengighet — dokumenter i docker-compose og setup-guides. +* Cross-workspace søk er det **eneste** stedet i systemet der bruker-initierte handlinger bypasser RLS. Isolér denne koden grundig — egen funksjon med eksplisitt workspace-filtrering, aldri gjenbruk i andre kontekster. +* Embedding-dimensjon (768) bør matche modellen som brukes. Konfigurér som konstant, ikke hardkod overalt. +* Jobbtype `generate_embeddings` bruker `sidelinja/rutine` som modellalias. +* Bridge er **Lag 4+** — krever fylt kunnskapsgraf i minst to workspaces. diff --git a/docs/features/kunnskapsgraf_og_relasjoner.md b/docs/features/kunnskapsgraf_og_relasjoner.md index 278de7d..91aa77a 100644 --- a/docs/features/kunnskapsgraf_og_relasjoner.md +++ b/docs/features/kunnskapsgraf_og_relasjoner.md @@ -62,24 +62,34 @@ CREATE INDEX idx_segments_transcript_fts ON segments USING GIN (to_tsvector('nor ``` ### 3.3 Kantene: `graph_edges` -All kobling skjer i én sentral tabell med ekte FK-integritet: +All kobling skjer i én sentral tabell med ekte FK-integritet. `workspace_id` er denormalisert fra `nodes` for å muliggjøre RLS direkte på edges: ```sql CREATE TABLE graph_edges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, 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, + confidence REAL CHECK (confidence BETWEEN 0.0 AND 1.0), + created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, + origin TEXT NOT NULL DEFAULT 'system', -- 'system', 'user', 'ai' created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - CONSTRAINT no_self_reference CHECK (source_id != target_id) + CONSTRAINT no_self_reference CHECK (source_id != target_id), + CONSTRAINT unique_edge UNIQUE (source_id, target_id, relation_type) ); 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); +CREATE INDEX idx_edges_workspace ON graph_edges(workspace_id); ``` +**Merk:** `UNIQUE(source_id, target_id, relation_type)` forhindrer duplikate relasjoner. Bruk `ON CONFLICT DO NOTHING` eller `ON CONFLICT DO UPDATE SET confidence = ...` ved upsert. + +**Merk:** `node_type`-enumet kan utvides av feature-spesifikke migrasjoner (f.eks. `valgomat_question`, `valgomat_axis`). Se `docs/concepts/valgomaten.md` for eksempel. + ## 4. Segmenter og Transkripsjoner ### 4.1 Segment som grafnode @@ -93,7 +103,7 @@ Episode 42 (node: episode) └── 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`). +Kapitler i RSS-feeden genereres fra segmentene, men er et eget konsern (se `docs/concepts/podcastfabrikken.md`). ### 4.2 Transkripsjoner: Git som master, PG som søkeindeks Transkripsjoner lever i **to steder** med klart eierskap: @@ -127,8 +137,10 @@ Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten: 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 +* **Workspace-isolasjon:** Alle noder tilhører en workspace. Opprett alltid noder med riktig `workspace_id`. Spørringer mot detailtabeller (actors, topics, etc.) filtrerer alltid via JOIN med nodes. * **`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. +* **`graph_edges` krever `workspace_id`.** Ved opprettelse av edges, sett `workspace_id` fra kilde-noden. `UNIQUE(source_id, target_id, relation_type)` hindrer duplikater — bruk `ON CONFLICT` ved upsert. * **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. +* **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. **Merk:** Hvis manuelle korrigeringer endrer segment-grenser (start_time), endres UUID-en. Løsning: automatisk flytt eksisterende edges til nærmeste nye segment basert på tidsintervall-overlapp. * **Full-text search:** Bruk `to_tsvector('norwegian', transcript)` for norsk språkstøtte i søk. diff --git a/docs/features/live_ai.md b/docs/features/live_ai.md new file mode 100644 index 0000000..c3b8bce --- /dev/null +++ b/docs/features/live_ai.md @@ -0,0 +1,66 @@ +# Feature: Live AI (Faktoid-oppslag & Referent) +**Filsti:** `docs/features/live_ai.md` + +## 1. Konsept +AI-drevet analyse av sanntids transkripsjon. Opererer i to moduser med samme underliggende pipeline (Whisper → NER → handling), men ulik output. + +## 2. Studio-modus: Faktoid-oppslag +Brukes i Studioet (se `docs/concepts/studioet.md`). En "virtuell co-host" som dytter relevante faktoider til programlederne i sanntid. + +### 2.1 Dataflyt +1. Live transkripsjon (se `docs/features/live_transkripsjon.md`) leverer tekst-chunks. +2. Rust-tjenesten analyserer for egennavn (Named Entity Recognition). +3. Lynraskt oppslag i PostgreSQL: `SELECT * FROM factoids JOIN actors... WHERE actor.name = $1`. +4. Treff dytter `LiveFactoidEvent` inn i SpacetimeDB. +5. SvelteKit-studio viser faktoiden lydløst i en egen boks. + +### 2.2 Lagring +Live-transkripsjonsloggen er flyktig (TTL 30 dager): + +```sql +live_transcription_log ( + id SERIAL, + session_id UUID, + chunk_timestamp TIMESTAMPTZ, + chunk_text TEXT, + matched_entities TEXT[], + pushed_factoids UUID[], + created_at TIMESTAMPTZ DEFAULT now() +) +``` + +En nattlig jobbkø-jobb sletter rader eldre enn TTL. + +## 3. Møte-modus: AI-Referent +Brukes i Møterommet (se `docs/concepts/møterommet.md`). Genererer referat og action points. + +### 3.1 Dataflyt +1. **Under møtet:** Whisper transkriberer i chunks. Aha-markører fra deltakerne lagres med tidsstempler. +2. **Ved møteslutt:** En `meeting_summarize`-jobb opprettes i jobbkøen (se `docs/infra/jobbkø.md`). +3. Rust-worker sender transkripsjonen + Aha-markører til AI Gateway (`sidelinja/rutine`) med instruksjon om å generere: + * Referat (strukturert Markdown) + * Action points (foreslåtte Kanban-kort) + * Identifiserte `#Temaer` og `@Aktører` +4. Referatet lagres som melding i relevant Tema-chat. Foreslåtte Kanban-kort vises for godkjenning. +5. `graph_edges` opprettes automatisk mellom møtesegmenter og identifiserte Temaer/Aktører. + +### 3.2 Off-the-record +Når off-the-record er aktivt, stopper Whisper-strømmen og ingen data lagres. Visuell indikator i alle deltakeres grensesnitt. Transkripsjonen får et gap i tidslinja. + +## 4. Kill Switch +Studio-modus har en **kill switch** — en synlig "Stopp AI"-knapp i studio-grensesnittet som umiddelbart: +1. Stopper NER-analyse av nye chunks +2. Skjuler faktoid-boksen +3. Logger tidspunkt og grunn (manuelt felt, valgfritt) + +Nødvendig fordi AI-en kan dytte feil eller irrelevante faktoider under live innspilling. Programlederen må kunne slå den av uten å forlate studio-viewet. + +Kill switch-status (`ai_enabled: bool`) lagres på LiveKit-rommet i SpacetimeDB og synkes til alle klienter i rommet. + +## 5. Instruks for Claude Code +* Begge moduser deler samme Whisper-pipeline — ikke dupliser transkripsjonskode. +* Studio-modus krever lav latens — hold NER-oppslaget raskt (indekserte spørringer). +* Møte-modus er asynkron — prosessér via jobbkøen etter møteslutt. +* All AI-kode peker på `http://ai-gateway:4000/v1`, aldri direkte til leverandør. +* Kill switch skal alltid være tilgjengelig i studio-modus — default: AI aktivert. +* Alt er workspace-scopet. diff --git a/docs/features/live_ai_assistent.md b/docs/features/live_ai_assistent.md deleted file mode 100644 index 5e147d2..0000000 --- a/docs/features/live_ai_assistent.md +++ /dev/null @@ -1,38 +0,0 @@ -# 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-server (`Systran/faster-whisper-small`) i chunks på ~5 sekunder. `small` er valgt for latency — den prosesserer ~5x raskere enn sanntid, noe som gir <1s forsinkelse per chunk. Ingen `initial_prompt` — hastighet prioriteres over navnenøyaktighet. -3. **Entity Extraction & Oppslag (Rust + PostgreSQL):** Rust-tjenesten 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-tjenesten 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. - -### 2.1 Lagringsstrategi -Live-transkripsjonen er **flyktig arbeidsdata** — ikke en del av det permanente datasettet. Den lagres i PostgreSQL som en feilsøkingslogg med TTL (standard 30 dager, konfigurerbart): - -```sql -live_transcription_log ( - id SERIAL, - session_id UUID, -- knytter chunks til en innspillingssesjon - chunk_timestamp TIMESTAMPTZ, - chunk_text TEXT, - matched_entities TEXT[], -- hvilke entiteter NER fant - pushed_factoids UUID[], -- hvilke faktoider ble pushet til frontend - created_at TIMESTAMPTZ DEFAULT now() -) -``` - -En nattlig jobbkø-jobb sletter rader eldre enn TTL. Dette gir mulighet til å feilsøke kjeden chunk → entity match → factoid push ("hvorfor dukket den faktoiden opp?") uten å akkumulere data over tid. - -Den endelige, kvalitetssikrede transkripsjonen av hele episoden lages etterpå via `medium` + `initial_prompt` gjennom Podcastfabrikkens pipeline (se `podcastfabrikken.md`). - -## 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. \ No newline at end of file diff --git a/docs/features/live_transkripsjon.md b/docs/features/live_transkripsjon.md new file mode 100644 index 0000000..ca5c8f0 --- /dev/null +++ b/docs/features/live_transkripsjon.md @@ -0,0 +1,40 @@ +# Feature: Live Transkripsjon (Whisper-pipeline) +**Filsti:** `docs/features/live_transkripsjon.md` + +## 1. Konsept +Den felles Whisper-pipelinen som brukes av flere konsepter for å transkribere lyd. Abstraherer bort konfigurasjonsforskjeller (modellvalg, latenskrav) bak et felles grensesnitt. + +## 2. Moduser + +| Kontekst | Modell | Latenskrav | initial_prompt | Output | +|---|---|---|---|---| +| **Studioet** (live) | `small` | <1s per chunk | Nei (hastighet prioritert) | Flyktig tekst → NER-pipeline | +| **Møterommet** (live) | `small` | <1s per chunk | Nei | Flyktig tekst → AI-referent | +| **Podcastfabrikken** (batch) | `medium` + prompt | Ingen (asynkront) | Ja (navneliste) | SRT → Git → PG | + +## 3. Teknisk arkitektur +1. **Lydkilde:** LiveKit server-side hooks (live) eller filopplasting (batch). +2. **Whisper-server:** `fedirz/faster-whisper-server` (Docker, OpenAI-kompatibelt API). Endepunkt: `POST /v1/audio/transcriptions`. +3. **Chunking (live):** Rust-tjeneste mater lyd i ~5-sekunders chunks. `small`-modellen prosesserer ~5x raskere enn sanntid, noe som gir <1s forsinkelse per chunk. +4. **Output:** SRT (batch) eller ren tekst (live). + +## 4. Whisper-konfigurasjon +* **Språk:** Sett `language=no` eksplisitt for norsk — unngå auto-detect som kan velge dansk/svensk. +* **`large-v3`** KREVER `vad_filter=true` — uten hallusinerer modellen repeterende tekst. +* **Benchmark og modellvalg:** Se `docs/concepts/podcastfabrikken.md` seksjon 4 for ytelsestall og anbefalinger. + +### 4.1 initial_prompt (navneliste) +Brukes kun i batch-modus (Podcastfabrikken). Prompten bygges automatisk fra workspace-config (`workspaces.settings.whisper_prompt`) + aktører i workspace-ets kunnskapsgraf. + +Effekten er tydelig: +* Uten prompt: "Vegard Nøgnes", "SideLinja", "Sidlinja" +* Med prompt: "Vegard Nøtnæs", "Sidelinja" (riktig) + +## 5. Lagring +* **Live-modus:** Transkripsjonen er flyktig (kategori 4, TTL 30 dager). Lagres i `live_transcription_log` for feilsøking. +* **Batch-modus:** SRT committes til Git (Forgejo, ett repo per workspace). Avledede formater (ren tekst, segmenter, søkeindeks) i PostgreSQL. + +## 6. Instruks for Claude Code +* Live transkripsjon blokkerer ALDRI web-requests — prosesseres i Rust-worker eller separat tjeneste. +* Batch-transkripsjon kjøres som `whisper_transcribe`-jobb i jobbkøen (se `docs/infra/jobbkø.md`). +* Workspace-spesifikk config (prompts) hentes fra `workspaces.settings`. diff --git a/docs/features/lydmeldinger.md b/docs/features/lydmeldinger.md new file mode 100644 index 0000000..0664b0e --- /dev/null +++ b/docs/features/lydmeldinger.md @@ -0,0 +1,137 @@ +# Feature: Lydmeldinger & Diktering +**Filsti:** `docs/features/lydmeldinger.md` + +## 1. Konsept +En mobil-first talefeature med tre moduser som dekker spekteret fra kjappe chatmeldinger til langt feltopptak. Grunnprinsippet er det samme: **du snakker inn i telefonen, og systemet gjør noe nyttig med det**. Forskjellen er hva som er output. + +| Modus | Typisk lengde | Output | Master-format | Lagring | +|---|---|---|---|---| +| **Voice-to-text** | <30 sek | Rå transkripsjon i meldingsfeltet | Tekst | Ingen (lyden kastes) | +| **Lydmelding** | <10 min | Lydfil + transkripsjon for søk | Lyd | `media_file` + node i grafen | +| **Diktering** | 1–30 min | AI-ryddet tekst (strukturert notat) | Tekst | Node i grafen | + +Voice-to-text er en del av chat-featuren (se `docs/features/chat.md`, seksjon 8). Denne specen dekker de to tyngre modusene. + +## 2. Lydmelding-modus + +### 2.1 Brukerflyt +1. Bruker åpner lydmelding-modus (via FAB-knapp, hurtigtast eller fra en channel). +2. Trykker record. Nettleseren fanger lyd via `MediaRecorder` API (WebM/Opus). +3. Kan valgfritt velge kontekst: et Tema, en Aktør, eller "Usortert". +4. Ved stopp lastes lydfilen opp til SvelteKit (streaming for store filer). +5. Filen lagres som `media_file` i workspace-mappen (`/srv/sidelinja/media/{workspace_slug}/voice/`). +6. En `whisper_transcribe`-jobb opprettes i jobbkøen for å gjøre opptaket søkbart. +7. Transkripsjonen lagres som metadata på noden — lydfilen forblir master. + +### 2.2 Datamodell +Lydmeldingen er en node i Kunnskapsgrafen (`node_type = 'melding'`, `message_type = 'voice_memo'`): + +``` +nodes (workspace_id, node_type = 'melding') + → messages (channel_id, message_type = 'voice_memo', body = transkripsjon, metadata = { duration, transcription_status }) + → message_attachments → media_files (lydfilen) +``` + +Lydmeldinger kan: +- Sendes i en channel (som vedlegg til en melding) +- Leve fristående i en personlig "Innboks"-channel +- Knyttes til Temaer/Aktører via `graph_edges` (manuelt eller AI-foreslått) + +### 2.3 Triagering +Redaksjonen trenger en flate for å gå gjennom nye lydmeldinger: +- Lytt, tagg med Temaer/Aktører +- Marker for bruk i episode (kobles til Kanban/Kjøreplan) +- Forkast / arkiver +- Denne flaten er en filtrert visning av "Innboks"-channelen, ikke et eget system + +### 2.4 Bruk i podcast +Lydmeldinger markert for bruk kan trekkes inn i Podcastfabrikkens pipeline. Lydfilen er allerede i media-mappen — den kan klippes inn i en episode direkte. + +## 3. Dikteringsmodus + +### 3.1 Brukerflyt +1. Bruker åpner dikteringsmodus (mobil-first, men fungerer på desktop). +2. Snakker fritt — kan være flere minutter. Visuell indikator viser at opptak pågår. +3. Ved stopp opprettes to jobber i jobbkøen (sekvensielt): + - `whisper_transcribe` — rå transkripsjon (`medium` + `initial_prompt` for kvalitet) + - `dictation_cleanup` — AI rydder i teksten +4. Brukeren ser resultatet: rå transkripsjon og AI-ryddet versjon side om side. +5. Kan redigere den ryddede versjonen før lagring. +6. Lagres som et notat (node i grafen). Lydfilen slettes etter vellykket transkripsjon. + +### 3.2 AI-opprydding (`dictation_cleanup`) +AI Gateway (`sidelinja/rutine`) prosesserer transkripsjonen med instruksjon om å: +- Fjerne fyllord, gjentakelser og ufullstendige setninger +- Strukturere i avsnitt med overskrifter der det gir mening +- Bevare talerens mening og tone — ikke omskrive til "AI-språk" +- Foreslå `#Tema`- og `@Aktør`-tags basert på innholdet + +### 3.3 Datamodell +Dikterte notater er meldinger i en channel: + +``` +nodes (workspace_id, node_type = 'melding') + → messages (channel_id, message_type = 'text', body = ryddet tekst, metadata = { raw_transcript, source: 'dictation' }) +``` + +Metadata bevarer rå-transkripsjonen slik at brukeren alltid kan se hva som faktisk ble sagt. + +### 3.4 Målkanal +Brukeren velger hvor notatet havner: +- **Personlig notat-channel** — default, kun synlig for brukeren selv +- **Tema-channel** — delt med redaksjonen som et innspill +- **Direkte til en annen bruker** — som en asynkron "talemelding i tekstform" + +## 4. Felles infrastruktur + +### 4.1 Opptak (klient) +Begge moduser bruker samme opptakskomponent i SvelteKit: +- `MediaRecorder` API med WebM/Opus +- Visuell feedback (lydnivå-indikator, varighet) +- Modus-velger: Lydmelding / Diktering +- Kontekst-velger: Tema, Aktør, eller Usortert + +### 4.2 Opplasting +SvelteKit håndterer filopplasting strømmende. For korte klipp (<2 min) kan opplasting starte umiddelbart etter stopp. For lengre opptak bør det vises en fremdriftsindikator. + +### 4.3 Whisper +Begge moduser bruker live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`) i batch-modus via jobbkøen. Modellvalg: +- Lydmelding: `small` (rask, transkripsjonen er sekundær — lyden er master) +- Diktering: `medium` + `initial_prompt` (kvalitet prioritert — teksten er master) + +### 4.4 Personlig "Innboks"-channel +Ved workspace-opprettelse opprettes en privat channel per bruker for usorterte lydmeldinger og notater. Config: + +```json +{ + "threads": false, + "mentions": false, + "attachments": true, + "research_clips": false, + "ttl_days": null +} +``` + +## 5. Dataklassifisering (ref. ARCHITECTURE.md 2.2) + +| Data | Kategori | Detaljer | +|---|---|---| +| Lydfiler (voice memos) | Kritisk (backup) | Unikt råmateriale — lyden *er* innholdet, kan brukes direkte i podcasten | +| Diktert tekst (ryddet) | Kritisk (PG) | Brukerens innhold | +| Rå transkripsjon | Avledet | Kan regenereres fra lydfil | +| Lydfil fra diktering | Flyktig (TTL) | Lyden er kun et *transportformat* for teksten — slettes etter at brukeren har godkjent den ryddede teksten. Spart diskplass, og unngår å lagre halvformulerte tanker som lyd. | + +## 6. Jobbtyper + +| Jobbtype | Modellalias | Beskrivelse | +|---|---|---| +| `whisper_transcribe` | — | Eksisterende jobbtype, gjenbrukes | +| `dictation_cleanup` | `sidelinja/rutine` | AI-opprydding av transkripsjon | + +## 7. Instruks for Claude Code +* Opptakskomponenten skal være en gjenbrukbar Svelte-komponent med modus-prop (`voice_memo` / `dictation`). +* Lydfiler lagres i `media/{workspace_slug}/voice/` — aldri i databasen. +* Diktering: slett lydfilen først *etter* at brukeren har godkjent den ryddede teksten. +* `voice_memo` er en ny `message_type` — utvid enum i migrasjonen. +* Personlig innboks-channel opprettes automatisk ved workspace-medlemskap (som del av `workspace_members`-inserten). +* Alt er workspace-scopet. diff --git a/docs/features/podcast_statistikk.md b/docs/features/podcast_statistikk.md index 54a3cd1..b3823a4 100644 --- a/docs/features/podcast_statistikk.md +++ b/docs/features/podcast_statistikk.md @@ -7,13 +7,24 @@ IAB-kompatibel lytterstatistikk bygget fra bunnen av. Vi fanger all rådata via ## 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. +3. **Rust Batch Processor (Jobbkø):** Statistikkparseren kjøres som en `stats_parse`-jobb i den felles jobbkøen (se `docs/infra/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`). +4. **Lagring (PostgreSQL):** Rust-programmet skriver det aggregerte resultatet inn i PostgreSQL (`episode_stats` tabell med felter for `workspace_id`, `date`, `episode_id`, `client_name`, `unique_downloads`). Workspace-tilhørighet utledes fra filstien i loggen (`/media/{workspace_slug}/...`). -## 3. Instruks for Claude Code +## 3. Personvern og GDPR + +Caddy access-logger inneholder IP-adresser og User-Agent — dette er personopplysninger under GDPR. + +**Tiltak:** +* **IP-anonymisering:** Rust-workeren hasher IP-adresser (SHA-256 med daglig roterende salt) *før* de lagres i `episode_stats`. Rå IP-er lagres aldri i PostgreSQL. +* **Loggretensjon:** Caddy-logger roteres og slettes etter 90 dager (kategori 4, flyktig). Etter at statistikk-workeren har prosessert en loggfil, inneholder PG kun aggregerte tall — ingen identifiserbar data. +* **Ingen sporing på tvers:** Vi bruker ikke cookies, fingerprinting eller tredjepartstracking. Deduplisering basert på IP + User-Agent i 24-timers vindu er IAB-standard og minimumsdata. +* **Dokumentasjon:** Podcastens RSS-feed bør lenke til en personvernerklæring som forklarer at anonymisert nedlastingsstatistikk samles inn. + +## 4. 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. \ No newline at end of file +* Rålogger skal ALDRI lagres i PostgreSQL. +* Statistikk er workspace-scopet. `episode_stats` merkes med `workspace_id`. Admin-visningen filtrerer per workspace. \ No newline at end of file diff --git a/docs/features/produktivitetssuite.md b/docs/features/produktivitetssuite.md deleted file mode 100644 index e70c4a7..0000000 --- a/docs/features/produktivitetssuite.md +++ /dev/null @@ -1,22 +0,0 @@ -# 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. \ No newline at end of file diff --git a/docs/features/prompt_lab.md b/docs/features/prompt_lab.md new file mode 100644 index 0000000..213ef4e --- /dev/null +++ b/docs/features/prompt_lab.md @@ -0,0 +1,101 @@ +# Feature: Prompt-Laboratorium +**Filsti:** `docs/features/prompt_lab.md` + +## 1. Konsept +Et internt kvalitetssikringsverktøy der redaksjonen kan teste, sammenligne og godkjenne LLM-prompts mot faktiske data fra eget workspace — før de ruller ut i produksjon. Integrert med AI Gateway (LiteLLM) og Promptfoo-testsettene. + +## 2. Problemet det løser +- Modellkvalitet på norsk varierer kraftig mellom leverandører og versjoner. +- Leverandører oppdaterer modeller uten varsel — kvaliteten kan degraderes over natten. +- Redaksjonen må kunne verifisere at en prompt fungerer *med deres data* (transkripsjoner, artikler, aktørnavn) før den settes i produksjon. +- I dag krever prompt-testing kommandolinje og Promptfoo — det bør være tilgjengelig i nettleseren. + +## 3. Brukeropplevelse + +### 3.1 Playground (Ad-hoc testing) +1. Bruker velger en **jobbtype** (research_clip, whisper_postprocess, metadata_extract, dictation_cleanup, etc.). +2. Systemet laster inn gjeldende system-prompt fra `workspaces.settings`. +3. Bruker kan redigere prompten i et tekstfelt. +4. Velger **testdata**: enten paste inn tekst manuelt, eller velg fra faktiske transkripsjoner/artikler i workspace-et. +5. Velger **modeller** å teste mot (f.eks. `sidelinja/rutine` + `sidelinja/resonering`). +6. Kjører testen — ser resultatene side-om-side. +7. Kan iterere: endre prompten, kjør igjen, sammenlign. + +### 3.2 Batch-evaluering (Promptfoo-integrasjon) +1. Bruker velger et lagret **testsett** (fra `tests/prompts/` i Git). +2. Kjører testsettene mot valgte modeller via AI Gateway. +3. Ser en matrise: testcase × modell × resultat, med pass/fail-markering. +4. Kan sammenligne mot tidligere kjøringer for å oppdage regresjoner. + +### 3.3 Deploy +Når en prompt er verifisert: +1. Bruker klikker "Deploy til workspace". +2. Prompten skrives til `workspaces.settings.llm_prompts[jobbtype]`. +3. Alle fremtidige jobber av den typen bruker den nye prompten. +4. Tidligere prompt lagres i en `prompt_history`-logg for rollback. + +## 4. Teknisk arkitektur + +### 4.1 Datamodell + +```sql +prompt_test_runs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + job_type TEXT NOT NULL, + system_prompt TEXT NOT NULL, + model_alias TEXT NOT NULL, + input_text TEXT NOT NULL, + output_text TEXT, + tokens_used INTEGER, + latency_ms INTEGER, + created_by TEXT NOT NULL REFERENCES users(authentik_id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_prompt_runs_workspace ON prompt_test_runs(workspace_id, job_type, created_at DESC); +``` + +`prompt_test_runs` er **ikke** en node i grafen — det er en intern verktøytabell for utviklere/redaktører. + +### 4.2 Prompt-historikk + +```sql +prompt_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + job_type TEXT NOT NULL, + system_prompt TEXT NOT NULL, + deployed_by TEXT NOT NULL REFERENCES users(authentik_id), + deployed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +Muliggjør rollback: "forrige prompt for research_clip fungerte bedre, rull tilbake." + +### 4.3 Kjøring +- **Ad-hoc tester:** SvelteKit server-side sender request direkte til AI Gateway (`http://ai-gateway:4000/v1`) med brukerens prompt og testdata. Resultatet returneres synkront. +- **Batch-evaluering:** Oppretter en `prompt_eval`-jobb i jobbkøen. Rust-worker kjører testsettene mot AI Gateway og lagrer resultatene. + +## 5. Dataklassifisering + +| Data | Kategori | Detaljer | +|---|---|---| +| Test-kjøringer | Flyktig (TTL 90 dager) | For analyse, ryddes automatisk | +| Prompt-historikk | Kritisk (PG) | Muliggjør rollback | +| Testsett (Promptfoo) | Gjenskapbar (Git) | `tests/prompts/` — versjonskontrollert | + +## 6. Jobbtyper + +| Jobbtype | Modellalias | Beskrivelse | +|---|---|---| +| `prompt_eval` | (varierer) | Batch-evaluering av testsett mot valgte modeller | + +## 7. Instruks for Claude Code +* Prompt Lab er et admin-verktøy — krev `admin` eller `owner`-rolle i workspace. +* Ad-hoc tester kjøres synkront (SvelteKit → AI Gateway → respons). Ikke bruk jobbkø for enkelt-tester. +* Batch-evaluering kjøres via jobbkø for å unngå timeouts. +* Vis alltid token-bruk og latens — dette er et kostnadsbevisst verktøy. +* Testdata fra workspace-et (transkripsjoner, artikler) lastes via SvelteKit server-side fra PG. Gjesten-UX vises aldri her. +* `prompt_test_runs` og `prompt_history` er workspace-scopet men trenger ikke RLS — de er kun tilgjengelige for admins via applikasjonslogikk. +* Alt er workspace-scopet. diff --git a/docs/features/synkronisering.md b/docs/features/synkronisering.md deleted file mode 100644 index 3be57b3..0000000 --- a/docs/features/synkronisering.md +++ /dev/null @@ -1,63 +0,0 @@ -# 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 diff --git a/docs/features/valgomat.md b/docs/features/valgomat.md deleted file mode 100644 index 0c44b19..0000000 --- a/docs/features/valgomat.md +++ /dev/null @@ -1,16 +0,0 @@ -# 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). \ No newline at end of file diff --git a/docs/features/visuell_graf.md b/docs/features/visuell_graf.md new file mode 100644 index 0000000..53671f8 --- /dev/null +++ b/docs/features/visuell_graf.md @@ -0,0 +1,20 @@ +# Feature: Visuell Kunnskapsgraf (Graph View) +**Filsti:** `docs/features/visuell_graf.md` + +## 1. Konsept +En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt navigerbar og redigerbar. Brukes i Kunnskapsgrafen-konseptet (se `docs/concepts/kunnskapsgrafen.md`). + +## 2. Funksjonalitet +* **Visualisering:** Viser noder (Temaer, Aktører, Faktoider, Segmenter) og relasjoner som et interaktivt nettverkskart. Bygges med D3.js eller Vis.js i Svelte-frontenden. +* **Visuell redigering:** Redaksjonen kan dra streker mellom noder for å opprette nye relasjoner i `graph_edges`-tabellen. Velg relasjonstype (`PART_OF`, `CONTRADICTS`, `MENTIONS`, etc.) via en kontekstmeny. +* **Hierarkier uten mapper:** `PART_OF`-relasjoner muliggjør fleksible prosjekthierarkier uten stive mappestrukturer. Et Tema kan være `PART_OF` et annet Tema, en Aktør kan være `PART_OF` en organisasjon, osv. +* **Filtrering:** Brukeren kan filtrere grafen etter nodetype, relasjonstype, tidsperiode eller fritekst. + +## 3. Datakilde +* **SvelteKit server-side:** Henter grafdata via Recursive CTE-spørringer mot PostgreSQL og returnerer `{ "nodes": [...], "edges": [...] }` til klienten. +* **SpacetimeDB:** Brukes ikke for graf-visualisering — dette er historiske data som lever i PostgreSQL. + +## 4. Instruks for Claude Code +* Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (D3.js, Vis.js). +* Begrens traversering til 2-3 ledd ut fra startnode for å unngå eksplosjoner i store grafer. +* Workspace-scopet: alle noder filtreres via `nodes.workspace_id`. diff --git a/docs/features/whiteboard.md b/docs/features/whiteboard.md new file mode 100644 index 0000000..72c8b69 --- /dev/null +++ b/docs/features/whiteboard.md @@ -0,0 +1,34 @@ +# Feature Spec: Whiteboard (Frihåndstavle) +**Filsti:** `docs/features/whiteboard.md` + +## 1. Konsept +Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainstorming. Whiteboardet er en selvstendig komponent som kan brukes i flere kontekster: i møterom, i chat-tråder knyttet til et Tema, eller alene som personlig skisseblokk. + +## 2. Brukskontekster + +| Kontekst | Deltakere | Lagring | +|---|---|---| +| **Møterom** | Alle møtedeltakere i sanntid | Eksporteres som bilde ved møteslutt, knyttes til møtereferatet | +| **Tema-chat** | Alle med tilgang til temaet | Lagres som vedlegg til en melding, synlig i chat-historikken | +| **Personlig** | Kun brukeren selv | Privat skisse, kan deles til et Tema senere | + +## 3. Sanntidssynkronisering +* **SpacetimeDB** synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid. +* Hvert whiteboard har en unik ID og tilhører en workspace. +* Tilgangskontroll følger konteksten: møterom-deltakere, tema-medlemmer, eller kun eieren for personlige tavler. + +## 4. Funksjonalitet +* **Tegneverktøy:** Frihåndstegning, rette linjer, rektangler, ellipser, tekst, piler. +* **Interaksjon:** Fargevelger, strektykkelse, viskelær, angre/gjør om. +* **Implementering:** HTML Canvas eller SVG i SvelteKit. Vurder et lett bibliotek (f.eks. tldraw, Excalidraw) hvis frihåndskvaliteten krever det — men foretrekk egenutviklet for å holde avhengigheter nede. + +## 5. Lagring og Eksport +* **Sanntidsdata (SpacetimeDB):** Strøk-historikk holdes i minnet så lenge tavlen er aktiv. +* **Eksport (PostgreSQL + filsystem):** Når tavlen "lukkes" eller deles, rendres den til PNG eller SVG og lagres som en `media_file` i workspace-mappen. Referansen knyttes til konteksten (melding, møte) via `message_attachments` eller tilsvarende. +* **Dataklassifisering:** Strøk-data i SpacetimeDB er flyktig (kategori 4). Eksporterte bilder er avledet (kategori 3) — kan gjenskapes fra strøk-data så lenge tavlen er aktiv, men etter lukking er bildet den permanente kopien. + +## 6. Instruks for Claude Code +* Whiteboard-komponenten skal være en gjenbrukbar Svelte-komponent som kan mountes i møterom, chat og som frittstående side. +* SpacetimeDB-tabellen for strøk bør være enkel: `whiteboard_id`, `stroke_data` (JSON), `user_id`, `timestamp`. +* Ikke bygg et fullverdig tegneprogram — start med frihåndstegning, viskelær og farger. Utvid ved behov. +* Alle whiteboards er workspace-scopet. diff --git a/docs/features/ai_gateway.md b/docs/infra/ai_gateway.md similarity index 93% rename from docs/features/ai_gateway.md rename to docs/infra/ai_gateway.md index 8406c97..db01473 100644 --- a/docs/features/ai_gateway.md +++ b/docs/infra/ai_gateway.md @@ -1,5 +1,5 @@ -# Feature Spec: AI Gateway (LiteLLM) -**Filsti:** `docs/features/ai_gateway.md` +# Infrastruktur: AI Gateway (LiteLLM) +**Filsti:** `docs/infra/ai_gateway.md` ## 1. Konsept Sidelinja bruker en sentralisert AI Gateway (LiteLLM) som eneste kontaktpunkt for alle AI-kall i systemet. All kode — Rust-workers, SvelteKit server-side — snakker med `http://ai-gateway:4000/v1`. Aldri direkte til leverandør-APIer. @@ -131,7 +131,7 @@ Dette lar oss svare på: ### 5.3 Når vi kjører tester * **Ved ny prompt:** Før den tas i bruk i produksjon * **Ved modellbytte:** Før en leverandør/modell settes som primær for en jobbtype -* **Periodisk:** Månedlig regresjonssjekk — leverandører oppdaterer modeller uten varsel +* **Periodisk (CI):** Månedlig cron-jobb i Forgejo Actions kjører `promptfoo eval` mot alle testsett. Resultater postes som issue ved regresjoner. Leverandører oppdaterer modeller uten varsel — automatisk regresjonssjekk fanger dette opp. * **Ved kvalitetsklager:** Når redaksjonen rapporterer dårlig output ### 5.4 Lagring av testsett diff --git a/docs/features/api_grensesnitt.md b/docs/infra/api_grensesnitt.md similarity index 96% rename from docs/features/api_grensesnitt.md rename to docs/infra/api_grensesnitt.md index 7aaa88e..8f25c72 100644 --- a/docs/features/api_grensesnitt.md +++ b/docs/infra/api_grensesnitt.md @@ -1,5 +1,5 @@ -# Feature Spec: API-grensesnitt og Tjenesteansvar -**Filsti:** `docs/features/api_grensesnitt.md` +# Infrastruktur: API-grensesnitt og Tjenesteansvar +**Filsti:** `docs/infra/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. diff --git a/docs/features/jobbkø.md b/docs/infra/jobbkø.md similarity index 61% rename from docs/features/jobbkø.md rename to docs/infra/jobbkø.md index f4fc07b..bb27bc6 100644 --- a/docs/features/jobbkø.md +++ b/docs/infra/jobbkø.md @@ -1,5 +1,5 @@ -# Feature Spec: Jobbkø (PostgreSQL-basert) -**Filsti:** `docs/features/jobbkø.md` +# Infrastruktur: Jobbkø (PostgreSQL-basert) +**Filsti:** `docs/infra/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. @@ -17,18 +17,19 @@ Et felles, sentralisert køsystem for alle asynkrone bakgrunnsjobber i Sidelinja 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, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + 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 ); @@ -74,9 +75,21 @@ CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for AS | `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 | +| `meeting_summarize` | Møterommet | Generer møtereferat og action points fra transkripsjon | +| `valgomat_generate_profile` | Valgomat | Generer syntetiske kandidatprofiler fra partiprogrammer | +| `valgomat_moderation` | Valgomat | Semantisk deduplisering og nøytralitetsvask av brukerspørsmål | +| `dictation_cleanup` | Lydmeldinger | AI-opprydding av diktert transkripsjon til strukturert notat | +| `generate_embeddings` | Kunnskaps-Bridge | Generer vector embeddings for noder (pgvector) | +| `prompt_eval` | Prompt-Laboratorium | Batch-evaluering av testsett mot valgte modeller | -## 6. Observabilitet -- Jobber med `status = 'error'` skal være synlige i Produktivitetssuiten (enkel admin-visning) +## 6. Workspace-isolasjon +Alle jobber merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode: +* Worker leser `workspace_id` fra jobben og bruker det til å lagre resultater tilbake i riktig silo +* Workspace-spesifikk config (AI-prompts, navnelister) hentes fra `workspaces.settings` +* Feilede jobber vises kun for brukere i riktig workspace i admin-visningen + +## 7. Observabilitet +- Jobber med `status = 'error'` skal være synlige i admin-visningen (SvelteKit `/admin/jobs`) - 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 @@ -84,4 +97,5 @@ CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for AS - 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 +- Opprett alltid jobber med riktig `workspace_id` — hent fra konteksten (innlogget bruker, webhook, etc.) - Ved `stats_parse`: denne erstatter den frittstående cronjobben beskrevet i podcast_statistikk.md — bruk jobbkøen med `scheduled_for` for periodisk kjøring diff --git a/docs/infra/synkronisering.md b/docs/infra/synkronisering.md new file mode 100644 index 0000000..00efbf7 --- /dev/null +++ b/docs/infra/synkronisering.md @@ -0,0 +1,121 @@ +# Infrastruktur: PostgreSQL ↔ SpacetimeDB Synkronisering +**Filsti:** `docs/infra/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. + +### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer +SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten: +1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller +2. Synkes til PostgreSQL innen ~5 sekunder (chat, kanban, markører). + +**Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring. + +### 1.2 PostgreSQL-fallback-prinsippet +Dersom SpacetimeDB fjernes fra stacken, skal systemet fungere med følgende erstatning: +- **Sanntidsoppdateringer:** PostgreSQL `LISTEN/NOTIFY` → SvelteKit Server-Sent Events (SSE) +- **Skriving:** Direkte til PostgreSQL via SvelteKit server-side +- **Autocomplete/cache:** PostgreSQL-spørringer med cursor-basert paginering + +Denne fallbacken trenger ikke implementeres på forhånd, men SpacetimeDB-moduler skal designes slik at fallbacken forblir triviell. + +## 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. Workspace-isolasjon +SpacetimeDB-synkronisering er fullstendig workspace-scopet: +* SpacetimeDB-tilkoblinger bærer `workspace_id` som context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. +* `sync_outbox`-events inkluderer `workspace_id` i payloaden, slik at Rust-workeren skriver til riktig silo i PostgreSQL. +* Ved oppvarming (PG → SpacetimeDB) laster workeren kun data for den aktuelle workspace-en. + +## 8. Konflikthåndtering + +### 8.1 Strategi: Last-Write-Wins (LWW) med reservasjoner +SpacetimeDB er autoritativ for sanntidsdata. Konflikter kan oppstå i to scenarioer: + +**Scenario A — Samtidig redigering i SpacetimeDB:** +SpacetimeDB er single-threaded per modul. Reducer-funksjoner serialiseres automatisk. Ingen klassiske race conditions. + +**Scenario B — PG-oppvarming kolliderer med fersk SpacetimeDB-state:** +Ved restart av SpacetimeDB lastes data fra PG (oppvarming). Hvis en klient skriver til SpacetimeDB *under* oppvarming, kan PG-dataen overskrive ferske endringer. + +**Løsning:** Oppvarming setter et `warming_up`-flagg i SpacetimeDB-modulen. Klienttilkoblinger aksepteres, men skrivinger bufres til oppvarmingen er fullført. Buffrede skrivinger appliseres deretter i rekkefølge. + +### 8.2 Kanban: Posisjonskonflikter +Kanban-kort har en `position`-kolonne (float). To brukere som drar kort samtidig kan skape overlappende posisjoner. Løsning: SpacetimeDB-modulen re-beregner posisjoner (midtpunkts-strategi) og kringkaster oppdatert rekkefølge til alle klienter. + +### 8.3 Chat: Ingen konflikter +Meldinger er append-only. Redigering av egne meldinger er last-write-wins — akseptabelt fordi kun én bruker eier meldingen. + +## 9. Implementeringsstatus (mars 2025) + +### Ferdig +- **SpacetimeDB Rust-modul** (`spacetimedb/src/lib.rs`): `ChatMessage`- og `SyncOutbox`-tabeller. `send_message`-reducer skriver til begge. Publisert som `sidelinja-realtime`. +- **Hybrid-adapter i frontend** (`web/src/lib/chat/spacetime.svelte.ts`): Henter historikk fra PG via REST, lytter på SpacetimeDB for sanntidspush. Ingen oppvarming nødvendig — PG har alltid historikken. +- **PG-fallback:** Fungerer automatisk. Hvis `VITE_SPACETIMEDB_URL` ikke er satt, brukes ren PG-polling (3 sek intervall). + +### Gjenstår +- **Sync-worker (§5.1):** Rust-worker som poller `sync_outbox` i SpacetimeDB og batch-skriver til PostgreSQL. Uten denne workeren persisteres meldinger sendt via SpacetimeDB kun i SpacetimeDB-minnet — de overlever ikke restart. +- **Oppvarming (§5.2):** Ikke implementert, og hybrid-adapteren gjør dette mindre kritisk (klienten henter alltid PG-historikk uavhengig av SpacetimeDB). +- **Workspace-partisjonering (§7):** SpacetimeDB-modulen har `workspace_id`-felt men bruker ikke workspace-token på tilkobling ennå. + +### Designvalg tatt +- **Hybrid fremfor ren SpacetimeDB:** Frontend bruker PG for historikk og SpacetimeDB kun for nye meldinger. Dette unngår oppvarmingsproblematikk og gir umiddelbar tilgang til all historikk. +- **Graceful degradation:** SpacetimeDB-tilkoblingsfeil faller stille tilbake til PG. Brukeren ser ingen feilmelding — PG-data beholdes. +- **Adapter-mønster:** `ChatConnection`-interface med to implementasjoner (PG og SpacetimeDB hybrid). Factory velger basert på env-variabel. Gjør det trivielt å teste hver adapter isolert. + +## 10. Instruks for Claude Code +- `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel +- Workeren skal bruke jobbkø-infrastrukturen (se `docs/infra/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen +- 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 +- Alle sync-payloads må inkludere `workspace_id`. Workeren bruker dette til å sette RLS-kontekst (`SET app.current_workspace_id`) før PG-skriving, eller kjører som superuser og filtrerer eksplisitt diff --git a/docs/proposals/README.md b/docs/proposals/README.md new file mode 100644 index 0000000..3370140 --- /dev/null +++ b/docs/proposals/README.md @@ -0,0 +1,40 @@ +# Forslag (Proposals) + +Halvtenkte idéer, kreative innfall og ting vi vil utforske når vi får tid. Ikke spesifisert, ikke forpliktet — bare parkert. + +## Pipeline + +``` +proposals/ → features/ eller concepts/ +(idé) (spesifisert, klar for implementering) +``` + +Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/features/` eller `docs/concepts/` og forslaget slettes herfra. + +## Oversikt + +| Forslag | Innsats | Wow-faktor | Bygger på | +|---|---|---|---| +| [Auto-Clipper](auto_clipper.md) | Middels | Høy | Live transkripsjon, jobbkø, Caddy byte-range | +| [Graph Health Monitor](graph_health_monitor.md) | Lav | Middels | Kunnskapsgraf, pgvector, jobbkø | +| [Serendipity Roulette](serendipity_roulette.md) | Lav | Høy | Kunnskapsgraf, Live AI | +| [Podcast Time Machine](podcast_time_machine.md) | Lav | Høy | Segmenter, Caddy byte-range, Live AI | +| [Meme Generator](meme_generator.md) | Lav | Høy | Whiteboard, transkripsjon, AI Gateway | +| [Valgomat Roast](valgomat_roast.md) | Lav | Middels | Valgomat, kunnskapsgraf | +| [Live Audience Q&A](live_audience_qa.md) | Middels | Høy | Valgomat, LiveKit, SpacetimeDB | +| [Guest Prep Simulator](guest_prep_simulator.md) | Middels | Høy | Kunnskapsgraf, AI Gateway | +| [Debate Club](debate_club.md) | Middels | Middels | Kunnskapsgraf, AI Gateway, jobbkø | +| [Ghost Host TTS](ghost_host_tts.md) | Stor | Høy | LiveKit, AI Gateway, ny TTS-infra | +| [Artikkel-publisering](artikkel_publisering.md) | Middels | Høy | Kunnskapsgraf, Caddy, jobbkø, AI Gateway | +| [Sosial publisering](social_posting.md) | Lav–Middels | Høy | Chat, jobbkø, workspace settings | +| [Komponerbare sider](komponerbare_sider.md) | Lav (Fase 1) | Middels–Høy | Workspace-modell, SvelteKit, alle feature-komponenter | + +**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator. + +## Format +Forslagsfiler er lette — ingen streng mal. Minimum: +- Hva er idéen? +- Hvorfor er den interessant? +- Hva bygger den på? (eksisterende features/infra) +- **Innsats** (Lav / Middels / Stor) og **Wow-faktor** (Lav / Middels / Høy) +- Åpne spørsmål diff --git a/docs/proposals/artikkel_publisering.md b/docs/proposals/artikkel_publisering.md new file mode 100644 index 0000000..95771cd --- /dev/null +++ b/docs/proposals/artikkel_publisering.md @@ -0,0 +1,128 @@ +# Forslag: Artikkel-publisering + +## Idé +Utvide Sidelinja fra et rent podcast-verktøy til en fullverdig publiseringsplattform. Brukere kan skrive, redigere og publisere artikler — integrert i kunnskapsgrafen, med støtte for vitenskapelig notasjon og en visuell standard som matcher de beste uavhengige publikasjonene. + +## Designfilosofi: Vakre tekster + +Sidelinja-artikler skal føles som noe mellom en Substack-essay og en akademisk publikasjon. Ingen sidebar-rot, ingen widget-helvete, ingen distraksjoner. Bare tekst, typografi og innhold. + +**Prinsipper:** +- **Typografi først.** Seriffont for brødtekst (Georgia, Literata eller lignende), god linjehøyde (1.6–1.8), komfortabel lesbredde (60–75 tegn). Overskrifter i sans-serif for kontrast. +- **Luft.** Generøse marginer. Innholdet puster. Bilder og figurer får plass. +- **Mørk/lys.** Respekterer `prefers-color-scheme`. Begge moduser skal være like gjennomtenkte. +- **Matematikk er førsteklasses.** LaTeX-notasjon rendres med KaTeX (raskere enn MathJax, server-side-kompatibelt). Formler skal se like naturlige ut som brødtekst. +- **Ingen visuelt støy.** Metadata (forfatter, dato, temaer) er diskret plassert. Delingsknappar og navigasjon forsvinner under lesing. Fokus er teksten. + +**Inspirasjon:** Gwern.net (typografi + fotnoter), Distill.pub (interaktive figurer), Stratechery (ren leseopplevelse), Edward Tufte (informasjonstetthet uten rot). + +## Hvorfor er det interessant? +- **Naturlig forlengelse:** Redaksjonen har allerede chat, research-klipper og kunnskapsgraf. Artikler er neste logiske steg — fra intern diskusjon til publisert innhold. +- **Grafkobling:** Artikler som noder i kunnskapsgrafen arver automatisk koblinger til temaer, aktører og episoder. En artikkel om "Skolepolitikk" kobles til alle podcast-episoder, faktoider og research-klipp om samme tema. +- **SEO & Distribusjon:** Podcast-innhold er vanskelig å søkemotor-indeksere. Artikler gir tekstlig innhold som rangerer, med innebygde lenker tilbake til lydsegmenter. +- **Show notes 2.0:** Dagens show notes kan bli fullverdige artikler med egne URL-er, illustrasjoner og grafkoblinger. +- **Vitenskapelig troverdighet:** LaTeX-støtte gjør det mulig å publisere kvantitative analyser, statistikk og formelle argumenter med presisjon som matcher akademisk standard. + +## Hva bygger den på? +- **Kunnskapsgrafen:** Artikkel som ny `node_type` (`'artikkel'`), med edges til temaer/aktører +- **Chat/Channels:** Hver artikkel kan ha en diskusjons-channel (kommentarfelt) +- **Jobbkø:** AI-assistert skriving, faktasjekk mot kunnskapsgrafen, automatisk oppsummering +- **Caddy:** Servering av publiserte artikler på egne URL-er +- **AI Gateway:** Forslag til relaterte temaer, faktoider og episodesegmenter under skriving + +## Skisse + +### Innholdsformat: Markdown + LaTeX + Embeds + +Artikler skrives i utvidet Markdown med tre tillegg: + +**1. LaTeX-notasjon (KaTeX)** +```markdown +Gini-koeffisienten beregnes som: + +$$G = \frac{\sum_{i=1}^{n} \sum_{j=1}^{n} |x_i - x_j|}{2n^2 \bar{x}}$$ + +der $x_i$ er inntekten til individ $i$ og $\bar{x}$ er gjennomsnittsinntekten. +``` + +Inline-matte med `$...$`, blokk-matte med `$$...$$`. KaTeX rendrer server-side i SvelteKit (ingen klient-JS for lesere). Editoren viser live preview av formler. + +**2. Podcast-embeds** +```markdown +{{segment:550e8400-e29b-41d4-a716-446655440000}} +``` +Rendrer en innebygd lydspiller med transkripsjonstekst fra segmentet. Klikk for å lytte til akkurat det øyeblikket i episoden. + +**3. Sidemerknad-fotnoter (Tufte-stil)** +```markdown +Dette er en påstand som trenger kontekst.^[Sidemerknad som vises i margen +på brede skjermer, som popup på mobil. Kan inneholde $\LaTeX$ og lenker.] +``` + +På brede skjermer (>1200px) vises fotnoter i margen ved siden av referansepunktet — aldri nederst på siden. På smale skjermer kollapses de til klikkbare popups. + +### Datamodell +```sql +ALTER TYPE node_type ADD VALUE 'artikkel'; + +CREATE TABLE articles ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + title TEXT NOT NULL, + slug TEXT NOT NULL, -- URL-vennlig tittel + body TEXT NOT NULL, -- Markdown + LaTeX + embeds (kildeformat) + body_html TEXT, -- Pre-rendret HTML (KaTeX + Markdown → HTML ved lagring) + excerpt TEXT, -- Kort oppsummering for RSS/OG-tags + status TEXT NOT NULL DEFAULT 'draft', -- 'draft', 'published', 'archived' + author_id TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, + published_at TIMESTAMPTZ, + CONSTRAINT unique_slug_per_workspace UNIQUE (id) -- slug-unikhet via appkode + workspace +); +``` + +`body` er kildeformatet (Markdown + LaTeX). `body_html` er pre-rendret ved lagring, slik at lesere aldri venter på klient-side rendering. Editoren jobber mot `body`, leseren får `body_html`. + +### Publiseringsflyt +``` +Skriving (draft) → Intern review (channel) → Publisering → RSS + sitemap + OG-tags +``` + +### Funksjoner +- **Markdown + LaTeX editor** med split-view live preview i SvelteKit +- **KaTeX server-side rendering** — ingen JavaScript-avhengighet for lesere +- **Sidemerknad-fotnoter** (Tufte-stil) på brede skjermer, popup på mobil +- **#-mentions** i artikkeltekst som automatisk oppretter graf-edges +- **Innebygg podcast-klipp:** `{{segment:uuid}}` rendrer innebygd lydspiller +- **Relatert innhold:** Diskret panel under artikkelen med automatisk foreslåtte temaer, aktører og episoder basert på graf-nabolag +- **RSS-feed for artikler:** Separat fra podcast-RSS (Atom-format med full HTML-innhold) +- **OG-tags / deling:** Automatisk generert Open Graph-metadata + +### Typografi-stack (CSS) +``` +Brødtekst: Literata / Georgia / serif (1.6 linjehøyde, 60–75ch bredde) +Overskrifter: Inter / system-ui / sans-serif (stramt, tydelig hierarki) +Kode: JetBrains Mono / monospace (med ligaturer) +Matematikk: KaTeX default (skalerer med brødtekst) +``` + +Fontene lastes som `font-display: swap` med system-fallback for umiddelbar rendering. + +### URL-struktur +``` +sidelinja.org/artikler/ → Artikkelliste (ren, minimal) +sidelinja.org/artikler/skolepolitikk-2026 → Enkeltartikkel (full leseopplevelse) +sidelinja.org/feed/artikler.xml → Artikkel-RSS (Atom) +``` + +## Innsats +**Middels** — Datamodellen er enkel (ny node_type + detailtabell). KaTeX har ferdig Svelte-integrasjon og SSR-støtte. Sidemerknad-fotnoter krever litt CSS-arbeid. Den tunge delen er å gjøre typografien og embed-syntaksen virkelig god. + +## Wow-faktor +**Høy** — Kombinasjonen av vakker typografi, LaTeX-støtte og podcast-embeds skaper noe som ikke finnes andre steder. En publiseringsplattform der en politisk analyse kan ha både formler, lydklipp fra intervjuer, og koblinger til kunnskapsgrafen. + +## Åpne spørsmål +- Skal artikler være multi-author (wiki-stil) eller single-author? +- Kommentarfelt for publikum, eller kun intern diskusjon? +- Versjonering av publiserte artikler (à la Wikipedia)? +- Skal draft-artikler leve i SpacetimeDB for sanntids-samskriving, eller er PG + autosave tilstrekkelig? +- Bilder: Content-addressable via `media_files`, eller ekstern hosting (CDN)? +- Skal KaTeX-rendering skje ved lagring (raskere serving) eller ved request (enklere oppdatering)? diff --git a/docs/proposals/auto_clipper.md b/docs/proposals/auto_clipper.md new file mode 100644 index 0000000..16056b1 --- /dev/null +++ b/docs/proposals/auto_clipper.md @@ -0,0 +1,26 @@ +# Forslag: Auto-Clipper (Sosiale medier-klipp) +**Innsats:** Middels | **Wow-faktor:** Høy + +## Idé +Under studio-innspilling (eller møterom) kjører en bakgrunnsprosess som lytter til Whisper-chunks + Aha-markører. Når AI-en oppdager en "punchline", en sterk mening eller et morsomt øyeblikk, genereres automatisk 15–45 sekunders lydklipp + thumbnail-tekst. Klippene havner i en "Clip Inbox"-channel med ett-klikk eksport. + +## Hvorfor +- Podcast-klipp er gull for vekst, men ingen vil klippe manuelt etter innspilling +- Utnytter allerede eksisterende live-transkripsjon + faktoid-oppslag +- Gir umiddelbar ROI: redaksjonen får 5–10 klipp per episode "gratis" + +## Bygger på +- Live transkripsjon + Live AI (studio-modus) +- Jobbkø (`clip_extract`-jobb) +- Caddy byte-range (klippene streames direkte fra original MP3 uten å duplisere filer) +- Kunnskapsgraf (klipp kobles automatisk til `#Tema` og `@Aktør`) + +## Dataklassifisering +- Lydklipp: Avledet (kategori 3) — kan regenereres fra original MP3 + tidsstempler +- Klipp-metadata (tidsstempler, score, thumbnail-tekst): Kritisk (PG) + +## Åpne spørsmål +- Scoring-modell: hva gjør et øyeblikk "klippverdig"? Humor, nyhet, kontrovers, emosjonell intensitet? +- Skal klippene genereres som faktiske MP3-filer umiddelbart, eller lagre kun tidsstempler og generere on-demand ved eksport? +- Eksportmål: TikTok/Instagram/YouTube Shorts — trenger vi video med waveform/teksting, eller holder lyd? +- Kan dette kombineres med Podcast Time Machine for "throwback-klipp"? diff --git a/docs/proposals/debate_club.md b/docs/proposals/debate_club.md new file mode 100644 index 0000000..63c3546 --- /dev/null +++ b/docs/proposals/debate_club.md @@ -0,0 +1,24 @@ +# Forslag: Debate Club (Simulerte debatter) + +## Idé +Velg to aktører + et tema. Systemet genererer en simulert debatt der AI-en spiller begge roller basert på faktiske faktoider og sitater fra kunnskapsgrafen. + +Kan brukes i studioet som research-verktøy: *"Hva ville Støre og Solberg sagt om dette i dag?"* — og eksporteres som lydklipp til episoden via TTS. + +## Hvorfor +- Kreativt bruk av kunnskapsgrafen som går utover oppslag +- Research-verktøy: hjelper programlederne å forberede motargumenter +- Underholdningsverdi: genuint morsomt når det treffer (og når det går galt) +- Kan brukes i valgomaten som "hør hva partiene mener" basert på AI-profiler + +## Bygger på +- Kunnskapsgrafen (faktoider, aktør-profiler, `CONTRADICTS`-edges) +- AI Gateway (`sidelinja/resonering` for nyansert debatt) +- Jobbkø (generering tar tid) +- Valgfritt: TTS for lydklipp (se `ghost_host_tts.md`) + +## Åpne spørsmål +- Etikk: OK å legge ord i munnen på ekte politikere? Trenger tydelig "AI-generert"-merking +- Skal debatten baseres kun på faktoider vi har, eller kan AI-en fylle inn basert på offentlig kjent posisjon? +- Node-type `debate` i grafen, eller bare en melding med spesiell `message_type`? +- Kobling til valgomaten: kan debatter genereres fra AI-kandidatprofiler? diff --git a/docs/proposals/ghost_host_tts.md b/docs/proposals/ghost_host_tts.md new file mode 100644 index 0000000..2815c28 --- /dev/null +++ b/docs/proposals/ghost_host_tts.md @@ -0,0 +1,29 @@ +# Forslag: Ghost Host (AI Text-to-Speech i Studio) + +## Idé +Under innspilling kan programlederne trykke "Ghost Host"-knappen. AI-en genererer en kort kommentar (10-15 sek) basert på kunnskapsgrafen og tidligere episoder, og spiller den av med syntetisk stemme direkte i LiveKit-rommet. + +*"Vegard, du sa akkurat 'det er jo helt bananas', men i episode 17 sa du det samme om vindkraft — skal vi sette inn et klipp?"* + +## Hvorfor +- Tar live AI-assistenten fra passiv (tekst-popup) til aktiv (snakker med i rommet) +- Kan gi ikoniske podcast-øyeblikk +- Unik feature som ingen andre podcast-plattformer har + +## Bygger på +- Live AI-assistent (faktoid-oppslag, NER) +- Kunnskapsgrafen (faktoider, segmenter) +- LiveKit (lydstrøm) +- AI Gateway (tekst-generering) + +## Ny avhengighet +- **Text-to-Speech (TTS)** — dette krever ny infrastruktur: + - Ekstern: ElevenLabs API (kan rutes via LiteLLM?) + - Lokal: Piper TTS, Coqui TTS, eller Tortoise-TTS (Docker-container) + - Vurdering: Lokal TTS passer bedre med self-hosted-filosofien, men kvaliteten er vesentlig lavere enn ElevenLabs + +## Åpne spørsmål +- Stemme: nøytral syntetisk stemme, eller voice clone av en vert? (etiske implikasjoner) +- Latens: kan vi generere tekst + TTS + injisere i LiveKit under 3 sekunder? +- Godkjenning: bør det spilles av direkte, eller vises som "Ghost Host vil si noe" med play-knapp? +- Kill switch: hva om den sier noe feil live? Trenger en "avbryt"-knapp diff --git a/docs/proposals/graph_health_monitor.md b/docs/proposals/graph_health_monitor.md new file mode 100644 index 0000000..72974dd --- /dev/null +++ b/docs/proposals/graph_health_monitor.md @@ -0,0 +1,39 @@ +# Forslag: Knowledge Graph Health Monitor +**Innsats:** Lav | **Wow-faktor:** Middels + +## Idé +En admin-side i SvelteKit som viser "Graf-helse": tetthet, isolerte noder, svake relasjoner, ubalanserte temaer. AI-en foreslår ukentlig nye edges: + +*"Du har 47 faktoider om Støre og 32 om Ap — skal vi koble dem med WORKS_FOR?"* +*"Dette segmentet nevner vindkraft 9 ganger, men ingen #Tema-knute finnes."* + +## Hvorfor +- Kunnskapsgrafen vokser organisk, men kan bli rotete uten vedlikehold +- Gir redaksjonen en "huskeliste" av ting de bør koble manuelt eller godkjenne +- Synliggjør verdien av grafen — man ser den bli smartere over tid +- Forhindrer "orphan nodes" som aldri dukker opp i oppslag + +## Bygger på +- Kunnskapsgrafen (nodes, graph_edges — rekursive CTEs for å finne isolerte subgrafer) +- pgvector (allerede planlagt i Kunnskaps-Bridge — brukes for å finne semantisk like noder som mangler eksplisitt kobling) +- `generate_embeddings`-jobb (eksisterende jobbtype) +- Jobbkø (`graph_suggest_edges` — ny jobbtype, scheduled ukentlig) +- AI Gateway (`sidelinja/resonering` for naturlig språk-forslag) + +## Helsemetrikker +| Metrikk | SQL-skisse | Handlingsforslag | +|---|---|---| +| Isolerte noder | `nodes LEFT JOIN graph_edges ... WHERE edge IS NULL` | "Koble til tema eller slett" | +| Temaer uten faktoider | `themes LEFT JOIN factoids ... HAVING count = 0` | "Tomt tema — berik eller arkiver" | +| Aktører uten relasjoner | Samme mønster | "Ny aktør — trenger kontekst" | +| Semantisk like noder | pgvector cosine distance < 0.15 | "Mulig duplikat — slå sammen?" | +| Manglende edges | AI-analyse av node-par med høy co-occurrence i segmenter | "Koble Støre → Ap?" | + +## Dataklassifisering +- Edge-forslag: Flyktig (TTL 30 dager) — godkjente forslag blir ekte `graph_edges` +- Helsemetrikker: Avledet (beregnes on-demand fra grafen) + +## Åpne spørsmål +- Skal forslagene dukke opp som "innboks-kort" i Redaksjonen, eller kun på en dedikert admin-side? +- Terskel for "semantisk lik": hvor lav cosine distance = mulig duplikat? +- Bør monitoren kjøre per workspace eller globalt (cross-workspace via Bridge)? diff --git a/docs/proposals/guest_prep_simulator.md b/docs/proposals/guest_prep_simulator.md new file mode 100644 index 0000000..c7564fc --- /dev/null +++ b/docs/proposals/guest_prep_simulator.md @@ -0,0 +1,29 @@ +# Forslag: Guest Prep Simulator +**Innsats:** Middels | **Wow-faktor:** Høy + +## Idé +Før man sender async-gjest-lenke eller inviterer noen til studio, kan redaksjonen trykke "Simuler gjest". Systemet genererer en 3–5 min "prøve-debatt" der AI-en spiller gjesten basert på alt som finnes i kunnskapsgrafen om vedkommende (faktoider, tidligere episoder, sitater). + +*"La meg forberede deg — her er hva 'AI-Erna' svarer når du spør om vindkraft..."* + +## Hvorfor +- Reduserer "overraskelser" live — programlederne er bedre forberedt +- Gir bedre spørsmål og motargumenter på forhånd +- Morsomt å høre AI-versjonen av gjesten svare på dagens tema +- Naturlig utvidelse av Debate Club-idéen, men som forberedelsesverktøy + +## Bygger på +- Kunnskapsgraf + `graph_edges` (`CONTRADICTS`, `MENTIONS`) +- AI Gateway (`sidelinja/resonering` — krever nyansert rollespill) +- Debate Club-forslaget (lignende genereringslogikk) +- Den Asynkrone Gjesten (konseptuell kobling — simuler *før* reell invitasjon) + +## Dataklassifisering +- Generert script (Markdown): Avledet (kategori 3) +- TTS-lydfil (valgfritt): Flyktig (TTL 7 dager) + +## Åpne spørsmål +- Trenger vi TTS for å gjøre det "levende", eller holder tekst-basert simulering? +- Etikk: tydelig "AI-simulert"-merking, spesielt hvis TTS brukes +- Skal simuleringen baseres kun på faktoider vi har, eller kan AI-en fylle inn basert på offentlig kjent posisjon? +- Kobling til Debate Club: er dette bare en spesialisert variant av samme feature? diff --git a/docs/proposals/komponerbare_sider.md b/docs/proposals/komponerbare_sider.md new file mode 100644 index 0000000..1b5e6c8 --- /dev/null +++ b/docs/proposals/komponerbare_sider.md @@ -0,0 +1,69 @@ +# Komponerbare sider (Dashboard-komposisjon) + +## Idé +Brukere ser ferdige sider (Redaksjonen, Studioet, etc.), men admin kan komponere egne sider fra tilgjengelige byggeklosser — chat, kanban, statistikk, graf-visning, whiteboard, osv. + +## Hvorfor interessant? +Ulike redaksjoner jobber ulikt. Noen vil ha chat + kanban side-om-side, andre vil ha statistikk + research. I stedet for å hardkode alle kombinasjoner, gir vi admin verktøy til å sette opp sider tilpasset teamets arbeidsflyt. Brukerne ser resultatet som ferdige sider i navigasjonen. + +## Modell + +### Fase 1: Forhåndsdefinerte layouts (implementeres først) +- Admin velger fra en katalog av **blokker** (chat, kanban, statistikk, etc.) +- Plasserer dem i et **grid-layout** med forhåndsdefinerte maler (1-kolonne, 2-kolonne, 2+1, etc.) +- Lagres som JSONB i `workspaces.settings` (nøkkel `pages`) +- Brukere ser sidene i navigasjonen — ingen komposisjon, bare bruk +- Mobil: blokkene stacker vertikalt (grid kollapser) + +```jsonc +// Eksempel: admin-definert side +{ + "slug": "oversikt", + "title": "Redaksjonsoversikt", + "layout": "two-column", // Forhåndsdefinert mal + "blocks": [ + { "type": "chat", "channel": "general", "span": 1 }, + { "type": "kanban", "board": "episoder", "span": 1 }, + { "type": "stats", "view": "lyttere-7d", "span": 2 } + ] +} +``` + +### Fase 2: Konfigurerbare dashboards (senere, ved behov) +- Brukere kan lage egne personlige dashboards +- Drag-and-drop for å endre rekkefølge og størrelse +- Lagres per bruker i `workspace_members.dashboard_config` JSONB (PG, ikke fil) + +### Fase 3: Full fri tiling (trolig unødvendig) +- VS Code / Bloomberg-stil fritt plassering med splitters +- Ekstremt høy kompleksitet, lav marginalverdi vs. Fase 2 +- Unngå med mindre det er et demonstrert behov + +## Arkitektur-krav +Hver feature-komponent MÅ bygges som en **selvstendig Svelte-komponent** som: +- Tar imot `workspaceId` (og evt. config-props som `channelId`, `boardId`) +- Håndterer sin egen datahenting og tilstand +- Respekterer container-størrelse (responsiv innenfor sin blokk) +- Eksponerer en `blockMeta`-descriptor (tittel, min-bredde, ikon) for katalogen + +Dette koster ingenting å gjøre fra start og gir full fleksibilitet senere. + +## Bygger på +- Workspace-modell, SvelteKit layout +- Alle feature-komponenter (chat, kanban, whiteboard, statistikk, etc.) + +## Innsats +- Fase 1: **Lav** (grid-layout + JSON-config, ingen drag-and-drop) +- Fase 2: **Middels** (svelte-grid, bruker-lagring) +- Fase 3: **Stor** (custom tiling engine) + +## Wow-faktor +Middels–Høy. Gir en "dette er MITT verktøy"-følelse som skiller Sidelinja fra rigide alternativer. + +## Åpne spørsmål +- Skal sider være workspace-globale (alle ser samme oppsett) eller per-bruker? + → Fase 1: workspace-globale via `workspaces.settings` JSONB. Fase 2: personlige overrides i `workspace_members.dashboard_config` JSONB. Alt i PG — ingen filer per bruker. +- Hvordan håndtere blokker som krever mye plass (whiteboard, graf) på mobil? + → Fullskjerm-modus per blokk som fallback. +- Bør det finnes et "standard-oppsett" per workspace-type (podcast, nyhetsredaksjon)? + → Ja, som templates admin kan velge som utgangspunkt. diff --git a/docs/proposals/live_audience_qa.md b/docs/proposals/live_audience_qa.md new file mode 100644 index 0000000..caee06f --- /dev/null +++ b/docs/proposals/live_audience_qa.md @@ -0,0 +1,52 @@ +# Forslag: Live Audience Q&A (Studio-integrasjon) +**Innsats:** Middels | **Wow-faktor:** Høy + +## Idé +Under live-innspilling åpner programlederne en "Spør oss"-QR-kode. Tilskuere (eller lyttere på nett) kan sende spørsmål anonymt via en mini-flate. Spørsmålene dukker opp i studio-chatten sortert etter popularitet (real-time voting). AI-en kan foreslå "Svar på dette med faktoid X" i sanntid. + +*"En lytter spør: 'Hvorfor sa dere det motsatte i episode 17?' — og AI-en har allerede funnet klippet."* + +## Hvorfor +- Gjør innspilling interaktiv uten å miste kontroll +- Gir gullmateriale til episoden ("En lytter spurte akkurat...") +- Kobler publikum direkte inn i studioet — bygger community +- Kan gjenbruke valgomat-konseptets anonyme deltakelse + +## Bygger på +- Valgomat (anonym UUID + SpacetimeDB for sanntids-stemming) +- LiveKit + studio-chat (spørsmål vises i studio-UI) +- Kunnskapsgraf (spørsmål kobles automatisk til `#Tema` via NER) +- Live AI (kan foreslå relevante faktoider som svar) + +## Datamodell (skisse) +``` +audience_sessions ( + id UUID, + workspace_id UUID, + livekit_room_id TEXT, + created_at TIMESTAMPTZ, + closed_at TIMESTAMPTZ +) + +audience_questions ( + id UUID, + session_id UUID, + anonymous_id UUID, -- ingen brukerregistrering + body TEXT, + votes INT DEFAULT 0, + status TEXT DEFAULT 'pending', -- pending / shown / dismissed + created_at TIMESTAMPTZ +) +``` + +## Dataklassifisering +- Spørsmål brukt i episode: Kritisk (PG) — del av episodhistorikken +- Spørsmål ikke brukt: Flyktig (TTL per sesjon, slettes ved lukking) +- Anonym ID: Flyktig — ingen kobling til ekte bruker + +## Åpne spørsmål +- Moderering: bør spørsmål gjennom en "godkjenn"-queue før de vises i studio? +- Spam: rate limiting per anonymous_id? Captcha? Eller holder det med manuell moderering? +- Kan dette utvides til live-polling ("Er dere enige med Erna?") med sanntids-resultater? +- Kobling til valgomat: bør QA-flaten være en utvidelse av valgomat-UI, eller helt separat? +- Personvern: lagre IP? Kun for rate limiting, aldri i PG? diff --git a/docs/proposals/meme_generator.md b/docs/proposals/meme_generator.md new file mode 100644 index 0000000..8dfb360 --- /dev/null +++ b/docs/proposals/meme_generator.md @@ -0,0 +1,22 @@ +# Forslag: Møteroms-Meme Generator + +## Idé +Når whiteboard lukkes i møterommet, kjører en `meme_gen`-jobb som tar siste 30 sek transkripsjon + whiteboard-PNG og genererer 3 meme-forslag (Impact-font, norske tekster). Lagres som vedlegg i scratchpad-channelen. + +## Hvorfor +- Redaksjonen elsker dette allerede i Slack — her blir det innebygd +- Viser at plattformen har personlighet +- Utnytter whiteboard + transkripsjon + AI som allerede finnes +- Lav innsats, høy moro + +## Bygger på +- Whiteboard (eksportert PNG) +- Live transkripsjon (siste 30 sek) +- AI Gateway (tekst-generering for meme-tekster) +- Jobbkø (`meme_gen`-jobb) +- Chat/channels (vedlegg i scratchpad) + +## Åpne spørsmål +- Trenger vi bildegenerering (DALL-E/Stable Diffusion), eller holder det med tekst-overlay på whiteboard-bildet? +- Impact-font rendering server-side: Rust image-lib, eller ImageMagick i Docker? +- Opt-in per møte, eller alltid-på? diff --git a/docs/proposals/podcast_time_machine.md b/docs/proposals/podcast_time_machine.md new file mode 100644 index 0000000..1c798bb --- /dev/null +++ b/docs/proposals/podcast_time_machine.md @@ -0,0 +1,23 @@ +# Forslag: Podcast Time Machine + +## Idé +Mens programlederne snakker om et tema, vises en "Play past clip"-knapp i studio-UI-et. Trykker du den, streamer systemet et 20-sekunders segment fra en gammel episode der samme aktør/tema ble nevnt. + +Morsom variant: "Time Machine Roast" — viser bare de mest selvmotsigende eller pinlige sitatene fra arkivet. + +## Hvorfor +- Naturlig utvidelse av segment-søk som allerede finnes +- Gjør podcast-arkivet til en aktiv ressurs under innspilling +- Kan gi gullmomenter: "Du sa jo det stikk motsatte i episode 17!" +- Caddy serverer allerede media med `Accept-Ranges: bytes` — byte-range streaming fungerer + +## Bygger på +- Segmenter med tidsstempler i PG (allerede indeksert med full-text search) +- `DISCUSSED_IN` og `MENTIONS` edges i kunnskapsgrafen +- Live AI-assistent (NER matcher aktører/temaer → oppslag) +- Caddy media-servering (byte-range for å streame bare segmentet) + +## Åpne spørsmål +- Trenger vi en egen "clip cache" eller kan vi streame direkte fra MP3 med start/end byte offset? +- Bør AI-en velge det mest *relevante* klippet, eller det mest *underholdende*? +- Kan dette kombineres med Serendipity Roulette til et "throwback"-modus? diff --git a/docs/proposals/serendipity_roulette.md b/docs/proposals/serendipity_roulette.md new file mode 100644 index 0000000..62d40bf --- /dev/null +++ b/docs/proposals/serendipity_roulette.md @@ -0,0 +1,22 @@ +# Forslag: Serendipity Roulette + +## Idé +Med jevne mellomrom (konfigurerbart, f.eks. hvert 8. minutt) under innspilling i Studioet, trekker systemet en tilfeldig `DISCUSSED_IN`-edge fra kunnskapsgrafen — 2-3 ledd unna dagens tema. AI-en presenterer en morsom eller relevant faktoide som en "overraskelse" i studio-UI-et. + +*"Visste dere at Jonas Gahr Støre i 2011 søkte jobb i AP… samtidig som han var i en episode der dere kalte ham 'den evige kronprinsen'?"* + +## Hvorfor +- Gir ekte "live co-host"-følelse — systemet bidrar aktivt til samtalen +- Utnytter kunnskapsgrafen på en måte som belønner at den er godt fylt +- Kan gi genuint gode podcast-øyeblikk som ellers ikke ville oppstått +- Kan skrus av med én toggle + +## Bygger på +- Kunnskapsgrafen (graph_edges, recursive CTE for 2-3 ledd) +- Live AI-assistent (SpacetimeDB push til studio-UI) +- Eksisterende segmenter + faktoider + +## Åpne spørsmål +- Trenger vi tekst-til-tale (TTS) for at AI-en "leser høyt", eller holder en tekst-popup? +- Hvordan unngå gjentakelser? Trenger vi en "already shown"-tabell per sesjon? +- Bør den være smart nok til å vente på en naturlig pause i samtalen? diff --git a/docs/proposals/social_posting.md b/docs/proposals/social_posting.md new file mode 100644 index 0000000..a734b51 --- /dev/null +++ b/docs/proposals/social_posting.md @@ -0,0 +1,150 @@ +# Forslag: Sosial publisering fra chat + +## Idé +Post til X (og senere Bluesky/Mastodon) direkte fra Redaksjonens chat. Noen sier noe morsomt eller skarpt — én kommando, og det er ute i verden. Flere teammedlemmer deler én konto uten å dele passord. + +## Hvorfor er det interessant? +- **Senker terskelen.** En podcast-konto på X dør ofte fordi kun én person har tilgang og gidder. Med chat-integrasjon kan hvem som helst foreslå en post, og flyten er én handling — ikke "åpne X, logg inn på riktig konto, skriv, post". +- **Spontanitet.** De beste sosiale medie-postene er de som kommer i øyeblikket. Å fange dem rett fra chatten der samtalen skjer bevarer energien. +- **Kontroll uten friksjon.** Konfigurerbar godkjenningsflyt per workspace — fra "alle poster fritt" til "admin godkjenner alt". +- **Bygger på det som finnes.** Chat, jobbkø, workspace-settings — ingen ny infrastruktur. + +## Hva bygger den på? +- **Chat** — `/x`-kommando som trigger publisering +- **Jobbkø** — `social_post`-jobbtype med retry ved rate limit +- **Workspace settings** — API-nøkler og godkjenningsflyt per workspace +- **Admin-panel** — Workspace-innstillinger for plattformer og tilgangsstyring + +## Flyt + +### Uten godkjenning (workspace-innstilling: `approval: none`) +``` +Vegard i chat: "Fantastisk intervju med @støre i dag, han innrømmet at han liker brunost på vaffel" +Marte svarer: /x + ┌──────────────────────────────────────┐ + │ Post til X: │ + │ │ + │ Fantastisk intervju med @støre i │ + │ dag, han innrømmet at han liker │ + │ brunost på vaffel 🧇 │ + │ │ + │ 156/280 tegn │ + │ │ + │ [Rediger] [Avbryt] [Post nå] │ + └──────────────────────────────────────┘ +→ Jobb opprettes → Rust-worker poster til X +→ Systemmelding i chat: "Marte postet til X: https://x.com/sidelinja/status/..." +``` + +### Med godkjenning (workspace-innstilling: `approval: required`) +``` +Marte: /x +→ Jobb opprettes med status 'pending_approval' +→ Systemmelding: "Marte foreslår X-post: «...» — reager med ✅ for å godkjenne" +→ Admin reagerer med ✅ +→ Jobb endres til 'pending', worker poster +→ Systemmelding oppdateres: "Postet av Marte, godkjent av Lars: https://x.com/..." +``` + +### Med godkjenning fra hvem som helst med riktig rolle (workspace-innstilling: `approval: role`) +Samme flyt, men `approval_role` i settings styrer hvem som kan godkjenne (default: `admin`+`owner`). + +## Workspace-innstillinger + +Lagres i `workspaces.settings` under nøkkelen `social`: + +```json +{ + "social": { + "platforms": { + "x": { + "enabled": true, + "api_key": "...", + "api_secret": "...", + "access_token": "...", + "access_token_secret": "..." + } + }, + "approval": "none", + "approval_role": "admin", + "default_hashtags": ["#sidelinja"], + "max_daily_posts": 20 + } +} +``` + +**`approval`-verdier:** +| Verdi | Oppførsel | +|---|---| +| `none` | Alle workspace-medlemmer kan poste direkte | +| `required` | Alle poster krever godkjenning | +| `role` | Poster fra brukere med lavere rolle enn `approval_role` krever godkjenning | + +## Datamodell + +Ingen ny tabell — bruker jobbkøen med `job_type = 'social_post'`: + +```json +{ + "job_type": "social_post", + "payload": { + "platform": "x", + "text": "Fantastisk intervju med @støre...", + "source_message_id": "uuid", + "suggested_by": "authentik_id", + "approved_by": null, + "thread_ids": [] + } +} +``` + +For godkjenningsflyt brukes en ekstra jobbstatus. Jobber med `approval: required` opprettes med `status = 'pending_approval'` (ny enum-verdi) og endres til `pending` ved godkjenning. + +## Admin-panel (workspace-innstillinger) + +Denne featuren forutsetter et minimalt admin-panel i SvelteKit (`/workspace/settings`). Første iterasjon trenger kun: + +- **Sosiale kontoer:** Koble til X (OAuth-flyt), se tilkoblet konto, fjern +- **Godkjenningsflyt:** Velg mellom `none` / `required` / `role` +- **Historikk:** Liste over poster sendt fra dette workspacet (fra jobbkø-data) + +Admin-panelet vil vokse med andre workspace-innstillinger over tid (Whisper-config, AI-prompts, channel-defaults, etc.), men sosial publisering er et godt første bruksområde. + +## Plattform-abstraksjon + +Chat-kommandoen og jobbkøen vet ikke om X spesifikt. Arkitekturen er: + +``` +Chat (/x, /bsky, /social) + │ + ▼ +Jobbkø: social_post { platform: "x", text: "..." } + │ + ▼ +Rust worker: matcher platform → riktig API-klient + ├── X (OAuth 1.0a, v2 API) + ├── Bluesky (AT Protocol) [fremtidig] + └── Mastodon (OAuth 2.0, REST) [fremtidig] +``` + +Å legge til en ny plattform er å implementere én ny handler i Rust-workeren + legge til credentials i workspace-settings. Ingen endring i chat-koden. + +## X API: Tekniske detaljer +- **API:** X API v2, `POST /2/tweets` +- **Auth:** OAuth 1.0a (User Context) — krever app + bruker-tokens +- **Rate limit:** 200 poster/15 min per bruker (mer enn nok) +- **Tråd-støtte:** `reply.in_reply_to_tweet_id` for tråder +- **Pris:** Free tier tillater 1500 poster/mnd. Basic ($100/mnd) gir 3000 poster + lesing. Free er nok for start. + +## Innsats +**Lav–Middels** — X API-integrasjonen er enkel. Chat-kommandoen er en ny handler. Det meste av arbeidet er UI for preview/redigering-popupen og admin-panelet for innstillinger. + +## Wow-faktor +**Høy** — Gjør podcast-kontoen levende uten at noen må "huske å poste". Demokratiserer kontoen uten å dele passord. + +## Åpne spørsmål +- Skal `/x` kunne brukes på en hel tråd (flere meldinger → X-tråd)? +- Bilder/media i poster — fra chat-vedlegg eller kun tekst i v1? +- Skal poster inkludere en automatisk lenke tilbake til episoden/temaet fra grafen? +- Bør det finnes en `/draft`-kommando som legger posten i en kø uten å sende (planlagt posting)? +- Varsling i chat når en foreslått post venter på godkjenning for lenge? diff --git a/docs/proposals/valgomat_roast.md b/docs/proposals/valgomat_roast.md new file mode 100644 index 0000000..6335400 --- /dev/null +++ b/docs/proposals/valgomat_roast.md @@ -0,0 +1,21 @@ +# Forslag: Valgomat Roast Mode + +## Idé +Når en bruker matcher dårlig mot en politiker i valgomaten, får de en ekstra fane: "Sidelinja Roast". Systemet trekker morsomme sitater fra kunnskapsgrafen der politikeren har motsagt seg selv, eller der brukeren er overraskende enig med noen de ikke forventet. + +*"Du er 98% uenig med Støre… men her er 3 ganger han har sagt akkurat det samme som deg."* + +## Hvorfor +- Gjør valgomaten delbar og viral — folk poster roasts på sosiale medier +- Utnytter `CONTRADICTS`-edges og faktoider som allerede finnes i grafen +- Pedagogisk: viser at politikk er mer nyansert enn venstre/høyre + +## Bygger på +- Valgomaten (match-algoritme, brukerprofil) +- Kunnskapsgrafen (faktoider, `CONTRADICTS`-edges, AI-kandidatprofiler) +- AI Gateway (generering av "roast"-tekst) + +## Åpne spørsmål +- Tone: morsom og spiss, eller saklig og overraskende? Begge? +- Bør roasts modereres av redaksjonen før de vises? +- Juss: kan vi "roaste" ekte politikere basert på AI-generert innhold? diff --git a/docs/setup/lokal.md b/docs/setup/lokal.md index eed3e07..88cca73 100644 --- a/docs/setup/lokal.md +++ b/docs/setup/lokal.md @@ -88,9 +88,29 @@ docker compose -f docker-compose.dev.yml --env-file .env.local exec postgres pg_ Lokale tjenester (docker-compose.dev.yml): - **PostgreSQL** — `127.0.0.1:5432` (kodeutvikling, migrasjoner) - **Redis** — `127.0.0.1:6379` +- **SpacetimeDB** — `127.0.0.1:3000` (sanntidsbuffer for chat, kanban m.m.) +- **AI Gateway (LiteLLM)** — `127.0.0.1:4000` (AI-ruter for Gemini, Claude, etc.) - **Caddy** — `127.0.0.1:80/443` (lokal HTTPS for WebRTC) - **faster-whisper** — `127.0.0.1:8000` (transkripsjon-eksperimentering) +## 5.1 SpacetimeDB-modul (valgfritt — kun for sanntidschat) + +Hvis du vil teste sanntidschat via SpacetimeDB (i stedet for PG-polling): + +```bash +# Installer SpacetimeDB CLI +curl -sSf https://install.spacetimedb.com | bash + +# Bygg og publiser Rust-modulen mot lokal SpacetimeDB +cd spacetimedb +spacetime publish sidelinja-realtime --server http://localhost:3000 + +# Legg til env-variabel for SvelteKit (web/.env) +echo "VITE_SPACETIMEDB_URL=ws://localhost:3000" > web/.env +``` + +Uten `VITE_SPACETIMEDB_URL` faller chatten automatisk tilbake til ren PG-polling — SpacetimeDB er ikke påkrevd for utvikling. + ## 6. Utviklingsflyt ### Kode (daglig) @@ -129,5 +149,6 @@ ssh sidelinja@157.180.81.26 "cd /srv/sidelinja && git pull && docker compose up | Forgejo | Ikke installert — push til prod | Docker container | | Authentik | Ikke installert — bypass auth lokalt | Docker container | | DB-data | Flyktig (`.docker-data/`, gitignored) | Persistent, backupes daglig | +| SpacetimeDB | Lokal, modul publiseres manuelt | Docker container, modul deployes via CI | | Whisper | For eksperimentering | Produksjonstranskripsjon | | AI Gateway | For prompt-testing | Produksjonsruting | diff --git a/docs/setup/migration_safety.md b/docs/setup/migration_safety.md new file mode 100644 index 0000000..bd4af3f --- /dev/null +++ b/docs/setup/migration_safety.md @@ -0,0 +1,66 @@ +# Migration Safety Checklist + +Sjekkliste for alle som kjører PostgreSQL-migrasjoner — lokalt eller i prod. + +## Før migrering + +- [ ] Les migrasjonfilen og forstå hva den gjør +- [ ] Har migrasjonen en tilhørende **down-migrering**? (påkrevd for skjema-endringer) +- [ ] Tar migrasjonen backup-hensyn? (dropper den kolonner/tabeller med data?) + +## Etter migrering + +### RLS-verifisering (KRITISK) +Etter *enhver* migrering som oppretter eller endrer tabeller med `workspace_id`: + +```sql +-- 1. Verifiser at RLS er aktivert på alle workspace-tabeller +SELECT tablename, rowsecurity +FROM pg_tables +WHERE schemaname = 'public' + AND tablename IN ('nodes', 'graph_edges', 'messages', 'channels', + 'media_files', 'job_queue', 'message_attachments') +ORDER BY tablename; +-- Forventet: rowsecurity = true for alle + +-- 2. Verifiser at policies eksisterer +SELECT tablename, policyname, cmd, qual +FROM pg_policies +WHERE schemaname = 'public' +ORDER BY tablename; +-- Forventet: workspace_isolation_* policy for hver tabell + +-- 3. Test isolasjon: sett workspace A, forsøk å lese workspace B +SET app.current_workspace_id = ''; +SELECT count(*) FROM nodes WHERE workspace_id = ''; +-- Forventet: 0 rader (RLS blokkerer) + +-- 4. Verifiser at superuser IKKE blokkeres (Rust workers trenger dette) +RESET app.current_workspace_id; +SET ROLE postgres; +SELECT count(*) FROM nodes; +-- Forventet: alle rader synlige +``` + +### Indeksverifisering +```sql +-- Sjekk at viktige indekser finnes +SELECT indexname, tablename +FROM pg_indexes +WHERE schemaname = 'public' + AND tablename IN ('nodes', 'graph_edges', 'messages') +ORDER BY tablename; +``` + +### Constraint-verifisering +```sql +-- Sjekk at foreign keys er intakte +SELECT tc.table_name, tc.constraint_name, tc.constraint_type +FROM information_schema.table_constraints tc +WHERE tc.table_schema = 'public' + AND tc.constraint_type IN ('FOREIGN KEY', 'CHECK', 'UNIQUE') +ORDER BY tc.table_name; +``` + +## Automatisering +Disse sjekkene kjøres automatisk i migrasjonstestene (se `ARCHITECTURE.md` §10.2). Manuell kjøring er kun nødvendig ved prod-migrasjoner til automatiserte tester er på plass. diff --git a/migrations/0001_initial_schema.sql b/migrations/0001_initial_schema.sql index 06f60be..dec0d55 100644 --- a/migrations/0001_initial_schema.sql +++ b/migrations/0001_initial_schema.sql @@ -1,5 +1,5 @@ -- Sidelinja: Initial database schema --- Kunnskapsgraf, jobbkø, meldinger, mediefiler +-- Workspaces, kunnskapsgraf, jobbkø, meldinger, mediefiler -- -- Kjøres mot en tom PostgreSQL-database. -- Krever: PostgreSQL 15+ med pgcrypto (gen_random_uuid) @@ -14,7 +14,39 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid() CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- uuid_generate_v5() for deterministiske segment-UUIDs -- ============================================================ --- 1. Brukere (tynn cache fra Authentik) +-- 0. Felles: updated_at trigger-funksjon +-- ============================================================ +-- Brukes av alle tabeller som trenger automatisk oppdatering av +-- updated_at-kolonnen. Nødvendig for inkrementell synk, cache- +-- invalidering og UI ("sist redigert"). + +CREATE OR REPLACE FUNCTION set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- ============================================================ +-- 1. Workspaces +-- ============================================================ + +CREATE TABLE workspaces ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, -- URL-vennlig ID, brukes i filstier og media-ruting + domain TEXT UNIQUE, -- Valgfritt eget domene for RSS/media + settings JSONB NOT NULL DEFAULT '{}', -- Workspace-config: Whisper initial_prompt, AI system-prompts + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TRIGGER trg_workspaces_updated_at BEFORE UPDATE ON workspaces + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ============================================================ +-- 2. Brukere (tynn cache fra Authentik) -- ============================================================ CREATE TABLE users ( @@ -24,22 +56,41 @@ CREATE TABLE users ( cached_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE TYPE workspace_role AS ENUM ('owner', 'admin', 'member'); + +CREATE TABLE workspace_members ( + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE, + role workspace_role NOT NULL DEFAULT 'member', + joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (workspace_id, user_id) +); + -- ============================================================ --- 2. Nodes — supertabell for alle grafentiteter +-- 3. Nodes — supertabell for alle grafentiteter -- ============================================================ CREATE TYPE node_type AS ENUM ( - 'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding' + 'tema', 'aktør', 'faktoide', 'episode', 'segment', + 'channel', 'melding', 'meeting' ); CREATE TABLE nodes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - node_type node_type NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + node_type node_type NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE INDEX idx_nodes_workspace ON nodes(workspace_id); +CREATE INDEX idx_nodes_type ON nodes(workspace_id, node_type); + +CREATE TRIGGER trg_nodes_updated_at BEFORE UPDATE ON nodes + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + -- ============================================================ --- 3. Relasjonstyper (seeded + utvidbar) +-- 4. Relasjonstyper (globale, ikke workspace-spesifikke) -- ============================================================ CREATE TABLE relation_types ( @@ -50,19 +101,24 @@ CREATE TABLE relation_types ( ); INSERT INTO relation_types (name, label, description, system) VALUES - ('MENTIONS', 'Nevner', 'Refererer til denne aktøren/temaet', true), - ('ABOUT', 'Handler om', 'Faktoide eller klipp som omhandler', true), - ('DISCUSSED_IN', 'Diskutert i', 'Tema diskutert i dette segmentet', true), - ('WORKS_FOR', 'Jobber for', 'Person tilknyttet organisasjon', true), - ('CONTRADICTS', 'Motsier', 'Står i motsetning til', true), - ('PART_OF', 'Del av', 'Tilhører / er underordnet', true); + ('MENTIONS', 'Nevner', 'Refererer til denne aktøren/temaet', true), + ('ABOUT', 'Handler om', 'Faktoide eller klipp som omhandler', true), + ('DISCUSSED_IN', 'Diskutert i', 'Tema diskutert i dette segmentet', true), + ('WORKS_FOR', 'Jobber for', 'Person tilknyttet organisasjon', true), + ('CONTRADICTS', 'Motsier', 'Står i motsetning til', true), + ('PART_OF', 'Del av', 'Tilhører / er underordnet', true), + ('AFFECTS_AXIS', 'Påvirker akse', 'Valgomat: spørsmål påvirker akse', true); -- ============================================================ --- 4. Graph edges +-- 5. Graph edges (workspace-scopet med RLS) -- ============================================================ +-- workspace_id er denormalisert fra nodes for å muliggjøre RLS direkte +-- på edges. Uten dette kan en direkte SELECT på graph_edges lekke +-- relasjoner på tvers av workspaces. CREATE TABLE graph_edges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, 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 REFERENCES relation_types(name), @@ -71,16 +127,20 @@ CREATE TABLE graph_edges ( created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, origin TEXT NOT NULL DEFAULT 'system', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - CONSTRAINT no_self_reference CHECK (source_id != target_id) + CONSTRAINT no_self_reference CHECK (source_id != target_id), + CONSTRAINT unique_edge UNIQUE (source_id, target_id, relation_type) ); -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); +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); +CREATE INDEX idx_edges_workspace ON graph_edges(workspace_id); -- ============================================================ --- 5. Detailtabeller — aktører, temaer, episoder, segmenter +-- 6. Detailtabeller — aktører, temaer, episoder, segmenter -- ============================================================ +-- Workspace-tilhørighet arves via FK til nodes. +-- Spørringer filtrerer alltid via JOIN med nodes. CREATE TABLE actors ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, @@ -88,18 +148,37 @@ CREATE TABLE actors ( type TEXT -- 'person', 'organisasjon', etc. ); +CREATE INDEX idx_actors_name ON actors(name); + CREATE TABLE topics ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, name TEXT NOT NULL ); +CREATE INDEX idx_topics_name ON topics(name); + 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 + guid TEXT UNIQUE NOT NULL -- RSS , immutabel etter opprettelse ); +-- Forhindre endring av episode-GUID etter opprettelse. +-- RSS-klienter bruker GUID for å identifisere episoder — endring bryter feeds. +CREATE OR REPLACE FUNCTION prevent_guid_change() +RETURNS TRIGGER AS $$ +BEGIN + IF OLD.guid IS NOT NULL AND NEW.guid != OLD.guid THEN + RAISE EXCEPTION 'episode guid er immutabel og kan ikke endres etter opprettelse'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_episodes_guid_immutable BEFORE UPDATE ON episodes + FOR EACH ROW EXECUTE FUNCTION prevent_guid_change(); + CREATE TABLE segments ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE, @@ -113,14 +192,41 @@ CREATE INDEX idx_segments_episode ON segments(episode_id); CREATE INDEX idx_segments_fts ON segments USING GIN (to_tsvector('norwegian', transcript)); -- ============================================================ --- 6. Faktoider +-- 7. Channels (chat-kontekster knyttet til vilkårlig node) +-- ============================================================ +-- En channel er en meldingsstrøm knyttet til en parent-node. +-- Enhver node (tema, episode, møte, aktør, ...) kan ha én eller flere channels. +-- Config styrer hvilke capabilities kanalen har. + +CREATE TABLE channels ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + parent_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + name TEXT NOT NULL DEFAULT 'Diskusjon', + config JSONB NOT NULL DEFAULT '{ + "threads": true, + "mentions": true, + "attachments": true, + "research_clips": false, + "ttl_days": null + }'::jsonb + -- config.threads — tillat reply-to-tråder + -- config.mentions — #/@ mention-parsing + grafkoblinger + -- config.attachments — filopplasting + -- config.research_clips — research_clip meldingstype + -- config.ttl_days — null = permanent, tall = auto-slett etter N dager +); + +CREATE INDEX idx_channels_parent ON channels(parent_id); + +-- ============================================================ +-- 8. Faktoider -- ============================================================ CREATE TABLE factoids ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, body TEXT NOT NULL, source_url TEXT, - created_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, + created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, -- NULL = slettet bruker created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); @@ -133,21 +239,22 @@ CREATE TABLE factoid_votes ( ); -- ============================================================ --- 7. Meldinger (chat) +-- 9. Meldinger (chat) -- ============================================================ CREATE TYPE message_type AS ENUM ( 'text', -- Vanlig Markdown-melding 'research_clip', -- Innlimt artikkel (Ctrl+A) 'factoid', -- Faktoid delt i chat + 'voice_memo', -- Lydmelding (lyd er master, transkripsjon som metadata) 'system' -- Automatisk systemmelding ); CREATE TABLE messages ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - topic_id UUID NOT NULL REFERENCES topics(id) ON DELETE CASCADE, + channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, reply_to UUID REFERENCES messages(id) ON DELETE SET NULL, - author_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, + author_id TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, -- NULL = slettet bruker message_type message_type NOT NULL DEFAULT 'text', body TEXT NOT NULL, metadata JSONB, @@ -155,7 +262,7 @@ CREATE TABLE messages ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE INDEX idx_messages_topic ON messages(topic_id, created_at); +CREATE INDEX idx_messages_channel ON messages(channel_id, created_at); CREATE TABLE message_revisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -174,17 +281,20 @@ CREATE TABLE message_votes ( ); -- ============================================================ --- 8. Mediefiler (content-addressable) +-- 10. Mediefiler (content-addressable, workspace-scopet) -- ============================================================ CREATE TABLE media_files ( - sha256 TEXT PRIMARY KEY, - mime_type TEXT NOT NULL, - file_size BIGINT NOT NULL, - uploaded_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, - uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() + sha256 TEXT PRIMARY KEY, + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + mime_type TEXT NOT NULL, + file_size BIGINT NOT NULL, + uploaded_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, -- NULL = slettet bruker + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE INDEX idx_media_workspace ON media_files(workspace_id); + CREATE TABLE message_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, @@ -193,13 +303,14 @@ CREATE TABLE message_attachments ( ); -- ============================================================ --- 9. Jobbkø +-- 11. Jobbkø (workspace-scopet) -- ============================================================ CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry'); CREATE TABLE job_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, job_type TEXT NOT NULL, payload JSONB NOT NULL, status job_status NOT NULL DEFAULT 'pending', @@ -216,5 +327,37 @@ CREATE TABLE job_queue ( CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC) WHERE status IN ('pending', 'retry'); +CREATE INDEX idx_job_queue_workspace ON job_queue(workspace_id); + +-- ============================================================ +-- 12. Row-Level Security (Workspace-isolasjon) +-- ============================================================ +-- SvelteKit setter session-variabel ved tilkobling: +-- SET app.current_workspace_id = ''; +-- +-- Rust-workers kjører som superuser (bypasser RLS) for å prosessere +-- jobber på tvers av workspaces. Workspace-isolasjon for workers +-- sikres i applikasjonskode (job_queue.workspace_id). +-- +-- Detailtabeller (actors, topics, etc.) har ikke egen RLS — de +-- arver workspace-tilhørighet via FK til nodes. +-- Applikasjonskode filtrerer via JOIN med nodes. + +ALTER TABLE nodes ENABLE ROW LEVEL SECURITY; +ALTER TABLE graph_edges ENABLE ROW LEVEL SECURITY; +ALTER TABLE media_files ENABLE ROW LEVEL SECURITY; +ALTER TABLE job_queue ENABLE ROW LEVEL SECURITY; + +CREATE POLICY workspace_isolation_nodes ON nodes + USING (workspace_id = current_setting('app.current_workspace_id')::uuid); + +CREATE POLICY workspace_isolation_edges ON graph_edges + USING (workspace_id = current_setting('app.current_workspace_id')::uuid); + +CREATE POLICY workspace_isolation_media ON media_files + USING (workspace_id = current_setting('app.current_workspace_id')::uuid); + +CREATE POLICY workspace_isolation_jobs ON job_queue + USING (workspace_id = current_setting('app.current_workspace_id')::uuid); COMMIT; diff --git a/migrations/seed_dev.sql b/migrations/seed_dev.sql new file mode 100644 index 0000000..f13284c --- /dev/null +++ b/migrations/seed_dev.sql @@ -0,0 +1,65 @@ +-- Utviklingsdata for lokalt testmiljø. +-- Kjøres etter 0001_initial_schema.sql. +-- IKKE bruk i produksjon. + +BEGIN; + +-- Test-workspace +INSERT INTO workspaces (id, name, slug) VALUES + ('a0000000-0000-0000-0000-000000000001', 'Sidelinja Podcast', 'sidelinja'); + +-- Vegard (Authentik sub claim + dev-user alias) +INSERT INTO users (authentik_id, display_name) VALUES + ('f0c628bf-2dde-42a9-86f9-6a308248a38f', 'Vegard Nøtnæs'), + ('dev-user-1', 'Vegard (dev)'); + +-- Koble begge bruker-IDer til workspace +INSERT INTO workspace_members (workspace_id, user_id, role) VALUES + ('a0000000-0000-0000-0000-000000000001', 'f0c628bf-2dde-42a9-86f9-6a308248a38f', 'owner'), + ('a0000000-0000-0000-0000-000000000001', 'dev-user-1', 'owner'); + +-- Workspace-rot-node (parent for workspace-level channels) +INSERT INTO nodes (id, workspace_id, node_type) VALUES + ('a0000000-0000-0000-0000-000000000010', 'a0000000-0000-0000-0000-000000000001', 'channel'); + +-- Generell chat-kanal +INSERT INTO nodes (id, workspace_id, node_type) VALUES + ('a0000000-0000-0000-0000-000000000011', 'a0000000-0000-0000-0000-000000000001', 'channel'); +INSERT INTO channels (id, parent_id, name) VALUES + ('a0000000-0000-0000-0000-000000000011', 'a0000000-0000-0000-0000-000000000010', 'Generelt'); + +-- Redaksjons-kanal +INSERT INTO nodes (id, workspace_id, node_type) VALUES + ('a0000000-0000-0000-0000-000000000012', 'a0000000-0000-0000-0000-000000000001', 'channel'); +INSERT INTO channels (id, parent_id, name) VALUES + ('a0000000-0000-0000-0000-000000000012', 'a0000000-0000-0000-0000-000000000010', 'Redaksjonen'); + +-- Default-sider for workspace +UPDATE workspaces SET settings = jsonb_set( + COALESCE(settings, '{}'::jsonb), + '{pages}', + '[ + { + "slug": "redaksjonen", + "title": "Redaksjonen", + "icon": "📰", + "layout": "2-1", + "blocks": [ + {"id": "chat-1", "type": "chat", "title": "Redaksjonschat", "props": {"channelId": "a0000000-0000-0000-0000-000000000012"}}, + {"id": "kanban-1", "type": "kanban", "title": "Planlegging"} + ] + }, + { + "slug": "research", + "title": "Research", + "icon": "🔍", + "layout": "2-col", + "blocks": [ + {"id": "research-1", "type": "research", "title": "Research-klipp"}, + {"id": "graph-1", "type": "graph", "title": "Kunnskapsgraf"} + ] + } + ]'::jsonb +) WHERE slug = 'sidelinja'; + +COMMIT; diff --git a/scripts/collect-docs.sh b/scripts/collect-docs.sh index b1560dd..306b45c 100755 --- a/scripts/collect-docs.sh +++ b/scripts/collect-docs.sh @@ -10,9 +10,29 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" OUT="$SCRIPT_DIR/server_context.md" files=( + # Overordnet "$ROOT/ARCHITECTURE.md" "$ROOT/CLAUDE.md" + + # Konsepter (brukeropplevelser) + "$ROOT"/docs/concepts/*.md + + # Features (tekniske byggeklosser) "$ROOT"/docs/features/*.md + + # Infrastruktur + "$ROOT"/docs/infra/*.md + + # Forslag (halvtenkte idéer) + "$ROOT"/docs/proposals/*.md + + # Erfaringer (lærdommer fra implementering) + "$ROOT"/docs/erfaringer/*.md + + # Setup + "$ROOT"/docs/setup/*.md + + # Migrasjoner "$ROOT"/migrations/*.sql )