Arkitekturbeslutninger: noder er sentrum, edges definerer alt
Grunnleggende arkitekturbeslutninger tatt og dokumentert: - Alt er noder (brukere, team, innhold, mediefiler, samlings-noder) - Edges definerer hva en node er (freeform typer, metadata i JSONB) - Materialisert tilgangsmatrise (node_access) erstatter workspace-RLS - Visibility (hidden/discoverable/readable/open) på noder - Aliaser via usynlige system-edges - Maskinrommet eier all skriving (SpacetimeDB først, PG asynk) - SpacetimeDB holder hele grafen, PG er persistent backup - Node- og edge-skjema spesifisert (docs/primitiver/) Fjernet workspace-konseptet fra hele dokumentasjonen (~40 filer). Fem retninger besluttet, én åpen (rom, ikke forum). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0a467066ba
commit
00bf5d27ce
44 changed files with 1663 additions and 1548 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# === Sidelinja v2 — Lokalt utviklingsmiljø ===
|
# === Synops — Lokalt utviklingsmiljø ===
|
||||||
# Kopier til .env.local og fyll inn verdier:
|
# Kopier til .env.local og fyll inn verdier:
|
||||||
# cp .env.example .env.local
|
# cp .env.example .env.local
|
||||||
#
|
#
|
||||||
|
|
|
||||||
31
CLAUDE.md
31
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,
|
- **Standard arbeidsmodus:** Start i planleggingsmodus. Lag en grundig plan,
|
||||||
få godkjenning, deretter implementer. Jobbene er ment å kunne kjøre
|
få godkjenning, deretter implementer. Jobbene er ment å kunne kjøre
|
||||||
lenge autonomt uten input underveis.
|
lenge autonomt uten input underveis.
|
||||||
- **Testmiljø:** `./dev.sh` er den kanoniske måten å starte utviklingsmiljøet.
|
- **Utvikling mot server.** Ingen lokale databaser eller tjenester.
|
||||||
Når nye steg eller oppsett-quirks oppdages, oppdater alltid `dev.sh`.
|
Frontend (SvelteKit) utvikles lokalt med HMR mot server-API.
|
||||||
Før Vegard tester i browser: kjør `./dev.sh`, verifiser med
|
Rust bygges lokalt, deployes til server for integrasjonstest.
|
||||||
`cargo check`/`svelte-check`/`curl`, og meld tilbake at det er klart.
|
|
||||||
- **Browser-testing:** Claude har ikke tilgang til browser. Visuell testing
|
- **Browser-testing:** Claude har ikke tilgang til browser. Visuell testing
|
||||||
gjøres av Vegard. Claude verifiserer backend (kompilering, API, DB-state).
|
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.
|
- **Commit og push:** Bruk egen vurdering. Trygt og reverserbart.
|
||||||
- **Deploy til produksjon:** Krever alltid eksplisitt godkjenning fra Vegard.
|
- **Deploy til produksjon:** Krever alltid eksplisitt godkjenning fra Vegard.
|
||||||
- **Diskusjon:** Forklar og diskuter før arkitekturendringer.
|
- **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
|
- `rom_ikke_forum.md` — Opplevelse-først, to-lags-modell, administrativ opplevelse
|
||||||
- `universell_input.md` — Tre primitiver (input, mottak, kommunikasjon), noder+edges
|
- `universell_input.md` — Tre primitiver (input, mottak, kommunikasjon), noder+edges
|
||||||
- `maskinrommet.md` — Rust-orkestrator: fang, prosesser, lever
|
- `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
|
- `datalaget.md` — PG(+AGE) som graf og arkiv, SpacetimeDB som sanntidslag
|
||||||
- `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene:
|
- `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:
|
- `docs/concepts/` — Brukeropplevelser/produktområder:
|
||||||
- `studioet.md`, `møterommet.md`, `redaksjonen.md`, `podcastfabrikken.md`,
|
- `studioet.md`, `møterommet.md`, `redaksjonen.md`, `podcastfabrikken.md`,
|
||||||
`kunnskapsgrafen.md`, `valgomaten.md`, `den_asynkrone_gjesten.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:
|
- `docs/setup/` — Oppsett og drift:
|
||||||
- `produksjon.md` — Steg-for-steg oppsett av Hetzner VPS fra scratch
|
- `produksjon.md` — Steg-for-steg oppsett av Hetzner VPS fra scratch
|
||||||
- `lokal.md` — Lokalt utviklingsmiljø (WSL2, mot server)
|
- `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:
|
- `docs/infra/` — Infrastruktur og drift:
|
||||||
- `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + fallback)
|
- `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + fallback)
|
||||||
- `api_grensesnitt.md` — Kommunikasjonskart: SvelteKit er web-API, Rust er worker
|
- `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
|
## Aktører
|
||||||
- **Vegard** — serveradmin, utvikler, bruker. SSH: `vegard@157.180.81.26`
|
- **Vegard** — serveradmin, utvikler, bruker. SSH: `vegard@157.180.81.26`
|
||||||
- **Claude** — AI-agent, utvikler. SSH: `claude@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
|
## Stack
|
||||||
- **Orkestrator/Backend:** Rust (maskinrommet)
|
- **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å)
|
- `synops.no` — Plattformdomene (reservert, ikke i bruk ennå)
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
- **Forgejo-org:** `sidelinja`
|
- **Repos i Forgejo:**
|
||||||
- **Repos:**
|
- `vegard/synops` — plattformkode og arkitektur: `ssh://git@git.sidelinja.org:222/vegard/synops.git`
|
||||||
- `synops` — plattformkode og arkitektur
|
- `sidelinja/sidelinja` — podcastinnhold: `ssh://git@git.sidelinja.org:222/sidelinja/sidelinja.git`
|
||||||
- `sidelinja` — podcastinnhold: `ssh://git@git.sidelinja.org:222/sidelinja/sidelinja.git`
|
|
||||||
- **Git-identitet:** vegard / vnotnes@pm.me
|
- **Git-identitet:** vegard / vnotnes@pm.me
|
||||||
- **Forgejo-bruker:** vegard (admin)
|
- **Forgejo-bruker:** vegard (admin)
|
||||||
- **CLI:** Bruk `tea`, ikke `gh` (vi bruker Forgejo, ikke GitHub)
|
- **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.
|
(samler folk). Alt annet er visninger og edges.
|
||||||
3. **Maskinrommet orkestrerer alt.** Fang, prosesser, lever. Edge-drevet
|
3. **Maskinrommet orkestrerer alt.** Fang, prosesser, lever. Edge-drevet
|
||||||
ressursallokering. Tjenester under er utbyttbare.
|
ressursallokering. Tjenester under er utbyttbare.
|
||||||
4. **Brukeren er sentrum.** Ingen workspace-velger. Du ser dine edges.
|
4. **Noder er sentrum.** Brukere, team, innhold — alt er noder.
|
||||||
Samlings-noder gir felles kontekst med RLS-siloer under.
|
Du ser dine edges. Tilgang via materialisert tilgangsmatrise.
|
||||||
5. **Privat er default.** Input uten mottaker-edge er privat. Security
|
5. **Privat er default.** Input uten mottaker-edge er privat. Security
|
||||||
by design, ikke konfigurasjon.
|
by design, ikke konfigurasjon.
|
||||||
6. **PG er arkivet, SpacetimeDB er nåtid.** Ingen eierskapskonflikt.
|
6. **PG er arkivet, SpacetimeDB er nåtid.** Ingen eierskapskonflikt.
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,134 @@
|
||||||
# Arkitektur — Sidelinja v2
|
# Arkitektur — Synops
|
||||||
|
|
||||||
## Visjon
|
## Visjon
|
||||||
|
|
||||||
Sidelinja er en plattform for redaksjonelt arbeid og podcast-produksjon.
|
Synops er en plattform for redaksjonelt arbeid og podcast-produksjon.
|
||||||
Ikke en webapp med features — en plattform med primitiver som kan bli
|
Ikke en webapp med features — en plattform med primitiver som kan bli
|
||||||
hva som helst.
|
hva som helst. Sidelinja (podcastredaksjonen) er en tenant som bruker
|
||||||
|
Synops.
|
||||||
|
|
||||||
Alt er noder og edges i en graf. Input fanger, mottak presenterer,
|
Alt er noder og edges i en graf. En bruker er en node. Et team er en
|
||||||
kommunikasjonsnoder samler folk. Maskinrommet orkestrerer tekniske
|
node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke
|
||||||
tjenester under. Frontend er et tynt lag som viser grafen.
|
av noden selv. Maskinrommet eier alle skrivinger. Frontend er et tynt
|
||||||
|
lag som leser grafen fra SpacetimeDB.
|
||||||
|
|
||||||
## Lagmodell
|
## Lagmodell
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
┌─────────────────────────────────────┐
|
||||||
│ GUI (SvelteKit) │
|
│ GUI (SvelteKit) │
|
||||||
│ Visninger: spørringer mot grafen │
|
│ Visninger: spørringer mot STDB │
|
||||||
└────────┬──────────────────┬─────────┘
|
└────────┬──────────────────┬─────────┘
|
||||||
│ skriv │ les (sanntid)
|
│ intensjoner │ les (sanntid)
|
||||||
│ │ direkte WebSocket
|
│ │ direkte WebSocket
|
||||||
┌────────▼────────┐ ┌─────▼─────────┐
|
┌────────▼────────┐ ┌──────▼──────────┐
|
||||||
│ Maskinrommet │ │ SpacetimeDB │
|
│ Maskinrommet │ │ SpacetimeDB │
|
||||||
│ (Rust) │ │ Aktive noder │
|
│ (Rust) │ │ Hele grafen │
|
||||||
│ Orkestrering │ └───────────────┘
|
│ Eier alle │ │ (noder+edges) │
|
||||||
|
│ skrivinger │ └─────────────────┘
|
||||||
└──┬─────┬─────┬──┘
|
└──┬─────┬─────┬──┘
|
||||||
│ │ │
|
│ │ │
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌─────┐┌─────┐┌─────┐┌─────────────┐
|
┌─────┐┌─────┐┌─────┐┌─────────────┐
|
||||||
│ PG ││STDB ││ CAS ││ Whisper, │
|
│ PG ││STDB ││ CAS ││ Whisper, │
|
||||||
│ ││(skr)││ ││ LiteLLM, │
|
│(bak)││(skr)││ ││ LiteLLM, │
|
||||||
│ ││ ││ ││ LiveKit ... │
|
│ ││ ││ ││ LiveKit ... │
|
||||||
└─────┘└─────┘└─────┘└─────────────┘
|
└─────┘└─────┘└─────┘└─────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Skrivestien
|
### Skrivestien
|
||||||
GUI → Maskinrommet (Rust) → tjenester
|
GUI → intensjon → Maskinrommet (Rust) → SpacetimeDB (instant) → PG (async)
|
||||||
|
|
||||||
All orkestrering, edge-logikk, validering og ressursallokering
|
Frontend sender intensjoner (ikke data). Maskinrommet validerer,
|
||||||
går gjennom Rust. Maskinrommet leser edges og bestemmer hvilke
|
skriver til SpacetimeDB først for umiddelbar oppdatering, deretter
|
||||||
tjenester som trigges.
|
persisterer til PG asynkront. Maskinrommet leser edges og bestemmer
|
||||||
|
hvilke tjenester som trigges.
|
||||||
|
|
||||||
### Lesestien (sanntid)
|
### Lesestien (sanntid)
|
||||||
SpacetimeDB → GUI (direkte WebSocket)
|
SpacetimeDB → GUI (direkte WebSocket)
|
||||||
|
|
||||||
STDB klient-SDK gir ~10μs-oppdateringer med lokal cache.
|
SpacetimeDB holder hele grafen — alle noder og edges. Frontend
|
||||||
En bevisst optimering — ikke et hull i lagmodellen.
|
abonnerer via WebSocket med edge-filtre. Visninger er spørringer
|
||||||
|
mot STDB, ikke forhåndsdefinerte API-endepunkter.
|
||||||
|
|
||||||
### Lesestien (tradisjonell)
|
### Lesestien (tunge spørringer)
|
||||||
GUI → Maskinrommet (Rust) → PG
|
GUI → Maskinrommet (Rust) → PG
|
||||||
|
|
||||||
Søk, historikk, statistikk, arkiv. Cypher via AGE for
|
Søk, statistikk, semantisk søk (pgvector), graftraversering
|
||||||
graftraversering når CTEs ikke holder.
|
(AGE/Cypher). For operasjoner der STDB ikke er egnet.
|
||||||
|
|
||||||
## Datamodell
|
## Datamodell
|
||||||
|
|
||||||
### Noder
|
### Alt er noder
|
||||||
Én tabell. Alt innhold er noder: meldinger, oppgaver, notater,
|
Én `nodes`-tabell. Alt er noder: brukere, team, meldinger, oppgaver,
|
||||||
faktoider, mediefiler, kommunikasjonsrom, samlings-noder, brukere.
|
notater, mediefiler, kommunikasjonsrom, samlings-noder. En bruker er
|
||||||
|
en node som tilfeldigvis kan logge inn.
|
||||||
|
|
||||||
Felles skjema: id, innhold, created_at, author, content_hash (→ CAS).
|
Felles skjema: id, innhold, created_at, content_hash (→ CAS).
|
||||||
Modalitetsspesifikk metadata i JSONB.
|
Modalitetsspesifikk metadata i JSONB.
|
||||||
|
|
||||||
### Edges
|
### Edges definerer alt
|
||||||
Én tabell. Alle relasjoner er edges: tilhørighet, tilgang, type,
|
Én `edges`-tabell. Edge-typer er frie strenger. Hva en node "er"
|
||||||
synlighet, rolle, status.
|
bestemmes utelukkende av dens edges:
|
||||||
|
|
||||||
Hva en node "er" bestemmes utelukkende av edges:
|
|
||||||
- Node + edge til kanal = chatmelding
|
- Node + edge til kanal = chatmelding
|
||||||
- Node + edge til board + status-edge = kanban-kort
|
- Node + edge til board + status-edge = kanban-kort
|
||||||
- Node + edge til dato = kalenderoppføring
|
- Node + edge til dato = kalenderoppføring
|
||||||
- Node + edge til kun bruker = privat notat
|
- Node + edge til kun bruker = privat notat
|
||||||
- Node + edge til topic = faktoid
|
|
||||||
- Node uten edges = løs tanke
|
- Node uten edges = løs tanke
|
||||||
|
|
||||||
### Visninger
|
### Visninger er spørringer
|
||||||
Visninger er spørringer mot grafen med ulike filtre:
|
Visninger er spørringer mot SpacetimeDB med edge-filtre:
|
||||||
- Chat = noder med kanal-edge, sortert på tid
|
- Chat = noder med kanal-edge, sortert på tid
|
||||||
- Kanban = noder med board-edge, gruppert på status
|
- Kanban = noder med board-edge, gruppert på status
|
||||||
- Kalender = noder med dato-edge, på tidslinje
|
- Kalender = noder med dato-edge, på tidslinje
|
||||||
- Mottaksflate = noder med edge til deg, vektet på relevans
|
- Mottaksflate = noder med edge til deg, vektet på relevans
|
||||||
|
|
||||||
## Tre primitiver
|
Ingen forhåndsdefinerte visningstyper. Nye visninger er nye filtre.
|
||||||
|
|
||||||
### 1. Input
|
## 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 universell input-komponent, gjenbrukt overalt. Fanger tekst, lyd,
|
||||||
Én personlig flate som presenterer alt tilpasset deg. Format
|
bilde, AI, URL. Konteksten (hvor du er) bestemmer hvilke edges som
|
||||||
bestemmes av mottaker (tekst/lyd). Filtrering via dine edges.
|
legges til. Output er alltid en node.
|
||||||
Sanntid (stream) eller asynkront (digest).
|
|
||||||
|
|
||||||
### 3. Kommunikasjon
|
## Struktur uten workspaces
|
||||||
En node som samler folk med tilgangsregler. Samme type fra
|
|
||||||
én-til-én-samtale til livesending. Forskjellen er edges:
|
Ingen workspace-velger. Ingen `workspace_id`. Samlings-noder gir
|
||||||
eier, input-tilgang, mottak-tilgang, rolle.
|
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
|
## Maskinrommet
|
||||||
|
|
||||||
Rust-tjeneste med tre operasjoner: **fang**, **prosesser**, **lever**.
|
Rust-tjeneste med tre operasjoner: **fang**, **prosesser**, **lever**.
|
||||||
|
|
||||||
Edge-drevet ressursorkestrering: maskinrommet leser edges og
|
Eier alle skrivinger. Frontend sender intensjoner, maskinrommet
|
||||||
bestemmer hvilke tjenester som spinnes opp. Dagboknotat = bare
|
validerer og utfører. Edge-drevet ressursorkestrering: maskinrommet
|
||||||
transkriber. Livesending = transkriber + LiveKit + AI + mediaprosessering.
|
leser edges og bestemmer hvilke tjenester som spinnes opp.
|
||||||
|
|
||||||
Forvalter også CAS (binærlagring) med intelligent pruning basert
|
Forvalter også CAS (binærlagring) med intelligent pruning basert
|
||||||
på modalitet, edges og aksessmønstre.
|
på modalitet, edges og aksessmønstre.
|
||||||
|
|
||||||
## Sikkerhet
|
## Sikkerhet
|
||||||
|
|
||||||
### RLS-siloer
|
### Synlighet (visibility)
|
||||||
Samlings-noder (det som var workspaces) er harde sikkerhetsgenser
|
Noder har en visibility-egenskap med fire nivåer:
|
||||||
i PG med RLS. `workspace_id = current_setting(...)` — instant,
|
- **hidden** — usynlig, kun tilgjengelig via system-edges
|
||||||
vanntett. Edge-basert tilgang er UX *innenfor* siloen.
|
- **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
|
### Privat er default
|
||||||
Input uten mottaker-edge er automatisk privat. Ingen ser det.
|
Input uten mottaker-edge er automatisk privat. Ingen ser det.
|
||||||
|
|
@ -122,28 +137,27 @@ Deling er å legge til edges.
|
||||||
## Datalag
|
## Datalag
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
Arkiv og graf. Alle noder og edges. Fulltekstsøk, pgvector
|
Persistent backup og arkiv. Alle noder og edges. Fulltekstsøk,
|
||||||
(semantisk søk), JSONB. Apache AGE for Cypher når CTEs ikke holder.
|
pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov.
|
||||||
|
|
||||||
### SpacetimeDB
|
### SpacetimeDB
|
||||||
Sanntidslag. Aktive noder og edges speilet fra PG. Frontend
|
Holder hele grafen — alle noder og edges. Frontend abonnerer via
|
||||||
abonnerer via WebSocket. Ingen eierskapskonflikt — STDB er en
|
WebSocket. Maskinrommet skriver hit først for umiddelbar oppdatering,
|
||||||
live-visning av en delmengde av PG-grafen.
|
PG synkroniseres asynkront.
|
||||||
|
|
||||||
### CAS (Content-Addressable Store)
|
### CAS (Content-Addressable Store)
|
||||||
Binærdata (lyd, bilde, video) lagret med hash. TTL basert på
|
Binærdata (lyd, bilde, video) lagret med hash. TTL basert på
|
||||||
modalitet, edges og aksesslog. Tekst lever evig, lyd 30 dager,
|
modalitet, edges og aksesslog. Generert innhold (TTS, thumbnails)
|
||||||
video 14 dager — forlenges ved aksess. Generert innhold (TTS,
|
er en cache som regenereres on-demand.
|
||||||
thumbnails) er en cache som regenereres on-demand.
|
|
||||||
|
|
||||||
## Teknologivalg
|
## Teknologivalg
|
||||||
|
|
||||||
| Rolle | Teknologi | Begrunnelse |
|
| Rolle | Teknologi | Begrunnelse |
|
||||||
|-------|-----------|-------------|
|
|-------|-----------|-------------|
|
||||||
| Orkestrator | Rust | Ytelse, typesikkerhet, allerede i stacken |
|
| Orkestrator | Rust | Ytelse, typesikkerhet, eier alle skrivinger |
|
||||||
| Frontend | SvelteKit | PWA, SSR, allerede i stacken |
|
| Frontend | SvelteKit | PWA, SSR, tynt lag mot STDB |
|
||||||
| Database | PostgreSQL | Økosystem (pgvector, fulltekstsøk, AGE), stabilitet |
|
| Database | PostgreSQL | Persistent backup, pgvector, fulltekstsøk, AGE |
|
||||||
| Sanntid | SpacetimeDB | In-memory, WebSocket-subscriptions, ~10μs |
|
| Sanntid | SpacetimeDB | Hele grafen, WebSocket-subscriptions, ~10μs |
|
||||||
| Binærlagring | CAS (filsystem) | Enkel, deduplisering, ingen ekstern avhengighet |
|
| Binærlagring | CAS (filsystem) | Enkel, deduplisering, ingen ekstern avhengighet |
|
||||||
| AI Gateway | LiteLLM | Multi-provider, BYOK, OpenRouter fallback |
|
| AI Gateway | LiteLLM | Multi-provider, BYOK, OpenRouter fallback |
|
||||||
| STT | faster-whisper | Lokal, god norsk kvalitet |
|
| STT | faster-whisper | Lokal, god norsk kvalitet |
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,7 @@ Mange interessante gjester har ikke tid til å stille i studio. Den Asynkrone Gj
|
||||||
```sql
|
```sql
|
||||||
guest_tokens (
|
guest_tokens (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- Samlings- eller tema-node
|
||||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
||||||
guest_name TEXT NOT NULL, -- Visningsnavn ("Erna Solberg")
|
guest_name TEXT NOT NULL, -- Visningsnavn ("Erna Solberg")
|
||||||
questions JSONB NOT NULL, -- [{ "sort": 1, "text": "Hva tenker du om...?" }]
|
questions JSONB NOT NULL, -- [{ "sort": 1, "text": "Hva tenker du om...?" }]
|
||||||
token TEXT UNIQUE NOT NULL, -- Kryptografisk sikker, URL-safe token
|
token TEXT UNIQUE NOT NULL, -- Kryptografisk sikker, URL-safe token
|
||||||
|
|
@ -58,9 +57,9 @@ guest_tokens (
|
||||||
|
|
||||||
### 4.2 Sikkerhet
|
### 4.2 Sikkerhet
|
||||||
- Token er kryptografisk tilfeldig (256-bit, URL-safe base64).
|
- 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.
|
- 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.
|
- Tokenet kan revokeres manuelt av redaksjonen.
|
||||||
|
|
||||||
### 4.2b Sikkerhetsdybde (mot token-lekkasje og misbruk)
|
### 4.2b Sikkerhetsdybde (mot token-lekkasje og misbruk)
|
||||||
|
|
@ -81,7 +80,7 @@ clamav:
|
||||||
image: clamav/clamav:latest
|
image: clamav/clamav:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/sidelinja/media:/scan:ro
|
- /srv/synops/media:/scan:ro
|
||||||
networks:
|
networks:
|
||||||
- sidelinja-net
|
- sidelinja-net
|
||||||
```
|
```
|
||||||
|
|
@ -101,7 +100,7 @@ Gjest åpner URL med token
|
||||||
→ SvelteKit validerer token
|
→ SvelteKit validerer token
|
||||||
→ Viser spørsmål + opptaksknapp
|
→ Viser spørsmål + opptaksknapp
|
||||||
→ Gjest tar opp svar
|
→ 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 message (voice_memo) i channelen
|
||||||
→ Oppretter whisper_transcribe-jobb i jobbkøen
|
→ Oppretter whisper_transcribe-jobb i jobbkøen
|
||||||
→ Inkrementerer recordings_count
|
→ Inkrementerer recordings_count
|
||||||
|
|
@ -118,8 +117,8 @@ Gjest åpner URL med token
|
||||||
|
|
||||||
## 6. Instruks for Claude Code
|
## 6. Instruks for Claude Code
|
||||||
* `guest_tokens`-tabellen er **ikke** en node i grafen — den er ren tilgangsstyring.
|
* `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.
|
* 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).
|
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
**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
|
## 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`.
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,23 @@ Hver publisert episode får en side med:
|
||||||
* Fane: **SRT** (nedlastbar undertekstfil — master-kopi fra Git)
|
* Fane: **SRT** (nedlastbar undertekstfil — master-kopi fra Git)
|
||||||
* Fane: **Ren tekst** (lesbart transkripsjonsdokument — avledet fra SRT, lagret i PG)
|
* 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)
|
## 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:
|
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
|
Peter Hagen, Nicolai Buzatu, Bjørn Einar Drag, Øystein Sjølie
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Workspace-spesifikk konfigurasjon
|
## 5. Per samlings-node konfigurasjon
|
||||||
Hver workspace har sin egen podcast-konfigurasjon, lagret i `workspaces.settings` (JSONB):
|
Hver samlings-node (f.eks. Sidelinja) har sin egen podcast-konfigurasjon, lagret som JSONB-metadata på noden:
|
||||||
|
|
||||||
### 5.1 Mediefiler
|
### 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)
|
### 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
|
#### 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
|
#### Filnavnkonvensjon
|
||||||
Flat struktur med prosesseringstidspunkt som filnavn:
|
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.
|
* **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
|
#### 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)
|
#### 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.
|
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
|
#### Webhook-flyt
|
||||||
```
|
```
|
||||||
Forgejo push-webhook → SvelteKit POST /api/webhooks/forgejo
|
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
|
→ 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.
|
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.
|
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
|
### 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.
|
* **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 `settings.llm_prompts` slik at AI-en kjenner konteksten og vertene for akkurat den podcasten.
|
* **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
|
### 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
|
### 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
|
## 6. Instruks for Claude Code
|
||||||
* **Lydfiler:** Håndter filopplasting i SvelteKit strømmende (streaming) for filer >100MB for å unngå minne-lekkasjer.
|
* **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.
|
* **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.
|
* **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.
|
* **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`.
|
* **Tilhørighet:** Alle jobber, mediefiler og metadata knyttes til riktig samlings-node via edges. Hent config (prompts, domene) fra samlings-nodens JSONB-metadata.
|
||||||
|
|
@ -33,4 +33,4 @@ Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen (
|
||||||
## 4. Instruks for Claude Code
|
## 4. Instruks for Claude Code
|
||||||
* Bruk SvelteKit for Drag-and-Drop. Unngå tunge biblioteker hvis native HTML5 Drag and Drop er tilstrekkelig.
|
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ spacetime generate --lang typescript --out-dir ../web/src/lib/chat/module_bindin
|
||||||
--module-path .
|
--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)
|
## 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
|
## 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`:
|
SpacetimeDB eksponerer Rust-parameternavn direkte i HTTP JSON API-et. Underscore-prefix (`_workspace_id`) blir til `_workspace_id` i JSON, ikke `workspace_id`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
||||||
|
|
@ -68,14 +68,11 @@ Eksempel:
|
||||||
```
|
```
|
||||||
|
|
||||||
**Hvorfor `users.settings` og ikke en egen tabell?**
|
**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
|
- JSONB er fleksibelt — nye innstillinger legges til uten migrering
|
||||||
- Ingen relasjoner, ingen spørringer mot enkeltfelt — bare hent hele objektet
|
- Ingen relasjoner, ingen spørringer mot enkeltfelt — bare hent hele objektet
|
||||||
|
|
||||||
**Workspace-spesifikke overrides** kan legges i `workspace_members` ved behov:
|
**Samlings-node-spesifikke overrides** kan legges som edges med metadata mellom bruker og samlings-node ved behov.
|
||||||
```sql
|
|
||||||
ALTER TABLE workspace_members ADD COLUMN settings JSONB NOT NULL DEFAULT '{}';
|
|
||||||
```
|
|
||||||
Men dette er fase 2. Start med globale brukerinnstillinger.
|
Men dette er fase 2. Start med globale brukerinnstillinger.
|
||||||
|
|
||||||
## 4. Frontend
|
## 4. Frontend
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
## 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 ┌─────────────┐
|
┌─────────────┐ 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` |
|
| **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` |
|
| **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
|
### 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.
|
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
|
### Gjenstår
|
||||||
- **Vedlegg, TTL** — avventer implementering.
|
- **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).
|
- **Pin/konvertering:** Går fortsatt direkte til PG API (ikke via SpacetimeDB).
|
||||||
|
|
||||||
## 11. Instruks for Claude Code
|
## 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.
|
* **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`.
|
* **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.
|
* **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.
|
* **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.
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i
|
||||||
|
|
||||||
### Implementert
|
### Implementert
|
||||||
- Migrering `0003_calendar.sql`: `calendars` + `calendar_events` (begge FK→nodes)
|
- 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
|
- Heldagshendelser (`T12:00:00` for tidssone-trygghet) vs. tidshendelser med klokkeslett
|
||||||
- Fargekoder per hendelse (7 forhåndsdefinerte) + standard kalenderfarge
|
- Fargekoder per hendelse (7 forhåndsdefinerte) + standard kalenderfarge
|
||||||
- REST API: GET med tidsvindu-filtrering, POST/PATCH/DELETE hendelser
|
- 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
|
- Dra-og-slipp for å flytte hendelser mellom datoer
|
||||||
- Flerdag-hendelser (vises over flere celler)
|
- Flerdag-hendelser (vises over flere celler)
|
||||||
- Abonnementsmodell (kalender → kalender via graph_edges)
|
- Abonnementsmodell (kalender → kalender via graph_edges)
|
||||||
- Personlige vs. workspace-kalendere
|
- Personlige vs. delte kalendere (via samlings-noder)
|
||||||
- ICS/CalDAV-eksport
|
- ICS/CalDAV-eksport
|
||||||
- SpacetimeDB-modul + hybrid-adapter
|
- SpacetimeDB-modul + hybrid-adapter
|
||||||
- Varsler/påminnelser via jobbkøen
|
- 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 |
|
| 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 |
|
| Personlig | Bruker | Kun eier + eksplisitt deling |
|
||||||
| Offentlig | Workspace | Alle som abonnerer |
|
| Offentlig | Samlings-node | Alle som abonnerer |
|
||||||
|
|
||||||
## 8. Fremtidig: ICS-eksport
|
## 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
|
## 9. Instruks for Claude Code
|
||||||
* Bruk `eventsForDate()` med lokal dato-konvertering, ikke UTC-substring
|
* Bruk `eventsForDate()` med lokal dato-konvertering, ikke UTC-substring
|
||||||
* Heldagshendelser: `T12:00:00`, aldri `T00:00:00` (tidssone-felle)
|
* 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
|
* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Et drag-and-drop Kanban-brett for planlegging. Primært brukt til episodeplanleg
|
||||||
|
|
||||||
### Implementert
|
### Implementert
|
||||||
- Migrering `0002_kanban.sql`: `kanban_boards`, `kanban_columns`, `kanban_cards`
|
- 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
|
- 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
|
- 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
|
- 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)
|
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
|
## 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
|
## 6. Instruks for Claude Code
|
||||||
* Bruk native HTML5 Drag and Drop i SvelteKit, unngå tunge biblioteker.
|
* Bruk native HTML5 Drag and Drop i SvelteKit, unngå tunge biblioteker.
|
||||||
* PG-adapter er autoritativ inntil SpacetimeDB-sync er på plass.
|
* 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.
|
* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi.
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
# Feature: Kunnskaps-Bridge (Cross-Workspace Discovery)
|
# Feature: Kunnskaps-Bridge (Cross-Context Discovery)
|
||||||
**Filsti:** `docs/features/kunnskaps_bridge.md`
|
**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
|
## 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
|
## 2. Avgrensning: Hva dette IKKE er
|
||||||
- **Ikke datadeling.** Ingen data kopieres mellom workspaces. Bridge viser kun at en relevant node *finnes*.
|
- **Ikke datadeling.** Ingen data kopieres mellom samlings-noder. Bridge viser kun at en relevant node *finnes*.
|
||||||
- **Ikke automatisk.** Begge workspaces må ha Bridge eksplisitt aktivert av en admin.
|
- **Ikke automatisk.** Begge samlings-noder må ha Bridge eksplisitt aktivert av en admin.
|
||||||
- **Ikke synlig for gjester.** Kun for workspace-medlemmer med tilgang til begge sider.
|
- **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 workspaces du allerede er medlem av.
|
- **Ikke et søk i andres data.** Du ser bare treff i samlings-noder du allerede har tilgang til.
|
||||||
|
|
||||||
## 3. Teknisk arkitektur
|
## 3. Teknisk arkitektur
|
||||||
|
|
||||||
|
|
@ -35,32 +37,33 @@ En jobbkø-jobb som genererer embeddings for nye/endrede noder:
|
||||||
4. Lagrer vektoren i pgvector-kolonnen.
|
4. Lagrer vektoren i pgvector-kolonnen.
|
||||||
5. Re-genereres ved vesentlige endringer av noden.
|
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"):
|
Når en bruker utforsker en node (f.eks. Tema "Skolepolitikk"):
|
||||||
1. SvelteKit server-side henter brukerens tilgjengelige workspaces fra `workspace_members`.
|
1. SvelteKit server-side henter brukerens tilgjengelige samlings-noder fra `node_access`-matrisen.
|
||||||
2. Kjører et similarity-søk med `<=>` (cosine distance) **som superuser** (bypasser RLS) — men filtrerer eksplisitt mot brukerens workspace-liste:
|
2. Kjører et similarity-søk med `<=>` (cosine distance), filtrert mot brukerens tilgjengelige noder:
|
||||||
```sql
|
```sql
|
||||||
SELECT n.workspace_id, t.name, t.embedding <=> $target_embedding AS distance
|
SELECT na.node_id, e.name, e.embedding <=> $target_embedding AS distance
|
||||||
FROM topics t
|
FROM entities e
|
||||||
JOIN nodes n ON t.id = n.id
|
JOIN nodes n ON e.id = n.id
|
||||||
WHERE n.workspace_id = ANY($user_workspace_ids)
|
JOIN node_access na ON n.id = na.node_id
|
||||||
AND n.workspace_id != $current_workspace_id
|
WHERE na.user_id = $current_user
|
||||||
AND t.embedding <=> $target_embedding < 0.3
|
AND n.id != $current_node_id
|
||||||
|
AND e.embedding <=> $target_embedding < 0.3
|
||||||
ORDER BY distance
|
ORDER BY distance
|
||||||
LIMIT 10;
|
LIMIT 10;
|
||||||
```
|
```
|
||||||
3. Resultatet vises som en diskret "Finnes også i..."-seksjon i UI-et.
|
3. Resultatet vises som en diskret "Finnes også i..."-seksjon i UI-et.
|
||||||
|
|
||||||
### 3.4 Workspace-config
|
### 3.4 Samlings-node config
|
||||||
Bridge aktiveres per workspace i `workspaces.settings`:
|
Bridge aktiveres per samlings-node i nodens metadata (JSONB):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"bridge_enabled": true,
|
"bridge_enabled": true,
|
||||||
"bridge_discoverable": true
|
"bridge_discoverable": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- `bridge_enabled` — workspace kan søke i andre workspaces
|
- `bridge_enabled` — samlings-noden kan søke i andre samlings-noder
|
||||||
- `bridge_discoverable` — andre workspaces kan finne noder i dette workspace-et
|
- `bridge_discoverable` — andre samlings-noder kan finne noder under denne
|
||||||
|
|
||||||
Begge må være `true` for at en kobling skal vises.
|
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
|
## 5. Instruks for Claude Code
|
||||||
* pgvector er en ny avhengighet — dokumenter i docker-compose og setup-guides.
|
* 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.
|
* Embedding-dimensjon (768) bør matche modellen som brukes. Konfigurér som konstant, ikke hardkod overalt.
|
||||||
* Jobbtype `generate_embeddings` bruker `sidelinja/rutine` som modellalias.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ CREATE TYPE node_type AS ENUM (
|
||||||
|
|
||||||
CREATE TABLE nodes (
|
CREATE TABLE nodes (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
node_type node_type NOT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
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`
|
### 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
|
```sql
|
||||||
CREATE TABLE graph_edges (
|
CREATE TABLE graph_edges (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
target_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'
|
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_source ON graph_edges(source_id);
|
||||||
CREATE INDEX idx_edges_target ON graph_edges(target_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_relation ON graph_edges(relation_type);
|
||||||
CREATE INDEX idx_edges_workspace ON graph_edges(workspace_id);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Merk:** `UNIQUE(source_id, target_id, relation_type)` forhindrer duplikate relasjoner. Bruk `ON CONFLICT DO NOTHING` eller `ON CONFLICT DO UPDATE SET confidence = ...` ved upsert.
|
**Merk:** `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.
|
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
|
## 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.
|
* **`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.
|
* **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": [...] }`.
|
* **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.
|
* **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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
* 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.
|
* 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.
|
* Kill switch skal alltid være tilgjengelig i studio-modus — default: AI aktivert.
|
||||||
* Alt er workspace-scopet.
|
* Tilgang styres via `node_access`-matrisen.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
* **Benchmark og modellvalg:** Se `docs/concepts/podcastfabrikken.md` seksjon 4 for ytelsestall og anbefalinger.
|
||||||
|
|
||||||
### 4.1 initial_prompt (navneliste)
|
### 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:
|
Effekten er tydelig:
|
||||||
* Uten prompt: "Vegard Nøgnes", "SideLinja", "Sidlinja"
|
* Uten prompt: "Vegard Nøgnes", "SideLinja", "Sidlinja"
|
||||||
|
|
@ -32,9 +32,9 @@ Effekten er tydelig:
|
||||||
|
|
||||||
## 5. Lagring
|
## 5. Lagring
|
||||||
* **Live-modus:** Transkripsjonen er flyktig (kategori 4, TTL 30 dager). Lagres i `live_transcription_log` for feilsøking.
|
* **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
|
## 6. Instruks for Claude Code
|
||||||
* Live transkripsjon blokkerer ALDRI web-requests — prosesseres i Rust-worker eller separat tjeneste.
|
* 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`).
|
* 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).
|
||||||
|
|
|
||||||
|
|
@ -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).
|
2. Trykker record. Nettleseren fanger lyd via `MediaRecorder` API (WebM/Opus).
|
||||||
3. Kan valgfritt velge kontekst: et Tema, en Aktør, eller "Usortert".
|
3. Kan valgfritt velge kontekst: et Tema, en Aktør, eller "Usortert".
|
||||||
4. Ved stopp lastes lydfilen opp til SvelteKit (streaming for store filer).
|
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.
|
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.
|
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'`):
|
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 })
|
→ messages (channel_id, message_type = 'voice_memo', body = transkripsjon, metadata = { duration, transcription_status })
|
||||||
→ message_attachments → media_files (lydfilen)
|
→ 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:
|
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' })
|
→ 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)
|
- Diktering: `medium` + `initial_prompt` (kvalitet prioritert — teksten er master)
|
||||||
|
|
||||||
### 4.4 Personlig "Innboks"-channel
|
### 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
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -130,8 +130,8 @@ Ved workspace-opprettelse opprettes en privat channel per bruker for usorterte l
|
||||||
|
|
||||||
## 7. Instruks for Claude Code
|
## 7. Instruks for Claude Code
|
||||||
* Opptakskomponenten skal være en gjenbrukbar Svelte-komponent med modus-prop (`voice_memo` / `dictation`).
|
* 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.
|
* Diktering: slett lydfilen først *etter* at brukeren har godkjent den ryddede teksten.
|
||||||
* `voice_memo` er en ny `message_type` — utvid enum i migrasjonen.
|
* `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).
|
* Personlig innboks-channel opprettes automatisk for nye brukere.
|
||||||
* Alt er workspace-scopet.
|
* Tilgang styres via `node_access`-matrisen.
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ CREATE TABLE messages (
|
||||||
body TEXT NOT NULL,
|
body TEXT NOT NULL,
|
||||||
metadata JSONB, -- Ekstra data per message_type
|
metadata JSONB, -- Ekstra data per message_type
|
||||||
pinned BOOLEAN NOT NULL DEFAULT false,
|
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,
|
edited_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
updated_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:**
|
**Forskjeller fra gammel `messages`-tabell:**
|
||||||
- `id` er FK til `nodes(id)` — **alle meldinger er noder** i kunnskapsgrafen
|
- `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
|
- `channel_id` er nullable — notater og standalone-bokser trenger ikke en channel
|
||||||
- `title` er førsteklasses felt — brukes av kanban-kort, notater, kalenderhendelser
|
- `title` er førsteklasses felt — brukes av kanban-kort, notater, kalenderhendelser
|
||||||
- `pinned` for manuell fritak fra TTL-sletting
|
- `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`
|
### 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. Eierskap, kurasjon og prominens
|
||||||
|
|
||||||
### 8.1 Eierskap
|
### 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)
|
### 8.2 Kurasjon (TODO — UI-features, bygges inkrementelt)
|
||||||
Datamodellen trenger ikke endres for disse — alt håndteres via `messages.metadata` (JSONB):
|
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. TTL og livsløp
|
||||||
|
|
||||||
### 9.1 To-trinns fading
|
### 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.
|
2. **Slettet** — etter tilleggsperiode (dobbel TTL) fjernes raden permanent.
|
||||||
|
|
||||||
### 9.2 Alder som dynamisk faktor
|
### 9.2 Alder som dynamisk faktor
|
||||||
|
|
@ -260,7 +260,7 @@ Lever boksen, lever alt under den — svar beholdes uansett alder.
|
||||||
|
|
||||||
### 9.4 Konfigurerbarhet
|
### 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
|
└── Channel kan overstyre: config.ttl_days
|
||||||
└── Individuelle meldinger frittes via reglene over
|
└── 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **Tilgang:** Styres via `node_access`-matrisen. Visibility håndteres i applikasjonskode (SvelteKit): `WHERE visibility = 'shared' OR author_id = $current_user`.
|
||||||
- **Visibility:** Default `'workspace'`. Sett `'private'` for personlige kladder. Endre til `'workspace'` for å dele — ingen kopiering nødvendig.
|
- **Visibility:** Default `'shared'`. Sett `'private'` for personlige kladder. Endre til `'shared'` for å dele — ingen kopiering nødvendig.
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike ko
|
||||||
|
|
||||||
### Implementert
|
### Implementert
|
||||||
- Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes)
|
- 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")
|
- Auto-save med 500ms debounce (visuell feedback: "Lagrer..."/"Lagret")
|
||||||
- REST API: GET og PATCH (tittel + innhold)
|
- REST API: GET og PATCH (tittel + innhold)
|
||||||
- PG polling-adapter med 10 sek intervall (tregere enn chat/kanban — notater endres sjeldnere)
|
- 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
|
- Polling (10 sek) henter siste versjon, men hopper over overskriving mens `saving`-flagget er satt
|
||||||
|
|
||||||
## 7. Instruks for Claude Code
|
## 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
|
* Auto-save bruker debounce — ikke send PATCH ved hvert tastetrykk
|
||||||
* `updated_at` brukes til UI-feedback, ikke til conflict resolution (ennå)
|
* `updated_at` brukes til UI-feedback, ikke til conflict resolution (ennå)
|
||||||
* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi
|
* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
## 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.
|
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.
|
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 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 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.
|
* **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
|
## 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.
|
* 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).
|
* 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.
|
* Rålogger skal ALDRI lagres i PostgreSQL.
|
||||||
* Statistikk er workspace-scopet. `episode_stats` merkes med `workspace_id`. Admin-visningen filtrerer per workspace.
|
* Statistikk er tenant-scopet. `episode_stats` merkes med `tenant_id`. Admin-visningen filtrerer per tenant.
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# Feature: Prompt-Laboratorium
|
# Feature: Prompt-Laboratorium
|
||||||
**Filsti:** `docs/features/prompt_lab.md`
|
**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
|
## 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
|
## 2. Problemet det løser
|
||||||
- Modellkvalitet på norsk varierer kraftig mellom leverandører og versjoner.
|
- 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)
|
### 3.1 Playground (Ad-hoc testing)
|
||||||
1. Bruker velger en **jobbtype** (research_clip, whisper_postprocess, metadata_extract, dictation_cleanup, etc.).
|
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.
|
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`).
|
5. Velger **modeller** å teste mot (f.eks. `sidelinja/rutine` + `sidelinja/resonering`).
|
||||||
6. Kjører testen — ser resultatene side-om-side.
|
6. Kjører testen — ser resultatene side-om-side.
|
||||||
7. Kan iterere: endre prompten, kjør igjen, sammenlign.
|
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
|
### 3.3 Deploy
|
||||||
Når en prompt er verifisert:
|
Når en prompt er verifisert:
|
||||||
1. Bruker klikker "Deploy til workspace".
|
1. Bruker klikker "Deploy".
|
||||||
2. Prompten skrives til `workspaces.settings.llm_prompts[jobbtype]`.
|
2. Prompten skrives til samlings-nodens metadata (`metadata.llm_prompts[jobbtype]`).
|
||||||
3. Alle fremtidige jobber av den typen bruker den nye prompten.
|
3. Alle fremtidige jobber av den typen bruker den nye prompten.
|
||||||
4. Tidligere prompt lagres i en `prompt_history`-logg for rollback.
|
4. Tidligere prompt lagres i en `prompt_history`-logg for rollback.
|
||||||
|
|
||||||
|
|
@ -41,7 +43,7 @@ Når en prompt er verifisert:
|
||||||
```sql
|
```sql
|
||||||
prompt_test_runs (
|
prompt_test_runs (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
job_type TEXT NOT NULL,
|
||||||
system_prompt TEXT NOT NULL,
|
system_prompt TEXT NOT NULL,
|
||||||
model_alias TEXT NOT NULL,
|
model_alias TEXT NOT NULL,
|
||||||
|
|
@ -53,7 +55,7 @@ prompt_test_runs (
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
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.
|
`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
|
```sql
|
||||||
prompt_history (
|
prompt_history (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
job_type TEXT NOT NULL,
|
||||||
system_prompt TEXT NOT NULL,
|
system_prompt TEXT NOT NULL,
|
||||||
deployed_by TEXT NOT NULL REFERENCES users(authentik_id),
|
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 |
|
| `prompt_eval` | (varierer) | Batch-evaluering av testsett mot valgte modeller |
|
||||||
|
|
||||||
## 7. Instruks for Claude Code
|
## 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.
|
* 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.
|
* Batch-evaluering kjøres via jobbkø for å unngå timeouts.
|
||||||
* Vis alltid token-bruk og latens — dette er et kostnadsbevisst verktøy.
|
* 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.
|
* Testdata (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.
|
* `prompt_test_runs` og `prompt_history` er knyttet til en samlings-node og trenger ikke RLS — de er kun tilgjengelige for admins via applikasjonslogikk.
|
||||||
* Alt er workspace-scopet.
|
* Tilgang styres via `node_access`-matrisen.
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ Høyreklikk kort → "Send til..." →
|
||||||
└── 🎬 Storyboard: Episode 47
|
└── 🎬 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
|
### 3.3 Flytt vs. kopier
|
||||||
|
|
||||||
|
|
@ -148,7 +148,6 @@ pub struct MessagePlacement {
|
||||||
pub context_id: String,
|
pub context_id: String,
|
||||||
pub entered_at: Timestamp,
|
pub entered_at: Timestamp,
|
||||||
pub position_json: String, // JSON-serialisert posisjon
|
pub position_json: String, // JSON-serialisert posisjon
|
||||||
pub workspace_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[reducer]
|
#[reducer]
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,4 @@ En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt naviger
|
||||||
## 4. Instruks for Claude Code
|
## 4. Instruks for Claude Code
|
||||||
* Design JSON-responsen slik at den lett kan mates inn i graf-visualiseringsbiblioteker (D3.js, Vis.js).
|
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst
|
||||||
|
|
||||||
## 3. Sanntidssynkronisering
|
## 3. Sanntidssynkronisering
|
||||||
* **SpacetimeDB** synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid.
|
* **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.
|
* Tilgangskontroll følger konteksten: møterom-deltakere, tema-medlemmer, eller kun eieren for personlige tavler.
|
||||||
|
|
||||||
## 4. Funksjonalitet
|
## 4. Funksjonalitet
|
||||||
|
|
@ -24,11 +24,11 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst
|
||||||
|
|
||||||
## 5. Lagring og Eksport
|
## 5. Lagring og Eksport
|
||||||
* **Sanntidsdata (SpacetimeDB):** Strøk-historikk holdes i minnet så lenge tavlen er aktiv.
|
* **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.
|
* **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
|
## 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.
|
* 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`.
|
* 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.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
**Filsti:** `docs/infra/ai_gateway.md`
|
**Filsti:** `docs/infra/ai_gateway.md`
|
||||||
|
|
||||||
## 1. Konsept
|
## 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:
|
Fordeler:
|
||||||
* **BYOK (Bring Your Own Key):** Direkte API-nøkler til Anthropic, Google, xAI — ingen markup
|
* **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
|
### 3.2 Datamodell
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Globale modellaliaser (server-nivå, ikke per workspace)
|
-- Globale modellaliaser (server-nivå)
|
||||||
CREATE TABLE ai_model_aliases (
|
CREATE TABLE ai_model_aliases (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
alias TEXT NOT NULL, -- 'sidelinja/rutine', 'sidelinja/resonering'
|
alias TEXT NOT NULL, -- 'sidelinja/rutine', 'sidelinja/resonering'
|
||||||
|
|
@ -180,14 +180,14 @@ tests/prompts/
|
||||||
|
|
||||||
## 6. Tokenregnskap og kostnadskontroll
|
## 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:
|
Rust-workeren logger tokenforbruk etter hvert AI-kall. Dataen lagres i PG:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE ai_usage_log (
|
CREATE TABLE ai_usage_log (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
job_id UUID REFERENCES job_queue(id) ON DELETE SET NULL,
|
||||||
model_alias TEXT NOT NULL, -- 'sidelinja/rutine'
|
model_alias TEXT NOT NULL, -- 'sidelinja/rutine'
|
||||||
model_actual TEXT, -- 'gemini/gemini-2.5-flash' (fra LiteLLM-respons)
|
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()
|
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:**
|
**Flyten:**
|
||||||
1. Rust-worker sender AI-kall via gateway, får tilbake `usage` i responsen
|
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)
|
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`):**
|
**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):**
|
**Samlings-node (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.
|
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:
|
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
|
- Rust-worker sjekker aggregert forbruk før AI-kall
|
||||||
- Ved budsjett nær: fall tilbake til `sidelinja/rutine` (billigste)
|
- 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
|
### 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).
|
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).
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'ret
|
||||||
|
|
||||||
CREATE TABLE job_queue (
|
CREATE TABLE job_queue (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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'
|
job_type TEXT NOT NULL, -- 'whisper_transcribe', 'openrouter_analyze', 'stats_parse', 'research_clip'
|
||||||
payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.)
|
payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.)
|
||||||
status job_status NOT NULL DEFAULT 'pending',
|
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 |
|
| `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 |
|
| `generate_waveform` | Waveforms (proposal) | Generer audio-peaks fra lydfil for visuell bølgeform |
|
||||||
|
|
||||||
## 6. Workspace-isolasjon
|
## 6. Tilgangsisolasjon
|
||||||
Alle jobber merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode:
|
Alle jobber merkes med `collection_node_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
|
* Worker leser `collection_node_id` fra jobben og bruker det til å lagre resultater tilbake i riktig samlings-node
|
||||||
* Workspace-spesifikk config (AI-prompts, navnelister) hentes fra `workspaces.settings`
|
* Per samlings-node config (AI-prompts, navnelister) hentes fra samlings-nodens JSONB-metadata
|
||||||
* Feilede jobber vises kun for brukere i riktig workspace i admin-visningen
|
* Feilede jobber vises kun for brukere med tilgang til samlings-noden (via node_access) i admin-visningen
|
||||||
|
|
||||||
## 7. Observabilitet
|
## 7. Observabilitet
|
||||||
- Jobber med `status = 'error'` skal være synlige i admin-visningen (SvelteKit `/admin/jobs`)
|
- 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<String, Handler>`
|
- Hver jobbtype implementeres som en handler-funksjon som registreres i en `HashMap<String, Handler>`
|
||||||
- Bruk `tokio` med semaphore for concurrency-kontroll (`--max-concurrent`)
|
- Bruk `tokio` med semaphore for concurrency-kontroll (`--max-concurrent`)
|
||||||
- Aldri lagre lydfiler i `payload` — bruk filstier
|
- 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
|
- 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
|
- Splitt til flere binærer kun hvis det blir eksplisitt bedt om — start med én
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,106 @@
|
||||||
# Infrastruktur: PostgreSQL ↔ SpacetimeDB Synkronisering
|
# Synkronisering: SpacetimeDB ↔ PostgreSQL
|
||||||
**Filsti:** `docs/infra/synkronisering.md`
|
|
||||||
|
|
||||||
## 1. Konsept
|
## 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 holder hele grafen (alle noder og edges) i minne.
|
||||||
SpacetimeDB skal **aldri** være eneste lagringssted for data med varig verdi. All data som SpacetimeDB holder skal enten:
|
PostgreSQL er persistent backup og arkiv. Skriving går til begge.
|
||||||
1. Allerede eksistere i PostgreSQL (read-only cache, f.eks. aktør-navn for autocomplete), eller
|
Lesing går fra SpacetimeDB.
|
||||||
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 ┌──────────────┐
|
GUI (SvelteKit)
|
||||||
│ SpacetimeDB │ ──────────────► │ Rust Worker │ ────────────────► │ PostgreSQL │
|
│ skriv │ les (sanntid)
|
||||||
│ (sanntid) │ │ (sync_to_pg) │ │ (persistent)│
|
▼ ▼
|
||||||
└──────────────┘ └──────────────┘ └──────────────┘
|
Maskinrommet (Rust) SpacetimeDB ──→ GUI
|
||||||
|
│ ▲
|
||||||
┌──────────────┐ oppvarming (oppstart / reconnect) ┌──────────────┐
|
├──→ PostgreSQL (persistent) │
|
||||||
│ PostgreSQL │ ──────────────────────────────────────► │ SpacetimeDB │
|
└──→ SpacetimeDB ───────────┘
|
||||||
└──────────────┘ └──────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Eierskapsmodell
|
## Hvorfor hele grafen i SpacetimeDB
|
||||||
|
|
||||||
| Data | Autoritativ kilde | Synkretning | Merknad |
|
Node- og edge-skjemaet er minimalt: åtte kolonner på nodes, åtte
|
||||||
|---|---|---|---|
|
på edges. For en liten brukerbase er hele grafen triviell å holde
|
||||||
| Chatmeldinger | SpacetimeDB | → PG (event, batched) | |
|
i minne. Dette forenkler alt:
|
||||||
| 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
|
- 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
|
||||||
|
|
||||||
### 5.1 SpacetimeDB → PostgreSQL (persistering)
|
Når dette *blir* problematisk (hundretusenvis av noder, minnepress),
|
||||||
- SpacetimeDB-reducerne legger sync-events i `sync_outbox`-tabellen med tidsstempel og payload
|
innføres eviction basert på aksessmønstre. Men det er en
|
||||||
- Rust-workeren poller `sync_outbox` hvert sekund, leser alle usynkede events, skriver til PostgreSQL, og markerer dem som synket via `mark_synced`-reducer
|
optimaliseringsbeslutning, ikke en arkitekturbeslutning. Modellen
|
||||||
- Ved PG-nedetid: events akkumuleres i `sync_outbox`. Workeren prøver igjen ved neste poll. Ingen data tapes så lenge SpacetimeDB kjører
|
endres ikke.
|
||||||
|
|
||||||
### 5.2 PostgreSQL → SpacetimeDB (oppvarming)
|
## Skrivestien
|
||||||
- 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
|
Maskinrommet validerer, så skriver i to steg:
|
||||||
- **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
|
1. **Validering** — tilgangssjekk, unique-constraints, forretningsregler
|
||||||
SpacetimeDB-synkronisering er fullstendig workspace-scopet:
|
2. **SpacetimeDB først** — frontend oppdateres umiddelbart (~10μs)
|
||||||
* SpacetimeDB-tilkoblinger bærer `workspace_id` som context-token. Modulen partisjonerer minnet og kringkaster kun til klienter i samme workspace.
|
3. **PG asynkront** — persistent backup i bakgrunnen
|
||||||
* `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
|
Frontend er allerede oppdatert mens PG-skrivingen skjer. Brukeren
|
||||||
|
merker ingenting.
|
||||||
|
|
||||||
### 8.1 Strategi: Last-Write-Wins (LWW) med reservasjoner
|
Hvis PG-skrivingen feiler: maskinrommet logger og prøver igjen.
|
||||||
SpacetimeDB er autoritativ for sanntidsdata. Konflikter kan oppstå i to scenarioer:
|
SpacetimeDB har dataen — den er bare ikke persistert ennå. Ingen
|
||||||
|
datatap så lenge SpacetimeDB kjører.
|
||||||
|
|
||||||
**Scenario A — Samtidig redigering i SpacetimeDB:**
|
## Lesestien
|
||||||
SpacetimeDB er single-threaded per modul. Reducer-funksjoner serialiseres automatisk. Ingen klassiske race conditions.
|
|
||||||
|
|
||||||
**Scenario B — PG-oppvarming kolliderer med fersk SpacetimeDB-state:**
|
Frontend leser **kun fra SpacetimeDB** via WebSocket-subscriptions.
|
||||||
Ved restart av SpacetimeDB lastes data fra PG (oppvarming). Hvis en klient skriver til SpacetimeDB *under* oppvarming, kan PG-dataen overskrive ferske endringer.
|
Ingen PG-kall fra frontend for data som vises i sanntid.
|
||||||
|
|
||||||
**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.
|
Unntak: tunge spørringer som ikke passer sanntidslaget — statistikk,
|
||||||
|
fulltekstsøk, pgvector-søk, AGE-traverseringer. Disse går
|
||||||
|
GUI → Maskinrommet → PG.
|
||||||
|
|
||||||
### 8.2 Kanban: Posisjonskonflikter
|
## Oppvarming (PG → SpacetimeDB)
|
||||||
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
|
Ved oppstart av SpacetimeDB laster maskinrommet hele grafen fra PG:
|
||||||
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
|
1. Alle noder
|
||||||
|
2. Alle edges
|
||||||
|
3. `node_access`-matrisen
|
||||||
|
|
||||||
**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.
|
Rekkefølge: noder først (edges refererer til noder).
|
||||||
|
CAS-noder lastes uten binærinnhold (bare metadata).
|
||||||
|
|
||||||
### Eksempel: AI-vask av chatmelding
|
## Feilhåndtering
|
||||||
|
|
||||||
**Riktig flyt:**
|
| Scenario | Konsekvens | Håndtering |
|
||||||
```
|
|----------|-----------|------------|
|
||||||
Frontend viser melding fra SpacetimeDB
|
| SpacetimeDB krasjer | Sanntid forsvinner, upersistert data kan tapes | Restart + oppvarming fra PG. Tap begrenset til skrivinger som ikke rakk PG. |
|
||||||
→ Bruker trykker ✨
|
| PG nede | Persistering stopper | SpacetimeDB serverer lesing og mottar skrivinger. PG-backlog tas igjen ved recovery. |
|
||||||
→ SvelteKit oppretter jobb i PG job_queue
|
| Maskinrommet krasjer | Ingen skriving | Frontend ser siste state. Restart plukker opp. |
|
||||||
→ 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):**
|
## SpacetimeDB som utbyttbar
|
||||||
```
|
|
||||||
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?
|
SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den
|
||||||
|
fjernes fra stacken:
|
||||||
|
|
||||||
Når frontend trenger å vise noe nytt (metadata, revisjoner, edited_at), er prosedyren:
|
- **Sanntid:** PG `LISTEN/NOTIFY` → SvelteKit SSE
|
||||||
|
- **Skriving:** Maskinrommet → PG direkte
|
||||||
|
- **Lesing:** Maskinrommet → PG → GUI
|
||||||
|
|
||||||
1. **Utvid SpacetimeDB-modulen** — legg til feltet i Rust-strukturen
|
Denne fallbacken trenger ikke implementeres, men arkitekturen
|
||||||
2. **Utvid warmup** — last feltet fra PG ved oppstart
|
skal aldri gjøre den umulig.
|
||||||
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?
|
## Tilgangsmatrise i SpacetimeDB
|
||||||
|
|
||||||
Workers kan og bør lese infrastrukturdata direkte fra PG:
|
`node_access`-matrisen lastes inn i SpacetimeDB ved oppvarming.
|
||||||
- Jobbkø (`job_queue`) — det er jobbens opphavssted
|
Når maskinrommet oppdaterer matrisen i PG (ved edge-endring),
|
||||||
- AI-prompts, modellkonfigurasjon, routing — det er infrastruktur, ikke brukerdata
|
oppdaterer den også SpacetimeDB. SpacetimeDB-modulen bruker
|
||||||
- Workspace-konfigurasjon
|
matrisen for å filtrere subscriptions — klienter ser kun noder
|
||||||
|
de har tilgang til.
|
||||||
|
|
||||||
Skillet er: **data frontend viser** → SpacetimeDB. **Data kun worker trenger** → PG direkte.
|
## Konflikthåndtering
|
||||||
|
|
||||||
## 10. Implementeringsstatus (mars 2026)
|
SpacetimeDB er single-threaded per modul — reducer-funksjoner
|
||||||
|
serialiseres automatisk. Ingen klassiske race conditions.
|
||||||
|
|
||||||
### Ferdig
|
Samtidig redigering (to brukere endrer samme node): maskinrommet
|
||||||
- **SpacetimeDB som cache foran PG:** PG er autoritativ, SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB.
|
serialiserer via SpacetimeDB. Last-write-wins. 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`.
|
kringkaster resultatet til alle klienter. PG oppdateres asynkront
|
||||||
- **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.
|
med det endelige resultatet.
|
||||||
- **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
|
|
||||||
|
|
|
||||||
117
docs/primitiver/edges.md
Normal file
117
docs/primitiver/edges.md
Normal file
|
|
@ -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.
|
||||||
140
docs/primitiver/nodes.md
Normal file
140
docs/primitiver/nodes.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# Forslag: Personlig workspace
|
# 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é
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| [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? |
|
| [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 |
|
| [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. |
|
||||||
| [Maskinrommet](maskinrommet.md) | Åpen | Én Rust-tjeneste med fast grensesnitt for alle tekniske tjenester: fang, prosesser, lever |
|
| [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. |
|
||||||
| [Bruker, ikke workspace](bruker_ikke_workspace.md) | Åpen | Brukeren er sentrum, workspaces er frivillige samlings-noder — ikke containere |
|
| [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) | Åpen | PG + Apache AGE som enhetlig graf og arkiv, SpacetimeDB som sanntidslag, CAS for binærdata |
|
| [Datalaget](datalaget.md) | **Besluttet** | SpacetimeDB holder hele grafen, PG er persistent arkiv, CAS for binærdata, AGE ved behov |
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
- Hva er tesen?
|
- Hva er tesen?
|
||||||
|
|
|
||||||
|
|
@ -1,154 +1,327 @@
|
||||||
# Bruker, ikke workspace
|
# Noder er sentrum
|
||||||
|
|
||||||
> Primærenheten er brukeren og brukerens edges. Workspaces er ikke
|
**Status: Besluttet.**
|
||||||
> containere — de er frivillige samlings-noder som gir felles kontekst.
|
|
||||||
|
|
||||||
## Observasjoner
|
> 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.
|
||||||
|
|
||||||
I dag er workspaces den organisatoriske enheten. Du "er i" et workspace,
|
## Beslutningen
|
||||||
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
|
1. **Alt er noder.** Brukere, team, prosjekter, innhold, møter —
|
||||||
— den har edges til ting. Og brukeren er ikke "i" et workspace — brukeren
|
alt er rader i `nodes`-tabellen. En bruker er en node som
|
||||||
har edges til noder, personer, topics, samtaler.
|
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.
|
||||||
|
|
||||||
## Tesen
|
## Visibility
|
||||||
|
|
||||||
**Brukeren er sentrum.** Du logger inn og ser dine edges — alt du er
|
Visibility er en egenskap på noden som definerer hva som gjelder
|
||||||
koblet til. Ikke "velg workspace" som første handling, men "her er alt
|
for alle *uten* eksplisitt edge. Eksplisitte edges overrider alltid
|
||||||
ditt."
|
oppover.
|
||||||
|
|
||||||
### Workspace som samlings-node, ikke container
|
```sql
|
||||||
|
CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open');
|
||||||
|
```
|
||||||
|
|
||||||
Et workspace eksisterer fortsatt som konsept, men det er en node i
|
| Nivå | Oppdagbar | Lesbar | Interagerbar |
|
||||||
grafen — ikke en organisatorisk boks:
|
|------|-----------|--------|-------------|
|
||||||
|
| `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 |
|
||||||
|
|
||||||
- **Tradisjonell modell:** Workspace inneholder kanaler, boards, filer.
|
Eksempler:
|
||||||
Brukeren er "i" workspacet.
|
- Spøkelsesbruker: `hidden` — usynlig, kun invitasjon
|
||||||
- **Node-modell:** Workspace er en node. Noder har edge til den. Brukeren
|
- Katalogbruker: `discoverable` — finnes i søk, profil krever edge
|
||||||
har edge til den. Workspace-noden bærer felles kontekst (tema,
|
- Podcast-episode: `readable` — alle kan lytte, bare teamet kan redigere
|
||||||
pruning-profil, AI-konfig).
|
- Kunnskapsgraf-entitet: `open` — alle kan se og bidra
|
||||||
|
|
||||||
En node kan ha edge til flere workspaces. En faktoid om en gjest er
|
### Traverseringsregelen
|
||||||
relevant for både podcast-prosjektet og research-samlingen. Ikke kopi —
|
|
||||||
samme node, to edges.
|
|
||||||
|
|
||||||
### Personlig er default
|
**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.
|
||||||
|
|
||||||
Alt uten workspace-edge eller mottaker-edge er privat — tilhører bare
|
Eksempel: Episode #42 (`readable`) har en `host`-edge til Peter
|
||||||
deg. "Personlig workspace" er ikke en egen ting du oppretter. Det er
|
(`hidden`). Anonym bruker leser episoden, følger `host`-edge →
|
||||||
fravær av deling. Dine private notater, dagbok, voice memos — de har
|
Peter er `hidden` → **stopp, usynlig.** Trond (som har edge til
|
||||||
bare edges til deg.
|
Peter) følger samme edge → **ser Peter.**
|
||||||
|
|
||||||
### Administrasjon er edges med roller
|
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.
|
||||||
|
|
||||||
Brukerstyring forvaltes i nodene selv, ikke i et sentralt admin-panel:
|
## Brukere er noder
|
||||||
|
|
||||||
| Edge-type | Hva den gir |
|
En brukernode er en node med en kobling til Authentik for
|
||||||
|-----------|------------|
|
autentisering. Alt annet — navn, preferanser, roller, relasjoner —
|
||||||
| Eier-edge | Full kontroll — slette, endre tilgang, endre innstillinger |
|
er noden og dens edges.
|
||||||
| 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
|
`users`-tabellen krymper til autentisering:
|
||||||
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
|
```sql
|
||||||
organisasjon (samlings-node med mange admin- og deltaker-edges).
|
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
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### Brukeropplevelsen
|
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:
|
Når du logger inn ser du:
|
||||||
- **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg
|
- **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg
|
||||||
- **Dine noder** — alt du har skapt eller er koblet til
|
- **Dine noder** — alt du har skapt eller er koblet til
|
||||||
- **Dine samlings-noder** (det som før var workspaces) — grupperer
|
- **Dine samlings-noder** — grupperer kontekst, filtrering er frivillig
|
||||||
kontekst, men du trenger ikke "gå inn i" dem
|
- **Din mottaksflate** — det som er relevant for deg nå
|
||||||
- **Din mottaksflate** — alt som er relevant for deg nå, vektet
|
|
||||||
|
|
||||||
Du kan filtrere etter samlings-node hvis du vil fokusere ("vis bare
|
Å filtrere etter samlings-node ("vis bare podcast-prosjektet") er et
|
||||||
podcast-prosjektet"), men det er et filter — ikke en modebytte.
|
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
|
## Forhold til andre retninger
|
||||||
|
|
||||||
- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er ikke et workspace,
|
- [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er summen av dine
|
||||||
det er summen av dine edges
|
edges, ikke en container du går inn i
|
||||||
- [Universell input og mottak](universell_input.md) — mottaksflaten er
|
- [Universell input og mottak](universell_input.md) — mottaksflaten
|
||||||
"noder med edge til *meg*", ikke "noder i *mitt workspace*"
|
er "noder med edge til *meg*", filtrert og vektet
|
||||||
- [Maskinrommet](maskinrommet.md) — leser samlings-node-edges for
|
- [Maskinrommet](maskinrommet.md) — eier matrise-oppdatering og leser
|
||||||
kontekst (pruning, kapasitet, AI-konfig)
|
samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig)
|
||||||
|
- [Datalaget](datalaget.md) — matrisen lever i PG, indeksert for
|
||||||
|
RLS-ytelse
|
||||||
|
|
|
||||||
|
|
@ -1,182 +1,97 @@
|
||||||
# Datalaget — PG + AGE som enhetlig graf og arkiv
|
# Datalaget
|
||||||
|
|
||||||
> PostgreSQL er den eneste databasen. Apache AGE gir Cypher-semantikk
|
**Status: Besluttet.**
|
||||||
> for graftraverseringer. SpacetimeDB er sanntidslaget over samme data.
|
|
||||||
> Én database, to tilgangsmønstre, ingen eierskapskonflikt.
|
|
||||||
|
|
||||||
## Observasjoner
|
> SpacetimeDB holder hele grafen i minne og mottar skrivinger først.
|
||||||
|
> PostgreSQL er persistent arkiv og backup. CAS lagrer binærdata.
|
||||||
Hele retningsarbeidet har bygget seg mot "alt er noder og edges."
|
> Apache AGE legges til ved behov for Cypher-traverseringer.
|
||||||
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
|
## Lagmodell
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
GUI (SvelteKit)
|
||||||
│ GUI (SvelteKit) │
|
│ skriv │ les (sanntid, WebSocket)
|
||||||
│ Primitiver, visninger │
|
▼ ▼
|
||||||
└────────┬──────────────────┬─────────┘
|
Maskinrommet (Rust) SpacetimeDB ──→ GUI
|
||||||
│ skriv │ les (sanntid)
|
│ validering ▲
|
||||||
│ │ direkte WebSocket
|
├──→ SpacetimeDB (først) ───┘
|
||||||
┌────────▼────────┐ ┌─────▼─────────┐
|
└──→ PostgreSQL (asynk, persistent)
|
||||||
│ Maskinrommet │ │ SpacetimeDB │──┐
|
|
||||||
│ (Rust) │ │ (STDB) │ │
|
|
||||||
│ Orkestrering │ └───────────────┘ │
|
|
||||||
└──┬─────┬─────┬──┘ │
|
|
||||||
│ │ │ sync ↕ │
|
|
||||||
▼ ▼ ▼ │
|
|
||||||
┌─────┐┌─────┐┌─────┐┌─────────────┐ │
|
|
||||||
│ PG ││STDB ││ CAS ││ Whisper, │ │
|
|
||||||
│+AGE ││(skr)││ ││ LiteLLM, │ │
|
|
||||||
│ ││ ││ ││ LiveKit ... │ │
|
|
||||||
└─────┘└─────┘└─────┘└─────────────┘ │
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Skriv vs les — to stier med god grunn
|
### Skrivestien
|
||||||
|
GUI → Maskinrommet → validering → SpacetimeDB (instant) → PG (asynk).
|
||||||
|
Frontend oppdateres umiddelbart. PG persisterer i bakgrunnen.
|
||||||
|
|
||||||
**Skriv:** GUI → Maskinrommet (Rust) → tjenester (PG, STDB, CAS, ...)
|
### Lesestien (sanntid)
|
||||||
|
SpacetimeDB → GUI (direkte WebSocket, ~10μs).
|
||||||
|
Hele grafen er i SpacetimeDB. Frontend har alltid alt tilgjengelig.
|
||||||
|
|
||||||
All orkestrering, edge-logikk, ressursallokering og validering går
|
### Lesestien (tunge spørringer)
|
||||||
gjennom Rust. Maskinrommet bestemmer hva som skjer: hva som skrives
|
GUI → Maskinrommet → PG.
|
||||||
til PG, hva som speiles til SpacetimeDB, hva som lagres i CAS,
|
Fulltekstsøk, pgvector (semantisk søk), statistikk, AGE-traverseringer.
|
||||||
hvilke tjenester som trigges.
|
|
||||||
|
|
||||||
**Les (sanntid):** SpacetimeDB → GUI (direkte WebSocket)
|
## SpacetimeDB — hele grafen i minne
|
||||||
|
|
||||||
SpacetimeDB sitt klient-SDK kobler seg direkte via WebSocket,
|
Node- og edge-skjemaet er minimalt (åtte kolonner hver). For en
|
||||||
definerer SQL-subscriptions, og synkroniserer automatisk med lokal
|
liten brukerbase er hele grafen triviell å holde i minne. Fordeler:
|
||||||
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
|
- 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
|
||||||
|
|
||||||
Søk, historikk, statistikk, arkiv — alt som ikke er sanntid går
|
Når dette blir problematisk (hundretusenvis av noder), innføres
|
||||||
gjennom Rust og PG. AGE-spørringer for graftraversering, pgvector
|
eviction. Det er en optimalisering, ikke en arkitekturendring.
|
||||||
for semantisk søk, SQL for aggregeringer.
|
|
||||||
|
|
||||||
### PG er arkivet og grafen
|
## PostgreSQL — arkiv og kraftspørringer
|
||||||
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
|
PG er persistent backup for hele grafen, pluss hjemsted for
|
||||||
Aktive noder og edges speilet til SpacetimeDB for live-oppdateringer.
|
tunge operasjoner SpacetimeDB ikke er laget for:
|
||||||
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
|
- **Fulltekstsøk** — `tsvector` på `nodes.content` og `nodes.title`
|
||||||
Lyd, bilde, video lagres content-addressable utenfor PG. Noder i
|
- **Semantisk søk** — pgvector for embedding-basert likhet
|
||||||
PG peker på CAS-hasher. Pruning-regler basert på edges, aksesslog
|
- **Graftraversering** — rekursive CTEs, Apache AGE ved behov
|
||||||
og modalitet (se maskinrommet).
|
- **Statistikk** — aggregeringer, tidsserier
|
||||||
|
- **Tilgangsmatrise** — `node_access` beregnes her, speiles til STDB
|
||||||
|
|
||||||
## Migreringsstrategi
|
### Apache AGE — ved behov
|
||||||
|
|
||||||
### Fase 1: AGE på eksisterende PG
|
De fleste spørringer er grunne (1-3 hopp) og håndteres av CTEs.
|
||||||
Installer Apache AGE extension. Eksponer eksisterende nodes/edges-
|
AGE legges til som PG-extension når Cypher-semantikk faktisk trengs:
|
||||||
tabeller som graf. Ingen endring for resten av systemet — bare en
|
|
||||||
ny måte å spørre på.
|
|
||||||
|
|
||||||
### Fase 2: Konsolider til node+edge-modellen
|
1. **Nå:** PG med nodes/edges-tabeller og CTEs
|
||||||
Migrer meldingsboks, kanban, kalender etc. til den universelle
|
2. **Når CTEs blir smertefulle:** Legg til AGE
|
||||||
node+edge-modellen gradvis. Gamle tabeller kan leve som views
|
3. **Usannsynlig:** Evaluer Neo4j hvis AGE ikke holder
|
||||||
over grafen i overgangsperioden.
|
|
||||||
|
|
||||||
### Fase 3: Parallell prototype
|
AGE er en extension, ikke en migrering — boltes på uten å endre
|
||||||
Det gamle systemet kjører uforstyrret. En ny frontend-prototype
|
eksisterende kode.
|
||||||
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
|
## CAS — binærlagring
|
||||||
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
|
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.
|
||||||
|
|
||||||
- **AGE-modenhet.** Prosjektet er aktivt og støttet av Azure/EDB,
|
Pruning-regler basert på modalitet, edges og aksessmønstre.
|
||||||
men community er mindre enn Neo4j. Risikoen er håndterbar fordi
|
Se [maskinrommet](maskinrommet.md).
|
||||||
dataene alltid er PG-tabeller — AGE er bare et spørrelag.
|
|
||||||
- **SpacetimeDB-synk.** Hvordan synkes noder/edges mellom PG (AGE)
|
## SpacetimeDB som utbyttbar
|
||||||
og SpacetimeDB effektivt? Trenger en sync-mekanisme som forstår
|
|
||||||
"aktiv delmengde."
|
SpacetimeDB er en sanntidscache, ikke en avhengighet. Hvis den
|
||||||
- **Skjemadesign.** Én node-tabell med alle typer innhold — hva er
|
fjernes:
|
||||||
felles kolonner vs edge-metadata vs JSONB-payload? Trenger
|
|
||||||
gjennomtenkt skjema som balanserer fleksibilitet og spørrbarhet.
|
- **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
|
## 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
|
- [Universell input og mottak](universell_input.md) — noder og edges
|
||||||
er den underliggende datamodellen for alle tre primitiver
|
er datamodellen for alle tre primitiver
|
||||||
- [Maskinrommet](maskinrommet.md) — CAS og pruning lever her, AGE
|
- [Maskinrommet](maskinrommet.md) — CAS-pruning, edge-drevet
|
||||||
brukes for edge-drevet ressursorkestrering
|
ressursorkestrering, validering før skriving
|
||||||
- [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
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,13 @@
|
||||||
# Maskinrommet — teknisk tjenestelaget
|
# Maskinrommet
|
||||||
|
|
||||||
> Én Rust-tjeneste med et fast grensesnitt. Alle tekniske tjenester beveger
|
**Status: Besluttet.**
|
||||||
> seg gjennom dette laget. Fang, prosesser, lever.
|
|
||||||
|
|
||||||
## Observasjoner
|
> É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.
|
||||||
|
|
||||||
I dag er tekniske tjenester spredt:
|
## Tre operasjoner
|
||||||
- **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)
|
### 1. Fang (input-absorpsjon)
|
||||||
Ta imot råmateriale i alle modaliteter:
|
Ta imot råmateriale i alle modaliteter:
|
||||||
|
|
@ -36,9 +21,8 @@ Ta imot råmateriale i alle modaliteter:
|
||||||
Analyser, transformer, berik og systematiser:
|
Analyser, transformer, berik og systematiser:
|
||||||
- **STT** — lyd → tekst (Whisper)
|
- **STT** — lyd → tekst (Whisper)
|
||||||
- **TTS** — tekst → lyd (ElevenLabs / lokal modell)
|
- **TTS** — tekst → lyd (ElevenLabs / lokal modell)
|
||||||
- **AI-analyse** — oppsummering, klassifisering, sentimentanalyse,
|
- **AI-analyse** — oppsummering, klassifisering, edge-forslag
|
||||||
faktasjekk, edge-forslag
|
- **Beriking** — URL → metadata, bilde → beskrivelse
|
||||||
- **Beriking** — URL → metadata, bilde → beskrivelse, lyd → segmenter
|
|
||||||
- **Søk** — fulltekst, semantisk (pgvector), graftraversering
|
- **Søk** — fulltekst, semantisk (pgvector), graftraversering
|
||||||
- **Mediaprosessering** — transcode, thumbnail, waveform
|
- **Mediaprosessering** — transcode, thumbnail, waveform
|
||||||
|
|
||||||
|
|
@ -46,24 +30,40 @@ Analyser, transformer, berik og systematiser:
|
||||||
Lever resultat i riktig modalitet til riktig mottaker:
|
Lever resultat i riktig modalitet til riktig mottaker:
|
||||||
- Tekst (melding, notifikasjon, digest)
|
- Tekst (melding, notifikasjon, digest)
|
||||||
- Lyd (TTS-opplesning, lydstream)
|
- Lyd (TTS-opplesning, lydstream)
|
||||||
- Video/bilde (stream, thumbnail, snapshot)
|
- Video/bilde (stream, thumbnail)
|
||||||
- Strukturert data (noder, edges, metadata tilbake i grafen)
|
- Strukturert data (noder, edges tilbake i grafen)
|
||||||
- Push (webhook, SSE, SpacetimeDB-reducer)
|
- 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
|
## Edge-drevet ressursorkestrering
|
||||||
|
|
||||||
Nøkkelinnsikten: **maskinrommet leser edges for å vite hva det skal gjøre.**
|
Maskinrommet leser edges for å vite hva det skal gjøre. Noden
|
||||||
Noden selv er alltid enkel. Det er edgene som bestemmer hvilke ressurser
|
er alltid enkel. Edges bestemmer hvilke ressurser som spinnes opp.
|
||||||
som spinnes opp.
|
|
||||||
|
|
||||||
### Security by default
|
### Privat er default
|
||||||
Input uten mottaker-edge er automatisk privat. Du trenger ikke "velge
|
Input uten mottaker-edge er automatisk privat. Ingen ressurser
|
||||||
privat" — det er utgangspunktet. Ingen ser det. Ingen ressurser kobles
|
kobles inn utover det grunnleggende (fang + transkriber).
|
||||||
inn utover det grunnleggende (fang + transkriber). Privat er ikke en
|
|
||||||
innstilling, det er fravær av deling.
|
|
||||||
|
|
||||||
### Ressurser er proporsjonale med edges
|
### Ressurser er proporsjonale med edges
|
||||||
Samme nodetype, vilt forskjellig ressursbruk:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Dagboknotat (privat voice memo):
|
Dagboknotat (privat voice memo):
|
||||||
|
|
@ -81,242 +81,116 @@ Redaksjonsmøte (5 deltakere):
|
||||||
Ressurser: STT + levering til 5 + LLM
|
Ressurser: STT + levering til 5 + LLM
|
||||||
|
|
||||||
Livesending (1000 lyttere):
|
Livesending (1000 lyttere):
|
||||||
node + mottaker-edges(∞) + stream-edge + publiserings-edge
|
node + mottaker-edges(∞) + stream-edge
|
||||||
→ fang lyd → transkriber → stream via LiveKit → distribuer
|
→ fang lyd → transkriber → stream via LiveKit → distribuer
|
||||||
→ generer segmenter → kjør live AI → publiser
|
→ generer segmenter → kjør live AI → publiser
|
||||||
Ressurser: STT + LiveKit + LLM + mediaprosessering
|
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
|
### Naturlig eskalering
|
||||||
Du starter en privat voice-note. Bestemmer deg for å dele den med Trond
|
Du starter en privat voice-note. Deler den med Trond → legg til
|
||||||
→ legg til mottaker-edge, maskinrommet begynner å levere. Trond foreslår
|
edge, maskinrommet begynner å levere. Trond foreslår møte → flere
|
||||||
at dere tar det som et møte → legg til flere deltaker-edges, maskinrommet
|
edges, maskinrommet kobler inn sanntidsstrømming. Møtet blir
|
||||||
kobler inn sanntidsstrømming. Møtet blir en innspilling → legg til
|
innspilling → publiserings-edge, maskinrommet aktiverer
|
||||||
publiserings-edge, maskinrommet aktiverer produksjonspipeline.
|
produksjonspipeline. Hvert steg er bare å legge til edges.
|
||||||
|
|
||||||
Hvert steg er bare å legge til edges. Maskinrommet reagerer og kobler
|
|
||||||
inn flere ressurser etter hvert. Ingen migrering, ingen modebytte.
|
|
||||||
|
|
||||||
## Grensesnittet
|
## Grensesnittet
|
||||||
|
|
||||||
Maskinrommet eksponerer et konsistent API — sannsynligvis en Rust trait
|
|
||||||
eller et sett traits:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
fang(input: RåInput) → NodeId
|
fang(input: RåInput) → NodeId
|
||||||
prosesser(node: NodeId, operasjon: Operasjon) → Resultat
|
prosesser(node: NodeId, operasjon: Operasjon) → Resultat
|
||||||
lever(node: NodeId, mottaker: Mottaker, format: Format) → Status
|
lever(node: NodeId, mottaker: Mottaker, format: Format) → Status
|
||||||
```
|
```
|
||||||
|
|
||||||
Men i praksis er mye av dette *reaktivt*: maskinrommet observerer
|
I praksis er mye av dette implisitt: maskinrommet ser hvilke edges
|
||||||
edge-endringer og handler automatisk. Legger noen til en mottaker-edge
|
som ble skrevet og handler deretter. Primitivene manipulerer edges
|
||||||
→ maskinrommet begynner å levere. Legger noen til en stream-edge →
|
via maskinrommet, og maskinrommet kobler inn riktige ressurser.
|
||||||
maskinrommet kobler inn LiveKit. Primitivene trenger ikke eksplisitt
|
|
||||||
kalle `lever()` — de manipulerer edges, og maskinrommet reagerer.
|
|
||||||
|
|
||||||
## Hva dette gir
|
## CAS og intelligent pruning
|
||||||
|
|
||||||
### 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
|
### CAS som lagringsprimitiv
|
||||||
All binærdata (lyd, bilde, video) lagres i et content-addressable store.
|
All binærdata (lyd, bilde, video) lagres content-addressable.
|
||||||
Fordeler:
|
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
|
- **Deduplisering gratis** — samme fil delt i tre kontekster = én kopi
|
||||||
- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet er
|
- **Separasjon** — "innholdet eksisterer" er adskilt fra "innholdet
|
||||||
tilgjengelig." Noden peker på en hash, CAS har filen (eller ikke).
|
er tilgjengelig"
|
||||||
- **Enkel opprydning** — slett hashen fra CAS, alle noder som pekte
|
- **Enkel opprydning** — slett filen fra CAS, noden beholder metadata
|
||||||
dit mister binærdataen men beholder metadata og transkripsjon.
|
|
||||||
|
|
||||||
### Lagringsregler per modalitet
|
### Lagringsregler per modalitet
|
||||||
|
|
||||||
| Modalitet | Default levetid | Begrunnelse |
|
| Modalitet | Default levetid | Begrunnelse |
|
||||||
|-----------|----------------|-------------|
|
|-----------|----------------|-------------|
|
||||||
| Tekst | Evig | Billig, er essensen av innholdet |
|
| Tekst | Evig | Billig, essensen av innholdet |
|
||||||
| Transkripsjon | Evig | Tekstlig representasjon av lyd/video — tar vare på meningen |
|
| Transkripsjon | Evig | Tekstlig representasjon — bevarer meningen |
|
||||||
| Lyd | 30 dager | Mellomkostnad, transkripsjon bevarer innholdet |
|
| Lyd | 30 dager | Transkripsjon bevarer innholdet |
|
||||||
| Bilde | 30 dager | Mellomkostnad, beskrivelse/metadata bevarer kontekst |
|
| Bilde | 30 dager | Beskrivelse/metadata bevarer kontekst |
|
||||||
| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste |
|
| Video | 14 dager | Dyrest, transkripsjon + thumbnail bevarer det meste |
|
||||||
|
|
||||||
### Signaler som forlenger levetid
|
### Signaler som forlenger levetid
|
||||||
|
|
||||||
Default-TTL er bare utgangspunktet. Maskinrommet justerer basert på:
|
- **Edges.** Lydfil med publiserings-edge = beholdes. Privat
|
||||||
|
voice-memo uten edges = 30-dagers TTL.
|
||||||
- **Edges.** En lydfil med edge til episoderegisteret = publisert
|
- **Aksesslog.** Avspilt i løpet av TTL-perioden = forlenges.
|
||||||
podcast, beholdes. En privat voice-memo uten edges = 30-dagers TTL.
|
- **Transkripsjonsstatus.** Utranskribert lyd kan trenge lengre TTL.
|
||||||
- **Aksesslog.** Hvis noen har spilt av lydfilen i løpet av TTL-perioden,
|
- **Edge-type.** Publisert = behold. Arkivert møte = transkripsjon
|
||||||
forlenges den. Ingen aksess = ingen verdi i å beholde binærdataen.
|
holder.
|
||||||
- **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
|
### Generert innhold er en cache
|
||||||
TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan regenereres
|
TTS, thumbnails, AI-oppsummeringer, waveforms — alt som kan
|
||||||
fra kildedata er i praksis en cache. Det lagres i CAS med samme TTL-
|
regenereres er en cache i CAS med samme TTL-mekanisme.
|
||||||
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
|
### Samlings-node-styrt aggressivitet
|
||||||
"generert lyd" (TTS) i pruning-logikken. Begge er binærdata i CAS med
|
Hver samlings-node kan justere sin pruning-profil:
|
||||||
en TTL som forlenges ved aksess. Forskjellen er bare at generert
|
- **Konservativt** — behold alt lenge (arkiv-node)
|
||||||
innhold alltid kan gjenskapes fra kilden — så det er tryggere å prune.
|
- **Aggressivt** — tekst bevares, binærdata prunes raskt
|
||||||
|
|
||||||
### 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
|
- **Tilpasset** — egne regler per modalitet og edge-type
|
||||||
|
|
||||||
### Brukerens erfaringsbaserte meny
|
### Disk-nødventil
|
||||||
Over tid bruker du noen edges oftere enn andre, noen noder oftere enn
|
Maskinrommet overvåker diskbruk:
|
||||||
andre. Maskinrommet observerer dette og tilbyr en erfaringsbasert meny:
|
- **>85 %:** Genererte filer slettes (kan regenereres)
|
||||||
dine mest brukte koblinger, dine vanligste input-mønstre, dine
|
- **>90 %:** Aggressiv pruning for alle samlings-noder
|
||||||
foretrukne modaliteter. Ikke som en rigid konfigurasjon — som en
|
- **>95 %:** Kritisk alarm. Alt uten publiserings-edge slettes.
|
||||||
adaptiv overflate du kan aktivere og deaktivere fortløpende.
|
Tekst og transkripsjoner bevares alltid.
|
||||||
|
|
||||||
Dette er ikke maskinlæring eller kompleks AI — det er frekvenstelling
|
## Isolasjon og observerbarhet
|
||||||
på edges og aksesslog. Enkelt å implementere, intuitivt for brukeren.
|
|
||||||
|
|
||||||
## Pragmatisk vei dit
|
### 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.
|
||||||
|
|
||||||
Ikke bygg dette fra scratch. Formaliser det som allerede finnes:
|
### Observerbarhet
|
||||||
|
Alt går gjennom ett punkt. Logging, metrikker, kostnadsrapportering
|
||||||
|
— alt på ett sted. "Hva bruker vi AI-ressurser på?" har ett svar.
|
||||||
|
|
||||||
1. **Worker + jobbkø er allerede kjernen.** De trenger et konsistent
|
### Kapasitetsstyring
|
||||||
API, ikke en omskriving.
|
Prioritering, kø, rate limiting, fallback mellom leverandører.
|
||||||
2. **AI Gateway (LiteLLM) absorberes** — i stedet for en separat proxy,
|
Live transkripsjon prioriteres over bakgrunns-oppsummering.
|
||||||
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
|
## Compute-separasjon
|
||||||
|
|
||||||
Maskinrommet orkestrerer — men tunge jobber trenger ikke kjøre på
|
Maskinrommet orkestrerer — tunge jobber trenger ikke kjøre på
|
||||||
samme maskin. Hetzner CPX42 (8 vCPU, 16 GB RAM) skal håndtere state
|
samme maskin.
|
||||||
(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.
|
- **Nå:** Alt på én VPS. Jobbkøen prioriterer sanntid over batch.
|
||||||
Whisper kjøres med lavere concurrency under live-sesjoner.
|
- **Snart:** Trekk ut tunge workers til separat node (billig
|
||||||
- **Senere:** Trekk ut tunge workers til en separat node (billig
|
ARM-instans) som poller jobbkøen. Maskinrommet ruter transparent.
|
||||||
ARM/Ampere-instans) som poller jobbkøen over internt nettverk.
|
- **Kildevern-modus:** Lokal LLM krever dedikert compute med
|
||||||
Maskinrommet ruter transparent — primitivene merker ingenting.
|
fysisk isolasjon.
|
||||||
- **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 å
|
Compute-separasjon er en konfigurasjon, ikke en arkitekturendring.
|
||||||
*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
|
## Forhold til andre retninger
|
||||||
|
|
||||||
Maskinrommet er infrastrukturen *under* de tre primitivene i
|
Maskinrommet er infrastrukturen *under* de tre primitivene i
|
||||||
[universell input og mottak](universell_input.md):
|
[universell input og mottak](universell_input.md):
|
||||||
- Input-primitiven kaller `fang()` + `prosesser()`
|
- Input-primitiven → `fang()` + `prosesser()`
|
||||||
- Mottak-primitiven kaller `lever()`
|
- Mottak-primitiven → `lever()`
|
||||||
- Kommunikasjonsnoden bruker alle tre (fang input fra deltakere,
|
- Kommunikasjonsnoden → alle tre
|
||||||
prosesser sanntid, lever til mottakere)
|
|
||||||
|
|
||||||
Det er også det som gjør to-lags-modellen fra [rom, ikke forum](rom_ikke_forum.md)
|
- [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet
|
||||||
praktisk: maskinrommet ruter til riktig lag (sanntid vs tradisjonelt)
|
eier tilgangsmatrise-oppdatering
|
||||||
uten at primitivene trenger å vite forskjellen.
|
- [Datalaget](datalaget.md) — maskinrommet skriver SpacetimeDB
|
||||||
|
først, PG asynk
|
||||||
|
|
|
||||||
|
|
@ -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.
|
kanban, kalender er ikke apper — de er visninger av samme tilstandsrom.
|
||||||
|
|
||||||
### Privat og delt som lag, ikke separate systemer
|
### 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
|
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.
|
du dagbok — begge er meldingsbokser, den ene er delt, den andre er privat.
|
||||||
De eksisterer side om side i samme rom.
|
De eksisterer side om side i samme rom.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
# Status quo — Hva Sidelinja er i dag
|
# 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.
|
> En redaksjonell webapp med ambisiøse primitiver og tradisjonell overflate.
|
||||||
|
|
||||||
## Hva fungerer
|
## Hva fungerer
|
||||||
|
|
|
||||||
|
|
@ -1,299 +1,186 @@
|
||||||
# Universell input og mottak
|
# Universell input og mottak
|
||||||
|
|
||||||
> Én multimodal input-primitiv. Én personlig mottaksflate. Alt som fanges
|
**Status: Besluttet.**
|
||||||
> er samme type objekt. Hva det "er" bestemmes av edges, ikke av tabellen
|
|
||||||
> det ligger i. Hvordan det *presenteres* bestemmes av mottakeren.
|
|
||||||
|
|
||||||
## Observasjoner
|
> É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.
|
||||||
|
|
||||||
I dag har vi meldingsboksen som "universell primitiv" — men den er egentlig en
|
## Input-primitiven
|
||||||
*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
|
Én overflate som fanger alt:
|
||||||
(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
|
- **Tekst** — skriving, Markdown, kodeblokker
|
||||||
- **Lyd** — voice memo, diktering → automatisk transkribert
|
- **Lyd** — voice memo, diktering → automatisk transkribert
|
||||||
- **Bilde** — foto, skjermbilde, tegning
|
- **Bilde** — foto, skjermbilde, tegning
|
||||||
- **AI-støtte** — spør AI, få forslag, la den transformere input
|
- **AI-støtte** — spør AI, få forslag, la den transformere input
|
||||||
- **Nettoppslag** — lim inn URL, den berikes automatisk
|
- **URL** — lim inn lenke, 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
|
Brukeren gjør det samme uansett kontekst — skriver, snakker eller
|
||||||
et kanban-kort" er ikke *hva brukeren gjør* — det er hvilke edges som knyttes
|
tegner i input-feltet. Forskjellen mellom "dagbok", "chatmelding"
|
||||||
til resultatet.
|
og "kanban-kort" er ikke hva brukeren gjør — det er hvilke edges
|
||||||
|
som knyttes til resultatet.
|
||||||
|
|
||||||
## Én tabell, edges definerer alt
|
### Én pipeline
|
||||||
|
|
||||||
All output fra input-primitiven lander som noder i kunnskapsgrafen. Én tabell.
|
All input går gjennom samme tekniske pipeline:
|
||||||
Ingen `messages`-tabell, ingen `cards`-tabell, ingen `notes`-tabell.
|
maskinrommet → SpacetimeDB (instant) → PG (asynk).
|
||||||
|
|
||||||
Hva en node "er" bestemmes utelukkende av edges:
|
Konteksten bestemmer routing, ikke en teknisk modus:
|
||||||
- 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 å
|
- Du er i et møte → inputen streames live til andre deltakere
|
||||||
legge til en edge til et board og en status-edge. Fjerne fra chat er å fjerne
|
- Du er alene på bussen → inputen lander som privat node
|
||||||
kanal-edgen. Ingen datamigrering, ingen transformasjon av innhold. Bare edges.
|
- Du er i en podcast-kanal → inputen går inn i produksjonspipeline
|
||||||
|
|
||||||
**Multitype er naturlig.** En node kan være *både* et kanban-kort *og* en
|
Samme pipeline. Ulike edges.
|
||||||
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
|
### 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
|
Du kan snakke inn et kanban-kort. Du kan tegne en kalenderoppføring.
|
||||||
Lyd inn i input-primitiven kan routes helt forskjellig basert på edges:
|
Input-primitiven bryr seg ikke om hva det *blir* — den fanger det
|
||||||
- Edge til et møterom → streames live til andre deltakere (sanntidslaget)
|
som kommer inn. Alt etterpå er edges.
|
||||||
- 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
|
### Én overflate å perfeksjonere
|
||||||
basert på kontekst og edges. Det er ingen "møte-app" eller "notat-app"
|
|
||||||
eller "meldings-app" — det er én input med ulike destinasjoner.
|
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
|
### Mottaker bestemmer format
|
||||||
All lyd transkriberes. All tekst kan leses opp (TTS). Noden har alltid
|
|
||||||
begge representasjoner. Mottaker setter sin preferanse:
|
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
|
- Trond snakker inn en tanke → node med lyd + transkripsjon
|
||||||
- Peter har tekst-preferanse → ser transkripsjonen
|
- Peter har tekst-preferanse → ser transkripsjonen
|
||||||
- Vegard har lyd-preferanse → hører originallyd
|
- 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 —
|
Modalitet er ikke en egenskap ved meldingen, men ved *lesningen*.
|
||||||
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
|
### Dimensjoner ved mottak
|
||||||
|
|
||||||
**Format.** Lyd, tekst, visuelt — mottaker bestemmer (allerede beskrevet
|
**Format.** Lyd, tekst, visuelt — mottaker bestemmer.
|
||||||
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
|
**Filtrering.** Mottaksflaten filtrerer basert på dine edges:
|
||||||
basert på dine edges: hvilke kanaler du følger, hvilke personer du
|
kanaler du følger, personer du samarbeider med, topics du er
|
||||||
samarbeider med, hvilke topics du er interessert i. Du kuraterer ikke
|
interessert i.
|
||||||
manuelt — du justerer edges, og mottaksflaten oppdateres.
|
|
||||||
|
|
||||||
**Prioritering.** Hva er viktig *nå*? En AI-assistert redaksjonell flate
|
**Prioritering.** AI-assistert vekting: ubesvarte meldinger,
|
||||||
som løfter frem det som trenger oppmerksomhet: ubesvarte meldinger,
|
oppgaver med frist, noder endret siden sist. Ikke en
|
||||||
oppgaver med frist, noder som er endret siden sist, tråder med aktivitet.
|
notifikasjonsliste — en vektet visning av det som er relevant.
|
||||||
Ikke en notifikasjonsliste — en *vektet visning* av det som er relevant.
|
|
||||||
|
|
||||||
**Tempo.** Sanntid eller asynkront. I sanntidslaget: ting streamer inn
|
**Tempo.** Sanntid (ting streamer inn) eller asynkront (digest,
|
||||||
mens de skjer — en kollega snakker, du hører/leser live. I det
|
oppsummering).
|
||||||
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
|
### Mottaksflaten er en visning av grafen
|
||||||
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
|
"Noder med edge til *meg*, vektet på relevans og tid." Ikke en
|
||||||
|
egen mekanisme — en spørring mot grafen med deg som sentrum.
|
||||||
| 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
|
## Kommunikasjonsnoden — den tredje primitiven
|
||||||
|
|
||||||
Input fanger. Mottak presenterer. Men det mangler noe: *stedet* der folk
|
Input fanger. Mottak presenterer. Kommunikasjonsnoden er *stedet*
|
||||||
møtes. En kommunikasjonsnode er en node i grafen som samler deltakere,
|
der folk møtes — en node som samler deltakere, definerer
|
||||||
definerer tilgangsregler, og fungerer som kontekst for input og mottak.
|
tilgangsregler, og fungerer som kontekst.
|
||||||
|
|
||||||
### Én node, mange former
|
### Én node, mange former
|
||||||
|
|
||||||
En kommunikasjonsnode er konseptuelt identisk uansett skala:
|
| 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 |
|
||||||
|
|
||||||
| Variant | Deltakere | Input-tilgang | Mottak-tilgang |
|
Forskjellen er edge-konfigurasjoner, ikke ulike systemer:
|
||||||
|---------|-----------|---------------|----------------|
|
- `owner`-edge — kontrollerer noden
|
||||||
| Én-til-én samtale | 2 | Begge | Begge |
|
- `member`-edge — kan gi input og motta
|
||||||
| Gruppechat | N | Alle medlemmer | Alle medlemmer |
|
- `reader`-edge — kan kun motta
|
||||||
| 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
|
### Kontekst arves automatisk
|
||||||
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
|
Input i en kommunikasjonsnode arver kontekst-edges. Sier du noe
|
||||||
|
i et møte → noden får `belongs_to`-edge til møtet automatisk.
|
||||||
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
|
### Livssyklus
|
||||||
|
|
||||||
En kommunikasjonsnode kan være:
|
- **Live** — deltakere til stede, input streames
|
||||||
- **Live** — aktiv i sanntidslaget. Deltakere er til stede, input streames.
|
- **Asynkron** — deltakere gir input i eget tempo
|
||||||
- **Asynkron** — aktiv i det tradisjonelle laget. Deltakere gir input i
|
- **Avsluttet** — arkivert, alt som ble sagt er noder med edges
|
||||||
eget tempo (chat, asynkron gjest).
|
- **Gjenåpnet** — reaktivert ("vi tar opp tråden fra forrige møte")
|
||||||
- **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
|
### Skalering er edge-endring
|
||||||
|
|
||||||
En samtale mellom to blir et møte ved å legge til flere deltaker-edges.
|
Samtale → møte = flere deltaker-edges.
|
||||||
Et møte blir en livesending ved å legge til offentlige mottak-edges.
|
Møte → livesending = offentlige mottak-edges.
|
||||||
En livesending blir en podcast ved å legge til publiserings-edges på
|
Livesending → podcast = publiserings-edges på arkivert innhold.
|
||||||
arkivert innhold. Ingen migrering, ingen konvertering — bare edges.
|
|
||||||
|
|
||||||
## Tekniske forutsetninger
|
## Tekniske forutsetninger
|
||||||
|
|
||||||
### STT (tale → tekst): løst
|
### STT (tale → tekst): løst
|
||||||
Faster-whisper kjører lokalt, god norsk kvalitet. Allerede i stacken.
|
Faster-whisper kjører lokalt, god norsk kvalitet.
|
||||||
|
|
||||||
### TTS (tekst → tale): løsbart
|
### TTS (tekst → tale): løsbart
|
||||||
Norsk TTS lokalt er ikke godt nok ennå (Piper er "usable" men dårlig rytme,
|
Start med ElevenLabs bak AI Gateway, bytt til lokal modell når
|
||||||
XTTS-v2 støtter ikke norsk). Kommersielt er det løst:
|
kvaliteten holder. Backend-swap bak gatewayen — brukeren merker
|
||||||
- **ElevenLabs** — beste kvalitet, eksplisitt norsk med regionale aksenter
|
ingenting.
|
||||||
- **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
|
## Visninger er spørringer
|
||||||
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
|
Chat = noder med kanal-edge, sortert på tid.
|
||||||
støtte, 350M params, lovende for lokal kjøring.
|
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.
|
||||||
|
|
||||||
## Spenninger og åpne spørsmål
|
Alle leser fra samme graf. Ingen har "sin egen" data.
|
||||||
|
|
||||||
- **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
|
## Forhold til andre retninger
|
||||||
|
|
||||||
Denne retningen konkretiserer [rom, ikke forum](rom_ikke_forum.md):
|
- [Noder er sentrum](bruker_ikke_workspace.md) — visibility,
|
||||||
- "Formløs input, struktur etterpå" → universell input + edges
|
tilgangsmatrise, aliaser
|
||||||
- "To lag" → sanntidsversjon + flat versjon
|
- [Datalaget](datalaget.md) — SpacetimeDB holder hele grafen,
|
||||||
- "Privat/delt som lag" → synlighet som edge
|
PG persisterer asynkront
|
||||||
- "Siloer forsvinner" → alt er noder, visninger er spørringer
|
- [Maskinrommet](maskinrommet.md) — validering, routing, CAS,
|
||||||
- "Rommet som primitiv" → kommunikasjonsnoden
|
tunge jobber (Whisper, TTS, AI)
|
||||||
|
- [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden
|
||||||
|
er den konkrete realiseringen av "rommet"
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| Skrive/teste kode (Rust, SvelteKit, TypeScript) | Lokalt | Rask iterasjon, HMR |
|
||||||
| PG-skjema og migrasjoner | Mot server-PG | Én sannhetskilde |
|
| PG-skjema og migrasjoner | Mot server-PG | Én sannhetskilde |
|
||||||
| Whisper/AI-eksperimentering | Via AI Gateway på server | Felles tjeneste |
|
| Whisper/AI-eksperimentering | Via AI Gateway på server | Felles tjeneste |
|
||||||
| Docker-compose endringer | Direkte i prod | Serveren er dev-miljø |
|
| Docker-compose endringer | Direkte på server | Serveren er dev-miljø |
|
||||||
| Caddy/Authentik/Forgejo config | Direkte i prod | Avhenger av domener, sertifikater, SSO |
|
| Caddy/Authentik/Forgejo config | Direkte på server | Avhenger av domener, sertifikater, SSO |
|
||||||
|
|
||||||
## 0. Forutsetninger
|
## 0. Forutsetninger
|
||||||
- Windows 11 med WSL2 (Ubuntu 24.04 LTS)
|
- Windows 11 med WSL2 (Ubuntu 24.04 LTS)
|
||||||
|
|
@ -37,8 +37,8 @@ source ~/.cargo/env
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~
|
cd ~
|
||||||
git clone ssh://git@git.sidelinja.org:222/sidelinja/server.git sidelinja-v2
|
git clone ssh://git@git.sidelinja.org:222/vegard/synops.git
|
||||||
cd sidelinja-v2
|
cd synops
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Miljøvariabler (.env.local)
|
## 3. Miljøvariabler (.env.local)
|
||||||
|
|
@ -53,16 +53,13 @@ cp .env.example .env.local
|
||||||
## 4. Utviklingsflyt
|
## 4. Utviklingsflyt
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start alt lokalt
|
# SvelteKit med HMR
|
||||||
./dev.sh
|
cd web && npm run dev
|
||||||
|
|
||||||
# Eller manuelt:
|
# Rust maskinrom
|
||||||
cd web && npm run dev # SvelteKit med HMR
|
cd rust && cargo run
|
||||||
cd rust && cargo run # Rust maskinrom
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`dev.sh` er kanonisk — oppdater alltid scriptet når nye steg oppdages.
|
|
||||||
|
|
||||||
## 5. Deploy
|
## 5. Deploy
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -70,7 +67,7 @@ cd rust && cargo run # Rust maskinrom
|
||||||
git push forgejo main
|
git push forgejo main
|
||||||
|
|
||||||
# 2. Deploy til prod (krever eksplisitt godkjenning)
|
# 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)
|
## 6. Forskjeller fra produksjon (bevisste)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
# Migration Safety Checklist
|
# 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.
|
Sjekkliste for alle som kjører PostgreSQL-migrasjoner — lokalt eller i prod.
|
||||||
|
|
||||||
## Før migrering
|
## Før migrering
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Oppsett: Produksjonsserver (Hetzner VPS)
|
# Oppsett: Produksjonsserver (Hetzner VPS)
|
||||||
**Filsti:** `docs/setup/produksjon.md`
|
**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
|
## 0. Forutsetninger
|
||||||
- Hetzner VPS med Ubuntu 24.04 LTS (8 vCPU, 16 GB RAM minimum)
|
- Hetzner VPS med Ubuntu 24.04 LTS (8 vCPU, 16 GB RAM minimum)
|
||||||
|
|
@ -58,17 +58,17 @@ newgrp docker
|
||||||
## 3. Opprett mappestruktur
|
## 3. Opprett mappestruktur
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /srv/sidelinja/{config,data,media,logs}
|
sudo mkdir -p /srv/synops/{config,data,media,logs}
|
||||||
sudo mkdir -p /srv/sidelinja/config/{caddy,authentik}
|
sudo mkdir -p /srv/synops/config/{caddy,authentik}
|
||||||
sudo mkdir -p /srv/sidelinja/data/{postgres,spacetimedb,forgejo,authentik}
|
sudo mkdir -p /srv/synops/data/{postgres,spacetimedb,forgejo,authentik}
|
||||||
sudo mkdir -p /srv/sidelinja/media/podcast
|
sudo mkdir -p /srv/synops/media/podcast
|
||||||
sudo mkdir -p /srv/sidelinja/logs/caddy
|
sudo mkdir -p /srv/synops/logs/caddy
|
||||||
sudo chown -R sidelinja:sidelinja /srv/sidelinja
|
sudo chown -R sidelinja:sidelinja /srv/synops
|
||||||
```
|
```
|
||||||
|
|
||||||
Resultat:
|
Resultat:
|
||||||
```
|
```
|
||||||
/srv/sidelinja/
|
/srv/synops/
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
├── .env
|
├── .env
|
||||||
├── config/
|
├── config/
|
||||||
|
|
@ -88,7 +88,7 @@ Resultat:
|
||||||
## 4. Miljøvariabler (.env)
|
## 4. Miljøvariabler (.env)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > /srv/sidelinja/.env << 'EOF'
|
cat > /srv/synops/.env << 'EOF'
|
||||||
# === Domener ===
|
# === Domener ===
|
||||||
DOMAIN_SIDELINJA=sidelinja.org
|
DOMAIN_SIDELINJA=sidelinja.org
|
||||||
DOMAIN_VEGARD=vegard.info
|
DOMAIN_VEGARD=vegard.info
|
||||||
|
|
@ -122,7 +122,7 @@ OPENROUTER_API_KEY=<fra openrouter.ai>
|
||||||
# Ingen porter eksponeres utenom 80/443. Alt rutes internt via Docker-nettverket.
|
# Ingen porter eksponeres utenom 80/443. Alt rutes internt via Docker-nettverket.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 600 /srv/sidelinja/.env
|
chmod 600 /srv/synops/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Tjeneste-installasjon (rekkefølge)
|
## 5. Tjeneste-installasjon (rekkefølge)
|
||||||
|
|
@ -153,7 +153,7 @@ Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i
|
||||||
# REGLER:
|
# REGLER:
|
||||||
# - Ingen "ports:" mot host UTENOM Caddy (80, 443)
|
# - Ingen "ports:" mot host UTENOM Caddy (80, 443)
|
||||||
# - Alle tjenester på samme interne nettverk (sidelinja-net)
|
# - 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
|
# - .env-filen lastes automatisk av Docker Compose
|
||||||
# - RESSURSGRENSER: Worker-containere (Whisper) MÅ ha deploy.resources.limits
|
# - RESSURSGRENSER: Worker-containere (Whisper) MÅ ha deploy.resources.limits
|
||||||
# for å forhindre at de sultefôrer LiveKit og PostgreSQL.
|
# for å forhindre at de sultefôrer LiveKit og PostgreSQL.
|
||||||
|
|
@ -165,10 +165,10 @@ networks:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
caddy: # Eneste tjeneste med eksponerte porter (80, 443)
|
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
|
authentik: # SSO for alle domener, på auth.sidelinja.org
|
||||||
forgejo: # data:/srv/sidelinja/data/forgejo, på git.sidelinja.org
|
forgejo: # data:/srv/synops/data/forgejo, på git.sidelinja.org
|
||||||
spacetimedb: # data:/srv/sidelinja/data/spacetimedb
|
spacetimedb: # data:/srv/synops/data/spacetimedb
|
||||||
livekit: # Intern port, proxyet via Caddy
|
livekit: # Intern port, proxyet via Caddy
|
||||||
sveltekit: # Intern port, proxyet via Caddy
|
sveltekit: # Intern port, proxyet via Caddy
|
||||||
workers: # Rust job workers, ingen porter
|
workers: # Rust job workers, ingen porter
|
||||||
|
|
@ -199,13 +199,13 @@ sidelinja.org {
|
||||||
|
|
||||||
# Podcast media (statiske filer med byte-range support)
|
# Podcast media (statiske filer med byte-range support)
|
||||||
handle_path /media/* {
|
handle_path /media/* {
|
||||||
root * /srv/sidelinja/media
|
root * /srv/synops/media
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
|
|
||||||
# Podcast access log (kun media-forespørsler)
|
# Podcast access log (kun media-forespørsler)
|
||||||
log {
|
log {
|
||||||
output file /srv/sidelinja/logs/caddy/podcast_access.log
|
output file /srv/synops/logs/caddy/podcast_access.log
|
||||||
format json
|
format json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,10 +261,10 @@ Se `docs/arkitektur.md` seksjon 2.2 for full dataklassifisering. Kun kategori 1
|
||||||
```bash
|
```bash
|
||||||
# pg_dump er konsistent selv under last — ingen nedetid
|
# pg_dump er konsistent selv under last — ingen nedetid
|
||||||
docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \
|
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
|
# 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)
|
### 11.1b PostgreSQL WAL-arkivering (kontinuerlig, PITR)
|
||||||
|
|
@ -307,21 +307,21 @@ repo1-retention-diff=14
|
||||||
### 11.2 Media-filer (daglig, 03:30)
|
### 11.2 Media-filer (daglig, 03:30)
|
||||||
```bash
|
```bash
|
||||||
# Inkrementell med rsync til lokal backup-disk eller ekstern lagring
|
# 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)
|
### 11.3 Forgejo-data (daglig, 04:00)
|
||||||
```bash
|
```bash
|
||||||
# Forgejo-repos kan gjenskapes, men det er tidkrevende.
|
# Forgejo-repos kan gjenskapes, men det er tidkrevende.
|
||||||
# Sikkerhetsnett-backup av hele data-mappen:
|
# 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)
|
### 11.4 Hemmeligheter (.env)
|
||||||
```bash
|
```bash
|
||||||
# Manuell kopi ved endring — ALDRI i Git
|
# Manuell kopi ved endring — ALDRI i Git
|
||||||
cp /srv/sidelinja/.env /srv/sidelinja/backup/env_$(date +%Y%m%d)
|
cp /srv/synops/.env /srv/synops/backup/env_$(date +%Y%m%d)
|
||||||
chmod 600 /srv/sidelinja/backup/env_*
|
chmod 600 /srv/synops/backup/env_*
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11.5 Off-site backup (rclone → Hetzner Object Storage)
|
### 11.5 Off-site backup (rclone → Hetzner Object Storage)
|
||||||
|
|
@ -335,33 +335,33 @@ rclone config
|
||||||
# Opprett remote "hetzner-s3" med Hetzner Object Storage credentials
|
# Opprett remote "hetzner-s3" med Hetzner Object Storage credentials
|
||||||
# (S3-kompatibelt, endpoint: fsn1.your-objectstorage.com eller nbg1)
|
# (S3-kompatibelt, endpoint: fsn1.your-objectstorage.com eller nbg1)
|
||||||
|
|
||||||
# /srv/sidelinja/scripts/backup-offsite.sh
|
# /srv/synops/scripts/backup-offsite.sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
BUCKET="s3:hetzner-s3/sidelinja-backup"
|
BUCKET="s3:hetzner-s3/sidelinja-backup"
|
||||||
|
|
||||||
# PG-dump (siste lokale dump)
|
# 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
|
if [ -n "$LATEST_DUMP" ]; then
|
||||||
rclone copy "$LATEST_DUMP" "$BUCKET/pg/"
|
rclone copy "$LATEST_DUMP" "$BUCKET/pg/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Media (inkrementell sync)
|
# 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
|
# Behold 90 dager PG-dumper off-site
|
||||||
rclone delete "$BUCKET/pg/" --min-age 90d
|
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
|
### 11.6 Cron-oppsett
|
||||||
```bash
|
```bash
|
||||||
# /etc/cron.d/sidelinja-backup
|
# /etc/cron.d/sidelinja-backup
|
||||||
0 3 * * * sidelinja /srv/sidelinja/scripts/backup-pg.sh
|
0 3 * * * sidelinja /srv/synops/scripts/backup-pg.sh
|
||||||
30 3 * * * sidelinja /srv/sidelinja/scripts/backup-media.sh
|
30 3 * * * sidelinja /srv/synops/scripts/backup-media.sh
|
||||||
0 4 * * * sidelinja /srv/sidelinja/scripts/backup-forgejo.sh
|
0 4 * * * sidelinja /srv/synops/scripts/backup-forgejo.sh
|
||||||
30 4 * * * sidelinja /srv/sidelinja/scripts/backup-offsite.sh
|
30 4 * * * sidelinja /srv/synops/scripts/backup-offsite.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 11.7 Hva som IKKE backupes (bevisst)
|
### 11.7 Hva som IKKE backupes (bevisst)
|
||||||
|
|
@ -376,14 +376,14 @@ echo "$(date): Off-site backup ferdig" >> /srv/sidelinja/logs/backup-offsite.log
|
||||||
```bash
|
```bash
|
||||||
# 1. PostgreSQL
|
# 1. PostgreSQL
|
||||||
docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \
|
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
|
# 2. Media
|
||||||
rsync -a /srv/sidelinja/backup/media/ /srv/sidelinja/media/
|
rsync -a /srv/synops/backup/media/ /srv/synops/media/
|
||||||
|
|
||||||
# 3. Forgejo
|
# 3. Forgejo
|
||||||
docker compose down 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
|
docker compose up -d forgejo
|
||||||
|
|
||||||
# 4. Avledede data: trigges automatisk ved webhook eller manuelt
|
# 4. Avledede data: trigges automatisk ved webhook eller manuelt
|
||||||
|
|
@ -399,7 +399,7 @@ git push forgejo main
|
||||||
|
|
||||||
# SSH inn til server:
|
# SSH inn til server:
|
||||||
ssh sidelinja@<server-ip>
|
ssh sidelinja@<server-ip>
|
||||||
cd /srv/sidelinja
|
cd /srv/synops
|
||||||
git pull
|
git pull
|
||||||
docker compose build --no-cache <tjeneste>
|
docker compose build --no-cache <tjeneste>
|
||||||
docker compose up -d <tjeneste>
|
docker compose up -d <tjeneste>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ men ikke implementert.
|
||||||
## Sjekkliste
|
## Sjekkliste
|
||||||
|
|
||||||
### 1. Git-status
|
### 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 ucommittede endringer lokalt som burde vært pushet?
|
||||||
- [ ] Er det commits på Forgejo som ikke er deployet til prod?
|
- [ ] Er det commits på Forgejo som ikke er deployet til prod?
|
||||||
|
|
||||||
|
|
@ -24,7 +24,6 @@ men ikke implementert.
|
||||||
|
|
||||||
### 3. Docker-tjenester
|
### 3. Docker-tjenester
|
||||||
- [ ] Kjører alle forventede containere i prod? (`docker compose ps`)
|
- [ ] 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?
|
- [ ] Er image-versjoner oppdatert?
|
||||||
|
|
||||||
### 4. Miljøvariabler
|
### 4. Miljøvariabler
|
||||||
|
|
|
||||||
|
|
@ -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 det gjort arbeid nylig som mangler erfaringsdokumentasjon i `docs/erfaringer/`?
|
||||||
- [ ] Er eksisterende erfaringsdokumenter fortsatt relevante og korrekte?
|
- [ ] Er eksisterende erfaringsdokumenter fortsatt relevante og korrekte?
|
||||||
|
|
||||||
### 8. dev.sh og utviklermiljø
|
### 8. Utviklermiljø
|
||||||
- [ ] Fungerer `./dev.sh` fra scratch?
|
- [ ] Fungerer lokal utvikling mot server (SvelteKit HMR, Rust build)?
|
||||||
- [ ] Er alle nødvendige tjenester dekket?
|
- [ ] Er `docs/setup/lokal.md` oppdatert med eventuelle nye steg?
|
||||||
- [ ] Er det nye quirks eller workarounds som bør inn i scriptet?
|
|
||||||
|
|
||||||
## Sist kjørt
|
## Sist kjørt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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)
|
## 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,
|
api_keys_toggle, api_keys_values, drop_priority_unique, workspace_ai_prompts,
|
||||||
usage_action_column. Pluss seed_dev.sql.
|
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
|
## Crontab
|
||||||
Ingen crontab konfigurert (backup-strategi er dokumentert men ikke implementert).
|
Ingen crontab konfigurert (backup-strategi er dokumentert men ikke implementert).
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,76 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Samler all prosjektdokumentasjon til én fil for deling med AI-er etc.
|
# Samler all prosjektdokumentasjon til én fil for deling med AI-er etc.
|
||||||
# Bruk: ./scripts/summary.sh → skriver scripts/summary.md
|
# Bruk: ./scripts/summary.sh → skriver scripts/synops.md
|
||||||
# ./scripts/summary.sh - → skriver til stdout (for piping)
|
# ./scripts/summary.sh - → skriver til stdout (for piping)
|
||||||
|
#
|
||||||
|
# Rekkefølge: overblikk → arkitektur → konsepter → tekniske detaljer → drift
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
OUT="$SCRIPT_DIR/summary.md"
|
OUT="$SCRIPT_DIR/synops.md"
|
||||||
|
|
||||||
files=(
|
# Prioritert rekkefølge av mapper under docs/
|
||||||
# Overblikk — visjon og retning først
|
# Fra overblikk og visjon → ned til implementeringsdetaljer
|
||||||
"$ROOT/docs/arkitektur.md"
|
DOC_ORDER=(
|
||||||
"$ROOT/CLAUDE.md"
|
retninger # Arkitektoniske teser og vedtatte retninger
|
||||||
"$ROOT"/docs/retninger/*.md
|
primitiver # Spesifikasjoner for kjerneprimitivene
|
||||||
|
concepts # Brukeropplevelser og produktområder
|
||||||
# Primitiver
|
features # Tekniske byggeklosser
|
||||||
"$ROOT"/docs/primitiver/*.md
|
infra # Infrastruktur og drift
|
||||||
|
setup # Oppsett og driftsprosedyrer
|
||||||
# Infrastruktur
|
erfaringer # Lærdommer fra implementering
|
||||||
"$ROOT"/docs/infra/*.md
|
proposals # Idébank (uimplementerte forslag)
|
||||||
|
|
||||||
# Erfaringer
|
|
||||||
"$ROOT"/docs/erfaringer/*.md
|
|
||||||
)
|
)
|
||||||
|
|
||||||
collect() {
|
collect() {
|
||||||
for f in "${files[@]}"; do
|
# 1. Prosjektguide (CLAUDE.md) først — gir komplett overblikk
|
||||||
[[ -f "$f" ]] || continue
|
if [[ -f "$ROOT/CLAUDE.md" ]]; then
|
||||||
rel="${f#"$ROOT/"}"
|
emit "$ROOT/CLAUDE.md"
|
||||||
echo "================================================================"
|
fi
|
||||||
echo "FILE: $rel"
|
|
||||||
echo "================================================================"
|
# 2. Overordnet arkitektur
|
||||||
echo ""
|
if [[ -f "$ROOT/docs/arkitektur.md" ]]; then
|
||||||
cat "$f"
|
emit "$ROOT/docs/arkitektur.md"
|
||||||
echo ""
|
fi
|
||||||
echo ""
|
|
||||||
|
# 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
|
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
|
if [[ "${1:-}" == "-" ]]; then
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue