diff --git a/.env.example b/.env.example index 9d934b5..b879b79 100644 --- a/.env.example +++ b/.env.example @@ -21,10 +21,6 @@ AUTHENTIK_CLIENT_SECRET= # === SvelteKit === AUTH_SECRET= # openssl rand -base64 33 -# === SpacetimeDB === -# URL til SpacetimeDB-instansen (server eller lokal) -VITE_SPACETIMEDB_URL=ws://sidelinja.org/spacetime - # === Whisper (STT) === # URL til faster-whisper-server (Docker-internt) # WHISPER_URL=http://faster-whisper:8000 diff --git a/CLAUDE.md b/CLAUDE.md index 3291825..ee37778 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `universell_input.md` — Tre primitiver (input, mottak, kommunikasjon), noder+edges - `maskinrommet.md` — Rust-orkestrator: fang, prosesser, lever - `bruker_ikke_workspace.md` — Noder er sentrum: brukere, team, innhold er noder. Tilgangsmatrise fra edges. - - `datalaget.md` — PG(+AGE) som graf og arkiv, SpacetimeDB som sanntidslag + - `datalaget.md` — PG(+AGE) som graf og arkiv, PG LISTEN/NOTIFY + WebSocket som sanntidslag - `arbeidsflaten.md` — Spatial canvas med verktøy-paneler, drag-and-drop, kompatibilitetsmatrise - `unix_filosofi.md` — Maskinrommet som orkestrator, arbeid i CLI-verktøy, Claude og maskinrommet deler verktøykasse - `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene: @@ -61,10 +61,10 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + fallback) - `api_grensesnitt.md` — Kommunikasjonskart: SvelteKit er web-API, Rust er worker - `jobbkø.md` — PostgreSQL-basert køsystem for bakgrunnsjobber - - `synkronisering.md` — PostgreSQL ↔ SpacetimeDB dataflyt og eierskapsmodell + - `synkronisering.md` — Historisk: PG ↔ SpacetimeDB dataflyt (utdatert, SpacetimeDB fjernet mars 2026) - `claude_agent.md` — Claude som chat-deltaker: arkitektur, triggere, sikkerhet - `observerbarhet.md` — Strukturert logging, metrikk-endepunkt (/metrics), AI-kostnad -- `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, SpacetimeDB, Authentik) +- `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, Authentik) - `reference/` — Kode fra v1 med gjenbruksverdi (Editor.svelte) - `ops/` — Repeterbare vedlikeholdsjobber (ryddejobb, doc-audit, drift-sjekk) @@ -76,7 +76,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: ## Stack - **Orkestrator/Backend:** Rust (maskinrommet) — kjører native på hosten - **Frontend:** SvelteKit (TypeScript, PWA) -- **Sanntid:** SpacetimeDB +- **Sanntid:** PG LISTEN/NOTIFY + WebSocket (portvokteren) - **Database/Graf:** PostgreSQL (+Apache AGE ved behov) - **Binærlagring:** CAS (content-addressable store) - **AI:** Claude Code (chat-agent), LiteLLM (AI Gateway), faster-whisper (STT) @@ -102,14 +102,13 @@ med Docker container-IPs. | Tjeneste | Begrunnelse | |----------|-------------| | **PostgreSQL** | Versjonsstyring, enkel oppgradering | -| **SpacetimeDB** | Eksperimentelt, offisielt image | | **Authentik** | Kompleks stack (server + worker + Redis) | | **LiteLLM** | Ferdig image, sjelden oppdatering | | **faster-whisper** | Modellhåndtering, ferdig image | ### Kommunikasjon mellom lagene - Caddy (native) → Docker-tjenester: via localhost-porter - (Authentik:9000, Forgejo:3000, SpacetimeDB:9080) + (Authentik:9000, Forgejo:3000) - Caddy (native) → native tjenester: direkte localhost (maskinrommet:3100, SvelteKit:3200) - Maskinrommet (host) → Docker-tjenester: via container-IP @@ -163,15 +162,15 @@ maskinrommet deler verktøykasse. ## Lagmodell ``` GUI (SvelteKit) — spatial canvas med verktøy-paneler - │ skriv │ les (sanntid, direkte WebSocket) - ▼ ▼ -Maskinrommet (Rust) SpacetimeDB ──→ GUI + │ skriv │ les (sanntid via WebSocket) + ▼ ▲ +Maskinrommet (Rust) ───┘ PG LISTEN/NOTIFY → WebSocket → GUI │ orkestrerer ▼ CLI-verktøy (tools/) ←── Claude bruker de samme │ ▼ -Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ... +Tjenester: PG+AGE, CAS, Whisper, LiteLLM, LiveKit ... ``` ## Kjerneprinsipper @@ -189,8 +188,8 @@ Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ... Du ser dine edges. Tilgang via materialisert tilgangsmatrise. 5. **Privat er default.** Input uten mottaker-edge er privat. Security by design, ikke konfigurasjon. -6. **PG er arkivet, SpacetimeDB er nåtid.** Ingen eierskapskonflikt. - To lag, to roller. +6. **PG er sannhetskilden.** All data lever i PostgreSQL. + Sanntid via PG LISTEN/NOTIFY + WebSocket i portvokteren. 7. **Alt er paneler i en arbeidsflate.** Brukergrensesnittet er et spatial canvas der verktøy plasseres, sizes og arrangeres fritt. Hver feature er et panel i BlockShell. Interaksjon mellom paneler via drag-and-drop: diff --git a/docs/arkitektur.md b/docs/arkitektur.md index ea42b02..ce4f0e5 100644 --- a/docs/arkitektur.md +++ b/docs/arkitektur.md @@ -9,8 +9,8 @@ Synops. Alt er noder og edges i en graf. En bruker er en node. Et team er en node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke -av noden selv. Maskinrommet eier alle skrivinger. Frontend er et tynt -lag som leser grafen fra SpacetimeDB. +av noden selv. Maskinrommet eier alle skrivinger. Frontend leser +sanntidsoppdateringer via WebSocket (PG LISTEN/NOTIFY). ## Lagmodell @@ -21,12 +21,12 @@ lag som leser grafen fra SpacetimeDB. │ Drag-and-drop mellom paneler │ └────────┬──────────────────┬─────────────┘ │ intensjoner │ les (sanntid) - │ │ direkte WebSocket -┌────────▼────────┐ ┌──────▼──────────┐ -│ Maskinrommet │ │ SpacetimeDB │ -│ (Rust) │ │ Hele grafen │ -│ Orkestrerer │ │ (noder+edges) │ -└──┬───────────┬──┘ └─────────────────┘ + │ │ WebSocket +┌────────▼──────────────────┘ +│ Maskinrommet (Rust) +│ Orkestrerer + WebSocket-hub +│ PG LISTEN/NOTIFY → WS → GUI +└──┬───────────┬──┘ │ spawner │ ▼ ▼ ┌─────────┐ ┌─────┐┌─────┐┌─────────────┐ @@ -38,25 +38,25 @@ lag som leser grafen fra SpacetimeDB. ``` ### Skrivestien -GUI → intensjon → Maskinrommet (Rust) → SpacetimeDB (instant) → PG (async) +GUI → intensjon → Maskinrommet (Rust) → PG → NOTIFY → WebSocket → GUI -Frontend sender intensjoner (ikke data). Maskinrommet validerer, -skriver til SpacetimeDB først for umiddelbar oppdatering, deretter -persisterer til PG asynkront. Maskinrommet leser edges og bestemmer -hvilke tjenester som trigges. +Frontend sender intensjoner (ikke data). Maskinrommet validerer og +skriver til PG. PG NOTIFY-triggere sender endringer via WebSocket +til alle tilkoblede klienter i sanntid. Maskinrommet leser edges +og bestemmer hvilke tjenester som trigges. ### Lesestien (sanntid) -SpacetimeDB → GUI (direkte WebSocket) +PG NOTIFY → Maskinrommet (WebSocket-hub) → GUI -SpacetimeDB holder hele grafen — alle noder og edges. Frontend -abonnerer via WebSocket med edge-filtre. Visninger er spørringer -mot STDB, ikke forhåndsdefinerte API-endepunkter. +Maskinrommet lytter på PG LISTEN/NOTIFY-kanaler og videresender +relevante endringer via WebSocket til tilkoblede klienter, filtrert +på tilgangsmatrisen. ### Lesestien (tunge spørringer) GUI → Maskinrommet (Rust) → PG Søk, statistikk, semantisk søk (pgvector), graftraversering -(AGE/Cypher). For operasjoner der STDB ikke er egnet. +(AGE/Cypher). ## Datamodell @@ -79,7 +79,7 @@ bestemmes utelukkende av dens edges: - Node uten edges = løs tanke ### Visninger er spørringer -Visninger er spørringer mot SpacetimeDB med edge-filtre: +Visninger er spørringer mot node-/edge-stores med edge-filtre: - Chat = noder med kanal-edge, sortert på tid - Kanban = noder med board-edge, gruppert på status - Kalender = noder med dato-edge, på tidslinje @@ -139,7 +139,7 @@ i `tools/` — maskinrommet spawner riktig verktøy fra jobbkøen. Kjerneansvar (forblir i maskinrommet): - Auth + tilgangskontroll -- Intentions (validering, edge-logikk, STDB+PG-skriving) +- Intentions (validering, edge-logikk, PG-skriving) - Jobbkø (polling, retry, dead letter) - CAS-forvaltning og pruning @@ -173,13 +173,9 @@ Deling er å legge til edges. ## Datalag ### PostgreSQL -Persistent backup og arkiv. Alle noder og edges. Fulltekstsøk, +Eneste datakilde. Alle noder og edges. Fulltekstsøk, pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov. - -### SpacetimeDB -Holder hele grafen — alle noder og edges. Frontend abonnerer via -WebSocket. Maskinrommet skriver hit først for umiddelbar oppdatering, -PG synkroniseres asynkront. +PG LISTEN/NOTIFY-triggere sender sanntidsoppdateringer. ### CAS (Content-Addressable Store) Binærdata (lyd, bilde, video) lagret med hash. TTL basert på @@ -193,7 +189,7 @@ Tredjepartstjenester kjører i **Docker**. Prinsipp: Docker for det vi ikke bygger selv, native for det vi har full kontroll over. **Native (systemd):** Caddy, maskinrommet (Rust), SvelteKit. -**Docker:** PostgreSQL, SpacetimeDB, Authentik, LiteLLM, faster-whisper. +**Docker:** PostgreSQL, Authentik, LiteLLM, faster-whisper. Maskinrommet og Caddy kjører native fordi de trenger direkte tilgang til host-ressurser (CLI-verktøy, TLS-konfig). Docker-tjenester @@ -207,7 +203,7 @@ eksponerer porter på localhost. | CLI-verktøy | Rust/Shell | Native | Prosessering (transcribe, render, audio, AI). Delt mellom maskinrommet og Claude | | Frontend | SvelteKit | Native (systemd) | PWA, SSR, spatial canvas med verktøy-paneler | | Database | PostgreSQL | Docker | Versjonsstyring, enkel oppgradering | -| Sanntid | SpacetimeDB | Docker | Eksperimentelt, offisielt image | +| Sanntid | PG LISTEN/NOTIFY + WebSocket | Native (i maskinrommet) | Ingen ekstra avhengighet, sanntid fra PG | | Binærlagring | CAS (filsystem) | Native | Enkel, deduplisering, ingen ekstern avhengighet | | AI Gateway | LiteLLM | Docker | Ferdig image, sjelden oppdatering | | AI Agent | Claude Code CLI | Native | Chat-deltaker, spawnes av maskinrommet | diff --git a/docs/concepts/adminpanelet.md b/docs/concepts/adminpanelet.md index 0140753..8aae38e 100644 --- a/docs/concepts/adminpanelet.md +++ b/docs/concepts/adminpanelet.md @@ -40,9 +40,9 @@ publisering. #### Varslingsmekanisme -- **Sanntidsvarsel via STDB:** Maskinrommet skriver en varslingsnode - som frontend abonnerer på. Vises som banner/toast i alle aktive - klienter umiddelbart +- **Sanntidsvarsel via WebSocket:** Maskinrommet skriver en varslingsnode + som frontend abonnerer på via WebSocket. Vises som banner/toast i alle + aktive klienter umiddelbart - **Varslingstyper:** - `info` — generell melding (f.eks. "Ny funksjonalitet tilgjengelig") - `warning` — planlagt vedlikehold med nedtelling @@ -100,10 +100,9 @@ Vises for alle med `visibility: open`. Forsvinner automatisk etter Sanntidsoversikt over systemtilstand. -- **Tjeneste-status:** PG, STDB, Caddy, Authentik, LiteLLM, Whisper, LiveKit — oppe/nede/degradert +- **Tjeneste-status:** PG, Caddy, Authentik, LiteLLM, Whisper, LiveKit — oppe/nede/degradert - **Metrikker:** CPU, minne, disk, nettverkstrafikk - **PG-helse:** Tilkoblingspool, aktive spørringer, replikerings-lag (fremtidig) -- **STDB-helse:** Minnebruk, antall abonnenter, graf-størrelse - **Logg-tilgang:** Siste feil og advarsler fra alle tjenester, filtrerbart - **Backup-status:** Siste vellykkede backup per type, neste planlagte kjøring @@ -118,7 +117,7 @@ Sanntidsoversikt over systemtilstand. - **Frontend:** `/admin/health` — dashboard med tjenestekort (opp/nede/degradert med latens), system-metrikker med progress-bars, PG-tilkoblinger og DB-størrelse, backup-status, og filtrerbar logg-visning -- **Tjeneste-sjekker:** PG (SQL ping), STDB (noop-kall), Caddy (admin-API), +- **Tjeneste-sjekker:** PG (SQL ping), Caddy (admin-API), Authentik (health-endpoint), LiteLLM/Whisper/LiveKit (HTTP health). Alle kjøres parallelt med 5s timeout - **Metrikker:** CPU load via `/proc/loadavg`, minne via `/proc/meminfo`, diff --git a/docs/concepts/arbeidstavlen.md b/docs/concepts/arbeidstavlen.md index 636affb..40f9d3f 100644 --- a/docs/concepts/arbeidstavlen.md +++ b/docs/concepts/arbeidstavlen.md @@ -310,14 +310,14 @@ Terminal (Claude/Vegard) Web (SvelteKit) │ ↕ synk │ - SpacetimeDB → WebSocket → sanntid i frontend + PG LISTEN/NOTIFY → WebSocket → sanntid i frontend ``` | Grensesnitt | Leser fra | Skriver til | |-------------|-----------|-------------| | `synops-tasks` CLI | PG direkte | PG direkte | -| Web (KanbanTrait) | SpacetimeDB | Maskinrommet → PG + STDB | -| `synops-respond` | PG | PG + STDB (via maskinrommet) | +| Web (KanbanTrait) | WebSocket (PG) | Maskinrommet → PG | +| `synops-respond` | PG | PG (via maskinrommet) | Ingen nytt UI trengs — arbeidstavlen er bare en `collection`-node med kanban-trait. Vegard ser den på arbeidsflaten ved siden av diff --git a/docs/concepts/møterommet.md b/docs/concepts/møterommet.md index c2ec9eb..2fdacbb 100644 --- a/docs/concepts/møterommet.md +++ b/docs/concepts/møterommet.md @@ -9,7 +9,7 @@ Et fullverdig virtuelt møterom for Sidelinjas redaksjon. Bygget på LiveKit for 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 + - **Delt scratchpad** — Sanntids 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`). @@ -23,6 +23,7 @@ Et fullverdig virtuelt møterom for Sidelinjas redaksjon. Bygget på LiveKit for | Feature | Rolle i Møterommet | |---|---| | LiveKit | WebRTC lyd/video | +| Lydmixer | Volumslidere per deltaker, mute, sound pads (se `docs/features/lydmixer.md`) | | 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`) | diff --git a/docs/concepts/redaksjonen.md b/docs/concepts/redaksjonen.md index 8934140..5b6f034 100644 --- a/docs/concepts/redaksjonen.md +++ b/docs/concepts/redaksjonen.md @@ -7,7 +7,7 @@ Redaksjonen er den daglige arbeidsflaten for Sidelinjas team. Her planlegges epi ## 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. +Alle pågående "Saker" vises i en oversikt. PostgreSQL er kilden til sannhet, med sanntidsoppdateringer via LISTEN/NOTIFY + WebSocket. ### 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. @@ -16,7 +16,7 @@ Hver melding tilhører et Tema. Meldinger støtter tråder (svar-på-svar) og ri 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. +Et kollaborativt tekstfelt koblet til et Tema. Lagres i PostgreSQL med sanntidssynk via WebSocket. ### 2.5 AI-behandling av tekst Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen (✨) og velger handling (rens, oppsummer, trekk ut fakta). Resultatet publiseres som en ny melding med foreslåtte graf-koblinger. Se `docs/proposals/editor.md` § "AI-behandling — universell knapp". @@ -32,5 +32,5 @@ Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen ( ## 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. -* All tilgang er styrt via node_access-matrisen. SpacetimeDB-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder. +* Frontend mottar sanntidsdata via WebSocket — ikke bygg kompleks lokal state. +* All tilgang er styrt via node_access-matrisen. WebSocket-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder. diff --git a/docs/concepts/selvdokumenterende_system.md b/docs/concepts/selvdokumenterende_system.md index dbef6bf..7d99656 100644 --- a/docs/concepts/selvdokumenterende_system.md +++ b/docs/concepts/selvdokumenterende_system.md @@ -63,7 +63,7 @@ Synops (collection, visibility: readable) │ └── (work_items) │ └── Synops Erfaringer (collection) - ├── SpacetimeDB-integrasjon (content) + ├── SpacetimeDB-integrasjon (content, historisk) ├── Authentik OIDC (content) └── ... ``` diff --git a/docs/concepts/studioet.md b/docs/concepts/studioet.md index 806269e..388a873 100644 --- a/docs/concepts/studioet.md +++ b/docs/concepts/studioet.md @@ -9,7 +9,7 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte 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. +5. Programlederne kan trykke **Aha-markør** for å markere viktige øyeblikk. Tidsstempelet lagres i PG og synkes til frontend via WebSocket, koblet til episoden. 6. Etter innspilling skyves lydfilen inn i Podcastfabrikken for full transkripsjon og publisering (se `docs/concepts/podcastfabrikken.md`). ## 3. Komponenter @@ -17,9 +17,10 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte | Feature | Rolle i Studioet | |---|---| | LiveKit | WebRTC lyd/video mellom deltakere | +| Lydmixer | Volumslidere, mute, sound pads, stemmeeffekter, EQ (se `docs/features/lydmixer.md`) | | 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 | +| Aha-markør | Manuell markering av viktige øyeblikk, lagres i PG | ## 4. Avgrensning - Studioet er for **innspilling**, ikke redigering. Klipping/postproduksjon skjer utenfor Sidelinja. @@ -27,6 +28,6 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte - 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. +1. Bygg WebSocket-lytter i frontend + dummy faktoid-push for å verifisere UI. 2. Koble Whisper til et offline lydopptak, kjør NER/oppslag mot PostgreSQL. 3. Koble LiveKit-strømmen til Whisper for sanntid. diff --git a/docs/concepts/valgomaten.md b/docs/concepts/valgomaten.md index 88249e6..98f15f3 100644 --- a/docs/concepts/valgomaten.md +++ b/docs/concepts/valgomaten.md @@ -18,14 +18,14 @@ For å unngå "Blank Canvas"-syndromet startes plattformen med anerkjente rammev 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. +* **Mekanikk:** Ingen registrering kreves for å starte. SvelteKit genererer en anonym UUID som lagres i `localStorage`. * **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. +* **Sanntid:** PCA (Prinsipalkomponentanalyse) beregnes og oppdaterer brukerens posisjon 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: @@ -50,7 +50,7 @@ Et spørsmål i PostgreSQL kan *aldri* endres (`UPDATE`) etter at folk har begyn ### 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. +* **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`. Denne `confidence`-scoren brukes som multiplikator i match-algoritmen. ## 5. Visualisering & Sidelinja Explorer @@ -74,9 +74,9 @@ Av kostnads- og ytelseshensyn skjer all AI-bruk asynkront i backend via jobbkøe |---|---| | **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. | +| **Sanntid (WebSocket)** | Match-kalkulering, swiping-logikk, PCA-beregning. Sanntidsoppdateringer via PG LISTEN/NOTIFY + WebSocket. | | **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`). | +| **PostgreSQL** | Eneste datakilde for spørsmål, akser, svar og kunnskapsgraf. | | **Rust Worker (AI)** | Kjører `valgomat_generate_profile` og `valgomat_moderation` via jobbkøen. | ## 8. Dataklassifisering (ref. docs/arkitektur.md 2.2) @@ -84,22 +84,20 @@ Av kostnads- og ytelseshensyn skjer all AI-bruk asynkront i backend via jobbkøe | 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 | +| Individuelle svar (aggregert) | Kritisk (PG) | Lagret direkte i PG | | AI-genererte kandidatprofiler | Avledet (PG) | Kan regenereres fra partiprogrammer | ## 9. Skaleringsrisiko -### PCA i SpacetimeDB -PCA-beregning i SpacetimeDB er minnekrevende og udokumentert for store datasett. Ved tusenvis av samtidige brukere med 50+ akser kan minnebruken eksplodere. Tiltak: -- **Materialized Views i PostgreSQL** for Sidelinja Explorer — aldri kjør tunge aggregeringer on-the-fly. Oppdater views via nattlig jobb eller etter batch-sync fra SpacetimeDB. -- **Batch-sync med størrelsesbegrensning** — sync_outbox kan bli bottleneck ved høy svaraktivitet. Vurder dedikert sync-frekvens for valgomat-data. -- **PCA-fallback i PG** — hvis SpacetimeDB-minnebruk overstiger terskel, flytt PCA-beregning til PostgreSQL (via `pg_stat_statements` for kostnadsmåling) med lengre oppdateringsintervall. -- **Overvåk tidlig** — legg til valgomat-spesifikke metrikker i `/admin/observability` (antall aktive sesjoner, PCA-beregningstid, SpacetimeDB-minnebruk for valgomat-tabeller). +### PCA-beregning +PCA-beregning er potensielt tung ved mange brukere og akser. Tiltak: +- **Materialized Views i PostgreSQL** for Sidelinja Explorer — aldri kjør tunge aggregeringer on-the-fly. Oppdater views via nattlig jobb. +- **PCA i PG** — beregnes i PostgreSQL med caching og oppdateringsintervall. +- **Overvåk tidlig** — legg til valgomat-spesifikke metrikker i `/admin/observability` (antall aktive sesjoner, PCA-beregningstid). ## 10. 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`. +2. **Swiping-logikk:** Implementer `SubmitSwipe`, `SuggestAxis` og match-algoritmen i maskinrommet. Støtt 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. diff --git a/docs/features/ai_verktoy.md b/docs/features/ai_verktoy.md index 47ba60b..c9fbe82 100644 --- a/docs/features/ai_verktoy.md +++ b/docs/features/ai_verktoy.md @@ -150,7 +150,7 @@ Maskinrommet: 6. Logg forbruk i ai_usage_log │ ▼ -Frontend mottar oppdatering via SpacetimeDB +Frontend mottar oppdatering via WebSocket (PG LISTEN/NOTIFY) ``` ### 6.2 Drag-and-drop integrasjon diff --git a/docs/features/canvas_primitiv.md b/docs/features/canvas_primitiv.md index c35c404..96b5245 100644 --- a/docs/features/canvas_primitiv.md +++ b/docs/features/canvas_primitiv.md @@ -30,13 +30,13 @@ Canvas-primitiv (felles) Whiteboard (consumer) ├── Tegneverktøy: penn, linje, rektangel, tekst ├── Strøk-modell: SVG paths / canvas paths -└── SpacetimeDB: strøk-synkronisering +└── Sanntid: strøk-synkronisering via WebSocket Storyboard (consumer) ├── Kort-rendering: i kompakt modus ├── Status-modell: Klar / Tatt opp / Droppet / Arkivert ├── Portal-soner: overføringsmekanikk til andre blokker -└── SpacetimeDB: kort-posisjon + status-synkronisering +└── Sanntid: kort-posisjon + status-synkronisering via WebSocket ``` ## 2. Kamera-modell @@ -167,9 +167,9 @@ Enhver blokk i `BlockShell` kan gå i fullskjerm. Dette er en generell feature, - **Escape:** Trykk Esc eller klikk "minimer"-knapp for å gå tilbake - **URL-state:** Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side -## 8. SpacetimeDB-integrasjon +## 8. Sanntidsintegrasjon -Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB: +Canvas-primitivet selv har ingen sanntidskobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til WebSocket: ```typescript interface CanvasEvents { @@ -180,7 +180,7 @@ interface CanvasEvents { } ``` -Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer for å synkronisere posisjon til andre klienter. +Storyboard-consumeren bruker `onObjectMove` til å oppdatere PG via maskinrommet, som propagerer endringen til andre klienter via WebSocket. ## 9. Bygger på @@ -201,7 +201,7 @@ Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer ### Fase 2: Storyboard som første consumer - `` rendrer meldingsboks-kort på canvaset -- SpacetimeDB-synk for posisjon og status +- Sanntidssynk for posisjon og status via WebSocket - Portal-soner for overføring ### Fase 3: Whiteboard-migrering diff --git a/docs/features/chat.md b/docs/features/chat.md index 80e6b3d..a6e7422 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -2,7 +2,7 @@ **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. +En universell, sanntids meldingskomponent. 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. Sanntid via PG LISTEN/NOTIFY + WebSocket. ## 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 tilgangsstyring via `node_access`-matrisen. @@ -74,14 +74,15 @@ messages ( ) ``` -### 3.2 SpacetimeDB (sanntid) -SpacetimeDB holder aktive channels' meldinger i minnet som varm cache foran PG. Ved oppstart gjør worker warmup fra PG → SpacetimeDB (per-kanal konfigurasjon). Nye meldinger sendes via SpacetimeDB-reducers og kringkastes til alle tilkoblede klienter. Synkes til PostgreSQL med ~1 sek forsinkelse (se `docs/infra/synkronisering.md`). +### 3.2 Sanntid +Nye meldinger skrives til PG og propageres via LISTEN/NOTIFY + WebSocket +til alle tilkoblede klienter i sanntid. ## 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"). +* **Filtrering:** Svelte-klienten filtrerer den lokale 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. @@ -119,16 +120,10 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s ### Ferdig (mars 2026) - **ChatBlock.svelte:** Adapter-mønster via `createChat()` factory. Bruker `chat.edit()`, `chat.delete()`, `chat.react()` — ingen direkte PG API-kall. -- **SpacetimeDB-adapter (`spacetime.svelte.ts`):** Ren SpacetimeDB-adapter. All data fra SpacetimeDB (historikk via warmup + sanntid). Reaksjoner fra `message_reaction`-tabellen. -- **PG-adapter (`pg.svelte.ts`):** Polling hvert 3. sek. Readonly fallback når SpacetimeDB ikke er konfigurert. -- **Factory (`create.svelte.ts`):** Velger adapter basert på `VITE_SPACETIMEDB_URL`. SSR-safe med `browser`-guard. +- **WebSocket-adapter:** Sanntidsdata via PG LISTEN/NOTIFY + WebSocket. - **Shared types (`types.ts`):** `ChatConnection` interface med `send`, `edit`, `delete`, `react`, `readonly`. -- **SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`):** `ChatMessage`, `MessageReaction`, `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `clear_channel`, `mark_synced`. -- **Worker warmup (`worker/src/warmup.rs`):** PG → SpacetimeDB ved oppstart. Per-kanal konfig (all/messages/days/none). Trådbasert henting. -- **Worker sync (`worker/src/sync.rs`):** SpacetimeDB → PG hvert sekund. Insert/delete/update meldinger + reaksjoner. -- **Admin-side (`/admin/channels`):** Per-kanal warmup-konfigurasjon. - **Tråder:** Komplett trådvisning med datogruppering, autoscroll og visuell skillelinje mellom tråder. -- **Reaksjoner:** Via SpacetimeDB-reducers, synket til PG. +- **Reaksjoner:** Lagret i PG, propagert via WebSocket. - **Meldingskollaps:** Lange meldinger begrenses til 2 linjer med "Vis mer"/"Vis mindre". - **AI-behandling:** Meldinger kan AI-behandles (✨-knapp, eldre modell). Revisjons-toggle viser original vs. AI-versjon. Markdown-rendering for AI-output. NB: Erstattes av frittstående AI-verktøy på arbeidsflaten — se `docs/features/ai_verktoy.md`. - **Konvertering:** Meldinger kan opprettes som kanban-kort eller kalenderhendelse (dialog sier "Opprett", ikke "Konverter" — meldingen beholdes i chatten). @@ -144,13 +139,12 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s ### Gjenstår - **Vedlegg, TTL** — avventer implementering. -- **Tilgangsfiltrering:** SpacetimeDB-laget må filtrere basert på `node_access`-matrisen. -- **Pin/konvertering:** Går fortsatt direkte til PG API (ikke via SpacetimeDB). +- **Pin/konvertering:** Gjenstår. ## 11. Instruks for Claude Code * **Opprettelsesrekkefølge:** Opprett `nodes`-rad → `channels`-rad → (for meldinger) `nodes`-rad → `messages`-rad. Alt i én transaksjon. * **Channel-opprettelse:** Når en Tema, Episode eller Møte opprettes, opprett alltid en default-channel i samme transaksjon. * **Mentions-parsing:** Skjer i sync-workeren ved persistering til PG. Parser mention-UUIDs fra HTML body og oppretter `graph_edges`. -* **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer. `channels.config` inneholder også `warmup_mode`/`warmup_value` for SpacetimeDB-oppvarming. -* **PG er autoritativ** — SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB. +* **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer. +* **PG er eneste datakilde.** Sanntid via LISTEN/NOTIFY + WebSocket. * **Tilgang styres via `node_access`-matrisen.** Channels arver tilgang fra sin parent-node via edges. diff --git a/docs/features/dagbok.md b/docs/features/dagbok.md index 2ee0ad1..68b4289 100644 --- a/docs/features/dagbok.md +++ b/docs/features/dagbok.md @@ -7,7 +7,7 @@ som ikke er delt med andre via edges. Fungerer som en kronologisk logg over tanker, notater og idéer som kun er synlige for eieren. ## 2. Status -**Implementert med nodes+edges (mars 2026).** Sanntid via SpacetimeDB. +**Implementert med nodes+edges (mars 2026).** Sanntid via PG LISTEN/NOTIFY + WebSocket. ### Implementert - Frontend: `/diary` route med dagbok-visning @@ -55,9 +55,9 @@ POST /intentions/create_edge `/diary` → `frontend/src/routes/diary/+page.svelte` ### Datakilde -SpacetimeDB sanntidsabonnement via `nodeStore` og `edgeStore`. +WebSocket-sanntidsdata via `nodeStore` og `edgeStore`. Ingen backend-query — all filtrering skjer i frontend basert på -SpacetimeDB-data som allerede er lastet. +data mottatt via initial sync + WebSocket. ### UI-struktur - Header med tilbake-lenke til mottak og innlegg-teller diff --git a/docs/features/kalender.md b/docs/features/kalender.md index 2f6ed09..1aa2909 100644 --- a/docs/features/kalender.md +++ b/docs/features/kalender.md @@ -6,7 +6,7 @@ Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i ## 2. Status **Kalendervisning implementert (mars 2026).** Bruker `scheduled`-edges i stedet for -separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gjenstår. +separat `calendar_events`-tabell. Abonnement og ICS-eksport gjenstår. ### Implementert - **Fase 1 (v1, mars 2025):** PG-adapter med `calendars` + `calendar_events` (legacy) @@ -22,7 +22,7 @@ separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gj - Hendelsesliste under rutenett for gjeldende måned - Lenke fra mottak-siden med hendelsesteller - Tilgang via `nodeVisibility` (respekterer `node_access`-matrise) - - Sanntidsoppdatering via SpacetimeDB-subscriptions + - Sanntidsoppdatering via WebSocket (PG LISTEN/NOTIFY) ### Gjenstår — Fase 2 - Kobling til kanban-kort (vis deadline på kalender) @@ -33,7 +33,6 @@ separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gj - Abonnementsmodell (kalender → kalender via graph_edges) - Personlige vs. delte kalendere (via samlings-noder) - ICS/CalDAV-eksport -- SpacetimeDB-modul + hybrid-adapter - Varsler/påminnelser via jobbkøen ## 3. Datamodell (implementert) diff --git a/docs/features/kanban.md b/docs/features/kanban.md index 7d3a96d..4a57f48 100644 --- a/docs/features/kanban.md +++ b/docs/features/kanban.md @@ -7,7 +7,7 @@ episodeplanlegging i Redaksjonen, men også mottaker av AI-genererte action points fra Møterommet. ## 2. Status -**Implementert med nodes+edges (mars 2026).** Sanntid via SpacetimeDB. +**Implementert med nodes+edges (mars 2026).** Sanntid via PG LISTEN/NOTIFY + WebSocket. ### Implementert - Board = samlings-node (`node_kind: 'collection'`, `metadata.board: true`) @@ -18,7 +18,7 @@ action points fra Møterommet. - Backend: `POST /intentions/update_edge` for statusendring - Backend: `GET /query/board?board_id=...` for board-spørring - Frontend: `/board/[id]` route med HTML5 drag-and-drop -- Sanntid via SpacetimeDB edge-subscriptions (ingen polling) +- Sanntid via PG LISTEN/NOTIFY + WebSocket (ingen polling) - Opprett kort direkte i kolonne (tittel-input) - Oppretting av nye brett fra mottak-siden @@ -97,5 +97,5 @@ med `submitted_to`-edge til samlingen, inkludert forfatterinfo. * Board er en collection-node med `metadata.board: true`. * Status er en `status`-edge (kort → board) med `metadata.value`. * Bruk native HTML5 Drag and Drop, unngå tunge biblioteker. -* Sanntid via SpacetimeDB edge-subscriptions. +* Sanntid via PG LISTEN/NOTIFY + WebSocket. * Tilgang styres via `node_access`-matrisen. diff --git a/docs/features/live_ai.md b/docs/features/live_ai.md index 42bceab..c18bdf0 100644 --- a/docs/features/live_ai.md +++ b/docs/features/live_ai.md @@ -11,7 +11,7 @@ Brukes i Studioet (se `docs/concepts/studioet.md`). En "virtuell co-host" som dy 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. +4. Treff dytter `LiveFactoidEvent` via WebSocket til frontend. 5. SvelteKit-studio viser faktoiden lydløst i en egen boks. ### 2.2 Lagring @@ -55,7 +55,7 @@ Studio-modus har en **kill switch** — en synlig "Stopp AI"-knapp i studio-gren 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. +Kill switch-status (`ai_enabled: bool`) lagres i kommunikasjonsnoden metadata i PG og synkes til alle klienter via WebSocket. ## 5. Instruks for Claude Code * Begge moduser deler samme Whisper-pipeline — ikke dupliser transkripsjonskode. diff --git a/docs/features/lydmixer.md b/docs/features/lydmixer.md index 33622b9..0470c49 100644 --- a/docs/features/lydmixer.md +++ b/docs/features/lydmixer.md @@ -63,14 +63,15 @@ Eksempler på standard-pads: jingle/intro, applaus, latter, dramatisk pause, ### 3.4 Delt mixer-kontroll (flerbruker) Alle deltakere i rommet kan se og bruke mixeren samtidig. Mixer-state -synkroniseres i sanntid via SpacetimeDB, slik at volumendringer, mutes, -effekttogles og pad-avspilling reflekteres hos alle klienter umiddelbart. +synkroniseres i sanntid via PG LISTEN/NOTIFY + WebSocket, slik at +volumendringer, mutes, effekttogles og pad-avspilling reflekteres hos +alle klienter umiddelbart. | Element | Synkronisering | |---|---| -| **Volumslider** | STDB: `MixerChannel`-tabell med `gain`-verdi per kanal | -| **Mute** | STDB: `is_muted` boolean per kanal | -| **Effekt av/på** | STDB: `active_effects` JSON per kanal | +| **Volumslider** | PG: `mixer_channels`-tabell med `gain`-verdi per kanal | +| **Mute** | PG: `is_muted` boolean per kanal | +| **Effekt av/på** | PG: `active_effects` JSON per kanal | | **Pad-trigger** | LiveKit Data Message (lav latens) | | **Pad-konfig** | Node metadata (persistent, sjelden endring) | @@ -83,35 +84,18 @@ effekttogles og pad-avspilling reflekteres hos alle klienter umiddelbart. rolle-system (owner/admin/member-edges). **Konflikthåndtering:** Last-write-wins. Volumslidere er kontinuerlige -verdier som oppdateres via STDB-reducers. Ved samtidig endring av samme +verdier som oppdateres via maskinrommet. Ved samtidig endring av samme kanal vinner siste skriving — i praksis uproblematisk fordi endringer er visuelt synlige for alle og deltakerne koordinerer naturlig. -**SpacetimeDB-tabeller:** +**PG-tabell:** `mixer_channels` med NOTIFY-trigger for sanntidspropagering via WebSocket. -```rust -#[spacetimedb::table(accessor = mixer_channel, public)] -pub struct MixerChannel { - #[primary_key] - pub id: String, // "{room_id}:{target_user_id}" - pub room_id: String, // "communication_{node_uuid}" - pub target_user_id: String, // hvem kanalen tilhører - pub gain: f64, // 0.0–1.5 - pub is_muted: bool, - pub active_effects: String, // JSON: {"fat_bottom": true, "robot": false, ...} - pub role: String, // "editor" | "viewer" — tilgangskontroll per kanal - pub updated_by: String, // hvem som sist endret - pub updated_at: Timestamp, -} -``` - -**STDB Reducers:** -- `create_mixer_channel(room_id, target_user_id, updated_by)` — idempotent opprettelse -- `set_gain(room_id, target_user_id, gain, updated_by)` — clamped 0.0–1.5, viewer-sjekk -- `set_mute(room_id, target_user_id, is_muted, updated_by)` — viewer-sjekk -- `toggle_effect(room_id, target_user_id, effect_name, updated_by)` — JSON-toggle -- `delete_mixer_channel(room_id, target_user_id)` — opprydding ved disconnect -- `set_mixer_role(room_id, target_user_id, role, updated_by)` — sett editor/viewer +**API-endepunkter (maskinrommet):** +- `POST /intentions/create_mixer_channel` — idempotent opprettelse +- `POST /intentions/set_gain` — clamped 0.0–1.5, viewer-sjekk +- `POST /intentions/set_mute` — viewer-sjekk +- `POST /intentions/toggle_effect` — JSON-toggle +- `POST /intentions/set_mixer_role` — sett editor/viewer Mixer-kanaler ryddes automatisk ved `close_live_room` og `clear_all`. @@ -213,7 +197,7 @@ Lydmixeren aktiveres via `mixer`-traitet på en samlings-node. Krever at - [x] Master fader og master mute ### Fase B: Delt mixer-kontroll -- [x] SpacetimeDB: `MixerChannel`-tabell + reducers (set_gain, set_mute, toggle_effect, set_mixer_role) +- [x] PG: `mixer_channels`-tabell + NOTIFY-trigger + maskinrommet-endepunkter - [x] Frontend abonnerer på mixer-state, oppdaterer Web Audio-graf ved endringer - [x] Visuell feedback: alle ser sliders bevege seg i sanntid - [x] Tilgangskontroll: eier/admin kan sette deltaker til "viewer" (kun observere) @@ -229,7 +213,7 @@ Lydmixeren aktiveres via `mixer`-traitet på en samlings-node. Krever at - [x] Fat bottom (lowshelf filter, +8dB @ 200Hz) - [x] Sparkle (highshelf filter, +4dB @ 10kHz) - [x] Exciter (WaveShaperNode soft-clip + highshelf @ 3.5kHz) -- [x] Per-kanal av/på-toggles for hver effekt (synkronisert via STDB `active_effects`) +- [x] Per-kanal av/på-toggles for hver effekt (synkronisert via PG `active_effects`) - [x] Preset-konfigurasjon: "Av", "Podcast-stemme" (bass+luft), "Radio-stemme" (bass+luft+exciter) - [x] Highpass-filter (80Hz) alltid aktiv for rumble-fjerning diff --git a/docs/features/notater.md b/docs/features/notater.md index 65065bd..5396cfd 100644 --- a/docs/features/notater.md +++ b/docs/features/notater.md @@ -5,7 +5,7 @@ Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike kontekster — show notes, møtenotater, research-notater. Notater er nodes i kunnskapsgrafen og kan kobles til andre noder. ## 2. Status -**PG-adapter ferdig og deployet (mars 2025).** Rich text og SpacetimeDB-sync gjenstår. +**PG-adapter ferdig og deployet (mars 2025).** Rich text gjenstår. ### Implementert - Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes) @@ -21,7 +21,6 @@ Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike ko - Versjonering / undo-historikk - Kobling til andre noder (temaer, episoder, aktører) - Flerbruker-redigering (conflict resolution) -- SpacetimeDB-modul + hybrid-adapter - Eksport (Markdown, PDF) ## 3. Datamodell (implementert) diff --git a/docs/features/universell_overfoering.md b/docs/features/universell_overfoering.md index a3c063f..7861ad0 100644 --- a/docs/features/universell_overfoering.md +++ b/docs/features/universell_overfoering.md @@ -168,33 +168,10 @@ med forklaring ved inkompatibilitet. Factory-funksjon `createBlockReceiver(toolType)` oppretter en `BlockReceiver` for en gitt verktøy-type. -## 5. SpacetimeDB-integrasjon +## 5. Sanntidsintegrasjon -Plasseringsdata for sanntidskontekster (storyboard, kanban) eies av SpacetimeDB: - -```rust -#[table(name = message_placement, public)] -pub struct MessagePlacement { - #[primary_key] - pub id: String, - pub message_id: String, - pub context_type: String, - pub context_id: String, - pub entered_at: Timestamp, - pub position_json: String, // JSON-serialisert posisjon -} - -#[reducer] -pub fn place_message(ctx: &ReducerContext, placement: MessagePlacement) { ... } - -#[reducer] -pub fn remove_placement(ctx: &ReducerContext, message_id: String, context_type: String, context_id: String) { ... } - -#[reducer] -pub fn move_on_canvas(ctx: &ReducerContext, placement_id: String, new_position_json: String) { ... } -``` - -Sync-workeren persisterer til PG `message_placements`-tabellen. +Plasseringsdata lagres i PG `message_placements`-tabellen. Endringer +propageres via LISTEN/NOTIFY + WebSocket for sanntidsoppdatering i frontend. ## 6. Responsivt design @@ -207,7 +184,7 @@ Sync-workeren persisterer til PG `message_placements`-tabellen. - **Meldingsboks** (`meldingsboks.md`): Alle overførte objekter er meldingsbokser - **Kunnskapsgraf** (`kunnskapsgraf_og_relasjoner.md`): Plasseringer er relasjoner i grafen - **BlockShell** / PageGrid: Verktøy-panel-rammen som rendrer mottakssoner -- **SpacetimeDB** (`synkronisering.md`): Sanntidssynk av plasseringer +- **PG LISTEN/NOTIFY + WebSocket**: Sanntidssynk av plasseringer ## 8. Konsekvenser for eksisterende kode @@ -222,9 +199,9 @@ Sync-workeren persisterer til PG `message_placements`-tabellen. `message_placements` er ny. Eksisterende `kanban_card_view` og `calendar_event_view` lever parallelt inntil migrering. -### 8.3 SpacetimeDB-modul +### 8.3 Sanntid -Ny tabell `message_placement` med reducers for place/remove/move. +PG NOTIFY-trigger på `message_placements` for sanntidspropagering via WebSocket. ## 9. Implementasjonsstatus diff --git a/docs/features/visuell_graf.md b/docs/features/visuell_graf.md index 591a4d9..8d07093 100644 --- a/docs/features/visuell_graf.md +++ b/docs/features/visuell_graf.md @@ -12,7 +12,7 @@ En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt naviger ## 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. +* Graf-visualisering er basert på 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). diff --git a/docs/features/whiteboard.md b/docs/features/whiteboard.md index 58f9e91..3655634 100644 --- a/docs/features/whiteboard.md +++ b/docs/features/whiteboard.md @@ -13,7 +13,7 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst | **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. +* PG LISTEN/NOTIFY + WebSocket synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid. * Hvert whiteboard har en unik ID og er en node i grafen. * Tilgangskontroll følger konteksten: møterom-deltakere, tema-medlemmer, eller kun eieren for personlige tavler. @@ -23,12 +23,11 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst * **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. +* **Sanntidsdata:** Strøk-historikk lagres i PG og propageres via WebSocket. * **Eksport (PostgreSQL + filsystem):** Når tavlen "lukkes" eller deles, rendres den til PNG eller SVG og lagres som en `media_file`. 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`. +* PG-tabellen for strøk bør være enkel: `whiteboard_id`, `stroke_data` (JSON), `user_id`, `timestamp`. NOTIFY-trigger for sanntid. * Ikke bygg et fullverdig tegneprogram — start med frihåndstegning, viskelær og farger. Utvid ved behov. * Tilgang til whiteboards styres via `node_access`-matrisen. diff --git a/docs/infra/agent_api.md b/docs/infra/agent_api.md index 30664dc..afbee38 100644 --- a/docs/infra/agent_api.md +++ b/docs/infra/agent_api.md @@ -107,7 +107,7 @@ Oppdater en spec-node. } ``` -Oppdaterer noden i STDB + PG. Logger endringen i `ai_usage_log` +Oppdaterer noden i PG. Logger endringen i `ai_usage_log` med `job_type: 'spec_update'`. Oppretter `revision`-node med forrige versjon for historikk. @@ -122,7 +122,7 @@ Send et svar i en samtale (erstatter dagens direkte STDB-skriving). } ``` -Maskinrommet håndterer node-opprettelse, edges, og STDB-synk. +Maskinrommet håndterer node-opprettelse og edges. Metadata kan inkludere kildereferanser som frontend kan vise. #### `POST /agent/suggest_edges` @@ -144,7 +144,7 @@ godkjenner. Over en viss confidence-terskel kan de auto-godkjennes. ## Autentisering Agent-endepunktene bruker agent-token fra `agent_identities`-tabellen. -Samme token som brukes for STDB-tilkobling. Header: +Header: ``` Authorization: Bearer @@ -161,7 +161,7 @@ Med Agent API endres flyten: ### Før (nåværende) ``` -melding → agent_respond-jobb → bygg prompt (SQL) → claude -p → parse svar → skriv STDB+PG +melding → agent_respond-jobb → bygg prompt (SQL) → claude -p → parse svar → skriv PG ``` ### Etter (med Agent API) diff --git a/docs/infra/api_grensesnitt.md b/docs/infra/api_grensesnitt.md index c372d19..985da90 100644 --- a/docs/infra/api_grensesnitt.md +++ b/docs/infra/api_grensesnitt.md @@ -7,7 +7,7 @@ Maskinrommet eier alle skrivinger: det validerer, skriver til PG, og orkestrerer konsekvenser. Sanntid: PG LISTEN/NOTIFY → maskinrommet → WebSocket `/ws` → frontend. -SpacetimeDB er under utfasing — frontend bruker kun portvokterens WebSocket (Fase M2). +Frontend bruker portvokterens WebSocket for sanntid. Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → PG. ## 2. Kommunikasjonskart @@ -57,7 +57,7 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → ## 5. Implementerte endepunkter ### Offentlige -- `GET /health` — Helsesjekk. Verifiserer PG- og STDB-tilkobling. +- `GET /health` — Helsesjekk. Verifiserer PG-tilkobling. ### WebSocket (sanntid, oppgave 22.1–22.2) - `GET /ws?token=` — WebSocket-oppgradering for sanntidsstrøm. @@ -73,8 +73,8 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → ### Autentiserte (krever `Authorization: Bearer `) - `GET /me` — Returnerer autentisert brukers `node_id` og `authentik_sub`. -- `POST /intentions/create_node` — Opprett node. Skriv til STDB (instant), - spawn async PG-skriving, returner `node_id` umiddelbart. +- `POST /intentions/create_node` — Opprett node. Skriv til PG, + returner `node_id` umiddelbart. - Body (JSON): `{ node_kind?, title?, content?, visibility?, metadata? }` - Defaults: `node_kind="content"`, `visibility="hidden"`, andre felter tomme - Respons: `{ node_id: "" }` @@ -110,12 +110,12 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → ### LiveKit / Sanntidslyd (oppgave 11.2) - `POST /intentions/join_communication` — Koble til sanntidslyd i en kommunikasjonsnode. Validerer deltaker-tilgang (owner/member_of/host_of-edge eller via alias). - Genererer LiveKit access token (JWT), oppretter rom i STDB, oppdaterer node-metadata. + Genererer LiveKit access token (JWT), oppdaterer node-metadata. - Body (JSON): `{ communication_id, role? }` (role: "publisher" | "subscriber", default "publisher") - Respons: `{ livekit_room_name, livekit_token, livekit_url, identity, participants[] }` - Frontend bruker `livekit_token` + `livekit_url` til å koble livekit-client SDK. - `POST /intentions/leave_communication` — Forlat sanntidsrom. - Fjerner deltaker fra STDB live-rom. + Fjerner deltaker fra live-rom. - Body (JSON): `{ communication_id }` - Respons: `{ status: "left" }` - `POST /intentions/close_communication` — Steng sanntidsrom (krever owner/admin). @@ -124,7 +124,7 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → - Respons: `{ status: "closed" }` ### Mixer-kanaler (oppgave 22.2) -Erstatter SpacetimeDB-reducers for delt mixer-tilstand i LiveKit-rom. +Delt mixer-tilstand i LiveKit-rom. Skriver til PG `mixer_channels`-tabell; NOTIFY-trigger propagerer til WS. - `POST /intentions/create_mixer_channel` — Opprett mixer-kanal for deltaker i rom. - Body: `{ room_id, target_user_id }` diff --git a/docs/infra/backup.md b/docs/infra/backup.md index 8068f13..ba9a412 100644 --- a/docs/infra/backup.md +++ b/docs/infra/backup.md @@ -3,8 +3,7 @@ **Filsti:** `docs/infra/backup.md` Synops sin backup-strategi bygger på én innsikt: **PostgreSQL er den -eneste autoriteten.** SpacetimeDB er en sanntidscache som gjenoppbygges -fra PG ved behov. Media-filer i CAS er innholdsadresserte og immutable. +eneste autoriteten.** Media-filer i CAS er innholdsadresserte og immutable. ## Arkitektur @@ -15,8 +14,7 @@ PostgreSQL (autoritativ kilde) │ └──→ /srv/synops/backup/pg/sidelinja_YYYYMMDD_HHMMSS.dump │ └──→ Rotasjon: 30 dager │ - └──→ SpacetimeDB (sanntidscache) - └──→ Gjenoppbygges fra PG ved krasj (warmup) + └──→ Sanntid via PG LISTEN/NOTIFY → WebSocket ``` ## 1. PG-dump (daglig) @@ -44,28 +42,10 @@ docker exec sidelinja-postgres-1 pg_restore --list /tmp/test.dump docker exec sidelinja-postgres-1 rm /tmp/test.dump ``` -## 2. STDB-gjenoppbygging ved krasj +## 2. Sanntid -**Modul:** `maskinrommet/src/stdb_monitor.rs` - -SpacetimeDB er en sanntidscache. Hvis den krasjer, tapes ingen data -fordi all skriving går gjennom maskinrommet som skriver til PG først -(asynkront, men alltid). Gjenoppbygging skjer automatisk: - -### Ved oppstart -Maskinrommet kjører `warmup::run()` i `main.rs` — laster alle noder, -edges og node_access fra PG til STDB. - -### Ved krasj under drift -`stdb_monitor` kjører i bakgrunnen og sjekker STDB hvert 30. sekund: - -1. **Oppdager** at STDB ikke svarer (var oppe, nå nede) -2. **Venter** opptil 10 minutter på at containeren restarter -3. **Kjører warmup** (PG → STDB) når STDB svarer igjen -4. **Logger** hele hendelsesforløpet - -Prosessen er automatisk og krever ingen manuell inngripen så lenge -Docker restarter containeren (restart-policy: `unless-stopped`). +Sanntid leveres via PG LISTEN/NOTIFY + WebSocket i portvokteren. +Ingen separat sanntidstjeneste å gjenoppbygge — PG er eneste datakilde. ## 3. Restore fra backup @@ -79,7 +59,7 @@ docker cp /srv/synops/backup/pg/sidelinja_YYYYMMDD.dump sidelinja-postgres-1:/tm docker exec sidelinja-postgres-1 pg_restore -U sidelinja -d sidelinja --clean /tmp/restore.dump docker exec sidelinja-postgres-1 rm /tmp/restore.dump -# Start maskinrommet (warmup laster PG → STDB automatisk) +# Start maskinrommet sudo systemctl start maskinrommet ``` @@ -88,7 +68,7 @@ Ved total serversvikt (ny VPS): 1. Installer OS og Docker (se `docs/setup/produksjon.md`) 2. Start PG-container 3. Restore dump (se over) -4. Start maskinrommet (warmup håndterer STDB) +4. Start maskinrommet 5. Avledede data (segmenter, søkeindeks) regenereres fra kildene ## 4. Overvåking @@ -98,12 +78,11 @@ Health-dashboardet (`/admin/health`) viser backup-status: - **stale** — dump-fil er eldre enn 25 timer - **missing** — ingen dump-filer funnet -Metrikk-endepunktet (`/metrics`) inkluderer STDB-status som del av +Metrikk-endepunktet (`/metrics`) inkluderer tjeneste-status som del av helsesjekken. ## 5. Hva som IKKE backupes (bevisst) -- **SpacetimeDB** — sanntidscache, gjenoppbygges fra PG - **Redis** — cache, regenereres automatisk - **Caddy-data** — sertifikater regenereres av Let's Encrypt - **Whisper-modeller** — re-download fra HuggingFace diff --git a/docs/infra/claude_agent.md b/docs/infra/claude_agent.md index 6dc5cc7..5443aca 100644 --- a/docs/infra/claude_agent.md +++ b/docs/infra/claude_agent.md @@ -21,12 +21,12 @@ Bruker sender melding (via frontend) → kaller: claude -p "" --output-format json → oppretter svar-node i PG, logger ai_usage + resource_usage → returnerer JSON med reply_node_id + response_text - → maskinrommet: skriver til STDB (sanntidsvisning) - → frontend viser melding i sanntid via STDB WebSocket + → PG NOTIFY propagerer til frontend via WebSocket + → frontend viser melding i sanntid ``` Ansvarsdeling (unix-filosofi): -- **Maskinrommet:** Auth, kill switch, rate limiting, loop-prevensjon, STDB-skriving +- **Maskinrommet:** Auth, kill switch, rate limiting, loop-prevensjon - **synops-respond:** Kontekst-henting, prompt-bygging, claude-kall, PG-skriving Latens: ~3-5 sekunder fra melding til svar. diff --git a/docs/infra/jobbkø.md b/docs/infra/jobbkø.md index 8a1dc08..bf4081b 100644 --- a/docs/infra/jobbkø.md +++ b/docs/infra/jobbkø.md @@ -53,17 +53,17 @@ CLI-verktøy (`Command::new("synops-X")`) som gjør selve jobben. - Payload-parsing og validering - Sikkerhetskontroller (kill switch, rate limiting, loop-prevensjon for agent) - Voice/model-oppslag fra node metadata (orchestrator-logikk) -- STDB-synk etter CLI fullført (sanntidsvisning) +- PG NOTIFY etter CLI fullført (sanntidsvisning via WebSocket) - Jobbstatus-håndtering (complete/fail/retry) | Jobbtype | CLI-verktøy | Maskinrommet beholder | |---|---|---| | `whisper_transcribe` | `synops-transcribe` | — | -| `agent_respond` | `synops-respond` | Kill switch, rate limit, loop-prevensjon, STDB-synk | +| `agent_respond` | `synops-respond` | Kill switch, rate limit, loop-prevensjon | | `suggest_edges` | `synops-suggest-edges` | — | | `summarize_communication` | `synops-summarize` | — | -| `tts_generate` | `synops-tts` | Voice-oppslag fra node metadata, STDB-synk | -| `audio_process` | `synops-audio` | STDB-synk | +| `tts_generate` | `synops-tts` | Voice-oppslag fra node metadata | +| `audio_process` | `synops-audio` | — | | `render_article` | `synops-render` | — | | `render_index` | `synops-render` | — | | `ai_process` | *(inline — mangler CLI)* | Alt (planlagt migrasjon) | @@ -223,7 +223,7 @@ tabell med alle felter, retry/avbryt-knapper. Poller hvert 5. sekund. **Migrasjon:** `014_resource_governor.sql` — `job_priority_rules` + `disk_status_log` -- Valgfritt: SpacetimeDB-event ved statusendring slik at UI kan vise fremdrift i sanntid (f.eks. "Transkriberer... 2/3 forsøk") +- Jobbstatus-endringer propageres til frontend via PG NOTIFY → WebSocket (f.eks. "Transkriberer... 2/3 forsøk") ## 8. Instruks for Claude Code - Én binær: `sidelinja-worker`. Én Rust-crate med polling-loop + handler-dispatch diff --git a/docs/infra/robusthet.md b/docs/infra/robusthet.md index ef6d665..c93bb87 100644 --- a/docs/infra/robusthet.md +++ b/docs/infra/robusthet.md @@ -33,7 +33,6 @@ kan den prosessere ubesvarte meldinger. Portvokteren eksponerer `/health` som sjekker: - PG-tilkobling -- STDB-tilkobling - LiteLLM-tilgjengelighet - Disk-status @@ -107,7 +106,7 @@ En `reader` kan spørre `@bot` om informasjon, men ikke trigge | Alle LLM-er nede | synops-respond exit != 0 | Statisk "utilgjengelig" + work_item | Vet at meldingen er mottatt | | Portvokteren nede | Systemd healthcheck → restart | CLI fungerer for Claude Code | Web-brukere venter, terminal funker | | PG nede | Connection refused | Alt stopper | Eneste reelle SPOF | -| STDB nede | Reconnect-loop | PG-fallback for lesing, skriving bufres | Sanntid borte, data trygt | +| WebSocket nede | Portvokteren restarter | Frontend rekobler automatisk | Sanntid midlertidig borte, data trygt | ## PG som eneste SPOF @@ -118,7 +117,6 @@ gjenoppbygging fra backup er veldokumentert Mitigering: - Automatisk PG-dump (se oppgave 12.2) -- STDB kan gjenoppbygges fra PG - `synops-snapshot` sikrer at docs-fallback finnes i repo - CAS-filer er uavhengig av PG (filsystem) diff --git a/docs/infra/synkronisering.md b/docs/infra/synkronisering.md index cb4011f..87b3519 100644 --- a/docs/infra/synkronisering.md +++ b/docs/infra/synkronisering.md @@ -1,5 +1,10 @@ # Synkronisering: SpacetimeDB ↔ PostgreSQL +> **UTGÅTT (mars 2026).** SpacetimeDB er fjernet fra prosjektet. +> Sanntid leveres nå via PG LISTEN/NOTIFY + WebSocket i portvokteren. +> Se `docs/infra/api_grensesnitt.md` for gjeldende arkitektur. +> Dokumentet beholdes som historisk referanse. + ## Konsept SpacetimeDB holder hele grafen (alle noder og edges) i minne. diff --git a/docs/primitiver/traits.md b/docs/primitiver/traits.md index 18c4133..12fe8c8 100644 --- a/docs/primitiver/traits.md +++ b/docs/primitiver/traits.md @@ -78,7 +78,7 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean |---|---|---| | `editor` | TipTap med presets (longform, note, chat, code) | Validering av dokumentstruktur | | `versioning` | Revisjonshistorikk, diff, rollback-knapp | Snapshot ved signifikante endringer | -| `collaboration` | Samtidig redigering, markører, inline-kommentarer | OT/CRDT via STDB | +| `collaboration` | Samtidig redigering, markører, inline-kommentarer | OT/CRDT via WebSocket | | `translation` | Språkvelger, side-ved-side-visning | AI-oversettelse via jobbkø | | `templates` | Mal-velger ved ny node | Mal-noder i samlingen | @@ -111,7 +111,7 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean | Trait | Frontend | Backend | |---|---|---| -| `chat` | Sanntidsmeldinger, tråder, reaksjoner | STDB-synk, TTL-håndtering | +| `chat` | Sanntidsmeldinger, tråder, reaksjoner | WebSocket-synk, TTL-håndtering | | `forum` | Trådet diskusjon, sortering (nyeste/populære/ubesvarte) | Tråd-indeksering | | `comments` | Kommentarfelt under publisert innhold | Moderasjonskø, evt. anonym input | | `guest_input` | Gjeste-lenke-generering, svar-oversikt | Token-generering, CAS-upload, rate limiting | diff --git a/docs/proposals/README.md b/docs/proposals/README.md index 5cf66f8..6e562c7 100644 --- a/docs/proposals/README.md +++ b/docs/proposals/README.md @@ -22,7 +22,7 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe | [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 | +| [Live Audience Q&A](live_audience_qa.md) | Middels | Høy | Valgomat, LiveKit, WebSocket | | [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 | @@ -48,7 +48,7 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe | [Flow Meter](flow_meter.md) | Lav | Middels | Storyboard | | [Emotion Tags](emotion_tags.md) | Lav | Middels | Meldingsboks, kanban, storyboard | | **Samarbeid** | | | | -| [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | SpacetimeDB, Svelte | +| [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | WebSocket, Svelte | | [Card Heat Map](card_heat_map.md) | Lav | Middels | Meldingsboks, kanban/storyboard | **Forfremmet til feature:** [Meldingsboks](../features/meldingsboks.md) — universell diskusjonsprimitiv. [Artikkel-publisering](artikkel_publisering.md) → Fase 14 / `docs/concepts/publisering.md`. [Tekst-primitiv](tekst_primitiv.md) — realisert i nodearkitekturen. diff --git a/docs/proposals/komponerbare_sider.md b/docs/proposals/komponerbare_sider.md index d1ffeb0..449a228 100644 --- a/docs/proposals/komponerbare_sider.md +++ b/docs/proposals/komponerbare_sider.md @@ -1,5 +1,12 @@ # Komponerbare sider (Dashboard-komposisjon) +> **Superseded:** Konseptet er videreført og utvidet i retningen +> "Arbeidsflaten" (`docs/retninger/arbeidsflaten.md`). Den spatial +> workspace-modellen med frie verktøy-paneler og drag-and-drop erstatter +> grid-baserte dashboard-komposisjoner. Dokumentet er bevart som +> historisk referanse — mye av tenkningen rundt blokker, resize og +> maximize er videreført i arbeidsflate-retningen. + ## Idé Brukere ser ferdige sider (Redaksjonen, Studioet, etc.), men admin kan komponere egne sider fra tilgjengelige byggeklosser — chat, kanban, statistikk, graf-visning, whiteboard, osv. diff --git a/docs/retninger/README.md b/docs/retninger/README.md index 45b9d98..c04be1a 100644 --- a/docs/retninger/README.md +++ b/docs/retninger/README.md @@ -26,7 +26,7 @@ andre dokumenter. En retning kan også forkastes eller parkeres. | [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. | | [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. | | [Noder er sentrum](bruker_ikke_workspace.md) | **Besluttet** | Alt er noder (brukere, team, innhold). Edges definerer relasjoner og tilgang. Materialisert tilgangsmatrise for RLS. | -| [Datalaget](datalaget.md) | **Revidert** | PG er eneste datakilde. Sanntid via LISTEN/NOTIFY + WebSocket. SpacetimeDB fases ut. CAS for binærdata, AGE ved behov. | +| [Datalaget](datalaget.md) | **Revidert** | PG er eneste datakilde. Sanntid via LISTEN/NOTIFY + WebSocket. CAS for binærdata, AGE ved behov. | | [Arbeidsflaten](arbeidsflaten.md) | **Besluttet** | Spatial canvas med verktøy-paneler. Drag-and-drop skaper nye noder med edges. | | [Unix-filosofi](unix_filosofi.md) | **Besluttet** | Maskinrommet orkestrerer, CLI-verktøy gjør jobben. Claude deler verktøykasse. | diff --git a/docs/retninger/arbeidsflaten.md b/docs/retninger/arbeidsflaten.md index c9e6a0b..a18abcc 100644 --- a/docs/retninger/arbeidsflaten.md +++ b/docs/retninger/arbeidsflaten.md @@ -152,7 +152,7 @@ spesifikk samling. - **Navigasjon:** Tilgjengelig via "Min flate"-knapp på mottak, og i kontekst-velger-dropdown på samlingssider - **Provisjonering:** Backend oppretter workspace-node + owner-edge ved - første forespørsel. STDB for instant synk, async PG for persistens. + første forespørsel. PG-skriving med NOTIFY for sanntidsoppdatering. ## `source_material`-edge @@ -185,8 +185,9 @@ Gir artikler sporbar lineage: du kan alltid se *hvor* materialet kom fra. - **[Noder er sentrum](bruker_ikke_workspace.md):** Verktøy er visninger av grafen. Arbeidsflaten er brukerens personlige arrangement av disse visningene. -- **[Datalaget](datalaget.md):** SpacetimeDB driver sanntidsoppdatering - mellom paneler. Drag-and-drop oppretter noder/edges som synkes instant. +- **[Datalaget](datalaget.md):** PG LISTEN/NOTIFY + WebSocket driver + sanntidsoppdatering mellom paneler. Drag-and-drop oppretter noder/edges + som propageres via sanntidsstrømmen. ## Hva ville vært annerledes diff --git a/docs/retninger/datalaget.md b/docs/retninger/datalaget.md index ea3c725..03aa3e4 100644 --- a/docs/retninger/datalaget.md +++ b/docs/retninger/datalaget.md @@ -1,11 +1,10 @@ # Datalaget -**Status: Besluttet. Revidert mars 2026 — SpacetimeDB fases ut.** +**Status: Besluttet. Revidert mars 2026 — SpacetimeDB fjernet.** > PostgreSQL er eneste datakilde. Sanntid via PG `LISTEN/NOTIFY` > og WebSocket i portvokteren. CAS lagrer binærdata. > Apache AGE legges til ved behov for Cypher-traverseringer. -> SpacetimeDB fases ut — se migrasjonsplan. ## Lagmodell @@ -105,61 +104,25 @@ Portvokteren holder: - Map av node_id → synlige noder (fra node_access) - Ved NOTIFY: filtrer på tilgang, push til relevante klienter -Enklere enn STDB-synk: én retning (PG → klient), ingen -reducer-logikk, ingen konsistensproblemer. +Én retning (PG → klient), ingen reducer-logikk, ingen +konsistensproblemer. -## Hvorfor SpacetimeDB fases ut +## Historikk: SpacetimeDB (fjernet mars 2026) -SpacetimeDB var et godt eksperiment. Det løste sanntid elegant -i prototype-fasen. Men for produksjon på én server: +SpacetimeDB ble brukt som sanntidslag i v1/prototype. Det løste +sanntid elegant, men for produksjon på én server skapte det +unødvendig kompleksitet: -- **Synk-kompleksitet.** PG ↔ STDB synk er en egen feilkategori. - Erfaringsdocs (adapter_moenster.md, spacetimedb_integrasjon.md) - dokumenterer smerten. -- **Dobbelt vedlikehold.** STDB-modul med reducers må holdes i - synk med PG-skjema. Endring i nodes-tabellen → to steder. -- **Ekstra SPOF.** Enda en tjeneste å overvåke, restarte, debugge. +- **Synk-kompleksitet.** PG ↔ STDB synk var en egen feilkategori. +- **Dobbelt vedlikehold.** STDB-modul med reducers måtte holdes i + synk med PG-skjema. +- **Ekstra SPOF.** Enda en tjeneste å overvåke og restarte. - **Unødvendig for skalaen.** PG LISTEN/NOTIFY + WebSocket gir - ~5ms latency. STDB ga ~0.01ms. Forskjellen er umerkelig for - brukere. -- **CLI-verktøy forenkles.** Bare PG-tilkobling, ingen STDB-klient. + ~5ms latency — umerkelig forskjell for brukere. -## Migrasjonsplan: STDB → PG LISTEN/NOTIFY - -### Fase M1: WebSocket-lag i portvokteren ✅ -Implementert LISTEN/NOTIFY-lytter og WebSocket-endepunkt i -portvokteren. PG-triggers for nodes, edges og access. -Frontend koblet til begge (STDB + nytt WS) i parallell. - -### Fase M2: Frontend-migrering ✅ -Frontend bruker nå kun portvokterens WebSocket. SpacetimeDB-klient -fjernet. Reactive stores oppdateres direkte fra WS-meldinger. -Berikede events: portvokteren henter full raddata fra PG etter -NOTIFY (ikke bare ID) slik at stores kan oppdateres uten ekstra -API-kall. Mixer-kanaler migrert fra STDB til PG-tabell med -tilhørende NOTIFY-trigger og HTTP API-endepunkter. - -### Fase M3: Fjern skrivestien til STDB ✅ -Portvokteren skriver kun til PG. STDB-skrivestien er fjernet. -Alle intensjoner (create/update/delete node/edge) skriver -synkront til PG. NOTIFY-triggere er eneste push-mekanisme. -Warmup (PG→STDB) og STDB-monitor er fjernet. StdbClient er -fjernet fra AppState. Job-handlere (agent, audio, tts, ai_process) -synker ikke lenger til STDB — PG NOTIFY dekker sanntid. - -### Fase M4: Fjern STDB -- Stopp SpacetimeDB Docker-container -- Fjern STDB-modul (spacetimedb/) -- Fjern STDB-klient fra portvokteren -- Fjern STDB-avhengigheter fra frontend -- Fjern synkroniserings-kode -- Oppdater docs (synkronisering.md → arkiver) -- Oppdater CLAUDE.md - -### Fase M5: Opprydding -- Slett erfaringsdocs som kun gjelder STDB -- Oppdater alle docs-referanser til STDB -- Fjern Docker-konfig for SpacetimeDB +SpacetimeDB ble faset ut i fire steg: WebSocket-lag, frontend- +migrering, fjern skrivestien, fjern alt. Se erfaringsdocs for +lærdommer: `docs/erfaringer/spacetimedb_integrasjon.md`. ## Forhold til andre retninger diff --git a/docs/retninger/maskinrommet.md b/docs/retninger/maskinrommet.md index 25efdf5..e03f2a4 100644 --- a/docs/retninger/maskinrommet.md +++ b/docs/retninger/maskinrommet.md @@ -32,7 +32,7 @@ Lever resultat i riktig modalitet til riktig mottaker: - Lyd (TTS-opplesning, lydstream) - Video/bilde (stream, thumbnail) - Strukturert data (noder, edges tilbake i grafen) -- Push (SpacetimeDB-reducer) +- Push (WebSocket via PG LISTEN/NOTIFY) ## Maskinrommet eier all skriving @@ -41,9 +41,9 @@ Frontend sender intensjoner. Maskinrommet utfører. ``` Frontend: "legg til Trond i møtet" → Maskinrommet validerer - → Skriver edge til SpacetimeDB (instant) + → Skriver edge til PG → Oppdaterer tilgangsmatrise - → Persisterer til PG (asynk) + → PG NOTIFY → WebSocket → frontend oppdateres i sanntid → Reagerer på konsekvensene (koble inn LiveKit, starte transkripsjon) ``` @@ -51,8 +51,7 @@ Alt i én operasjon. Maskinrommet er ikke reaktivt i en pub/sub- forstand — det orkestrerer hele sekvensen. Enklere å forstå, enklere å debugge. -Skrivestien: validering → SpacetimeDB (instant) → PG (asynk). -Se [synkronisering](../infra/synkronisering.md). +Skrivestien: validering → PG → NOTIFY → WebSocket (sanntid). ## Edge-drevet ressursorkestrering @@ -185,13 +184,13 @@ Compute-separasjon er en konfigurasjon, ikke en arkitekturendring. ## Evolusjon: Maskinrommet → Portvokteren Maskinrommet ble bygget som en monolitt — auth, validering, -prosessering, jobbkø, STDB-synk i én binær. Med unix-filosofi- +prosessering, jobbkø i én binær. Med unix-filosofi- retningen (se `docs/retninger/unix_filosofi.md`) flyttes all prosessering til CLI-verktøy. Det som blir igjen er: 1. **Auth** — JWT-validering, "hvem er denne requesten?" 2. **HTTP-ruting** — frontend → riktig CLI-verktøy -3. **STDB-synk** — push PG-endringer til SpacetimeDB +3. **Sanntid** — PG LISTEN/NOTIFY → WebSocket til frontend 4. **Jobbkø-dispatch** — poll PG, spawn CLI-verktøy Dette er en **portvokter**, ikke et maskinrom. Når uttynningen er @@ -219,5 +218,5 @@ Maskinrommet er infrastrukturen *under* de tre primitivene i - [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet eier tilgangsmatrise-oppdatering -- [Datalaget](datalaget.md) — maskinrommet skriver SpacetimeDB - først, PG asynk +- [Datalaget](datalaget.md) — maskinrommet skriver PG, + sanntid via LISTEN/NOTIFY + WebSocket diff --git a/docs/retninger/rom_ikke_forum.md b/docs/retninger/rom_ikke_forum.md index 6134871..0ce7102 100644 --- a/docs/retninger/rom_ikke_forum.md +++ b/docs/retninger/rom_ikke_forum.md @@ -6,15 +6,14 @@ ## Observasjoner Sidelinja har vokst organisk. Vi har bygget chat, kanban, kalender, notater, -kunnskapsgraf — hver som sin feature, hver med sin spec. SpacetimeDB ble lagt til -for sanntid, men arkitekturen er fortsatt "PostgreSQL-app med sanntidskrydder." +kunnskapsgraf — hver som sin feature, hver med sin spec. Sanntid ble lagt til, +men arkitekturen var fortsatt "PostgreSQL-app med sanntidskrydder." Resultatet: - **Forum-følelsen.** Ting er organisert i tråder, kort, lister. Brukeren navigerer mellom sider. Det føles som et tradisjonelt verktøy med litt polish. -- **Databasespenning.** PG og SpacetimeDB har et komplisert eierforhold. - SpacetimeDB-loven løser grensesnittet, men ikke det underliggende spørsmålet: - hva *er* primæropplevelsen? +- **Arkitekturspenning.** Spørsmålet om hva *primæropplevelsen* er + — sanntid eller tradisjonell webapp — forblir åpent. - **Feature-fragmentering.** Chat, kanban, whiteboard, notater — hver lever i sin boks. "Universell overføring" og "meldingsboks" prøver å lime dem sammen, men utgangspunktet er fortsatt separate primitiver. @@ -34,34 +33,22 @@ Forskjellen er subtil men fundamental: ## Hva ville vært annerledes? -### SpacetimeDB som verden, PG som arkiv -SpacetimeDB er ikke en "sanntidskache foran PG" — det er verdenen brukerne -lever i. PG er arkivet som husker hva som har skjedd. +### Sanntidslaget og arkivet +PG er eneste datakilde. Sanntid leveres via PG LISTEN/NOTIFY + WebSocket +i portvokteren. Arkivet og sanntidslaget er ikke to separate systemer — +PG er begge deler. -Rollene blir klare og adskilte: -- **SpacetimeDB** = sanntidslaget. Aktivt samarbeid, live interaksjon, - ting som skjer *nå*. -- **PostgreSQL** = arkivet. Alt som noensinne har skjedd. Søk, historikk, - statistikk, revisjon. +Ikke alt trenger sanntid. En kunnskapsgraf-utforsker, et søk i gamle +episoder, en statistikkside, en offentlig publisert artikkel — disse +bruker tradisjonelle API-kall mot PG. Sanntidsstrømmen er for det som +er levende: chat, whiteboard, live redigering. -Men viktig: sanntidslaget er *bare* et sanntidslag. Ikke alt trenger -sanntid. En kunnskapsgraf-utforsker, et søk i gamle episoder, en -statistikkside, en offentlig publisert artikkel — disse snakker rett med -PG-arkivet som tradisjonelle nettsider. De trenger ikke gå gjennom -SpacetimeDB. Begge lagene leser og skriver til det samme arkivet. - -Dermed har vi to parallelle overflater: -- **Sanntidsopplevelsen** (via SpacetimeDB) — for alt som er levende, - aktivt, samarbeidende. Chat, whiteboard, live redigering. -- **Tradisjonelt lag** (rett mot PG) — for alt som er retrospektivt, +To overflater, én datakilde: +- **Sanntidsopplevelsen** (via WebSocket) — for alt som er levende, + aktivt, samarbeidende. +- **Tradisjonelt lag** (API-kall mot PG) — for alt som er retrospektivt, utforskende, statisk. Arkiv, søk, publisering, statistikk. -Dataflyt mellom dem: ting som oppstår i sanntidslaget synkes til PG. -Ting i PG kan løftes inn i sanntidslaget når de blir aktive igjen. -Men det er ingen eierskapskonflikt — de to lagene har fundamentalt -forskjellige roller. Det er ikke to konkurrerende sannheter, det er -*nåtid* og *arkiv*, med to overflater som passer til hver sin rolle. - ### Rommet som primitiv, ikke siden I dag navigerer brukeren mellom `/chat`, `/kanban`, `/kalender`. I "rom"-modellen er brukeren alltid *et sted*, og funksjonalitet er lag som kan slås av og på: @@ -121,22 +108,17 @@ systemet tar vare på det riktig sted basert på kontekst og synlighet. Input-metode (tekst, voice, tegning) og synlighet (privat, delt, publisert) er ortogonale egenskaper. Ingen av dem bør diktere *hva* innholdet blir. -### SpacetimeDB som naturlig motor -SpacetimeDB tenker "dette eksisterer i verden nå" — ikke "lagre dette i -riktig tabell." Å trylle frem et whiteboard er naturlig i en verden-modell: -det er bare et nytt objekt med en tilstand. I PG-modellen må du opprette -rader, definere relasjoner, sette opp persistens. SpacetimeDB låner seg -til flytende, formløs interaksjon på en måte PG ikke gjør. - -PG briljerer i rollen som arkiv: relasjonelle spørringer over historikk, -fulltekstsøk, pgvector for semantisk søk, aggregeringer og statistikk. -Når du spør "hva snakket vi om i mars?" er det PG som svarer. Når du -spør "hva skjer nå?" er det SpacetimeDB. +### PG som eneste kilde, WebSocket som sanntidslag +PG briljerer som både arkiv og sanntidskilde: relasjonelle spørringer +over historikk, fulltekstsøk, pgvector for semantisk søk, aggregeringer +og statistikk. LISTEN/NOTIFY + WebSocket gir sanntidsoppdatering uten +et ekstra system. Å trylle frem et whiteboard er bare å opprette en node +— WebSocket-strømmen sørger for at alle ser det umiddelbart. ## Spenninger og åpne spørsmål - **Ytelse.** En alltid-på sanntidsopplevelse krever mer av både klient og - server enn tradisjonelle sideinnlastinger. Er SpacetimeDB klar for dette? + server enn tradisjonelle sideinnlastinger. - **Kompleksitet.** "Alt er et rom" høres elegant ut, men kan bli kaotisk. Hvordan unngår vi at det blir uoversiktlig? - **Discovery vs fokus.** "Alt kan bli hva som helst" er kraftig, men kan @@ -146,9 +128,9 @@ spør "hva skjer nå?" er det SpacetimeDB. - **~~Gradvis overgang.~~** *(Løst — se "Innebygd utviklingsstrategi" nedenfor.)* - **Solo-bruk.** Mye av verdien i "rom" kommer fra å være der sammen. Hvordan føles det for én person som jobber alene? -- **Er det vi allerede har?** Meldingsboks-konseptet, universell overføring og - SpacetimeDB-loven peker allerede i denne retningen. Kanskje dette ikke er en - ny retning, men en artikulering av det vi ubevisst har bygget mot? +- **Er det vi allerede har?** Meldingsboks-konseptet og universell overføring + peker allerede i denne retningen. Kanskje dette ikke er en ny retning, + men en artikulering av det vi ubevisst har bygget mot? - **Inspirasjon.** Spillverdener (MMO-lobbyer, shared spaces), Figma (alle i samme canvas), tldraw, Gather.town. Hva kan vi lære fra disse? @@ -157,31 +139,24 @@ spør "hva skjer nå?" er det SpacetimeDB. To-lags-modellen gir en viktig implikasjon for utviklingen: **innebygd fallback og to veier inn til alt.** -Ny funksjonalitet kan alltid starte i det tradisjonelle laget — rett mot PG, -vanlig request/response, kjent terreng. Den fungerer med en gang. Når det -senere gir verdi (samarbeid, sanntid, live interaksjon) kan den løftes inn -i sanntidslaget. Men den trenger ikke det for å være nyttig. +Ny funksjonalitet kan alltid starte som tradisjonell request/response mot PG. +Sanntidsoppdatering kommer gratis via LISTEN/NOTIFY + WebSocket — ingen +separat "løfting" til et sanntidslag nødvendig. Dette betyr: - **Ingenting vi har bygget er bortkastet.** Eksisterende PG-baserte features - er ikke "gammel arkitektur" — de er det tradisjonelle laget, og det er et - fullverdig lag. -- **Ingen stor omskriving.** Retningen er ikke en migrasjon med en frist. Den - er en utviklingsstrategi: bygg tradisjonelt, løft til sanntid ved behov. -- **Risikoen er lav.** Hvis SpacetimeDB skuffer, har vi fortsatt et komplett - tradisjonelt system. Hvis det leverer, får ting gradvis en rikere opplevelse. -- **Primitivene er nøkkelen.** Så lenge meldingsboksen og kunnskapsgrafen er - fleksible nok, kan begge lag bruke dem. Arkitekturen holder uavhengig av - hvilket lag en feature lever i. + er fullverdige og har sanntid via WebSocket. +- **Ingen stor omskriving.** Retningen er en utviklingsstrategi: bygg mot PG, + sanntid følger automatisk. +- **Risikoen er lav.** PG er stabil og velprøvd. +- **Primitivene er nøkkelen.** Så lenge noder, edges og kunnskapsgrafen er + fleksible nok, holder arkitekturen uavhengig av kontekst. ## Kritisk vurdering -### SpacetimeDB er en ung teknologi -Å lene seg tungt på SpacetimeDB er et veddemål. Hvis prosjektet stagnerer, -endrer API, eller har skaleringstak vi ikke ser ennå — er vi eksponert. -*Mildnet* av at det tradisjonelle laget mot PG alltid finnes som fallback: -SpacetimeDB er ikke alt-eller-ingenting, men et lag for det som trenger -sanntid. Resten lever trygt mot PG uansett. +### Sanntidslaget er PG-basert +SpacetimeDB ble fjernet (mars 2026). Sanntid leveres nå via PG +LISTEN/NOTIFY + WebSocket. Én datakilde, ingen synkroniseringskompleksitet. ### "Formløs input" er vanskelig i praksis Det høres elegant ut, men noen må bestemme hva noe *blir*. AI? Brukeren @@ -195,20 +170,17 @@ hverdagen. En samtale fra i går som fortsatt er relevant — er den "nå" eller "arkiv"? Et kanban-kort som har stått stille i to uker? Vi trenger regler for når ting flyttes mellom lagene, og de reglene kan bli like komplekse som -SpacetimeDB-loven de erstatter. "Tid og arkiv" er renere *i prinsippet*, +reglene de erstatter. "Tid og arkiv" er renere *i prinsippet*, men i praksis er "aktiv" et spektrum, ikke en binær tilstand. ### Solo-bruk er underadressert Vegard er primærbruker. "Rom"-konseptet henter mye av sin kraft fra tilstedeværelse og samarbeid. For én person som produserer podcast er det en reell risiko at sanntidslaget er overhead sammenlignet med et godt -organisert tradisjonelt verktøy. *Mildnet* av to-lags-modellen: solo-bruk -kan lene seg tyngre på det tradisjonelle PG-laget, og sanntidslaget -aktiveres når det faktisk gir verdi (live innspilling, samarbeid). +organisert tradisjonelt verktøy. *Mildnet* av at PG-basert sanntid har null overhead — WebSocket +leverer oppdateringer uten ekstra lag å vedlikeholde. ### Omfanget -*Betydelig mildnet* av utviklingsstrategien ovenfor. Retningen krever ikke -en omskriving — den er kompatibel med inkrementell utvikling. Den -gjenværende risikoen er mer subtil: at vi bruker mental energi på å -vurdere "bør dette være sanntid?" for hver feature i stedet for å bare -bygge. Pragmatisk default bør være: bygg tradisjonelt, løft senere. +*Betydelig mildnet* av at sanntid nå er innebygd i arkitekturen via +PG LISTEN/NOTIFY + WebSocket. Ingen vurdering "bør dette være sanntid?" +nødvendig — alt som skrives til PG propageres automatisk. diff --git a/docs/retninger/status_quo.md b/docs/retninger/status_quo.md index 90bf722..2e1825d 100644 --- a/docs/retninger/status_quo.md +++ b/docs/retninger/status_quo.md @@ -44,17 +44,17 @@ er form-basert og eksplisitt. Brukeren "administrerer innhold" mer enn de "jobber sammen i et miljø." ### Sanntid som tillegg -SpacetimeDB er lagt til for å gi sanntidsoppdatering, men arkitekturen -er PostgreSQL-først. Sanntid er noe som *skjer med* tradisjonelle -operasjoner, ikke noe som er *grunnlaget* for opplevelsen. +Sanntid (opprinnelig via SpacetimeDB, nå PG LISTEN/NOTIFY + WebSocket) +er lagt til, men arkitekturen er PostgreSQL-først. Sanntid er noe som +*skjer med* tradisjonelle operasjoner, ikke noe som er *grunnlaget* +for opplevelsen. ## Spenninger -### To sannhetskilder -PG og SpacetimeDB har et komplisert forhold. SpacetimeDB-loven definerer -klare regler for hvem som eier hva, men selve eksistensen av loven vitner -om en arkitektonisk spenning: vi har to systemer som begge vil være -primærkilde, og vi bruker konvensjoner for å holde dem fra å kollidere. +### To sannhetskilder (historisk, nå løst) +PG og SpacetimeDB hadde et komplisert forhold. Denne spenningen er +løst ved fjerning av SpacetimeDB (mars 2026) — PG er nå eneste +datakilde, med sanntid via LISTEN/NOTIFY + WebSocket. ### Ambisiøs bunn, forsiktig topp Meldingsboksen og kunnskapsgrafen åpner for opplevelser vi ikke leverer diff --git a/docs/retninger/universell_input.md b/docs/retninger/universell_input.md index 1c401b8..559321b 100644 --- a/docs/retninger/universell_input.md +++ b/docs/retninger/universell_input.md @@ -24,7 +24,7 @@ som knyttes til resultatet. ### Én pipeline All input går gjennom samme tekniske pipeline: -maskinrommet → SpacetimeDB (instant) → PG (asynk). +maskinrommet → PG → NOTIFY → WebSocket (sanntid). Konteksten bestemmer routing, ikke en teknisk modus: @@ -260,8 +260,8 @@ Alle leser fra samme graf. Ingen har "sin egen" data. - [Noder er sentrum](bruker_ikke_workspace.md) — visibility, tilgangsmatrise, aliaser -- [Datalaget](datalaget.md) — SpacetimeDB holder hele grafen, - PG persisterer asynkront +- [Datalaget](datalaget.md) — PG er eneste datakilde, + sanntid via LISTEN/NOTIFY + WebSocket - [Maskinrommet](maskinrommet.md) — validering, routing, CAS, tunge jobber (Whisper, TTS, AI) - [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden diff --git a/docs/retninger/unix_filosofi.md b/docs/retninger/unix_filosofi.md index fb56ddf..d525743 100644 --- a/docs/retninger/unix_filosofi.md +++ b/docs/retninger/unix_filosofi.md @@ -52,7 +52,7 @@ Maskinrommet (Rust) │ └── ... │ ▼ direkte -PG, STDB, CAS +PG, CAS ``` Claude har tilgang til hele `tools/`-katalogen og kan kjøre alt direkte: @@ -109,7 +109,7 @@ Ikke en big-bang refaktor. Gradvis utbryting: Kjernen som ikke bør brytes ut: - Auth-middleware (JWT-validering, node-oppslag) -- Intentions (validering, STDB+PG-skriving, edge-logikk) +- Intentions (validering, PG-skriving, edge-logikk) - Jobbkø (polling, retry, dead letter) - Tilgangskontroll (node_access, recompute_access) - Health-endepunkt diff --git a/docs/setup/lokal.md b/docs/setup/lokal.md index d16f661..65a53f3 100644 --- a/docs/setup/lokal.md +++ b/docs/setup/lokal.md @@ -5,7 +5,7 @@ > produksjonsserveren via Claude Code. Dokumentet beholdes som referanse > i tilfelle lokalt utviklingsmiljø gjeninnføres. -Det lokale miljøet var et **utviklingsmiljø for kode**. Frontend (SvelteKit) ble kjørt lokalt med HMR, Rust ble bygd lokalt. Alle tjenester (PG, SpacetimeDB, AI Gateway, etc.) kjørte på produksjonsserveren — ingen lokal Docker-replika. +Det lokale miljøet var et **utviklingsmiljø for kode**. Frontend (SvelteKit) ble kjørt lokalt med HMR, Rust ble bygd lokalt. Alle tjenester (PG, AI Gateway, etc.) kjørte på produksjonsserveren — ingen lokal Docker-replika. ## Hva som gjøres hvor diff --git a/docs/setup/produksjon.md b/docs/setup/produksjon.md index 790d391..3139005 100644 --- a/docs/setup/produksjon.md +++ b/docs/setup/produksjon.md @@ -63,7 +63,7 @@ newgrp docker ```bash sudo mkdir -p /srv/synops/{config,data,media,logs} sudo mkdir -p /srv/synops/config/{caddy,authentik} -sudo mkdir -p /srv/synops/data/{postgres,spacetimedb,forgejo,authentik} +sudo mkdir -p /srv/synops/data/{postgres,forgejo,authentik} sudo mkdir -p /srv/synops/data/whisper-models sudo mkdir -p /srv/synops/media/podcast sudo mkdir -p /srv/synops/logs/caddy @@ -80,7 +80,6 @@ Resultat: │ └── authentik/ ├── data/ │ ├── postgres/ -│ ├── spacetimedb/ │ ├── forgejo/ │ ├── whisper-models/ │ └── authentik/ @@ -128,11 +127,6 @@ AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/ AUTHENTIK_CLIENT_ID= AUTHENTIK_CLIENT_SECRET= -# === SpacetimeDB === -SPACETIMEDB_URL=http://spacetimedb:3000 -SPACETIMEDB_DATABASE=synops -SPACETIMEDB_TOKEN= - # === Whisper (STT) === # Modell lastes ned automatisk ved oppstart. large-v3 gir best norsk kvalitet. # Ved GPU: bytt image til fedirz/faster-whisper-server:latest-cuda og WHISPER__COMPUTE_TYPE=float16 @@ -156,8 +150,7 @@ Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i 5. **Forgejo:** Start med Authentik som OAuth2-provider, opprett organisasjon og repo ### Lag B: Sanntid (krever nettverk) -6. **SpacetimeDB:** Start, verifiser tilkobling -7. **LiveKit:** Start, verifiser at WebRTC fungerer +6. **LiveKit:** Start, verifiser at WebRTC fungerer ### Lag C: Applikasjon (krever alt over) 8. **SvelteKit:** Bygg og start container, verifiser at frontenden laster @@ -187,7 +180,6 @@ services: postgres: # data:/srv/synops/data/postgres authentik: # SSO for alle domener, på auth.sidelinja.org forgejo: # data:/srv/synops/data/forgejo, på git.sidelinja.org - spacetimedb: # data:/srv/synops/data/spacetimedb maskinrommet: # Rust/axum API, intern port 3100, proxyet via Caddy livekit: # Intern port, proxyet via Caddy sveltekit: # Intern port, proxyet via Caddy @@ -205,11 +197,6 @@ auth.sidelinja.org { # === Sidelinja (hovedapplikasjon) === sidelinja.org { - # SpacetimeDB (WebSocket) — handle_path stripper prefix - handle_path /spacetime/* { - reverse_proxy spacetimedb:3000 - } - # LiveKit signaling (WebSocket upgrade) handle_path /livekit/* { reverse_proxy livekit:7880 @@ -401,7 +388,6 @@ echo "$(date): Off-site backup ferdig" >> /srv/synops/logs/backup-offsite.log - **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git - **Logger** — rulleres med logrotate, arkiveres separat ved behov - **Whisper-modeller** — re-download fra HuggingFace -- **SpacetimeDB** — sanntidsdata synkes til PG, in-memory state er flyktig ### 11.8 Restore-prosedyre ```bash @@ -454,8 +440,7 @@ sudo journalctl -u maskinrommet -f ``` Env-filen (`/tmp/maskinrommet.env`) genereres automatisk av -`scripts/maskinrommet-env.sh` med Docker container-IPs for PG og STDB. -Ved oppstart laster maskinrommet hele grafen fra PG inn i STDB (warmup). +`scripts/maskinrommet-env.sh` med Docker container-IPs for PG. Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`. Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes @@ -470,7 +455,7 @@ ikke i produksjon. - [ ] Git push til Forgejo fungerer ### Lag B-C -- [x] `https://api.sidelinja.org/health` returnerer `{"status":"ok"}` med PG og STDB tilkoblet (verifisert 2026-03-17) +- [x] `https://api.sidelinja.org/health` returnerer `{"status":"ok"}` med PG tilkoblet (verifisert 2026-03-17) - [x] `https://api.sidelinja.org/me` returnerer 401 uten token (verifisert 2026-03-17) - [x] `https://sidelinja.org` laster SvelteKit-appen (deployet 2025-03-15) - [x] `https://sidelinja.org/api/health` returnerer 200 @@ -478,6 +463,5 @@ ikke i produksjon. - [x] Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15) - [ ] `https://synops.no` viser placeholder - [ ] `https://vegard.info` svarer -- [ ] SpacetimeDB: WebSocket-tilkobling fra nettleser fungerer - [x] LiveKit: Container kjører, signaling proxyet via Caddy (verifisert 2026-03-17) - [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes` diff --git a/frontend/.env.example b/frontend/.env.example index 4a79fb3..2ed806e 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -7,7 +7,3 @@ AUTH_TRUST_HOST=true # Maskinrommet API MASKINROMMET_URL=https://api.sidelinja.org - -# SpacetimeDB (sanntids WebSocket-tilkobling) -VITE_SPACETIMEDB_URL=wss://sidelinja.org/spacetime -VITE_SPACETIMEDB_MODULE=synops diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b8fcece..8e9a9a5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,7 +18,6 @@ "@tiptap/starter-kit": "^3.20.4", "d3": "^7.9.0", "livekit-client": "^2.17.3", - "spacetimedb": "^2.0.4", "wavesurfer.js": "^7.12.4" }, "devDependencies": { @@ -2205,26 +2204,6 @@ "node": ">= 0.4" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2888,12 +2867,6 @@ "node": ">= 0.4" } }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "license": "MIT" - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3357,18 +3330,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3457,21 +3418,6 @@ "preact": ">=10" } }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prosemirror-changeset": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", @@ -3676,22 +3622,6 @@ "node": ">=6" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3812,15 +3742,6 @@ "node": ">=6" } }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3871,59 +3792,6 @@ "node": ">=0.10.0" } }, - "node_modules/spacetimedb": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/spacetimedb/-/spacetimedb-2.0.4.tgz", - "integrity": "sha512-7GiZerC9SKXvHvcaOLCgLEjFL4XOcG30k5f/ogA9QqBuD+tO/om6DfhIplo5dUg976gfqxr3Hsp5XtY2pPSCKw==", - "license": "ISC", - "dependencies": { - "base64-js": "^1.5.1", - "headers-polyfill": "^4.0.3", - "object-inspect": "^1.13.4", - "prettier": "^3.3.3", - "pure-rand": "^7.0.1", - "safe-stable-stringify": "^2.5.0", - "statuses": "^2.0.2", - "url-polyfill": "^1.1.14" - }, - "peerDependencies": { - "@angular/core": ">=17.0.0", - "@tanstack/react-query": "^5.0.0", - "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0", - "svelte": "^4.0.0 || ^5.0.0", - "undici": "^6.19.2", - "vue": "^3.3.0" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@tanstack/react-query": { - "optional": true - }, - "react": { - "optional": true - }, - "svelte": { - "optional": true - }, - "undici": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4069,12 +3937,6 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, - "node_modules/url-polyfill": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz", - "integrity": "sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==", - "license": "MIT" - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3c11bcc..41535d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,6 @@ "@tiptap/starter-kit": "^3.20.4", "d3": "^7.9.0", "livekit-client": "^2.17.3", - "spacetimedb": "^2.0.4", "wavesurfer.js": "^7.12.4" } } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 42705e5..e771328 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -629,7 +629,7 @@ export interface CreateAnnouncementResponse { node_id: string; } -/** Opprett et systemvarsel (vises for alle klienter umiddelbart via STDB). */ +/** Opprett et systemvarsel (vises for alle klienter umiddelbart via WebSocket). */ export function createAnnouncement( accessToken: string, data: CreateAnnouncementRequest @@ -1283,7 +1283,7 @@ export async function fetchNodeUsage( } // ============================================================================= -// Mixer-kanaler (oppgave 22.2 — erstatter STDB-reducers) +// Mixer-kanaler // ============================================================================= export async function createMixerChannel( diff --git a/frontend/src/lib/components/AiToolPanel.svelte b/frontend/src/lib/components/AiToolPanel.svelte index 0730229..0d13795 100644 --- a/frontend/src/lib/components/AiToolPanel.svelte +++ b/frontend/src/lib/components/AiToolPanel.svelte @@ -55,7 +55,7 @@ let shareError = $state(''); let shareSaving = $state(false); - // --- Derived: AI-preset noder fra STDB --- + // --- Derived: AI-preset noder fra store --- const presets = $derived.by(() => { const all = nodeStore.byKind('ai_preset'); return all.sort((a, b) => { diff --git a/frontend/src/lib/components/ContextHeader.svelte b/frontend/src/lib/components/ContextHeader.svelte index cc23c16..75e00af 100644 --- a/frontend/src/lib/components/ContextHeader.svelte +++ b/frontend/src/lib/components/ContextHeader.svelte @@ -12,7 +12,7 @@ collectionNode: Node | undefined; /** Current user's node ID */ userId: string | undefined; - /** Whether STDB is connected */ + /** Whether WebSocket is connected */ connected: boolean; /** Active trait names on this collection */ traitNames: string[]; @@ -259,7 +259,7 @@ {#if connected} - + {:else} {/if} diff --git a/frontend/src/lib/components/canvas/types.ts b/frontend/src/lib/components/canvas/types.ts index f311421..f0698f9 100644 --- a/frontend/src/lib/components/canvas/types.ts +++ b/frontend/src/lib/components/canvas/types.ts @@ -27,7 +27,7 @@ export interface Rect { height: number; } -/** Events emitted by the canvas for consumer integration (e.g. SpacetimeDB sync) */ +/** Events emitted by the canvas for consumer integration (e.g. WebSocket sync) */ export interface CanvasEvents { onObjectMove?: (id: string, x: number, y: number) => void; onObjectResize?: (id: string, w: number, h: number) => void; diff --git a/frontend/src/lib/components/traits/CalendarTrait.svelte b/frontend/src/lib/components/traits/CalendarTrait.svelte index ceee883..04d6053 100644 --- a/frontend/src/lib/components/traits/CalendarTrait.svelte +++ b/frontend/src/lib/components/traits/CalendarTrait.svelte @@ -115,7 +115,7 @@ }); // ========================================================================= - // Scheduled events from SpacetimeDB + // Scheduled events from WebSocket store // ========================================================================= interface ScheduledEvent { diff --git a/frontend/src/lib/spacetime/connection.svelte.ts b/frontend/src/lib/spacetime/connection.svelte.ts index 27d1d14..050e56d 100644 --- a/frontend/src/lib/spacetime/connection.svelte.ts +++ b/frontend/src/lib/spacetime/connection.svelte.ts @@ -1,7 +1,7 @@ /** * WebSocket-tilkobling til portvokteren (maskinrommet). * - * Erstatter SpacetimeDB-klient i Fase M2. Kobler til /ws-endepunktet, + * Kobler til /ws-endepunktet, * mottar initial_sync og inkrementelle events, og oppdaterer reactive stores. * * Usage: diff --git a/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts b/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts deleted file mode 100644 index e18fbc0..0000000 --- a/frontend/src/lib/spacetime/module_bindings/clear_all_reducer.ts +++ /dev/null @@ -1,13 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default {}; diff --git a/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts deleted file mode 100644 index 75105cf..0000000 --- a/frontend/src/lib/spacetime/module_bindings/create_edge_reducer.ts +++ /dev/null @@ -1,21 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), - sourceId: __t.string(), - targetId: __t.string(), - edgeType: __t.string(), - metadata: __t.string(), - system: __t.bool(), - createdBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/create_mixer_channel_reducer.ts b/frontend/src/lib/spacetime/module_bindings/create_mixer_channel_reducer.ts deleted file mode 100644 index 2ca95d3..0000000 --- a/frontend/src/lib/spacetime/module_bindings/create_mixer_channel_reducer.ts +++ /dev/null @@ -1,17 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), - updatedBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts deleted file mode 100644 index f1f8ee2..0000000 --- a/frontend/src/lib/spacetime/module_bindings/create_node_reducer.ts +++ /dev/null @@ -1,21 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), - nodeKind: __t.string(), - title: __t.string(), - content: __t.string(), - visibility: __t.string(), - metadata: __t.string(), - createdBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts deleted file mode 100644 index ab36a29..0000000 --- a/frontend/src/lib/spacetime/module_bindings/delete_edge_reducer.ts +++ /dev/null @@ -1,15 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_mixer_channel_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_mixer_channel_reducer.ts deleted file mode 100644 index 98f99b1..0000000 --- a/frontend/src/lib/spacetime/module_bindings/delete_mixer_channel_reducer.ts +++ /dev/null @@ -1,16 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_node_access_for_subject_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_node_access_for_subject_reducer.ts deleted file mode 100644 index c6bff5c..0000000 --- a/frontend/src/lib/spacetime/module_bindings/delete_node_access_for_subject_reducer.ts +++ /dev/null @@ -1,15 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - subjectId: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_node_access_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_node_access_reducer.ts deleted file mode 100644 index e2c1041..0000000 --- a/frontend/src/lib/spacetime/module_bindings/delete_node_access_reducer.ts +++ /dev/null @@ -1,16 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - subjectId: __t.string(), - objectId: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts deleted file mode 100644 index ab36a29..0000000 --- a/frontend/src/lib/spacetime/module_bindings/delete_node_reducer.ts +++ /dev/null @@ -1,15 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/edge_table.ts b/frontend/src/lib/spacetime/module_bindings/edge_table.ts deleted file mode 100644 index 6fcff6c..0000000 --- a/frontend/src/lib/spacetime/module_bindings/edge_table.ts +++ /dev/null @@ -1,22 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default __t.row({ - id: __t.string().primaryKey(), - sourceId: __t.string().name("source_id"), - targetId: __t.string().name("target_id"), - edgeType: __t.string().name("edge_type"), - metadata: __t.string(), - system: __t.bool(), - createdAt: __t.timestamp().name("created_at"), - createdBy: __t.string().name("created_by"), -}); diff --git a/frontend/src/lib/spacetime/module_bindings/index.ts b/frontend/src/lib/spacetime/module_bindings/index.ts deleted file mode 100644 index 00053a4..0000000 --- a/frontend/src/lib/spacetime/module_bindings/index.ts +++ /dev/null @@ -1,203 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -// This was generated using spacetimedb cli version 2.0.5 (commit d60138999206c06c776829072f46b5d1c1101f7e). - -/* eslint-disable */ -/* tslint:disable */ -import { - DbConnectionBuilder as __DbConnectionBuilder, - DbConnectionImpl as __DbConnectionImpl, - SubscriptionBuilderImpl as __SubscriptionBuilderImpl, - TypeBuilder as __TypeBuilder, - Uuid as __Uuid, - convertToAccessorMap as __convertToAccessorMap, - makeQueryBuilder as __makeQueryBuilder, - procedureSchema as __procedureSchema, - procedures as __procedures, - reducerSchema as __reducerSchema, - reducers as __reducers, - schema as __schema, - t as __t, - table as __table, - type AlgebraicTypeType as __AlgebraicTypeType, - type DbConnectionConfig as __DbConnectionConfig, - type ErrorContextInterface as __ErrorContextInterface, - type Event as __Event, - type EventContextInterface as __EventContextInterface, - type Infer as __Infer, - type QueryBuilder as __QueryBuilder, - type ReducerEventContextInterface as __ReducerEventContextInterface, - type RemoteModule as __RemoteModule, - type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, - type SubscriptionHandleImpl as __SubscriptionHandleImpl, -} from "spacetimedb"; - -// Import all reducer arg schemas -import ClearAllReducer from "./clear_all_reducer"; -import CreateEdgeReducer from "./create_edge_reducer"; -import CreateNodeReducer from "./create_node_reducer"; -import DeleteEdgeReducer from "./delete_edge_reducer"; -import DeleteNodeReducer from "./delete_node_reducer"; -import DeleteNodeAccessReducer from "./delete_node_access_reducer"; -import DeleteNodeAccessForSubjectReducer from "./delete_node_access_for_subject_reducer"; -import UpdateEdgeReducer from "./update_edge_reducer"; -import UpdateNodeReducer from "./update_node_reducer"; -import UpsertNodeAccessReducer from "./upsert_node_access_reducer"; -import CreateMixerChannelReducer from "./create_mixer_channel_reducer"; -import SetGainReducer from "./set_gain_reducer"; -import SetMuteReducer from "./set_mute_reducer"; -import ToggleEffectReducer from "./toggle_effect_reducer"; -import DeleteMixerChannelReducer from "./delete_mixer_channel_reducer"; -import SetMixerRoleReducer from "./set_mixer_role_reducer"; - -// Import all procedure arg schemas - -// Import all table schema definitions -import EdgeRow from "./edge_table"; -import MixerChannelRow from "./mixer_channel_table"; -import NodeRow from "./node_table"; -import NodeAccessRow from "./node_access_table"; - -/** Type-only namespace exports for generated type groups. */ - -/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ -const tablesSchema = __schema({ - edge: __table({ - name: 'edge', - indexes: [ - { accessor: 'id', name: 'edge_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'source_id', name: 'edge_source_id_idx_btree', algorithm: 'btree', columns: [ - 'sourceId', - ] }, - { accessor: 'target_id', name: 'edge_target_id_idx_btree', algorithm: 'btree', columns: [ - 'targetId', - ] }, - ], - constraints: [ - { name: 'edge_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, EdgeRow), - mixer_channel: __table({ - name: 'mixer_channel', - indexes: [ - { accessor: 'id', name: 'mixer_channel_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'room_id', name: 'mixer_channel_room_id_idx_btree', algorithm: 'btree', columns: [ - 'roomId', - ] }, - { accessor: 'target_user_id', name: 'mixer_channel_target_user_id_idx_btree', algorithm: 'btree', columns: [ - 'targetUserId', - ] }, - ], - constraints: [ - { name: 'mixer_channel_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, MixerChannelRow), - node: __table({ - name: 'node', - indexes: [ - { accessor: 'id', name: 'node_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - ], - constraints: [ - { name: 'node_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, NodeRow), - node_access: __table({ - name: 'node_access', - indexes: [ - { accessor: 'id', name: 'node_access_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'object_id', name: 'node_access_object_id_idx_btree', algorithm: 'btree', columns: [ - 'objectId', - ] }, - { accessor: 'subject_id', name: 'node_access_subject_id_idx_btree', algorithm: 'btree', columns: [ - 'subjectId', - ] }, - ], - constraints: [ - { name: 'node_access_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, NodeAccessRow), -}); - -/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ -const reducersSchema = __reducers( - __reducerSchema("clear_all", ClearAllReducer), - __reducerSchema("create_edge", CreateEdgeReducer), - __reducerSchema("create_node", CreateNodeReducer), - __reducerSchema("delete_edge", DeleteEdgeReducer), - __reducerSchema("delete_node", DeleteNodeReducer), - __reducerSchema("delete_node_access", DeleteNodeAccessReducer), - __reducerSchema("delete_node_access_for_subject", DeleteNodeAccessForSubjectReducer), - __reducerSchema("update_edge", UpdateEdgeReducer), - __reducerSchema("update_node", UpdateNodeReducer), - __reducerSchema("upsert_node_access", UpsertNodeAccessReducer), - __reducerSchema("create_mixer_channel", CreateMixerChannelReducer), - __reducerSchema("set_gain", SetGainReducer), - __reducerSchema("set_mute", SetMuteReducer), - __reducerSchema("toggle_effect", ToggleEffectReducer), - __reducerSchema("delete_mixer_channel", DeleteMixerChannelReducer), - __reducerSchema("set_mixer_role", SetMixerRoleReducer), -); - -/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ -const proceduresSchema = __procedures( -); - -/** The remote SpacetimeDB module schema, both runtime and type information. */ -const REMOTE_MODULE = { - versionInfo: { - cliVersion: "2.0.5" as const, - }, - tables: tablesSchema.schemaType.tables, - reducers: reducersSchema.reducersType.reducers, - ...proceduresSchema, -} satisfies __RemoteModule< - typeof tablesSchema.schemaType, - typeof reducersSchema.reducersType, - typeof proceduresSchema ->; - -/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); - -/** The reducers available in this remote SpacetimeDB module. */ -export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); - -/** The context type returned in callbacks for all possible events. */ -export type EventContext = __EventContextInterface; -/** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; -/** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; -/** The context type returned in callbacks for error events. */ -export type ErrorContext = __ErrorContextInterface; -/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ -export type SubscriptionHandle = __SubscriptionHandleImpl; - -/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ -export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} - -/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ -export class DbConnectionBuilder extends __DbConnectionBuilder {} - -/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ -export class DbConnection extends __DbConnectionImpl { - /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ - static builder = (): DbConnectionBuilder => { - return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); - }; - - /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ - override subscriptionBuilder = (): SubscriptionBuilder => { - return new SubscriptionBuilder(this); - }; -} - diff --git a/frontend/src/lib/spacetime/module_bindings/mixer_channel_table.ts b/frontend/src/lib/spacetime/module_bindings/mixer_channel_table.ts deleted file mode 100644 index 39ee615..0000000 --- a/frontend/src/lib/spacetime/module_bindings/mixer_channel_table.ts +++ /dev/null @@ -1,23 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default __t.row({ - id: __t.string().primaryKey(), - roomId: __t.string().name("room_id"), - targetUserId: __t.string().name("target_user_id"), - gain: __t.f64(), - isMuted: __t.bool().name("is_muted"), - activeEffects: __t.string().name("active_effects"), - role: __t.string(), - updatedBy: __t.string().name("updated_by"), - updatedAt: __t.timestamp().name("updated_at"), -}); diff --git a/frontend/src/lib/spacetime/module_bindings/node_access_table.ts b/frontend/src/lib/spacetime/module_bindings/node_access_table.ts deleted file mode 100644 index 3bb5d6e..0000000 --- a/frontend/src/lib/spacetime/module_bindings/node_access_table.ts +++ /dev/null @@ -1,19 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default __t.row({ - id: __t.string().primaryKey(), - subjectId: __t.string().name("subject_id"), - objectId: __t.string().name("object_id"), - access: __t.string(), - viaEdge: __t.string().name("via_edge"), -}); diff --git a/frontend/src/lib/spacetime/module_bindings/node_table.ts b/frontend/src/lib/spacetime/module_bindings/node_table.ts deleted file mode 100644 index 8715992..0000000 --- a/frontend/src/lib/spacetime/module_bindings/node_table.ts +++ /dev/null @@ -1,22 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default __t.row({ - id: __t.string().primaryKey(), - nodeKind: __t.string().name("node_kind"), - title: __t.string(), - content: __t.string(), - visibility: __t.string(), - metadata: __t.string(), - createdAt: __t.timestamp().name("created_at"), - createdBy: __t.string().name("created_by"), -}); diff --git a/frontend/src/lib/spacetime/module_bindings/set_gain_reducer.ts b/frontend/src/lib/spacetime/module_bindings/set_gain_reducer.ts deleted file mode 100644 index cceac7b..0000000 --- a/frontend/src/lib/spacetime/module_bindings/set_gain_reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), - gain: __t.f64(), - updatedBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/set_mixer_role_reducer.ts b/frontend/src/lib/spacetime/module_bindings/set_mixer_role_reducer.ts deleted file mode 100644 index 40bdc70..0000000 --- a/frontend/src/lib/spacetime/module_bindings/set_mixer_role_reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), - role: __t.string(), - updatedBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/set_mute_reducer.ts b/frontend/src/lib/spacetime/module_bindings/set_mute_reducer.ts deleted file mode 100644 index ee488b0..0000000 --- a/frontend/src/lib/spacetime/module_bindings/set_mute_reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), - isMuted: __t.bool(), - updatedBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/toggle_effect_reducer.ts b/frontend/src/lib/spacetime/module_bindings/toggle_effect_reducer.ts deleted file mode 100644 index d832fd0..0000000 --- a/frontend/src/lib/spacetime/module_bindings/toggle_effect_reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - roomId: __t.string(), - targetUserId: __t.string(), - effectName: __t.string(), - updatedBy: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/types.ts b/frontend/src/lib/spacetime/module_bindings/types.ts deleted file mode 100644 index 045df8a..0000000 --- a/frontend/src/lib/spacetime/module_bindings/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export const Edge = __t.object("Edge", { - id: __t.string(), - sourceId: __t.string(), - targetId: __t.string(), - edgeType: __t.string(), - metadata: __t.string(), - system: __t.bool(), - createdAt: __t.timestamp(), - createdBy: __t.string(), -}); -export type Edge = __Infer; - -export const Node = __t.object("Node", { - id: __t.string(), - nodeKind: __t.string(), - title: __t.string(), - content: __t.string(), - visibility: __t.string(), - metadata: __t.string(), - createdAt: __t.timestamp(), - createdBy: __t.string(), -}); -export type Node = __Infer; - -export const MixerChannel = __t.object("MixerChannel", { - id: __t.string(), - roomId: __t.string(), - targetUserId: __t.string(), - gain: __t.f64(), - isMuted: __t.bool(), - activeEffects: __t.string(), - role: __t.string(), - updatedBy: __t.string(), - updatedAt: __t.timestamp(), -}); -export type MixerChannel = __Infer; - -export const NodeAccess = __t.object("NodeAccess", { - id: __t.string(), - subjectId: __t.string(), - objectId: __t.string(), - access: __t.string(), - viaEdge: __t.string(), -}); -export type NodeAccess = __Infer; - diff --git a/frontend/src/lib/spacetime/module_bindings/types/procedures.ts b/frontend/src/lib/spacetime/module_bindings/types/procedures.ts deleted file mode 100644 index d5ac825..0000000 --- a/frontend/src/lib/spacetime/module_bindings/types/procedures.ts +++ /dev/null @@ -1,10 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { type Infer as __Infer } from "spacetimedb"; - -// Import all procedure arg schemas - - diff --git a/frontend/src/lib/spacetime/module_bindings/types/reducers.ts b/frontend/src/lib/spacetime/module_bindings/types/reducers.ts deleted file mode 100644 index 62308e5..0000000 --- a/frontend/src/lib/spacetime/module_bindings/types/reducers.ts +++ /dev/null @@ -1,42 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { type Infer as __Infer } from "spacetimedb"; - -// Import all reducer arg schemas -import ClearAllReducer from "../clear_all_reducer"; -import CreateEdgeReducer from "../create_edge_reducer"; -import CreateNodeReducer from "../create_node_reducer"; -import DeleteEdgeReducer from "../delete_edge_reducer"; -import DeleteNodeReducer from "../delete_node_reducer"; -import DeleteNodeAccessReducer from "../delete_node_access_reducer"; -import DeleteNodeAccessForSubjectReducer from "../delete_node_access_for_subject_reducer"; -import UpdateEdgeReducer from "../update_edge_reducer"; -import UpdateNodeReducer from "../update_node_reducer"; -import UpsertNodeAccessReducer from "../upsert_node_access_reducer"; -import CreateMixerChannelReducer from "../create_mixer_channel_reducer"; -import SetGainReducer from "../set_gain_reducer"; -import SetMuteReducer from "../set_mute_reducer"; -import ToggleEffectReducer from "../toggle_effect_reducer"; -import DeleteMixerChannelReducer from "../delete_mixer_channel_reducer"; -import SetMixerRoleReducer from "../set_mixer_role_reducer"; - -export type ClearAllParams = __Infer; -export type CreateEdgeParams = __Infer; -export type CreateNodeParams = __Infer; -export type DeleteEdgeParams = __Infer; -export type DeleteNodeParams = __Infer; -export type DeleteNodeAccessParams = __Infer; -export type DeleteNodeAccessForSubjectParams = __Infer; -export type UpdateEdgeParams = __Infer; -export type UpdateNodeParams = __Infer; -export type UpsertNodeAccessParams = __Infer; -export type CreateMixerChannelParams = __Infer; -export type SetGainParams = __Infer; -export type SetMuteParams = __Infer; -export type ToggleEffectParams = __Infer; -export type DeleteMixerChannelParams = __Infer; -export type SetMixerRoleParams = __Infer; - diff --git a/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts b/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts deleted file mode 100644 index 4a0d616..0000000 --- a/frontend/src/lib/spacetime/module_bindings/update_edge_reducer.ts +++ /dev/null @@ -1,17 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), - edgeType: __t.string(), - metadata: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts b/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts deleted file mode 100644 index 0383185..0000000 --- a/frontend/src/lib/spacetime/module_bindings/update_node_reducer.ts +++ /dev/null @@ -1,20 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - id: __t.string(), - nodeKind: __t.string(), - title: __t.string(), - content: __t.string(), - visibility: __t.string(), - metadata: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/module_bindings/upsert_node_access_reducer.ts b/frontend/src/lib/spacetime/module_bindings/upsert_node_access_reducer.ts deleted file mode 100644 index a485b0b..0000000 --- a/frontend/src/lib/spacetime/module_bindings/upsert_node_access_reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE -// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. - -/* eslint-disable */ -/* tslint:disable */ -import { - TypeBuilder as __TypeBuilder, - t as __t, - type AlgebraicTypeType as __AlgebraicTypeType, - type Infer as __Infer, -} from "spacetimedb"; - -export default { - subjectId: __t.string(), - objectId: __t.string(), - access: __t.string(), - viaEdge: __t.string(), -}; diff --git a/frontend/src/lib/spacetime/types.ts b/frontend/src/lib/spacetime/types.ts index 0000b87..c96b3b4 100644 --- a/frontend/src/lib/spacetime/types.ts +++ b/frontend/src/lib/spacetime/types.ts @@ -1,7 +1,6 @@ /** * Lokale type-definisjoner for noder, edges, access og mixer-kanaler. * - * Erstatter SpacetimeDB module_bindings/types.ts. * Feltnavnene matcher JSON-formatet fra portvokterens WebSocket (camelCase). */ diff --git a/frontend/src/routes/admin/health/+page.svelte b/frontend/src/routes/admin/health/+page.svelte index 52c45e7..c3f95b3 100644 --- a/frontend/src/routes/admin/health/+page.svelte +++ b/frontend/src/routes/admin/health/+page.svelte @@ -94,7 +94,7 @@ return 'text-neutral-500'; } - const allServices = ['maskinrommet', 'caddy', 'postgres', 'spacetimedb', 'authentik', 'litellm', 'whisper', 'livekit']; + const allServices = ['maskinrommet', 'caddy', 'postgres', 'authentik', 'litellm', 'whisper', 'livekit'];
diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 8bc2792..5ff2f20 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -94,7 +94,7 @@ } // ========================================================================= - // Scheduled events from SpacetimeDB + // Scheduled events from WebSocket store // ========================================================================= interface ScheduledEvent { diff --git a/frontend/src/routes/editorial/[id]/+page.svelte b/frontend/src/routes/editorial/[id]/+page.svelte index 312edda..9b053e1 100644 --- a/frontend/src/routes/editorial/[id]/+page.svelte +++ b/frontend/src/routes/editorial/[id]/+page.svelte @@ -41,7 +41,7 @@ }; // ========================================================================= - // Kort fra SpacetimeDB — noder med submitted_to-edge til samlingen + // Kort fra WebSocket store — noder med submitted_to-edge til samlingen // ========================================================================= interface CardData { diff --git a/frontend/src/routes/studio/[id]/+page.svelte b/frontend/src/routes/studio/[id]/+page.svelte index 02ac178..d0d8485 100644 --- a/frontend/src/routes/studio/[id]/+page.svelte +++ b/frontend/src/routes/studio/[id]/+page.svelte @@ -25,7 +25,7 @@ const connected = $derived(connectionState.current === 'connected'); const mediaNodeId = $derived($page.params.id ?? ''); - // Media node from STDB + // Media node from store const mediaNode = $derived(connected ? nodeStore.get(mediaNodeId) : undefined); const metadata = $derived.by(() => { if (!mediaNode?.metadata) return null; diff --git a/frontend/src/routes/workspace/+page.svelte b/frontend/src/routes/workspace/+page.svelte index 3aaf860..66ebec8 100644 --- a/frontend/src/routes/workspace/+page.svelte +++ b/frontend/src/routes/workspace/+page.svelte @@ -78,7 +78,7 @@ }); }); - // Also try to read layout from STDB node (for real-time sync) + // Also try to read layout from WS node store (for real-time sync) const workspaceNode = $derived( workspaceNodeId && connected ? nodeStore.get(workspaceNodeId) : undefined ); @@ -90,7 +90,7 @@ let layout = $state({ panels: [] }); let layoutInitialized = $state(false); - // When workspace node appears in STDB (after creation), load its layout + // When workspace node appears in store (after creation), load its layout $effect(() => { if (!workspaceNode || layoutInitialized) return; try { @@ -124,7 +124,7 @@ clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { try { - // Read current metadata from STDB node + // Read current metadata from node store const currentMeta = workspaceNode ? JSON.parse(workspaceNode.metadata ?? '{}') : {}; @@ -460,7 +460,7 @@
{#if connected} - + {:else} {/if} diff --git a/maskinrommet/src/audio.rs b/maskinrommet/src/audio.rs index bb91a8e..8d88780 100644 --- a/maskinrommet/src/audio.rs +++ b/maskinrommet/src/audio.rs @@ -758,8 +758,8 @@ async fn resolve_silence_cuts( // ─── Jobbhåndterer — delegerer til synops-audio CLI ──────────────── // -// Maskinrommet beholder: STDB-synk for sanntidsvisning. // CLI gjør: FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging. +// PG NOTIFY-triggere sender sanntidsoppdateringer. /// Synops-audio binary path. fn audio_bin() -> String { @@ -771,7 +771,7 @@ fn audio_bin() -> String { /// /// Spawner synops-audio med --write for å gjøre alt arbeidet: /// FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging. -/// Maskinrommet gjør etterpå STDB-synk for sanntidsvisning. +/// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter. /// /// Payload: /// ```json diff --git a/maskinrommet/src/cli_dispatch.rs b/maskinrommet/src/cli_dispatch.rs index f4ebe2a..99a3143 100644 --- a/maskinrommet/src/cli_dispatch.rs +++ b/maskinrommet/src/cli_dispatch.rs @@ -1,7 +1,7 @@ // Felles hjelpefunksjoner for å spawne CLI-verktøy fra jobbkø-handlere. // -// Mønsteret: maskinrommet orkestrerer (payload-parsing, sikkerhetskontroller, -// STDB-synk), CLI-verktøyet gjør jobben (API-kall, DB-skriving, prosessering). +// Mønsteret: maskinrommet orkestrerer (payload-parsing, sikkerhetskontroller), +// CLI-verktøyet gjør jobben (API-kall, DB-skriving, prosessering). // Stdout → jobbresultat (JSON), stderr → feillogg, exitkode → status. // // Ref: docs/retninger/unix_filosofi.md diff --git a/maskinrommet/src/health.rs b/maskinrommet/src/health.rs index 923243b..d0a3d78 100644 --- a/maskinrommet/src/health.rs +++ b/maskinrommet/src/health.rs @@ -1,6 +1,6 @@ // Serverhelse-dashboard — tjeneste-status, metrikker, backup-status, logg-tilgang. // -// Sjekker alle tjenester i stacken (PG, STDB, Caddy, Authentik, LiteLLM, +// Sjekker alle tjenester i stacken (PG, Caddy, Authentik, LiteLLM, // Whisper, LiveKit) og samler system-metrikker (CPU, minne, disk). // // Ref: docs/concepts/adminpanelet.md § 4 "Serverhelse", oppgave 15.6 @@ -373,10 +373,9 @@ fn read_service_logs(service: &str, max_lines: usize) -> Vec { "maskinrommet" | "caddy" | "sveltekit" => { format!("journalctl -u {service} --no-pager -n {max_lines} --output=short-iso 2>/dev/null") } - "postgres" | "spacetimedb" | "authentik" | "litellm" | "whisper" | "livekit" => { + "postgres" | "authentik" | "litellm" | "whisper" | "livekit" => { let container = match service { "postgres" => "sidelinja-postgres-1", - "spacetimedb" => "sidelinja-spacetimedb-1", "authentik" => "sidelinja-authentik-server-1", "litellm" => "sidelinja-ai-gateway-1", "whisper" => "sidelinja-faster-whisper-1", @@ -473,7 +472,7 @@ pub async fn health_logs( read_service_logs(service, max_lines) } else { // Alle tjenester, siste linjer fra hver - let services = ["maskinrommet", "caddy", "postgres", "spacetimedb", "authentik", "litellm", "whisper", "livekit"]; + let services = ["maskinrommet", "caddy", "postgres", "authentik", "litellm", "whisper", "livekit"]; let per_service = (max_lines / services.len()).max(10); let mut all = Vec::new(); for svc in &services { diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index 686c5fb..4bd6370 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -457,8 +457,7 @@ pub struct CreateNodeResponse { /// POST /intentions/create_node /// -/// Validerer input, skriver til STDB (instant), spawner async PG-skriving. -/// Returnerer node_id umiddelbart. +/// Validerer input, skriver til PG. NOTIFY-triggere sender sanntidsoppdateringer. /// /// Hvis `context_id` er satt, opprettes automatisk en `belongs_to`-edge /// fra den nye noden til kontekstnoden. Kontekstnoden må eksistere og @@ -1179,7 +1178,7 @@ pub struct DeleteNodeResponse { /// POST /intentions/delete_node /// -/// Sletter en node og alle dens edges (CASCADE i PG, eksplisitt i STDB). +/// Sletter en node og alle dens edges (CASCADE i PG). /// Krever at brukeren er created_by eller har owner/admin-edge til noden. pub async fn delete_node( State(state): State, @@ -3264,7 +3263,7 @@ pub struct RoomParticipantInfo { /// /// Kobler en bruker til sanntidslyd i en kommunikasjonsnode. /// Validerer tilgang (bruker må ha member_of/owner/host_of-edge), -/// genererer LiveKit-token, oppdaterer STDB med live-status. +/// genererer LiveKit-token, oppdaterer PG med live-status. pub async fn join_communication( State(state): State, user: AuthUser, @@ -3521,7 +3520,7 @@ pub struct CloseCommunicationResponse { /// POST /intentions/close_communication /// /// Stenger et sanntidsrom. Krever owner/admin-tilgang. -/// Fjerner alle deltakere fra STDB, oppdaterer metadata til "ended". +/// Oppdaterer metadata til "ended". pub async fn close_communication( State(state): State, user: AuthUser, @@ -3734,7 +3733,7 @@ pub struct CreateAnnouncementResponse { /// POST /intentions/create_announcement /// /// Oppretter en systemvarslingsnode med `visibility: open` slik at alle -/// aktive klienter ser varselet via STDB umiddelbart. +/// aktive klienter ser varselet via WebSocket umiddelbart. /// /// Kun autentiserte brukere kan opprette varsler (MVP — full admin-sjekk /// legges til når admin-rollesystemet er på plass). @@ -3817,8 +3816,8 @@ pub struct ExpireAnnouncementResponse { /// POST /intentions/expire_announcement /// -/// Fjerner (sletter) en systemvarslingsnode. Sletter fra STDB først -/// (umiddelbar fjerning fra alle klienter), deretter fra PG. +/// Fjerner (sletter) en systemvarslingsnode fra PG. +/// WebSocket NOTIFY sørger for umiddelbar fjerning fra alle klienter. /// /// Kun eier (created_by) eller admin kan fjerne varsler. pub async fn expire_announcement( diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index cc82a07..335b20b 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -22,8 +22,6 @@ pub mod resource_usage; pub mod resources; mod rss; mod serving; -#[allow(dead_code)] -mod stdb; // Beholdt som død kode — fjernes i oppgave 22.4 pub mod summarize; pub mod ws; pub mod mixer; @@ -257,7 +255,7 @@ async fn main() { .route("/custom-domain/sok", get(custom_domain::serve_custom_domain_search)) .route("/custom-domain/om", get(custom_domain::serve_custom_domain_about)) .route("/custom-domain/{article_id}", get(custom_domain::serve_custom_domain_article)) - // Mixer-kanaler (oppgave 22.2 — erstatter STDB-reducers) + // Mixer-kanaler .route("/intentions/create_mixer_channel", post(mixer::create_mixer_channel)) .route("/intentions/set_gain", post(mixer::set_gain)) .route("/intentions/set_mute", post(mixer::set_mute)) diff --git a/maskinrommet/src/mixer.rs b/maskinrommet/src/mixer.rs index 9603b65..8a3a527 100644 --- a/maskinrommet/src/mixer.rs +++ b/maskinrommet/src/mixer.rs @@ -1,10 +1,7 @@ //! Mixer-kanaler — HTTP API for delt lydmixer-tilstand. //! -//! Erstatter SpacetimeDB-reducers for mixer (createMixerChannel, setGain, -//! setMute, toggleEffect, setMixerRole). Skriver direkte til PG; -//! NOTIFY-trigger propagerer endringer til WebSocket-klienter. -//! -//! Ref: oppgave 22.2 (SpacetimeDB-migrering) +//! Skriver direkte til PG; NOTIFY-trigger propagerer endringer til +//! WebSocket-klienter. use axum::{extract::State, http::StatusCode, Json}; use serde::{Deserialize, Serialize}; diff --git a/maskinrommet/src/pg_writes.rs b/maskinrommet/src/pg_writes.rs index 584b82f..3b02c01 100644 --- a/maskinrommet/src/pg_writes.rs +++ b/maskinrommet/src/pg_writes.rs @@ -202,7 +202,7 @@ fn edge_type_to_access_level(edge_type: &str) -> Option<&'static str> { /// Handler: pg_insert_edge /// /// Håndterer tilgangsgivende edges (owner/admin/member_of/reader) med -/// recompute_access i transaksjon, og synker til STDB. For belongs_to-edges +/// recompute_access i transaksjon. For belongs_to-edges /// trigges artikkelrendering hvis target er en publiseringssamling. pub async fn handle_insert_edge( job: &JobRow, diff --git a/maskinrommet/src/queries.rs b/maskinrommet/src/queries.rs index 6285531..f2db0c3 100644 --- a/maskinrommet/src/queries.rs +++ b/maskinrommet/src/queries.rs @@ -1,6 +1,6 @@ // Tunge spørringer — lesestien via PostgreSQL med RLS. // -// For søk, statistikk, og graf-traversering brukes PG direkte (ikke STDB). +// For søk, statistikk, og graf-traversering brukes PG direkte. // Alle spørringer kjøres med SET LOCAL ROLE synops_reader, som er underlagt // RLS-policies. Brukerens node_id settes som sesjonsvariabel. // diff --git a/maskinrommet/src/stdb.rs b/maskinrommet/src/stdb.rs deleted file mode 100644 index b07552d..0000000 --- a/maskinrommet/src/stdb.rs +++ /dev/null @@ -1,466 +0,0 @@ -// SpacetimeDB HTTP-klient for maskinrommet. -// -// Kaller STDB-reducere via HTTP JSON API. -// Maskinrommet eier all skriving — denne klienten er eneste vei inn. -// -// API-format: POST /v1/database/{db}/call/{reducer} -// Body: JSON-objekt med navngitte parametre. -// Auth: Bearer-token fra STDB-identitet. -// -// Ref: docs/retninger/datalaget.md, docs/infra/synkronisering.md - -use reqwest::Client; -use serde::Serialize; - -/// SpacetimeDB-klient som kaller reducere via HTTP. -#[derive(Clone)] -pub struct StdbClient { - client: Client, - base_url: String, - database: String, - token: String, -} - -impl StdbClient { - /// Opprett ny klient. `base_url` er STDB-serverens URL (f.eks. "http://spacetimedb:3000"). - pub fn new(base_url: &str, database: &str, token: &str) -> Self { - Self { - client: Client::new(), - base_url: base_url.trim_end_matches('/').to_string(), - database: database.to_string(), - token: token.to_string(), - } - } - - /// Hent en ny identitet og token fra STDB-serveren. - /// Brukes ved oppstart hvis ingen token er konfigurert. - pub async fn create_identity(base_url: &str) -> Result<(String, String), StdbError> { - let client = Client::new(); - let url = format!("{}/v1/identity", base_url.trim_end_matches('/')); - let resp = client.post(&url).send().await?; - - if !resp.status().is_success() { - return Err(StdbError::Http(format!( - "Kunne ikke opprette identitet: {}", - resp.status() - ))); - } - - let body: serde_json::Value = resp.json().await?; - let identity = body["identity"] - .as_str() - .ok_or_else(|| StdbError::Http("Mangler identity i respons".into()))? - .to_string(); - let token = body["token"] - .as_str() - .ok_or_else(|| StdbError::Http("Mangler token i respons".into()))? - .to_string(); - - Ok((identity, token)) - } - - /// Kall en reducer med navngitte parametre (JSON-objekt). - async fn call_reducer(&self, reducer: &str, args: &T) -> Result<(), StdbError> { - let url = format!( - "{}/v1/database/{}/call/{}", - self.base_url, self.database, reducer - ); - - let resp = self - .client - .post(&url) - .bearer_auth(&self.token) - .json(args) - .send() - .await?; - - if resp.status().is_success() { - Ok(()) - } else { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - Err(StdbError::Reducer { - reducer: reducer.to_string(), - status: status.as_u16(), - message: body, - }) - } - } - - // ========================================================================= - // Node-operasjoner - // ========================================================================= - - pub async fn create_node( - &self, - id: &str, - node_kind: &str, - title: &str, - content: &str, - visibility: &str, - metadata: &str, - created_by: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - node_kind: &'a str, - title: &'a str, - content: &'a str, - visibility: &'a str, - metadata: &'a str, - created_by: &'a str, - } - - self.call_reducer( - "create_node", - &Args { - id, - node_kind, - title, - content, - visibility, - metadata, - created_by, - }, - ) - .await - } - - pub async fn update_node( - &self, - id: &str, - node_kind: &str, - title: &str, - content: &str, - visibility: &str, - metadata: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - node_kind: &'a str, - title: &'a str, - content: &'a str, - visibility: &'a str, - metadata: &'a str, - } - - self.call_reducer( - "update_node", - &Args { - id, - node_kind, - title, - content, - visibility, - metadata, - }, - ) - .await - } - - pub async fn delete_node(&self, id: &str) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - } - - self.call_reducer("delete_node", &Args { id }).await - } - - // ========================================================================= - // Edge-operasjoner - // ========================================================================= - - pub async fn create_edge( - &self, - id: &str, - source_id: &str, - target_id: &str, - edge_type: &str, - metadata: &str, - system: bool, - created_by: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - source_id: &'a str, - target_id: &'a str, - edge_type: &'a str, - metadata: &'a str, - system: bool, - created_by: &'a str, - } - - self.call_reducer( - "create_edge", - &Args { - id, - source_id, - target_id, - edge_type, - metadata, - system, - created_by, - }, - ) - .await - } - - pub async fn update_edge( - &self, - id: &str, - edge_type: &str, - metadata: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - edge_type: &'a str, - metadata: &'a str, - } - - self.call_reducer("update_edge", &Args { id, edge_type, metadata }) - .await - } - - pub async fn delete_edge(&self, id: &str) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - } - - self.call_reducer("delete_edge", &Args { id }).await - } - - // ========================================================================= - // NodeAccess-operasjoner - // ========================================================================= - - pub async fn upsert_node_access( - &self, - subject_id: &str, - object_id: &str, - access: &str, - via_edge: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - subject_id: &'a str, - object_id: &'a str, - access: &'a str, - via_edge: &'a str, - } - - self.call_reducer( - "upsert_node_access", - &Args { - subject_id, - object_id, - access, - via_edge, - }, - ) - .await - } - - pub async fn delete_node_access( - &self, - subject_id: &str, - object_id: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - subject_id: &'a str, - object_id: &'a str, - } - - self.call_reducer("delete_node_access", &Args { subject_id, object_id }) - .await - } - - // ========================================================================= - // Live-rom (LiveKit) - // ========================================================================= - - pub async fn create_live_room( - &self, - room_id: &str, - communication_id: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - room_id: &'a str, - communication_id: &'a str, - } - - self.call_reducer("create_live_room", &Args { room_id, communication_id }) - .await - } - - pub async fn add_room_participant( - &self, - room_id: &str, - user_id: &str, - display_name: &str, - role: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - room_id: &'a str, - user_id: &'a str, - display_name: &'a str, - role: &'a str, - } - - self.call_reducer( - "add_room_participant", - &Args { room_id, user_id, display_name, role }, - ) - .await - } - - pub async fn remove_room_participant( - &self, - room_id: &str, - user_id: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - room_id: &'a str, - user_id: &'a str, - } - - self.call_reducer("remove_room_participant", &Args { room_id, user_id }) - .await - } - - pub async fn close_live_room(&self, room_id: &str) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - room_id: &'a str, - } - - self.call_reducer("close_live_room", &Args { room_id }).await - } - - // ========================================================================= - // Placement-operasjoner (message_placements) - // ========================================================================= - - /// Plasser en melding i en kontekst. Idempotent (upsert). - pub async fn place_message( - &self, - id: &str, - message_id: &str, - context_type: &str, - context_id: &str, - position_json: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - id: &'a str, - message_id: &'a str, - context_type: &'a str, - context_id: &'a str, - position_json: &'a str, - } - - self.call_reducer( - "place_message", - &Args { id, message_id, context_type, context_id, position_json }, - ) - .await - } - - /// Fjern en meldings plassering fra en kontekst. - pub async fn remove_placement( - &self, - message_id: &str, - context_type: &str, - context_id: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - message_id: &'a str, - context_type: &'a str, - context_id: &'a str, - } - - self.call_reducer( - "remove_placement", - &Args { message_id, context_type, context_id }, - ) - .await - } - - /// Flytt en plassering (oppdater posisjon). - pub async fn move_on_canvas( - &self, - placement_id: &str, - new_position_json: &str, - ) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Args<'a> { - placement_id: &'a str, - new_position_json: &'a str, - } - - self.call_reducer( - "move_on_canvas", - &Args { placement_id, new_position_json }, - ) - .await - } - - // ========================================================================= - // Vedlikehold - // ========================================================================= - - /// Tøm alle noder og edges. Brukes ved warmup for å unngå duplikater. - pub async fn clear_all(&self) -> Result<(), StdbError> { - #[derive(Serialize)] - struct Empty {} - - self.call_reducer("clear_all", &Empty {}).await - } -} - -// ============================================================================= -// Feilhåndtering -// ============================================================================= - -#[derive(Debug)] -pub enum StdbError { - /// HTTP-transportfeil (nettverk, timeout) - Http(String), - /// Reducer returnerte feil (400, 500, etc.) - Reducer { - reducer: String, - status: u16, - message: String, - }, -} - -impl std::fmt::Display for StdbError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StdbError::Http(msg) => write!(f, "STDB HTTP-feil: {msg}"), - StdbError::Reducer { - reducer, - status, - message, - } => write!(f, "STDB reducer {reducer} feilet ({status}): {message}"), - } - } -} - -impl std::error::Error for StdbError {} - -impl From for StdbError { - fn from(e: reqwest::Error) -> Self { - StdbError::Http(e.to_string()) - } -} diff --git a/maskinrommet/src/tts.rs b/maskinrommet/src/tts.rs index 68f75b3..90872f7 100644 --- a/maskinrommet/src/tts.rs +++ b/maskinrommet/src/tts.rs @@ -25,7 +25,7 @@ fn tts_bin() -> String { /// /// Spawner synops-tts med --write for å gjøre alt arbeidet: /// ElevenLabs-kall, CAS-lagring, PG-skriving, ressurslogging. -/// Maskinrommet gjør etterpå STDB-synk for sanntidsvisning. +/// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter. pub async fn handle_tts_job( job: &JobRow, db: &PgPool, diff --git a/maskinrommet/src/ws.rs b/maskinrommet/src/ws.rs index ca26cb5..00b1b30 100644 --- a/maskinrommet/src/ws.rs +++ b/maskinrommet/src/ws.rs @@ -4,7 +4,6 @@ //! og `mixer_channel_changed` kanaler i PostgreSQL og videresender relevante //! endringer via WebSocket til tilkoblede klienter, filtrert på tilgangsmatrisen. //! -//! Fase M2: Frontend bruker kun denne WebSocket-tilkoblingen (SpacetimeDB fjernet). //! Events berikes med full raddata fra PG slik at klienten kan oppdatere stores direkte. //! Ref: docs/retninger/datalaget.md diff --git a/ops/doc-audit.md b/ops/doc-audit.md index bbe0f46..0eff747 100644 --- a/ops/doc-audit.md +++ b/ops/doc-audit.md @@ -24,7 +24,7 @@ faktisk kode. Mer fokusert enn ryddejobben — kun docs. ### 3. Tekniske detaljer - [ ] Stemmer database-skjema beskrevet i docs med faktiske migrasjoner? - [ ] Stemmer API-endepunkter beskrevet i docs med faktiske routes? -- [ ] Stemmer SpacetimeDB-tabeller/reducere i docs med modulen? +- [ ] Stemmer WebSocket-/sanntids-beskrivelser i docs med implementasjonen? ### 4. Setup-docs - [ ] `docs/setup/lokal.md` — fungerer stegene fortsatt? diff --git a/ops/drift-sjekk.md b/ops/drift-sjekk.md index beb1f06..b3789a9 100644 --- a/ops/drift-sjekk.md +++ b/ops/drift-sjekk.md @@ -29,10 +29,7 @@ men ikke implementert. - [ ] Er det env-vars i `.env` som er utdaterte? - [ ] Er secrets rotert der de bør være? -### 5. SpacetimeDB-modul -- [ ] Er SpacetimeDB-modulen publisert med siste endringer? - -### 6. Caddy / reverse proxy +### 5. Caddy / reverse proxy - [ ] Er Caddyfile i repo synk med `/srv/synops/config/caddy/Caddyfile`? - [ ] Er det nye subdomener eller routes som mangler? diff --git a/ops/ryddejobb.md b/ops/ryddejobb.md index f251408..e0a1314 100644 --- a/ops/ryddejobb.md +++ b/ops/ryddejobb.md @@ -29,7 +29,6 @@ fjerne utdaterte referanser, og sikre at dokumentasjon stemmer med virkeligheten - [ ] Ubrukte SvelteKit-routes (mapper i `web/src/routes/` uten innhold eller med stub) - [ ] Ubrukte komponenter (filer i `web/src/lib/components/` som ikke importeres) - [ ] Ubrukte Rust-moduler i worker -- [ ] Ubrukte SpacetimeDB-reducere eller tabeller - [ ] Gamle migrations som bør dokumenteres eller konsolideres - [ ] `package.json` / `Cargo.toml` — ubrukte dependencies diff --git a/reference/server-state.md b/reference/server-state.md index 8bfca79..382579b 100644 --- a/reference/server-state.md +++ b/reference/server-state.md @@ -13,7 +13,7 @@ Slettes når Synops er oppe og stabilt. | authentik-worker | goauthentik/server:latest | SSO bakgrunn | JA | | redis | redis:7-alpine | Cache for Authentik | JA | | forgejo | forgejo:10 | Git | JA | -| spacetimedb | clockworklabs/spacetime:latest | Sanntid | FJERNES (tom v1-modul) | +| ~~spacetimedb~~ | ~~clockworklabs/spacetime:latest~~ | ~~Sanntid~~ | FJERNET (mars 2026) | | web | sidelinja-web (bygget) | SvelteKit v1 | FJERNES | | worker | sidelinja-worker (bygget) | Rust worker v1 | FJERNES | | ai-gateway | litellm:main-stable | AI-ruter | BEHOLDES (uavhengig av app) | @@ -23,7 +23,7 @@ Slettes når Synops er oppe og stabilt. - git.sidelinja.org → forgejo:3000 - sidelinja.org → web:3000 + /media/* (filservering) + JSON access log - vegard.info → placeholder -- rt.sidelinja.org → spacetimedb:3000 +- ~~rt.sidelinja.org → spacetimedb:3000~~ (fjernet) ## PG init-script `/srv/sidelinja/config/postgres/init/01-create-databases.sql` @@ -34,7 +34,7 @@ kjøres kun ved *første* PG-oppstart. ## Dockerfiles (referanse) ### web/Dockerfile - node:20-alpine, to-stegs bygg -- VITE_SPACETIMEDB_URL som build-arg +- ~~VITE_SPACETIMEDB_URL som build-arg~~ (fjernet) - `npm run build` → `node build` på port 3000 ### worker/Dockerfile diff --git a/scripts/maskinrommet-env.sh b/scripts/maskinrommet-env.sh index db6980e..a818d98 100755 --- a/scripts/maskinrommet-env.sh +++ b/scripts/maskinrommet-env.sh @@ -9,16 +9,12 @@ read_env() { grep "^$1=" "$ENV_FILE" | head -1 | cut -d= -f2; } container_ip() { docker inspect "$1" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'; } PG_IP=$(container_ip sidelinja-postgres-1) -STDB_IP=$(container_ip sidelinja-spacetimedb-1) WHISPER_IP=$(container_ip sidelinja-faster-whisper-1 2>/dev/null || echo "") AI_GW_IP=$(container_ip sidelinja-ai-gateway-1 2>/dev/null || echo "") LIVEKIT_IP=$(container_ip sidelinja-livekit-1 2>/dev/null || echo "") cat > /tmp/maskinrommet.env < Alt er noder. En bruker er en node. Et team er en node. Et møte er -> en node. Sidelinja er en node. Relasjoner mellom dem er edges. -> Tilgangskontroll via materialisert tilgangsmatrise beregnet fra -> edge-grafen. - -## Beslutningen - -1. **Alt er noder.** Brukere, team, prosjekter, innhold, møter — - alt er rader i `nodes`-tabellen. En bruker er en node som - tilfeldigvis kan logge inn. -2. **Relasjoner er edges.** Vegard → Sidelinja (`owner`). - Trond → Sidelinja (`member`). Møtereferat → møte (`belongs_to`). -3. **Ingen containere.** Hva du ser er summen av dine edges. -4. **Samlings-noder gir struktur** — de er vanlige noder som - fungerer som gravitasjonspunkt. -5. **Privat er default** — en node uten edges til andre er kun din. -6. **Tilgangskontroll via `node_access`-matrise**, oppdatert ved - edge-endring, brukt av RLS ved lesing. - -## Visibility - -Visibility er en egenskap på noden som definerer hva som gjelder -for alle *uten* eksplisitt edge. Eksplisitte edges overrider alltid -oppover. - -```sql -CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open'); -``` - -| Nivå | Oppdagbar | Lesbar | Interagerbar | -|------|-----------|--------|-------------| -| `hidden` | Nei | Nei | Nei — kun via eksplisitt edge | -| `discoverable` | Ja (søk/katalog) | Nei | Kontaktforespørsel | -| `readable` | Ja | Ja | Nei — krever edge | -| `open` | Ja | Ja | Ja | - -Eksempler: -- Spøkelsesbruker: `hidden` — usynlig, kun invitasjon -- Katalogbruker: `discoverable` — finnes i søk, profil krever edge -- Podcast-episode: `readable` — alle kan lytte, bare teamet kan redigere -- Kunnskapsgraf-entitet: `open` — alle kan se og bidra - -### Traverseringsregelen - -**Visibility er en hard grense ved traversering.** Når du følger -edges i grafen, kan du bare se noder hvis deres visibility tillater -det — eller du har en eksplisitt edge. - -Eksempel: Episode #42 (`readable`) har en `host`-edge til Peter -(`hidden`). Anonym bruker leser episoden, følger `host`-edge → -Peter er `hidden` → **stopp, usynlig.** Trond (som har edge til -Peter) følger samme edge → **ser Peter.** - -Ingen transitivitet bryter denne regelen. Uansett hvor mange -offentlige noder som har edges til en `hidden` node, forblir den -skjult for de uten eksplisitt edge. - -## Brukere er noder - -En brukernode er en node med en kobling til Authentik for -autentisering. Alt annet — navn, preferanser, roller, relasjoner — -er noden og dens edges. - -`users`-tabellen krymper til autentisering: - -```sql -CREATE TABLE auth_identities ( - node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - authentik_sub TEXT UNIQUE NOT NULL, -- Authentik subject ID - email TEXT UNIQUE NOT NULL -); -``` - -Brukerens profil, innstillinger og metadata lever på noden (JSONB) -eller som edges. Autentiseringstabellen er en tynn bro mellom -"denne HTTP-sesjonen" og "denne noden i grafen." - -## Aliaser - -En bruker kan opprette aliaser — separate noder som representerer -en offentlig persona eller anonym identitet. - -``` -Aleks (hidden) ──alias──→ Bjørn (readable) -``` - -Bjørn er en egen node med eget navn, egen profil, egen visibility. -Omverdenen ser bare Bjørn. `alias`-edgen er en **systemedge** — -usynlig ved traversering, aldri eksponert utad. Ellers ville -"hvem kontrollerer Bjørn?" lekke Aleks' identitet. - -### Én flate, kontekst bestemmer identitet - -Aleks ser *alt* i sin mottaksflate — egne noder og alt som -kommer til Bjørn. Han svarer fra sin egen flate. Systemet -bestemmer `created_by` basert på konteksten: - -- NN skrev til Bjørn → Aleks svarer → meldingen stemplet - med Bjørn som `created_by`. Automatisk, ingen bytte. -- Vegard skrev til Aleks direkte → svaret kommer fra Aleks. - -**Ingen manuell identitetsbytte.** Aleks trenger aldri å "logge -inn som Bjørn" eller bytte modus. Konteksten (hvilken samtale, -hvilken kanal, hvem mottakeren kjenner) bestemmer hvilken node -som er avsender. - -### Identitetsvalg ved tvetydighet - -Noen ganger kjenner mottakeren *både* personen og aliaset. -Vegard kjenner Aleks og vet hvem Bjørn er. Hvis Vegard sender -en melding til Bjørn under en episode, og Aleks svarer: - -- **Default:** svaret kommer fra Bjørn (konteksten er Bjørn). -- **Valg:** systemet vet at Vegard har edge til Aleks. Aleks - kan velge "svar som Aleks" — da startes en ny, separat - samtale mellom Vegard og Aleks. - -**Alias-grensen brytes aldri automatisk.** Bare med eksplisitt -handling fra personen bak aliaset. - -### Flere aliaser - -Ingenting stopper en bruker fra å ha flere aliaser — Bjørn for -podcast, et anonymt alias for publisering, seg selv for privat. -Hver er en node med en `alias`-edge fra brukernoden. Alle samles -i én mottaksflate. - -### Alias vs. owner - -| Edge | Betydning | Eksempel | -|------|-----------|----------| -| `alias` | Jeg *er* denne noden. Usynlig systemedge. | Aleks → Bjørn | -| `owner` | Jeg *forvalter* denne noden. Synlig for medlemmer. | Vegard → Sidelinja | - -`alias` = identitet. `owner` = ansvar. Aleks *er* Bjørn. Vegard -*eier* Sidelinja, men han *er* ikke Sidelinja. - -## Team er noder - -Et team er en node med `member`-edges til brukernoder. Gi teamet -tilgang til en samlings-node → alle teammedlemmer arver tilgang -via transitiv traversering. Ingen egen team-mekanisme. - -``` -Vegard (node) ──member──→ Podcastteamet (node) ──member──→ Sidelinja (node) -Trond (node) ──member──→ Podcastteamet (node) -``` - -Trond får tilgang til alt under Sidelinja fordi han er medlem av -Podcastteamet som er medlem av Sidelinja. Tilgangsmatrisen beregner -dette transitivt. - -## Samlings-noder - -En samlings-node er en vanlig node som fungerer som gravitasjonspunkt. -"Sidelinja" er en samlings-node Vegard oppretter og eier. Trond -kobles på — direkte eller via et team. Andre inviteres inn. - -Det kan finnes mange samlings-noder — eller få. Et podcast-prosjekt, -en research-samling, en vennegjeng. De er ikke noe spesielt i -datamodellen — bare noder med edges til andre noder. - -En innholdsnode kan ha edge til flere samlings-noder. En faktoid om -en gjest er relevant for både podcast-prosjektet og research-samlingen. -Ikke kopi — samme node, to edges. - -### Felles kontekst - -En samlings-node bærer kontekst (i JSONB) som arves av tilknyttede -noder: - -- **Pruning-profil** — hvor aggressivt slettes binærdata? -- **Tema** — visuelt uttrykk -- **AI-konfigurasjon** — prompts, modell, regler -- **Default synlighet** — for nye noder opprettet i denne konteksten -- **Kapasitet** — ressursgrenser for maskinrommet - -Konflikter (node med edge til to samlings-noder med ulike -pruning-profiler) løses med: mest konservativ vinner, eller -eier-edge bestemmer. - -## Tilgangsroller - -| Rolle | Hva den gir | -|-------|------------| -| `owner` | Full kontroll — slette, endre tilgang, endre innstillinger | -| `admin` | Kan invitere/fjerne andre, endre konfigurasjon | -| `member` | Kan gi input og motta | -| `reader` | Kan kun motta (observatør, lytter) | - -Roller er en egenskap på edgen, ikke på noden. Vegard har `owner`-edge -til Sidelinja og `member`-edge til Research-gruppa. Samme node, ulike -roller i ulike kontekster. - -## Tilgangsmatrise — spesifikasjon - -### Skjema - -```sql -CREATE TYPE access_level AS ENUM ('reader', 'member', 'admin', 'owner'); - -CREATE TABLE node_access ( - subject_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, - object_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, - access access_level NOT NULL, - via_edge UUID REFERENCES edges(id) ON DELETE CASCADE, - PRIMARY KEY (subject_id, object_id) -); - -CREATE INDEX idx_na_subject ON node_access (subject_id); -``` - -`subject_id` er noden som har tilgang (bruker eller team). -`object_id` er noden det gis tilgang til. -`via_edge` peker på edgen som ga tilgangen — for debugging og -deterministisk revokering. - -### RLS-policy - -```sql -CREATE POLICY node_read ON nodes FOR SELECT - USING ( - created_by = current_node_id() - OR id IN ( - SELECT object_id FROM node_access - WHERE subject_id = current_node_id() - ) - OR visibility >= 'discoverable' - ); -``` - -`current_node_id()` returnerer brukernodens id fra sesjonen. -Tre sjekker i prioritert rekkefølge: -1. Egne noder (`created_by`) — instant -2. Eksplisitt tilgang via matrisen — indeksert lookup -3. Offentlig synlige noder — kolonne-sjekk - -Merk: `discoverable` gir kun at noden *finnes* i søkeresultater. -Innholdet filtreres i applikasjonslaget basert på visibility-nivå. - -### Matrise-oppdatering - -Matrisen oppdateres **når edges endres**, ikke ved lesing. -Én transaksjon: edge + matrise-oppdatering. Alltid synkront — -ingen vindu med stale tilgang. - -**Transitiv tilgang:** Når Trond får `member`-edge til Sidelinja, -beregnes hans tilgang til alle noder med edge til Sidelinja. - -``` -Brukernode → samlings-node → innholdsnoder: 2 hopp -Brukernode → team → samlings-node → innholdsnoder: 3 hopp -Brukernode → team → samlings-node → komm.node → noder: 4 hopp -``` - -**Beregningsfunksjon:** - -```sql -CREATE OR REPLACE FUNCTION recompute_access( - p_subject_id UUID, -- bruker- eller teamnode - p_root_node_id UUID, -- noden det gis tilgang til - p_access access_level, - p_via_edge UUID -) RETURNS void AS $$ -BEGIN - -- Direkte tilgang til roten - INSERT INTO node_access (subject_id, object_id, access, via_edge) - VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge) - ON CONFLICT (subject_id, object_id) - DO UPDATE SET access = GREATEST(node_access.access, p_access); - - -- Transitiv: noder som tilhører roten - INSERT INTO node_access (subject_id, object_id, access, via_edge) - SELECT p_subject_id, e.source_id, p_access, p_via_edge - FROM edges e - WHERE e.target_id = p_root_node_id - AND e.edge_type = 'belongs_to' - ON CONFLICT (subject_id, object_id) - DO UPDATE SET access = GREATEST(node_access.access, p_access); - - -- Hvis subject er et team: propager til alle teammedlemmer - INSERT INTO node_access (subject_id, object_id, access, via_edge) - SELECT e.source_id, na.object_id, na.access, na.via_edge - FROM node_access na - JOIN edges e ON e.target_id = p_subject_id - AND e.edge_type = 'member_of' - WHERE na.subject_id = p_subject_id - ON CONFLICT (subject_id, object_id) - DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access); -END; -$$ LANGUAGE plpgsql; -``` - -Detaljer (hvilke edge-typer som gir transitiv tilgang, maks dybde) -avklares ved implementering. - -### Matrisestørrelse - -Sparse matrise. Typisk bruker med tilgang til 2-3 samlings-noder -med noen tusen noder hver: ~10k rader per bruker. Med 50 brukere: -~500k rader. Trivielt for PG. - -## Brukeropplevelse - -Når du logger inn ser du: -- **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg -- **Dine noder** — alt du har skapt eller er koblet til -- **Dine samlings-noder** — grupperer kontekst, filtrering er frivillig -- **Din mottaksflate** — det som er relevant for deg nå - -Å filtrere etter samlings-node ("vis bare podcast-prosjektet") er et -filter, ikke en modebytte. - -## Forhold til andre retninger - -- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er summen av dine - edges, ikke en container du går inn i -- [Universell input og mottak](universell_input.md) — mottaksflaten - er "noder med edge til *meg*", filtrert og vektet -- [Maskinrommet](maskinrommet.md) — eier matrise-oppdatering og leser - samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig) -- [Datalaget](datalaget.md) — matrisen lever i PG, indeksert for - RLS-ytelse - - -================================================================ -FILE: docs/retninger/datalaget.md -================================================================ - -# Datalaget - -**Status: Besluttet.** - -> SpacetimeDB holder hele grafen i minne og mottar skrivinger først. -> PostgreSQL er persistent arkiv og backup. CAS lagrer binærdata. -> Apache AGE legges til ved behov for Cypher-traverseringer. - -## Lagmodell - -``` -GUI (SvelteKit) - │ skriv │ les (sanntid, WebSocket) - ▼ ▼ -Maskinrommet (Rust) SpacetimeDB ──→ GUI - │ validering ▲ - ├──→ SpacetimeDB (først) ───┘ - └──→ PostgreSQL (asynk, persistent) -``` - -### Skrivestien -GUI → Maskinrommet → validering → SpacetimeDB (instant) → PG (asynk). -Frontend oppdateres umiddelbart. PG persisterer i bakgrunnen. - -### Lesestien (sanntid) -SpacetimeDB → GUI (direkte WebSocket, ~10μs). -Hele grafen er i SpacetimeDB. Frontend har alltid alt tilgjengelig. - -### Lesestien (tunge spørringer) -GUI → Maskinrommet → PG. -Fulltekstsøk, pgvector (semantisk søk), statistikk, AGE-traverseringer. - -## SpacetimeDB — hele grafen i minne - -Node- og edge-skjemaet er minimalt (åtte kolonner hver). For en -liten brukerbase er hele grafen triviell å holde i minne. Fordeler: - -- Ingen sync-logikk for å bestemme hva som er "aktivt" -- Ingen henting fra PG når noen åpner noe gammelt -- Frontend har alltid alt via WebSocket -- Én lesekilde — ingen tvetydighet - -Når dette blir problematisk (hundretusenvis av noder), innføres -eviction. Det er en optimalisering, ikke en arkitekturendring. - -## PostgreSQL — arkiv og kraftspørringer - -PG er persistent backup for hele grafen, pluss hjemsted for -tunge operasjoner SpacetimeDB ikke er laget for: - -- **Fulltekstsøk** — `tsvector` på `nodes.content` og `nodes.title` -- **Semantisk søk** — pgvector for embedding-basert likhet -- **Graftraversering** — rekursive CTEs, Apache AGE ved behov -- **Statistikk** — aggregeringer, tidsserier -- **Tilgangsmatrise** — `node_access` beregnes her, speiles til STDB - -### Apache AGE — ved behov - -De fleste spørringer er grunne (1-3 hopp) og håndteres av CTEs. -AGE legges til som PG-extension når Cypher-semantikk faktisk trengs: - -1. **Nå:** PG med nodes/edges-tabeller og CTEs -2. **Når CTEs blir smertefulle:** Legg til AGE -3. **Usannsynlig:** Evaluer Neo4j hvis AGE ikke holder - -AGE er en extension, ikke en migrering — boltes på uten å endre -eksisterende kode. - -## CAS — binærlagring - -Lyd, bilde, video lagres content-addressable på disk. CAS-noder -i grafen bærer metadata (`cas_hash`, `mime`, `size_bytes`). -Selve biten lever utenfor PG. - -Pruning-regler basert på modalitet, edges og aksessmønstre. -Se [maskinrommet](maskinrommet.md). - -## SpacetimeDB som utbyttbar - -SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den -fjernes: - -- **Sanntid:** PG `LISTEN/NOTIFY` → SvelteKit SSE -- **Skriving:** Maskinrommet → PG direkte -- **Lesing:** Maskinrommet → PG → GUI - -Trenger ikke implementeres, men arkitekturen skal aldri gjøre -det umulig. Se [synkronisering](../infra/synkronisering.md). - -## Forhold til andre retninger - -- [Noder er sentrum](bruker_ikke_workspace.md) — tilgangsmatrise - beregnet fra edge-grafen, speiles til SpacetimeDB -- [Universell input og mottak](universell_input.md) — noder og edges - er datamodellen for alle tre primitiver -- [Maskinrommet](maskinrommet.md) — CAS-pruning, edge-drevet - ressursorkestrering, validering før skriving - - -================================================================ -FILE: docs/retninger/maskinrommet.md -================================================================ - -# Maskinrommet - -**Status: Besluttet.** - -> Én Rust-tjeneste med et fast grensesnitt. Alt som krever tunge -> ressurser eller eksterne tjenester går gjennom dette laget. -> Fang, prosesser, lever. Maskinrommet er det eneste som skriver -> noder og edges. - -## Tre operasjoner - -### 1. Fang (input-absorpsjon) -Ta imot råmateriale i alle modaliteter: -- Tekst (melding, URL, dokument) -- Lyd (voice memo, live stream, filopplasting) -- Bilde (foto, skjermbilde, tegning) -- Video (stream, opptak) -- Strukturert data (JSON, metadata, edges) - -### 2. Prosesser (transformasjon) -Analyser, transformer, berik og systematiser: -- **STT** — lyd → tekst (Whisper) -- **TTS** — tekst → lyd (ElevenLabs / lokal modell) -- **AI-analyse** — oppsummering, klassifisering, edge-forslag -- **Beriking** — URL → metadata, bilde → beskrivelse -- **Søk** — fulltekst, semantisk (pgvector), graftraversering -- **Mediaprosessering** — transcode, thumbnail, waveform - -### 3. Lever (output-distribusjon) -Lever resultat i riktig modalitet til riktig mottaker: -- Tekst (melding, notifikasjon, digest) -- Lyd (TTS-opplesning, lydstream) -- Video/bilde (stream, thumbnail) -- Strukturert data (noder, edges tilbake i grafen) -- Push (SpacetimeDB-reducer) - -## Maskinrommet eier all skriving - -Frontend sender intensjoner. Maskinrommet utfører. - -``` -Frontend: "legg til Trond i møtet" - → Maskinrommet validerer - → Skriver edge til SpacetimeDB (instant) - → Oppdaterer tilgangsmatrise - → Persisterer til PG (asynk) - → Reagerer på konsekvensene (koble inn LiveKit, starte transkripsjon) -``` - -Alt i én operasjon. Maskinrommet er ikke reaktivt i en pub/sub- -forstand — det orkestrerer hele sekvensen. Enklere å forstå, -enklere å debugge. - -Skrivestien: validering → SpacetimeDB (instant) → PG (asynk). -Se [synkronisering](../infra/synkronisering.md). - -## Edge-drevet ressursorkestrering - -Maskinrommet leser edges for å vite hva det skal gjøre. Noden -er alltid enkel. Edges bestemmer hvilke ressurser som spinnes opp. - -### Privat er default -Input uten mottaker-edge er automatisk privat. Ingen ressurser -kobles inn utover det grunnleggende (fang + transkriber). - -### Ressurser er proporsjonale med edges - -``` -Dagboknotat (privat voice memo): - node → fang lyd → transkriber (Whisper) → lagre - Ressurser: minimal - -Samtale med Trond: - node + mottaker-edge(Trond) - → fang lyd → transkriber → lever tekst/lyd til Trond - Ressurser: STT + levering til én - -Redaksjonsmøte (5 deltakere): - node + mottaker-edges(5) + rolle-edges - → fang lyd fra alle → transkriber → lever til alle → AI-referent - Ressurser: STT + levering til 5 + LLM - -Livesending (1000 lyttere): - node + mottaker-edges(∞) + stream-edge - → fang lyd → transkriber → stream via LiveKit → distribuer - → generer segmenter → kjør live AI → publiser - Ressurser: STT + LiveKit + LLM + mediaprosessering -``` - -### Naturlig eskalering -Du starter en privat voice-note. Deler den med Trond → legg til -edge, maskinrommet begynner å levere. Trond foreslår møte → flere -edges, maskinrommet kobler inn sanntidsstrømming. Møtet blir -innspilling → publiserings-edge, maskinrommet aktiverer -produksjonspipeline. Hvert steg er bare å legge til edges. - -## Grensesnittet - -``` -fang(input: RåInput) → NodeId -prosesser(node: NodeId, operasjon: Operasjon) → Resultat -lever(node: NodeId, mottaker: Mottaker, format: Format) → Status -``` - -I praksis er mye av dette implisitt: maskinrommet ser hvilke edges -som ble skrevet og handler deretter. Primitivene manipulerer edges -via maskinrommet, og maskinrommet kobler inn riktige ressurser. - -## CAS og intelligent pruning - -### CAS som lagringsprimitiv -All binærdata (lyd, bilde, video) lagres content-addressable. -CAS-noder i grafen bærer metadata (`cas_hash`, `mime`, `size`). -Selve biten lever på disk. - -- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi -- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet - er tilgjengelig" -- **Enkel opprydning** — slett filen fra CAS, noden beholder metadata - -### Lagringsregler per modalitet - -| Modalitet | Default levetid | Begrunnelse | -|-----------|----------------|-------------| -| Tekst | Evig | Billig, essensen av innholdet | -| Transkripsjon | Evig | Tekstlig representasjon — bevarer meningen | -| Lyd | 30 dager | Transkripsjon bevarer innholdet | -| Bilde | 30 dager | Beskrivelse/metadata bevarer kontekst | -| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste | - -### Signaler som forlenger levetid - -- **Edges.** Lydfil med publiserings-edge = beholdes. Privat - voice-memo uten edges = 30-dagers TTL. -- **Aksesslog.** Avspilt i løpet av TTL-perioden = forlenges. -- **Transkripsjonsstatus.** Utranskribert lyd kan trenge lengre TTL. -- **Edge-type.** Publisert = behold. Arkivert møte = transkripsjon - holder. - -### Generert innhold er en cache -TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan -regenereres er en cache i CAS med samme TTL-mekanisme. - -### Samlings-node-styrt aggressivitet -Hver samlings-node kan justere sin pruning-profil: -- **Konservativt** — behold alt lenge (arkiv-node) -- **Aggressivt** — tekst bevares, binærdata prunes raskt -- **Tilpasset** — egne regler per modalitet og edge-type - -### Disk-nødventil -Maskinrommet overvåker diskbruk: -- **>85 %:** Genererte filer slettes (kan regenereres) -- **>90 %:** Aggressiv pruning for alle samlings-noder -- **>95 %:** Kritisk alarm. Alt uten publiserings-edge slettes. - Tekst og transkripsjoner bevares alltid. - -## Isolasjon og observerbarhet - -### Isolasjon -Bytt Whisper med noe annet? Endre maskinrommet. Frontend vet -ingenting. Legg til bildegenerering? Ny operasjon. Primitivene -kaller den uten å vite hva som skjer under. - -### Observerbarhet -Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering -— alt på ett sted. "Hva bruker vi AI-ressurser på?" har ett svar. - -### Kapasitetsstyring -Prioritering, kø, rate limiting, fallback mellom leverandører. -Live transkripsjon prioriteres over bakgrunns-oppsummering. - -## Compute-separasjon - -Maskinrommet orkestrerer — tunge jobber trenger ikke kjøre på -samme maskin. - -- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch. -- **Snart:** Trekk ut tunge workers til separat node (billig - ARM-instans) som poller jobbkøen. Maskinrommet ruter transparent. -- **Kildevern-modus:** Lokal LLM krever dedikert compute med - fysisk isolasjon. - -Compute-separasjon er en konfigurasjon, ikke en arkitekturendring. - -## Forhold til andre retninger - -Maskinrommet er infrastrukturen *under* de tre primitivene i -[universell input og mottak](universell_input.md): -- Input-primitiven → `fang()` + `prosesser()` -- Mottak-primitiven → `lever()` -- Kommunikasjonsnoden → alle tre - -- [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet - eier tilgangsmatrise-oppdatering -- [Datalaget](datalaget.md) — maskinrommet skriver SpacetimeDB - først, PG asynk - - -================================================================ -FILE: docs/retninger/rom_ikke_forum.md -================================================================ - + + +================================================================ +FILE: docs/retninger/bruker_ikke_workspace.md +================================================================ + +# Noder er sentrum + +**Status: Besluttet.** + +> Alt er noder. En bruker er en node. Et team er en node. Et møte er +> en node. Sidelinja er en node. Relasjoner mellom dem er edges. +> Tilgangskontroll via materialisert tilgangsmatrise beregnet fra +> edge-grafen. + +## Beslutningen + +1. **Alt er noder.** Brukere, team, prosjekter, innhold, møter — + alt er rader i `nodes`-tabellen. En bruker er en node som + tilfeldigvis kan logge inn. +2. **Relasjoner er edges.** Vegard → Sidelinja (`owner`). + Trond → Sidelinja (`member`). Møtereferat → møte (`belongs_to`). +3. **Ingen containere.** Hva du ser er summen av dine edges. +4. **Samlings-noder gir struktur** — de er vanlige noder som + fungerer som gravitasjonspunkt. +5. **Privat er default** — en node uten edges til andre er kun din. +6. **Tilgangskontroll via `node_access`-matrise**, oppdatert ved + edge-endring, brukt av RLS ved lesing. + +## Visibility + +Visibility er en egenskap på noden som definerer hva som gjelder +for alle *uten* eksplisitt edge. Eksplisitte edges overrider alltid +oppover. + +```sql +CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open'); +``` + +| Nivå | Oppdagbar | Lesbar | Interagerbar | +|------|-----------|--------|-------------| +| `hidden` | Nei | Nei | Nei — kun via eksplisitt edge | +| `discoverable` | Ja (søk/katalog) | Nei | Kontaktforespørsel | +| `readable` | Ja | Ja | Nei — krever edge | +| `open` | Ja | Ja | Ja | + +Eksempler: +- Spøkelsesbruker: `hidden` — usynlig, kun invitasjon +- Katalogbruker: `discoverable` — finnes i søk, profil krever edge +- Podcast-episode: `readable` — alle kan lytte, bare teamet kan redigere +- Kunnskapsgraf-entitet: `open` — alle kan se og bidra + +### Traverseringsregelen + +**Visibility er en hard grense ved traversering.** Når du følger +edges i grafen, kan du bare se noder hvis deres visibility tillater +det — eller du har en eksplisitt edge. + +Eksempel: Episode #42 (`readable`) har en `host`-edge til Peter +(`hidden`). Anonym bruker leser episoden, følger `host`-edge → +Peter er `hidden` → **stopp, usynlig.** Trond (som har edge til +Peter) følger samme edge → **ser Peter.** + +Ingen transitivitet bryter denne regelen. Uansett hvor mange +offentlige noder som har edges til en `hidden` node, forblir den +skjult for de uten eksplisitt edge. + +## Brukere er noder + +En brukernode er en node med en kobling til Authentik for +autentisering. Alt annet — navn, preferanser, roller, relasjoner — +er noden og dens edges. + +`users`-tabellen krymper til autentisering: + +```sql +CREATE TABLE auth_identities ( + node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + authentik_sub TEXT UNIQUE NOT NULL, -- Authentik subject ID + email TEXT UNIQUE NOT NULL +); +``` + +Brukerens profil, innstillinger og metadata lever på noden (JSONB) +eller som edges. Autentiseringstabellen er en tynn bro mellom +"denne HTTP-sesjonen" og "denne noden i grafen." + +## Aliaser + +En bruker kan opprette aliaser — separate noder som representerer +en offentlig persona eller anonym identitet. + +``` +Aleks (hidden) ──alias──→ Bjørn (readable) +``` + +Bjørn er en egen node med eget navn, egen profil, egen visibility. +Omverdenen ser bare Bjørn. `alias`-edgen er en **systemedge** — +usynlig ved traversering, aldri eksponert utad. Ellers ville +"hvem kontrollerer Bjørn?" lekke Aleks' identitet. + +### Én flate, kontekst bestemmer identitet + +Aleks ser *alt* i sin mottaksflate — egne noder og alt som +kommer til Bjørn. Han svarer fra sin egen flate. Systemet +bestemmer `created_by` basert på konteksten: + +- NN skrev til Bjørn → Aleks svarer → meldingen stemplet + med Bjørn som `created_by`. Automatisk, ingen bytte. +- Vegard skrev til Aleks direkte → svaret kommer fra Aleks. + +**Ingen manuell identitetsbytte.** Aleks trenger aldri å "logge +inn som Bjørn" eller bytte modus. Konteksten (hvilken samtale, +hvilken kanal, hvem mottakeren kjenner) bestemmer hvilken node +som er avsender. + +### Identitetsvalg ved tvetydighet + +Noen ganger kjenner mottakeren *både* personen og aliaset. +Vegard kjenner Aleks og vet hvem Bjørn er. Hvis Vegard sender +en melding til Bjørn under en episode, og Aleks svarer: + +- **Default:** svaret kommer fra Bjørn (konteksten er Bjørn). +- **Valg:** systemet vet at Vegard har edge til Aleks. Aleks + kan velge "svar som Aleks" — da startes en ny, separat + samtale mellom Vegard og Aleks. + +**Alias-grensen brytes aldri automatisk.** Bare med eksplisitt +handling fra personen bak aliaset. + +### Flere aliaser + +Ingenting stopper en bruker fra å ha flere aliaser — Bjørn for +podcast, et anonymt alias for publisering, seg selv for privat. +Hver er en node med en `alias`-edge fra brukernoden. Alle samles +i én mottaksflate. + +### Alias vs. owner + +| Edge | Betydning | Eksempel | +|------|-----------|----------| +| `alias` | Jeg *er* denne noden. Usynlig systemedge. | Aleks → Bjørn | +| `owner` | Jeg *forvalter* denne noden. Synlig for medlemmer. | Vegard → Sidelinja | + +`alias` = identitet. `owner` = ansvar. Aleks *er* Bjørn. Vegard +*eier* Sidelinja, men han *er* ikke Sidelinja. + +## Team er noder + +Et team er en node med `member`-edges til brukernoder. Gi teamet +tilgang til en samlings-node → alle teammedlemmer arver tilgang +via transitiv traversering. Ingen egen team-mekanisme. + +``` +Vegard (node) ──member──→ Podcastteamet (node) ──member──→ Sidelinja (node) +Trond (node) ──member──→ Podcastteamet (node) +``` + +Trond får tilgang til alt under Sidelinja fordi han er medlem av +Podcastteamet som er medlem av Sidelinja. Tilgangsmatrisen beregner +dette transitivt. + +## Samlings-noder + +En samlings-node er en vanlig node som fungerer som gravitasjonspunkt. +"Sidelinja" er en samlings-node Vegard oppretter og eier. Trond +kobles på — direkte eller via et team. Andre inviteres inn. + +Det kan finnes mange samlings-noder — eller få. Et podcast-prosjekt, +en research-samling, en vennegjeng. De er ikke noe spesielt i +datamodellen — bare noder med edges til andre noder. + +En innholdsnode kan ha edge til flere samlings-noder. En faktoid om +en gjest er relevant for både podcast-prosjektet og research-samlingen. +Ikke kopi — samme node, to edges. + +### Felles kontekst + +En samlings-node bærer kontekst (i JSONB) som arves av tilknyttede +noder: + +- **Pruning-profil** — hvor aggressivt slettes binærdata? +- **Tema** — visuelt uttrykk +- **AI-konfigurasjon** — prompts, modell, regler +- **Default synlighet** — for nye noder opprettet i denne konteksten +- **Kapasitet** — ressursgrenser for maskinrommet + +Konflikter (node med edge til to samlings-noder med ulike +pruning-profiler) løses med: mest konservativ vinner, eller +eier-edge bestemmer. + +## Tilgangsroller + +| Rolle | Hva den gir | +|-------|------------| +| `owner` | Full kontroll — slette, endre tilgang, endre innstillinger | +| `admin` | Kan invitere/fjerne andre, endre konfigurasjon | +| `member` | Kan gi input og motta | +| `reader` | Kan kun motta (observatør, lytter) | + +Roller er en egenskap på edgen, ikke på noden. Vegard har `owner`-edge +til Sidelinja og `member`-edge til Research-gruppa. Samme node, ulike +roller i ulike kontekster. + +## Tilgangsmatrise — spesifikasjon + +### Skjema + +```sql +CREATE TYPE access_level AS ENUM ('reader', 'member', 'admin', 'owner'); + +CREATE TABLE node_access ( + subject_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + object_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + access access_level NOT NULL, + via_edge UUID REFERENCES edges(id) ON DELETE CASCADE, + PRIMARY KEY (subject_id, object_id) +); + +CREATE INDEX idx_na_subject ON node_access (subject_id); +``` + +`subject_id` er noden som har tilgang (bruker eller team). +`object_id` er noden det gis tilgang til. +`via_edge` peker på edgen som ga tilgangen — for debugging og +deterministisk revokering. + +### RLS-policy + +```sql +CREATE POLICY node_read ON nodes FOR SELECT + USING ( + created_by = current_node_id() + OR id IN ( + SELECT object_id FROM node_access + WHERE subject_id = current_node_id() + ) + OR visibility >= 'discoverable' + ); +``` + +`current_node_id()` returnerer brukernodens id fra sesjonen. +Tre sjekker i prioritert rekkefølge: +1. Egne noder (`created_by`) — instant +2. Eksplisitt tilgang via matrisen — indeksert lookup +3. Offentlig synlige noder — kolonne-sjekk + +Merk: `discoverable` gir kun at noden *finnes* i søkeresultater. +Innholdet filtreres i applikasjonslaget basert på visibility-nivå. + +### Matrise-oppdatering + +Matrisen oppdateres **når edges endres**, ikke ved lesing. +Én transaksjon: edge + matrise-oppdatering. Alltid synkront — +ingen vindu med stale tilgang. + +**Transitiv tilgang:** Når Trond får `member`-edge til Sidelinja, +beregnes hans tilgang til alle noder med edge til Sidelinja. + +``` +Brukernode → samlings-node → innholdsnoder: 2 hopp +Brukernode → team → samlings-node → innholdsnoder: 3 hopp +Brukernode → team → samlings-node → komm.node → noder: 4 hopp +``` + +**Beregningsfunksjon:** + +```sql +CREATE OR REPLACE FUNCTION recompute_access( + p_subject_id UUID, -- bruker- eller teamnode + p_root_node_id UUID, -- noden det gis tilgang til + p_access access_level, + p_via_edge UUID +) RETURNS void AS $$ +BEGIN + -- Direkte tilgang til roten + INSERT INTO node_access (subject_id, object_id, access, via_edge) + VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge) + ON CONFLICT (subject_id, object_id) + DO UPDATE SET access = GREATEST(node_access.access, p_access); + + -- Transitiv: noder som tilhører roten + INSERT INTO node_access (subject_id, object_id, access, via_edge) + SELECT p_subject_id, e.source_id, p_access, p_via_edge + FROM edges e + WHERE e.target_id = p_root_node_id + AND e.edge_type = 'belongs_to' + ON CONFLICT (subject_id, object_id) + DO UPDATE SET access = GREATEST(node_access.access, p_access); + + -- Hvis subject er et team: propager til alle teammedlemmer + INSERT INTO node_access (subject_id, object_id, access, via_edge) + SELECT e.source_id, na.object_id, na.access, na.via_edge + FROM node_access na + JOIN edges e ON e.target_id = p_subject_id + AND e.edge_type = 'member_of' + WHERE na.subject_id = p_subject_id + ON CONFLICT (subject_id, object_id) + DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access); +END; +$$ LANGUAGE plpgsql; +``` + +Detaljer (hvilke edge-typer som gir transitiv tilgang, maks dybde) +avklares ved implementering. + +### Matrisestørrelse + +Sparse matrise. Typisk bruker med tilgang til 2-3 samlings-noder +med noen tusen noder hver: ~10k rader per bruker. Med 50 brukere: +~500k rader. Trivielt for PG. + +## Brukeropplevelse + +Når du logger inn ser du: +- **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg +- **Dine noder** — alt du har skapt eller er koblet til +- **Dine samlings-noder** — grupperer kontekst, filtrering er frivillig +- **Din mottaksflate** — det som er relevant for deg nå + +Å filtrere etter samlings-node ("vis bare podcast-prosjektet") er et +filter, ikke en modebytte. + +## Forhold til andre retninger + +- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er summen av dine + edges, ikke en container du går inn i +- [Universell input og mottak](universell_input.md) — mottaksflaten + er "noder med edge til *meg*", filtrert og vektet +- [Maskinrommet](maskinrommet.md) — eier matrise-oppdatering og leser + samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig) +- [Datalaget](datalaget.md) — matrisen lever i PG, indeksert for + RLS-ytelse + + +================================================================ +FILE: docs/retninger/datalaget.md +================================================================ + +# Datalaget + +**Status: Besluttet.** + +> SpacetimeDB holder hele grafen i minne og mottar skrivinger først. +> PostgreSQL er persistent arkiv og backup. CAS lagrer binærdata. +> Apache AGE legges til ved behov for Cypher-traverseringer. + +## Lagmodell + +``` +GUI (SvelteKit) + │ skriv │ les (sanntid, WebSocket) + ▼ ▼ +Maskinrommet (Rust) SpacetimeDB ──→ GUI + │ validering ▲ + ├──→ SpacetimeDB (først) ───┘ + └──→ PostgreSQL (asynk, persistent) +``` + +### Skrivestien +GUI → Maskinrommet → validering → SpacetimeDB (instant) → PG (asynk). +Frontend oppdateres umiddelbart. PG persisterer i bakgrunnen. + +### Lesestien (sanntid) +SpacetimeDB → GUI (direkte WebSocket, ~10μs). +Hele grafen er i SpacetimeDB. Frontend har alltid alt tilgjengelig. + +### Lesestien (tunge spørringer) +GUI → Maskinrommet → PG. +Fulltekstsøk, pgvector (semantisk søk), statistikk, AGE-traverseringer. + +## SpacetimeDB — hele grafen i minne + +Node- og edge-skjemaet er minimalt (åtte kolonner hver). For en +liten brukerbase er hele grafen triviell å holde i minne. Fordeler: + +- Ingen sync-logikk for å bestemme hva som er "aktivt" +- Ingen henting fra PG når noen åpner noe gammelt +- Frontend har alltid alt via WebSocket +- Én lesekilde — ingen tvetydighet + +Når dette blir problematisk (hundretusenvis av noder), innføres +eviction. Det er en optimalisering, ikke en arkitekturendring. + +## PostgreSQL — arkiv og kraftspørringer + +PG er persistent backup for hele grafen, pluss hjemsted for +tunge operasjoner SpacetimeDB ikke er laget for: + +- **Fulltekstsøk** — `tsvector` på `nodes.content` og `nodes.title` +- **Semantisk søk** — pgvector for embedding-basert likhet +- **Graftraversering** — rekursive CTEs, Apache AGE ved behov +- **Statistikk** — aggregeringer, tidsserier +- **Tilgangsmatrise** — `node_access` beregnes her, speiles til STDB + +### Apache AGE — ved behov + +De fleste spørringer er grunne (1-3 hopp) og håndteres av CTEs. +AGE legges til som PG-extension når Cypher-semantikk faktisk trengs: + +1. **Nå:** PG med nodes/edges-tabeller og CTEs +2. **Når CTEs blir smertefulle:** Legg til AGE +3. **Usannsynlig:** Evaluer Neo4j hvis AGE ikke holder + +AGE er en extension, ikke en migrering — boltes på uten å endre +eksisterende kode. + +## CAS — binærlagring + +Lyd, bilde, video lagres content-addressable på disk. CAS-noder +i grafen bærer metadata (`cas_hash`, `mime`, `size_bytes`). +Selve biten lever utenfor PG. + +Pruning-regler basert på modalitet, edges og aksessmønstre. +Se [maskinrommet](maskinrommet.md). + +## SpacetimeDB som utbyttbar + +SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den +fjernes: + +- **Sanntid:** PG `LISTEN/NOTIFY` → SvelteKit SSE +- **Skriving:** Maskinrommet → PG direkte +- **Lesing:** Maskinrommet → PG → GUI + +Trenger ikke implementeres, men arkitekturen skal aldri gjøre +det umulig. Se [synkronisering](../infra/synkronisering.md). + +## Forhold til andre retninger + +- [Noder er sentrum](bruker_ikke_workspace.md) — tilgangsmatrise + beregnet fra edge-grafen, speiles til SpacetimeDB +- [Universell input og mottak](universell_input.md) — noder og edges + er datamodellen for alle tre primitiver +- [Maskinrommet](maskinrommet.md) — CAS-pruning, edge-drevet + ressursorkestrering, validering før skriving + + +================================================================ +FILE: docs/retninger/maskinrommet.md +================================================================ + +# Maskinrommet + +**Status: Besluttet.** + +> Én Rust-tjeneste med et fast grensesnitt. Alt som krever tunge +> ressurser eller eksterne tjenester går gjennom dette laget. +> Fang, prosesser, lever. Maskinrommet er det eneste som skriver +> noder og edges. + +## Tre operasjoner + +### 1. Fang (input-absorpsjon) +Ta imot råmateriale i alle modaliteter: +- Tekst (melding, URL, dokument) +- Lyd (voice memo, live stream, filopplasting) +- Bilde (foto, skjermbilde, tegning) +- Video (stream, opptak) +- Strukturert data (JSON, metadata, edges) + +### 2. Prosesser (transformasjon) +Analyser, transformer, berik og systematiser: +- **STT** — lyd → tekst (Whisper) +- **TTS** — tekst → lyd (ElevenLabs / lokal modell) +- **AI-analyse** — oppsummering, klassifisering, edge-forslag +- **Beriking** — URL → metadata, bilde → beskrivelse +- **Søk** — fulltekst, semantisk (pgvector), graftraversering +- **Mediaprosessering** — transcode, thumbnail, waveform + +### 3. Lever (output-distribusjon) +Lever resultat i riktig modalitet til riktig mottaker: +- Tekst (melding, notifikasjon, digest) +- Lyd (TTS-opplesning, lydstream) +- Video/bilde (stream, thumbnail) +- Strukturert data (noder, edges tilbake i grafen) +- Push (SpacetimeDB-reducer) + +## Maskinrommet eier all skriving + +Frontend sender intensjoner. Maskinrommet utfører. + +``` +Frontend: "legg til Trond i møtet" + → Maskinrommet validerer + → Skriver edge til SpacetimeDB (instant) + → Oppdaterer tilgangsmatrise + → Persisterer til PG (asynk) + → Reagerer på konsekvensene (koble inn LiveKit, starte transkripsjon) +``` + +Alt i én operasjon. Maskinrommet er ikke reaktivt i en pub/sub- +forstand — det orkestrerer hele sekvensen. Enklere å forstå, +enklere å debugge. + +Skrivestien: validering → SpacetimeDB (instant) → PG (asynk). +Se [synkronisering](../infra/synkronisering.md). + +## Edge-drevet ressursorkestrering + +Maskinrommet leser edges for å vite hva det skal gjøre. Noden +er alltid enkel. Edges bestemmer hvilke ressurser som spinnes opp. + +### Privat er default +Input uten mottaker-edge er automatisk privat. Ingen ressurser +kobles inn utover det grunnleggende (fang + transkriber). + +### Ressurser er proporsjonale med edges + +``` +Dagboknotat (privat voice memo): + node → fang lyd → transkriber (Whisper) → lagre + Ressurser: minimal + +Samtale med Trond: + node + mottaker-edge(Trond) + → fang lyd → transkriber → lever tekst/lyd til Trond + Ressurser: STT + levering til én + +Redaksjonsmøte (5 deltakere): + node + mottaker-edges(5) + rolle-edges + → fang lyd fra alle → transkriber → lever til alle → AI-referent + Ressurser: STT + levering til 5 + LLM + +Livesending (1000 lyttere): + node + mottaker-edges(∞) + stream-edge + → fang lyd → transkriber → stream via LiveKit → distribuer + → generer segmenter → kjør live AI → publiser + Ressurser: STT + LiveKit + LLM + mediaprosessering +``` + +### Naturlig eskalering +Du starter en privat voice-note. Deler den med Trond → legg til +edge, maskinrommet begynner å levere. Trond foreslår møte → flere +edges, maskinrommet kobler inn sanntidsstrømming. Møtet blir +innspilling → publiserings-edge, maskinrommet aktiverer +produksjonspipeline. Hvert steg er bare å legge til edges. + +## Grensesnittet + +``` +fang(input: RåInput) → NodeId +prosesser(node: NodeId, operasjon: Operasjon) → Resultat +lever(node: NodeId, mottaker: Mottaker, format: Format) → Status +``` + +I praksis er mye av dette implisitt: maskinrommet ser hvilke edges +som ble skrevet og handler deretter. Primitivene manipulerer edges +via maskinrommet, og maskinrommet kobler inn riktige ressurser. + +## CAS og intelligent pruning + +### CAS som lagringsprimitiv +All binærdata (lyd, bilde, video) lagres content-addressable. +CAS-noder i grafen bærer metadata (`cas_hash`, `mime`, `size`). +Selve biten lever på disk. + +- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi +- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet + er tilgjengelig" +- **Enkel opprydning** — slett filen fra CAS, noden beholder metadata + +### Lagringsregler per modalitet + +| Modalitet | Default levetid | Begrunnelse | +|-----------|----------------|-------------| +| Tekst | Evig | Billig, essensen av innholdet | +| Transkripsjon | Evig | Tekstlig representasjon — bevarer meningen | +| Lyd | 30 dager | Transkripsjon bevarer innholdet | +| Bilde | 30 dager | Beskrivelse/metadata bevarer kontekst | +| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste | + +### Signaler som forlenger levetid + +- **Edges.** Lydfil med publiserings-edge = beholdes. Privat + voice-memo uten edges = 30-dagers TTL. +- **Aksesslog.** Avspilt i løpet av TTL-perioden = forlenges. +- **Transkripsjonsstatus.** Utranskribert lyd kan trenge lengre TTL. +- **Edge-type.** Publisert = behold. Arkivert møte = transkripsjon + holder. + +### Generert innhold er en cache +TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan +regenereres er en cache i CAS med samme TTL-mekanisme. + +### Samlings-node-styrt aggressivitet +Hver samlings-node kan justere sin pruning-profil: +- **Konservativt** — behold alt lenge (arkiv-node) +- **Aggressivt** — tekst bevares, binærdata prunes raskt +- **Tilpasset** — egne regler per modalitet og edge-type + +### Disk-nødventil +Maskinrommet overvåker diskbruk: +- **>85 %:** Genererte filer slettes (kan regenereres) +- **>90 %:** Aggressiv pruning for alle samlings-noder +- **>95 %:** Kritisk alarm. Alt uten publiserings-edge slettes. + Tekst og transkripsjoner bevares alltid. + +## Isolasjon og observerbarhet + +### Isolasjon +Bytt Whisper med noe annet? Endre maskinrommet. Frontend vet +ingenting. Legg til bildegenerering? Ny operasjon. Primitivene +kaller den uten å vite hva som skjer under. + +### Observerbarhet +Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering +— alt på ett sted. "Hva bruker vi AI-ressurser på?" har ett svar. + +### Kapasitetsstyring +Prioritering, kø, rate limiting, fallback mellom leverandører. +Live transkripsjon prioriteres over bakgrunns-oppsummering. + +## Compute-separasjon + +Maskinrommet orkestrerer — tunge jobber trenger ikke kjøre på +samme maskin. + +- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch. +- **Snart:** Trekk ut tunge workers til separat node (billig + ARM-instans) som poller jobbkøen. Maskinrommet ruter transparent. +- **Kildevern-modus:** Lokal LLM krever dedikert compute med + fysisk isolasjon. + +Compute-separasjon er en konfigurasjon, ikke en arkitekturendring. + +## Forhold til andre retninger + +Maskinrommet er infrastrukturen *under* de tre primitivene i +[universell input og mottak](universell_input.md): +- Input-primitiven → `fang()` + `prosesser()` +- Mottak-primitiven → `lever()` +- Kommunikasjonsnoden → alle tre + +- [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet + eier tilgangsmatrise-oppdatering +- [Datalaget](datalaget.md) — maskinrommet skriver SpacetimeDB + først, PG asynk + + +================================================================ +FILE: docs/retninger/rom_ikke_forum.md +================================================================ + # Rom, ikke forum > Hva om Sidelinja ikke er en webapp med sanntidsfunksjoner, men en oppslukende @@ -1203,12 +1199,12 @@ en omskriving — den er kompatibel med inkrementell utvikling. Den gjenværende risikoen er mer subtil: at vi bruker mental energi på å vurdere "bør dette være sanntid?" for hver feature i stedet for å bare bygge. Pragmatisk default bør være: bygg tradisjonelt, løft senere. - - -================================================================ -FILE: docs/retninger/status_quo.md -================================================================ - + + +================================================================ +FILE: docs/retninger/status_quo.md +================================================================ + # Status quo — Hva Sidelinja er i dag > **Historisk dokument (v1).** Denne teksten beskriver tilstanden i v1 — @@ -1286,557 +1282,557 @@ Sidelinja har en sterkere grunnmur enn overflaten viser. Meldingsboksen, kunnskapsgrafen og AI-infrastrukturen er genuint interessante primitiver. Spørsmålet er ikke om vi har bygget feil — men om overflaten utnytter det fundamentet faktisk tillater. - - -================================================================ -FILE: docs/retninger/universell_input.md -================================================================ - -# Universell input og mottak - -**Status: Besluttet.** - -> Én multimodal input-primitiv. Én personlig mottaksflate. Alt som -> fanges er en node. Hva det "er" bestemmes av edges. Hvordan det -> presenteres bestemmes av mottakeren. - -## Input-primitiven - -Én overflate som fanger alt: - -- **Tekst** — skriving, Markdown, kodeblokker -- **Lyd** — voice memo, diktering → automatisk transkribert -- **Bilde** — foto, skjermbilde, tegning -- **AI-støtte** — spør AI, få forslag, la den transformere input -- **URL** — lim inn lenke, den berikes automatisk - -Brukeren gjør det samme uansett kontekst — skriver, snakker eller -tegner i input-feltet. Forskjellen mellom "dagbok", "chatmelding" -og "kanban-kort" er ikke hva brukeren gjør — det er hvilke edges -som knyttes til resultatet. - -### Én pipeline - -All input går gjennom samme tekniske pipeline: -maskinrommet → SpacetimeDB (instant) → PG (asynk). - -Konteksten bestemmer routing, ikke en teknisk modus: - -- Du er i et møte → inputen streames live til andre deltakere -- Du er alene på bussen → inputen lander som privat node -- Du er i en podcast-kanal → inputen går inn i produksjonspipeline - -Samme pipeline. Ulike edges. - -### Input-metode og innholdstype er ortogonale - -Du kan snakke inn et kanban-kort. Du kan tegne en kalenderoppføring. -Input-primitiven bryr seg ikke om hva det *blir* — den fanger det -som kommer inn. Alt etterpå er edges. - -### Én overflate å perfeksjonere - -All UX-investering konsentreres ett sted. Én perfekt input-opplevelse -— responsiv, multimodal, med god AI-støtte — i stedet for ti -middelmådige spesialgrensesnitt. - -## Editoren - -Input-komponenten er en TipTap-editor (ProseMirror-basert) som -konfigureres med ulike extensions basert på kontekst. - -### Kontekst setter default, brukeren bestemmer - -Konteksten (kommunikasjonsnoden du er i, visningen du bruker) -setter en *default* editor-konfigurasjon. Men brukeren kan alltid -overstyre — utvide til full editor eller forenkle. Ingen kunstig -grense mellom "chatmelding" og "artikkel." - -Du *kan* skrive en gjennomformatert tekst med overskrifter, bilder -og blockquotes i chatten. Om du vil publisere den etterpå er det -bare å legge til en publiserings-edge. Innholdet er det samme. - -Editoren husker brukerens valg per kontekst via preferanser på -brukernoden. - -### Presets - -| Kontekst | Default extensions | Eksempel | -|----------|-------------------|----------| -| Chat | Tekst, markdown, kodeblokker, lenker | Enkel melding | -| Artikkel/blogg | + overskrifter, bilder, embeds, blockquotes, tabeller | Publisert tekst | -| Show notes | + lister, tidskoder, lenker | Episodenotater | -| Kanban-kort | Tekst, sjekklister | Oppgavebeskrivelse | - -Presets er bare default — brukeren kan utvide eller forenkle med -en knapp eller tastatursnarvei. Ikke en modebytte, bare at flere -verktøy blir tilgjengelig. - -### Tekstlagring - -Noden lagrer to representasjoner: - -- `content TEXT` — ren tekst uten formatering, for fulltekstsøk - og enkel visning -- `metadata.document JSONB` — strukturert TipTap/ProseMirror- - dokument for rendering - -```json -{ - "type": "doc", - "content": [ - { "type": "paragraph", "content": [ - { "type": "text", "text": "Her er en intro." } - ]}, - { "type": "image", "attrs": { - "node_id": "uuid-av-cas-node", "alt": "Diagram" - }}, - { "type": "paragraph", "content": [ - { "type": "text", "text": "Teksten fortsetter." } - ]} - ] -} -``` - -`content` genereres automatisk fra dokumentet ved lagring — bare -teksten, uten markup. Editoren produserer begge. - -For enkle meldinger (ren tekst uten formatering) er -`metadata.document` null — `content` er alt som trengs. - -### Bilder og media i tekst - -Bilder i dokumentet refererer til CAS-noder via `node_id`. -CAS-noden er en egen node med `has_media`-edge til innholdsnoden. -Dokumentstrukturen bestemmer *hvor* bildet plasseres i teksten. - -Tekst er *på* noden. Binærfiler er *andre* noder koblet med edges. -Ren separasjon: tekst er innhold, binærfiler er vedlegg som kan -plasseres inline. - -## Edges definerer alt - -Hva en node "er" bestemmes utelukkende av edges: - -- Node + `belongs_to` → kanal = chatmelding -- Node + `belongs_to` → board + `status` = kanban-kort -- Node + `scheduled` → tidspunkt = kalenderoppføring -- Node uten edges til andre = privat notat -- Node + `mentions` → topic = faktoid i kunnskapsgrafen -- Node uten edges = løs tanke, ennå uorganisert - -**Retyping er trivielt.** Chatmelding → kanban-kort = legg til -board-edge og status-edge. Ingen datamigrering, bare edges. - -**Multitype er naturlig.** En node kan være *både* kanban-kort -*og* kalenderoppføring *og* faktoid. Det er ikke en edge case — -det er arkitekturen. - -### Edge-tildeling - -Når du "bare sier noe" — hvem bestemmer edges? - -1. **Kontekst gir det meste.** Du er i en samtale → `belongs_to`- - edge til samtalen. Du er alene → privat. Dekker 80%. -2. **Eksplisitt handling.** Du drar en node til kanban-brettet. - Du tagger noe. Du setter en dato. -3. **AI-foreslått.** Systemet foreslår `mentions`-edge når du - nevner en person. Foreslår kanban når noe ligner en oppgave. - -Detaljer for AI-foreslåtte edges avklares ved implementering. - -## Mottak-primitiven - -Der input er "én overflate som fanger alt", er mottak "én overflate -som presenterer alt tilpasset *deg*." - -### Mottaker bestemmer format - -All lyd transkriberes. All tekst kan leses opp (TTS). Noden har -alltid begge representasjoner. Mottaker setter sin preferanse: - -- Trond snakker inn en tanke → node med lyd + transkripsjon -- Peter har tekst-preferanse → ser transkripsjonen -- Vegard har lyd-preferanse → hører originallyd - -Modalitet er ikke en egenskap ved meldingen, men ved *lesningen*. - -### Dimensjoner ved mottak - -**Format.** Lyd, tekst, visuelt — mottaker bestemmer. - -**Filtrering.** Mottaksflaten filtrerer basert på dine edges: -kanaler du følger, personer du samarbeider med, topics du er -interessert i. - -**Prioritering.** AI-assistert vekting: ubesvarte meldinger, -oppgaver med frist, noder endret siden sist. Ikke en -notifikasjonsliste — en vektet visning av det som er relevant. - -**Tempo.** Sanntid (ting streamer inn) eller asynkront (digest, -oppsummering). - -### Mottaksflaten er en visning av grafen - -"Noder med edge til *meg*, vektet på relevans og tid." Ikke en -egen mekanisme — en spørring mot grafen med deg som sentrum. - -## Kommunikasjonsnoden — den tredje primitiven - -Input fanger. Mottak presenterer. Kommunikasjonsnoden er *stedet* -der folk møtes — en node som samler deltakere, definerer -tilgangsregler, og fungerer som kontekst. - -### Én node, mange former - -| Variant | Deltakere | Input | Mottak | -|---------|-----------|-------|--------| -| Én-til-én | 2 | Begge | Begge | -| Gruppechat | N | Alle | Alle | -| Møte | N | Alle | Alle | -| Allmøte | 1 + N | Leder | Alle lytter | -| Podcastinnspilling | 2-4 + N | Verter | Alle lytter | -| Livesending | 1-4 + ∞ | Verter | Streamet | -| Asynkron gjest | 1 + 1 | Gjest | Redaksjonen | - -Forskjellen er edge-konfigurasjoner, ikke ulike systemer: -- `owner`-edge — kontrollerer noden -- `member`-edge — kan gi input og motta -- `reader`-edge — kan kun motta - -### Kontekst arves automatisk - -Input i en kommunikasjonsnode arver kontekst-edges. Sier du noe -i et møte → noden får `belongs_to`-edge til møtet automatisk. - -### Livssyklus - -- **Live** — deltakere til stede, input streames -- **Asynkron** — deltakere gir input i eget tempo -- **Avsluttet** — arkivert, alt som ble sagt er noder med edges -- **Gjenåpnet** — reaktivert ("vi tar opp tråden fra forrige møte") - -### Skalering er edge-endring - -Samtale → møte = flere deltaker-edges. -Møte → livesending = offentlige mottak-edges. -Livesending → podcast = publiserings-edges på arkivert innhold. - -## Tekniske forutsetninger - -### STT (tale → tekst): løst -Faster-whisper kjører lokalt, god norsk kvalitet. - -### TTS (tekst → tale): løsbart -Start med ElevenLabs bak AI Gateway, bytt til lokal modell når -kvaliteten holder. Backend-swap bak gatewayen — brukeren merker -ingenting. - -## Visninger er spørringer - -Chat = noder med kanal-edge, sortert på tid. -Kanban = noder med board-edge, gruppert på status. -Kalender = noder med dato-edge, på tidslinje. -Dagbok = private noder, sortert på tid. -Mottaksflate = noder med edge til deg, vektet. - -Alle leser fra samme graf. Ingen har "sin egen" data. - -## Forhold til andre retninger - -- [Noder er sentrum](bruker_ikke_workspace.md) — visibility, - tilgangsmatrise, aliaser -- [Datalaget](datalaget.md) — SpacetimeDB holder hele grafen, - PG persisterer asynkront -- [Maskinrommet](maskinrommet.md) — validering, routing, CAS, - tunge jobber (Whisper, TTS, AI) -- [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden - er den konkrete realiseringen av "rommet" - - -================================================================ -FILE: docs/primitiver/edges.md -================================================================ - -# Edges — spesifikasjon - -**Status: Besluttet.** - -> Edges er relasjoner mellom noder. De er retningsbestemte, typede med -> freeform strenger, og kan bære metadata. Hva en node "er" bestemmes -> av dens edges, ikke av noden selv. - -## Skjema - -```sql -CREATE TABLE edges ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, - target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, - edge_type TEXT NOT NULL, - metadata JSONB NOT NULL DEFAULT '{}', - system BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - created_by UUID REFERENCES nodes(id), - - UNIQUE (source_id, target_id, edge_type) -); - -CREATE INDEX idx_edges_source ON edges (source_id); -CREATE INDEX idx_edges_target ON edges (target_id); -CREATE INDEX idx_edges_type ON edges (edge_type); -``` - -## Retning - -Edges er retningsbestemte: `source_id → target_id`. "Vegard eier -Sidelinja" er en annen edge enn "Sidelinja eier Vegard" (som ikke -gir mening). Retningen leses naturlig: - -``` -Vegard ──owner──→ Sidelinja (Vegard eier Sidelinja) -Referat ──belongs_to──→ Møte (referatet tilhører møtet) -Episode ──mentions──→ Jonas Gahr Støre (episoden nevner ham) -``` - -Symmetriske relasjoner (f.eks. vennskap) modelleres som to edges -eller som en edge-type maskinrommet vet er symmetrisk. - -## Edge-typer - -Freeform strenger med konvensjoner — ikke en hard enum. Nye -relasjonstyper krever ingen migrering. Systemkritiske typer -valideres i maskinrommet. - -### Kjente edge-typer - -| Type | Betydning | Metadata | -|------|-----------|----------| -| `owner` | Eier noden | — | -| `admin` | Administrerer noden | — | -| `member_of` | Er medlem av | — | -| `reader` | Kan kun lese | — | -| `belongs_to` | Tilhører (innhold → samling) | — | -| `alias` | Identitet (systemedge) | `system: true` | -| `mentions` | Refererer til | — | -| `reply_to` | Svar på | — | -| `scheduled` | Tidsplanlagt | `{ "at": "2026-03-20T14:00Z" }` | -| `status` | Tilstand | `{ "value": "done" }` | -| `tagged` | Merket med | `{ "tag": "urgent" }` | -| `host_of` | Vert i | — | - -Listen vokser organisk. Nye typer legges til ved behov uten -skjemaendring. - -## Metadata - -JSONB-feltet bærer kontekstspesifikk data om relasjonen: - -- `status`-edge: `{ "value": "in_progress" }` -- `scheduled`-edge: `{ "at": "2026-03-20T14:00Z" }` -- `tagged`-edge: `{ "tag": "urgent", "color": "#ff0000" }` - -Metadata er fleksibelt og spørrbart uten migrering. - -## Systemedges - -Edges med `system: true` er usynlige ved traversering. De -eksisterer for systemet, ikke for brukere. - -Kjente systemedges: -- `alias` — identitetskobling, aldri eksponert - -## Unique-constraint - -`UNIQUE (source_id, target_id, edge_type)` betyr: én edge av -hver type mellom to noder. Vegard kan ha `owner`-edge *og* -`member_of`-edge til Sidelinja, men ikke to `owner`-edges. - -## Hva edges gjør med noder - -Edges definerer hva en node "er" — ikke noden selv: - -``` -Node + member_of → kanal = chatmelding -Node + belongs_to → board - + status → "todo" = kanban-kort -Node + scheduled → tidspunkt = kalenderoppføring -Node + belongs_to → kun bruker = privat notat -Node + mentions → topic = faktoid i kunnskapsgrafen -Node uten edges = løs tanke -``` - -Retyping er å endre edges. Ingen datamigrasjon. - -## Tilgangsmatrise-oppdatering - -Når en edge med tilgangsrolle (`owner`, `admin`, `member_of`, -`reader`) opprettes eller slettes, oppdaterer maskinrommet -`node_access`-matrisen i samme transaksjon. Se -[noder er sentrum](../retninger/bruker_ikke_workspace.md) -for full spesifikasjon. - - -================================================================ -FILE: docs/primitiver/nodes.md -================================================================ - -# Nodes — spesifikasjon - -**Status: Besluttet.** - -> Alt er noder. Brukere, team, prosjekter, innhold, møter, mediefiler, -> kunnskapsgraf-entiteter — alt er rader i `nodes`-tabellen. Hva en -> node "er" bestemmes av dens edges, ikke av noden selv. - -## Skjema - -```sql -CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open'); - -CREATE TABLE nodes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - node_kind TEXT NOT NULL DEFAULT 'content', - title TEXT, - content TEXT, - visibility visibility NOT NULL DEFAULT 'hidden', - metadata JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - created_by UUID REFERENCES nodes(id) -); - -CREATE INDEX idx_nodes_kind ON nodes (node_kind); -CREATE INDEX idx_nodes_created_by ON nodes (created_by); -CREATE INDEX idx_nodes_visibility ON nodes (visibility); -``` - -## Kolonner - -### `id` -UUID, generert. Primærnøkkel for alt i systemet. - -### `node_kind` -Hint om hva noden primært er. Freeform streng — ikke en enum, -samme filosofi som edge-typer. Edges kan gi noden flere roller -utover dette. - -Kjente node_kinds: - -| Kind | Eksempel | -|------|----------| -| `person` | Vegard, Trond, Bjørn (alias) | -| `team` | Podcastteamet | -| `collection` | Sidelinja Podcast, Research-gruppa | -| `content` | Chatmelding, bloggpost, notat, dagboknotis | -| `communication` | Møte, samtale, livesending | -| `topic` | Kunnskapsgraf-entitet (Jonas Gahr Støre, Skolepolitikk) | -| `media` | CAS-node (lydfil, bilde, video) | - -Listen vokser organisk etter behov. - -### `title` -Det du viser overalt: i lister, søkeresultater, mottaksflaten. -Universelt — navnet på en person, tittelen på en bloggpost, -navnet på en podcast. - -- Vegard: `'Vegard'` -- Sidelinja: `'Sidelinja Podcast'` -- Bloggpost: `'Hvorfor noder er sentrum'` -- Chatmelding: `NULL` (har sjelden tittel) - -### `content` -Ren tekst uten formatering. Brukes til fulltekstsøk og enkel -visning. For rike dokumenter (formatert tekst med bilder) genereres -`content` automatisk fra `metadata.document` ved lagring. - -- Bloggpost: teksten uten markup (generert fra `metadata.document`) -- Chatmelding: `'Hei, er du klar?'` -- Voice memo: `NULL` ved opprettelse, fylles etter transkribering -- Brukernode: `NULL` -- CAS-node: `NULL` (binærdata lever på disk) - -For formatert innhold: se `metadata.document` under metadata. - -### `visibility` -Default synlighet for alle uten eksplisitt edge. -Se [noder er sentrum](../retninger/bruker_ikke_workspace.md) -for full spesifikasjon av visibility-nivåer og traverseringsregelen. - -### `metadata` -JSONB for alt typespesifikt som ikke er tittel eller ren tekst: - -- Person: `{ "display_name": "Vegard", "preferences": { ... } }` -- CAS-node: `{ "cas_hash": "abc123", "mime": "audio/mp3", "size_bytes": 84000000 }` -- Kommunikasjonsnode: `{ "started_at": "...", "ended_at": "..." }` -- Samlings-node: `{ "pruning_profile": "conservative", "theme": "dark" }` -- Rikt dokument: `{ "document": { "type": "doc", "content": [...] } }` - -`metadata.document` inneholder TipTap/ProseMirror JSON for formatert -innhold (overskrifter, bilder, blockquotes, etc.). Bilder refererer -til CAS-noder via `node_id`. For enkle meldinger (ren tekst) er -`document` null — `content` er alt som trengs. Se -[universell input](../retninger/universell_input.md) for detaljer. - -### `created_at` -Tidsstempel, automatisk. - -### `created_by` -Referanse til noden som opprettet denne noden. For alias-bruk: -`created_by` settes til aliasnoden, ikke brukernoden bak. - -## Brukernoder - -En brukernode er en node med `node_kind = 'person'` og en rad i -`auth_identities`: - -```sql -CREATE TABLE auth_identities ( - node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - authentik_sub TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL -); -``` - -Alt annet — profil, preferanser, relasjoner — er noden og dens -edges. Autentiseringstabellen er en tynn bro mellom HTTP-sesjonen -og grafen. - -## CAS-noder (mediefiler) - -Binærfiler er noder med `node_kind = 'media'`. Selve biten lever -på disk i CAS-lageret. Noden bærer bare metadata. - -``` -Episode #42 (content) - ──has_media──→ CAS-node (media, metadata: { cas_hash: "abc123", mime: "audio/mp3" }) - ──has_media──→ CAS-node (media, metadata: { cas_hash: "def456", mime: "text/srt" }) - ──belongs_to──→ Sidelinja Podcast (collection) -``` - -En innholdsnode kan ha mange mediefiler via edges. Pruning-logikken -i maskinrommet opererer på CAS-noder — sletter binærfilen fra disk, -men noden kan leve videre som tombstone. - -## Eierskap og tilgang - -`created_by` gir redigeringsrett til egne noder. Men sletting og -strukturelle endringer scopes av rollen i konteksten: - -- Du kan alltid redigere noder du opprettet. -- Sletting krever `owner` eller `admin` i samlings-noden - innholdet tilhører — selv om du opprettet det. -- En privat node (ingen samlings-edge) kan slettes fritt av creator. - -Flere noder kan ha `owner`-tilgang til samme node via -tilgangsmatrisen (direkte og transitiv). Forretningslogikk for -hva ulike tilgangsnivåer tillater lever i maskinrommet, ikke i -databasen. - - -================================================================ -FILE: docs/concepts/den_asynkrone_gjesten.md -================================================================ - + + +================================================================ +FILE: docs/retninger/universell_input.md +================================================================ + +# Universell input og mottak + +**Status: Besluttet.** + +> Én multimodal input-primitiv. Én personlig mottaksflate. Alt som +> fanges er en node. Hva det "er" bestemmes av edges. Hvordan det +> presenteres bestemmes av mottakeren. + +## Input-primitiven + +Én overflate som fanger alt: + +- **Tekst** — skriving, Markdown, kodeblokker +- **Lyd** — voice memo, diktering → automatisk transkribert +- **Bilde** — foto, skjermbilde, tegning +- **AI-støtte** — spør AI, få forslag, la den transformere input +- **URL** — lim inn lenke, den berikes automatisk + +Brukeren gjør det samme uansett kontekst — skriver, snakker eller +tegner i input-feltet. Forskjellen mellom "dagbok", "chatmelding" +og "kanban-kort" er ikke hva brukeren gjør — det er hvilke edges +som knyttes til resultatet. + +### Én pipeline + +All input går gjennom samme tekniske pipeline: +maskinrommet → SpacetimeDB (instant) → PG (asynk). + +Konteksten bestemmer routing, ikke en teknisk modus: + +- Du er i et møte → inputen streames live til andre deltakere +- Du er alene på bussen → inputen lander som privat node +- Du er i en podcast-kanal → inputen går inn i produksjonspipeline + +Samme pipeline. Ulike edges. + +### Input-metode og innholdstype er ortogonale + +Du kan snakke inn et kanban-kort. Du kan tegne en kalenderoppføring. +Input-primitiven bryr seg ikke om hva det *blir* — den fanger det +som kommer inn. Alt etterpå er edges. + +### Én overflate å perfeksjonere + +All UX-investering konsentreres ett sted. Én perfekt input-opplevelse +— responsiv, multimodal, med god AI-støtte — i stedet for ti +middelmådige spesialgrensesnitt. + +## Editoren + +Input-komponenten er en TipTap-editor (ProseMirror-basert) som +konfigureres med ulike extensions basert på kontekst. + +### Kontekst setter default, brukeren bestemmer + +Konteksten (kommunikasjonsnoden du er i, visningen du bruker) +setter en *default* editor-konfigurasjon. Men brukeren kan alltid +overstyre — utvide til full editor eller forenkle. Ingen kunstig +grense mellom "chatmelding" og "artikkel." + +Du *kan* skrive en gjennomformatert tekst med overskrifter, bilder +og blockquotes i chatten. Om du vil publisere den etterpå er det +bare å legge til en publiserings-edge. Innholdet er det samme. + +Editoren husker brukerens valg per kontekst via preferanser på +brukernoden. + +### Presets + +| Kontekst | Default extensions | Eksempel | +|----------|-------------------|----------| +| Chat | Tekst, markdown, kodeblokker, lenker | Enkel melding | +| Artikkel/blogg | + overskrifter, bilder, embeds, blockquotes, tabeller | Publisert tekst | +| Show notes | + lister, tidskoder, lenker | Episodenotater | +| Kanban-kort | Tekst, sjekklister | Oppgavebeskrivelse | + +Presets er bare default — brukeren kan utvide eller forenkle med +en knapp eller tastatursnarvei. Ikke en modebytte, bare at flere +verktøy blir tilgjengelig. + +### Tekstlagring + +Noden lagrer to representasjoner: + +- `content TEXT` — ren tekst uten formatering, for fulltekstsøk + og enkel visning +- `metadata.document JSONB` — strukturert TipTap/ProseMirror- + dokument for rendering + +```json +{ + "type": "doc", + "content": [ + { "type": "paragraph", "content": [ + { "type": "text", "text": "Her er en intro." } + ]}, + { "type": "image", "attrs": { + "node_id": "uuid-av-cas-node", "alt": "Diagram" + }}, + { "type": "paragraph", "content": [ + { "type": "text", "text": "Teksten fortsetter." } + ]} + ] +} +``` + +`content` genereres automatisk fra dokumentet ved lagring — bare +teksten, uten markup. Editoren produserer begge. + +For enkle meldinger (ren tekst uten formatering) er +`metadata.document` null — `content` er alt som trengs. + +### Bilder og media i tekst + +Bilder i dokumentet refererer til CAS-noder via `node_id`. +CAS-noden er en egen node med `has_media`-edge til innholdsnoden. +Dokumentstrukturen bestemmer *hvor* bildet plasseres i teksten. + +Tekst er *på* noden. Binærfiler er *andre* noder koblet med edges. +Ren separasjon: tekst er innhold, binærfiler er vedlegg som kan +plasseres inline. + +## Edges definerer alt + +Hva en node "er" bestemmes utelukkende av edges: + +- Node + `belongs_to` → kanal = chatmelding +- Node + `belongs_to` → board + `status` = kanban-kort +- Node + `scheduled` → tidspunkt = kalenderoppføring +- Node uten edges til andre = privat notat +- Node + `mentions` → topic = faktoid i kunnskapsgrafen +- Node uten edges = løs tanke, ennå uorganisert + +**Retyping er trivielt.** Chatmelding → kanban-kort = legg til +board-edge og status-edge. Ingen datamigrering, bare edges. + +**Multitype er naturlig.** En node kan være *både* kanban-kort +*og* kalenderoppføring *og* faktoid. Det er ikke en edge case — +det er arkitekturen. + +### Edge-tildeling + +Når du "bare sier noe" — hvem bestemmer edges? + +1. **Kontekst gir det meste.** Du er i en samtale → `belongs_to`- + edge til samtalen. Du er alene → privat. Dekker 80%. +2. **Eksplisitt handling.** Du drar en node til kanban-brettet. + Du tagger noe. Du setter en dato. +3. **AI-foreslått.** Systemet foreslår `mentions`-edge når du + nevner en person. Foreslår kanban når noe ligner en oppgave. + +Detaljer for AI-foreslåtte edges avklares ved implementering. + +## Mottak-primitiven + +Der input er "én overflate som fanger alt", er mottak "én overflate +som presenterer alt tilpasset *deg*." + +### Mottaker bestemmer format + +All lyd transkriberes. All tekst kan leses opp (TTS). Noden har +alltid begge representasjoner. Mottaker setter sin preferanse: + +- Trond snakker inn en tanke → node med lyd + transkripsjon +- Peter har tekst-preferanse → ser transkripsjonen +- Vegard har lyd-preferanse → hører originallyd + +Modalitet er ikke en egenskap ved meldingen, men ved *lesningen*. + +### Dimensjoner ved mottak + +**Format.** Lyd, tekst, visuelt — mottaker bestemmer. + +**Filtrering.** Mottaksflaten filtrerer basert på dine edges: +kanaler du følger, personer du samarbeider med, topics du er +interessert i. + +**Prioritering.** AI-assistert vekting: ubesvarte meldinger, +oppgaver med frist, noder endret siden sist. Ikke en +notifikasjonsliste — en vektet visning av det som er relevant. + +**Tempo.** Sanntid (ting streamer inn) eller asynkront (digest, +oppsummering). + +### Mottaksflaten er en visning av grafen + +"Noder med edge til *meg*, vektet på relevans og tid." Ikke en +egen mekanisme — en spørring mot grafen med deg som sentrum. + +## Kommunikasjonsnoden — den tredje primitiven + +Input fanger. Mottak presenterer. Kommunikasjonsnoden er *stedet* +der folk møtes — en node som samler deltakere, definerer +tilgangsregler, og fungerer som kontekst. + +### Én node, mange former + +| Variant | Deltakere | Input | Mottak | +|---------|-----------|-------|--------| +| Én-til-én | 2 | Begge | Begge | +| Gruppechat | N | Alle | Alle | +| Møte | N | Alle | Alle | +| Allmøte | 1 + N | Leder | Alle lytter | +| Podcastinnspilling | 2-4 + N | Verter | Alle lytter | +| Livesending | 1-4 + ∞ | Verter | Streamet | +| Asynkron gjest | 1 + 1 | Gjest | Redaksjonen | + +Forskjellen er edge-konfigurasjoner, ikke ulike systemer: +- `owner`-edge — kontrollerer noden +- `member`-edge — kan gi input og motta +- `reader`-edge — kan kun motta + +### Kontekst arves automatisk + +Input i en kommunikasjonsnode arver kontekst-edges. Sier du noe +i et møte → noden får `belongs_to`-edge til møtet automatisk. + +### Livssyklus + +- **Live** — deltakere til stede, input streames +- **Asynkron** — deltakere gir input i eget tempo +- **Avsluttet** — arkivert, alt som ble sagt er noder med edges +- **Gjenåpnet** — reaktivert ("vi tar opp tråden fra forrige møte") + +### Skalering er edge-endring + +Samtale → møte = flere deltaker-edges. +Møte → livesending = offentlige mottak-edges. +Livesending → podcast = publiserings-edges på arkivert innhold. + +## Tekniske forutsetninger + +### STT (tale → tekst): løst +Faster-whisper kjører lokalt, god norsk kvalitet. + +### TTS (tekst → tale): løsbart +Start med ElevenLabs bak AI Gateway, bytt til lokal modell når +kvaliteten holder. Backend-swap bak gatewayen — brukeren merker +ingenting. + +## Visninger er spørringer + +Chat = noder med kanal-edge, sortert på tid. +Kanban = noder med board-edge, gruppert på status. +Kalender = noder med dato-edge, på tidslinje. +Dagbok = private noder, sortert på tid. +Mottaksflate = noder med edge til deg, vektet. + +Alle leser fra samme graf. Ingen har "sin egen" data. + +## Forhold til andre retninger + +- [Noder er sentrum](bruker_ikke_workspace.md) — visibility, + tilgangsmatrise, aliaser +- [Datalaget](datalaget.md) — SpacetimeDB holder hele grafen, + PG persisterer asynkront +- [Maskinrommet](maskinrommet.md) — validering, routing, CAS, + tunge jobber (Whisper, TTS, AI) +- [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden + er den konkrete realiseringen av "rommet" + + +================================================================ +FILE: docs/primitiver/edges.md +================================================================ + +# Edges — spesifikasjon + +**Status: Besluttet.** + +> Edges er relasjoner mellom noder. De er retningsbestemte, typede med +> freeform strenger, og kan bære metadata. Hva en node "er" bestemmes +> av dens edges, ikke av noden selv. + +## Skjema + +```sql +CREATE TABLE edges ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, + edge_type TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + system BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES nodes(id), + + UNIQUE (source_id, target_id, edge_type) +); + +CREATE INDEX idx_edges_source ON edges (source_id); +CREATE INDEX idx_edges_target ON edges (target_id); +CREATE INDEX idx_edges_type ON edges (edge_type); +``` + +## Retning + +Edges er retningsbestemte: `source_id → target_id`. "Vegard eier +Sidelinja" er en annen edge enn "Sidelinja eier Vegard" (som ikke +gir mening). Retningen leses naturlig: + +``` +Vegard ──owner──→ Sidelinja (Vegard eier Sidelinja) +Referat ──belongs_to──→ Møte (referatet tilhører møtet) +Episode ──mentions──→ Jonas Gahr Støre (episoden nevner ham) +``` + +Symmetriske relasjoner (f.eks. vennskap) modelleres som to edges +eller som en edge-type maskinrommet vet er symmetrisk. + +## Edge-typer + +Freeform strenger med konvensjoner — ikke en hard enum. Nye +relasjonstyper krever ingen migrering. Systemkritiske typer +valideres i maskinrommet. + +### Kjente edge-typer + +| Type | Betydning | Metadata | +|------|-----------|----------| +| `owner` | Eier noden | — | +| `admin` | Administrerer noden | — | +| `member_of` | Er medlem av | — | +| `reader` | Kan kun lese | — | +| `belongs_to` | Tilhører (innhold → samling) | — | +| `alias` | Identitet (systemedge) | `system: true` | +| `mentions` | Refererer til | — | +| `reply_to` | Svar på | — | +| `scheduled` | Tidsplanlagt | `{ "at": "2026-03-20T14:00Z" }` | +| `status` | Tilstand | `{ "value": "done" }` | +| `tagged` | Merket med | `{ "tag": "urgent" }` | +| `host_of` | Vert i | — | + +Listen vokser organisk. Nye typer legges til ved behov uten +skjemaendring. + +## Metadata + +JSONB-feltet bærer kontekstspesifikk data om relasjonen: + +- `status`-edge: `{ "value": "in_progress" }` +- `scheduled`-edge: `{ "at": "2026-03-20T14:00Z" }` +- `tagged`-edge: `{ "tag": "urgent", "color": "#ff0000" }` + +Metadata er fleksibelt og spørrbart uten migrering. + +## Systemedges + +Edges med `system: true` er usynlige ved traversering. De +eksisterer for systemet, ikke for brukere. + +Kjente systemedges: +- `alias` — identitetskobling, aldri eksponert + +## Unique-constraint + +`UNIQUE (source_id, target_id, edge_type)` betyr: én edge av +hver type mellom to noder. Vegard kan ha `owner`-edge *og* +`member_of`-edge til Sidelinja, men ikke to `owner`-edges. + +## Hva edges gjør med noder + +Edges definerer hva en node "er" — ikke noden selv: + +``` +Node + member_of → kanal = chatmelding +Node + belongs_to → board + + status → "todo" = kanban-kort +Node + scheduled → tidspunkt = kalenderoppføring +Node + belongs_to → kun bruker = privat notat +Node + mentions → topic = faktoid i kunnskapsgrafen +Node uten edges = løs tanke +``` + +Retyping er å endre edges. Ingen datamigrasjon. + +## Tilgangsmatrise-oppdatering + +Når en edge med tilgangsrolle (`owner`, `admin`, `member_of`, +`reader`) opprettes eller slettes, oppdaterer maskinrommet +`node_access`-matrisen i samme transaksjon. Se +[noder er sentrum](../retninger/bruker_ikke_workspace.md) +for full spesifikasjon. + + +================================================================ +FILE: docs/primitiver/nodes.md +================================================================ + +# Nodes — spesifikasjon + +**Status: Besluttet.** + +> Alt er noder. Brukere, team, prosjekter, innhold, møter, mediefiler, +> kunnskapsgraf-entiteter — alt er rader i `nodes`-tabellen. Hva en +> node "er" bestemmes av dens edges, ikke av noden selv. + +## Skjema + +```sql +CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open'); + +CREATE TABLE nodes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + node_kind TEXT NOT NULL DEFAULT 'content', + title TEXT, + content TEXT, + visibility visibility NOT NULL DEFAULT 'hidden', + metadata JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES nodes(id) +); + +CREATE INDEX idx_nodes_kind ON nodes (node_kind); +CREATE INDEX idx_nodes_created_by ON nodes (created_by); +CREATE INDEX idx_nodes_visibility ON nodes (visibility); +``` + +## Kolonner + +### `id` +UUID, generert. Primærnøkkel for alt i systemet. + +### `node_kind` +Hint om hva noden primært er. Freeform streng — ikke en enum, +samme filosofi som edge-typer. Edges kan gi noden flere roller +utover dette. + +Kjente node_kinds: + +| Kind | Eksempel | +|------|----------| +| `person` | Vegard, Trond, Bjørn (alias) | +| `team` | Podcastteamet | +| `collection` | Sidelinja Podcast, Research-gruppa | +| `content` | Chatmelding, bloggpost, notat, dagboknotis | +| `communication` | Møte, samtale, livesending | +| `topic` | Kunnskapsgraf-entitet (Jonas Gahr Støre, Skolepolitikk) | +| `media` | CAS-node (lydfil, bilde, video) | + +Listen vokser organisk etter behov. + +### `title` +Det du viser overalt: i lister, søkeresultater, mottaksflaten. +Universelt — navnet på en person, tittelen på en bloggpost, +navnet på en podcast. + +- Vegard: `'Vegard'` +- Sidelinja: `'Sidelinja Podcast'` +- Bloggpost: `'Hvorfor noder er sentrum'` +- Chatmelding: `NULL` (har sjelden tittel) + +### `content` +Ren tekst uten formatering. Brukes til fulltekstsøk og enkel +visning. For rike dokumenter (formatert tekst med bilder) genereres +`content` automatisk fra `metadata.document` ved lagring. + +- Bloggpost: teksten uten markup (generert fra `metadata.document`) +- Chatmelding: `'Hei, er du klar?'` +- Voice memo: `NULL` ved opprettelse, fylles etter transkribering +- Brukernode: `NULL` +- CAS-node: `NULL` (binærdata lever på disk) + +For formatert innhold: se `metadata.document` under metadata. + +### `visibility` +Default synlighet for alle uten eksplisitt edge. +Se [noder er sentrum](../retninger/bruker_ikke_workspace.md) +for full spesifikasjon av visibility-nivåer og traverseringsregelen. + +### `metadata` +JSONB for alt typespesifikt som ikke er tittel eller ren tekst: + +- Person: `{ "display_name": "Vegard", "preferences": { ... } }` +- CAS-node: `{ "cas_hash": "abc123", "mime": "audio/mp3", "size_bytes": 84000000 }` +- Kommunikasjonsnode: `{ "started_at": "...", "ended_at": "..." }` +- Samlings-node: `{ "pruning_profile": "conservative", "theme": "dark" }` +- Rikt dokument: `{ "document": { "type": "doc", "content": [...] } }` + +`metadata.document` inneholder TipTap/ProseMirror JSON for formatert +innhold (overskrifter, bilder, blockquotes, etc.). Bilder refererer +til CAS-noder via `node_id`. For enkle meldinger (ren tekst) er +`document` null — `content` er alt som trengs. Se +[universell input](../retninger/universell_input.md) for detaljer. + +### `created_at` +Tidsstempel, automatisk. + +### `created_by` +Referanse til noden som opprettet denne noden. For alias-bruk: +`created_by` settes til aliasnoden, ikke brukernoden bak. + +## Brukernoder + +En brukernode er en node med `node_kind = 'person'` og en rad i +`auth_identities`: + +```sql +CREATE TABLE auth_identities ( + node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + authentik_sub TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL +); +``` + +Alt annet — profil, preferanser, relasjoner — er noden og dens +edges. Autentiseringstabellen er en tynn bro mellom HTTP-sesjonen +og grafen. + +## CAS-noder (mediefiler) + +Binærfiler er noder med `node_kind = 'media'`. Selve biten lever +på disk i CAS-lageret. Noden bærer bare metadata. + +``` +Episode #42 (content) + ──has_media──→ CAS-node (media, metadata: { cas_hash: "abc123", mime: "audio/mp3" }) + ──has_media──→ CAS-node (media, metadata: { cas_hash: "def456", mime: "text/srt" }) + ──belongs_to──→ Sidelinja Podcast (collection) +``` + +En innholdsnode kan ha mange mediefiler via edges. Pruning-logikken +i maskinrommet opererer på CAS-noder — sletter binærfilen fra disk, +men noden kan leve videre som tombstone. + +## Eierskap og tilgang + +`created_by` gir redigeringsrett til egne noder. Men sletting og +strukturelle endringer scopes av rollen i konteksten: + +- Du kan alltid redigere noder du opprettet. +- Sletting krever `owner` eller `admin` i samlings-noden + innholdet tilhører — selv om du opprettet det. +- En privat node (ingen samlings-edge) kan slettes fritt av creator. + +Flere noder kan ha `owner`-tilgang til samme node via +tilgangsmatrisen (direkte og transitiv). Forretningslogikk for +hva ulike tilgangsnivåer tillater lever i maskinrommet, ikke i +databasen. + + +================================================================ +FILE: docs/concepts/den_asynkrone_gjesten.md +================================================================ + # Konsept: Den Asynkrone Gjesten **Filsti:** `docs/concepts/den_asynkrone_gjesten.md` @@ -1961,12 +1957,12 @@ Gjest åpner URL med token * 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. * Tilgang er styrt via node_access. Token bærer node_id eksplisitt. - - -================================================================ -FILE: docs/concepts/kunnskapsgrafen.md -================================================================ - + + +================================================================ +FILE: docs/concepts/kunnskapsgrafen.md +================================================================ + # Konsept: Kunnskapsgrafen (Utforsking og redigering) **Filsti:** `docs/concepts/kunnskapsgrafen.md` @@ -2019,12 +2015,12 @@ Grafen vokser organisk via `#`-mentions, men dette skaper uunngåelig fragmenter ## 5. Datamodell Den tekniske datamodellen (nodes-supertabell, graph_edges, detailtabeller, deterministiske UUIDs, tilgangsstyring via node_access) er dokumentert i `docs/features/kunnskapsgraf_og_relasjoner.md`. - - -================================================================ -FILE: docs/concepts/møterommet.md -================================================================ - + + +================================================================ +FILE: docs/concepts/møterommet.md +================================================================ + # Konsept: Møterommet (Interne redaksjonsmøter) **Filsti:** `docs/concepts/møterommet.md` @@ -2065,12 +2061,12 @@ Når off-the-record er aktivt: ## 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. - - -================================================================ -FILE: docs/concepts/podcastfabrikken.md -================================================================ - + + +================================================================ +FILE: docs/concepts/podcastfabrikken.md +================================================================ + # Konsept: Podcastfabrikken (Lyd & Publiserings-Pipeline) **Filsti:** `docs/concepts/podcastfabrikken.md` @@ -2217,12 +2213,12 @@ Rust-workeren `stats_parse` knytter nedlastingstall fra Caddy-logger til riktig * **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. -* **Tilhørighet:** Alle jobber, mediefiler og metadata knyttes til riktig samlings-node via edges. Hent config (prompts, domene) fra samlings-nodens JSONB-metadata. - -================================================================ -FILE: docs/concepts/redaksjonen.md -================================================================ - +* **Tilhørighet:** Alle jobber, mediefiler og metadata knyttes til riktig samlings-node via edges. Hent config (prompts, domene) fra samlings-nodens JSONB-metadata. + +================================================================ +FILE: docs/concepts/redaksjonen.md +================================================================ + # Konsept: Redaksjonen (Daglig redaksjonelt arbeid) **Filsti:** `docs/concepts/redaksjonen.md` @@ -2259,12 +2255,12 @@ Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen ( * 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. * All tilgang er styrt via node_access-matrisen. SpacetimeDB-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder. - - -================================================================ -FILE: docs/concepts/studioet.md -================================================================ - + + +================================================================ +FILE: docs/concepts/studioet.md +================================================================ + # Konsept: Studioet (Podcast-innspilling) **Filsti:** `docs/concepts/studioet.md` @@ -2297,12 +2293,12 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte 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. - - -================================================================ -FILE: docs/concepts/valgomaten.md -================================================================ - + + +================================================================ +FILE: docs/concepts/valgomaten.md +================================================================ + # Konsept: Valgomaten (Crowdsourced & Datadrevet) **Filsti:** `docs/concepts/valgomaten.md` @@ -2409,12 +2405,12 @@ PCA-beregning i SpacetimeDB er minnekrevende og udokumentert for store datasett. 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. - - -================================================================ -FILE: docs/features/brukerinnstillinger.md -================================================================ - + + +================================================================ +FILE: docs/features/brukerinnstillinger.md +================================================================ + # Feature: Brukerinnstillinger **Filsti:** `docs/features/brukerinnstillinger.md` @@ -2587,12 +2583,12 @@ PATCH gjør en shallow merge: `settings = settings || patch`. Kun oppgitte felt - Innstillingspanelet er en modal/drawer, ikke en egen side - Auto-save med debounce (500ms) — ingen "lagre"-knapp - Ved nye innstillinger: legg til i defaults-objektet, ingen migrering nødvendig - - -================================================================ -FILE: docs/features/canvas_primitiv.md -================================================================ - + + +================================================================ +FILE: docs/features/canvas_primitiv.md +================================================================ + # Feature: Canvas-primitiv — felles fritt-canvas underlag **Filsti:** `docs/features/canvas_primitiv.md` @@ -2806,12 +2802,12 @@ Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer - Pointer events, ikke mouse events — unified input - Test med touch-emulering i DevTools for responsivitet - Viewport culling er påkrevd fra dag 1 — ikke optimaliser bort - - -================================================================ -FILE: docs/features/chat.md -================================================================ - + + +================================================================ +FILE: docs/features/chat.md +================================================================ + # Feature: Chat (Channels & Meldinger) **Filsti:** `docs/features/chat.md` @@ -2959,116 +2955,116 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s * **Config-respekt:** Frontend-komponenten må lese `channel.config` og slå av/på UI-elementer. `channels.config` inneholder også `warmup_mode`/`warmup_value` for SpacetimeDB-oppvarming. * **PG er autoritativ** — SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB. * **Tilgang styres via `node_access`-matrisen.** Channels arver tilgang fra sin parent-node via edges. - - -================================================================ -FILE: docs/features/kalender.md -================================================================ - -# Feature: Kalender -**Filsti:** `docs/features/kalender.md` - -## 1. Konsept -Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i kunnskapsgrafen og kan kobles til episoder, temaer, aktører og kanban-kort. Komplementerer Kanban ("hva" vs "når"). - -## 2. Status -**PG-adapter ferdig og deployet (mars 2025).** Abonnement, ICS-eksport og SpacetimeDB-sync gjenstår. - -### Implementert -- Migrering `0003_calendar.sql`: `calendars` + `calendar_events` (begge FK→nodes) -- Hendelser er nodes — tilgangsstyrt via `node_access`-matrise -- Heldagshendelser (`T12:00:00` for tidssone-trygghet) vs. tidshendelser med klokkeslett -- Fargekoder per hendelse (7 forhåndsdefinerte) + standard kalenderfarge -- REST API: GET med tidsvindu-filtrering, POST/PATCH/DELETE hendelser -- PG polling-adapter med 5 sek intervall -- CalendarBlock.svelte: månedsrutenett, navigering, opprett/rediger-modal, Escape-lukking -- `linked_node`-kolonne for fremtidig kobling til kanban-kort, episoder etc. - -### Gjenstår — Fase 2 -- Kobling til kanban-kort (vis deadline på kalender) -- Uke- og dagsvisning -- Gjentakende hendelser (RRULE) -- Dra-og-slipp for å flytte hendelser mellom datoer -- Flerdag-hendelser (vises over flere celler) -- Abonnementsmodell (kalender → kalender via graph_edges) -- Personlige vs. delte kalendere (via samlings-noder) -- ICS/CalDAV-eksport -- SpacetimeDB-modul + hybrid-adapter -- Varsler/påminnelser via jobbkøen - -## 3. Datamodell (implementert) - -```sql -CREATE TABLE calendars ( - id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - parent_id UUID NOT NULL REFERENCES nodes(id), - name TEXT NOT NULL, - color TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -CREATE TABLE calendar_events ( - id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - calendar_id UUID NOT NULL REFERENCES calendars(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, - color TEXT, - linked_node UUID REFERENCES nodes(id) ON DELETE SET NULL, - created_by TEXT REFERENCES users(authentik_id), - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -``` - -Indekser: `(calendar_id)` og `(calendar_id, starts_at)` for effektiv tidsvindu-filtrering. - -## 4. API-endepunkter - -| Metode | Sti | Beskrivelse | -|---|---|---| -| GET | `/api/calendar/[calendarId]?from=...&to=...` | Hent kalender med hendelser i tidsvindu | -| POST | `/api/calendar/[calendarId]/events` | Opprett hendelse | -| PATCH | `/api/calendar/[calendarId]/events/[eventId]` | Oppdater hendelse | -| DELETE | `/api/calendar/[calendarId]/events/[eventId]` | Slett hendelse | - -## 5. Brukes av - -| Konsept | Bruk | -|---|---| -| Redaksjonen | Innspillingsdatoer, publiseringsplan, deadlines | -| Foreningen Liberalistene | Styremøter, arrangementer | -| Møterommet (fremtidig) | Møteplanlegging | - -## 6. Tidssone-håndtering -- Heldagshendelser lagres som `T12:00:00` (middag) — unngår datoforskyvning ved UTC-konvertering -- Tidshendelser konverteres til ISO via `new Date().toISOString()` (lokal → UTC) -- Visning bruker `new Date()` som konverterer tilbake til lokal tid - -## 7. Fremtidig: Abonnementsmodell -Kalendere kan abonnere på andre kalendere via `SUBSCRIBES_TO`-edge i grafen. Abonnenten ser hendelser read-only. - -| Type | Eier | Synlighet | -|------|------|-----------| -| Samlings-node | Admin for samlings-noden | Alle med tilgang via `node_access` | -| Personlig | Bruker | Kun eier + eksplisitt deling | -| Offentlig | Samlings-node | Alle som abonnerer | - -## 8. Fremtidig: ICS-eksport -Offentlige kalendere får en ICS-URL (`/cal/{calendar_id}.ics`) for Google Calendar, Apple Calendar etc. - -## 9. Instruks for Claude Code -* Bruk `eventsForDate()` med lokal dato-konvertering, ikke UTC-substring -* Heldagshendelser: `T12:00:00`, aldri `T00:00:00` (tidssone-felle) -* Tilgang styres via `node_access`-matrisen -* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi - - -================================================================ -FILE: docs/features/kanban.md -================================================================ - + + +================================================================ +FILE: docs/features/kalender.md +================================================================ + +# Feature: Kalender +**Filsti:** `docs/features/kalender.md` + +## 1. Konsept +Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i kunnskapsgrafen og kan kobles til episoder, temaer, aktører og kanban-kort. Komplementerer Kanban ("hva" vs "når"). + +## 2. Status +**PG-adapter ferdig og deployet (mars 2025).** Abonnement, ICS-eksport og SpacetimeDB-sync gjenstår. + +### Implementert +- Migrering `0003_calendar.sql`: `calendars` + `calendar_events` (begge FK→nodes) +- Hendelser er nodes — tilgangsstyrt via `node_access`-matrise +- Heldagshendelser (`T12:00:00` for tidssone-trygghet) vs. tidshendelser med klokkeslett +- Fargekoder per hendelse (7 forhåndsdefinerte) + standard kalenderfarge +- REST API: GET med tidsvindu-filtrering, POST/PATCH/DELETE hendelser +- PG polling-adapter med 5 sek intervall +- CalendarBlock.svelte: månedsrutenett, navigering, opprett/rediger-modal, Escape-lukking +- `linked_node`-kolonne for fremtidig kobling til kanban-kort, episoder etc. + +### Gjenstår — Fase 2 +- Kobling til kanban-kort (vis deadline på kalender) +- Uke- og dagsvisning +- Gjentakende hendelser (RRULE) +- Dra-og-slipp for å flytte hendelser mellom datoer +- Flerdag-hendelser (vises over flere celler) +- Abonnementsmodell (kalender → kalender via graph_edges) +- Personlige vs. delte kalendere (via samlings-noder) +- ICS/CalDAV-eksport +- SpacetimeDB-modul + hybrid-adapter +- Varsler/påminnelser via jobbkøen + +## 3. Datamodell (implementert) + +```sql +CREATE TABLE calendars ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + parent_id UUID NOT NULL REFERENCES nodes(id), + name TEXT NOT NULL, + color TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE calendar_events ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + calendar_id UUID NOT NULL REFERENCES calendars(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, + color TEXT, + linked_node UUID REFERENCES nodes(id) ON DELETE SET NULL, + created_by TEXT REFERENCES users(authentik_id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +Indekser: `(calendar_id)` og `(calendar_id, starts_at)` for effektiv tidsvindu-filtrering. + +## 4. API-endepunkter + +| Metode | Sti | Beskrivelse | +|---|---|---| +| GET | `/api/calendar/[calendarId]?from=...&to=...` | Hent kalender med hendelser i tidsvindu | +| POST | `/api/calendar/[calendarId]/events` | Opprett hendelse | +| PATCH | `/api/calendar/[calendarId]/events/[eventId]` | Oppdater hendelse | +| DELETE | `/api/calendar/[calendarId]/events/[eventId]` | Slett hendelse | + +## 5. Brukes av + +| Konsept | Bruk | +|---|---| +| Redaksjonen | Innspillingsdatoer, publiseringsplan, deadlines | +| Foreningen Liberalistene | Styremøter, arrangementer | +| Møterommet (fremtidig) | Møteplanlegging | + +## 6. Tidssone-håndtering +- Heldagshendelser lagres som `T12:00:00` (middag) — unngår datoforskyvning ved UTC-konvertering +- Tidshendelser konverteres til ISO via `new Date().toISOString()` (lokal → UTC) +- Visning bruker `new Date()` som konverterer tilbake til lokal tid + +## 7. Fremtidig: Abonnementsmodell +Kalendere kan abonnere på andre kalendere via `SUBSCRIBES_TO`-edge i grafen. Abonnenten ser hendelser read-only. + +| Type | Eier | Synlighet | +|------|------|-----------| +| Samlings-node | Admin for samlings-noden | Alle med tilgang via `node_access` | +| Personlig | Bruker | Kun eier + eksplisitt deling | +| Offentlig | Samlings-node | Alle som abonnerer | + +## 8. Fremtidig: ICS-eksport +Offentlige kalendere får en ICS-URL (`/cal/{calendar_id}.ics`) for Google Calendar, Apple Calendar etc. + +## 9. Instruks for Claude Code +* Bruk `eventsForDate()` med lokal dato-konvertering, ikke UTC-substring +* Heldagshendelser: `T12:00:00`, aldri `T00:00:00` (tidssone-felle) +* Tilgang styres via `node_access`-matrisen +* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi + + +================================================================ +FILE: docs/features/kanban.md +================================================================ + # Feature: Kanban (Planlegging) **Filsti:** `docs/features/kanban.md` @@ -3128,12 +3124,12 @@ Kort og brett er nodes — tilgang styres via `node_access`-matrisen. * PG-adapter er autoritativ inntil SpacetimeDB-sync er på plass. * Tilgang styres via `node_access`-matrisen. * Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi. - - -================================================================ -FILE: docs/features/kunnskaps_bridge.md -================================================================ - + + +================================================================ +FILE: docs/features/kunnskaps_bridge.md +================================================================ + # Feature: Kunnskaps-Bridge (Cross-Context Discovery) **Filsti:** `docs/features/kunnskaps_bridge.md` @@ -3215,12 +3211,12 @@ Begge må være `true` for at en kobling skal vises. * 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 samlings-noder. - - -================================================================ -FILE: docs/features/kunnskapsgraf_og_relasjoner.md -================================================================ - + + +================================================================ +FILE: docs/features/kunnskapsgraf_og_relasjoner.md +================================================================ + # Feature Spec: Kunnskapsgraf og Relasjoner (Logseq-modell) **Filsti:** `docs/features/kunnskapsgraf_og_relasjoner.md` @@ -3388,12 +3384,12 @@ Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten: * **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. **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. - - -================================================================ -FILE: docs/features/live_ai.md -================================================================ - + + +================================================================ +FILE: docs/features/live_ai.md +================================================================ + # Feature: Live AI (Faktoid-oppslag & Referent) **Filsti:** `docs/features/live_ai.md` @@ -3460,12 +3456,12 @@ Kill switch-status (`ai_enabled: bool`) lagres på LiveKit-rommet i SpacetimeDB * 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. * Tilgang styres via `node_access`-matrisen. - - -================================================================ -FILE: docs/features/live_transkripsjon.md -================================================================ - + + +================================================================ +FILE: docs/features/live_transkripsjon.md +================================================================ + # Feature: Live Transkripsjon (Whisper-pipeline) **Filsti:** `docs/features/live_transkripsjon.md` @@ -3506,12 +3502,12 @@ Effekten er tydelig: * 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`). * Config (prompts) hentes fra samlings-nodens metadata (JSONB). - - -================================================================ -FILE: docs/features/lydmeldinger.md -================================================================ - + + +================================================================ +FILE: docs/features/lydmeldinger.md +================================================================ + # Feature: Lydmeldinger & Diktering **Filsti:** `docs/features/lydmeldinger.md` @@ -3649,12 +3645,12 @@ Ved brukeropprettelse opprettes en privat channel per bruker for usorterte lydme * `voice_memo` er en ny `message_type` — utvid enum i migrasjonen. * Personlig innboks-channel opprettes automatisk for nye brukere. * Tilgang styres via `node_access`-matrisen. - - -================================================================ -FILE: docs/features/meldingsboks.md -================================================================ - + + +================================================================ +FILE: docs/features/meldingsboks.md +================================================================ + # Feature: Meldingsboks — universell diskusjonsprimitiv **Filsti:** `docs/features/meldingsboks.md` @@ -3974,86 +3970,86 @@ Migrasjonen konverterer eksisterende data: - **TTL:** Implementér som nattlig jobbkø-jobb (`message_ttl_cleanup`). Sjekk fritak-regler før sletting. Ved sletting: slett fra `messages` → cascade til `nodes` via FK. - **Tilgang:** Styres via `node_access`-matrisen. Visibility håndteres i applikasjonskode (SvelteKit): `WHERE visibility = 'shared' OR author_id = $current_user`. - **Visibility:** Default `'shared'`. Sett `'private'` for personlige kladder. Endre til `'shared'` for å dele — ingen kopiering nødvendig. - - -================================================================ -FILE: docs/features/notater.md -================================================================ - -# Feature: Notater (Scratchpad) -**Filsti:** `docs/features/notater.md` - -## 1. Konsept -Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike kontekster — show notes, møtenotater, research-notater. Notater er nodes i kunnskapsgrafen og kan kobles til andre noder. - -## 2. Status -**PG-adapter ferdig og deployet (mars 2025).** Rich text og SpacetimeDB-sync gjenstår. - -### Implementert -- Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes) -- Notater er nodes — tilgangsstyrt via `node_access`-matrise -- Auto-save med 500ms debounce (visuell feedback: "Lagrer..."/"Lagret") -- REST API: GET og PATCH (tittel + innhold) -- PG polling-adapter med 10 sek intervall (tregere enn chat/kanban — notater endres sjeldnere) -- NotesBlock.svelte: tittel-input + fritekst-textarea med auto-save -- Polling pauses mens brukeren skriver (unngår overskriving av egne endringer) - -### Gjenstår — Fase 2 -- Markdown-editor (rich text med forhåndsvisning) -- Versjonering / undo-historikk -- Kobling til andre noder (temaer, episoder, aktører) -- Flerbruker-redigering (conflict resolution) -- SpacetimeDB-modul + hybrid-adapter -- Eksport (Markdown, PDF) - -## 3. Datamodell (implementert) - -```sql -CREATE TABLE notes ( - id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, - parent_id UUID NOT NULL REFERENCES nodes(id), - title TEXT NOT NULL DEFAULT '', - content TEXT NOT NULL DEFAULT '', - created_by TEXT REFERENCES users(authentik_id), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); -``` - -`updated_at` oppdateres automatisk ved PATCH og brukes til "Lagret [tidspunkt]"-visning. - -## 4. API-endepunkter - -| Metode | Sti | Beskrivelse | -|---|---|---| -| GET | `/api/notes/[noteId]` | Hent notat | -| PATCH | `/api/notes/[noteId]` | Oppdater tittel og/eller innhold | - -## 5. Brukes av - -| Konsept | Bruk | -|---|---| -| Redaksjonen (Sidelinja) | Show notes for episoder | -| Foreningen Liberalistene | Møtenotater | -| Møterommet (fremtidig) | Scratchpad under møter | - -## 6. Auto-save-mønster -- Bruker skriver → 500ms debounce → PATCH til server -- Under lagring vises "Lagrer..." (gul) -- Etter vellykket lagring vises "Lagret [dato]" (grå) -- Polling (10 sek) henter siste versjon, men hopper over overskriving mens `saving`-flagget er satt - -## 7. Instruks for Claude Code -* Tilgang til notater styres via `node_access`-matrisen -* Auto-save bruker debounce — ikke send PATCH ved hvert tastetrykk -* `updated_at` brukes til UI-feedback, ikke til conflict resolution (ennå) -* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi - - -================================================================ -FILE: docs/features/podcast_statistikk.md -================================================================ - + + +================================================================ +FILE: docs/features/notater.md +================================================================ + +# Feature: Notater (Scratchpad) +**Filsti:** `docs/features/notater.md` + +## 1. Konsept +Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike kontekster — show notes, møtenotater, research-notater. Notater er nodes i kunnskapsgrafen og kan kobles til andre noder. + +## 2. Status +**PG-adapter ferdig og deployet (mars 2025).** Rich text og SpacetimeDB-sync gjenstår. + +### Implementert +- Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes) +- Notater er nodes — tilgangsstyrt via `node_access`-matrise +- Auto-save med 500ms debounce (visuell feedback: "Lagrer..."/"Lagret") +- REST API: GET og PATCH (tittel + innhold) +- PG polling-adapter med 10 sek intervall (tregere enn chat/kanban — notater endres sjeldnere) +- NotesBlock.svelte: tittel-input + fritekst-textarea med auto-save +- Polling pauses mens brukeren skriver (unngår overskriving av egne endringer) + +### Gjenstår — Fase 2 +- Markdown-editor (rich text med forhåndsvisning) +- Versjonering / undo-historikk +- Kobling til andre noder (temaer, episoder, aktører) +- Flerbruker-redigering (conflict resolution) +- SpacetimeDB-modul + hybrid-adapter +- Eksport (Markdown, PDF) + +## 3. Datamodell (implementert) + +```sql +CREATE TABLE notes ( + id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, + parent_id UUID NOT NULL REFERENCES nodes(id), + title TEXT NOT NULL DEFAULT '', + content TEXT NOT NULL DEFAULT '', + created_by TEXT REFERENCES users(authentik_id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +`updated_at` oppdateres automatisk ved PATCH og brukes til "Lagret [tidspunkt]"-visning. + +## 4. API-endepunkter + +| Metode | Sti | Beskrivelse | +|---|---|---| +| GET | `/api/notes/[noteId]` | Hent notat | +| PATCH | `/api/notes/[noteId]` | Oppdater tittel og/eller innhold | + +## 5. Brukes av + +| Konsept | Bruk | +|---|---| +| Redaksjonen (Sidelinja) | Show notes for episoder | +| Foreningen Liberalistene | Møtenotater | +| Møterommet (fremtidig) | Scratchpad under møter | + +## 6. Auto-save-mønster +- Bruker skriver → 500ms debounce → PATCH til server +- Under lagring vises "Lagrer..." (gul) +- Etter vellykket lagring vises "Lagret [dato]" (grå) +- Polling (10 sek) henter siste versjon, men hopper over overskriving mens `saving`-flagget er satt + +## 7. Instruks for Claude Code +* Tilgang til notater styres via `node_access`-matrisen +* Auto-save bruker debounce — ikke send PATCH ved hvert tastetrykk +* `updated_at` brukes til UI-feedback, ikke til conflict resolution (ennå) +* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi + + +================================================================ +FILE: docs/features/podcast_statistikk.md +================================================================ + # Feature Spec: Podcast-Statistikk **Filsti:** `docs/features/podcast_statistikk.md` @@ -4083,12 +4079,12 @@ Caddy access-logger inneholder IP-adresser og User-Agent — dette er personoppl * 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 tenant-scopet. `episode_stats` merkes med `tenant_id`. Admin-visningen filtrerer per tenant. - -================================================================ -FILE: docs/features/prompt_lab.md -================================================================ - +* Statistikk er tenant-scopet. `episode_stats` merkes med `tenant_id`. Admin-visningen filtrerer per tenant. + +================================================================ +FILE: docs/features/prompt_lab.md +================================================================ + # Feature: Prompt-Laboratorium **Filsti:** `docs/features/prompt_lab.md` @@ -4192,12 +4188,12 @@ Muliggjør rollback: "forrige prompt for research_clip fungerte bedre, rull tilb * Testdata (transkripsjoner, artikler) lastes via SvelteKit server-side fra PG. Gjesten-UX vises aldri her. * `prompt_test_runs` og `prompt_history` er knyttet til en samlings-node og trenger ikke RLS — de er kun tilgjengelige for admins via applikasjonslogikk. * Tilgang styres via `node_access`-matrisen. - - -================================================================ -FILE: docs/features/universell_overfoering.md -================================================================ - + + +================================================================ +FILE: docs/features/universell_overfoering.md +================================================================ + # Feature: Universell overføring — flytt objekter mellom blokker **Filsti:** `docs/features/universell_overfoering.md` @@ -4399,12 +4395,12 @@ Ny tabell `message_placement` med reducers for place/remove/move. - Drag-and-drop bruker HTML5 Drag and Drop API for blokk-til-blokk, og pointer events for intra-canvas (storyboard/whiteboard) - Hold overføringslogikken i en sentral `transferService` — ikke spread ut i hver blokk-type - Mottaker-interfacet er obligatorisk for alle blokk-typer - - -================================================================ -FILE: docs/features/visuell_graf.md -================================================================ - + + +================================================================ +FILE: docs/features/visuell_graf.md +================================================================ + # Feature: Visuell Kunnskapsgraf (Graph View) **Filsti:** `docs/features/visuell_graf.md` @@ -4425,12 +4421,12 @@ En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt naviger * 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. * Tilgangsstyrt: noder filtreres via `node_access`-matrisen. - - -================================================================ -FILE: docs/features/whiteboard.md -================================================================ - + + +================================================================ +FILE: docs/features/whiteboard.md +================================================================ + # Feature Spec: Whiteboard (Frihåndstavle) **Filsti:** `docs/features/whiteboard.md` @@ -4465,12 +4461,12 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst * 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. * Tilgang til whiteboards styres via `node_access`-matrisen. - - -================================================================ -FILE: docs/infra/ai_gateway.md -================================================================ - + + +================================================================ +FILE: docs/infra/ai_gateway.md +================================================================ + # Infrastruktur: AI Gateway (LiteLLM) **Filsti:** `docs/infra/ai_gateway.md` @@ -4720,12 +4716,12 @@ For sensitive redaksjonelle diskusjoner kan en lokal LLM-leverandør (Ollama/vLL * Test alltid med norsk innhold før en ny modell/leverandør tas i bruk for en produksjonsoppgave * Kjør `promptfoo eval` før du endrer prompts eller bytter modell for en jobbtype * Nye jobbtyper som bruker LLM skal ha et tilhørende testsett i `tests/prompts/` før de merges - - -================================================================ -FILE: docs/infra/api_grensesnitt.md -================================================================ - + + +================================================================ +FILE: docs/infra/api_grensesnitt.md +================================================================ + # Infrastruktur: API-grensesnitt og Tjenesteansvar **Filsti:** `docs/infra/api_grensesnitt.md` @@ -4799,12 +4795,12 @@ Definerer hvordan SvelteKit-frontenden kommuniserer med backend-tjenestene. Prin - Bruk SvelteKit `+server.ts` (API routes) eller `+page.server.ts` (form actions/load) for all HTTP-kommunikasjon - Rust-kode skal struktureres som worker-binærer som konsumerer fra `job_queue` - For PG-tilgang i SvelteKit, bruk et bibliotek som `postgres.js` eller `drizzle-orm` - - -================================================================ -FILE: docs/infra/jobbkø.md -================================================================ - + + +================================================================ +FILE: docs/infra/jobbkø.md +================================================================ + # Infrastruktur: Jobbkø (PostgreSQL-basert) **Filsti:** `docs/infra/jobbkø.md` @@ -4945,124 +4941,124 @@ Alle jobber merkes med `collection_node_id`. Rust-workers kjører som superuser - Opprett alltid jobber med riktig `collection_node_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 - Splitt til flere binærer kun hvis det blir eksplisitt bedt om — start med én - - -================================================================ -FILE: docs/infra/synkronisering.md -================================================================ - -# Synkronisering: SpacetimeDB ↔ PostgreSQL - -## Konsept - -SpacetimeDB holder hele grafen (alle noder og edges) i minne. -PostgreSQL er persistent backup og arkiv. Skriving går til begge. -Lesing går fra SpacetimeDB. - -``` -GUI (SvelteKit) - │ skriv │ les (sanntid) - ▼ ▼ -Maskinrommet (Rust) SpacetimeDB ──→ GUI - │ ▲ - ├──→ PostgreSQL (persistent) │ - └──→ SpacetimeDB ───────────┘ -``` - -## Hvorfor hele grafen i SpacetimeDB - -Node- og edge-skjemaet er minimalt: åtte kolonner på nodes, åtte -på edges. For en liten brukerbase er hele grafen triviell å holde -i minne. Dette forenkler alt: - -- Ingen sync-logikk for å bestemme hva som er "aktivt" -- Ingen henting fra PG når noen åpner noe gammelt -- Frontend har alltid alt tilgjengelig via WebSocket -- Én lesekilde — ingen tvetydighet - -Når dette *blir* problematisk (hundretusenvis av noder, minnepress), -innføres eviction basert på aksessmønstre. Men det er en -optimaliseringsbeslutning, ikke en arkitekturbeslutning. Modellen -endres ikke. - -## Skrivestien - -Maskinrommet validerer, så skriver i to steg: - -1. **Validering** — tilgangssjekk, unique-constraints, forretningsregler -2. **SpacetimeDB først** — frontend oppdateres umiddelbart (~10μs) -3. **PG asynkront** — persistent backup i bakgrunnen - -Frontend er allerede oppdatert mens PG-skrivingen skjer. Brukeren -merker ingenting. - -Hvis PG-skrivingen feiler: maskinrommet logger og prøver igjen. -SpacetimeDB har dataen — den er bare ikke persistert ennå. Ingen -datatap så lenge SpacetimeDB kjører. - -## Lesestien - -Frontend leser **kun fra SpacetimeDB** via WebSocket-subscriptions. -Ingen PG-kall fra frontend for data som vises i sanntid. - -Unntak: tunge spørringer som ikke passer sanntidslaget — statistikk, -fulltekstsøk, pgvector-søk, AGE-traverseringer. Disse går -GUI → Maskinrommet → PG. - -## Oppvarming (PG → SpacetimeDB) - -Ved oppstart av SpacetimeDB laster maskinrommet hele grafen fra PG: - -1. Alle noder -2. Alle edges -3. `node_access`-matrisen - -Rekkefølge: noder først (edges refererer til noder). -CAS-noder lastes uten binærinnhold (bare metadata). - -## Feilhåndtering - -| Scenario | Konsekvens | Håndtering | -|----------|-----------|------------| -| SpacetimeDB krasjer | Sanntid forsvinner, upersistert data kan tapes | Restart + oppvarming fra PG. Tap begrenset til skrivinger som ikke rakk PG. | -| PG nede | Persistering stopper | SpacetimeDB serverer lesing og mottar skrivinger. PG-backlog tas igjen ved recovery. | -| Maskinrommet krasjer | Ingen skriving | Frontend ser siste state. Restart plukker opp. | - -## SpacetimeDB som utbyttbar - -SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den -fjernes fra stacken: - -- **Sanntid:** PG `LISTEN/NOTIFY` → SvelteKit SSE -- **Skriving:** Maskinrommet → PG direkte -- **Lesing:** Maskinrommet → PG → GUI - -Denne fallbacken trenger ikke implementeres, men arkitekturen -skal aldri gjøre den umulig. - -## Tilgangsmatrise i SpacetimeDB - -`node_access`-matrisen lastes inn i SpacetimeDB ved oppvarming. -Når maskinrommet oppdaterer matrisen i PG (ved edge-endring), -oppdaterer den også SpacetimeDB. SpacetimeDB-modulen bruker -matrisen for å filtrere subscriptions — klienter ser kun noder -de har tilgang til. - -## Konflikthåndtering - -SpacetimeDB er single-threaded per modul — reducer-funksjoner -serialiseres automatisk. Ingen klassiske race conditions. - -Samtidig redigering (to brukere endrer samme node): maskinrommet -serialiserer via SpacetimeDB. Last-write-wins. SpacetimeDB -kringkaster resultatet til alle klienter. PG oppdateres asynkront -med det endelige resultatet. - - -================================================================ -FILE: docs/setup/lokal.md -================================================================ - + + +================================================================ +FILE: docs/infra/synkronisering.md +================================================================ + +# Synkronisering: SpacetimeDB ↔ PostgreSQL + +## Konsept + +SpacetimeDB holder hele grafen (alle noder og edges) i minne. +PostgreSQL er persistent backup og arkiv. Skriving går til begge. +Lesing går fra SpacetimeDB. + +``` +GUI (SvelteKit) + │ skriv │ les (sanntid) + ▼ ▼ +Maskinrommet (Rust) SpacetimeDB ──→ GUI + │ ▲ + ├──→ PostgreSQL (persistent) │ + └──→ SpacetimeDB ───────────┘ +``` + +## Hvorfor hele grafen i SpacetimeDB + +Node- og edge-skjemaet er minimalt: åtte kolonner på nodes, åtte +på edges. For en liten brukerbase er hele grafen triviell å holde +i minne. Dette forenkler alt: + +- Ingen sync-logikk for å bestemme hva som er "aktivt" +- Ingen henting fra PG når noen åpner noe gammelt +- Frontend har alltid alt tilgjengelig via WebSocket +- Én lesekilde — ingen tvetydighet + +Når dette *blir* problematisk (hundretusenvis av noder, minnepress), +innføres eviction basert på aksessmønstre. Men det er en +optimaliseringsbeslutning, ikke en arkitekturbeslutning. Modellen +endres ikke. + +## Skrivestien + +Maskinrommet validerer, så skriver i to steg: + +1. **Validering** — tilgangssjekk, unique-constraints, forretningsregler +2. **SpacetimeDB først** — frontend oppdateres umiddelbart (~10μs) +3. **PG asynkront** — persistent backup i bakgrunnen + +Frontend er allerede oppdatert mens PG-skrivingen skjer. Brukeren +merker ingenting. + +Hvis PG-skrivingen feiler: maskinrommet logger og prøver igjen. +SpacetimeDB har dataen — den er bare ikke persistert ennå. Ingen +datatap så lenge SpacetimeDB kjører. + +## Lesestien + +Frontend leser **kun fra SpacetimeDB** via WebSocket-subscriptions. +Ingen PG-kall fra frontend for data som vises i sanntid. + +Unntak: tunge spørringer som ikke passer sanntidslaget — statistikk, +fulltekstsøk, pgvector-søk, AGE-traverseringer. Disse går +GUI → Maskinrommet → PG. + +## Oppvarming (PG → SpacetimeDB) + +Ved oppstart av SpacetimeDB laster maskinrommet hele grafen fra PG: + +1. Alle noder +2. Alle edges +3. `node_access`-matrisen + +Rekkefølge: noder først (edges refererer til noder). +CAS-noder lastes uten binærinnhold (bare metadata). + +## Feilhåndtering + +| Scenario | Konsekvens | Håndtering | +|----------|-----------|------------| +| SpacetimeDB krasjer | Sanntid forsvinner, upersistert data kan tapes | Restart + oppvarming fra PG. Tap begrenset til skrivinger som ikke rakk PG. | +| PG nede | Persistering stopper | SpacetimeDB serverer lesing og mottar skrivinger. PG-backlog tas igjen ved recovery. | +| Maskinrommet krasjer | Ingen skriving | Frontend ser siste state. Restart plukker opp. | + +## SpacetimeDB som utbyttbar + +SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den +fjernes fra stacken: + +- **Sanntid:** PG `LISTEN/NOTIFY` → SvelteKit SSE +- **Skriving:** Maskinrommet → PG direkte +- **Lesing:** Maskinrommet → PG → GUI + +Denne fallbacken trenger ikke implementeres, men arkitekturen +skal aldri gjøre den umulig. + +## Tilgangsmatrise i SpacetimeDB + +`node_access`-matrisen lastes inn i SpacetimeDB ved oppvarming. +Når maskinrommet oppdaterer matrisen i PG (ved edge-endring), +oppdaterer den også SpacetimeDB. SpacetimeDB-modulen bruker +matrisen for å filtrere subscriptions — klienter ser kun noder +de har tilgang til. + +## Konflikthåndtering + +SpacetimeDB er single-threaded per modul — reducer-funksjoner +serialiseres automatisk. Ingen klassiske race conditions. + +Samtidig redigering (to brukere endrer samme node): maskinrommet +serialiserer via SpacetimeDB. Last-write-wins. SpacetimeDB +kringkaster resultatet til alle klienter. PG oppdateres asynkront +med det endelige resultatet. + + +================================================================ +FILE: docs/setup/lokal.md +================================================================ + # Oppsett: Lokalt Utviklingsmiljø (WSL2) **Filsti:** `docs/setup/lokal.md` @@ -5144,12 +5140,12 @@ ssh vegard@157.180.81.26 "cd /srv/synops && git pull && docker compose up -d --b | Database/tjenester | Kobler til server | Kjører lokalt i Docker | | HTTPS | Ikke nødvendig | Let's Encrypt via Caddy | | Auth | Authentik bypass eller dev-token | Full Authentik OIDC | - - -================================================================ -FILE: docs/setup/migration_safety.md -================================================================ - + + +================================================================ +FILE: docs/setup/migration_safety.md +================================================================ + # Migration Safety Checklist > **Merk:** Denne sjekklisten er skrevet for v1-arkitekturen der RLS var @@ -5307,12 +5303,12 @@ WHERE c.column_name = 'workspace_id' ## Automatisering Disse sjekkene kjøres automatisk i migrasjonstestene (se `docs/arkitektur.md` §10.2). Manuell kjøring er kun nødvendig ved prod-migrasjoner til automatiserte tester er på plass. **RLS Leak Hunter bør prioriteres som første CI-steg — den beskytter mot den mest alvorlige feilkategorien (cross-workspace datalekkasje).** - - -================================================================ -FILE: docs/setup/produksjon.md -================================================================ - + + +================================================================ +FILE: docs/setup/produksjon.md +================================================================ + # Oppsett: Produksjonsserver (Hetzner VPS) **Filsti:** `docs/setup/produksjon.md` @@ -5737,463 +5733,463 @@ docker compose up -d - [ ] SpacetimeDB: WebSocket-tilkobling fra nettleser fungerer - [ ] LiveKit: Test-rom med video/lyd fungerer - [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes` - - -================================================================ -FILE: docs/erfaringer/README.md -================================================================ - -# 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 | -| `authentik_oidc.md` | Authentik sub-claim format, @auth/sveltekit JWT-quirks | - -## 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. - - -================================================================ -FILE: docs/erfaringer/adapter_moenster.md -================================================================ - -# Erfaring: Adapter-mønster for chat (PG ↔ SpacetimeDB) - -## 1. Mønsteret - -Et felles interface (`ChatConnection`) med to implementasjoner: -- **SpacetimeDB-adapter** (primær) — all data fra SpacetimeDB, worker håndterer warmup + sync -- **PG-adapter** (fallback) — polling hvert 3 sek, brukes kun når SpacetimeDB ikke er konfigurert - -Factory-funksjon velger adapter basert på miljøvariabel (`VITE_SPACETIMEDB_URL`). - -``` -ChatBlock.svelte → createChat() → SpacetimeDB-adapter (primær) - → PG-adapter (fallback, readonly) -``` - -**Fordeler:** -- Kan teste PG-adapter isolert uten Docker/SpacetimeDB -- Fallback er trivielt — fjern env-variabelen -- Komponenten vet ingenting om hvilken adapter som brukes -- ChatConnection-interface har `edit()`, `delete()`, `react()` — ingen direkte PG API-kall fra komponenten - -**Referanse:** `web/src/lib/chat/` — hele mappen er organisert etter dette mønsteret. - -## 2. Historisk anti-pattern: Lazy wrapper som byttet 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. - -## 3. Historisk anti-pattern: Hybrid-adapter (PG + SpacetimeDB samtidig) - -Den andre iterasjonen brukte en hybrid-adapter som hentet historikk fra PG via REST og lyttet på SpacetimeDB for nye meldinger. Dette skapte: - -- Kompleks dedup-logikk (`deletedIds` Set, merge av PG- og ST-meldinger) -- Race conditions mellom PG-polling og SpacetimeDB-callbacks -- BigInt-konverteringer og workarounds i frontend - -**Løsningen:** SpacetimeDB som cache foran PG. Worker gjør warmup (PG → ST) ved oppstart, frontend snakker kun med SpacetimeDB. Ingen merge-logikk nødvendig. - -## 4. Nåværende arkitektur - -SpacetimeDB er en varm cache foran PostgreSQL: -- **Worker warmup:** Ved oppstart lastes meldinger + reaksjoner fra PG → SpacetimeDB per kanal -- **Frontend → SpacetimeDB:** Subscription gir alle meldinger (historikk fra warmup + nye) -- **SpacetimeDB → PG:** Sync-worker poller `sync_outbox` hvert sekund -- **PG er autoritativ** — ved SpacetimeDB-restart oppvarmes fra PG - -**Fordeler over hybrid:** -- Ingen dedup, merge eller deletedIds -- Frontend-koden er dramatisk enklere -- Konsistent datamodell — alt kommer fra én kilde -- Reaksjoner håndteres via SpacetimeDB-tabeller, ikke PG API - -## 5. Historisk anti-pattern: "PG-lekkasje" i SpacetimeDB-adapteren - -Gjentatt feil (mars 2026, minst 3 iterasjoner): Når en ny feature trenger data som SpacetimeDB-modulen ikke har (metadata, edited_at, revisjoner), er det fristende å legge til en `enrichFromPg()`-funksjon som henter fra PG direkte. Dette bryter hele poenget med caching-laget. - -**Symptomer:** -- SpacetimeDB-adapteren har `fetch('/api/messages/...')` kall -- Worker skriver til PG først, SpacetimeDB er "best-effort" -- Etter AI-vask vises ikke metadata/revisjoner fordi de bare finnes i PG -- Race conditions mellom SpacetimeDB-oppdateringer og PG-fetch - -**Hvorfor det skjer:** Det er raskere å lage en PG API-rute enn å utvide SpacetimeDB-modulen (Rust compile, publish, regenerer bindings). Men det skaper teknisk gjeld som akkumulerer og undergraver hele arkitekturen. - -**Riktig løsning når SpacetimeDB mangler et felt:** -1. Legg til feltet i SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`) -2. Utvid warmup til å laste feltet fra PG -3. Utvid sync til å persistere feltet til PG -4. Worker skriver til SpacetimeDB via reducer -5. Frontend leser kun fra SpacetimeDB - -**Aldri:** Legg til `fetch('/api/.../metadata')` i SpacetimeDB-adapteren. - -## 6. 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-adapter med warmup.** Worker laster data fra PG ved oppstart. -3. **Bruk samme factory-mønster.** Felles interface, env-variabel for valg. -4. **Legg til warmup-config** i `channels.config` (eller tilsvarende config-felt). -5. **Test begge adaptere uavhengig** før du integrerer i UI-komponenten. -6. **Sjekk at alle felter frontend trenger finnes i SpacetimeDB-modulen** før du implementerer adapteren. Utvid modulen først hvis nødvendig. - - -================================================================ -FILE: docs/erfaringer/authentik_oidc.md -================================================================ - -# Erfaring: Authentik OIDC-integrasjon - -## 1. `profile.sub` er IKKE Authentik sin PostgreSQL-UUID - -Authentik sin OIDC `sub`-claim er en **SHA256-hash**, ikke UUID-kolonnen fra `authentik_core_user`. Eksempel: - -| Felt | Verdi | -|---|---| -| Authentik DB `uuid` | `0ac94e00-015b-4e78-9f32-269fa6ce3f44` | -| OIDC `sub` claim | `6af61f43c6647a237cbb381ee7788376a9bc20299c2c06281d9954d763e854f0` | - -Bruk **alltid** `sub`-verdien fra OIDC som nøkkel i `users.authentik_id`. For å finne den riktige verdien for en bruker: logg inn og les `profile.sub` fra callback, eller sjekk JWT-tokenet. - -## 2. `@auth/sveltekit` sin `user.id` er IKKE `profile.sub` - -`@auth/sveltekit` genererer sin egen interne UUID for `user.id` i JWT. Denne overlever ikke mellom sesjoner og matcher ingenting i vår database. - -For å bruke Authentik `sub` som bruker-ID: - -```typescript -callbacks: { - jwt({ token, user, profile }) { - if (user) token.id = user.id; - if (profile?.sub) token.authentik_sub = profile.sub; - return token; - }, - session({ session, token }) { - if (session.user) { - // Bruk Authentik sub, IKKE token.id - session.user.id = (token.authentik_sub ?? token.id) as string; - } - return session; - } -} -``` - -`profile` er kun tilgjengelig i JWT-callbacken ved innlogging (ikke ved token-refresh), derfor må `authentik_sub` lagres i tokenet. - -**Referanse:** `web/src/lib/server/auth.ts` - -## 3. Redirect-URI i Authentik - -`@auth/sveltekit` bruker callback-URL `https:///auth/callback/`. For oss: `https://sidelinja.org/auth/callback/authentik`. - -Denne MÅ være registrert som redirect-URI i Authentik sin OAuth2-provider. Verifiser via: - -```sql -SELECT _redirect_uris FROM authentik_providers_oauth2_oauth2provider -WHERE client_id = ''; -``` - -**Tips:** Legg til en regex-variant for lokal utvikling: `http://localhost:\d+/auth/callback/authentik` med `matching_mode: "regex"`. - - -================================================================ -FILE: docs/erfaringer/spacetimedb_integrasjon.md -================================================================ - -# 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. Timestamps — bruk SDK-metoder, ikke interne felter - -SpacetimeDB `Timestamp`-objektet har en intern property `__timestamp_micros_since_unix_epoch__` (BigInt). **Ikke bruk den direkte** — bruk SDK-metodene: - -```typescript -// FEIL — microsSinceEpoch finnes ikke, __timestamp_micros_since_unix_epoch__ er internt -const micros = row.createdAt?.microsSinceEpoch; - -// RIKTIG — bruk SDK-metoder -const iso = row.createdAt?.toISOString(); // "2026-03-15T23:57:11.677139Z" -const date = row.createdAt?.toDate(); // Date-objekt -const ms = row.createdAt?.toDate()?.getTime(); // millisekunder for sortering -``` - -### Timestamp-parsing i Rust-modul (warmup) - -PG returnerer timestamps som `"2026-03-15 23:57:11.677139+00"`. chrono parser IKKE `+00` — krever `+00:00`: - -```rust -// FEIL — chrono gir ParseError(TooShort) på "+00" -let dt = s.parse::>(); - -// RIKTIG — normaliser PG-offset først -let normalized = if s.ends_with("+00") { format!("{}:00", s) } else { s.to_string() }; -let dt = chrono::DateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S%.f%:z"); -``` - -Uten korrekt parsing faller `load_messages` tilbake til `ctx.timestamp` (nåtidspunkt), og alle meldinger får samme klokkeslett. - -**Referanse:** `spacetimedb/src/lib.rs` — `parse_timestamp()`, `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 local - -# Generer TypeScript-bindings -spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindings \ - --module-path . -``` - -**Merk:** SpacetimeDB-modulen publiseres manuelt med `spacetime publish` mot server-instansen. - -## 6. Arkitekturendring: SpacetimeDB som cache foran PG (mars 2026) - -Tidligere: Hybrid-adapter der frontend merget data fra PG (historikk) og SpacetimeDB (sanntid) med dedup, deletedIds og BigInt-workarounds. - -Ny modell: -- **PG autoritativ** — all persistent data i PostgreSQL -- **SpacetimeDB = varm cache** — worker gjør warmup (PG → ST) ved oppstart -- **Frontend snakker KUN med ST** — ingen PG API-kall fra chat-adapteren -- **Worker håndterer toveissynk** — ST → PG for nye/redigerte/slettede meldinger og reaksjoner - -### Warmup-flyt -1. Worker starter → `warmup::run()` leser kanaler med config fra PG -2. Per kanal: sjekker `channels.config.warmup_mode` (all/messages/days/none) -3. Kaller `clear_channel` reducer (unngår duplikater ved restart) -4. **Trådbasert henting:** Finner kvalifiserende tråder, henter alle meldinger i disse (komplett med svar) - - `messages`-modus: de N nyeste trådene (sortert etter siste aktivitet) - - `days`-modus: alle tråder med minst én melding i tidsvinduet - - Et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter) -5. Kaller `load_messages` reducer med JSON-array -6. Laster også reaksjoner via `load_reactions` reducer - -### Per-kanal konfigurasjon -- Lagres i `channels.config` JSONB: `warmup_mode` + `warmup_value` -- Admin-UI: `/admin/channels` — tabell med inline-redigering -- Default: `"all"` (last alt). Andre: `"messages"` (siste N tråder), `"days"` (siste N dager), `"none"` (inaktiv) - -### Sync-flyt (ST → PG) -- SyncOutbox-events prosesseres hver 1. sekund -- Støtter: `messages/insert`, `messages/delete`, `messages/update`, `messages/ai_update`, `message_reactions/insert`, `message_reactions/delete` -- `ai_update`-action: oppdaterer body + metadata + edited_at i PG, inserter revisjon - -## 7. Subscription-begrensninger - -**SpacetimeDB-subscriptions støtter IKKE JOINs.** En subscription-query som `SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE ...` feiler stille — `onApplied` kalles aldri, og ingen data vises. - -Bruk kun enkle `SELECT * FROM tabell WHERE ...`-queries i `.subscribe([...])`. Filtrer heller klient-side etter at data er lastet. - -Eksempel: -```typescript -// FEIL — feiler stille, ingen data -.subscribe([ - `SELECT * FROM chat_message WHERE channel_id = '${id}'`, - `SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE cm.channel_id = '${id}'` -]); - -// RIKTIG — last alle reaksjoner, filtrer i koden -.subscribe([ - `SELECT * FROM chat_message WHERE channel_id = '${id}'`, - `SELECT * FROM message_reaction` -]); -``` - -### Fallback -PG-polling adapter (`pg.svelte.ts`) brukes kun når SpacetimeDB ikke er konfigurert. Markeres som `readonly: true`. - -## 8. Reducer-parameternavn — unngå underscore-prefix - -> *Kodeeksemplene i denne seksjonen er fra v1 og bruker `workspace_id`-parametere. -> Workspace-modellen er erstattet av noder og edges (se `docs/retninger/bruker_ikke_workspace.md`), -> men lærdommen om underscore-prefix gjelder generelt for alle SpacetimeDB-reducere.* - -SpacetimeDB eksponerer Rust-parameternavn direkte i HTTP JSON API-et. Underscore-prefix (`_workspace_id`) blir til `_workspace_id` i JSON, ikke `workspace_id`: - -```rust -// FEIL — HTTP-kall med {"workspace_id": "..."} feiler med 400 -pub fn set_ai_processing(ctx: &ReducerContext, id: String, _workspace_id: String) { ... } - -// RIKTIG — bruk vanlig navn, suppress warning med let _ = &var; -pub fn set_ai_processing(ctx: &ReducerContext, id: String, workspace_id: String) -> Result<(), String> { - let _ = &workspace_id; - // ... -} -``` - -## 9. Schema-migrering ved nye kolonner - -Å legge til kolonner på eksisterende SpacetimeDB-tabeller krever `--delete-data` ved publish. Dette sletter all data og krever warmup på nytt: - -```bash -# Feiler uten --delete-data: -# "Adding a column metadata to table chat_message requires a default value annotation" -echo "y" | spacetime publish sidelinja-realtime --server local --delete-data -``` - -## 10. AI-worker-flyt via SpacetimeDB - -Worker som gjør AI-behandling av meldinger: -1. Leser meldingens body fra PG (OK — PG er persistent lager) -2. Kaller `set_ai_processing` reducer → frontend ser pulsering umiddelbart -3. Kaller AI Gateway med prompt -4. Kaller `ai_update_message` reducer → SpacetimeDB oppdaterer body/metadata/edited_at atomisk, lagrer revisjon, legger outbox-entry -5. Sync-worker persisterer til PG via `ai_update` action -6. Ved feil: `clear_ai_processing` reducer rydder flagget - - -================================================================ -FILE: docs/erfaringer/svelte5_reaktivitet.md -================================================================ - -# Erfaring: Svelte 5 Reaktivitet - -## 1. `$state` i `.svelte.ts` krever getters - -Svelte 5 sin `$state` lager reaktive proxyer. Når en funksjon returnerer et objekt med `$state`-verdier, **mister man reaktiviteten** hvis man returnerer verdien direkte: - -```typescript -// FEIL — mister reaktivitet, verdien fryses ved retur -function createThing() { - let count = $state(0); - return { count }; // snapshot, ikke reaktiv -} - -// RIKTIG — getter bevarer proxy-tilgang -function createThing() { - let count = $state(0); - return { - get count() { return count; } - }; -} -``` - -**Referanse:** `web/src/lib/chat/pg.svelte.ts` — alle returnerte verdier bruker getters. - -## 2. SSR kjører alt utenfor `onMount` - -SvelteKit server-renderer kjører komponent-script ved SSR. Alt som bruker browser-APIer (WebSocket, `fetch` til relative URLer, `setInterval`, `sessionStorage`) **krasjer på serveren** hvis det ikke er beskyttet. - -```typescript -// FEIL — krasjer ved SSR -let chat = createChat(channelId); // kjøres på server - -// RIKTIG — kun i browser -import { onMount } from 'svelte'; -let chat = $state(null); -onMount(() => { - chat = createChat(channelId); - return () => chat?.destroy(); -}); -``` - -Alternativt kan factory-funksjonen selv sjekke: -```typescript -import { browser } from '$app/environment'; -if (browser) { /* ... */ } -``` - -**Referanse:** `web/src/lib/chat/create.svelte.ts` — `browser`-guard i factory, `web/src/lib/blocks/ChatBlock.svelte` — `onMount` for opprettelse. - -## 3. `$derived` og `$effect` med null-initialisert state - -Når en `$state`-variabel starter som `null` (fordi den settes i `onMount`), må `$derived` og `$effect` håndtere null-tilfellet: - -```typescript -let chat = $state(null); -let messages = $derived(chat?.messages ?? []); // fallback til tom liste - -$effect(() => { - const count = messages.length; // trygt, alltid array - if (count > prevCount) scrollToBottom(); - prevCount = count; -}); -``` - -**Referanse:** `web/src/lib/blocks/ChatBlock.svelte` — `$derived` med optional chaining. - -## 4. Polling: full-fetch slår inkrementell akkumulering - -Vi prøvde først å bruke en `latestTimestamp`-cursor for å hente kun nye meldinger og appende dem. Dette ga duplikater — telleren vokste mens man tastet (polling i bakgrunnen). - -**Løsning:** Enkel `refresh()` som alltid henter full liste og erstatter `messages` i sin helhet. For et lite volum meldinger er dette enklere og tryggere enn inkrementell logikk. - -**Referanse:** `web/src/lib/chat/pg.svelte.ts` — `refresh()` gjør full fetch, ingen akkumulering. - - -================================================================ -FILE: docs/proposals/README.md -================================================================ - + + +================================================================ +FILE: docs/erfaringer/README.md +================================================================ + +# 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 | +| `authentik_oidc.md` | Authentik sub-claim format, @auth/sveltekit JWT-quirks | + +## 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. + + +================================================================ +FILE: docs/erfaringer/adapter_moenster.md +================================================================ + +# Erfaring: Adapter-mønster for chat (PG ↔ SpacetimeDB) + +## 1. Mønsteret + +Et felles interface (`ChatConnection`) med to implementasjoner: +- **SpacetimeDB-adapter** (primær) — all data fra SpacetimeDB, worker håndterer warmup + sync +- **PG-adapter** (fallback) — polling hvert 3 sek, brukes kun når SpacetimeDB ikke er konfigurert + +Factory-funksjon velger adapter basert på miljøvariabel (`VITE_SPACETIMEDB_URL`). + +``` +ChatBlock.svelte → createChat() → SpacetimeDB-adapter (primær) + → PG-adapter (fallback, readonly) +``` + +**Fordeler:** +- Kan teste PG-adapter isolert uten Docker/SpacetimeDB +- Fallback er trivielt — fjern env-variabelen +- Komponenten vet ingenting om hvilken adapter som brukes +- ChatConnection-interface har `edit()`, `delete()`, `react()` — ingen direkte PG API-kall fra komponenten + +**Referanse:** `web/src/lib/chat/` — hele mappen er organisert etter dette mønsteret. + +## 2. Historisk anti-pattern: Lazy wrapper som byttet 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. + +## 3. Historisk anti-pattern: Hybrid-adapter (PG + SpacetimeDB samtidig) + +Den andre iterasjonen brukte en hybrid-adapter som hentet historikk fra PG via REST og lyttet på SpacetimeDB for nye meldinger. Dette skapte: + +- Kompleks dedup-logikk (`deletedIds` Set, merge av PG- og ST-meldinger) +- Race conditions mellom PG-polling og SpacetimeDB-callbacks +- BigInt-konverteringer og workarounds i frontend + +**Løsningen:** SpacetimeDB som cache foran PG. Worker gjør warmup (PG → ST) ved oppstart, frontend snakker kun med SpacetimeDB. Ingen merge-logikk nødvendig. + +## 4. Nåværende arkitektur + +SpacetimeDB er en varm cache foran PostgreSQL: +- **Worker warmup:** Ved oppstart lastes meldinger + reaksjoner fra PG → SpacetimeDB per kanal +- **Frontend → SpacetimeDB:** Subscription gir alle meldinger (historikk fra warmup + nye) +- **SpacetimeDB → PG:** Sync-worker poller `sync_outbox` hvert sekund +- **PG er autoritativ** — ved SpacetimeDB-restart oppvarmes fra PG + +**Fordeler over hybrid:** +- Ingen dedup, merge eller deletedIds +- Frontend-koden er dramatisk enklere +- Konsistent datamodell — alt kommer fra én kilde +- Reaksjoner håndteres via SpacetimeDB-tabeller, ikke PG API + +## 5. Historisk anti-pattern: "PG-lekkasje" i SpacetimeDB-adapteren + +Gjentatt feil (mars 2026, minst 3 iterasjoner): Når en ny feature trenger data som SpacetimeDB-modulen ikke har (metadata, edited_at, revisjoner), er det fristende å legge til en `enrichFromPg()`-funksjon som henter fra PG direkte. Dette bryter hele poenget med caching-laget. + +**Symptomer:** +- SpacetimeDB-adapteren har `fetch('/api/messages/...')` kall +- Worker skriver til PG først, SpacetimeDB er "best-effort" +- Etter AI-vask vises ikke metadata/revisjoner fordi de bare finnes i PG +- Race conditions mellom SpacetimeDB-oppdateringer og PG-fetch + +**Hvorfor det skjer:** Det er raskere å lage en PG API-rute enn å utvide SpacetimeDB-modulen (Rust compile, publish, regenerer bindings). Men det skaper teknisk gjeld som akkumulerer og undergraver hele arkitekturen. + +**Riktig løsning når SpacetimeDB mangler et felt:** +1. Legg til feltet i SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`) +2. Utvid warmup til å laste feltet fra PG +3. Utvid sync til å persistere feltet til PG +4. Worker skriver til SpacetimeDB via reducer +5. Frontend leser kun fra SpacetimeDB + +**Aldri:** Legg til `fetch('/api/.../metadata')` i SpacetimeDB-adapteren. + +## 6. 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-adapter med warmup.** Worker laster data fra PG ved oppstart. +3. **Bruk samme factory-mønster.** Felles interface, env-variabel for valg. +4. **Legg til warmup-config** i `channels.config` (eller tilsvarende config-felt). +5. **Test begge adaptere uavhengig** før du integrerer i UI-komponenten. +6. **Sjekk at alle felter frontend trenger finnes i SpacetimeDB-modulen** før du implementerer adapteren. Utvid modulen først hvis nødvendig. + + +================================================================ +FILE: docs/erfaringer/authentik_oidc.md +================================================================ + +# Erfaring: Authentik OIDC-integrasjon + +## 1. `profile.sub` er IKKE Authentik sin PostgreSQL-UUID + +Authentik sin OIDC `sub`-claim er en **SHA256-hash**, ikke UUID-kolonnen fra `authentik_core_user`. Eksempel: + +| Felt | Verdi | +|---|---| +| Authentik DB `uuid` | `0ac94e00-015b-4e78-9f32-269fa6ce3f44` | +| OIDC `sub` claim | `6af61f43c6647a237cbb381ee7788376a9bc20299c2c06281d9954d763e854f0` | + +Bruk **alltid** `sub`-verdien fra OIDC som nøkkel i `users.authentik_id`. For å finne den riktige verdien for en bruker: logg inn og les `profile.sub` fra callback, eller sjekk JWT-tokenet. + +## 2. `@auth/sveltekit` sin `user.id` er IKKE `profile.sub` + +`@auth/sveltekit` genererer sin egen interne UUID for `user.id` i JWT. Denne overlever ikke mellom sesjoner og matcher ingenting i vår database. + +For å bruke Authentik `sub` som bruker-ID: + +```typescript +callbacks: { + jwt({ token, user, profile }) { + if (user) token.id = user.id; + if (profile?.sub) token.authentik_sub = profile.sub; + return token; + }, + session({ session, token }) { + if (session.user) { + // Bruk Authentik sub, IKKE token.id + session.user.id = (token.authentik_sub ?? token.id) as string; + } + return session; + } +} +``` + +`profile` er kun tilgjengelig i JWT-callbacken ved innlogging (ikke ved token-refresh), derfor må `authentik_sub` lagres i tokenet. + +**Referanse:** `web/src/lib/server/auth.ts` + +## 3. Redirect-URI i Authentik + +`@auth/sveltekit` bruker callback-URL `https:///auth/callback/`. For oss: `https://sidelinja.org/auth/callback/authentik`. + +Denne MÅ være registrert som redirect-URI i Authentik sin OAuth2-provider. Verifiser via: + +```sql +SELECT _redirect_uris FROM authentik_providers_oauth2_oauth2provider +WHERE client_id = ''; +``` + +**Tips:** Legg til en regex-variant for lokal utvikling: `http://localhost:\d+/auth/callback/authentik` med `matching_mode: "regex"`. + + +================================================================ +FILE: docs/erfaringer/spacetimedb_integrasjon.md +================================================================ + +# 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. Timestamps — bruk SDK-metoder, ikke interne felter + +SpacetimeDB `Timestamp`-objektet har en intern property `__timestamp_micros_since_unix_epoch__` (BigInt). **Ikke bruk den direkte** — bruk SDK-metodene: + +```typescript +// FEIL — microsSinceEpoch finnes ikke, __timestamp_micros_since_unix_epoch__ er internt +const micros = row.createdAt?.microsSinceEpoch; + +// RIKTIG — bruk SDK-metoder +const iso = row.createdAt?.toISOString(); // "2026-03-15T23:57:11.677139Z" +const date = row.createdAt?.toDate(); // Date-objekt +const ms = row.createdAt?.toDate()?.getTime(); // millisekunder for sortering +``` + +### Timestamp-parsing i Rust-modul (warmup) + +PG returnerer timestamps som `"2026-03-15 23:57:11.677139+00"`. chrono parser IKKE `+00` — krever `+00:00`: + +```rust +// FEIL — chrono gir ParseError(TooShort) på "+00" +let dt = s.parse::>(); + +// RIKTIG — normaliser PG-offset først +let normalized = if s.ends_with("+00") { format!("{}:00", s) } else { s.to_string() }; +let dt = chrono::DateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S%.f%:z"); +``` + +Uten korrekt parsing faller `load_messages` tilbake til `ctx.timestamp` (nåtidspunkt), og alle meldinger får samme klokkeslett. + +**Referanse:** `spacetimedb/src/lib.rs` — `parse_timestamp()`, `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 local + +# Generer TypeScript-bindings +spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindings \ + --module-path . +``` + +**Merk:** SpacetimeDB-modulen publiseres manuelt med `spacetime publish` mot server-instansen. + +## 6. Arkitekturendring: SpacetimeDB som cache foran PG (mars 2026) + +Tidligere: Hybrid-adapter der frontend merget data fra PG (historikk) og SpacetimeDB (sanntid) med dedup, deletedIds og BigInt-workarounds. + +Ny modell: +- **PG autoritativ** — all persistent data i PostgreSQL +- **SpacetimeDB = varm cache** — worker gjør warmup (PG → ST) ved oppstart +- **Frontend snakker KUN med ST** — ingen PG API-kall fra chat-adapteren +- **Worker håndterer toveissynk** — ST → PG for nye/redigerte/slettede meldinger og reaksjoner + +### Warmup-flyt +1. Worker starter → `warmup::run()` leser kanaler med config fra PG +2. Per kanal: sjekker `channels.config.warmup_mode` (all/messages/days/none) +3. Kaller `clear_channel` reducer (unngår duplikater ved restart) +4. **Trådbasert henting:** Finner kvalifiserende tråder, henter alle meldinger i disse (komplett med svar) + - `messages`-modus: de N nyeste trådene (sortert etter siste aktivitet) + - `days`-modus: alle tråder med minst én melding i tidsvinduet + - Et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter) +5. Kaller `load_messages` reducer med JSON-array +6. Laster også reaksjoner via `load_reactions` reducer + +### Per-kanal konfigurasjon +- Lagres i `channels.config` JSONB: `warmup_mode` + `warmup_value` +- Admin-UI: `/admin/channels` — tabell med inline-redigering +- Default: `"all"` (last alt). Andre: `"messages"` (siste N tråder), `"days"` (siste N dager), `"none"` (inaktiv) + +### Sync-flyt (ST → PG) +- SyncOutbox-events prosesseres hver 1. sekund +- Støtter: `messages/insert`, `messages/delete`, `messages/update`, `messages/ai_update`, `message_reactions/insert`, `message_reactions/delete` +- `ai_update`-action: oppdaterer body + metadata + edited_at i PG, inserter revisjon + +## 7. Subscription-begrensninger + +**SpacetimeDB-subscriptions støtter IKKE JOINs.** En subscription-query som `SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE ...` feiler stille — `onApplied` kalles aldri, og ingen data vises. + +Bruk kun enkle `SELECT * FROM tabell WHERE ...`-queries i `.subscribe([...])`. Filtrer heller klient-side etter at data er lastet. + +Eksempel: +```typescript +// FEIL — feiler stille, ingen data +.subscribe([ + `SELECT * FROM chat_message WHERE channel_id = '${id}'`, + `SELECT mr.* FROM message_reaction mr JOIN chat_message cm ON cm.id = mr.message_id WHERE cm.channel_id = '${id}'` +]); + +// RIKTIG — last alle reaksjoner, filtrer i koden +.subscribe([ + `SELECT * FROM chat_message WHERE channel_id = '${id}'`, + `SELECT * FROM message_reaction` +]); +``` + +### Fallback +PG-polling adapter (`pg.svelte.ts`) brukes kun når SpacetimeDB ikke er konfigurert. Markeres som `readonly: true`. + +## 8. Reducer-parameternavn — unngå underscore-prefix + +> *Kodeeksemplene i denne seksjonen er fra v1 og bruker `workspace_id`-parametere. +> Workspace-modellen er erstattet av noder og edges (se `docs/retninger/bruker_ikke_workspace.md`), +> men lærdommen om underscore-prefix gjelder generelt for alle SpacetimeDB-reducere.* + +SpacetimeDB eksponerer Rust-parameternavn direkte i HTTP JSON API-et. Underscore-prefix (`_workspace_id`) blir til `_workspace_id` i JSON, ikke `workspace_id`: + +```rust +// FEIL — HTTP-kall med {"workspace_id": "..."} feiler med 400 +pub fn set_ai_processing(ctx: &ReducerContext, id: String, _workspace_id: String) { ... } + +// RIKTIG — bruk vanlig navn, suppress warning med let _ = &var; +pub fn set_ai_processing(ctx: &ReducerContext, id: String, workspace_id: String) -> Result<(), String> { + let _ = &workspace_id; + // ... +} +``` + +## 9. Schema-migrering ved nye kolonner + +Å legge til kolonner på eksisterende SpacetimeDB-tabeller krever `--delete-data` ved publish. Dette sletter all data og krever warmup på nytt: + +```bash +# Feiler uten --delete-data: +# "Adding a column metadata to table chat_message requires a default value annotation" +echo "y" | spacetime publish sidelinja-realtime --server local --delete-data +``` + +## 10. AI-worker-flyt via SpacetimeDB + +Worker som gjør AI-behandling av meldinger: +1. Leser meldingens body fra PG (OK — PG er persistent lager) +2. Kaller `set_ai_processing` reducer → frontend ser pulsering umiddelbart +3. Kaller AI Gateway med prompt +4. Kaller `ai_update_message` reducer → SpacetimeDB oppdaterer body/metadata/edited_at atomisk, lagrer revisjon, legger outbox-entry +5. Sync-worker persisterer til PG via `ai_update` action +6. Ved feil: `clear_ai_processing` reducer rydder flagget + + +================================================================ +FILE: docs/erfaringer/svelte5_reaktivitet.md +================================================================ + +# Erfaring: Svelte 5 Reaktivitet + +## 1. `$state` i `.svelte.ts` krever getters + +Svelte 5 sin `$state` lager reaktive proxyer. Når en funksjon returnerer et objekt med `$state`-verdier, **mister man reaktiviteten** hvis man returnerer verdien direkte: + +```typescript +// FEIL — mister reaktivitet, verdien fryses ved retur +function createThing() { + let count = $state(0); + return { count }; // snapshot, ikke reaktiv +} + +// RIKTIG — getter bevarer proxy-tilgang +function createThing() { + let count = $state(0); + return { + get count() { return count; } + }; +} +``` + +**Referanse:** `web/src/lib/chat/pg.svelte.ts` — alle returnerte verdier bruker getters. + +## 2. SSR kjører alt utenfor `onMount` + +SvelteKit server-renderer kjører komponent-script ved SSR. Alt som bruker browser-APIer (WebSocket, `fetch` til relative URLer, `setInterval`, `sessionStorage`) **krasjer på serveren** hvis det ikke er beskyttet. + +```typescript +// FEIL — krasjer ved SSR +let chat = createChat(channelId); // kjøres på server + +// RIKTIG — kun i browser +import { onMount } from 'svelte'; +let chat = $state(null); +onMount(() => { + chat = createChat(channelId); + return () => chat?.destroy(); +}); +``` + +Alternativt kan factory-funksjonen selv sjekke: +```typescript +import { browser } from '$app/environment'; +if (browser) { /* ... */ } +``` + +**Referanse:** `web/src/lib/chat/create.svelte.ts` — `browser`-guard i factory, `web/src/lib/blocks/ChatBlock.svelte` — `onMount` for opprettelse. + +## 3. `$derived` og `$effect` med null-initialisert state + +Når en `$state`-variabel starter som `null` (fordi den settes i `onMount`), må `$derived` og `$effect` håndtere null-tilfellet: + +```typescript +let chat = $state(null); +let messages = $derived(chat?.messages ?? []); // fallback til tom liste + +$effect(() => { + const count = messages.length; // trygt, alltid array + if (count > prevCount) scrollToBottom(); + prevCount = count; +}); +``` + +**Referanse:** `web/src/lib/blocks/ChatBlock.svelte` — `$derived` med optional chaining. + +## 4. Polling: full-fetch slår inkrementell akkumulering + +Vi prøvde først å bruke en `latestTimestamp`-cursor for å hente kun nye meldinger og appende dem. Dette ga duplikater — telleren vokste mens man tastet (polling i bakgrunnen). + +**Løsning:** Enkel `refresh()` som alltid henter full liste og erstatter `messages` i sin helhet. For et lite volum meldinger er dette enklere og tryggere enn inkrementell logikk. + +**Referanse:** `web/src/lib/chat/pg.svelte.ts` — `refresh()` gjør full fetch, ingen akkumulering. + + +================================================================ +FILE: docs/proposals/README.md +================================================================ + # Forslag (Proposals) Halvtenkte idéer, kreative innfall og ting vi vil utforske når vi får tid. Ikke spesifisert, ikke forpliktet — bare parkert. @@ -6258,12 +6254,12 @@ Forslagsfiler er lette — ingen streng mal. Minimum: - Hva bygger den på? (eksisterende features/infra) - **Innsats** (Lav / Middels / Stor) og **Wow-faktor** (Lav / Middels / Høy) - Åpne spørsmål - - -================================================================ -FILE: docs/proposals/artikkel_publisering.md -================================================================ - + + +================================================================ +FILE: docs/proposals/artikkel_publisering.md +================================================================ + # Forslag: Artikkel-publisering og publikasjonsmodell ## Idé @@ -6458,12 +6454,12 @@ Kombinasjonen av vakker typografi, individuelle forfattere, kollaborativt forfat - **Tekst-primitiv** — fundament: editor, `article_view`, lagringsformat - **Personlig workspace** — kontekst: der individet skriver og publiserer fra - Denne proposalen handler om *hva som skjer etter at teksten er skrevet* — publisering, kurasjon, distribusjon, leseopplevelse - - -================================================================ -FILE: docs/proposals/audience_voice_memo.md -================================================================ - + + +================================================================ +FILE: docs/proposals/audience_voice_memo.md +================================================================ + # Forslag: Audience Voice Memo (Live publikums-innspill) **Innsats:** Lav | **Wow-faktor:** Høy @@ -6505,12 +6501,12 @@ Under live-innspilling vises en QR-kode (eller kort-URL) som publikum kan skanne - Skalering: hva om 100+ lyttere sender memos samtidig? Whisper-kø kan bli overbelastet - Kan dette kombineres med Live Audience Q&A-forslaget (stemmegiving på spørsmål)? - Personvern: skal lytterne akseptere at memoet kan brukes i podcasten? - - -================================================================ -FILE: docs/proposals/auto_clipper.md -================================================================ - + + +================================================================ +FILE: docs/proposals/auto_clipper.md +================================================================ + # Forslag: Auto-Clipper (Sosiale medier-klipp) **Innsats:** Middels | **Wow-faktor:** Høy @@ -6537,12 +6533,12 @@ Under studio-innspilling (eller møterom) kjører en bakgrunnsprosess som lytter - 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"? - - -================================================================ -FILE: docs/proposals/auto_highlight_reel.md -================================================================ - + + +================================================================ +FILE: docs/proposals/auto_highlight_reel.md +================================================================ + # Forslag: Auto-Highlight Reel (Post-innspilling) **Innsats:** Middels | **Wow-faktor:** Høy @@ -6582,12 +6578,12 @@ Auto-Clipper kjører *live* under innspilling og fanger øyeblikk i sanntid. Aut - Videostøtte: trenger vi waveform-video med teksting for TikTok/Shorts, eller holder lyd + bilde? - Skal AI-en foreslå rekkefølge/gruppering av klipp til en "highlight reel" (2-3 min sammenklipp)? - Kan den lære av hvilke klipp redaksjonen godkjenner over tid (feedback loop)? - - -================================================================ -FILE: docs/proposals/avisvisning.md -================================================================ - + + +================================================================ +FILE: docs/proposals/avisvisning.md +================================================================ + # Forslag: Avisvisning ## Idé @@ -6818,12 +6814,12 @@ Spørringen er rett frem (prominens-faktorer finnes allerede). Layout er CSS gri - **Tekst-primitiv / Meldingsboks** — all data som vises er meldinger med view-configs - **Kunnskapsgraf** — entitets-filtrering er en graf-spørring - **Artikkel-publisering** — publiserte artikler får prominente kort i avisen - - -================================================================ -FILE: docs/proposals/card_chaining.md -================================================================ - + + +================================================================ +FILE: docs/proposals/card_chaining.md +================================================================ + # Card Chaining — Automatisk kobling av relaterte kort ## Idé @@ -6850,12 +6846,12 @@ Lav — graph_edges finnes, bare UI for proximity + auto-edge. ## Wow-faktor Middels — subtilt, men forbedrer metadata-kvaliteten dramatisk over tid. - - -================================================================ -FILE: docs/proposals/card_heat_map.md -================================================================ - + + +================================================================ +FILE: docs/proposals/card_heat_map.md +================================================================ + # Card Heat Map — Visuell indikator for engasjement ## Idé @@ -6881,12 +6877,12 @@ Lav — ren frontend-logikk med enkel server-aggregering. ## Wow-faktor Middels — subtilt men nyttig for redaksjonell prioritering. - - -================================================================ -FILE: docs/proposals/collaborative_cursors.md -================================================================ - + + +================================================================ +FILE: docs/proposals/collaborative_cursors.md +================================================================ + # Collaborative Cursors — Sanntids-pekere for flerbrukermiljø ## Idé @@ -6916,12 +6912,12 @@ Middels — visuelt tiltalende, men ikke kritisk funksjonalitet. ## Åpne spørsmål - Bør pekere vises i chat-visning også, eller bare canvas-baserte views? - Throttling-strategi: SpacetimeDB-reducer eller klient-side debounce? - - -================================================================ -FILE: docs/proposals/contradiction_detector.md -================================================================ - + + +================================================================ +FILE: docs/proposals/contradiction_detector.md +================================================================ + # Forslag: Contradiction Detector (Live i Studioet) **Innsats:** Middels | **Wow-faktor:** Høy @@ -6963,12 +6959,12 @@ Programlederen kan: - Skal den kun matche mot egne episoder, eller også mot eksterne faktoider? - Kan dette kombineres med Ghost Host for å "lese opp" motstridelsen? - Latens-krav: må fungere innen 10-15 sek etter utsagnet for å være nyttig live - - -================================================================ -FILE: docs/proposals/debate_club.md -================================================================ - + + +================================================================ +FILE: docs/proposals/debate_club.md +================================================================ + # Forslag: Debate Club (Simulerte debatter) ## Idé @@ -6993,12 +6989,12 @@ Kan brukes i studioet som research-verktøy: *"Hva ville Støre og Solberg sagt - 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? - - -================================================================ -FILE: docs/proposals/editor.md -================================================================ - + + +================================================================ +FILE: docs/proposals/editor.md +================================================================ + # Forslag: Universell editor ## Idé @@ -7384,12 +7380,12 @@ En editor der du bare skriver — markdown rendres automatisk, LaTeX rendres aut - **Komponerbare sider** — maximize gir editoren plass til å gå fra chatboks til fullskjerm skriveverksted - **Chat** (feature) — kompakt modus erstatter dagens chat-input - **Notater** (feature) — utvidet modus erstatter dagens textarea - - -================================================================ -FILE: docs/proposals/emotion_tags.md -================================================================ - + + +================================================================ +FILE: docs/proposals/emotion_tags.md +================================================================ + # Emotion Tags — Hurtigkategorisering av segmenter ## Idé @@ -7416,12 +7412,12 @@ Lav — ren frontend + metadata-felt. ## Wow-faktor Middels — nyttig for store episoder med mange segmenter. - - -================================================================ -FILE: docs/proposals/flow_meter.md -================================================================ - + + +================================================================ +FILE: docs/proposals/flow_meter.md +================================================================ + # Flow Meter — Visuell episodeprogresjon ## Idé @@ -7446,12 +7442,12 @@ Lav — én beregning + CSS gradient. ## Wow-faktor Middels — liten ting, men fjerner mental overhead. - - -================================================================ -FILE: docs/proposals/ghost_cards.md -================================================================ - + + +================================================================ +FILE: docs/proposals/ghost_cards.md +================================================================ + # Ghost Cards — Visuelle spor fra tidligere episoder ## Idé @@ -7479,12 +7475,12 @@ Lav–Middels — mest metadata-design og UI for ghost-rendering. ## Wow-faktor Høy — gir podcasten "hukommelse" som oppleves magisk for brukeren. - - -================================================================ -FILE: docs/proposals/ghost_host_tts.md -================================================================ - + + +================================================================ +FILE: docs/proposals/ghost_host_tts.md +================================================================ + # Forslag: Ghost Host (AI Text-to-Speech i Studio) ## Idé @@ -7514,12 +7510,12 @@ Under innspilling kan programlederne trykke "Ghost Host"-knappen. AI-en generere - 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 - - -================================================================ -FILE: docs/proposals/graph_health_monitor.md -================================================================ - + + +================================================================ +FILE: docs/proposals/graph_health_monitor.md +================================================================ + # Forslag: Knowledge Graph Health Monitor **Innsats:** Lav | **Wow-faktor:** Middels @@ -7559,12 +7555,12 @@ En admin-side i SvelteKit som viser "Graf-helse": tetthet, isolerte noder, svake - 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)? - - -================================================================ -FILE: docs/proposals/guest_prep_simulator.md -================================================================ - + + +================================================================ +FILE: docs/proposals/guest_prep_simulator.md +================================================================ + # Forslag: Guest Prep Simulator **Innsats:** Middels | **Wow-faktor:** Høy @@ -7594,12 +7590,12 @@ Før man sender async-gjest-lenke eller inviterer noen til studio, kan redaksjon - 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? - - -================================================================ -FILE: docs/proposals/kildevern_modus.md -================================================================ - + + +================================================================ +FILE: docs/proposals/kildevern_modus.md +================================================================ + # Forslag: Kildevern-modus (100% lokal LLM) ## Idé @@ -7634,154 +7630,154 @@ Når Møterommet eller en channel brukes til sensitive, upubliserte redaksjonell ## Innsats: Lav–Middels ## Wow-faktor: Høy - - -================================================================ -FILE: docs/proposals/komponerbare_sider.md -================================================================ - -# 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 - -## Blokker som resizable containere - -Hver blokk på en side er et selvstendig, resizable panel. Brukeren kan: - -### Resize -Dra i kanten mellom blokker for å justere proporsjoner. Midlertidig (session) eller persistent (lagres i brukerens `dashboard_config`). - -``` -Standard layout: Etter resize: -┌──────┬──────┐ ┌───┬─────────┐ -│ Chat │Kanban│ │ │ │ -│ │ │ → │ │ Kanban │ -│ │ │ │ │ │ -└──────┴──────┘ └───┴─────────┘ -``` - -### Maximize -Dobbelklikk på tittellinjen, eller en maximize-knapp — blokken utvider seg til hele tilgjengelig skjermflate. Alle andre blokker kollapses. Trykk Escape eller minimize for å gå tilbake. - -``` -Maksimert chat: -┌──────────────────────────┐ -│ Chat — #Mediepolitikk ✕ │ -│ │ -│ [full editor, │ -│ full tråd-oversikt, │ -│ full funksjonalitet] │ -│ │ -│ │ -└──────────────────────────┘ -``` - -Dette er spesielt verdifullt for: -- **Editoren** — en chatmelding som vokser til en artikkel trenger plutselig hele skjermen. Maximize gir deg et reelt skriveverksted i stedet for en bitteliten boks. -- **Whiteboard** — trenger plass for å være nyttig -- **Graf-visualisering** — uleselig i en liten boks -- **Mobil** — der skjermplassen er begrenset er maximize den naturlige interaksjonen. Blokkene stacker vertikalt, og du tapper en blokk for å utvide den til fullskjerm. - -### Mobil-opplevelse -På mobil er default-visningen en stacked liste med kollapsede blokker: - -``` -┌──────────────────┐ -│ ▼ Chat (3 nye) │ -│ ▼ Kanban │ -│ ▼ Statistikk │ -└──────────────────┘ -``` - -Tapp en blokk → den ekspanderer til fullskjerm. Swipe ned eller tilbake-knapp → tilbake til oversikten. Editoren i fullskjerm på mobil = et reelt skriveverksted, ikke en liten inputboks nederst. - -### Teknisk implementering -- CSS Grid med `grid-template-columns`/`grid-template-rows` som justeres via drag -- Resize via pointer events på grid-gapene (eller et lett bibliotek som `svelte-splitpanes`) -- Maximize = CSS `position: fixed` + z-index overlay, med smooth transition -- Blokk-størrelse lagres i `dashboard_config` JSONB: `{ "blockId": { "span": 1.5 } }` eller lignende -- Midlertidig resize (ikke lagret) = Svelte `$state`, forsvinner ved refresh - -## 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, `container queries` i CSS) -- Eksponerer en `blockMeta`-descriptor (tittel, min-bredde, min-høyde, ikon) for katalogen -- Har en `maximized`-prop som tilpasser layout (f.eks. editoren viser full toolbar i maximized) - -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.) -- Editor (proposal) — maximize gir editoren plass til å være et reelt skriveverksted - -## Innsats -- Fase 1: **Lav** (grid-layout + JSON-config, ingen drag-and-drop) -- Fase 1.5: **Lav** (maximize per blokk — relativt enkelt med CSS overlay) -- Fase 2: **Middels** (resize, svelte-splitpanes, bruker-lagring) -- Fase 3: **Stor** (custom tiling engine — trolig unødvendig) - -## Wow-faktor -Middels–Høy. Gir en "dette er MITT verktøy"-følelse. Maximize alene er en stor forbedring — spesielt på mobil der det transformerer opplevelsen fra "begrenset app" til "fullverdig arbeidsflate". - -## Å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. -- Midlertidig vs persistent resize: bør default være at resize forsvinner ved refresh (session), eller lagres automatisk? - → Trolig: auto-lagre med debounce. Brukeren forventer at ting "huskes". En "tilbakestill layout"-knapp for å gå tilbake til admin-default. -- Bør det finnes et "standard-oppsett" per workspace-type (podcast, nyhetsredaksjon)? - → Ja, som templates admin kan velge som utgangspunkt. -- Keyboard shortcut for maximize? F11 er konvensjon for fullskjerm, men kolliderer med nettleser. Kanskje Ctrl+Shift+F eller dobbelklikk. - -## Relasjon til andre proposals -- **Editor** — maximize gir editoren rom til å gå fra chatinput til fullverdig skriveverksted -- **Personlig workspace** — personlige dashboards (fase 2) henger tett sammen med personlig workspace -- **Tekst-primitiv** — en melding som vokser trenger plass; maximize er mekanismen som gir den det - - -================================================================ -FILE: docs/proposals/live_audience_qa.md -================================================================ - + + +================================================================ +FILE: docs/proposals/komponerbare_sider.md +================================================================ + +# 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 + +## Blokker som resizable containere + +Hver blokk på en side er et selvstendig, resizable panel. Brukeren kan: + +### Resize +Dra i kanten mellom blokker for å justere proporsjoner. Midlertidig (session) eller persistent (lagres i brukerens `dashboard_config`). + +``` +Standard layout: Etter resize: +┌──────┬──────┐ ┌───┬─────────┐ +│ Chat │Kanban│ │ │ │ +│ │ │ → │ │ Kanban │ +│ │ │ │ │ │ +└──────┴──────┘ └───┴─────────┘ +``` + +### Maximize +Dobbelklikk på tittellinjen, eller en maximize-knapp — blokken utvider seg til hele tilgjengelig skjermflate. Alle andre blokker kollapses. Trykk Escape eller minimize for å gå tilbake. + +``` +Maksimert chat: +┌──────────────────────────┐ +│ Chat — #Mediepolitikk ✕ │ +│ │ +│ [full editor, │ +│ full tråd-oversikt, │ +│ full funksjonalitet] │ +│ │ +│ │ +└──────────────────────────┘ +``` + +Dette er spesielt verdifullt for: +- **Editoren** — en chatmelding som vokser til en artikkel trenger plutselig hele skjermen. Maximize gir deg et reelt skriveverksted i stedet for en bitteliten boks. +- **Whiteboard** — trenger plass for å være nyttig +- **Graf-visualisering** — uleselig i en liten boks +- **Mobil** — der skjermplassen er begrenset er maximize den naturlige interaksjonen. Blokkene stacker vertikalt, og du tapper en blokk for å utvide den til fullskjerm. + +### Mobil-opplevelse +På mobil er default-visningen en stacked liste med kollapsede blokker: + +``` +┌──────────────────┐ +│ ▼ Chat (3 nye) │ +│ ▼ Kanban │ +│ ▼ Statistikk │ +└──────────────────┘ +``` + +Tapp en blokk → den ekspanderer til fullskjerm. Swipe ned eller tilbake-knapp → tilbake til oversikten. Editoren i fullskjerm på mobil = et reelt skriveverksted, ikke en liten inputboks nederst. + +### Teknisk implementering +- CSS Grid med `grid-template-columns`/`grid-template-rows` som justeres via drag +- Resize via pointer events på grid-gapene (eller et lett bibliotek som `svelte-splitpanes`) +- Maximize = CSS `position: fixed` + z-index overlay, med smooth transition +- Blokk-størrelse lagres i `dashboard_config` JSONB: `{ "blockId": { "span": 1.5 } }` eller lignende +- Midlertidig resize (ikke lagret) = Svelte `$state`, forsvinner ved refresh + +## 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, `container queries` i CSS) +- Eksponerer en `blockMeta`-descriptor (tittel, min-bredde, min-høyde, ikon) for katalogen +- Har en `maximized`-prop som tilpasser layout (f.eks. editoren viser full toolbar i maximized) + +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.) +- Editor (proposal) — maximize gir editoren plass til å være et reelt skriveverksted + +## Innsats +- Fase 1: **Lav** (grid-layout + JSON-config, ingen drag-and-drop) +- Fase 1.5: **Lav** (maximize per blokk — relativt enkelt med CSS overlay) +- Fase 2: **Middels** (resize, svelte-splitpanes, bruker-lagring) +- Fase 3: **Stor** (custom tiling engine — trolig unødvendig) + +## Wow-faktor +Middels–Høy. Gir en "dette er MITT verktøy"-følelse. Maximize alene er en stor forbedring — spesielt på mobil der det transformerer opplevelsen fra "begrenset app" til "fullverdig arbeidsflate". + +## Å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. +- Midlertidig vs persistent resize: bør default være at resize forsvinner ved refresh (session), eller lagres automatisk? + → Trolig: auto-lagre med debounce. Brukeren forventer at ting "huskes". En "tilbakestill layout"-knapp for å gå tilbake til admin-default. +- Bør det finnes et "standard-oppsett" per workspace-type (podcast, nyhetsredaksjon)? + → Ja, som templates admin kan velge som utgangspunkt. +- Keyboard shortcut for maximize? F11 er konvensjon for fullskjerm, men kolliderer med nettleser. Kanskje Ctrl+Shift+F eller dobbelklikk. + +## Relasjon til andre proposals +- **Editor** — maximize gir editoren rom til å gå fra chatinput til fullverdig skriveverksted +- **Personlig workspace** — personlige dashboards (fase 2) henger tett sammen med personlig workspace +- **Tekst-primitiv** — en melding som vokser trenger plass; maximize er mekanismen som gir den det + + +================================================================ +FILE: docs/proposals/live_audience_qa.md +================================================================ + # Forslag: Live Audience Q&A (Studio-integrasjon) **Innsats:** Middels | **Wow-faktor:** Høy @@ -7834,12 +7830,12 @@ audience_questions ( - 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? - - -================================================================ -FILE: docs/proposals/meme_generator.md -================================================================ - + + +================================================================ +FILE: docs/proposals/meme_generator.md +================================================================ + # Forslag: Møteroms-Meme Generator ## Idé @@ -7862,12 +7858,12 @@ Når whiteboard lukkes i møterommet, kjører en `meme_gen`-jobb som tar siste 3 - 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å? - - -================================================================ -FILE: docs/proposals/personlig_workspace.md -================================================================ - + + +================================================================ +FILE: docs/proposals/personlig_workspace.md +================================================================ + # Forslag: Personlig workspace > **Superseded:** Dette forslaget er erstattet av retningen @@ -7992,12 +7988,12 @@ Alene er det "et privat workspace". Med publisering blir det en personlig plattf - **Tekst-primitiv** — gir notater og kladder en skikkelig editor - **Artikkel-publisering** — gir publiseringsmodellen (publikasjoner, kuratorer, feeds) - Personlig workspace er *konteksten* der tekst-primitiven og publisering møtes for individet - - -================================================================ -FILE: docs/proposals/pinboard_mode.md -================================================================ - + + +================================================================ +FILE: docs/proposals/pinboard_mode.md +================================================================ + # Pinboard Mode — Fugleperspektiv over episode-arc ## Idé @@ -8024,12 +8020,12 @@ Lav — ren CSS/UI-jobb. ## Wow-faktor Høy — visuelt imponerende og genuint nyttig under innspilling. - - -================================================================ -FILE: docs/proposals/podcast_time_machine.md -================================================================ - + + +================================================================ +FILE: docs/proposals/podcast_time_machine.md +================================================================ + # Forslag: Podcast Time Machine ## Idé @@ -8053,12 +8049,12 @@ Morsom variant: "Time Machine Roast" — viser bare de mest selvmotsigende eller - 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? - - -================================================================ -FILE: docs/proposals/podcasting_2_0.md -================================================================ - + + +================================================================ +FILE: docs/proposals/podcasting_2_0.md +================================================================ + # Forslag: Podcasting 2.0 — strukturert RSS ## Idé @@ -8096,12 +8092,12 @@ Sidelinja har allerede strukturert data for transkripsjoner (segmenter), kapitte ## Innsats: Lav ## Wow-faktor: Høy - - -================================================================ -FILE: docs/proposals/serendipity_roulette.md -================================================================ - + + +================================================================ +FILE: docs/proposals/serendipity_roulette.md +================================================================ + # Forslag: Serendipity Roulette ## Idé @@ -8124,12 +8120,12 @@ Med jevne mellomrom (konfigurerbart, f.eks. hvert 8. minutt) under innspilling i - 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? - - -================================================================ -FILE: docs/proposals/social_posting.md -================================================================ - + + +================================================================ +FILE: docs/proposals/social_posting.md +================================================================ + # Forslag: Sosial publisering fra chat ## Idé @@ -8280,12 +8276,12 @@ Rust worker: matcher platform → riktig API-klient - 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? - - -================================================================ -FILE: docs/proposals/storyboard.md -================================================================ - + + +================================================================ +FILE: docs/proposals/storyboard.md +================================================================ + # Storyboard — Fritt canvas for innspillingsplanlegging og live-produksjon ## 1. Konsept @@ -8584,12 +8580,12 @@ Høy — dette er "limen" mellom redaksjonelt arbeid og faktisk innspilling. Vis - Bruk Canvas-primitivet for all canvas-logikk — ikke reimplementer pan/zoom - Mobil-fallback (listevisning) er en egen komponent, ikke en "responsiv" versjon av canvaset - Status-endring skal alltid gå via SpacetimeDB reducer, aldri direkte state-mutasjon - - -================================================================ -FILE: docs/proposals/tekst_primitiv.md -================================================================ - + + +================================================================ +FILE: docs/proposals/tekst_primitiv.md +================================================================ + # Forslag: Tekst-primitiv ## Idé @@ -8712,12 +8708,12 @@ Filosofien — at enhver tekst kan bli hva som helst — er enableren for person - **Personlig workspace** — konteksten der brukeren kladder og publiserer - **Artikkel-publisering** — publiseringsmodellen (publikasjoner, kuratorer, feeds) bygger på tekst-primitiven - **Meldingsboks** (feature) — tekst-primitiven er den naturlige utviklingen: "én primitiv som vokser med brukerens intensjon" - - -================================================================ -FILE: docs/proposals/valgomat_roast.md -================================================================ - + + +================================================================ +FILE: docs/proposals/valgomat_roast.md +================================================================ + # Forslag: Valgomat Roast Mode ## Idé @@ -8739,12 +8735,12 @@ Når en bruker matcher dårlig mot en politiker i valgomaten, får de en ekstra - 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? - - -================================================================ -FILE: docs/proposals/waveforms.md -================================================================ - + + +================================================================ +FILE: docs/proposals/waveforms.md +================================================================ + # Forslag: Visuelle Waveforms som UI-primitiv ## Idé @@ -8798,12 +8794,12 @@ Lagring: media_files.metadata (JSONB) eller separat fil ## Innsats: Lav–Middels ## Wow-faktor: Høy - - -================================================================ -FILE: docs/proposals/web_clipper.md -================================================================ - + + +================================================================ +FILE: docs/proposals/web_clipper.md +================================================================ + # Forslag: Web Clipper / "Send til Sidelinja" ## Idé @@ -8847,5 +8843,5 @@ URL mottatt via /api/clip ## Innsats: Lav–Middels ## Wow-faktor: Høy - - + + diff --git a/scripts/test-sanntid.sh b/scripts/test-sanntid.sh deleted file mode 100755 index 16ace66..0000000 --- a/scripts/test-sanntid.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -# Test sanntidsflyt: STDB node/edge CRUD via HTTP API -# -# Verifiserer at maskinrommet kan skrive til STDB, og at data -# er tilgjengelig for subscribers (grunnlaget for at to faner -# kan se hverandres endringer i sanntid). -# -# Kjøres fra lokal maskin. SSH-er til serveren for å nå Docker-nettverket. -# -# Bruk: ./scripts/test-sanntid.sh - -set -euo pipefail - -SERVER="claude@157.180.81.26" -NETWORK="sidelinja_sidelinja-net" -STDB_URL="http://spacetimedb:3000/v1/database/synops" -VEGARD_NODE="a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" - -# Les STDB-token fra .env -STDB_TOKEN=$(ssh "$SERVER" "grep SPACETIMEDB_TOKEN /srv/synops/.env | cut -d= -f2-") - -if [ -z "$STDB_TOKEN" ]; then - echo "FEIL: Fant ikke SPACETIMEDB_TOKEN i .env" - exit 1 -fi - -TEST_NODE_ID="test-sanntid-$(date +%s)" -TEST_EDGE_ID="test-edge-$(date +%s)" - -# Hjelpefunksjon: kall STDB reducer via curl i Docker-nettverket -stdb_call() { - local reducer=$1 - local data=$2 - ssh "$SERVER" "docker run --rm --network $NETWORK curlimages/curl:latest \ - -s -w '%{http_code}' -X POST $STDB_URL/call/$reducer \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer $STDB_TOKEN' \ - -d '$data'" 2>/dev/null -} - -# Hjelpefunksjon: spør STDB via SQL -stdb_sql() { - ssh "$SERVER" "docker exec sidelinja-spacetimedb-1 spacetime sql synops \"$1\"" 2>/dev/null | grep -v WARNING -} - -echo "=== Sanntidstest: STDB node/edge CRUD ===" -echo "" - -# 1. Opprett testnode -echo -n "1. Oppretter testnode ($TEST_NODE_ID)... " -RESULT=$(stdb_call "create_node" "{\"id\": \"$TEST_NODE_ID\", \"node_kind\": \"content\", \"title\": \"Sanntidstest\", \"content\": \"Automatisk test av sanntidsflyt\", \"visibility\": \"hidden\", \"metadata\": \"{}\", \"created_by\": \"$VEGARD_NODE\"}") -if [ "$RESULT" = "200" ]; then - echo "OK" -else - echo "FEIL (HTTP $RESULT)" - exit 1 -fi - -# 2. Verifiser node finnes i STDB -echo -n "2. Verifiserer node i STDB... " -NODE_RESULT=$(stdb_sql "SELECT id FROM node WHERE id = '$TEST_NODE_ID'") -if echo "$NODE_RESULT" | grep -q "$TEST_NODE_ID"; then - echo "OK" -else - echo "FEIL: Node ikke funnet" - exit 1 -fi - -# 3. Opprett owner-edge (Vegard → testnode) -echo -n "3. Oppretter owner-edge... " -RESULT=$(stdb_call "create_edge" "{\"id\": \"$TEST_EDGE_ID\", \"source_id\": \"$VEGARD_NODE\", \"target_id\": \"$TEST_NODE_ID\", \"edge_type\": \"owner\", \"metadata\": \"{}\", \"system\": false, \"created_by\": \"$VEGARD_NODE\"}") -if [ "$RESULT" = "200" ]; then - echo "OK" -else - echo "FEIL (HTTP $RESULT)" - exit 1 -fi - -# 4. Verifiser edge finnes -echo -n "4. Verifiserer edge i STDB... " -EDGE_RESULT=$(stdb_sql "SELECT id FROM edge WHERE id = '$TEST_EDGE_ID'") -if echo "$EDGE_RESULT" | grep -q "$TEST_EDGE_ID"; then - echo "OK" -else - echo "FEIL: Edge ikke funnet" - exit 1 -fi - -# 5. Oppdater noden -echo -n "5. Oppdaterer nodetittel... " -RESULT=$(stdb_call "update_node" "{\"id\": \"$TEST_NODE_ID\", \"node_kind\": \"content\", \"title\": \"Sanntidstest (oppdatert)\", \"content\": \"Automatisk test av sanntidsflyt\", \"visibility\": \"hidden\", \"metadata\": \"{}\"}") -if [ "$RESULT" = "200" ]; then - echo "OK" -else - echo "FEIL (HTTP $RESULT)" - exit 1 -fi - -# 6. Verifiser oppdateringen -echo -n "6. Verifiserer oppdatert tittel... " -TITLE_RESULT=$(stdb_sql "SELECT title FROM node WHERE id = '$TEST_NODE_ID'") -if echo "$TITLE_RESULT" | grep -q "oppdatert"; then - echo "OK" -else - echo "FEIL: Tittel ikke oppdatert" - exit 1 -fi - -# 7. Slett testnode (kaskade-sletter edges) -echo -n "7. Sletter testnode (kaskaderer edges)... " -RESULT=$(stdb_call "delete_node" "{\"id\": \"$TEST_NODE_ID\"}") -if [ "$RESULT" = "200" ]; then - echo "OK" -else - echo "FEIL (HTTP $RESULT)" - exit 1 -fi - -# 8. Verifiser at node og edge er borte -echo -n "8. Verifiserer opprydding... " -NODE_GONE=$(stdb_sql "SELECT id FROM node WHERE id = '$TEST_NODE_ID'" | grep -c "$TEST_NODE_ID" || true) -EDGE_GONE=$(stdb_sql "SELECT id FROM edge WHERE id = '$TEST_EDGE_ID'" | grep -c "$TEST_EDGE_ID" || true) -if [ "$NODE_GONE" = "0" ] && [ "$EDGE_GONE" = "0" ]; then - echo "OK" -else - echo "FEIL: Data ikke ryddet opp" - exit 1 -fi - -echo "" -echo "=== Alle 8 tester bestått ===" -echo "" -echo "Backend-sanntidsflyt fungerer: STDB mottar og serverer" -echo "node/edge-operasjoner korrekt. WebSocket-subscribers" -echo "(frontend-faner) vil motta disse endringene i sanntid." -echo "" -echo "For å teste i browser:" -echo " 1. Start frontend: cd frontend && npm run dev" -echo " 2. Åpne to faner: http://localhost:5173" -echo " 3. Logg inn i begge" -echo " 4. Skriv en node i fane 1, se den dukke opp i fane 2" diff --git a/spacetime.json b/spacetime.json deleted file mode 100644 index e2868bc..0000000 --- a/spacetime.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "server": "synops-server", - "database": "synops", - "module-path": "./spacetimedb" -} \ No newline at end of file diff --git a/spacetimedb/Cargo.lock b/spacetimedb/Cargo.lock deleted file mode 100644 index 2c94201..0000000 --- a/spacetimedb/Cargo.lock +++ /dev/null @@ -1,980 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "approx" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "blake3" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "num-traits", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "decorum" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281759d3c8a14f5c3f0c49363be56810fcd7f910422f97f2db850c2920fde5cf" -dependencies = [ - "approx", - "num-traits", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "ethnum" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" -dependencies = [ - "serde", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lean_string" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4deb1a7e906e1587ab20848215f09e1bd6dee902aca914abf439d511320c4339" -dependencies = [ - "castaway", - "itoa", - "ryu", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "second-stack" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4904c83c6e51f1b9b08bfa5a86f35a51798e8307186e6f5513852210a219c0bb" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spacetimedb" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e62efa76b2da92965201f18d4e4389f02e10434a0d057804d9b6637287cf95" -dependencies = [ - "anyhow", - "bytemuck", - "bytes", - "derive_more", - "getrandom 0.2.17", - "http", - "log", - "rand 0.8.5", - "scoped-tls", - "serde_json", - "spacetimedb-bindings-macro", - "spacetimedb-bindings-sys", - "spacetimedb-lib", - "spacetimedb-primitives", - "spacetimedb-query-builder", -] - -[[package]] -name = "spacetimedb-bindings-macro" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af5ab57e0775e6eccbba21ef31f0e63fe4b5252e640d9101e248058c301ac8" -dependencies = [ - "heck 0.4.1", - "humantime", - "proc-macro2", - "quote", - "spacetimedb-primitives", - "syn", -] - -[[package]] -name = "spacetimedb-bindings-sys" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb35450f3163c04dfa1f7cde3a845e8a2340e3d541e2caed32b2b49a6656a6f" -dependencies = [ - "spacetimedb-primitives", -] - -[[package]] -name = "spacetimedb-lib" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c80ff1ea937acda2ba77b0c8ab6f7e7711b184f80ed30af2ae8e7ed77ebf8e1" -dependencies = [ - "anyhow", - "bitflags", - "blake3", - "chrono", - "derive_more", - "enum-as-inner", - "hex", - "itertools", - "log", - "spacetimedb-bindings-macro", - "spacetimedb-primitives", - "spacetimedb-sats", - "thiserror", -] - -[[package]] -name = "spacetimedb-primitives" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "550c36f217add7215875f09a19c4e2b20b003acd2037b1ae319ba96ccc6181c7" -dependencies = [ - "bitflags", - "either", - "enum-as-inner", - "itertools", - "nohash-hasher", -] - -[[package]] -name = "spacetimedb-query-builder" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9a9181623114986bedcebf53f53f7d84cde42b1812210e34e0019ff460b26fc" -dependencies = [ - "spacetimedb-lib", -] - -[[package]] -name = "spacetimedb-sats" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe7823814cdc9597b878fdc0571b02a905aec864ba6bfa930bb213a746e31ce" -dependencies = [ - "anyhow", - "arrayvec", - "bitflags", - "bytemuck", - "bytes", - "chrono", - "decorum", - "derive_more", - "enum-as-inner", - "ethnum", - "hex", - "itertools", - "lean_string", - "rand 0.9.2", - "second-stack", - "sha3", - "smallvec", - "spacetimedb-bindings-macro", - "spacetimedb-primitives", - "thiserror", - "uuid", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synops_module" -version = "0.1.0" -dependencies = [ - "log", - "spacetimedb", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "getrandom 0.4.2", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "zerocopy" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/spacetimedb/Cargo.toml b/spacetimedb/Cargo.toml deleted file mode 100644 index 4f7382e..0000000 --- a/spacetimedb/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "synops_module" -version = "0.1.0" -edition = "2024" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -spacetimedb = { version = "2.0.5" } -log = "0.4" diff --git a/spacetimedb/src/lib.rs b/spacetimedb/src/lib.rs deleted file mode 100644 index c3ebeb6..0000000 --- a/spacetimedb/src/lib.rs +++ /dev/null @@ -1,820 +0,0 @@ -// Synops SpacetimeDB-modul -// -// Speiler PG-skjema (nodes + edges) som sanntids-cache. -// PG er autoritativ; SpacetimeDB er varm cache for frontend. -// Maskinrommet gjør warmup (PG → ST) ved oppstart og toveissynk deretter. -// -// Ref: docs/retninger/datalaget.md, docs/primitiver/nodes.md, docs/primitiver/edges.md - -use spacetimedb::{reducer, Table, ReducerContext, Timestamp}; - -// ============================================================================= -// Tabeller -// ============================================================================= - -/// Speiler PG nodes-tabellen. UUID-er representeres som String. -/// visibility og node_kind er strenger (PG-enums har ikke STDB-ekvivalent). -/// metadata er JSON-streng (PG JSONB). -#[spacetimedb::table(accessor = node, public)] -pub struct Node { - #[primary_key] - pub id: String, - - pub node_kind: String, - pub title: String, - pub content: String, - pub visibility: String, - pub metadata: String, - pub created_at: Timestamp, - pub created_by: String, -} - -/// Speiler PG node_access-tabellen (materialisert tilgangsmatrise). -/// subject_id = bruker/team, object_id = noden det gis tilgang til. -/// Brukes av frontend for visibility-filtrering. -#[spacetimedb::table(accessor = node_access, public)] -pub struct NodeAccess { - #[primary_key] - pub id: String, // "{subject_id}:{object_id}" — kompositt-nøkkel som streng - - #[index(btree)] - pub subject_id: String, - #[index(btree)] - pub object_id: String, - pub access: String, // reader, member, admin, owner - pub via_edge: String, -} - -/// Speiler PG edges-tabellen. -#[spacetimedb::table(accessor = edge, public)] -pub struct Edge { - #[primary_key] - pub id: String, - - #[index(btree)] - pub source_id: String, - #[index(btree)] - pub target_id: String, - pub edge_type: String, - pub metadata: String, - pub system: bool, - pub created_at: Timestamp, - pub created_by: String, -} - -// ============================================================================= -// Livssyklus -// ============================================================================= - -#[reducer(init)] -pub fn init(_ctx: &ReducerContext) { - log::info!("Synops-modul initialisert"); -} - -#[reducer(client_connected)] -pub fn client_connected(ctx: &ReducerContext) { - log::info!("Klient tilkoblet: {}", ctx.sender().to_hex()); -} - -#[reducer(client_disconnected)] -pub fn client_disconnected(ctx: &ReducerContext) { - log::info!("Klient frakoblet: {}", ctx.sender().to_hex()); -} - -// ============================================================================= -// Node CRUD -// ============================================================================= - -#[reducer] -pub fn create_node( - ctx: &ReducerContext, - id: String, - node_kind: String, - title: String, - content: String, - visibility: String, - metadata: String, - created_by: String, -) -> Result<(), String> { - if id.is_empty() { - return Err("id kan ikke være tom".into()); - } - if ctx.db.node().id().find(&id).is_some() { - return Err(format!("Node med id {} finnes allerede", id)); - } - - ctx.db.node().insert(Node { - id, - node_kind, - title, - content, - visibility, - metadata, - created_at: ctx.timestamp, - created_by, - }); - Ok(()) -} - -#[reducer] -pub fn update_node( - ctx: &ReducerContext, - id: String, - node_kind: String, - title: String, - content: String, - visibility: String, - metadata: String, -) -> Result<(), String> { - let existing = ctx.db.node().id().find(&id) - .ok_or_else(|| format!("Node {} ikke funnet", id))?; - - ctx.db.node().id().update(Node { - node_kind, - title, - content, - visibility, - metadata, - ..existing - }); - Ok(()) -} - -#[reducer] -pub fn delete_node(ctx: &ReducerContext, id: String) -> Result<(), String> { - // Slett tilhørende edges først (speiler PG ON DELETE CASCADE) - let source_edges: Vec<_> = ctx.db.edge().source_id().filter(&id).collect(); - for e in source_edges { - ctx.db.edge().id().delete(&e.id); - } - let target_edges: Vec<_> = ctx.db.edge().target_id().filter(&id).collect(); - for e in target_edges { - ctx.db.edge().id().delete(&e.id); - } - - ctx.db.node().id().delete(&id); - Ok(()) -} - -// ============================================================================= -// NodeAccess CRUD -// ============================================================================= - -/// Upsert: opprett eller oppdater tilgang. id = "{subject_id}:{object_id}". -#[reducer] -pub fn upsert_node_access( - ctx: &ReducerContext, - subject_id: String, - object_id: String, - access: String, - via_edge: String, -) -> Result<(), String> { - let id = format!("{subject_id}:{object_id}"); - - if let Some(existing) = ctx.db.node_access().id().find(&id) { - ctx.db.node_access().id().update(NodeAccess { - access, - via_edge, - ..existing - }); - } else { - ctx.db.node_access().insert(NodeAccess { - id, - subject_id, - object_id, - access, - via_edge, - }); - } - Ok(()) -} - -/// Slett en spesifikk tilgangsrad. -#[reducer] -pub fn delete_node_access( - ctx: &ReducerContext, - subject_id: String, - object_id: String, -) -> Result<(), String> { - let id = format!("{subject_id}:{object_id}"); - ctx.db.node_access().id().delete(&id); - Ok(()) -} - -/// Slett all tilgang for et gitt subject (brukes ved fjerning av bruker/team). -#[reducer] -pub fn delete_node_access_for_subject( - ctx: &ReducerContext, - subject_id: String, -) -> Result<(), String> { - let entries: Vec<_> = ctx.db.node_access().subject_id().filter(&subject_id).collect(); - for entry in entries { - ctx.db.node_access().id().delete(&entry.id); - } - Ok(()) -} - -// ============================================================================= -// Edge CRUD -// ============================================================================= - -#[reducer] -pub fn create_edge( - ctx: &ReducerContext, - id: String, - source_id: String, - target_id: String, - edge_type: String, - metadata: String, - system: bool, - created_by: String, -) -> Result<(), String> { - if id.is_empty() { - return Err("id kan ikke være tom".into()); - } - if ctx.db.edge().id().find(&id).is_some() { - return Err(format!("Edge med id {} finnes allerede", id)); - } - - ctx.db.edge().insert(Edge { - id, - source_id, - target_id, - edge_type, - metadata, - system, - created_at: ctx.timestamp, - created_by, - }); - Ok(()) -} - -#[reducer] -pub fn update_edge( - ctx: &ReducerContext, - id: String, - edge_type: String, - metadata: String, -) -> Result<(), String> { - let existing = ctx.db.edge().id().find(&id) - .ok_or_else(|| format!("Edge {} ikke funnet", id))?; - - ctx.db.edge().id().update(Edge { - edge_type, - metadata, - ..existing - }); - Ok(()) -} - -#[reducer] -pub fn delete_edge(ctx: &ReducerContext, id: String) -> Result<(), String> { - ctx.db.edge().id().delete(&id); - Ok(()) -} - -// ============================================================================= -// Plasseringsrelasjon (message_placements) -// ============================================================================= - -/// Sporer hvor meldinger vises på tvers av kontekster (chat, kanban, storyboard, etc.). -/// Sanntidskopi — PG er autoritativ, SpacetimeDB gir instant UI-oppdatering. -/// Synk: STDB → PG via sync-worker. -/// Ref: docs/features/universell_overfoering.md § 2, 5 -#[spacetimedb::table(accessor = message_placement, public)] -pub struct MessagePlacement { - #[primary_key] - pub id: String, - - #[index(btree)] - pub message_id: String, - #[index(btree)] - pub context_id: String, - pub context_type: String, // 'chat', 'kanban', 'storyboard', 'calendar', 'notes' - pub entered_at: Timestamp, - pub position_json: String, // JSON-serialisert posisjon (null for chat, {x,y} for storyboard, etc.) -} - -// ============================================================================= -// Mixer-kanaler (delt mixer-kontroll via SpacetimeDB) -// ============================================================================= - -/// Delt mixer-state per kanal i et live-rom. -/// Transient — finnes bare mens rommet er aktivt. -/// Alle deltakere abonnerer og ser endringer i sanntid. -#[spacetimedb::table(accessor = mixer_channel, public)] -pub struct MixerChannel { - #[primary_key] - pub id: String, // "{room_id}:{target_user_id}" - - #[index(btree)] - pub room_id: String, - #[index(btree)] - pub target_user_id: String, - pub gain: f64, // 0.0–1.5 - pub is_muted: bool, - pub active_effects: String, // JSON: {"fat_bottom": true, "robot": false, ...} - pub role: String, // "editor" | "viewer" — tilgangskontroll - pub updated_by: String, - pub updated_at: Timestamp, -} - -// ============================================================================= -// Live-rom (sanntidslyd via LiveKit) -// ============================================================================= - -/// Aktive LiveKit-rom knyttet til kommunikasjonsnoder. -/// Transient — finnes bare mens rommet er aktivt. -#[spacetimedb::table(accessor = live_room, public)] -pub struct LiveRoom { - #[primary_key] - pub room_id: String, // "communication_{uuid}" - - #[index(btree)] - pub communication_id: String, - pub is_active: bool, - pub started_at: Timestamp, - pub participant_count: u32, -} - -/// Deltakere i aktive LiveKit-rom. -#[spacetimedb::table(accessor = room_participant, public)] -pub struct RoomParticipant { - #[primary_key] - pub id: String, // "{room_id}:{user_id}" - - #[index(btree)] - pub room_id: String, - #[index(btree)] - pub user_id: String, - pub display_name: String, - pub role: String, // "publisher" | "subscriber" - pub joined_at: Timestamp, -} - -#[reducer] -pub fn create_live_room( - ctx: &ReducerContext, - room_id: String, - communication_id: String, -) -> Result<(), String> { - if room_id.is_empty() { - return Err("room_id kan ikke være tom".into()); - } - // Idempotent — hvis rommet allerede finnes, oppdater - if let Some(existing) = ctx.db.live_room().room_id().find(&room_id) { - ctx.db.live_room().room_id().update(LiveRoom { - is_active: true, - ..existing - }); - return Ok(()); - } - ctx.db.live_room().insert(LiveRoom { - room_id, - communication_id, - is_active: true, - started_at: ctx.timestamp, - participant_count: 0, - }); - Ok(()) -} - -#[reducer] -pub fn add_room_participant( - ctx: &ReducerContext, - room_id: String, - user_id: String, - display_name: String, - role: String, -) -> Result<(), String> { - let id = format!("{room_id}:{user_id}"); - - // Idempotent — oppdater hvis allerede finnes - if let Some(existing) = ctx.db.room_participant().id().find(&id) { - ctx.db.room_participant().id().update(RoomParticipant { - display_name, - role, - ..existing - }); - return Ok(()); - } - - ctx.db.room_participant().insert(RoomParticipant { - id, - room_id: room_id.clone(), - user_id, - display_name, - role, - joined_at: ctx.timestamp, - }); - - // Oppdater deltakertelling - if let Some(room) = ctx.db.live_room().room_id().find(&room_id) { - ctx.db.live_room().room_id().update(LiveRoom { - participant_count: room.participant_count + 1, - ..room - }); - } - Ok(()) -} - -#[reducer] -pub fn remove_room_participant( - ctx: &ReducerContext, - room_id: String, - user_id: String, -) -> Result<(), String> { - let id = format!("{room_id}:{user_id}"); - ctx.db.room_participant().id().delete(&id); - - // Oppdater deltakertelling - if let Some(room) = ctx.db.live_room().room_id().find(&room_id) { - let new_count = room.participant_count.saturating_sub(1); - ctx.db.live_room().room_id().update(LiveRoom { - participant_count: new_count, - ..room - }); - } - Ok(()) -} - -#[reducer] -pub fn close_live_room( - ctx: &ReducerContext, - room_id: String, -) -> Result<(), String> { - // Fjern alle deltakere - let participants: Vec<_> = ctx.db.room_participant().room_id().filter(&room_id).collect(); - for p in participants { - ctx.db.room_participant().id().delete(&p.id); - } - - // Fjern alle mixer-kanaler for dette rommet - let mixer_channels: Vec<_> = ctx.db.mixer_channel().room_id().filter(&room_id).collect(); - for mc in mixer_channels { - ctx.db.mixer_channel().id().delete(&mc.id); - } - - // Marker rommet som inaktivt - if let Some(room) = ctx.db.live_room().room_id().find(&room_id) { - ctx.db.live_room().room_id().update(LiveRoom { - is_active: false, - participant_count: 0, - ..room - }); - } - Ok(()) -} - -// ============================================================================= -// Mixer-kontroll reducers -// ============================================================================= - -/// Opprett eller oppdater en mixer-kanal med standardverdier. -#[reducer] -pub fn create_mixer_channel( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - updated_by: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - - // Idempotent — oppdater hvis allerede finnes - if let Some(existing) = ctx.db.mixer_channel().id().find(&id) { - ctx.db.mixer_channel().id().update(MixerChannel { - updated_by, - updated_at: ctx.timestamp, - ..existing - }); - return Ok(()); - } - - ctx.db.mixer_channel().insert(MixerChannel { - id, - room_id, - target_user_id, - gain: 1.0, - is_muted: false, - active_effects: "{}".to_string(), - role: "editor".to_string(), - updated_by, - updated_at: ctx.timestamp, - }); - Ok(()) -} - -/// Sett gain (volum) for en mixer-kanal. Clampes til 0.0–1.5. -#[reducer] -pub fn set_gain( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - gain: f64, - updated_by: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - let clamped = gain.clamp(0.0, 1.5); - - let existing = ctx.db.mixer_channel().id().find(&id) - .ok_or_else(|| format!("Mixer-kanal {} ikke funnet", id))?; - - // Tilgangskontroll: viewer kan ikke endre - if existing.role == "viewer" && existing.target_user_id != updated_by { - return Err("Viewer kan ikke endre mixer-innstillinger".into()); - } - - ctx.db.mixer_channel().id().update(MixerChannel { - gain: clamped, - updated_by, - updated_at: ctx.timestamp, - ..existing - }); - Ok(()) -} - -/// Sett mute-status for en mixer-kanal. -#[reducer] -pub fn set_mute( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - is_muted: bool, - updated_by: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - - let existing = ctx.db.mixer_channel().id().find(&id) - .ok_or_else(|| format!("Mixer-kanal {} ikke funnet", id))?; - - if existing.role == "viewer" && existing.target_user_id != updated_by { - return Err("Viewer kan ikke endre mixer-innstillinger".into()); - } - - ctx.db.mixer_channel().id().update(MixerChannel { - is_muted, - updated_by, - updated_at: ctx.timestamp, - ..existing - }); - Ok(()) -} - -/// Toggle en effekt for en mixer-kanal. Effektnavnet slås av/på i active_effects JSON. -#[reducer] -pub fn toggle_effect( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - effect_name: String, - updated_by: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - - let existing = ctx.db.mixer_channel().id().find(&id) - .ok_or_else(|| format!("Mixer-kanal {} ikke funnet", id))?; - - if existing.role == "viewer" && existing.target_user_id != updated_by { - return Err("Viewer kan ikke endre mixer-innstillinger".into()); - } - - // Parse, toggle, serialize tilbake - // Enkel JSON-håndtering uten serde (STDB-moduler er lette) - let mut effects = existing.active_effects.clone(); - if effects.contains(&format!("\"{}\":true", effect_name)) { - effects = effects.replace( - &format!("\"{}\":true", effect_name), - &format!("\"{}\":false", effect_name), - ); - } else if effects.contains(&format!("\"{}\":false", effect_name)) { - effects = effects.replace( - &format!("\"{}\":false", effect_name), - &format!("\"{}\":true", effect_name), - ); - } else { - // Effekten finnes ikke — legg til som aktiv - if effects == "{}" { - effects = format!("{{\"{}\":true}}", effect_name); - } else { - // Sett inn før siste } - effects = effects.trim_end_matches('}').to_string() - + &format!(",\"{}\":true}}", effect_name); - } - } - - ctx.db.mixer_channel().id().update(MixerChannel { - active_effects: effects, - updated_by, - updated_at: ctx.timestamp, - ..existing - }); - Ok(()) -} - -/// Sett en numerisk effektparameter i active_effects JSON. -/// Brukes for stemmeeffekter (robot_freq, robot_depth, monster_pitch) som har -/// parameterverdier, ikke bare av/på. Nøkkelen settes til den gitte verdien. -#[reducer] -pub fn set_effect_param( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - param_name: String, - value: f64, - updated_by: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - - let existing = ctx.db.mixer_channel().id().find(&id) - .ok_or_else(|| format!("Mixer-kanal {} ikke funnet", id))?; - - if existing.role == "viewer" && existing.target_user_id != updated_by { - return Err("Viewer kan ikke endre mixer-innstillinger".into()); - } - - let mut effects = existing.active_effects.clone(); - let param_pattern = format!("\"{}\":", param_name); - - if effects.contains(¶m_pattern) { - // Replace existing value — find the key and replace up to next comma or } - if let Some(start) = effects.find(¶m_pattern) { - let value_start = start + param_pattern.len(); - // Find end of value (next comma or closing brace) - let rest = &effects[value_start..]; - let value_end = rest.find(',').unwrap_or_else(|| rest.find('}').unwrap_or(rest.len())); - effects = format!( - "{}{}{}", - &effects[..value_start], - value, - &effects[value_start + value_end..] - ); - } - } else { - // Add new param - if effects == "{}" { - effects = format!("{{\"{}\":{}}}", param_name, value); - } else { - effects = effects.trim_end_matches('}').to_string() - + &format!(",\"{}\":{}}}", param_name, value); - } - } - - ctx.db.mixer_channel().id().update(MixerChannel { - active_effects: effects, - updated_by, - updated_at: ctx.timestamp, - ..existing - }); - Ok(()) -} - -/// Slett en mixer-kanal (når deltaker forlater rommet). -#[reducer] -pub fn delete_mixer_channel( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, -) -> Result<(), String> { - let id = format!("{room_id}:{target_user_id}"); - ctx.db.mixer_channel().id().delete(&id); - Ok(()) -} - -/// Sett rolle (editor/viewer) for en mixer-kanal. -/// Bare eier/admin kan endre rolle. -#[reducer] -pub fn set_mixer_role( - ctx: &ReducerContext, - room_id: String, - target_user_id: String, - role: String, - _updated_by: String, -) -> Result<(), String> { - if role != "editor" && role != "viewer" { - return Err(format!("Ugyldig rolle: {}. Bruk 'editor' eller 'viewer'", role)); - } - - let id = format!("{room_id}:{target_user_id}"); - let existing = ctx.db.mixer_channel().id().find(&id) - .ok_or_else(|| format!("Mixer-kanal {} ikke funnet", id))?; - - ctx.db.mixer_channel().id().update(MixerChannel { - role, - updated_at: ctx.timestamp, - ..existing - }); - Ok(()) -} - -// ============================================================================= -// Placement reducers -// ============================================================================= - -/// Plasser en melding i en kontekst. Idempotent — oppdaterer hvis -/// (message_id, context_type, context_id) allerede finnes. -#[reducer] -pub fn place_message( - ctx: &ReducerContext, - id: String, - message_id: String, - context_type: String, - context_id: String, - position_json: String, -) -> Result<(), String> { - if id.is_empty() { - return Err("id kan ikke være tom".into()); - } - - // Sjekk om plassering allerede finnes (upsert-semantikk) - // Søk på message_id + context_id (context_type er implisitt i context_id) - let existing: Option = ctx.db.message_placement() - .message_id() - .filter(&message_id) - .find(|p| p.context_type == context_type && p.context_id == context_id); - - if let Some(existing) = existing { - ctx.db.message_placement().id().update(MessagePlacement { - position_json, - ..existing - }); - } else { - ctx.db.message_placement().insert(MessagePlacement { - id, - message_id, - context_type, - context_id, - entered_at: ctx.timestamp, - position_json, - }); - } - Ok(()) -} - -/// Fjern en meldings plassering fra en kontekst. -#[reducer] -pub fn remove_placement( - ctx: &ReducerContext, - message_id: String, - context_type: String, - context_id: String, -) -> Result<(), String> { - let existing: Option = ctx.db.message_placement() - .message_id() - .filter(&message_id) - .find(|p| p.context_type == context_type && p.context_id == context_id); - - if let Some(placement) = existing { - ctx.db.message_placement().id().delete(&placement.id); - } - Ok(()) -} - -/// Flytt en plassering på canvas (oppdater posisjon). -/// Brukes for storyboard, kanban-rekkefølge, kalender-dato, etc. -#[reducer] -pub fn move_on_canvas( - ctx: &ReducerContext, - placement_id: String, - new_position_json: String, -) -> Result<(), String> { - let existing = ctx.db.message_placement().id().find(&placement_id) - .ok_or_else(|| format!("Plassering {} ikke funnet", placement_id))?; - - ctx.db.message_placement().id().update(MessagePlacement { - position_json: new_position_json, - ..existing - }); - Ok(()) -} - -// ============================================================================= -// Warmup/vedlikehold -// ============================================================================= - -/// Tøm alle noder, edges, node_access og transiente data (brukes ved restart/warmup) -#[reducer] -pub fn clear_all(ctx: &ReducerContext) -> Result<(), String> { - let all_access: Vec<_> = ctx.db.node_access().iter().collect(); - for a in all_access { - ctx.db.node_access().id().delete(&a.id); - } - let all_edges: Vec<_> = ctx.db.edge().iter().collect(); - for e in all_edges { - ctx.db.edge().id().delete(&e.id); - } - let all_nodes: Vec<_> = ctx.db.node().iter().collect(); - for n in all_nodes { - ctx.db.node().id().delete(&n.id); - } - // Rydd opp transiente data - let all_mixer: Vec<_> = ctx.db.mixer_channel().iter().collect(); - for m in all_mixer { - ctx.db.mixer_channel().id().delete(&m.id); - } - let all_placements: Vec<_> = ctx.db.message_placement().iter().collect(); - for p in all_placements { - ctx.db.message_placement().id().delete(&p.id); - } - log::info!("Alle noder, edges, node_access, mixer_channels og message_placements slettet (clear_all)"); - Ok(()) -} diff --git a/tasks.md b/tasks.md index 02e0f48..0d2cd8c 100644 --- a/tasks.md +++ b/tasks.md @@ -284,8 +284,7 @@ ingen synk-kompleksitet. - [x] 22.1 WebSocket-lag i portvokteren: implementer PG LISTEN/NOTIFY-lytter og WebSocket-endepunkt. Legg til PG-triggers (`notify_node_change`, `notify_edge_change`) for nodes og edges. Frontend kobler til begge (STDB + nytt WS) i parallell for verifisering. - [x] 22.2 Frontend-migrering: erstatt SpacetimeDB-klient med vanlig WebSocket til portvokteren. Erstatt STDB-stores med reaktive stores som lytter på WebSocket. Verifiser all sanntidsfunksjonalitet (chat, kanban, kalender, mixer, canvas). - [x] 22.3 Fjern STDB-skrivestien: portvokteren slutter å skrive til SpacetimeDB. All skriving går kun til PG. NOTIFY-triggere er eneste push-mekanisme. Verifiser at ingenting avhenger av STDB-data. -- [~] 22.4 Fjern SpacetimeDB: stopp Docker-container, fjern STDB-modul, fjern STDB-klient fra portvokteren og frontend, fjern synkroniseringskode, oppdater docs og CLAUDE.md. - > Påbegynt: 2026-03-18T13:15 +- [x] 22.4 Fjern SpacetimeDB: stopp Docker-container, fjern STDB-modul, fjern STDB-klient fra portvokteren og frontend, fjern synkroniseringskode, oppdater docs og CLAUDE.md. - [ ] 22.5 Opprydding: arkiver STDB-relaterte erfaringsdocs, oppdater alle docs-referanser, fjern Docker-konfig for SpacetimeDB, fjern SpacetimeDB-loven fra feedback-memories. ## Fase 23: Validering — test og kvalitetssikring per fase diff --git a/tools/synops-respond/src/main.rs b/tools/synops-respond/src/main.rs index c9ba528..9f8e21b 100644 --- a/tools/synops-respond/src/main.rs +++ b/tools/synops-respond/src/main.rs @@ -4,8 +4,7 @@ // kaller claude CLI, og returnerer svartekst. Med --write: // oppretter svar-node og edges i PG + logger ressursforbruk. // -// STDB-skriving (sanntidsvisning) gjøres av maskinrommet etter -// at dette verktøyet returnerer — maskinrommet eier STDB-skriving. +// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter. // // Auth, ratelimit, kill switch og loop-prevensjon forblir i maskinrommet. // diff --git a/tools/synops-tts/src/main.rs b/tools/synops-tts/src/main.rs index b0a8750..1efd66d 100644 --- a/tools/synops-tts/src/main.rs +++ b/tools/synops-tts/src/main.rs @@ -181,7 +181,7 @@ async fn call_elevenlabs(text: &str, voice_id: &str) -> Result, String> } /// Opprett media-node og has_media-edge i PostgreSQL. -/// Returnerer media_node_id for STDB-synk. +/// Returnerer media_node_id. PG NOTIFY-triggere sender sanntidsoppdateringer. async fn write_to_db( cas_hash: &str, text: &str,