Dokumentasjon, erfaringslogg, migrasjoner og infra-oppdateringer
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
747244d078
commit
a5985ef3f8
54 changed files with 2777 additions and 244 deletions
43
.env.example
Normal file
43
.env.example
Normal file
|
|
@ -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=
|
||||
156
ARCHITECTURE.md
156
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 <tjeneste>
|
||||
```
|
||||
|
||||
**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/<feature-navn>.md` først. Oppdater disse dokumentene hvis arkitekturen for funksjonen endres.
|
||||
* **Ingen "gh" CLI:** Vi bruker Forgejo. For Pull Requests/Issues, bruk `tea` CLI.
|
||||
* **Deployment:** Kod og test lokalt i WSL. Push til Forgejo, logg inn via SSH for å pulle kode og restarte containere/tjenester (`docker compose up -d`).
|
||||
* **Asynkron AI:** Tyngre jobber (Whisper, OpenRouter) skal aldri blokkere web-forespørsler. Alle bakgrunnsjobber kjøres via den felles PostgreSQL-baserte jobbkøen (se `docs/features/jobbkø.md`).
|
||||
* **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.
|
||||
37
CLAUDE.md
37
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/<navn>.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/`
|
||||
|
|
|
|||
93
docs/concepts/den_asynkrone_gjesten.md
Normal file
93
docs/concepts/den_asynkrone_gjesten.md
Normal file
|
|
@ -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.
|
||||
36
docs/concepts/kunnskapsgrafen.md
Normal file
36
docs/concepts/kunnskapsgrafen.md
Normal file
|
|
@ -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`.
|
||||
40
docs/concepts/møterommet.md
Normal file
40
docs/concepts/møterommet.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
* **Workspace:** Alle jobber, mediefiler og metadata opprettes med riktig `workspace_id`. Hent workspace-config (prompts, domene) fra `workspaces.settings`.
|
||||
36
docs/concepts/redaksjonen.md
Normal file
36
docs/concepts/redaksjonen.md
Normal file
|
|
@ -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`.
|
||||
32
docs/concepts/studioet.md
Normal file
32
docs/concepts/studioet.md
Normal file
|
|
@ -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.
|
||||
97
docs/concepts/valgomaten.md
Normal file
97
docs/concepts/valgomaten.md
Normal file
|
|
@ -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.
|
||||
20
docs/erfaringer/README.md
Normal file
20
docs/erfaringer/README.md
Normal file
|
|
@ -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.
|
||||
65
docs/erfaringer/adapter_moenster.md
Normal file
65
docs/erfaringer/adapter_moenster.md
Normal file
|
|
@ -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.
|
||||
78
docs/erfaringer/spacetimedb_integrasjon.md
Normal file
78
docs/erfaringer/spacetimedb_integrasjon.md
Normal file
|
|
@ -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`).
|
||||
73
docs/erfaringer/svelte5_reaktivitet.md
Normal file
73
docs/erfaringer/svelte5_reaktivitet.md
Normal file
|
|
@ -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<ChatConnection | null>(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<ChatConnection | null>(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.
|
||||
|
|
@ -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*.
|
||||
|
|
|
|||
139
docs/features/chat.md
Normal file
139
docs/features/chat.md
Normal file
|
|
@ -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`.
|
||||
134
docs/features/kalender.md
Normal file
134
docs/features/kalender.md
Normal file
|
|
@ -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.
|
||||
22
docs/features/kanban.md
Normal file
22
docs/features/kanban.md
Normal file
|
|
@ -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.
|
||||
78
docs/features/kunnskaps_bridge.md
Normal file
78
docs/features/kunnskaps_bridge.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
66
docs/features/live_ai.md
Normal file
66
docs/features/live_ai.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
40
docs/features/live_transkripsjon.md
Normal file
40
docs/features/live_transkripsjon.md
Normal file
|
|
@ -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`.
|
||||
137
docs/features/lydmeldinger.md
Normal file
137
docs/features/lydmeldinger.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
* Statistikk er workspace-scopet. `episode_stats` merkes med `workspace_id`. Admin-visningen filtrerer per workspace.
|
||||
|
|
@ -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.
|
||||
101
docs/features/prompt_lab.md
Normal file
101
docs/features/prompt_lab.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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).
|
||||
20
docs/features/visuell_graf.md
Normal file
20
docs/features/visuell_graf.md
Normal file
|
|
@ -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`.
|
||||
34
docs/features/whiteboard.md
Normal file
34
docs/features/whiteboard.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -18,6 +18,7 @@ CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'ret
|
|||
|
||||
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, -- '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',
|
||||
|
|
@ -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
|
||||
121
docs/infra/synkronisering.md
Normal file
121
docs/infra/synkronisering.md
Normal file
|
|
@ -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
|
||||
40
docs/proposals/README.md
Normal file
40
docs/proposals/README.md
Normal file
|
|
@ -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
|
||||
128
docs/proposals/artikkel_publisering.md
Normal file
128
docs/proposals/artikkel_publisering.md
Normal file
|
|
@ -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)?
|
||||
26
docs/proposals/auto_clipper.md
Normal file
26
docs/proposals/auto_clipper.md
Normal file
|
|
@ -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"?
|
||||
24
docs/proposals/debate_club.md
Normal file
24
docs/proposals/debate_club.md
Normal file
|
|
@ -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?
|
||||
29
docs/proposals/ghost_host_tts.md
Normal file
29
docs/proposals/ghost_host_tts.md
Normal file
|
|
@ -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
|
||||
39
docs/proposals/graph_health_monitor.md
Normal file
39
docs/proposals/graph_health_monitor.md
Normal file
|
|
@ -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)?
|
||||
29
docs/proposals/guest_prep_simulator.md
Normal file
29
docs/proposals/guest_prep_simulator.md
Normal file
|
|
@ -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?
|
||||
69
docs/proposals/komponerbare_sider.md
Normal file
69
docs/proposals/komponerbare_sider.md
Normal file
|
|
@ -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.
|
||||
52
docs/proposals/live_audience_qa.md
Normal file
52
docs/proposals/live_audience_qa.md
Normal file
|
|
@ -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?
|
||||
22
docs/proposals/meme_generator.md
Normal file
22
docs/proposals/meme_generator.md
Normal file
|
|
@ -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å?
|
||||
23
docs/proposals/podcast_time_machine.md
Normal file
23
docs/proposals/podcast_time_machine.md
Normal file
|
|
@ -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?
|
||||
22
docs/proposals/serendipity_roulette.md
Normal file
22
docs/proposals/serendipity_roulette.md
Normal file
|
|
@ -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?
|
||||
150
docs/proposals/social_posting.md
Normal file
150
docs/proposals/social_posting.md
Normal file
|
|
@ -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?
|
||||
21
docs/proposals/valgomat_roast.md
Normal file
21
docs/proposals/valgomat_roast.md
Normal file
|
|
@ -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?
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
66
docs/setup/migration_safety.md
Normal file
66
docs/setup/migration_safety.md
Normal file
|
|
@ -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 = '<workspace_a_uuid>';
|
||||
SELECT count(*) FROM nodes WHERE workspace_id = '<workspace_b_uuid>';
|
||||
-- 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.
|
||||
|
|
@ -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(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
node_type node_type NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
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 (
|
||||
|
|
@ -55,14 +106,19 @@ INSERT INTO relation_types (name, label, description, system) VALUES
|
|||
('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);
|
||||
('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_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 <guid>, 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,
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
mime_type TEXT NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
uploaded_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET 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 = '<uuid>';
|
||||
--
|
||||
-- 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;
|
||||
|
|
|
|||
65
migrations/seed_dev.sql
Normal file
65
migrations/seed_dev.sql
Normal file
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue