diff --git a/.env.example b/.env.example index af76cae..6ef1f67 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# === Sidelinja v2 — Lokalt utviklingsmiljø === +# === Synops — Lokalt utviklingsmiljø === # Kopier til .env.local og fyll inn verdier: # cp .env.example .env.local # diff --git a/CLAUDE.md b/CLAUDE.md index 79550ba..94e908a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,15 +12,11 @@ plattformkode og infrastruktur er skilt fra tenant-data og -innhold. - **Standard arbeidsmodus:** Start i planleggingsmodus. Lag en grundig plan, få godkjenning, deretter implementer. Jobbene er ment å kunne kjøre lenge autonomt uten input underveis. -- **Testmiljø:** `./dev.sh` er den kanoniske måten å starte utviklingsmiljøet. - Når nye steg eller oppsett-quirks oppdages, oppdater alltid `dev.sh`. - Før Vegard tester i browser: kjør `./dev.sh`, verifiser med - `cargo check`/`svelte-check`/`curl`, og meld tilbake at det er klart. +- **Utvikling mot server.** Ingen lokale databaser eller tjenester. + Frontend (SvelteKit) utvikles lokalt med HMR mot server-API. + Rust bygges lokalt, deployes til server for integrasjonstest. - **Browser-testing:** Claude har ikke tilgang til browser. Visuell testing gjøres av Vegard. Claude verifiserer backend (kompilering, API, DB-state). -- **Utvikling mot server.** Ingen lokale databaser eller tjenester. - Frontend utvikles lokalt mot server-API. Rust bygges lokalt, - deployes til server for integrasjonstest. - **Commit og push:** Bruk egen vurdering. Trygt og reverserbart. - **Deploy til produksjon:** Krever alltid eksplisitt godkjenning fra Vegard. - **Diskusjon:** Forklar og diskuter før arkitekturendringer. @@ -36,10 +32,12 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `rom_ikke_forum.md` — Opplevelse-først, to-lags-modell, administrativ opplevelse - `universell_input.md` — Tre primitiver (input, mottak, kommunikasjon), noder+edges - `maskinrommet.md` — Rust-orkestrator: fang, prosesser, lever - - `bruker_ikke_workspace.md` — Brukeren er sentrum, samlings-noder med RLS-siloer + - `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 - `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene: - - (kommer: input, mottak, kommunikasjonsnode, node, edge) + - `nodes.md` — Node-skjema, node_kind, visibility, CAS-noder, eierskap + - `edges.md` — Edge-skjema, typer, metadata, systemedges + - (kommer: input, mottak, kommunikasjonsnode) - `docs/concepts/` — Brukeropplevelser/produktområder: - `studioet.md`, `møterommet.md`, `redaksjonen.md`, `podcastfabrikken.md`, `kunnskapsgrafen.md`, `valgomaten.md`, `den_asynkrone_gjesten.md` @@ -50,7 +48,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `docs/setup/` — Oppsett og drift: - `produksjon.md` — Steg-for-steg oppsett av Hetzner VPS fra scratch - `lokal.md` — Lokalt utviklingsmiljø (WSL2, mot server) - - `migration_safety.md` — Sjekkliste for PostgreSQL-migrasjoner (RLS-verifisering) + - `migration_safety.md` — Sjekkliste for PostgreSQL-migrasjoner (v1 workspace-RLS, trenger omskriving til node_access) - `docs/infra/` — Infrastruktur og drift: - `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + fallback) - `api_grensesnitt.md` — Kommunikasjonskart: SvelteKit er web-API, Rust er worker @@ -63,7 +61,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: ## Aktører - **Vegard** — serveradmin, utvikler, bruker. SSH: `vegard@157.180.81.26` - **Claude** — AI-agent, utvikler. SSH: `claude@157.180.81.26` -- Begge har sudo + docker-tilgang. Tjenestebruker `sidelinja` eier filer/Docker. +- Begge har sudo + docker-tilgang på serveren. ## Stack - **Orkestrator/Backend:** Rust (maskinrommet) @@ -88,10 +86,9 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`: - `synops.no` — Plattformdomene (reservert, ikke i bruk ennå) ## Git -- **Forgejo-org:** `sidelinja` -- **Repos:** - - `synops` — plattformkode og arkitektur - - `sidelinja` — podcastinnhold: `ssh://git@git.sidelinja.org:222/sidelinja/sidelinja.git` +- **Repos i Forgejo:** + - `vegard/synops` — plattformkode og arkitektur: `ssh://git@git.sidelinja.org:222/vegard/synops.git` + - `sidelinja/sidelinja` — podcastinnhold: `ssh://git@git.sidelinja.org:222/sidelinja/sidelinja.git` - **Git-identitet:** vegard / vnotnes@pm.me - **Forgejo-bruker:** vegard (admin) - **CLI:** Bruk `tea`, ikke `gh` (vi bruker Forgejo, ikke GitHub) @@ -121,8 +118,8 @@ Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ... (samler folk). Alt annet er visninger og edges. 3. **Maskinrommet orkestrerer alt.** Fang, prosesser, lever. Edge-drevet ressursallokering. Tjenester under er utbyttbare. -4. **Brukeren er sentrum.** Ingen workspace-velger. Du ser dine edges. - Samlings-noder gir felles kontekst med RLS-siloer under. +4. **Noder er sentrum.** Brukere, team, innhold — alt er noder. + 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. diff --git a/docs/arkitektur.md b/docs/arkitektur.md index 322dfa4..bff2e89 100644 --- a/docs/arkitektur.md +++ b/docs/arkitektur.md @@ -1,158 +1,172 @@ -# Arkitektur — Sidelinja v2 - -## Visjon - -Sidelinja er en plattform for redaksjonelt arbeid og podcast-produksjon. -Ikke en webapp med features — en plattform med primitiver som kan bli -hva som helst. - -Alt er noder og edges i en graf. Input fanger, mottak presenterer, -kommunikasjonsnoder samler folk. Maskinrommet orkestrerer tekniske -tjenester under. Frontend er et tynt lag som viser grafen. - -## Lagmodell - -``` -┌─────────────────────────────────────┐ -│ GUI (SvelteKit) │ -│ Visninger: spørringer mot grafen │ -└────────┬──────────────────┬─────────┘ - │ skriv │ les (sanntid) - │ │ direkte WebSocket -┌────────▼────────┐ ┌─────▼─────────┐ -│ Maskinrommet │ │ SpacetimeDB │ -│ (Rust) │ │ Aktive noder │ -│ Orkestrering │ └───────────────┘ -└──┬─────┬─────┬──┘ - │ │ │ - ▼ ▼ ▼ -┌─────┐┌─────┐┌─────┐┌─────────────┐ -│ PG ││STDB ││ CAS ││ Whisper, │ -│ ││(skr)││ ││ LiteLLM, │ -│ ││ ││ ││ LiveKit ... │ -└─────┘└─────┘└─────┘└─────────────┘ -``` - -### Skrivestien -GUI → Maskinrommet (Rust) → tjenester - -All orkestrering, edge-logikk, validering og ressursallokering -går gjennom Rust. Maskinrommet leser edges og bestemmer hvilke -tjenester som trigges. - -### Lesestien (sanntid) -SpacetimeDB → GUI (direkte WebSocket) - -STDB klient-SDK gir ~10μs-oppdateringer med lokal cache. -En bevisst optimering — ikke et hull i lagmodellen. - -### Lesestien (tradisjonell) -GUI → Maskinrommet (Rust) → PG - -Søk, historikk, statistikk, arkiv. Cypher via AGE for -graftraversering når CTEs ikke holder. - -## Datamodell - -### Noder -Én tabell. Alt innhold er noder: meldinger, oppgaver, notater, -faktoider, mediefiler, kommunikasjonsrom, samlings-noder, brukere. - -Felles skjema: id, innhold, created_at, author, content_hash (→ CAS). -Modalitetsspesifikk metadata i JSONB. - -### Edges -Én tabell. Alle relasjoner er edges: tilhørighet, tilgang, type, -synlighet, rolle, status. - -Hva en node "er" bestemmes utelukkende av edges: -- Node + edge til kanal = chatmelding -- Node + edge til board + status-edge = kanban-kort -- Node + edge til dato = kalenderoppføring -- Node + edge til kun bruker = privat notat -- Node + edge til topic = faktoid -- Node uten edges = løs tanke - -### Visninger -Visninger er spørringer mot grafen med ulike filtre: -- Chat = noder med kanal-edge, sortert på tid -- Kanban = noder med board-edge, gruppert på status -- Kalender = noder med dato-edge, på tidslinje -- Mottaksflate = noder med edge til deg, vektet på relevans - -## Tre primitiver - -### 1. Input -Én multimodal overflate som fanger alt: tekst, lyd, bilde, AI, -URL. To versjoner — sanntid (STDB-lag) og flat (PG-lag). -Output er alltid en node. - -### 2. Mottak -Én personlig flate som presenterer alt tilpasset deg. Format -bestemmes av mottaker (tekst/lyd). Filtrering via dine edges. -Sanntid (stream) eller asynkront (digest). - -### 3. Kommunikasjon -En node som samler folk med tilgangsregler. Samme type fra -én-til-én-samtale til livesending. Forskjellen er edges: -eier, input-tilgang, mottak-tilgang, rolle. - -## Maskinrommet - -Rust-tjeneste med tre operasjoner: **fang**, **prosesser**, **lever**. - -Edge-drevet ressursorkestrering: maskinrommet leser edges og -bestemmer hvilke tjenester som spinnes opp. Dagboknotat = bare -transkriber. Livesending = transkriber + LiveKit + AI + mediaprosessering. - -Forvalter også CAS (binærlagring) med intelligent pruning basert -på modalitet, edges og aksessmønstre. - -## Sikkerhet - -### RLS-siloer -Samlings-noder (det som var workspaces) er harde sikkerhetsgenser -i PG med RLS. `workspace_id = current_setting(...)` — instant, -vanntett. Edge-basert tilgang er UX *innenfor* siloen. - -### Privat er default -Input uten mottaker-edge er automatisk privat. Ingen ser det. -Deling er å legge til edges. - -## Datalag - -### PostgreSQL -Arkiv og graf. Alle noder og edges. Fulltekstsøk, pgvector -(semantisk søk), JSONB. Apache AGE for Cypher når CTEs ikke holder. - -### SpacetimeDB -Sanntidslag. Aktive noder og edges speilet fra PG. Frontend -abonnerer via WebSocket. Ingen eierskapskonflikt — STDB er en -live-visning av en delmengde av PG-grafen. - -### CAS (Content-Addressable Store) -Binærdata (lyd, bilde, video) lagret med hash. TTL basert på -modalitet, edges og aksesslog. Tekst lever evig, lyd 30 dager, -video 14 dager — forlenges ved aksess. Generert innhold (TTS, -thumbnails) er en cache som regenereres on-demand. - -## Teknologivalg - -| Rolle | Teknologi | Begrunnelse | -|-------|-----------|-------------| -| Orkestrator | Rust | Ytelse, typesikkerhet, allerede i stacken | -| Frontend | SvelteKit | PWA, SSR, allerede i stacken | -| Database | PostgreSQL | Økosystem (pgvector, fulltekstsøk, AGE), stabilitet | -| Sanntid | SpacetimeDB | In-memory, WebSocket-subscriptions, ~10μs | -| Binærlagring | CAS (filsystem) | Enkel, deduplisering, ingen ekstern avhengighet | -| AI Gateway | LiteLLM | Multi-provider, BYOK, OpenRouter fallback | -| STT | faster-whisper | Lokal, god norsk kvalitet | -| TTS | ElevenLabs (→ lokal) | Kommersiell start, lokal når kvaliteten holder | -| Auth | Authentik | SSO, OIDC, self-hosted | -| Reverse proxy | Caddy | Auto-TLS, enkel config | -| Lyd/video | LiveKit | WebRTC, self-hosted | - -## Retninger - -Arkitekturen er basert på vedtatte retninger dokumentert i -`docs/retninger/`. Se `docs/retninger/README.md` for oversikt. +# Arkitektur — Synops + +## Visjon + +Synops er en plattform for redaksjonelt arbeid og podcast-produksjon. +Ikke en webapp med features — en plattform med primitiver som kan bli +hva som helst. Sidelinja (podcastredaksjonen) er en tenant som bruker +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. + +## Lagmodell + +``` +┌─────────────────────────────────────┐ +│ GUI (SvelteKit) │ +│ Visninger: spørringer mot STDB │ +└────────┬──────────────────┬─────────┘ + │ intensjoner │ les (sanntid) + │ │ direkte WebSocket +┌────────▼────────┐ ┌──────▼──────────┐ +│ Maskinrommet │ │ SpacetimeDB │ +│ (Rust) │ │ Hele grafen │ +│ Eier alle │ │ (noder+edges) │ +│ skrivinger │ └─────────────────┘ +└──┬─────┬─────┬──┘ + │ │ │ + ▼ ▼ ▼ +┌─────┐┌─────┐┌─────┐┌─────────────┐ +│ PG ││STDB ││ CAS ││ Whisper, │ +│(bak)││(skr)││ ││ LiteLLM, │ +│ ││ ││ ││ LiveKit ... │ +└─────┘└─────┘└─────┘└─────────────┘ +``` + +### Skrivestien +GUI → intensjon → Maskinrommet (Rust) → SpacetimeDB (instant) → PG (async) + +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. + +### Lesestien (sanntid) +SpacetimeDB → GUI (direkte WebSocket) + +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. + +### 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. + +## Datamodell + +### Alt er noder +Én `nodes`-tabell. Alt er noder: brukere, team, meldinger, oppgaver, +notater, mediefiler, kommunikasjonsrom, samlings-noder. En bruker er +en node som tilfeldigvis kan logge inn. + +Felles skjema: id, innhold, created_at, content_hash (→ CAS). +Modalitetsspesifikk metadata i JSONB. + +### Edges definerer alt +Én `edges`-tabell. Edge-typer er frie strenger. Hva en node "er" +bestemmes utelukkende av dens edges: + +- Node + edge til kanal = chatmelding +- Node + edge til board + status-edge = kanban-kort +- Node + edge til dato = kalenderoppføring +- Node + edge til kun bruker = privat notat +- Node uten edges = løs tanke + +### Visninger er spørringer +Visninger er spørringer mot SpacetimeDB 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 +- Mottaksflate = noder med edge til deg, vektet på relevans + +Ingen forhåndsdefinerte visningstyper. Nye visninger er nye filtre. + +## Input + +Én universell input-komponent, gjenbrukt overalt. Fanger tekst, lyd, +bilde, AI, URL. Konteksten (hvor du er) bestemmer hvilke edges som +legges til. Output er alltid en node. + +## Struktur uten workspaces + +Ingen workspace-velger. Ingen `workspace_id`. Samlings-noder gir +struktur: et team er en samlings-node, et prosjekt er en samlings-node. +Du ser noder du har tilgang til via dine edges. + +### Aliaser +En bruker kan ha alias-noder (f.eks. for ulike roller). Koblet med +system-edges som er usynlige for traversering. + +## Maskinrommet + +Rust-tjeneste med tre operasjoner: **fang**, **prosesser**, **lever**. + +Eier alle skrivinger. Frontend sender intensjoner, maskinrommet +validerer og utfører. Edge-drevet ressursorkestrering: maskinrommet +leser edges og bestemmer hvilke tjenester som spinnes opp. + +Forvalter også CAS (binærlagring) med intelligent pruning basert +på modalitet, edges og aksessmønstre. + +## Sikkerhet + +### Synlighet (visibility) +Noder har en visibility-egenskap med fire nivåer: +- **hidden** — usynlig, kun tilgjengelig via system-edges +- **discoverable** — kan finnes, men innhold skjult +- **readable** — innhold lesbart for de med tilgang +- **open** — tilgjengelig for alle med traverseringssti + +Traversering respekterer visibility. Du kan ikke følge edges gjennom +noder du ikke har lov til å se. + +### Materialisert tilgangsmatrise +`node_access`-tabell som cacher beregnet tilgang fra edge-grafen. +Oppdateres ved edge-endring, ikke ved lesing. Rask oppslag — ingen +rekursiv graftraversering per forespørsel. + +### Privat er default +Input uten mottaker-edge er automatisk privat. Ingen ser det. +Deling er å legge til edges. + +## Datalag + +### PostgreSQL +Persistent backup og arkiv. 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. + +### CAS (Content-Addressable Store) +Binærdata (lyd, bilde, video) lagret med hash. TTL basert på +modalitet, edges og aksesslog. Generert innhold (TTS, thumbnails) +er en cache som regenereres on-demand. + +## Teknologivalg + +| Rolle | Teknologi | Begrunnelse | +|-------|-----------|-------------| +| Orkestrator | Rust | Ytelse, typesikkerhet, eier alle skrivinger | +| Frontend | SvelteKit | PWA, SSR, tynt lag mot STDB | +| Database | PostgreSQL | Persistent backup, pgvector, fulltekstsøk, AGE | +| Sanntid | SpacetimeDB | Hele grafen, WebSocket-subscriptions, ~10μs | +| Binærlagring | CAS (filsystem) | Enkel, deduplisering, ingen ekstern avhengighet | +| AI Gateway | LiteLLM | Multi-provider, BYOK, OpenRouter fallback | +| STT | faster-whisper | Lokal, god norsk kvalitet | +| TTS | ElevenLabs (→ lokal) | Kommersiell start, lokal når kvaliteten holder | +| Auth | Authentik | SSO, OIDC, self-hosted | +| Reverse proxy | Caddy | Auto-TLS, enkel config | +| Lyd/video | LiveKit | WebRTC, self-hosted | + +## Retninger + +Arkitekturen er basert på vedtatte retninger dokumentert i +`docs/retninger/`. Se `docs/retninger/README.md` for oversikt. diff --git a/docs/concepts/den_asynkrone_gjesten.md b/docs/concepts/den_asynkrone_gjesten.md index 27b700f..5397c02 100644 --- a/docs/concepts/den_asynkrone_gjesten.md +++ b/docs/concepts/den_asynkrone_gjesten.md @@ -43,8 +43,7 @@ Mange interessante gjester har ikke tid til å stille i studio. Den Asynkrone Gj ```sql guest_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- Samlings- eller tema-node guest_name TEXT NOT NULL, -- Visningsnavn ("Erna Solberg") questions JSONB NOT NULL, -- [{ "sort": 1, "text": "Hva tenker du om...?" }] token TEXT UNIQUE NOT NULL, -- Kryptografisk sikker, URL-safe token @@ -58,9 +57,9 @@ guest_tokens ( ### 4.2 Sikkerhet - Token er kryptografisk tilfeldig (256-bit, URL-safe base64). -- SvelteKit validerer token ved hvert request: sjekker expiry, recordings_count < max_recordings, og workspace-tilhørighet. +- SvelteKit validerer token ved hvert request: sjekker expiry, recordings_count < max_recordings, og node-tilhørighet. - Gjestens meldinger merkes med `author_id = NULL` og `metadata.guest_name` + `metadata.guest_token_id` for sporbarhet. -- Ingen tilgang til andre channels, workspaces eller funksjoner. +- Ingen tilgang til andre noder eller funksjoner. - Tokenet kan revokeres manuelt av redaksjonen. ### 4.2b Sikkerhetsdybde (mot token-lekkasje og misbruk) @@ -81,7 +80,7 @@ clamav: image: clamav/clamav:latest restart: unless-stopped volumes: - - /srv/sidelinja/media:/scan:ro + - /srv/synops/media:/scan:ro networks: - sidelinja-net ``` @@ -101,7 +100,7 @@ Gjest åpner URL med token → SvelteKit validerer token → Viser spørsmål + opptaksknapp → Gjest tar opp svar - → SvelteKit streamer lydfil til media/{workspace_slug}/voice/ + → SvelteKit streamer lydfil til CAS (content-addressable store) → Oppretter message (voice_memo) i channelen → Oppretter whisper_transcribe-jobb i jobbkøen → Inkrementerer recordings_count @@ -118,8 +117,8 @@ Gjest åpner URL med token ## 6. Instruks for Claude Code * `guest_tokens`-tabellen er **ikke** en node i grafen — den er ren tilgangsstyring. -* Gjeste-UI er en egen SvelteKit-rute (`/guest/[token]`) med minimal layout (ingen navbar, ingen workspace-switcher). +* Gjeste-UI er en egen SvelteKit-rute (`/guest/[token]`) med minimal layout (ingen navbar). * Gjenbruk lydmeldinger-komponenten — ikke bygg en egen opptaksflyt. * Meldinger fra gjester har `author_id = NULL`. Frontend må håndtere dette gracefully (vis `guest_name` i stedet). * Tokenet skal **aldri** gi tilgang til å lese andre meldinger i channelen — gjesten kan kun skrive. -* Alt er workspace-scopet. Token bærer workspace_id eksplisitt. +* Tilgang er styrt via node_access. Token bærer node_id eksplisitt. diff --git a/docs/concepts/kunnskapsgrafen.md b/docs/concepts/kunnskapsgrafen.md index 221ac07..5604caa 100644 --- a/docs/concepts/kunnskapsgrafen.md +++ b/docs/concepts/kunnskapsgrafen.md @@ -49,4 +49,4 @@ Grafen vokser organisk via `#`-mentions, men dette skaper uunngåelig fragmenter **Forebygging:** Autocomplete bør vise eksisterende entiteter med matchende aliases *før* brukeren oppretter nye. "Mente du #Jonas Gahr Støre?" ved skriving av `#Støre`. ## 5. Datamodell -Den tekniske datamodellen (nodes-supertabell, graph_edges, detailtabeller, deterministiske UUIDs, workspace-isolasjon) er dokumentert i `docs/features/kunnskapsgraf_og_relasjoner.md`. +Den tekniske datamodellen (nodes-supertabell, graph_edges, detailtabeller, deterministiske UUIDs, tilgangsstyring via node_access) er dokumentert i `docs/features/kunnskapsgraf_og_relasjoner.md`. diff --git a/docs/concepts/podcastfabrikken.md b/docs/concepts/podcastfabrikken.md index 22c62ce..b46da86 100644 --- a/docs/concepts/podcastfabrikken.md +++ b/docs/concepts/podcastfabrikken.md @@ -29,6 +29,23 @@ Hver publisert episode får en side med: * Fane: **SRT** (nedlastbar undertekstfil — master-kopi fra Git) * Fane: **Ren tekst** (lesbart transkripsjonsdokument — avledet fra SRT, lagret i PG) +### 2.2 Live-to-Archive (Studio/Møterom → Episode) + +Mye av innholdet som ender i podcastfabrikken starter som live-innspilling +i Studioet eller Møterommet. Denne flyten unngår manuell opplasting: + +1. Under innspilling i LiveKit produserer Whisper chunks i sanntid +2. Når innspillingen stoppes → automatisk jobb `studio_to_episode`: + - Konsoliderer live-chunks til én SRT-fil + - Oppretter episode-node med storyboard basert på markører satt under innspilling + - Trigger AI-analyse (samme pipeline som ved vanlig opplasting) +3. Lydfilen lagres i CAS med edge til episode-noden +4. Episoden dukker opp i podcastfabrikken som et utkast — klar for + godkjenning, redigering og publisering + +Fordel: aldri behov for «last opp MP3 etter innspilling» — flyten er +live → arkiv → publisering. + ## 3. Spesialhåndtering: Oppdatering av eksisterende episoder (Cache-busting) Podcast-apper (Apple, Spotify) og CDN-er cacher innhold aggressivt. For at en endring i f.eks. "Introepisoden" skal slå gjennom hos lytterne, MÅ følgende tekniske regler følges: @@ -69,17 +86,17 @@ Sidelinja podcast med Vegard Nøtnæs, Trond Sørensen, Arne Eidshagen, Peter Hagen, Nicolai Buzatu, Bjørn Einar Drag, Øystein Sjølie ``` -## 5. Workspace-spesifikk konfigurasjon -Hver workspace har sin egen podcast-konfigurasjon, lagret i `workspaces.settings` (JSONB): +## 5. Per samlings-node konfigurasjon +Hver samlings-node (f.eks. Sidelinja) har sin egen podcast-konfigurasjon, lagret som JSONB-metadata på noden: ### 5.1 Mediefiler -Lydfiler lagres i undermapper per workspace: `/srv/sidelinja/media/{workspace_slug}/`. Caddy ruter trafikk basert på domene (fra `workspaces.domain`) til riktig undermappe. +Lydfiler lagres i CAS (content-addressable store) med edges til episode-noder. Caddy ruter trafikk basert på domene (fra samlings-nodens metadata) til riktig innhold. ### 5.2 Transkripsjoner (Git-repostruktur) -Det opprettes **ett Forgejo-repo per workspace** for SRT-filer, slik at historikk og redigering ikke blandes på tvers av podcaster. +Det opprettes **ett Forgejo-repo per samlings-node** for SRT-filer, slik at historikk og redigering ikke blandes på tvers av podcaster. #### Repo-oppretting -Repoet opprettes **on-demand** ved første transkripsjonsjobb for en workspace, via Forgejo API. Ikke alle workspaces trenger transkripsjonsrepo. +Repoet opprettes **on-demand** ved første transkripsjonsjobb for en samlings-node, via Forgejo API. Ikke alle samlings-noder trenger transkripsjonsrepo. #### Filnavnkonvensjon Flat struktur med prosesseringstidspunkt som filnavn: @@ -93,7 +110,7 @@ Flat struktur med prosesseringstidspunkt som filnavn: * **Ingen metadata i filnavn:** Episodenummer, tittel, slug og annen metadata lever i PostgreSQL, ikke i filnavnet. Filnavnet er en stabil identifikator som aldri endres. #### Mediefiler matcher Git -Lydfilen i `/srv/sidelinja/media/{workspace_slug}/` bruker **samme navnekonvensjon** som SRT-filen: `20260315_143022.mp3` matcher `20260315_143022.srt`. Dette kobler mediefil og transkripsjon uten databaseoppslag. +Lydfilen i CAS bruker **samme navnekonvensjon** som SRT-filen: `20260315_143022.mp3` matcher `20260315_143022.srt`. Dette kobler mediefil og transkripsjon uten databaseoppslag. #### Reprosessering (redigert lyd) Når en lydfil redigeres og transkriberes på nytt, **beholdes det opprinnelige filnavnet**. Rust-worker overskriver SRT-filen i Git — historikken viser endringene via `git log`/`git diff`. Mediefilen i arkivet døpes om til å matche Git-filnavnet dersom den opprinnelig hadde et annet navn. @@ -104,7 +121,7 @@ En dedikert servicebruker **"serverassistent"** opprettes i Forgejo med push-til #### Webhook-flyt ``` Forgejo push-webhook → SvelteKit POST /api/webhooks/forgejo - → INSERT INTO job_queue (type: 'srt_parse', payload: {repo, commit, workspace_id}) + → INSERT INTO job_queue (type: 'srt_parse', payload: {repo, commit, node_id}) → Rust-worker plukker opp jobben og parser SRT → avledede formater i PG ``` SvelteKit validerer webhook-signatur og legger jobb i køen. Rust-worker forblir en ren kø-consumer uten eget HTTP-endepunkt. @@ -113,18 +130,18 @@ SvelteKit validerer webhook-signatur og legger jobb i køen. Rust-worker forblir En enkel SRT-editor bygges i SvelteKit (Lag 3, sammen med Podcastfabrikken): segmenter som redigerbare tekstfelt med tidsstempler, "Lagre" committer tilbake til Git via Forgejo API. Forgejo web-UI fungerer som fallback for power users. ### 5.3 AI-prompts -* **Whisper `initial_prompt`:** Navnelister og kontekst lagres per workspace i `settings.whisper_prompt`. Rust-worker bygger prompten fra statisk liste + aktører i workspace-ets kunnskapsgraf. -* **LLM system-prompts:** OpenRouter-prompts for metadata-uttrekk lagres i `settings.llm_prompts` slik at AI-en kjenner konteksten og vertene for akkurat den podcasten. +* **Whisper `initial_prompt`:** Navnelister og kontekst lagres per samlings-node i metadata (`whisper_prompt`). Rust-worker bygger prompten fra statisk liste + aktører knyttet til samlings-noden via edges. +* **LLM system-prompts:** OpenRouter-prompts for metadata-uttrekk lagres i metadata (`llm_prompts`) slik at AI-en kjenner konteksten og vertene for akkurat den podcasten. ### 5.4 RSS-feed -SvelteKit genererer `/feed.xml` dynamisk basert på domenet forespørselen kommer fra (matcher `workspaces.domain`), eller workspace-slug som fallback. +SvelteKit genererer `/feed.xml` dynamisk basert på domenet forespørselen kommer fra (matcher samlings-nodens domene-metadata), eller node-slug som fallback. ### 5.5 Statistikk -Rust-workeren `stats_parse` knytter nedlastingstall fra Caddy-logger til riktig `workspace_id` basert på filsti i loggen. +Rust-workeren `stats_parse` knytter nedlastingstall fra Caddy-logger til riktig samlings-node basert på domene i loggen. ## 6. Instruks for Claude Code * **Lydfiler:** Håndter filopplasting i SvelteKit strømmende (streaming) for filer >100MB for å unngå minne-lekkasjer. * **Feilhåndtering:** Hvis OpenRouter timer ut eller Whisper feiler, må oppgaven flagges med status `error` i databasen slik at brukeren kan trigge jobben på nytt manuelt via UI. * **Opprydding (Disk):** Når en fil oppdateres vellykket, skal den gamle/foreldede `.mp3`-filen enten slettes fra Hetzner-serveren automatisk, eller flyttes til en `/archive/`-mappe basert på en miljøvariabel. * **Transkripsjoner:** Master-kopi alltid i Git. Aldri rediger avledede formater direkte i PG — de regenereres fra Git-kilden. -* **Workspace:** Alle jobber, mediefiler og metadata opprettes med riktig `workspace_id`. Hent workspace-config (prompts, domene) fra `workspaces.settings`. \ No newline at end of file +* **Tilhørighet:** Alle jobber, mediefiler og metadata knyttes til riktig samlings-node via edges. Hent config (prompts, domene) fra samlings-nodens JSONB-metadata. \ No newline at end of file diff --git a/docs/concepts/redaksjonen.md b/docs/concepts/redaksjonen.md index 98542a0..8934140 100644 --- a/docs/concepts/redaksjonen.md +++ b/docs/concepts/redaksjonen.md @@ -33,4 +33,4 @@ 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. -* Alle data er workspace-scopet. SpacetimeDB-tilkoblinger bærer `workspace_id`. +* All tilgang er styrt via node_access-matrisen. SpacetimeDB-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder. diff --git a/docs/erfaringer/spacetimedb_integrasjon.md b/docs/erfaringer/spacetimedb_integrasjon.md index a267ac0..5feae6c 100644 --- a/docs/erfaringer/spacetimedb_integrasjon.md +++ b/docs/erfaringer/spacetimedb_integrasjon.md @@ -90,7 +90,7 @@ spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindin --module-path . ``` -**Merk:** Bruk `./dev.sh` for å starte hele stacken automatisk (inkl. SpacetimeDB publish + binding-generering). `./dev.sh --clean` starter blankt. +**Merk:** SpacetimeDB-modulen publiseres manuelt med `spacetime publish` mot server-instansen. ## 6. Arkitekturendring: SpacetimeDB som cache foran PG (mars 2026) @@ -149,6 +149,10 @@ PG-polling adapter (`pg.svelte.ts`) brukes kun når SpacetimeDB ikke er konfigur ## 8. Reducer-parameternavn — unngå underscore-prefix +> *Kodeeksemplene i denne seksjonen er fra v1 og bruker `workspace_id`-parametere. +> I v2 er workspace-modellen 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 diff --git a/docs/features/brukerinnstillinger.md b/docs/features/brukerinnstillinger.md index 747ac87..ef25bc3 100644 --- a/docs/features/brukerinnstillinger.md +++ b/docs/features/brukerinnstillinger.md @@ -68,14 +68,11 @@ Eksempel: ``` **Hvorfor `users.settings` og ikke en egen tabell?** -- Innstillingene er per bruker, ikke per workspace +- Innstillingene er per bruker, ikke per samlings-node - JSONB er fleksibelt — nye innstillinger legges til uten migrering - Ingen relasjoner, ingen spørringer mot enkeltfelt — bare hent hele objektet -**Workspace-spesifikke overrides** kan legges i `workspace_members` ved behov: -```sql -ALTER TABLE workspace_members ADD COLUMN settings JSONB NOT NULL DEFAULT '{}'; -``` +**Samlings-node-spesifikke overrides** kan legges som edges med metadata mellom bruker og samlings-node ved behov. Men dette er fase 2. Start med globale brukerinnstillinger. ## 4. Frontend diff --git a/docs/features/chat.md b/docs/features/chat.md index a54a854..da783cb 100644 --- a/docs/features/chat.md +++ b/docs/features/chat.md @@ -5,7 +5,7 @@ En universell, sanntids meldingskomponent bygget på SpacetimeDB. Chat er ikke bundet til én kontekst — den kan knyttes til enhver node i Kunnskapsgrafen via **channels**. Ulike konsepter bruker chat med ulik konfigurasjon, men all infrastruktur er delt. ## 2. Channels-modellen -En **channel** er en meldingsstrøm knyttet til en vilkårlig node (parent) i grafen. Channelen er selv en node (`node_type = 'channel'`), noe som betyr at den deltar i grafen og arver workspace-isolasjon. +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. ``` ┌─────────────┐ parent_id ┌─────────────┐ @@ -50,7 +50,7 @@ Hver channel har en `config` (JSONB) som styrer hvilke capabilities den støtter | **Studioet** | "Studio-chat" | `threads: false, mentions: false, attachments: false, research_clips: false, ttl_days: 30` | | **Episode** | "Redaksjonelt" | `threads: true, mentions: true, attachments: true, research_clips: false, ttl_days: null` | -Presets er kun defaults — en workspace-admin kan justere config per channel. +Presets er kun defaults — en admin for samlings-noden kan justere config per channel. ### 2.3 Automatisk opprettelse Når en node opprettes som forventes å ha chat (Tema, Episode, Møte), oppretter systemet automatisk en default-channel. Dette skjer i SvelteKit server-side som en del av node-opprettelsestransaksjonen. @@ -135,13 +135,13 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s ### Gjenstår - **Vedlegg, TTL** — avventer implementering. -- **Workspace-partisjonering:** SpacetimeDB har `workspace_id` men bruker ikke token ennå. +- **Tilgangsfiltrering:** SpacetimeDB-laget må filtrere basert på `node_access`-matrisen. - **Pin/konvertering:** Går fortsatt direkte til PG API (ikke via SpacetimeDB). ## 11. Instruks for Claude Code -* **Opprettelsesrekkefølge:** Opprett `nodes`-rad → `channels`-rad → (for meldinger) `nodes`-rad → `messages`-rad. Alt i én transaksjon med riktig `workspace_id`. +* **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. -* **Alt er workspace-scopet.** Channels arver workspace via `nodes.workspace_id`. +* **Tilgang styres via `node_access`-matrisen.** Channels arver tilgang fra sin parent-node via edges. diff --git a/docs/features/kalender.md b/docs/features/kalender.md index 34df49d..5d942eb 100644 --- a/docs/features/kalender.md +++ b/docs/features/kalender.md @@ -9,7 +9,7 @@ Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i ### Implementert - Migrering `0003_calendar.sql`: `calendars` + `calendar_events` (begge FK→nodes) -- Hendelser er nodes — arver workspace-isolasjon automatisk +- 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 @@ -24,7 +24,7 @@ Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i - Dra-og-slipp for å flytte hendelser mellom datoer - Flerdag-hendelser (vises over flere celler) - Abonnementsmodell (kalender → kalender via graph_edges) -- Personlige vs. workspace-kalendere +- Personlige vs. delte kalendere (via samlings-noder) - ICS/CalDAV-eksport - SpacetimeDB-modul + hybrid-adapter - Varsler/påminnelser via jobbkøen @@ -84,15 +84,15 @@ Kalendere kan abonnere på andre kalendere via `SUBSCRIBES_TO`-edge i grafen. Ab | Type | Eier | Synlighet | |------|------|-----------| -| Workspace | Workspace (admin) | Alle medlemmer | +| Samlings-node | Admin for samlings-noden | Alle med tilgang via `node_access` | | Personlig | Bruker | Kun eier + eksplisitt deling | -| Offentlig | Workspace | Alle som abonnerer | +| Offentlig | Samlings-node | Alle som abonnerer | ## 8. Fremtidig: ICS-eksport -Offentlige kalendere får en ICS-URL (`/cal/{workspace_slug}/{calendar_id}.ics`) for Google Calendar, Apple Calendar etc. +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) -* Alt er workspace-scopet via node-modellen +* Tilgang styres via `node_access`-matrisen * Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi diff --git a/docs/features/kanban.md b/docs/features/kanban.md index 93b8349..46bb520 100644 --- a/docs/features/kanban.md +++ b/docs/features/kanban.md @@ -9,7 +9,7 @@ Et drag-and-drop Kanban-brett for planlegging. Primært brukt til episodeplanleg ### Implementert - Migrering `0002_kanban.sql`: `kanban_boards`, `kanban_columns`, `kanban_cards` -- Kanban-kort er nodes i kunnskapsgrafen (arver workspace-isolasjon) +- Kanban-kort er nodes i kunnskapsgrafen (tilgangsstyrt via `node_access`-matrise) - REAL-posisjon for midpoint-innsetting (`(1.0 + 2.0) / 2 = 1.5`) — ingen re-nummerering - REST API: GET brett, POST kolonne/kort, PATCH kort/flytt, DELETE kort - PG polling-adapter (`pg.svelte.ts`) med 5 sek intervall og optimistisk UI @@ -31,7 +31,7 @@ kanban_columns (id, board_id FK→kanban_boards, name, color, position REAL) kanban_cards (id FK→nodes, column_id FK→kanban_columns, title, description, assignee_id, position REAL, created_by, created_at) ``` -Kort og brett er nodes — all workspace-isolasjon arves automatisk via `nodes.workspace_id`. +Kort og brett er nodes — tilgang styres via `node_access`-matrisen. ## 4. API-endepunkter @@ -55,5 +55,5 @@ Kort og brett er nodes — all workspace-isolasjon arves automatisk via `nodes.w ## 6. Instruks for Claude Code * Bruk native HTML5 Drag and Drop i SvelteKit, unngå tunge biblioteker. * PG-adapter er autoritativ inntil SpacetimeDB-sync er på plass. -* Alt er workspace-scopet via node-modellen. +* Tilgang styres via `node_access`-matrisen. * Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi. diff --git a/docs/features/kunnskaps_bridge.md b/docs/features/kunnskaps_bridge.md index 8d00305..05a4470 100644 --- a/docs/features/kunnskaps_bridge.md +++ b/docs/features/kunnskaps_bridge.md @@ -1,14 +1,16 @@ -# Feature: Kunnskaps-Bridge (Cross-Workspace Discovery) +# Feature: Kunnskaps-Bridge (Cross-Context Discovery) **Filsti:** `docs/features/kunnskaps_bridge.md` +> **NB:** Dette dokumentet er en skisse fra v1 og må oppdateres til node/edge-modellen ved implementering. + ## 1. Konsept -En valgfri, opt-in feature som lar brukere oppdage semantisk beslektet kunnskap på tvers av workspaces de har tilgang til. Bryter ikke workspace-isolasjonen — resultatet er en *peker* ("dette finnes i Podcast B"), ikke selve innholdet. Brukeren må ha tilgang til begge workspaces for å se treffet. +En valgfri, opt-in feature som lar brukere oppdage semantisk beslektet kunnskap på tvers av samlings-noder de har tilgang til. Bryter ikke tilgangsstyringen — resultatet er en *peker* ("dette finnes i Podcast B"), ikke selve innholdet. Brukeren må ha tilgang til begge samlings-nodene via `node_access` for å se treffet. ## 2. Avgrensning: Hva dette IKKE er -- **Ikke datadeling.** Ingen data kopieres mellom workspaces. Bridge viser kun at en relevant node *finnes*. -- **Ikke automatisk.** Begge workspaces må ha Bridge eksplisitt aktivert av en admin. -- **Ikke synlig for gjester.** Kun for workspace-medlemmer med tilgang til begge sider. -- **Ikke et søk i andres data.** Du ser bare treff i workspaces du allerede er medlem av. +- **Ikke datadeling.** Ingen data kopieres mellom samlings-noder. Bridge viser kun at en relevant node *finnes*. +- **Ikke automatisk.** Begge samlings-noder må ha Bridge eksplisitt aktivert av en admin. +- **Ikke synlig for gjester.** Kun for brukere med tilgang til begge samlings-nodene. +- **Ikke et søk i andres data.** Du ser bare treff i samlings-noder du allerede har tilgang til. ## 3. Teknisk arkitektur @@ -35,32 +37,33 @@ En jobbkø-jobb som genererer embeddings for nye/endrede noder: 4. Lagrer vektoren i pgvector-kolonnen. 5. Re-genereres ved vesentlige endringer av noden. -### 3.3 Cross-workspace søk +### 3.3 Cross-context søk Når en bruker utforsker en node (f.eks. Tema "Skolepolitikk"): -1. SvelteKit server-side henter brukerens tilgjengelige workspaces fra `workspace_members`. -2. Kjører et similarity-søk med `<=>` (cosine distance) **som superuser** (bypasser RLS) — men filtrerer eksplisitt mot brukerens workspace-liste: +1. SvelteKit server-side henter brukerens tilgjengelige samlings-noder fra `node_access`-matrisen. +2. Kjører et similarity-søk med `<=>` (cosine distance), filtrert mot brukerens tilgjengelige noder: ```sql - SELECT n.workspace_id, t.name, t.embedding <=> $target_embedding AS distance - FROM topics t - JOIN nodes n ON t.id = n.id - WHERE n.workspace_id = ANY($user_workspace_ids) - AND n.workspace_id != $current_workspace_id - AND t.embedding <=> $target_embedding < 0.3 + SELECT na.node_id, e.name, e.embedding <=> $target_embedding AS distance + FROM entities e + JOIN nodes n ON e.id = n.id + JOIN node_access na ON n.id = na.node_id + WHERE na.user_id = $current_user + AND n.id != $current_node_id + AND e.embedding <=> $target_embedding < 0.3 ORDER BY distance LIMIT 10; ``` 3. Resultatet vises som en diskret "Finnes også i..."-seksjon i UI-et. -### 3.4 Workspace-config -Bridge aktiveres per workspace i `workspaces.settings`: +### 3.4 Samlings-node config +Bridge aktiveres per samlings-node i nodens metadata (JSONB): ```json { "bridge_enabled": true, "bridge_discoverable": true } ``` -- `bridge_enabled` — workspace kan søke i andre workspaces -- `bridge_discoverable` — andre workspaces kan finne noder i dette workspace-et +- `bridge_enabled` — samlings-noden kan søke i andre samlings-noder +- `bridge_discoverable` — andre samlings-noder kan finne noder under denne Begge må være `true` for at en kobling skal vises. @@ -72,7 +75,7 @@ Begge må være `true` for at en kobling skal vises. ## 5. Instruks for Claude Code * pgvector er en ny avhengighet — dokumenter i docker-compose og setup-guides. -* Cross-workspace søk er det **eneste** stedet i systemet der bruker-initierte handlinger bypasser RLS. Isolér denne koden grundig — egen funksjon med eksplisitt workspace-filtrering, aldri gjenbruk i andre kontekster. +* Cross-context søk bruker `node_access`-matrisen for tilgangsstyring. Isolér denne koden grundig — egen funksjon, aldri gjenbruk i andre kontekster. * Embedding-dimensjon (768) bør matche modellen som brukes. Konfigurér som konstant, ikke hardkod overalt. * Jobbtype `generate_embeddings` bruker `sidelinja/rutine` som modellalias. -* Bridge er **Lag 4+** — krever fylt kunnskapsgraf i minst to workspaces. +* Bridge er **Lag 4+** — krever fylt kunnskapsgraf i minst to samlings-noder. diff --git a/docs/features/kunnskapsgraf_og_relasjoner.md b/docs/features/kunnskapsgraf_og_relasjoner.md index 07906e9..f87cc3d 100644 --- a/docs/features/kunnskapsgraf_og_relasjoner.md +++ b/docs/features/kunnskapsgraf_og_relasjoner.md @@ -28,7 +28,6 @@ CREATE TYPE node_type AS ENUM ( CREATE TABLE nodes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, node_type node_type NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() @@ -86,12 +85,11 @@ CREATE INDEX idx_segments_transcript_fts ON segments USING GIN (to_tsvector('nor ``` ### 3.3 Kantene: `graph_edges` -All kobling skjer i én sentral tabell med ekte FK-integritet. `workspace_id` er denormalisert fra `nodes` for å muliggjøre RLS direkte på edges: +All kobling skjer i én sentral tabell med ekte FK-integritet: ```sql CREATE TABLE graph_edges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, relation_type TEXT NOT NULL, -- 'MENTIONS', 'CONTRADICTS', 'WORKS_FOR', 'PART_OF', 'DISCUSSED_IN' @@ -107,7 +105,6 @@ CREATE TABLE graph_edges ( CREATE INDEX idx_edges_source ON graph_edges(source_id); CREATE INDEX idx_edges_target ON graph_edges(target_id); CREATE INDEX idx_edges_relation ON graph_edges(relation_type); -CREATE INDEX idx_edges_workspace ON graph_edges(workspace_id); ``` **Merk:** `UNIQUE(source_id, target_id, relation_type)` forhindrer duplikate relasjoner. Bruk `ON CONFLICT DO NOTHING` eller `ON CONFLICT DO UPDATE SET confidence = ...` ved upsert. @@ -161,9 +158,9 @@ Grafen bygger seg opp organisk gjennom daglig bruk av Sidelinja-suiten: 5. **Publisering:** Når en episode publiseres, kobles segmentene automatisk til relevante Temaer og Aktører basert på AI-analyse av transkripsjonen. ## 6. Instruks for Claude Code -* **Workspace-isolasjon:** Alle noder tilhører en workspace. Opprett alltid noder med riktig `workspace_id`. Spørringer mot detailtabeller (actors, topics, etc.) filtrerer alltid via JOIN med nodes. +* **Tilgangsstyring:** Tilgang til noder styres via `node_access`-matrisen (materialized view). Spørringer mot detailtabeller (entities, etc.) filtrerer via JOIN med `node_access`. * **`nodes`-tabellen er obligatorisk.** Opprett alltid en rad i `nodes` før du inserter i en detailtabell. Bruk en hjelpefunksjon som gjør begge i én transaksjon. -* **`graph_edges` krever `workspace_id`.** Ved opprettelse av edges, sett `workspace_id` fra kilde-noden. `UNIQUE(source_id, target_id, relation_type)` hindrer duplikater — bruk `ON CONFLICT` ved upsert. +* **`graph_edges`-tilgang.** Tilgang til edges avledes fra tilgang til kilde- og målnoder via `node_access`. `UNIQUE(source_id, target_id, relation_type)` hindrer duplikater — bruk `ON CONFLICT` ved upsert. * **Graf-spørringer:** Bruk `WITH RECURSIVE` i PostgreSQL når du bygger endepunkter som skal hente ut "Linked Mentions" eller nettverket rundt en spesifikk Aktør opp til 2-3 ledd ut. * **Fremtidssikring for UI:** Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (som D3.js eller Vis.js) i Svelte-frontenden. Formatet bør være `{ "nodes": [...], "edges": [...] }`. * **Transkripsjon-reimport:** Workeren må være idempotent. Bruk `uuid_v5(episode_uuid, start_time_ms)` for deterministiske segment-UUIDs. Slett og gjenopprett segmenter i én transaksjon, men **ikke** slett edges som peker til segmentene — de overlever fordi UUID-en er stabil. **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. diff --git a/docs/features/live_ai.md b/docs/features/live_ai.md index c3b8bce..42bceab 100644 --- a/docs/features/live_ai.md +++ b/docs/features/live_ai.md @@ -63,4 +63,4 @@ Kill switch-status (`ai_enabled: bool`) lagres på LiveKit-rommet i SpacetimeDB * Møte-modus er asynkron — prosessér via jobbkøen etter møteslutt. * All AI-kode peker på `http://ai-gateway:4000/v1`, aldri direkte til leverandør. * Kill switch skal alltid være tilgjengelig i studio-modus — default: AI aktivert. -* Alt er workspace-scopet. +* Tilgang styres via `node_access`-matrisen. diff --git a/docs/features/live_transkripsjon.md b/docs/features/live_transkripsjon.md index ca5c8f0..58275a0 100644 --- a/docs/features/live_transkripsjon.md +++ b/docs/features/live_transkripsjon.md @@ -24,7 +24,7 @@ Den felles Whisper-pipelinen som brukes av flere konsepter for å transkribere l * **Benchmark og modellvalg:** Se `docs/concepts/podcastfabrikken.md` seksjon 4 for ytelsestall og anbefalinger. ### 4.1 initial_prompt (navneliste) -Brukes kun i batch-modus (Podcastfabrikken). Prompten bygges automatisk fra workspace-config (`workspaces.settings.whisper_prompt`) + aktører i workspace-ets kunnskapsgraf. +Brukes kun i batch-modus (Podcastfabrikken). Prompten bygges automatisk fra samlings-nodens metadata (`metadata.whisper_prompt`) + entiteter i kunnskapsgrafen. Effekten er tydelig: * Uten prompt: "Vegard Nøgnes", "SideLinja", "Sidlinja" @@ -32,9 +32,9 @@ Effekten er tydelig: ## 5. Lagring * **Live-modus:** Transkripsjonen er flyktig (kategori 4, TTL 30 dager). Lagres i `live_transcription_log` for feilsøking. -* **Batch-modus:** SRT committes til Git (Forgejo, ett repo per workspace). Avledede formater (ren tekst, segmenter, søkeindeks) i PostgreSQL. +* **Batch-modus:** SRT committes til Git (Forgejo). Avledede formater (ren tekst, segmenter, søkeindeks) i PostgreSQL. ## 6. Instruks for Claude Code * Live transkripsjon blokkerer ALDRI web-requests — prosesseres i Rust-worker eller separat tjeneste. * Batch-transkripsjon kjøres som `whisper_transcribe`-jobb i jobbkøen (se `docs/infra/jobbkø.md`). -* Workspace-spesifikk config (prompts) hentes fra `workspaces.settings`. +* Config (prompts) hentes fra samlings-nodens metadata (JSONB). diff --git a/docs/features/lydmeldinger.md b/docs/features/lydmeldinger.md index 6289716..79b9975 100644 --- a/docs/features/lydmeldinger.md +++ b/docs/features/lydmeldinger.md @@ -19,7 +19,7 @@ Voice-to-text er en del av chat-featuren (se `docs/features/chat.md`, seksjon 8) 2. Trykker record. Nettleseren fanger lyd via `MediaRecorder` API (WebM/Opus). 3. Kan valgfritt velge kontekst: et Tema, en Aktør, eller "Usortert". 4. Ved stopp lastes lydfilen opp til SvelteKit (streaming for store filer). -5. Filen lagres som `media_file` i workspace-mappen (`/srv/sidelinja/media/{workspace_slug}/voice/`). +5. Filen lagres som `media_file` i mediamappen (`/srv/synops/media/voice/`). 6. En `whisper_transcribe`-jobb opprettes i jobbkøen for å gjøre opptaket søkbart. 7. Transkripsjonen lagres som metadata på noden — lydfilen forblir master. @@ -27,7 +27,7 @@ Voice-to-text er en del av chat-featuren (se `docs/features/chat.md`, seksjon 8) Lydmeldingen er en node i Kunnskapsgrafen (`node_type = 'melding'`, `message_type = 'voice_memo'`): ``` -nodes (workspace_id, node_type = 'melding') +nodes (node_type = 'melding') → messages (channel_id, message_type = 'voice_memo', body = transkripsjon, metadata = { duration, transcription_status }) → message_attachments → media_files (lydfilen) ``` @@ -70,7 +70,7 @@ AI Gateway (`sidelinja/rutine`) prosesserer transkripsjonen med instruksjon om Dikterte notater er meldinger i en channel: ``` -nodes (workspace_id, node_type = 'melding') +nodes (node_type = 'melding') → messages (channel_id, message_type = 'text', body = ryddet tekst, metadata = { raw_transcript, source: 'dictation' }) ``` @@ -100,7 +100,7 @@ Begge moduser bruker live transkripsjonspipelinen (se `docs/features/live_transk - Diktering: `medium` + `initial_prompt` (kvalitet prioritert — teksten er master) ### 4.4 Personlig "Innboks"-channel -Ved workspace-opprettelse opprettes en privat channel per bruker for usorterte lydmeldinger og notater. Config: +Ved brukeropprettelse opprettes en privat channel per bruker for usorterte lydmeldinger og notater. Config: ```json { @@ -130,8 +130,8 @@ Ved workspace-opprettelse opprettes en privat channel per bruker for usorterte l ## 7. Instruks for Claude Code * Opptakskomponenten skal være en gjenbrukbar Svelte-komponent med modus-prop (`voice_memo` / `dictation`). -* Lydfiler lagres i `media/{workspace_slug}/voice/` — aldri i databasen. +* Lydfiler lagres i `media/voice/` — aldri i databasen. * Diktering: slett lydfilen først *etter* at brukeren har godkjent den ryddede teksten. * `voice_memo` er en ny `message_type` — utvid enum i migrasjonen. -* Personlig innboks-channel opprettes automatisk ved workspace-medlemskap (som del av `workspace_members`-inserten). -* Alt er workspace-scopet. +* Personlig innboks-channel opprettes automatisk for nye brukere. +* Tilgang styres via `node_access`-matrisen. diff --git a/docs/features/meldingsboks.md b/docs/features/meldingsboks.md index 5136206..3130d92 100644 --- a/docs/features/meldingsboks.md +++ b/docs/features/meldingsboks.md @@ -50,7 +50,7 @@ CREATE TABLE messages ( body TEXT NOT NULL, metadata JSONB, -- Ekstra data per message_type pinned BOOLEAN NOT NULL DEFAULT false, - visibility TEXT NOT NULL DEFAULT 'workspace', -- 'workspace' | 'private' + visibility TEXT NOT NULL DEFAULT 'shared', -- 'shared' | 'private' edited_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() @@ -65,11 +65,11 @@ CREATE TRIGGER trg_messages_updated_at BEFORE UPDATE ON messages **Forskjeller fra gammel `messages`-tabell:** - `id` er FK til `nodes(id)` — **alle meldinger er noder** i kunnskapsgrafen -- Ingen `workspace_id` — arves via `nodes.workspace_id` (RLS på nodes gjelder) +- Ingen `workspace_id` — tilgang styres via `node_access`-matrisen - `channel_id` er nullable — notater og standalone-bokser trenger ikke en channel - `title` er førsteklasses felt — brukes av kanban-kort, notater, kalenderhendelser - `pinned` for manuell fritak fra TTL-sletting -- `visibility` styrer synlighet — `'workspace'` (alle i workspacet) eller `'private'` (kun forfatter). Private meldingsbokser kan brukes som kladd for notater, kanban-kort og kalenderhendelser. Endre til `'workspace'` for å dele. +- `visibility` styrer synlighet — `'shared'` (alle med tilgang via `node_access`) eller `'private'` (kun forfatter). Private meldingsbokser kan brukes som kladd for notater, kanban-kort og kalenderhendelser. Endre til `'shared'` for å dele. ### 3.1.1 Kontekst-navigering via `reply_to` @@ -219,7 +219,7 @@ Ved nivå 3 tilbyr systemet: **"Skill ut som egen diskusjon?"** ## 8. Eierskap, kurasjon og prominens ### 8.1 Eierskap -Trådstarter og workspace-admin deler eierskap over en diskusjonstråd. Eierskap gir tilgang til kurasjonsverktøy (se 7.2). +Trådstarter og admin for samlings-noden deler eierskap over en diskusjonstråd. Eierskap gir tilgang til kurasjonsverktøy (se 7.2). ### 8.2 Kurasjon (TODO — UI-features, bygges inkrementelt) Datamodellen trenger ikke endres for disse — alt håndteres via `messages.metadata` (JSONB): @@ -240,7 +240,7 @@ Beregnes ved visning eller caches i materialized view ved behov. Algoritmen kan ## 9. TTL og livsløp ### 9.1 To-trinns fading -1. **Skjult fra visning** — meldingen forsvinner fra default UI etter TTL (arvet fra channel/workspace). `messages.metadata.hidden_at` settes. +1. **Skjult fra visning** — meldingen forsvinner fra default UI etter TTL (arvet fra channel eller samlings-node). `messages.metadata.hidden_at` settes. 2. **Slettet** — etter tilleggsperiode (dobbel TTL) fjernes raden permanent. ### 9.2 Alder som dynamisk faktor @@ -260,7 +260,7 @@ Lever boksen, lever alt under den — svar beholdes uansett alder. ### 9.4 Konfigurerbarhet ``` -Workspace-default TTL: 30 dager (workspaces.settings.default_ttl_days) +Samlings-node default TTL: 30 dager (samlings-node metadata.default_ttl_days) └── Channel kan overstyre: config.ttl_days └── Individuelle meldinger frittes via reglene over ``` @@ -311,9 +311,9 @@ Migrasjonen konverterer eksisterende data: - **Kanban-kort:** INSERT i `nodes` + `messages` (med tittel) + `kanban_card_view`. Én transaksjon. - **Kalenderhendelse:** INSERT i `nodes` + `messages` (med tittel) + `calendar_event_view`. Én transaksjon. - **Faktoide:** INSERT i `nodes` + `messages` (`message_type = 'factoid'`) + `graph_edges` med `ABOUT`-relasjon til aktør/tema. Én transaksjon. -- **Notat:** INSERT i `nodes` + `messages` (med tittel + body). `channel_id` peker på en personal/workspace channel eller er NULL. +- **Notat:** INSERT i `nodes` + `messages` (med tittel + body). `channel_id` peker på en personlig eller delt channel, eller er NULL. - **Konvertering mellom roller:** Legg til view-config-rad (kanban/kalender). Aldri kopier data. - **Kontekst-navigering:** Bruk `reply_to`-kjeden for å bygge brødsmulesti tilbake til opprinnelig diskusjonskontekst. - **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. -- **RLS:** Workspace-isolasjon arves fra `nodes.workspace_id`. Visibility håndteres i applikasjonskode (SvelteKit): `WHERE visibility = 'workspace' OR author_id = $current_user`. Ikke i RLS — RLS håndterer kun workspace-grenser. -- **Visibility:** Default `'workspace'`. Sett `'private'` for personlige kladder. Endre til `'workspace'` for å dele — ingen kopiering nødvendig. +- **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. diff --git a/docs/features/notater.md b/docs/features/notater.md index d926b34..65065bd 100644 --- a/docs/features/notater.md +++ b/docs/features/notater.md @@ -9,7 +9,7 @@ Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike ko ### Implementert - Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes) -- Notater er nodes — arver workspace-isolasjon automatisk +- 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) @@ -62,7 +62,7 @@ CREATE TABLE notes ( - Polling (10 sek) henter siste versjon, men hopper over overskriving mens `saving`-flagget er satt ## 7. Instruks for Claude Code -* Notater er workspace-scopet via node-modellen +* 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 diff --git a/docs/features/podcast_statistikk.md b/docs/features/podcast_statistikk.md index b3823a4..9e39163 100644 --- a/docs/features/podcast_statistikk.md +++ b/docs/features/podcast_statistikk.md @@ -5,13 +5,13 @@ IAB-kompatibel lytterstatistikk bygget fra bunnen av. Vi fanger all rådata via Caddy, og bruker asynkron batch-prosessering for å bygge grafer og tall uten å belaste webserveren eller databasen med sanntids-skriving. ## 2. Arkitektur & Dataflyt -1. **Rådata (Caddy):** Caddy konfigureres til å skrive access-logs for stien `/media/podcast/*.mp3` til en formatert JSON-fil (f.eks. `/srv/sidelinja/logs/caddy/podcast_access.log`). +1. **Rådata (Caddy):** Caddy konfigureres til å skrive access-logs for stien `/media/podcast/*.mp3` til en formatert JSON-fil (f.eks. `/srv/synops/logs/caddy/podcast_access.log`). 2. **Logrotate:** Standard Linux logrotate arkiverer loggene nattlig. 3. **Rust Batch Processor (Jobbkø):** Statistikkparseren kjøres som en `stats_parse`-jobb i den felles jobbkøen (se `docs/infra/jobbkø.md`), med `scheduled_for` satt 1 time frem for periodisk kjøring. Workeren re-enqueuer seg selv ved fullføring. * **Steg A (Filtrering):** Leser JSON-loggen. Fjerner treff fra kjente bots ved å krysjekke `User-Agent` mot OPAWG (Open Podcast Analytics Working Group) sine åpne bot-lister. * **Steg B (Deduplisering):** Slår sammen byte-range forespørsler. Hvis samme IP og User-Agent har lastet ned deler av samme fil innenfor et 24-timers vindu, telles det som KUN én (1) nedlasting. * **Steg C (Geografi/Klient):** Mapper User-Agent til Podcast-klient (Spotify, Apple) basert på OPAWG-regler. -4. **Lagring (PostgreSQL):** Rust-programmet skriver det aggregerte resultatet inn i PostgreSQL (`episode_stats` tabell med felter for `workspace_id`, `date`, `episode_id`, `client_name`, `unique_downloads`). Workspace-tilhørighet utledes fra filstien i loggen (`/media/{workspace_slug}/...`). +4. **Lagring (PostgreSQL):** Rust-programmet skriver det aggregerte resultatet inn i PostgreSQL (`episode_stats` tabell med felter for `tenant_id`, `date`, `episode_id`, `client_name`, `unique_downloads`). Tenant-tilhørighet utledes fra filstien i loggen (`/media/{tenant_slug}/...`). ## 3. Personvern og GDPR @@ -27,4 +27,4 @@ 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 workspace-scopet. `episode_stats` merkes med `workspace_id`. Admin-visningen filtrerer per workspace. \ No newline at end of file +* Statistikk er tenant-scopet. `episode_stats` merkes med `tenant_id`. Admin-visningen filtrerer per tenant. \ No newline at end of file diff --git a/docs/features/prompt_lab.md b/docs/features/prompt_lab.md index 213ef4e..01c9c9d 100644 --- a/docs/features/prompt_lab.md +++ b/docs/features/prompt_lab.md @@ -1,8 +1,10 @@ # Feature: Prompt-Laboratorium **Filsti:** `docs/features/prompt_lab.md` +> **NB:** Dette dokumentet er en skisse fra v1 og må oppdateres til node/edge-modellen ved implementering. + ## 1. Konsept -Et internt kvalitetssikringsverktøy der redaksjonen kan teste, sammenligne og godkjenne LLM-prompts mot faktiske data fra eget workspace — før de ruller ut i produksjon. Integrert med AI Gateway (LiteLLM) og Promptfoo-testsettene. +Et internt kvalitetssikringsverktøy der redaksjonen kan teste, sammenligne og godkjenne LLM-prompts mot faktiske data fra egen samlings-node — før de ruller ut i produksjon. Integrert med AI Gateway (LiteLLM) og Promptfoo-testsettene. ## 2. Problemet det løser - Modellkvalitet på norsk varierer kraftig mellom leverandører og versjoner. @@ -14,9 +16,9 @@ Et internt kvalitetssikringsverktøy der redaksjonen kan teste, sammenligne og g ### 3.1 Playground (Ad-hoc testing) 1. Bruker velger en **jobbtype** (research_clip, whisper_postprocess, metadata_extract, dictation_cleanup, etc.). -2. Systemet laster inn gjeldende system-prompt fra `workspaces.settings`. +2. Systemet laster inn gjeldende system-prompt fra samlings-nodens metadata. 3. Bruker kan redigere prompten i et tekstfelt. -4. Velger **testdata**: enten paste inn tekst manuelt, eller velg fra faktiske transkripsjoner/artikler i workspace-et. +4. Velger **testdata**: enten paste inn tekst manuelt, eller velg fra faktiske transkripsjoner/artikler brukeren har tilgang til. 5. Velger **modeller** å teste mot (f.eks. `sidelinja/rutine` + `sidelinja/resonering`). 6. Kjører testen — ser resultatene side-om-side. 7. Kan iterere: endre prompten, kjør igjen, sammenlign. @@ -29,8 +31,8 @@ Et internt kvalitetssikringsverktøy der redaksjonen kan teste, sammenligne og g ### 3.3 Deploy Når en prompt er verifisert: -1. Bruker klikker "Deploy til workspace". -2. Prompten skrives til `workspaces.settings.llm_prompts[jobbtype]`. +1. Bruker klikker "Deploy". +2. Prompten skrives til samlings-nodens metadata (`metadata.llm_prompts[jobbtype]`). 3. Alle fremtidige jobber av den typen bruker den nye prompten. 4. Tidligere prompt lagres i en `prompt_history`-logg for rollback. @@ -41,7 +43,7 @@ Når en prompt er verifisert: ```sql prompt_test_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + context_node UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- samlings-node job_type TEXT NOT NULL, system_prompt TEXT NOT NULL, model_alias TEXT NOT NULL, @@ -53,7 +55,7 @@ prompt_test_runs ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE INDEX idx_prompt_runs_workspace ON prompt_test_runs(workspace_id, job_type, created_at DESC); +CREATE INDEX idx_prompt_runs_context ON prompt_test_runs(context_node, job_type, created_at DESC); ``` `prompt_test_runs` er **ikke** en node i grafen — det er en intern verktøytabell for utviklere/redaktører. @@ -63,7 +65,7 @@ CREATE INDEX idx_prompt_runs_workspace ON prompt_test_runs(workspace_id, job_typ ```sql prompt_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + context_node UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- samlings-node job_type TEXT NOT NULL, system_prompt TEXT NOT NULL, deployed_by TEXT NOT NULL REFERENCES users(authentik_id), @@ -92,10 +94,10 @@ Muliggjør rollback: "forrige prompt for research_clip fungerte bedre, rull tilb | `prompt_eval` | (varierer) | Batch-evaluering av testsett mot valgte modeller | ## 7. Instruks for Claude Code -* Prompt Lab er et admin-verktøy — krev `admin` eller `owner`-rolle i workspace. +* Prompt Lab er et admin-verktøy — krev admin-tilgang til samlings-noden. * Ad-hoc tester kjøres synkront (SvelteKit → AI Gateway → respons). Ikke bruk jobbkø for enkelt-tester. * Batch-evaluering kjøres via jobbkø for å unngå timeouts. * Vis alltid token-bruk og latens — dette er et kostnadsbevisst verktøy. -* Testdata fra workspace-et (transkripsjoner, artikler) lastes via SvelteKit server-side fra PG. Gjesten-UX vises aldri her. -* `prompt_test_runs` og `prompt_history` er workspace-scopet men trenger ikke RLS — de er kun tilgjengelige for admins via applikasjonslogikk. -* Alt er workspace-scopet. +* 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. diff --git a/docs/features/universell_overfoering.md b/docs/features/universell_overfoering.md index 14a5cbb..d7696d0 100644 --- a/docs/features/universell_overfoering.md +++ b/docs/features/universell_overfoering.md @@ -88,7 +88,7 @@ Høyreklikk kort → "Send til..." → └── 🎬 Storyboard: Episode 47 ``` -Listen viser alle blokker i det aktive workspacet som kan motta meldinger. +Listen viser alle blokker brukeren har tilgang til som kan motta meldinger. ### 3.3 Flytt vs. kopier @@ -148,7 +148,6 @@ pub struct MessagePlacement { pub context_id: String, pub entered_at: Timestamp, pub position_json: String, // JSON-serialisert posisjon - pub workspace_id: String, } #[reducer] diff --git a/docs/features/visuell_graf.md b/docs/features/visuell_graf.md index 53671f8..591a4d9 100644 --- a/docs/features/visuell_graf.md +++ b/docs/features/visuell_graf.md @@ -17,4 +17,4 @@ En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt naviger ## 4. Instruks for Claude Code * Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (D3.js, Vis.js). * Begrens traversering til 2-3 ledd ut fra startnode for å unngå eksplosjoner i store grafer. -* Workspace-scopet: alle noder filtreres via `nodes.workspace_id`. +* Tilgangsstyrt: noder filtreres via `node_access`-matrisen. diff --git a/docs/features/whiteboard.md b/docs/features/whiteboard.md index 72c8b69..58f9e91 100644 --- a/docs/features/whiteboard.md +++ b/docs/features/whiteboard.md @@ -14,7 +14,7 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst ## 3. Sanntidssynkronisering * **SpacetimeDB** synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid. -* Hvert whiteboard har en unik ID og tilhører en workspace. +* 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. ## 4. Funksjonalitet @@ -24,11 +24,11 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst ## 5. Lagring og Eksport * **Sanntidsdata (SpacetimeDB):** Strøk-historikk holdes i minnet så lenge tavlen er aktiv. -* **Eksport (PostgreSQL + filsystem):** Når tavlen "lukkes" eller deles, rendres den til PNG eller SVG og lagres som en `media_file` i workspace-mappen. Referansen knyttes til konteksten (melding, møte) via `message_attachments` eller tilsvarende. +* **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`. * Ikke bygg et fullverdig tegneprogram — start med frihåndstegning, viskelær og farger. Utvid ved behov. -* Alle whiteboards er workspace-scopet. +* Tilgang til whiteboards styres via `node_access`-matrisen. diff --git a/docs/infra/ai_gateway.md b/docs/infra/ai_gateway.md index c8e046d..d510054 100644 --- a/docs/infra/ai_gateway.md +++ b/docs/infra/ai_gateway.md @@ -2,7 +2,7 @@ **Filsti:** `docs/infra/ai_gateway.md` ## 1. Konsept -Sidelinja bruker en sentralisert AI Gateway (LiteLLM) som eneste kontaktpunkt for alle AI-kall i systemet. All kode — Rust-workers, SvelteKit server-side — snakker med `http://ai-gateway:4000/v1`. Aldri direkte til leverandør-APIer. +Synops bruker en sentralisert AI Gateway (LiteLLM) som eneste kontaktpunkt for alle AI-kall i systemet. All kode — Rust-workers, SvelteKit server-side — snakker med `http://ai-gateway:4000/v1`. Aldri direkte til leverandør-APIer. Fordeler: * **BYOK (Bring Your Own Key):** Direkte API-nøkler til Anthropic, Google, xAI — ingen markup @@ -36,7 +36,7 @@ PostgreSQL er single source of truth for all modellkonfigurasjon. LiteLLM er en ### 3.2 Datamodell ```sql --- Globale modellaliaser (server-nivå, ikke per workspace) +-- Globale modellaliaser (server-nivå) CREATE TABLE ai_model_aliases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), alias TEXT NOT NULL, -- 'sidelinja/rutine', 'sidelinja/resonering' @@ -180,14 +180,14 @@ tests/prompts/ ## 6. Tokenregnskap og kostnadskontroll -### 6.1 Token-logging per workspace +### 6.1 Token-logging per samlings-node Rust-workeren logger tokenforbruk etter hvert AI-kall. Dataen lagres i PG: ```sql CREATE TABLE ai_usage_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + collection_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, job_id UUID REFERENCES job_queue(id) ON DELETE SET NULL, model_alias TEXT NOT NULL, -- 'sidelinja/rutine' model_actual TEXT, -- 'gemini/gemini-2.5-flash' (fra LiteLLM-respons) @@ -199,30 +199,30 @@ CREATE TABLE ai_usage_log ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE INDEX idx_ai_usage_workspace_month ON ai_usage_log (workspace_id, created_at); +CREATE INDEX idx_ai_usage_collection_month ON ai_usage_log (collection_node_id, created_at); ``` **Flyten:** 1. Rust-worker sender AI-kall via gateway, får tilbake `usage` i responsen -2. Worker skriver rad til `ai_usage_log` med workspace_id, tokens og modellinfo +2. Worker skriver rad til `ai_usage_log` med collection_node_id, tokens og modellinfo 3. Estimert kostnad beregnes fra en enkel prisliste i config (oppdateres manuelt) -### 6.2 Visning — to nivåer +### 6.2 Visning -- to nivåer **Admin (`/admin/ai`):** -Aggregert oversikt over alle workspaces. Tabell med totaler per workspace/modell/periode. Identifiserer kostnadsdrivere. +Aggregert oversikt over alle samlings-noder. Tabell med totaler per samlings-node/modell/periode. Identifiserer kostnadsdrivere. -**Workspace (sidebar-widget):** -Enkel tekst-indikator i workspace-sidebar: `✨ 12.4k tokens denne uken`. Klikk åpner detaljert visning med fordeling per jobbtype og modell. Ingen speedometer — det krever et definert budsjett for å gi mening, og det er overkill for MVP. +**Samlings-node (sidebar-widget):** +Enkel tekst-indikator i sidebar: `12.4k tokens denne uken`. Klikk åpner detaljert visning med fordeling per jobbtype og modell. Ingen speedometer -- det krever et definert budsjett for å gi mening, og det er overkill for MVP. -### 6.3 Workspace-budsjett (fase 2) +### 6.3 Budsjett per samlings-node (fase 2) Når token-logging er på plass, kan budsjett-tak legges til: -- Budsjett lagres i `workspaces.settings` (JSONB): `{ "ai_budget": { "monthly_limit_usd": 50 } }` +- Budsjett lagres som JSONB-metadata på samlings-noden: `{ "ai_budget": { "monthly_limit_usd": 50 } }` - Rust-worker sjekker aggregert forbruk før AI-kall - Ved budsjett nær: fall tilbake til `sidelinja/rutine` (billigste) -- Ved budsjett nådd: sett jobb i `paused` med varsel i workspace-chat +- Ved budsjett nådd: sett jobb i `paused` med varsel i samlings-nodens chat ### 6.4 Per-episode maks-kostnad Podcastfabrikken-jobber (whisper + metadata + oppsummering) kan estimere totalkostnad basert på lydlengde. Jobben avbrytes med varsel hvis estimert kostnad overstiger `max_cost_per_episode` (default: $5). diff --git a/docs/infra/jobbkø.md b/docs/infra/jobbkø.md index 498e2c6..eae8f65 100644 --- a/docs/infra/jobbkø.md +++ b/docs/infra/jobbkø.md @@ -18,7 +18,7 @@ CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'ret CREATE TABLE job_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + collection_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- Samlings-node jobben tilhører job_type TEXT NOT NULL, -- 'whisper_transcribe', 'openrouter_analyze', 'stats_parse', 'research_clip' payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.) status job_status NOT NULL DEFAULT 'pending', @@ -120,11 +120,11 @@ Verdiene er veiledende — SvelteKit setter prioritet ved opprettelse basert på | `url_ingest` | Web Clipper (proposal) | Hent URL, oppsummer via AI, opprett research-klipp med graf-koblinger | | `generate_waveform` | Waveforms (proposal) | Generer audio-peaks fra lydfil for visuell bølgeform | -## 6. Workspace-isolasjon -Alle jobber merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode: -* Worker leser `workspace_id` fra jobben og bruker det til å lagre resultater tilbake i riktig silo -* Workspace-spesifikk config (AI-prompts, navnelister) hentes fra `workspaces.settings` -* Feilede jobber vises kun for brukere i riktig workspace i admin-visningen +## 6. Tilgangsisolasjon +Alle jobber merkes med `collection_node_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode: +* Worker leser `collection_node_id` fra jobben og bruker det til å lagre resultater tilbake i riktig samlings-node +* Per samlings-node config (AI-prompts, navnelister) hentes fra samlings-nodens JSONB-metadata +* Feilede jobber vises kun for brukere med tilgang til samlings-noden (via node_access) i admin-visningen ## 7. Observabilitet - Jobber med `status = 'error'` skal være synlige i admin-visningen (SvelteKit `/admin/jobs`) @@ -135,6 +135,6 @@ Alle jobber merkes med `workspace_id`. Rust-workers kjører som superuser (bypas - Hver jobbtype implementeres som en handler-funksjon som registreres i en `HashMap` - Bruk `tokio` med semaphore for concurrency-kontroll (`--max-concurrent`) - Aldri lagre lydfiler i `payload` — bruk filstier -- Opprett alltid jobber med riktig `workspace_id` — hent fra konteksten (innlogget bruker, webhook, etc.) +- 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 diff --git a/docs/infra/synkronisering.md b/docs/infra/synkronisering.md index aef0fd5..cb4011f 100644 --- a/docs/infra/synkronisering.md +++ b/docs/infra/synkronisering.md @@ -1,176 +1,106 @@ -# Infrastruktur: PostgreSQL ↔ SpacetimeDB Synkronisering -**Filsti:** `docs/infra/synkronisering.md` - -## 1. Konsept -SpacetimeDB gir sanntidsopplevelsen, PostgreSQL er langtidsminnet. Denne spec-en definerer hvordan data flyter mellom dem, hvem som eier sannheten, og hva som skjer ved feil. - -### 1.1 Grunnregel: SpacetimeDB er en ren sanntidsbuffer -SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten: -1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller -2. Synkes til PostgreSQL innen ~1 sekund (chat, kanban, markører). - -**Konsekvens for design:** Ethvert SpacetimeDB-modul-skjema skal ha en tilsvarende PostgreSQL-tabell som kan fungere som drop-in erstatning dersom SpacetimeDB fjernes. Frontend-kode som leser fra SpacetimeDB skal kunne peke om til et SvelteKit SSE-endepunkt + REST uten arkitekturendring. - -### 1.2 PostgreSQL-fallback-prinsippet -Dersom SpacetimeDB fjernes fra stacken, skal systemet fungere med følgende erstatning: -- **Sanntidsoppdateringer:** PostgreSQL `LISTEN/NOTIFY` → SvelteKit Server-Sent Events (SSE) -- **Skriving:** Direkte til PostgreSQL via SvelteKit server-side -- **Autocomplete/cache:** PostgreSQL-spørringer med cursor-basert paginering - -Denne fallbacken trenger ikke implementeres på forhånd, men SpacetimeDB-moduler skal designes slik at fallbacken forblir triviell. - -## 2. Strategi: Event-drevet med kort forsinkelse -SpacetimeDB-modulene (Rust) produserer persisterings-events ved dataendringer. En Rust-worker konsumerer disse og skriver til PostgreSQL med ~1 sekunds intervall. - -**Akseptabelt datatap:** Maks 1 sekund ved hard krasj av SpacetimeDB. Dette er akseptabelt for chat, kanban og show notes. - -**Unntak — kritiske events:** Aha-markører fra studioet (live-innspilling) er tidssensitive og vanskelige å gjenskape. Disse bør flushes til PG umiddelbart (ikke batched) via en dedikert `sync_critical()`-funksjon som skriver direkte til PG i stedet for via `sync_outbox`. Alternativt kan SpacetimeDB-modulen skrive kritiske events til sin egen WAL/disk umiddelbart. Hvilke event-typer som er "kritiske" defineres per workspace i `workspaces.settings`. - -## 3. Dataflyt - -``` -┌──────────────┐ events ┌──────────────┐ batch write ┌──────────────┐ -│ SpacetimeDB │ ──────────────► │ Rust Worker │ ────────────────► │ PostgreSQL │ -│ (sanntid) │ │ (sync_to_pg) │ │ (persistent)│ -└──────────────┘ └──────────────┘ └──────────────┘ - -┌──────────────┐ oppvarming (oppstart / reconnect) ┌──────────────┐ -│ PostgreSQL │ ──────────────────────────────────────► │ SpacetimeDB │ -└──────────────┘ └──────────────┘ -``` - -## 4. Eierskapsmodell - -| Data | Autoritativ kilde | Synkretning | Merknad | -|---|---|---|---| -| Chatmeldinger | SpacetimeDB | → PG (event, batched) | | -| Kanban-posisjon | SpacetimeDB | → PG (event) | | -| Show notes | SpacetimeDB | → PG (event) | | -| Live studio-markører | SpacetimeDB | → PG (event) | | -| Kunnskapsgraf | PostgreSQL | → SpacetimeDB (oppvarming) | Read-only i SpacetimeDB | -| Episodemetadata | PostgreSQL | Ingen synk | | -| Brukerkontoer | PostgreSQL (Authentik) | Ingen synk | | -| Statistikk | PostgreSQL | Ingen synk | | -| Valgomat | TBD | TBD | Konseptet må modnes. Mulig PG-autoritativ med SpacetimeDB som serveringslag | - -## 5. Mekanisme - -### 5.1 SpacetimeDB → PostgreSQL (persistering) -- SpacetimeDB-reducerne legger sync-events i `sync_outbox`-tabellen med tidsstempel og payload -- Rust-workeren poller `sync_outbox` hvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket via `mark_synced`-reducer -- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører - -### 5.2 PostgreSQL → SpacetimeDB (oppvarming) -- Ved oppstart laster Rust-workeren data fra PG per kanal, basert på `channels.config`: - - `warmup_mode: "all"` — alle meldinger i kanalen - - `warmup_mode: "messages"` — de N nyeste *trådene* (komplett med svar) - - `warmup_mode: "days"` — alle tråder med aktivitet siste N dager (komplett) - - `warmup_mode: "none"` — kanalen hoppes over (arkivert/inaktiv) -- Trådbasert henting: et svar som kvalifiserer tar med hele tråden (inkludert eldre trådstarter) -- Konfigureres per kanal via admin-UI (`/admin/channels`) eller `channels.config` JSONB -- Kanalen ryddes (`clear_channel`) før lasting for å unngå duplikater ved restart -- Reaksjoner lastes også inn via `load_reactions`-reducer - -## 6. Feilhåndtering -- **SpacetimeDB krasjer:** Data siden siste synk (~1 sek) tapes. Ved restart oppvarmes fra PG -- **PostgreSQL nede:** Sanntidsfunksjoner fortsetter å fungere. `sync_outbox` vokser. Workeren logger advarsler. Ved PG-recovery synkes backloggen automatisk -- **Rust-worker krasjer:** `sync_outbox` akkumuleres. Ved restart plukker workeren opp der den slapp (usynkede events har ingen markering) - -## 7. Workspace-isolasjon -SpacetimeDB-synkronisering er fullstendig workspace-scopet: -* SpacetimeDB-tilkoblinger bærer `workspace_id` som context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace. -* `sync_outbox`-events inkluderer `workspace_id` i payloaden, slik at Rust-workeren skriver til riktig silo i PostgreSQL. -* Ved oppvarming (PG → SpacetimeDB) laster workeren kun data for den aktuelle workspace-en. - -## 8. Konflikthåndtering - -### 8.1 Strategi: Last-Write-Wins (LWW) med reservasjoner -SpacetimeDB er autoritativ for sanntidsdata. Konflikter kan oppstå i to scenarioer: - -**Scenario A — Samtidig redigering i SpacetimeDB:** -SpacetimeDB er single-threaded per modul. Reducer-funksjoner serialiseres automatisk. Ingen klassiske race conditions. - -**Scenario B — PG-oppvarming kolliderer med fersk SpacetimeDB-state:** -Ved restart av SpacetimeDB lastes data fra PG (oppvarming). Hvis en klient skriver til SpacetimeDB *under* oppvarming, kan PG-dataen overskrive ferske endringer. - -**Løsning:** Oppvarming setter et `warming_up`-flagg i SpacetimeDB-modulen. Klienttilkoblinger aksepteres, men skrivinger bufres til oppvarmingen er fullført. Buffrede skrivinger appliseres deretter i rekkefølge. - -### 8.2 Kanban: Posisjonskonflikter -Kanban-kort har en `position`-kolonne (float). To brukere som drar kort samtidig kan skape overlappende posisjoner. Løsning: SpacetimeDB-modulen re-beregner posisjoner (midtpunkts-strategi) og kringkaster oppdatert rekkefølge til alle klienter. - -### 8.3 Chat: Ingen konflikter -Meldinger er append-only. Redigering av egne meldinger er last-write-wins — akseptabelt fordi kun én bruker eier meldingen. - -## 9. Workers som endrer data frontend ser - -**Kritisk regel:** En worker (Rust) som transformerer data som frontend viser, MÅ skrive resultatet til SpacetimeDB — ikke direkte til PG. PG-oppdatering skjer via sync-worker i bakgrunnen. - -### Eksempel: AI-vask av chatmelding - -**Riktig flyt:** -``` -Frontend viser melding fra SpacetimeDB - → Bruker trykker ✨ - → SvelteKit oppretter jobb i PG job_queue - → Worker plukker opp jobb - → Worker leser prompt + routing fra PG (det er infrastrukturdata, ikke frontend-data) - → Worker kaller AI Gateway - → Worker skriver resultat til SpacetimeDB via edit_message reducer - → SpacetimeDB pusher oppdatering til frontend via onUpdate - → Sync-worker persisterer til PG i bakgrunnen -``` - -**Feil flyt (anti-pattern som har blitt implementert gjentatte ganger):** -``` -Worker skriver til PG direkte - → Frontend henter metadata fra PG via enrichFromPg() ← BRYTER ABSTRAKSJONEN - → SpacetimeDB-oppdatering er "best-effort" ← FEIL PRIORITERING -``` - -### Hva betyr dette for nye felter? - -Når frontend trenger å vise noe nytt (metadata, revisjoner, edited_at), er prosedyren: - -1. **Utvid SpacetimeDB-modulen** — legg til feltet i Rust-strukturen -2. **Utvid warmup** — last feltet fra PG ved oppstart -3. **Utvid sync** — persist feltet til PG i bakgrunnen -4. **Worker skriver til SpacetimeDB** — via reducer, aldri direkte PG for synlig data -5. **Frontend leser fra SpacetimeDB** — ingen enrichFromPg, ingen PG API-kall - -### Hva kan workers lese fra PG? - -Workers kan og bør lese infrastrukturdata direkte fra PG: -- Jobbkø (`job_queue`) — det er jobbens opphavssted -- AI-prompts, modellkonfigurasjon, routing — det er infrastruktur, ikke brukerdata -- Workspace-konfigurasjon - -Skillet er: **data frontend viser** → SpacetimeDB. **Data kun worker trenger** → PG direkte. - -## 10. Implementeringsstatus (mars 2026) - -### Ferdig -- **SpacetimeDB som cache foran PG:** PG er autoritativ, SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB. -- **SpacetimeDB Rust-modul** (`spacetimedb/src/lib.rs`): `ChatMessage` (med `metadata`, `edited_at`), `MessageReaction`, `MessageRevision` og `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `load_revisions`, `clear_channel`, `mark_synced`, `set_ai_processing`, `clear_ai_processing`, `ai_update_message`. -- **Worker warmup** (`worker/src/warmup.rs`): Ved oppstart lastes meldinger (med metadata/edited_at), reaksjoner og revisjoner per kanal fra PG → SpacetimeDB. Originale timestamps parses korrekt. Kanaler ryddes først med `clear_channel` for å unngå duplikater. -- **Worker sync** (`worker/src/sync.rs`): Poller `sync_outbox` hvert 1. sekund. Håndterer insert/delete/update/ai_update for meldinger og insert/delete for reaksjoner. -- **SpacetimeDB-adapter** (`web/src/lib/chat/spacetime.svelte.ts`): Ren SpacetimeDB-adapter. Null PG API-kall (`fetch()`). Bruker `onInsert`/`onUpdate`/`onDelete` callbacks for sanntid. Reaksjoner bygges fra `message_reaction`-tabellen. Revisjoner leses fra `message_revision`-tabellen. -- **PG-fallback** (`web/src/lib/chat/pg.svelte.ts`): Brukes kun når SpacetimeDB ikke er konfigurert. Markert som `readonly: true`. -- **Adapter-mønster:** `ChatConnection`-interface med `send`, `edit`, `delete`, `react` metoder. Factory velger basert på env-variabel. - -### Fikset (mars 2026) -- **`enrichFromPg` fjernet** — SpacetimeDB-modulen har nå `metadata` og `edited_at` på `ChatMessage`. Frontend leser alt fra SpacetimeDB, null `fetch()`-kall i adapteren. -- **Worker AI-vask via reducers** — `set_ai_processing`, `ai_update_message` og `clear_ai_processing` reducers erstatter direkte PG-skriving. Sync-worker persisterer til PG via `ai_update` outbox-action. -- **Revisjoner i SpacetimeDB** — `MessageRevision`-tabell med `load_revisions` warmup-reducer. Frontend leser revisjoner via `getRevisions()` fra `conn.db.message_revision`. -- **Warmup med metadata** — `load_messages` inkluderer `metadata`, `edited_at` og parser originale timestamps korrekt. - -### Gjenstår -- **Workspace-partisjonering (§7):** SpacetimeDB-modulen har `workspace_id`-felt men bruker ikke workspace-token på tilkobling ennå. -- **Pin/konvertering via SpacetimeDB:** Pin og kanban/kalender-konvertering går fortsatt direkte til PG API. -- **Lazy warmup per kanal:** Alle aktive kanaler oppvarmes ved oppstart. Kan optimaliseres til per-kanal ved tilkobling. - -## 11. Instruks for Claude Code -- `sync_outbox`-tabellen i SpacetimeDB bør ha et `synced`-flagg og `created_at`-tidsstempel -- Workeren skal bruke jobbkø-infrastrukturen (se `docs/infra/jobbkø.md`) for sin egen helse/observabilitet, men selve pollingen er en egen loop — ikke en vanlig jobb i køen -- Hold sync-payloaden enkel: `{ "table": "chat_messages", "action": "insert", "data": {...} }` — workeren mapper dette til riktig PG-tabell -- Ikke optimaliser for store datamengder ennå. Enkle INSERTs er bra nok til volumet stabiliserer seg -- Alle sync-payloads må inkludere `workspace_id`. Workeren bruker dette til å sette RLS-kontekst (`SET app.current_workspace_id`) før PG-skriving, eller kjører som superuser og filtrerer eksplisitt +# 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. diff --git a/docs/primitiver/edges.md b/docs/primitiver/edges.md new file mode 100644 index 0000000..bade71a --- /dev/null +++ b/docs/primitiver/edges.md @@ -0,0 +1,117 @@ +# 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. diff --git a/docs/primitiver/nodes.md b/docs/primitiver/nodes.md new file mode 100644 index 0000000..93961d0 --- /dev/null +++ b/docs/primitiver/nodes.md @@ -0,0 +1,140 @@ +# 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` +Primært tekstinnhold. Det du leser, søker i, viser i detalj. +Blogginnhold, chatmeldinger, transkripsjoner. + +- Bloggpost: hele artikkelen +- Chatmelding: `'Hei, er du klar?'` +- Voice memo: `NULL` ved opprettelse, fylles etter transkribering +- Brukernode: `NULL` +- CAS-node: `NULL` (binærdata lever på disk) + +### `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 tekstinnhold: + +- 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" }` + +### `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. diff --git a/docs/proposals/personlig_workspace.md b/docs/proposals/personlig_workspace.md index 4d2969e..c87442e 100644 --- a/docs/proposals/personlig_workspace.md +++ b/docs/proposals/personlig_workspace.md @@ -1,5 +1,12 @@ # Forslag: Personlig workspace +> **Superseded (v2):** Dette forslaget er erstattet av retningen +> "noder er sentrum" (se `docs/retninger/bruker_ikke_workspace.md`). +> I v2 finnes det ingen workspaces. Privat rom oppstår naturlig: +> noder uten edges til andre brukere er ditt private rom. Personlig +> kanban, kalender og notater er bare noder du eier uten delte edges. +> Dokumentet er bevart som historisk referanse. + ## Idé Hver bruker får et personlig workspace som fungerer som en individuell produktivitetssuite. Alle verktøyene et delt workspace har — kanban, kalender, notater, graf-koblinger — men privat og selvorganisert. I tillegg: en personlig publiseringskanal ("blogg") der tekster kan deles med omverdenen. diff --git a/docs/retninger/README.md b/docs/retninger/README.md index b928f4e..81cc62f 100644 --- a/docs/retninger/README.md +++ b/docs/retninger/README.md @@ -23,10 +23,10 @@ andre dokumenter. En retning kan også forkastes eller parkeres. |---------|--------|----------------| | [Status quo](status_quo.md) | Referanse | Hva er Sidelinja i dag? Ankerpunkt for de andre retningene. | | [Rom, ikke forum](rom_ikke_forum.md) | Åpen | Bør Sidelinja være en oppslukende sanntidsopplevelse fremfor en tradisjonell webapp? | -| [Universell input og mottak](universell_input.md) | Åpen | Én multimodal input-primitiv + personlig mottaksflate, noder + edges i stedet for separate tabeller | -| [Maskinrommet](maskinrommet.md) | Åpen | Én Rust-tjeneste med fast grensesnitt for alle tekniske tjenester: fang, prosesser, lever | -| [Bruker, ikke workspace](bruker_ikke_workspace.md) | Åpen | Brukeren er sentrum, workspaces er frivillige samlings-noder — ikke containere | -| [Datalaget](datalaget.md) | Åpen | PG + Apache AGE som enhetlig graf og arkiv, SpacetimeDB som sanntidslag, CAS for binærdata | +| [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) | **Besluttet** | SpacetimeDB holder hele grafen, PG er persistent arkiv, CAS for binærdata, AGE ved behov | ## Format - Hva er tesen? diff --git a/docs/retninger/bruker_ikke_workspace.md b/docs/retninger/bruker_ikke_workspace.md index d700f9e..d2a782e 100644 --- a/docs/retninger/bruker_ikke_workspace.md +++ b/docs/retninger/bruker_ikke_workspace.md @@ -1,154 +1,327 @@ -# Bruker, ikke workspace - -> Primærenheten er brukeren og brukerens edges. Workspaces er ikke -> containere — de er frivillige samlings-noder som gir felles kontekst. - -## Observasjoner - -I dag er workspaces den organisatoriske enheten. Du "er i" et workspace, -og det bestemmer hva du ser: kanaler, boards, kalendre. Vil du se noe -fra et annet workspace må du bytte. Det er en container-modell — ting -*bor* i workspaces. - -Men i node+edge-modellen gir dette ikke mening. En node er ikke "i" noe -— den har edges til ting. Og brukeren er ikke "i" et workspace — brukeren -har edges til noder, personer, topics, samtaler. - -## Tesen - -**Brukeren er sentrum.** Du logger inn og ser dine edges — alt du er -koblet til. Ikke "velg workspace" som første handling, men "her er alt -ditt." - -### Workspace som samlings-node, ikke container - -Et workspace eksisterer fortsatt som konsept, men det er en node i -grafen — ikke en organisatorisk boks: - -- **Tradisjonell modell:** Workspace inneholder kanaler, boards, filer. - Brukeren er "i" workspacet. -- **Node-modell:** Workspace er en node. Noder har edge til den. Brukeren - har edge til den. Workspace-noden bærer felles kontekst (tema, - pruning-profil, AI-konfig). - -En node kan ha edge til flere workspaces. En faktoid om en gjest er -relevant for både podcast-prosjektet og research-samlingen. Ikke kopi — -samme node, to edges. - -### Personlig er default - -Alt uten workspace-edge eller mottaker-edge er privat — tilhører bare -deg. "Personlig workspace" er ikke en egen ting du oppretter. Det er -fravær av deling. Dine private notater, dagbok, voice memos — de har -bare edges til deg. - -### Administrasjon er edges med roller - -Brukerstyring forvaltes i nodene selv, ikke i et sentralt admin-panel: - -| Edge-type | Hva den gir | -|-----------|------------| -| Eier-edge | Full kontroll — slette, endre tilgang, endre innstillinger | -| Admin-edge | Kan invitere/fjerne andre, endre konfigurasjon | -| Deltaker-edge | Kan gi input og motta | -| Leser-edge | Kan kun motta (observatør, lytter) | - -Du oppretter en kommunikasjonsnode (podcast-prosjekt). Du er eier -automatisk. Du inviterer Trond → deltaker-edge. Trond kan gi input -men ikke slette eller endre tilgang. Du gir Trond admin-edge → nå -kan han invitere andre og endre innstillinger på den noden. - -Dette skalerer fra en privat notat (kun eier-edge til deg) til en -organisasjon (samlings-node med mange admin- og deltaker-edges). - -### Brukeropplevelsen - -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** (det som før var workspaces) — grupperer - kontekst, men du trenger ikke "gå inn i" dem -- **Din mottaksflate** — alt som er relevant for deg nå, vektet - -Du kan filtrere etter samlings-node hvis du vil fokusere ("vis bare -podcast-prosjektet"), men det er et filter — ikke en modebytte. - -### Felles kontekst på samlings-noder - -En samlings-node (workspace) bærer kontekst som arves av tilknyttede -noder: - -- **Pruning-profil** — hvor aggressivt slettes binærdata? -- **Tema** — visuelt uttrykk (CSS custom properties) -- **AI-konfigurasjon** — hvilke prompts, hvilken modell, hvilke regler -- **Tilgangsnivå** — default synlighet for nye noder opprettet i - denne konteksten -- **Kapasitet** — ressursgrenser for maskinrommet (f.eks. maks - samtidige transkripsjoner) - -## Implikasjoner - -### Kryssgående noder er naturlig -En node med edge til to samlings-noder arver kontekst fra begge. -Konflikter (ulik pruning-profil) løses med prioriteringsregler: -mest konservativ vinner, eller eier-edge bestemmer. - -### Onboarding forenkles -Ny bruker trenger ikke "settes opp i et workspace." De får edges til -de nodene de trenger tilgang til. Ferdig. Endre tilgang = endre edges. - -### Forlate et prosjekt er å fjerne edges -Ingen "slett bruker fra workspace." Fjern deltaker-edges. Brukerens -private noder som hadde edge til samlings-noden beholder den edgen -(det er brukerens innhold), men de mister tilgang til andres noder. - -## RLS og sikkerhet: samlings-noder som harde siloer - -Viktig arkitekturbeslutning: **"bruker, ikke workspace" er en -UX-modell, ikke en sikkerhetsmodell.** - -Ren edge-basert tilgangskontroll — der hver lesing traverserer grafen -for å sjekke om brukeren har en sti til noden — ville kvelt -databasen når grafen vokser. I dag har PG bunnsolid RLS med -`workspace_id = current_setting(...)` — instant og vanntett. - -Løsningen er å skille UX fra sikkerhet: -- **UX-laget:** Brukeren ser sine edges. Ingen "velg workspace." - Filtrering etter samlings-node er frivillig. -- **Sikkerhetslaget:** Under panseret har hver node fortsatt en - `workspace_id` (eller samlings-node-id) som RLS sjekker. Det er - den harde sikkerhetsgensen — billig, velprøvd, instant. -- **Finere tilgang innenfor siloen:** Edge-basert tilgang (eier, - admin, deltaker, leser) sjekkes *innenfor* en silo, ikke som - erstatning for den. - -Samlings-noder er altså ikke bare frivillig organisering — de er -sikkerhetsiloer i databasen. Brukeren merker det ikke, men PG -trenger det. - -## Spenninger og åpne spørsmål - -- **Overblikk.** Uten workspaces som organisatorisk enhet — hvordan - unngår du at alt flyter sammen? Samlings-noder er svaret, men de - må være intuitive å opprette og bruke uten å bli "workspaces med - ny navn." -- **Kryssgående noder og siloer.** Hvis noder har `workspace_id` for - RLS, kan en node da tilhøre to samlings-noder? Kanskje via en - "primær silo" for RLS + sekundære edges for visning. Trenger - gjennomtenkt design. -- **Arv og konflikter.** En node med edge til to samlings-noder med - ulike pruning-profiler — hva vinner? Trenger klare regler som er - intuitive for brukeren. -- **Migrering.** Eksisterende workspace-modell har innhold. Kan - workspaces bli samlings-noder gradvis? RLS-modellen gjør dette - enklere — workspace_id kan bli samlings-node-id uten endring i - sikkerhetslogikk. - -## Forhold til andre retninger - -- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er ikke et workspace, - det er summen av dine edges -- [Universell input og mottak](universell_input.md) — mottaksflaten er - "noder med edge til *meg*", ikke "noder i *mitt workspace*" -- [Maskinrommet](maskinrommet.md) — leser samlings-node-edges for - kontekst (pruning, kapasitet, AI-konfig) +# 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 diff --git a/docs/retninger/datalaget.md b/docs/retninger/datalaget.md index 78ca613..1aca801 100644 --- a/docs/retninger/datalaget.md +++ b/docs/retninger/datalaget.md @@ -1,182 +1,97 @@ -# Datalaget — PG + AGE som enhetlig graf og arkiv - -> PostgreSQL er den eneste databasen. Apache AGE gir Cypher-semantikk -> for graftraverseringer. SpacetimeDB er sanntidslaget over samme data. -> Én database, to tilgangsmønstre, ingen eierskapskonflikt. - -## Observasjoner - -Hele retningsarbeidet har bygget seg mot "alt er noder og edges." -Tilgang er edges, visninger er spørringer, administrasjon er -traversering. Spørsmålet er: hvilken teknologi støtter dette best? - -### Neo4j ble vurdert og forkastet - -Neo4j er best-in-class for dype graftraverseringer, men for dette -prosjektet gir den mer problemer enn den løser: - -- **Tredje database.** PG + SpacetimeDB + Neo4j = tre systemer å - drifte, sikkerhetskopiere og overvåke. På én Hetzner VPS. -- **Mister PG-økosystemet.** pgvector (semantisk søk), fulltekstsøk, - JSONB, pg_cron, statistikk-aggregeringer — alt dette trenger vi - uansett, og det lever i PG. Med Neo4j trenger vi PG *i tillegg*. -- **Minnehungrig.** Neo4j anbefaler 8-16GB heap. På en delt VPS med - PG, SpacetimeDB, Whisper og LiteLLM er det for mye. -- **Lisens.** Community er AGPLv3. Enterprise-features (clustering, - avansert sikkerhet) krever betalt lisens. - -### Apache AGE — Cypher i PG - -Apache AGE er en PG-extension som legger til openCypher-støtte. Noder -og edges lagres i PG-tabeller, men spørres med Cypher-semantikk. - -**Oppsider:** -- **Én database.** Ingen ny infrastruktur. Samme backup, connection - pooling, tooling, monitoring. -- **SQL + Cypher i samme spørring.** "Finn alle noder brukeren har - tilgang til via team-traversering, JOINet med fulltekstsøk og - pgvector." Det kan du ikke gjøre med en separat grafdatabase. -- **Null migrasjonskostnad.** Eksisterende nodes/edges-tabeller kan - eksponeres som graf. Legg til extension, ikke bytt database. -- **Økosystemet bevares.** pgvector, fulltekstsøk, JSONB, pg_cron — - alt fungerer side om side med Cypher-spørringer. -- **Produksjonsbruk.** Azure og EDB tilbyr AGE som managed extension. - -**Nedsider:** -- **Ikke native graf.** Under panseret er det PG-tabeller. For svært - dype traverseringer (10+ hopp over millioner av noder) er Neo4j - raskere. Men de fleste spørringene våre er 1-5 hopp. -- **Yngre prosjekt.** Mindre community, tynnere dokumentasjon enn Neo4j. -- **Quirks.** Spørringer må være enten read-only eller write-only - (WITH-clause for overgang). Noen pg_upgrade-begrensninger. - -## Beslutning - -**PostgreSQL** som enhetlig datalager for noder og edges. -**Apache AGE** som planlagt utvidelse — ikke forpliktet fra dag én. - -De fleste spørringer i dette systemet er grunne: -- "Vis noder med edge til denne brukeren" — 1 hopp -- "Sjekk tilgang via team" — 2 hopp -- "Finn alle kommunikasjonsnoder brukeren har tilgang til" — 2-3 hopp -- "Traverser kunnskapsgrafen mellom to emner" — 3-5 hopp - -PG med rekursive CTEs håndterer 1-3 hopp utmerket. AGE legges til -når graftraversering faktisk blir en målbar flaskehals — ikke før. -AGE er en extension, ikke en migrering, så den kan boltes på uten -å endre eksisterende kode. - -Pragmatisk rekkefølge: -1. **Nå:** PG med nodes/edges-tabeller og CTEs -2. **Når CTEs blir smertefulle:** Legg til AGE for Cypher-spørringer -3. **Usannsynlig:** Evaluer Neo4j hvis AGE ikke holder - -## Lagmodell - -``` -┌─────────────────────────────────────┐ -│ GUI (SvelteKit) │ -│ Primitiver, visninger │ -└────────┬──────────────────┬─────────┘ - │ skriv │ les (sanntid) - │ │ direkte WebSocket -┌────────▼────────┐ ┌─────▼─────────┐ -│ Maskinrommet │ │ SpacetimeDB │──┐ -│ (Rust) │ │ (STDB) │ │ -│ Orkestrering │ └───────────────┘ │ -└──┬─────┬─────┬──┘ │ - │ │ │ sync ↕ │ - ▼ ▼ ▼ │ -┌─────┐┌─────┐┌─────┐┌─────────────┐ │ -│ PG ││STDB ││ CAS ││ Whisper, │ │ -│+AGE ││(skr)││ ││ LiteLLM, │ │ -│ ││ ││ ││ LiveKit ... │ │ -└─────┘└─────┘└─────┘└─────────────┘ │ -``` - -### Skriv vs les — to stier med god grunn - -**Skriv:** GUI → Maskinrommet (Rust) → tjenester (PG, STDB, CAS, ...) - -All orkestrering, edge-logikk, ressursallokering og validering går -gjennom Rust. Maskinrommet bestemmer hva som skjer: hva som skrives -til PG, hva som speiles til SpacetimeDB, hva som lagres i CAS, -hvilke tjenester som trigges. - -**Les (sanntid):** SpacetimeDB → GUI (direkte WebSocket) - -SpacetimeDB sitt klient-SDK kobler seg direkte via WebSocket, -definerer SQL-subscriptions, og synkroniserer automatisk med lokal -cache — uten network round-trips for lesing (~10μs per transaksjon). -Å proxy dette gjennom Rust ville lagt til et hopp, serialisering og -kontekstbytte — en dårligere reimplementering av noe STDB gjør -optimalt. Den direkte lese-stien er en bevisst arkitekturbeslutning, -ikke et hull i lagmodellen. - -**Les (tradisjonelt):** GUI → Maskinrommet (Rust) → PG - -Søk, historikk, statistikk, arkiv — alt som ikke er sanntid går -gjennom Rust og PG. AGE-spørringer for graftraversering, pgvector -for semantisk søk, SQL for aggregeringer. - -### PG er arkivet og grafen -Alle noder og edges lever i PG. AGE gir Cypher-spørringer for -traversering. Standard SQL for alt annet (aggregeringer, fulltekstsøk, -JOINs mot pgvector). Én database, to spørrespråk som utfyller -hverandre. - -### SpacetimeDB er sanntidslaget -Aktive noder og edges speilet til SpacetimeDB for live-oppdateringer. -Ting som er "nå" lever i SpacetimeDB. Ting som er "ferdig" lever kun -i PG. Ingen eierskapskonflikt — SpacetimeDB er en live-visning av -en delmengde av PG-grafen. - -### CAS er binærlageret -Lyd, bilde, video lagres content-addressable utenfor PG. Noder i -PG peker på CAS-hasher. Pruning-regler basert på edges, aksesslog -og modalitet (se maskinrommet). - -## Migreringsstrategi - -### Fase 1: AGE på eksisterende PG -Installer Apache AGE extension. Eksponer eksisterende nodes/edges- -tabeller som graf. Ingen endring for resten av systemet — bare en -ny måte å spørre på. - -### Fase 2: Konsolider til node+edge-modellen -Migrer meldingsboks, kanban, kalender etc. til den universelle -node+edge-modellen gradvis. Gamle tabeller kan leve som views -over grafen i overgangsperioden. - -### Fase 3: Parallell prototype -Det gamle systemet kjører uforstyrret. En ny frontend-prototype -bygges ved siden av som bruker AGE-grafen direkte. Live sync fra -gammelt til nytt via enveis oppdateringer. Testbrukere leker i -det nye, produksjon er trygt i det gamle. - -### Fase 4: Cutover -Når det nye er godt nok, flyttes trafikk over. Det gamle systemet -kan kjøre som read-only fallback en periode. - -## Spenninger og åpne spørsmål - -- **AGE-modenhet.** Prosjektet er aktivt og støttet av Azure/EDB, - men community er mindre enn Neo4j. Risikoen er håndterbar fordi - dataene alltid er PG-tabeller — AGE er bare et spørrelag. -- **SpacetimeDB-synk.** Hvordan synkes noder/edges mellom PG (AGE) - og SpacetimeDB effektivt? Trenger en sync-mekanisme som forstår - "aktiv delmengde." -- **Skjemadesign.** Én node-tabell med alle typer innhold — hva er - felles kolonner vs edge-metadata vs JSONB-payload? Trenger - gjennomtenkt skjema som balanserer fleksibilitet og spørrbarhet. - -## Forhold til andre retninger - -- [Universell input og mottak](universell_input.md) — noder og edges - er den underliggende datamodellen for alle tre primitiver -- [Maskinrommet](maskinrommet.md) — CAS og pruning lever her, AGE - brukes for edge-drevet ressursorkestrering -- [Bruker, ikke workspace](bruker_ikke_workspace.md) — tilgang via - graf-traversering (AGE) i stedet for workspace-membership-tabeller -- [Rom, ikke forum](rom_ikke_forum.md) — to-lags-modellen: - SpacetimeDB for nåtid, PG+AGE for arkiv og graf +# 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 diff --git a/docs/retninger/maskinrommet.md b/docs/retninger/maskinrommet.md index 0a1856b..4c8bb00 100644 --- a/docs/retninger/maskinrommet.md +++ b/docs/retninger/maskinrommet.md @@ -1,322 +1,196 @@ -# Maskinrommet — teknisk tjenestelaget - -> Én Rust-tjeneste med et fast grensesnitt. Alle tekniske tjenester beveger -> seg gjennom dette laget. Fang, prosesser, lever. - -## Observasjoner - -I dag er tekniske tjenester spredt: -- **Worker** (Rust) — kjører bakgrunnsjobber -- **Jobbkø** (PG) — koordinerer arbeid -- **AI Gateway** (LiteLLM) — ruter AI-kall -- **Whisper** — transkripsjon -- **LiveKit** — lyd/video-strømmer - -Hver har sitt eget grensesnitt. Frontend og primitiv-laget må vite hva -som finnes under panseret. Det er ingen felles abstraksjon, ingen felles -logging, ingen felles kapasitetsstyring. - -## Tesen - -Alt som krever tunge ressurser eller eksterne tjenester går gjennom -**ett lag** med **ett grensesnitt**. Ikke fordi det er elegant — fordi -det gir et fast punkt som er enkelt å fange, modifisere og forbedre. - -Maskinrommet gjør tre ting: - -### 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, sentimentanalyse, - faktasjekk, edge-forslag -- **Beriking** — URL → metadata, bilde → beskrivelse, lyd → segmenter -- **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, snapshot) -- Strukturert data (noder, edges, metadata tilbake i grafen) -- Push (webhook, SSE, SpacetimeDB-reducer) - -## Edge-drevet ressursorkestrering - -Nøkkelinnsikten: **maskinrommet leser edges for å vite hva det skal gjøre.** -Noden selv er alltid enkel. Det er edgene som bestemmer hvilke ressurser -som spinnes opp. - -### Security by default -Input uten mottaker-edge er automatisk privat. Du trenger ikke "velge -privat" — det er utgangspunktet. Ingen ser det. Ingen ressurser kobles -inn utover det grunnleggende (fang + transkriber). Privat er ikke en -innstilling, det er fravær av deling. - -### Ressurser er proporsjonale med edges -Samme nodetype, vilt forskjellig ressursbruk: - -``` -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 + publiserings-edge - → fang lyd → transkriber → stream via LiveKit → distribuer - → generer segmenter → kjør live AI → publiser - Ressurser: STT + LiveKit + LLM + mediaprosessering -``` - -Maskinrommet gjør ikke mer enn det edges krever. Ingen overhead for -enkle ting. Noden vet ingenting om LiveKit — den har bare edges som -sier "stream til disse mottakerne", og maskinrommet bestemmer at det -betyr LiveKit. - -### Naturlig eskalering -Du starter en privat voice-note. Bestemmer deg for å dele den med Trond -→ legg til mottaker-edge, maskinrommet begynner å levere. Trond foreslår -at dere tar det som et møte → legg til flere deltaker-edges, maskinrommet -kobler inn sanntidsstrømming. Møtet blir en innspilling → legg til -publiserings-edge, maskinrommet aktiverer produksjonspipeline. - -Hvert steg er bare å legge til edges. Maskinrommet reagerer og kobler -inn flere ressurser etter hvert. Ingen migrering, ingen modebytte. - -## Grensesnittet - -Maskinrommet eksponerer et konsistent API — sannsynligvis en Rust trait -eller et sett traits: - -``` -fang(input: RåInput) → NodeId -prosesser(node: NodeId, operasjon: Operasjon) → Resultat -lever(node: NodeId, mottaker: Mottaker, format: Format) → Status -``` - -Men i praksis er mye av dette *reaktivt*: maskinrommet observerer -edge-endringer og handler automatisk. Legger noen til en mottaker-edge -→ maskinrommet begynner å levere. Legger noen til en stream-edge → -maskinrommet kobler inn LiveKit. Primitivene trenger ikke eksplisitt -kalle `lever()` — de manipulerer edges, og maskinrommet reagerer. - -## Hva dette gir - -### Isolasjon -Bytt Whisper med noe annet? Endre maskinrommet. Frontend vet ingenting. -Legg til bildegenerering? Ny operasjon i maskinrommet. Primitivene -kaller den uten å vite hva som skjer under. - -### Observerbarhet -Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering, -feilhåndtering — alt på ett sted. "Hva bruker vi AI-ressurser på?" -har ett svar. - -### Kapasitetsstyring -Prioritering, kø, rate limiting, fallback mellom leverandører — alt -håndtert av maskinrommet. En podcastinnspilling som trenger live -transkripsjon kan prioriteres over en bakgrunns-oppsummering. - -### Fast utviklingspunkt -To team (eller to hatter) med klart grensesnitt: -- **Over maskinrommet:** primitiver, noder, edges, UI, brukeropplevelse -- **I maskinrommet:** ytelse, integrasjoner, kapasitet, kostnad - -Du kan perfeksjonere det ene uten å røre det andre. - -## Content-Addressable Storage og intelligent pruning - -Maskinrommet forvalter også lagring. Ikke alt kan lagres for evig — men -ikke alt trenger det heller. Signalene for hva som er viktig finnes -allerede i grafen. - -### CAS som lagringsprimitiv -All binærdata (lyd, bilde, video) lagres i et content-addressable store. -Fordeler: -- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi -- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet er - tilgjengelig." Noden peker på en hash, CAS har filen (eller ikke). -- **Enkel opprydning** — slett hashen fra CAS, alle noder som pekte - dit mister binærdataen men beholder metadata og transkripsjon. - -### Lagringsregler per modalitet - -| Modalitet | Default levetid | Begrunnelse | -|-----------|----------------|-------------| -| Tekst | Evig | Billig, er essensen av innholdet | -| Transkripsjon | Evig | Tekstlig representasjon av lyd/video — tar vare på meningen | -| Lyd | 30 dager | Mellomkostnad, transkripsjon bevarer innholdet | -| Bilde | 30 dager | Mellomkostnad, beskrivelse/metadata bevarer kontekst | -| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste | - -### Signaler som forlenger levetid - -Default-TTL er bare utgangspunktet. Maskinrommet justerer basert på: - -- **Edges.** En lydfil med edge til episoderegisteret = publisert - podcast, beholdes. En privat voice-memo uten edges = 30-dagers TTL. -- **Aksesslog.** Hvis noen har spilt av lydfilen i løpet av TTL-perioden, - forlenges den. Ingen aksess = ingen verdi i å beholde binærdataen. -- **Transkripsjonsstatus.** Lyd som er transkribert har "overlevert sin - essens" til tekst. Lyd som *ikke* er transkribert (f.eks. musikk, - lydeffekter) kan trenge lengre TTL. -- **Edge-type.** Edge til publisert innhold = behold. Edge til arkivert - møte = transkripsjon holder. Edge til ingenting = teksten lever videre, - binærdataen kan dø. - -### Eksempler - -``` -Privat voice-memo, aldri delt: - → Lyd transkriberes → tekst lagres evig - → Lydfil: 30 dager, ingen aksess, ingen edges → slettes - → Noden lever videre med teksten - -Podcastepisode: - → Lyd har edge til episoderegister + publiserings-edge - → Aksesseres regelmessig via podcastarkivet - → Lydfil: beholdes så lenge edges og aksess tilsier det - -Rutinemøte for et år siden: - → Video (6 kanaler): ingen har sett den på 6 måneder → slettes - → Lyd: ingen har spilt av → slettes - → Transkripsjon: tekst, lagres evig. Søkbar, refererbar. - → Noden lever med full kontekst minus binærdata - -Viktig styremøte: - → Video aksesseres av styremedlemmer → forlenges - → Workspace-innstilling: "behold video i 1 år" → overrider default -``` - -### Generert innhold er en cache -TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan regenereres -fra kildedata er i praksis en cache. Det lagres i CAS med samme TTL- -mekanisme som alt annet: -- Peter ber om lyd-versjon av en tekstmelding → TTS genereres, lagres -- Ingen spiller den av på 30 dager → filen slettes fra CAS -- Peter (eller noen andre) ber om lyd igjen → regenereres on-demand -- Teksten er der alltid. Binærdataen er flyktig. - -Maskinrommet trenger ikke skille mellom "original lyd" (voice memo) og -"generert lyd" (TTS) i pruning-logikken. Begge er binærdata i CAS med -en TTL som forlenges ved aksess. Forskjellen er bare at generert -innhold alltid kan gjenskapes fra kilden — så det er tryggere å prune. - -### Workspace-styrt aggressivitet -Hvert workspace kan justere sin pruning-profil: -- **Konservativt** — behold alt lenge (f.eks. arkiv-workspace) -- **Aggressivt** — tekst bevares, binærdata prunes raskt (f.eks. - daglig drift-workspace med mye rutineinnhold) -- **Tilpasset** — egne regler per modalitet og edge-type - -### Brukerens erfaringsbaserte meny -Over tid bruker du noen edges oftere enn andre, noen noder oftere enn -andre. Maskinrommet observerer dette og tilbyr en erfaringsbasert meny: -dine mest brukte koblinger, dine vanligste input-mønstre, dine -foretrukne modaliteter. Ikke som en rigid konfigurasjon — som en -adaptiv overflate du kan aktivere og deaktivere fortløpende. - -Dette er ikke maskinlæring eller kompleks AI — det er frekvenstelling -på edges og aksesslog. Enkelt å implementere, intuitivt for brukeren. - -## Pragmatisk vei dit - -Ikke bygg dette fra scratch. Formaliser det som allerede finnes: - -1. **Worker + jobbkø er allerede kjernen.** De trenger et konsistent - API, ikke en omskriving. -2. **AI Gateway (LiteLLM) absorberes** — i stedet for en separat proxy, - blir LLM-kall en operasjon i maskinrommet som alt annet. -3. **Whisper, TTS, mediaprosessering** — allerede planlagt som - worker-jobber. Gi dem samme grensesnitt. -4. **LiveKit** — den mest spesielle tjenesten (sanntidsstrømmer). Kan - starte som en separat integrasjon og formaliseres inn over tid. - -Rekkefølge: definer traits → migrer eksisterende worker-jobber inn → -legg til nye tjenester etter hvert. Fast punkt fra dag én, full -dekning over tid. - -## Compute-separasjon - -Maskinrommet orkestrerer — men tunge jobber trenger ikke kjøre på -samme maskin. Hetzner CPX42 (8 vCPU, 16 GB RAM) skal håndtere state -(PG, SpacetimeDB) og sanntid (Caddy, LiveKit, SvelteKit). Whisper -(CPU-intensiv, spesielt large-v3) og lokal LLM (kildevern-modus) -vil konkurrere om ressurser under live innspilling. - -Maskinrommets abstraksjon gjør dette løsbart: -- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch. - Whisper kjøres med lavere concurrency under live-sesjoner. -- **Senere:** Trekk ut tunge workers til en separat node (billig - ARM/Ampere-instans) som poller jobbkøen over internt nettverk. - Maskinrommet ruter transparent — primitivene merker ingenting. -- **Kildevern-modus:** Lokal LLM (Llama/Gemma) krever GPU eller - dedikert compute. Urealistisk på delt VPS. Egen node for dette. - -Poenget: maskinrommet er designet for å rute arbeid, ikke for å -*utføre* alt selv. Compute-separasjon er en konfigurasjon, ikke en -arkitekturendring. - -## Spenninger og åpne spørsmål - -- **Synkron vs asynkron.** "Fang" og "lever" kan være instant, men - "prosesser" kan ta sekunder (TTS) eller minutter (full episode- - transkripsjon). Grensesnittet må håndtere begge naturlig. -- **Strømmer.** Live lyd/video er fundamentalt annerledes enn - request/response. Men edge-modellen løser mye: maskinrommet ser en - stream-edge og vet at det betyr LiveKit. Utfordringen er *reaktivitet* - — maskinrommet må observere edge-endringer i sanntid og koble inn/ut - ressurser dynamisk. -- **Granularitet.** Hvor mye skal maskinrommet vite om domenet? "Fang - lyd" er generisk, men "transkriber og splitt i segmenter med - taler-identifikasjon" er domenespesifikt. Hvor går grensen? -- **Overhead.** Et ekstra lag betyr et ekstra kall. For tunge - operasjoner (Whisper, LLM) er det neglisjerbart. For lette - operasjoner (slå opp metadata) kan det være unødvendig indirection. - -## Plassering i lagmodellen - -Maskinrommet (Rust) er det eneste orkestringslaget. Alle tjenester — -inkludert PG, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit — er -likeverdige tjenester *under* maskinrommet. SpacetimeDB er ikke et lag -mellom Rust og GUI, det er en tjeneste maskinrommet skriver til. - -Ett unntak: SpacetimeDB har en direkte WebSocket-kobling til frontend -for sanntids lese-strøm. Dette er en bevisst optimering — STDB sitt -klient-SDK gir ~10μs-oppdateringer med automatisk synk og lokal cache. -Å proxy dette gjennom Rust ville vært å bygge en dårligere versjon av -noe STDB gjør optimalt. - -Se [datalaget](datalaget.md) for full lagmodell med diagram. - -## Forhold til andre retninger - -Maskinrommet er infrastrukturen *under* de tre primitivene i -[universell input og mottak](universell_input.md): -- Input-primitiven kaller `fang()` + `prosesser()` -- Mottak-primitiven kaller `lever()` -- Kommunikasjonsnoden bruker alle tre (fang input fra deltakere, - prosesser sanntid, lever til mottakere) - -Det er også det som gjør to-lags-modellen fra [rom, ikke forum](rom_ikke_forum.md) -praktisk: maskinrommet ruter til riktig lag (sanntid vs tradisjonelt) -uten at primitivene trenger å vite forskjellen. +# 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 diff --git a/docs/retninger/rom_ikke_forum.md b/docs/retninger/rom_ikke_forum.md index 57b017b..957ccee 100644 --- a/docs/retninger/rom_ikke_forum.md +++ b/docs/retninger/rom_ikke_forum.md @@ -104,7 +104,7 @@ noe er — du endrer *hvordan* du ser på det som allerede er der. Chat, kanban, kalender er ikke apper — de er visninger av samme tilstandsrom. ### Privat og delt som lag, ikke separate systemer -"Personlig workspace" trenger ikke være en egen ting. Privat/delt er bare en +Privat/delt er bare en synlighetsbryter på alt du gjør. Du chatter med en kollega og samtidig skriver du dagbok — begge er meldingsbokser, den ene er delt, den andre er privat. De eksisterer side om side i samme rom. diff --git a/docs/retninger/status_quo.md b/docs/retninger/status_quo.md index 44ce58f..f781885 100644 --- a/docs/retninger/status_quo.md +++ b/docs/retninger/status_quo.md @@ -1,5 +1,10 @@ # Status quo — Hva Sidelinja er i dag +> **Historisk dokument (v1).** Denne teksten beskriver tilstanden i v1 — +> workspace-basert arkitektur, CRUD-mønster, fragmentert navigasjon. +> Den er bevart som referanse for å forstå utgangspunktet for v2-retningene +> i `docs/retninger/`. + > En redaksjonell webapp med ambisiøse primitiver og tradisjonell overflate. ## Hva fungerer diff --git a/docs/retninger/universell_input.md b/docs/retninger/universell_input.md index 09c8d61..f627040 100644 --- a/docs/retninger/universell_input.md +++ b/docs/retninger/universell_input.md @@ -1,299 +1,186 @@ -# Universell input og mottak - -> Én multimodal input-primitiv. Én personlig mottaksflate. Alt som fanges -> er samme type objekt. Hva det "er" bestemmes av edges, ikke av tabellen -> det ligger i. Hvordan det *presenteres* bestemmes av mottakeren. - -## Observasjoner - -I dag har vi meldingsboksen som "universell primitiv" — men den er egentlig en -*lagringsprimitiv*. Den samler chat, kanban-kort, kalenderoppføringer og notater -i én tabell, men *input* er fortsatt forskjellig per kontekst: chat har ett -tekstfelt, kanban har et skjema, kalender har en datovelger. Brukeren velger -kontekst først, deretter gir de input. - -Og vi har separate pipelines for ulike modaliteter: tekst går én vei, lyd -(voice/transkripsjon) en annen, bilder en tredje. Hver med sin egen flyt. - -## Tesen - -**Én input-primitiv, to versjoner:** - -- **Sanntidsversjon** — live i SpacetimeDB-laget. Brukes i rom, samarbeid, - samtaler. Streamer input og viser resultater i sanntid. -- **Flat versjon** — tradisjonelt mot PG. Brukes på bussen, alene, offline-aktig. - Fanger input og lagrer asynkront. - -Begge aksepterer 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 -- **Nettoppslag** — lim inn URL, den berikes automatisk -- **Kommunikasjon** — samme primitiv for alene (dagbok), en-til-en (melding), - gruppe (kanal) - -Forskjellen mellom "jeg skriver dagbok", "jeg sender en melding" og "jeg lager -et kanban-kort" er ikke *hva brukeren gjør* — det er hvilke edges som knyttes -til resultatet. - -## Én tabell, edges definerer alt - -All output fra input-primitiven lander som noder i kunnskapsgrafen. Én tabell. -Ingen `messages`-tabell, ingen `cards`-tabell, ingen `notes`-tabell. - -Hva en node "er" bestemmes utelukkende av edges: -- Node + edge til kanal = chatmelding -- Node + edge til board + status-edge = kanban-kort -- Node + edge til dato = kalenderoppføring -- Node + edge til kun bruker (privat) = dagboknotis -- Node + edge til topic = faktoid i kunnskapsgrafen -- Node uten edges = løs tanke, ennå uorganisert - -**Retyping er trivielt.** Å gjøre en chatmelding om til et kanban-kort er å -legge til en edge til et board og en status-edge. Fjerne fra chat er å fjerne -kanal-edgen. Ingen datamigrering, ingen transformasjon av innhold. Bare edges. - -**Multitype er naturlig.** En node kan være *både* et kanban-kort *og* en -kalenderoppføring *og* en faktoid. Det er ikke en edge case — det er -arkitekturen. - -## Implikasjoner - -### Meldingsboksen erstattes av noe dypere -Meldingsboksen var riktig intuisjon — men den er en lagringsprimitiv som -prøver å forene ulike domenemodeller. Universell input + kunnskapsgrafen -gjør det renere: det finnes bare noder og edges. "Meldingsboks" blir et -view-konsept (hvordan noder vises i en kontekst), ikke et lagrings-konsept. - -### Input-metode og innholdstype er ortogonale -Du kan snakke inn et kanban-kort. Du kan tegne en kalenderoppføring. Du kan -skrive en voice memo (tekst som transkriberes til lyd for en annen bruker). -Input-primitiven bryr seg ikke om hva det *blir* — den fanger det som -kommer inn. - -### Samme input, ulik routing -Lyd inn i input-primitiven kan routes helt forskjellig basert på edges: -- Edge til et møterom → streames live til andre deltakere (sanntidslaget) -- Edge til kun deg selv → transkriberes og lagres som personlig notat -- Edge til en podcast-kanal → goes into produksjonspipeline -- Edge til en person → sendes som lydmelding - -Brukeren gjør det samme — snakker inn i input-feltet. *Systemet* router -basert på kontekst og edges. Det er ingen "møte-app" eller "notat-app" -eller "meldings-app" — det er én input med ulike destinasjoner. - -### 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 -- Anna skriver tekst → node med tekst + TTS-versjon -- Trond har lyd-preferanse → hører TTS-opplesning av Annas tekst - -Senderen trenger ikke vite eller bry seg. Innholdet er det samme — -presentasjonen er en mottaker-side preferanse. Modalitet er ikke -en egenskap ved meldingen, men ved *lesningen* av den. - -### Én overflate å perfeksjonere -Brukerens mentale modell kollapser til én ting: input-feltet. All -UX-investering konsentreres ett sted i stedet for å smøres tynt utover -ti ulike grensesnitt. Én perfekt input-opplevelse — responsiv, multimodal, -med god AI-støtte — i stedet for ti middelmådige spesialgrensesnitt. - -Dette er en radikal forenkling av utviklingsoverflaten. I stedet for å -bygge og vedlikeholde chat-input, kanban-skjema, kalender-dialog, -notat-editor, voice-recorder, dagbok-felt — bygger vi *ett* grensesnitt -og investerer alt i å gjøre det feilfritt. Alt etterpå er edges. - -### Visninger er spørringer mot grafen -Chat-visningen = "vis noder med edge til denne kanalen, sortert på tid." -Kanban-visningen = "vis noder med edge til dette boardet, gruppert på status." -Kalender-visningen = "vis noder med dato-edge, plassert på tidslinje." -Dagbok-visningen = "vis private noder for denne brukeren, sortert på tid." - -Alle visninger leser fra samme graf. Ingen har "sin egen" data. - -### To versjoner passer to-lags-modellen -Sanntidsversjonen lever i SpacetimeDB-laget: input streames, resultater er -live, andre ser hva du gjør. Flat versjonen lever i det tradisjonelle laget: -input sendes, lagres i PG, ferdig. Begge produserer identiske noder i grafen. - -### Synlighet er bare en edge -Privat = edge kun til deg. Delt = edge til en gruppe/kanal. Publisert = edge -til en offentlig kontekst. Å "dele" noe er å legge til en edge. Å "gjøre -privat" er å fjerne den. Innholdet endres aldri. - -## Universelt mottak — den andre primitiven - -Input-primitiven er halvparten. Den andre halvdelen er *mottak*: hvordan -du konsumerer det andre produserer. Der input er "én overflate som fanger -alt", er mottak "én overflate som presenterer alt tilpasset *deg*." - -### Dimensjoner ved mottak - -**Format.** Lyd, tekst, visuelt — mottaker bestemmer (allerede beskrevet -over). Men det gjelder alt, ikke bare meldinger: en AI-oppsummering kan -leses eller høres. Et whiteboard-snapshot kan vises som bilde eller som -tekstlig beskrivelse. - -**Filtrering.** Hva ser du? Alt fra alle er støy. Mottaksflaten filtrerer -basert på dine edges: hvilke kanaler du følger, hvilke personer du -samarbeider med, hvilke topics du er interessert i. Du kuraterer ikke -manuelt — du justerer edges, og mottaksflaten oppdateres. - -**Prioritering.** Hva er viktig *nå*? En AI-assistert redaksjonell flate -som løfter frem det som trenger oppmerksomhet: ubesvarte meldinger, -oppgaver med frist, noder som er endret siden sist, tråder med aktivitet. -Ikke en notifikasjonsliste — en *vektet visning* av det som er relevant. - -**Tempo.** Sanntid eller asynkront. I sanntidslaget: ting streamer inn -mens de skjer — en kollega snakker, du hører/leser live. I det -tradisjonelle laget: du får en digest, en oppsummering, et overblikk -over hva som har skjedd siden sist. Samme noder, ulikt tempo. - -**Kilde.** Direkte fra en person, eller via en node. Et møte som -genererer innsikter. En AI-jobb som er ferdig. En tråd som har blitt -aktiv igjen. En podcast-episode som er klar for review. Kilden trenger -ikke være et menneske — det kan være en prosess, en hendelse, en -tilstandsendring i grafen. - -### Mottaksflaten som speilbilde av input - -| Input | Mottak | -|-------|--------| -| Én overflate for all input | Én overflate for alt mottak | -| Sender bestemmer ikke format | Mottaker bestemmer format | -| Modalitet er ortogonal | Presentasjon er ortogonal | -| Kontekst gir edges | Preferanser gir filtrering | -| Sanntid + flat versjon | Sanntid (stream) + asynkron (digest) | - -### Mange-til-én og mange-via-mange -Mottaksflaten håndterer naturlig: -- **Én-til-én** — Trond sender deg en melding -- **Mange-til-én** — fem personer i en kanal, du ser alt -- **Via node** — et møte genererer et referat, du mottar det -- **Via kjede** — en chatmelding → blir oppgave → oppgaven fullføres → - du får oppdatering. Hele kjeden er edges, og du ser resultatet i din - mottaksflate uten å ha fulgt hvert steg. - -### Mottaksflaten er også en visning av grafen -Akkurat som chat-visningen er "noder med kanal-edge sortert på tid", er -mottaksflaten "noder med edge til *meg*, vektet på relevans og tid." -Det er ikke en egen mekanisme — det er enda en spørring mot samme graf, -bare med *deg* som sentrum. - -## Kommunikasjonsnoden — den tredje primitiven - -Input fanger. Mottak presenterer. Men det mangler noe: *stedet* der folk -møtes. En kommunikasjonsnode er en node i grafen som samler deltakere, -definerer tilgangsregler, og fungerer som kontekst for input og mottak. - -### Én node, mange former - -En kommunikasjonsnode er konseptuelt identisk uansett skala: - -| Variant | Deltakere | Input-tilgang | Mottak-tilgang | -|---------|-----------|---------------|----------------| -| Én-til-én samtale | 2 | Begge | Begge | -| Gruppechat | N | Alle medlemmer | Alle medlemmer | -| Redaksjonsmøte | N | Alle medlemmer | Alle medlemmer | -| Allmøte | 1 + N | Lederen snakker | Alle lytter, noen kan rekke opp hånden | -| Podcastinnspilling | 2-4 + N | Vertene snakker | Alle lytter, markører for produsent | -| Livesending | 1-4 + ∞ | Vertene | Streamet til nettside/app, lyd eller video | -| Asynkron gjest | 1 + 1 | Gjest gir input innen frist | Redaksjonen mottar | - -Forskjellen er *ikke* ulike systemer — det er ulike edge-konfigurasjoner -på samme nodetype: -- **Eier-edge** — hvem kontrollerer noden (kan invitere, endre regler, avslutte) -- **Input-edge** — hvem kan gi input (snakke, skrive, tegne, dele skjerm) -- **Mottak-edge** — hvem kan motta (lytte, lese, se stream) -- **Rolle-edge** — spesialroller (moderator, produsent, gjest) - -### Kommunikasjonsnoden er en kontekst for de andre primitivene - -Når du gir input *i* en kommunikasjonsnode, arver inputen kontekst-edges -automatisk. Sier du noe i et møte → noden du skaper får edge til møtet. -Du trenger ikke tenke på det — konteksten følger med. - -Mottak i en kommunikasjonsnode er det samme som universelt mottak, bare -scoped til den noden: du ser/hører det andre deltakere gir som input, -presentert etter dine preferanser. - -### Livssyklus - -En kommunikasjonsnode kan være: -- **Live** — aktiv i sanntidslaget. Deltakere er til stede, input streames. -- **Asynkron** — aktiv i det tradisjonelle laget. Deltakere gir input i - eget tempo (chat, asynkron gjest). -- **Avsluttet** — arkivert i PG. Alt som ble sagt/delt er noder med edges - til kommunikasjonsnoden. Kan søkes, gjenfinnes, refereres. -- **Gjenåpnet** — løftet tilbake til sanntidslaget. "Vi tar opp tråden - fra forrige møte" er bokstavelig talt å reaktivere en node. - -### Skalering er en edge-endring, ikke en migrasjonsoperasjon - -En samtale mellom to blir et møte ved å legge til flere deltaker-edges. -Et møte blir en livesending ved å legge til offentlige mottak-edges. -En livesending blir en podcast ved å legge til publiserings-edges på -arkivert innhold. Ingen migrering, ingen konvertering — bare edges. - -## Tekniske forutsetninger - -### STT (tale → tekst): løst -Faster-whisper kjører lokalt, god norsk kvalitet. Allerede i stacken. - -### TTS (tekst → tale): løsbart -Norsk TTS lokalt er ikke godt nok ennå (Piper er "usable" men dårlig rytme, -XTTS-v2 støtter ikke norsk). Kommersielt er det løst: -- **ElevenLabs** — beste kvalitet, eksplisitt norsk med regionale aksenter -- **Azure Neural TTS** — god kvalitet, ~$15/M tegn -- **Google Cloud TTS** — god kvalitet, WaveNet/Neural2 - -Strategi: start med kommersiell API (ElevenLabs) bak AI Gateway, bytt til -lokal modell (Chatterbox Multilingual el.l.) når kvaliteten er god nok. -Brukeren merker ingenting — det er en backend-swap bak gatewayen. Samme -mønster som Whisper: tung jobb → jobbkø → worker → resultat som node-metadata. - -Følg med på: **Chatterbox Multilingual** (Resemble AI) — annonsert norsk -støtte, 350M params, lovende for lokal kjøring. - -## Spenninger og åpne spørsmål - -- **Ytelse.** Én tabell med *alt* i — skalerer det? PG med riktige indekser - og partisjonering håndterer mye, men det er en reell designbeslutning. -- **Skjema.** Noder trenger noe felles skjema (innhold, created_at, author). - Men ulike modaliteter har ulike metadata (transkripsjon, bildestørrelse, - varighet). Hva er felles og hva er edge-metadata? -- **AI-klassifisering.** Når brukeren bare "sier noe" — hvem bestemmer hvilke - edges som knyttes? Manuelt? AI-foreslått? Kontekstbasert (sa det i en - kanal → kanal-edge)? Sannsynligvis en blanding, men det krever gjennomtenkt - UX. -- **Migrering.** Meldingsboksen har allerede innhold. Kan vi migrere til - node+edge-modellen gradvis, eller er det et brudd? -- **Kompleksitet for utviklere.** "Alt er noder og edges" er konseptuelt rent, - men å bygge en kanban-visning som spørrer en graf er mer komplekst enn å - lese fra en `cards`-tabell. Er abstraksjonen verdt kompleksiteten? - -## Tre primitiver, én graf - -| Primitiv | Hva den gjør | Brukerens opplevelse | -|----------|-------------|---------------------| -| **Input** | Fanger alt — tekst, lyd, bilde, AI | Én overflate å snakke/skrive/tegne i | -| **Mottak** | Presenterer alt tilpasset deg | Én personlig flate med det som er relevant | -| **Kommunikasjon** | Samler folk med tilgangsregler | Et sted å være — samtale, møte, sending | - -Alt er noder og edges i samme graf. Input skaper noder. Mottak spør -grafen med deg som sentrum. Kommunikasjonsnoder gir kontekst og -tilgangsregler. Visninger (chat, kanban, kalender, dagbok, stream) -er bare spørringer med ulike filtre. - -## Forhold til andre retninger - -Denne retningen konkretiserer [rom, ikke forum](rom_ikke_forum.md): -- "Formløs input, struktur etterpå" → universell input + edges -- "To lag" → sanntidsversjon + flat versjon -- "Privat/delt som lag" → synlighet som edge -- "Siloer forsvinner" → alt er noder, visninger er spørringer -- "Rommet som primitiv" → kommunikasjonsnoden +# 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. + +## 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" diff --git a/docs/setup/lokal.md b/docs/setup/lokal.md index 23da049..7c79774 100644 --- a/docs/setup/lokal.md +++ b/docs/setup/lokal.md @@ -10,8 +10,8 @@ Det lokale miljøet er et **utviklingsmiljø for kode**. Frontend (SvelteKit) kj | Skrive/teste kode (Rust, SvelteKit, TypeScript) | Lokalt | Rask iterasjon, HMR | | PG-skjema og migrasjoner | Mot server-PG | Én sannhetskilde | | Whisper/AI-eksperimentering | Via AI Gateway på server | Felles tjeneste | -| Docker-compose endringer | Direkte i prod | Serveren er dev-miljø | -| Caddy/Authentik/Forgejo config | Direkte i prod | Avhenger av domener, sertifikater, SSO | +| Docker-compose endringer | Direkte på server | Serveren er dev-miljø | +| Caddy/Authentik/Forgejo config | Direkte på server | Avhenger av domener, sertifikater, SSO | ## 0. Forutsetninger - Windows 11 med WSL2 (Ubuntu 24.04 LTS) @@ -37,8 +37,8 @@ source ~/.cargo/env ```bash cd ~ -git clone ssh://git@git.sidelinja.org:222/sidelinja/server.git sidelinja-v2 -cd sidelinja-v2 +git clone ssh://git@git.sidelinja.org:222/vegard/synops.git +cd synops ``` ## 3. Miljøvariabler (.env.local) @@ -53,16 +53,13 @@ cp .env.example .env.local ## 4. Utviklingsflyt ```bash -# Start alt lokalt -./dev.sh +# SvelteKit med HMR +cd web && npm run dev -# Eller manuelt: -cd web && npm run dev # SvelteKit med HMR -cd rust && cargo run # Rust maskinrom +# Rust maskinrom +cd rust && cargo run ``` -`dev.sh` er kanonisk — oppdater alltid scriptet når nye steg oppdages. - ## 5. Deploy ```bash @@ -70,7 +67,7 @@ cd rust && cargo run # Rust maskinrom git push forgejo main # 2. Deploy til prod (krever eksplisitt godkjenning) -ssh sidelinja@157.180.81.26 "cd /srv/sidelinja && git pull && docker compose up -d --build" +ssh vegard@157.180.81.26 "cd /srv/synops && git pull && docker compose up -d --build" ``` ## 6. Forskjeller fra produksjon (bevisste) diff --git a/docs/setup/migration_safety.md b/docs/setup/migration_safety.md index ddfc395..d49639b 100644 --- a/docs/setup/migration_safety.md +++ b/docs/setup/migration_safety.md @@ -1,5 +1,14 @@ # Migration Safety Checklist +> **Merk (v2):** Denne sjekklisten er skrevet for v1-arkitekturen der RLS var +> basert på `workspace_id`-kolonner og `SET app.current_workspace_id`. I v2 er +> workspace-modellen erstattet av en node-basert tilgangsmatrise (se +> `docs/retninger/bruker_ikke_workspace.md`). Sjekklisten må skrives om for +> det nye mønsteret: `node_access`-matrise, edge-basert tilgang, og +> RLS-policies som opererer på bruker→node-edges i stedet for workspace-scope. +> +> Seksjonene under er bevart som referanse for v1-mønsteret. + Sjekkliste for alle som kjører PostgreSQL-migrasjoner — lokalt eller i prod. ## Før migrering diff --git a/docs/setup/produksjon.md b/docs/setup/produksjon.md index 70c6d4e..97713f1 100644 --- a/docs/setup/produksjon.md +++ b/docs/setup/produksjon.md @@ -1,7 +1,7 @@ # Oppsett: Produksjonsserver (Hetzner VPS) **Filsti:** `docs/setup/produksjon.md` -Denne oppskriften tar en fersk Ubuntu VPS fra null til en komplett Sidelinja-installasjon. Hvert steg er sekvensielt — ikke hopp over noe. +Denne oppskriften tar en fersk Ubuntu VPS fra null til en komplett Synops-installasjon. Hvert steg er sekvensielt — ikke hopp over noe. ## 0. Forutsetninger - Hetzner VPS med Ubuntu 24.04 LTS (8 vCPU, 16 GB RAM minimum) @@ -58,17 +58,17 @@ newgrp docker ## 3. Opprett mappestruktur ```bash -sudo mkdir -p /srv/sidelinja/{config,data,media,logs} -sudo mkdir -p /srv/sidelinja/config/{caddy,authentik} -sudo mkdir -p /srv/sidelinja/data/{postgres,spacetimedb,forgejo,authentik} -sudo mkdir -p /srv/sidelinja/media/podcast -sudo mkdir -p /srv/sidelinja/logs/caddy -sudo chown -R sidelinja:sidelinja /srv/sidelinja +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/media/podcast +sudo mkdir -p /srv/synops/logs/caddy +sudo chown -R sidelinja:sidelinja /srv/synops ``` Resultat: ``` -/srv/sidelinja/ +/srv/synops/ ├── docker-compose.yml ├── .env ├── config/ @@ -88,7 +88,7 @@ Resultat: ## 4. Miljøvariabler (.env) ```bash -cat > /srv/sidelinja/.env << 'EOF' +cat > /srv/synops/.env << 'EOF' # === Domener === DOMAIN_SIDELINJA=sidelinja.org DOMAIN_VEGARD=vegard.info @@ -122,7 +122,7 @@ OPENROUTER_API_KEY= # Ingen porter eksponeres utenom 80/443. Alt rutes internt via Docker-nettverket. EOF -chmod 600 /srv/sidelinja/.env +chmod 600 /srv/synops/.env ``` ## 5. Tjeneste-installasjon (rekkefølge) @@ -153,7 +153,7 @@ Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i # REGLER: # - Ingen "ports:" mot host UTENOM Caddy (80, 443) # - Alle tjenester på samme interne nettverk (sidelinja-net) -# - Volumer bruker bind mounts til /srv/sidelinja/ +# - Volumer bruker bind mounts til /srv/synops/ # - .env-filen lastes automatisk av Docker Compose # - RESSURSGRENSER: Worker-containere (Whisper) MÅ ha deploy.resources.limits # for å forhindre at de sultefôrer LiveKit og PostgreSQL. @@ -165,10 +165,10 @@ networks: services: caddy: # Eneste tjeneste med eksponerte porter (80, 443) - postgres: # data:/srv/sidelinja/data/postgres + postgres: # data:/srv/synops/data/postgres authentik: # SSO for alle domener, på auth.sidelinja.org - forgejo: # data:/srv/sidelinja/data/forgejo, på git.sidelinja.org - spacetimedb: # data:/srv/sidelinja/data/spacetimedb + forgejo: # data:/srv/synops/data/forgejo, på git.sidelinja.org + spacetimedb: # data:/srv/synops/data/spacetimedb livekit: # Intern port, proxyet via Caddy sveltekit: # Intern port, proxyet via Caddy workers: # Rust job workers, ingen porter @@ -199,13 +199,13 @@ sidelinja.org { # Podcast media (statiske filer med byte-range support) handle_path /media/* { - root * /srv/sidelinja/media + root * /srv/synops/media file_server } # Podcast access log (kun media-forespørsler) log { - output file /srv/sidelinja/logs/caddy/podcast_access.log + output file /srv/synops/logs/caddy/podcast_access.log format json } } @@ -261,10 +261,10 @@ Se `docs/arkitektur.md` seksjon 2.2 for full dataklassifisering. Kun kategori 1 ```bash # pg_dump er konsistent selv under last — ingen nedetid docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \ - > /srv/sidelinja/backup/pg/sidelinja_$(date +%Y%m%d).dump + > /srv/synops/backup/pg/sidelinja_$(date +%Y%m%d).dump # Behold 30 dager, slett eldre -find /srv/sidelinja/backup/pg/ -name "*.dump" -mtime +30 -delete +find /srv/synops/backup/pg/ -name "*.dump" -mtime +30 -delete ``` ### 11.1b PostgreSQL WAL-arkivering (kontinuerlig, PITR) @@ -307,21 +307,21 @@ repo1-retention-diff=14 ### 11.2 Media-filer (daglig, 03:30) ```bash # Inkrementell med rsync til lokal backup-disk eller ekstern lagring -rsync -a --delete /srv/sidelinja/media/ /srv/sidelinja/backup/media/ +rsync -a --delete /srv/synops/media/ /srv/synops/backup/media/ ``` ### 11.3 Forgejo-data (daglig, 04:00) ```bash # Forgejo-repos kan gjenskapes, men det er tidkrevende. # Sikkerhetsnett-backup av hele data-mappen: -rsync -a --delete /srv/sidelinja/data/forgejo/ /srv/sidelinja/backup/forgejo/ +rsync -a --delete /srv/synops/data/forgejo/ /srv/synops/backup/forgejo/ ``` ### 11.4 Hemmeligheter (.env) ```bash # Manuell kopi ved endring — ALDRI i Git -cp /srv/sidelinja/.env /srv/sidelinja/backup/env_$(date +%Y%m%d) -chmod 600 /srv/sidelinja/backup/env_* +cp /srv/synops/.env /srv/synops/backup/env_$(date +%Y%m%d) +chmod 600 /srv/synops/backup/env_* ``` ### 11.5 Off-site backup (rclone → Hetzner Object Storage) @@ -335,33 +335,33 @@ rclone config # Opprett remote "hetzner-s3" med Hetzner Object Storage credentials # (S3-kompatibelt, endpoint: fsn1.your-objectstorage.com eller nbg1) -# /srv/sidelinja/scripts/backup-offsite.sh +# /srv/synops/scripts/backup-offsite.sh #!/bin/bash set -euo pipefail BUCKET="s3:hetzner-s3/sidelinja-backup" # PG-dump (siste lokale dump) -LATEST_DUMP=$(ls -t /srv/sidelinja/backup/pg/*.dump 2>/dev/null | head -1) +LATEST_DUMP=$(ls -t /srv/synops/backup/pg/*.dump 2>/dev/null | head -1) if [ -n "$LATEST_DUMP" ]; then rclone copy "$LATEST_DUMP" "$BUCKET/pg/" fi # Media (inkrementell sync) -rclone sync /srv/sidelinja/media/ "$BUCKET/media/" --transfers 4 +rclone sync /srv/synops/media/ "$BUCKET/media/" --transfers 4 # Behold 90 dager PG-dumper off-site rclone delete "$BUCKET/pg/" --min-age 90d -echo "$(date): Off-site backup ferdig" >> /srv/sidelinja/logs/backup-offsite.log +echo "$(date): Off-site backup ferdig" >> /srv/synops/logs/backup-offsite.log ``` ### 11.6 Cron-oppsett ```bash # /etc/cron.d/sidelinja-backup -0 3 * * * sidelinja /srv/sidelinja/scripts/backup-pg.sh -30 3 * * * sidelinja /srv/sidelinja/scripts/backup-media.sh -0 4 * * * sidelinja /srv/sidelinja/scripts/backup-forgejo.sh -30 4 * * * sidelinja /srv/sidelinja/scripts/backup-offsite.sh +0 3 * * * sidelinja /srv/synops/scripts/backup-pg.sh +30 3 * * * sidelinja /srv/synops/scripts/backup-media.sh +0 4 * * * sidelinja /srv/synops/scripts/backup-forgejo.sh +30 4 * * * sidelinja /srv/synops/scripts/backup-offsite.sh ``` ### 11.7 Hva som IKKE backupes (bevisst) @@ -376,14 +376,14 @@ echo "$(date): Off-site backup ferdig" >> /srv/sidelinja/logs/backup-offsite.log ```bash # 1. PostgreSQL docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \ - < /srv/sidelinja/backup/pg/sidelinja_YYYYMMDD.dump + < /srv/synops/backup/pg/sidelinja_YYYYMMDD.dump # 2. Media -rsync -a /srv/sidelinja/backup/media/ /srv/sidelinja/media/ +rsync -a /srv/synops/backup/media/ /srv/synops/media/ # 3. Forgejo docker compose down forgejo -rsync -a /srv/sidelinja/backup/forgejo/ /srv/sidelinja/data/forgejo/ +rsync -a /srv/synops/backup/forgejo/ /srv/synops/data/forgejo/ docker compose up -d forgejo # 4. Avledede data: trigges automatisk ved webhook eller manuelt @@ -399,7 +399,7 @@ git push forgejo main # SSH inn til server: ssh sidelinja@ -cd /srv/sidelinja +cd /srv/synops git pull docker compose build --no-cache docker compose up -d diff --git a/ops/drift-sjekk.md b/ops/drift-sjekk.md index b7d21a6..010f2cb 100644 --- a/ops/drift-sjekk.md +++ b/ops/drift-sjekk.md @@ -13,7 +13,7 @@ men ikke implementert. ## Sjekkliste ### 1. Git-status -- [ ] Er prod-server på siste commit? (`ssh sidelinja@157.180.81.26 'cd /srv/sidelinja/server && git log -1'`) +- [ ] Er prod-server på siste commit? (`ssh vegard@157.180.81.26 'cd /srv/synops/app && git log -1'`) - [ ] Er det ucommittede endringer lokalt som burde vært pushet? - [ ] Er det commits på Forgejo som ikke er deployet til prod? @@ -24,7 +24,6 @@ men ikke implementert. ### 3. Docker-tjenester - [ ] Kjører alle forventede containere i prod? (`docker compose ps`) -- [ ] Er det tjenester i `docker-compose.dev.yml` som mangler i prod (eller omvendt)? - [ ] Er image-versjoner oppdatert? ### 4. Miljøvariabler diff --git a/ops/ryddejobb.md b/ops/ryddejobb.md index 8c807d0..4d98172 100644 --- a/ops/ryddejobb.md +++ b/ops/ryddejobb.md @@ -55,10 +55,9 @@ fjerne utdaterte referanser, og sikre at dokumentasjon stemmer med virkeligheten - [ ] Er det gjort arbeid nylig som mangler erfaringsdokumentasjon i `docs/erfaringer/`? - [ ] Er eksisterende erfaringsdokumenter fortsatt relevante og korrekte? -### 8. dev.sh og utviklermiljø -- [ ] Fungerer `./dev.sh` fra scratch? -- [ ] Er alle nødvendige tjenester dekket? -- [ ] Er det nye quirks eller workarounds som bør inn i scriptet? +### 8. Utviklermiljø +- [ ] Fungerer lokal utvikling mot server (SvelteKit HMR, Rust build)? +- [ ] Er `docs/setup/lokal.md` oppdatert med eventuelle nye steg? ## Sist kjørt diff --git a/reference/server-state.md b/reference/server-state.md index 281ad7a..8bfca79 100644 --- a/reference/server-state.md +++ b/reference/server-state.md @@ -1,7 +1,7 @@ -# Server-tilstand ved v1→v2 overgang (mars 2025) +# Server-tilstand ved overgang fra Sidelinja v1 (mars 2025) Denne filen dokumenterer hva som kjører på serveren ved overgangen. -Slettes når v2 er oppe og stabilt. +Slettes når Synops er oppe og stabilt. ## Kjørende containere (docker-compose) @@ -56,7 +56,7 @@ warmup_config, ai_config, ai_prompts, provider_extra_params, openrouter_only, api_keys_toggle, api_keys_values, drop_priority_unique, workspace_ai_prompts, usage_action_column. Pluss seed_dev.sql. -v2 starter med nytt skjema (noder+edges). Disse migreringene er irrelevante. +Synops starter med nytt skjema (noder+edges). Disse migreringene er irrelevante. ## Crontab Ingen crontab konfigurert (backup-strategi er dokumentert men ikke implementert). diff --git a/scripts/summary.sh b/scripts/summary.sh index 8ab4eea..600302b 100755 --- a/scripts/summary.sh +++ b/scripts/summary.sh @@ -1,47 +1,81 @@ -#!/usr/bin/env bash -# Samler all prosjektdokumentasjon til én fil for deling med AI-er etc. -# Bruk: ./scripts/summary.sh → skriver scripts/summary.md -# ./scripts/summary.sh - → skriver til stdout (for piping) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -OUT="$SCRIPT_DIR/summary.md" - -files=( - # Overblikk — visjon og retning først - "$ROOT/docs/arkitektur.md" - "$ROOT/CLAUDE.md" - "$ROOT"/docs/retninger/*.md - - # Primitiver - "$ROOT"/docs/primitiver/*.md - - # Infrastruktur - "$ROOT"/docs/infra/*.md - - # Erfaringer - "$ROOT"/docs/erfaringer/*.md -) - -collect() { - for f in "${files[@]}"; do - [[ -f "$f" ]] || continue - rel="${f#"$ROOT/"}" - echo "================================================================" - echo "FILE: $rel" - echo "================================================================" - echo "" - cat "$f" - echo "" - echo "" - done -} - -if [[ "${1:-}" == "-" ]]; then - collect -else - collect > "$OUT" - echo "Wrote $OUT ($(wc -l < "$OUT") lines)" -fi +#!/usr/bin/env bash +# Samler all prosjektdokumentasjon til én fil for deling med AI-er etc. +# Bruk: ./scripts/summary.sh → skriver scripts/synops.md +# ./scripts/summary.sh - → skriver til stdout (for piping) +# +# Rekkefølge: overblikk → arkitektur → konsepter → tekniske detaljer → drift + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +OUT="$SCRIPT_DIR/synops.md" + +# Prioritert rekkefølge av mapper under docs/ +# Fra overblikk og visjon → ned til implementeringsdetaljer +DOC_ORDER=( + retninger # Arkitektoniske teser og vedtatte retninger + primitiver # Spesifikasjoner for kjerneprimitivene + concepts # Brukeropplevelser og produktområder + features # Tekniske byggeklosser + infra # Infrastruktur og drift + setup # Oppsett og driftsprosedyrer + erfaringer # Lærdommer fra implementering + proposals # Idébank (uimplementerte forslag) +) + +collect() { + # 1. Prosjektguide (CLAUDE.md) først — gir komplett overblikk + if [[ -f "$ROOT/CLAUDE.md" ]]; then + emit "$ROOT/CLAUDE.md" + fi + + # 2. Overordnet arkitektur + if [[ -f "$ROOT/docs/arkitektur.md" ]]; then + emit "$ROOT/docs/arkitektur.md" + fi + + # 3. Dokumentmapper i prioritert rekkefølge + for dir in "${DOC_ORDER[@]}"; do + [[ -d "$ROOT/docs/$dir" ]] || continue + for f in "$ROOT/docs/$dir"/*.md; do + [[ -f "$f" ]] || continue + emit "$f" + done + done + + # 4. Eventuelle docs/-mapper som ikke er i DOC_ORDER (fang nye mapper automatisk) + for dir in "$ROOT"/docs/*/; do + [[ -d "$dir" ]] || continue + dirname="$(basename "$dir")" + # Hopp over de vi allerede har tatt + skip=false + for known in "${DOC_ORDER[@]}"; do + [[ "$dirname" == "$known" ]] && skip=true && break + done + $skip && continue + for f in "$dir"*.md; do + [[ -f "$f" ]] || continue + emit "$f" + done + done +} + +emit() { + local f="$1" + local rel="${f#"$ROOT/"}" + echo "================================================================" + echo "FILE: $rel" + echo "================================================================" + echo "" + cat "$f" + echo "" + echo "" +} + +if [[ "${1:-}" == "-" ]]; then + collect +else + collect > "$OUT" + echo "Wrote $OUT ($(wc -l < "$OUT") lines)" +fi