Fjern SpacetimeDB komplett (oppgave 22.4)

SpacetimeDB er nå helt fjernet fra Synops. Sanntid håndteres av
PG LISTEN/NOTIFY + WebSocket i portvokteren (maskinrommet).

Kode fjernet:
- spacetimedb/ Rust-modul og spacetime.json
- maskinrommet/src/stdb.rs (HTTP-klient for STDB-reducers)
- frontend module_bindings/ (23 auto-genererte filer)
- spacetimedb npm-avhengighet fra package.json
- scripts/test-sanntid.sh (testet STDB-flyt)

Infrastruktur:
- Docker-container stoppet og fjernet fra docker-compose.yml
- Caddy: fjernet /spacetime/* reverse proxy
- maskinrommet-env.sh: fjernet STDB_IP og SPACETIMEDB_*-variabler
- .env.example: fjernet SpacetimeDB-seksjoner

Dokumentasjon oppdatert:
- CLAUDE.md: stack, lagmodell, kjerneprinsipper, driftsmodell
- docs/arkitektur.md: skrivestien, lesestien, datalag, teknologivalg
- docs/retninger/datalaget.md: migrasjonshistorikk, status "fjernet"
- 37 andre docs oppdatert (features, concepts, infra, ops, retninger)
- Alle kode-kommentarer med STDB-referanser oppdatert

Verifisert: maskinrommet bygger og starter OK, frontend bygger OK,
helsesjekk returnerer 200. Caddy reloadet.
This commit is contained in:
vegard 2026-03-18 13:39:09 +00:00
parent f418aec33b
commit b5aa5bb243
106 changed files with 2930 additions and 6346 deletions

View file

@ -21,10 +21,6 @@ AUTHENTIK_CLIENT_SECRET=
# === SvelteKit === # === SvelteKit ===
AUTH_SECRET= # openssl rand -base64 33 AUTH_SECRET= # openssl rand -base64 33
# === SpacetimeDB ===
# URL til SpacetimeDB-instansen (server eller lokal)
VITE_SPACETIMEDB_URL=ws://sidelinja.org/spacetime
# === Whisper (STT) === # === Whisper (STT) ===
# URL til faster-whisper-server (Docker-internt) # URL til faster-whisper-server (Docker-internt)
# WHISPER_URL=http://faster-whisper:8000 # WHISPER_URL=http://faster-whisper:8000

View file

@ -33,7 +33,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
- `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` — Noder er sentrum: brukere, team, innhold er noder. Tilgangsmatrise fra edges. - `bruker_ikke_workspace.md` — Noder er sentrum: brukere, team, innhold er noder. Tilgangsmatrise fra edges.
- `datalaget.md` — PG(+AGE) som graf og arkiv, SpacetimeDB som sanntidslag - `datalaget.md` — PG(+AGE) som graf og arkiv, PG LISTEN/NOTIFY + WebSocket som sanntidslag
- `arbeidsflaten.md` — Spatial canvas med verktøy-paneler, drag-and-drop, kompatibilitetsmatrise - `arbeidsflaten.md` — Spatial canvas med verktøy-paneler, drag-and-drop, kompatibilitetsmatrise
- `unix_filosofi.md` — Maskinrommet som orkestrator, arbeid i CLI-verktøy, Claude og maskinrommet deler verktøykasse - `unix_filosofi.md` — Maskinrommet som orkestrator, arbeid i CLI-verktøy, Claude og maskinrommet deler verktøykasse
- `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene: - `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene:
@ -61,10 +61,10 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
- `ai_gateway.md` — LiteLLM som sentralisert AI-ruter (BYOK + fallback) - `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
- `jobbkø.md` — PostgreSQL-basert køsystem for bakgrunnsjobber - `jobbkø.md` — PostgreSQL-basert køsystem for bakgrunnsjobber
- `synkronisering.md`PostgreSQL ↔ SpacetimeDB dataflyt og eierskapsmodell - `synkronisering.md`Historisk: PG ↔ SpacetimeDB dataflyt (utdatert, SpacetimeDB fjernet mars 2026)
- `claude_agent.md` — Claude som chat-deltaker: arkitektur, triggere, sikkerhet - `claude_agent.md` — Claude som chat-deltaker: arkitektur, triggere, sikkerhet
- `observerbarhet.md` — Strukturert logging, metrikk-endepunkt (/metrics), AI-kostnad - `observerbarhet.md` — Strukturert logging, metrikk-endepunkt (/metrics), AI-kostnad
- `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, SpacetimeDB, Authentik) - `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, Authentik)
- `reference/` — Kode fra v1 med gjenbruksverdi (Editor.svelte) - `reference/` — Kode fra v1 med gjenbruksverdi (Editor.svelte)
- `ops/` — Repeterbare vedlikeholdsjobber (ryddejobb, doc-audit, drift-sjekk) - `ops/` — Repeterbare vedlikeholdsjobber (ryddejobb, doc-audit, drift-sjekk)
@ -76,7 +76,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
## Stack ## Stack
- **Orkestrator/Backend:** Rust (maskinrommet) — kjører native på hosten - **Orkestrator/Backend:** Rust (maskinrommet) — kjører native på hosten
- **Frontend:** SvelteKit (TypeScript, PWA) - **Frontend:** SvelteKit (TypeScript, PWA)
- **Sanntid:** SpacetimeDB - **Sanntid:** PG LISTEN/NOTIFY + WebSocket (portvokteren)
- **Database/Graf:** PostgreSQL (+Apache AGE ved behov) - **Database/Graf:** PostgreSQL (+Apache AGE ved behov)
- **Binærlagring:** CAS (content-addressable store) - **Binærlagring:** CAS (content-addressable store)
- **AI:** Claude Code (chat-agent), LiteLLM (AI Gateway), faster-whisper (STT) - **AI:** Claude Code (chat-agent), LiteLLM (AI Gateway), faster-whisper (STT)
@ -102,14 +102,13 @@ med Docker container-IPs.
| Tjeneste | Begrunnelse | | Tjeneste | Begrunnelse |
|----------|-------------| |----------|-------------|
| **PostgreSQL** | Versjonsstyring, enkel oppgradering | | **PostgreSQL** | Versjonsstyring, enkel oppgradering |
| **SpacetimeDB** | Eksperimentelt, offisielt image |
| **Authentik** | Kompleks stack (server + worker + Redis) | | **Authentik** | Kompleks stack (server + worker + Redis) |
| **LiteLLM** | Ferdig image, sjelden oppdatering | | **LiteLLM** | Ferdig image, sjelden oppdatering |
| **faster-whisper** | Modellhåndtering, ferdig image | | **faster-whisper** | Modellhåndtering, ferdig image |
### Kommunikasjon mellom lagene ### Kommunikasjon mellom lagene
- Caddy (native) → Docker-tjenester: via localhost-porter - Caddy (native) → Docker-tjenester: via localhost-porter
(Authentik:9000, Forgejo:3000, SpacetimeDB:9080) (Authentik:9000, Forgejo:3000)
- Caddy (native) → native tjenester: direkte localhost - Caddy (native) → native tjenester: direkte localhost
(maskinrommet:3100, SvelteKit:3200) (maskinrommet:3100, SvelteKit:3200)
- Maskinrommet (host) → Docker-tjenester: via container-IP - Maskinrommet (host) → Docker-tjenester: via container-IP
@ -163,15 +162,15 @@ maskinrommet deler verktøykasse.
## Lagmodell ## Lagmodell
``` ```
GUI (SvelteKit) — spatial canvas med verktøy-paneler GUI (SvelteKit) — spatial canvas med verktøy-paneler
│ skriv │ les (sanntid, direkte WebSocket) │ skriv │ les (sanntid via WebSocket)
Maskinrommet (Rust) SpacetimeDB ──→ GUI Maskinrommet (Rust) ───┘ PG LISTEN/NOTIFY → WebSocket → GUI
│ orkestrerer │ orkestrerer
CLI-verktøy (tools/) ←── Claude bruker de samme CLI-verktøy (tools/) ←── Claude bruker de samme
Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ... Tjenester: PG+AGE, CAS, Whisper, LiteLLM, LiveKit ...
``` ```
## Kjerneprinsipper ## Kjerneprinsipper
@ -189,8 +188,8 @@ Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ...
Du ser dine edges. Tilgang via materialisert tilgangsmatrise. 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 sannhetskilden.** All data lever i PostgreSQL.
To lag, to roller. Sanntid via PG LISTEN/NOTIFY + WebSocket i portvokteren.
7. **Alt er paneler i en arbeidsflate.** Brukergrensesnittet er et spatial 7. **Alt er paneler i en arbeidsflate.** Brukergrensesnittet er et spatial
canvas der verktøy plasseres, sizes og arrangeres fritt. Hver feature canvas der verktøy plasseres, sizes og arrangeres fritt. Hver feature
er et panel i BlockShell. Interaksjon mellom paneler via drag-and-drop: er et panel i BlockShell. Interaksjon mellom paneler via drag-and-drop:

View file

@ -9,8 +9,8 @@ Synops.
Alt er noder og edges i en graf. En bruker er en node. Et team er en Alt er noder og edges i en graf. En bruker er en node. Et team er en
node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke
av noden selv. Maskinrommet eier alle skrivinger. Frontend er et tynt av noden selv. Maskinrommet eier alle skrivinger. Frontend leser
lag som leser grafen fra SpacetimeDB. sanntidsoppdateringer via WebSocket (PG LISTEN/NOTIFY).
## Lagmodell ## Lagmodell
@ -21,12 +21,12 @@ lag som leser grafen fra SpacetimeDB.
│ Drag-and-drop mellom paneler │ │ Drag-and-drop mellom paneler │
└────────┬──────────────────┬─────────────┘ └────────┬──────────────────┬─────────────┘
│ intensjoner │ les (sanntid) │ intensjoner │ les (sanntid)
│ │ direkte WebSocket │ │ WebSocket
┌────────▼────────┐ ┌──────▼──────────┐ ┌────────▼──────────────────┘
│ Maskinrommet │ │ SpacetimeDB │ │ Maskinrommet (Rust)
(Rust) │ │ Hele grafen │ Orkestrerer + WebSocket-hub
Orkestrerer │ │ (noder+edges) │ PG LISTEN/NOTIFY → WS → GUI
└──┬───────────┬──┘ └─────────────────┘ └──┬───────────┬──┘
│ spawner │ │ spawner │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────┐┌─────┐┌─────────────┐ ┌─────────┐ ┌─────┐┌─────┐┌─────────────┐
@ -38,25 +38,25 @@ lag som leser grafen fra SpacetimeDB.
``` ```
### Skrivestien ### Skrivestien
GUI → intensjon → Maskinrommet (Rust) → SpacetimeDB (instant) → PG (async) GUI → intensjon → Maskinrommet (Rust) → PG → NOTIFY → WebSocket → GUI
Frontend sender intensjoner (ikke data). Maskinrommet validerer, Frontend sender intensjoner (ikke data). Maskinrommet validerer og
skriver til SpacetimeDB først for umiddelbar oppdatering, deretter skriver til PG. PG NOTIFY-triggere sender endringer via WebSocket
persisterer til PG asynkront. Maskinrommet leser edges og bestemmer til alle tilkoblede klienter i sanntid. Maskinrommet leser edges
hvilke tjenester som trigges. og bestemmer hvilke tjenester som trigges.
### Lesestien (sanntid) ### Lesestien (sanntid)
SpacetimeDB → GUI (direkte WebSocket) PG NOTIFY → Maskinrommet (WebSocket-hub) → GUI
SpacetimeDB holder hele grafen — alle noder og edges. Frontend Maskinrommet lytter på PG LISTEN/NOTIFY-kanaler og videresender
abonnerer via WebSocket med edge-filtre. Visninger er spørringer relevante endringer via WebSocket til tilkoblede klienter, filtrert
mot STDB, ikke forhåndsdefinerte API-endepunkter. på tilgangsmatrisen.
### Lesestien (tunge spørringer) ### Lesestien (tunge spørringer)
GUI → Maskinrommet (Rust) → PG GUI → Maskinrommet (Rust) → PG
Søk, statistikk, semantisk søk (pgvector), graftraversering Søk, statistikk, semantisk søk (pgvector), graftraversering
(AGE/Cypher). For operasjoner der STDB ikke er egnet. (AGE/Cypher).
## Datamodell ## Datamodell
@ -79,7 +79,7 @@ bestemmes utelukkende av dens edges:
- Node uten edges = løs tanke - Node uten edges = løs tanke
### Visninger er spørringer ### Visninger er spørringer
Visninger er spørringer mot SpacetimeDB med edge-filtre: Visninger er spørringer mot node-/edge-stores med edge-filtre:
- Chat = noder med kanal-edge, sortert på tid - 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
@ -139,7 +139,7 @@ i `tools/` — maskinrommet spawner riktig verktøy fra jobbkøen.
Kjerneansvar (forblir i maskinrommet): Kjerneansvar (forblir i maskinrommet):
- Auth + tilgangskontroll - Auth + tilgangskontroll
- Intentions (validering, edge-logikk, STDB+PG-skriving) - Intentions (validering, edge-logikk, PG-skriving)
- Jobbkø (polling, retry, dead letter) - Jobbkø (polling, retry, dead letter)
- CAS-forvaltning og pruning - CAS-forvaltning og pruning
@ -173,13 +173,9 @@ Deling er å legge til edges.
## Datalag ## Datalag
### PostgreSQL ### PostgreSQL
Persistent backup og arkiv. Alle noder og edges. Fulltekstsøk, Eneste datakilde. Alle noder og edges. Fulltekstsøk,
pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov. pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov.
PG LISTEN/NOTIFY-triggere sender sanntidsoppdateringer.
### SpacetimeDB
Holder hele grafen — alle noder og edges. Frontend abonnerer via
WebSocket. Maskinrommet skriver hit først for umiddelbar oppdatering,
PG synkroniseres asynkront.
### CAS (Content-Addressable Store) ### 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å
@ -193,7 +189,7 @@ Tredjepartstjenester kjører i **Docker**. Prinsipp: Docker for det vi
ikke bygger selv, native for det vi har full kontroll over. ikke bygger selv, native for det vi har full kontroll over.
**Native (systemd):** Caddy, maskinrommet (Rust), SvelteKit. **Native (systemd):** Caddy, maskinrommet (Rust), SvelteKit.
**Docker:** PostgreSQL, SpacetimeDB, Authentik, LiteLLM, faster-whisper. **Docker:** PostgreSQL, Authentik, LiteLLM, faster-whisper.
Maskinrommet og Caddy kjører native fordi de trenger direkte tilgang Maskinrommet og Caddy kjører native fordi de trenger direkte tilgang
til host-ressurser (CLI-verktøy, TLS-konfig). Docker-tjenester til host-ressurser (CLI-verktøy, TLS-konfig). Docker-tjenester
@ -207,7 +203,7 @@ eksponerer porter på localhost.
| CLI-verktøy | Rust/Shell | Native | Prosessering (transcribe, render, audio, AI). Delt mellom maskinrommet og Claude | | CLI-verktøy | Rust/Shell | Native | Prosessering (transcribe, render, audio, AI). Delt mellom maskinrommet og Claude |
| Frontend | SvelteKit | Native (systemd) | PWA, SSR, spatial canvas med verktøy-paneler | | Frontend | SvelteKit | Native (systemd) | PWA, SSR, spatial canvas med verktøy-paneler |
| Database | PostgreSQL | Docker | Versjonsstyring, enkel oppgradering | | Database | PostgreSQL | Docker | Versjonsstyring, enkel oppgradering |
| Sanntid | SpacetimeDB | Docker | Eksperimentelt, offisielt image | | Sanntid | PG LISTEN/NOTIFY + WebSocket | Native (i maskinrommet) | Ingen ekstra avhengighet, sanntid fra PG |
| Binærlagring | CAS (filsystem) | Native | Enkel, deduplisering, ingen ekstern avhengighet | | Binærlagring | CAS (filsystem) | Native | Enkel, deduplisering, ingen ekstern avhengighet |
| AI Gateway | LiteLLM | Docker | Ferdig image, sjelden oppdatering | | AI Gateway | LiteLLM | Docker | Ferdig image, sjelden oppdatering |
| AI Agent | Claude Code CLI | Native | Chat-deltaker, spawnes av maskinrommet | | AI Agent | Claude Code CLI | Native | Chat-deltaker, spawnes av maskinrommet |

View file

@ -40,9 +40,9 @@ publisering.
#### Varslingsmekanisme #### Varslingsmekanisme
- **Sanntidsvarsel via STDB:** Maskinrommet skriver en varslingsnode - **Sanntidsvarsel via WebSocket:** Maskinrommet skriver en varslingsnode
som frontend abonnerer på. Vises som banner/toast i alle aktive som frontend abonnerer på via WebSocket. Vises som banner/toast i alle
klienter umiddelbart aktive klienter umiddelbart
- **Varslingstyper:** - **Varslingstyper:**
- `info` — generell melding (f.eks. "Ny funksjonalitet tilgjengelig") - `info` — generell melding (f.eks. "Ny funksjonalitet tilgjengelig")
- `warning` — planlagt vedlikehold med nedtelling - `warning` — planlagt vedlikehold med nedtelling
@ -100,10 +100,9 @@ Vises for alle med `visibility: open`. Forsvinner automatisk etter
Sanntidsoversikt over systemtilstand. Sanntidsoversikt over systemtilstand.
- **Tjeneste-status:** PG, STDB, Caddy, Authentik, LiteLLM, Whisper, LiveKit — oppe/nede/degradert - **Tjeneste-status:** PG, Caddy, Authentik, LiteLLM, Whisper, LiveKit — oppe/nede/degradert
- **Metrikker:** CPU, minne, disk, nettverkstrafikk - **Metrikker:** CPU, minne, disk, nettverkstrafikk
- **PG-helse:** Tilkoblingspool, aktive spørringer, replikerings-lag (fremtidig) - **PG-helse:** Tilkoblingspool, aktive spørringer, replikerings-lag (fremtidig)
- **STDB-helse:** Minnebruk, antall abonnenter, graf-størrelse
- **Logg-tilgang:** Siste feil og advarsler fra alle tjenester, filtrerbart - **Logg-tilgang:** Siste feil og advarsler fra alle tjenester, filtrerbart
- **Backup-status:** Siste vellykkede backup per type, neste planlagte kjøring - **Backup-status:** Siste vellykkede backup per type, neste planlagte kjøring
@ -118,7 +117,7 @@ Sanntidsoversikt over systemtilstand.
- **Frontend:** `/admin/health` — dashboard med tjenestekort (opp/nede/degradert med - **Frontend:** `/admin/health` — dashboard med tjenestekort (opp/nede/degradert med
latens), system-metrikker med progress-bars, PG-tilkoblinger og DB-størrelse, latens), system-metrikker med progress-bars, PG-tilkoblinger og DB-størrelse,
backup-status, og filtrerbar logg-visning backup-status, og filtrerbar logg-visning
- **Tjeneste-sjekker:** PG (SQL ping), STDB (noop-kall), Caddy (admin-API), - **Tjeneste-sjekker:** PG (SQL ping), Caddy (admin-API),
Authentik (health-endpoint), LiteLLM/Whisper/LiveKit (HTTP health). Alle kjøres Authentik (health-endpoint), LiteLLM/Whisper/LiveKit (HTTP health). Alle kjøres
parallelt med 5s timeout parallelt med 5s timeout
- **Metrikker:** CPU load via `/proc/loadavg`, minne via `/proc/meminfo`, - **Metrikker:** CPU load via `/proc/loadavg`, minne via `/proc/meminfo`,

View file

@ -310,14 +310,14 @@ Terminal (Claude/Vegard) Web (SvelteKit)
↕ synk ↕ synk
SpacetimeDB → WebSocket → sanntid i frontend PG LISTEN/NOTIFY → WebSocket → sanntid i frontend
``` ```
| Grensesnitt | Leser fra | Skriver til | | Grensesnitt | Leser fra | Skriver til |
|-------------|-----------|-------------| |-------------|-----------|-------------|
| `synops-tasks` CLI | PG direkte | PG direkte | | `synops-tasks` CLI | PG direkte | PG direkte |
| Web (KanbanTrait) | SpacetimeDB | Maskinrommet → PG + STDB | | Web (KanbanTrait) | WebSocket (PG) | Maskinrommet → PG |
| `synops-respond` | PG | PG + STDB (via maskinrommet) | | `synops-respond` | PG | PG (via maskinrommet) |
Ingen nytt UI trengs — arbeidstavlen er bare en `collection`-node Ingen nytt UI trengs — arbeidstavlen er bare en `collection`-node
med kanban-trait. Vegard ser den på arbeidsflaten ved siden av med kanban-trait. Vegard ser den på arbeidsflaten ved siden av

View file

@ -9,7 +9,7 @@ Et fullverdig virtuelt møterom for Sidelinjas redaksjon. Bygget på LiveKit for
2. Under møtet er følgende verktøy tilgjengelige: 2. Under møtet er følgende verktøy tilgjengelige:
- **Video/lyd** — LiveKit-strøm mellom deltakere - **Video/lyd** — LiveKit-strøm mellom deltakere
- **Whiteboard** — Frihåndstavle for skisser (se `docs/features/whiteboard.md`) - **Whiteboard** — Frihåndstavle for skisser (se `docs/features/whiteboard.md`)
- **Delt scratchpad** — SpacetimeDB-drevet tekstfelt for kjappe notater - **Delt scratchpad** — Sanntids tekstfelt for kjappe notater
- **Aha-markør** — Markerer viktige øyeblikk med tidsstempel - **Aha-markør** — Markerer viktige øyeblikk med tidsstempel
- **Off-the-record** — Pauser AI-lytting og opptak - **Off-the-record** — Pauser AI-lytting og opptak
3. Whisper transkriberer møtet via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`). 3. Whisper transkriberer møtet via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`).
@ -23,6 +23,7 @@ Et fullverdig virtuelt møterom for Sidelinjas redaksjon. Bygget på LiveKit for
| Feature | Rolle i Møterommet | | Feature | Rolle i Møterommet |
|---|---| |---|---|
| LiveKit | WebRTC lyd/video | | LiveKit | WebRTC lyd/video |
| Lydmixer | Volumslidere per deltaker, mute, sound pads (se `docs/features/lydmixer.md`) |
| Live transkripsjon | Whisper-pipeline for møtetranskripsjon (se `docs/features/live_transkripsjon.md`) | | Live transkripsjon | Whisper-pipeline for møtetranskripsjon (se `docs/features/live_transkripsjon.md`) |
| Live AI (møte-modus) | Referat, action points, tråding (se `docs/features/live_ai.md`) | | Live AI (møte-modus) | Referat, action points, tråding (se `docs/features/live_ai.md`) |
| Whiteboard | Frihåndstavle for visuell brainstorming (se `docs/features/whiteboard.md`) | | Whiteboard | Frihåndstavle for visuell brainstorming (se `docs/features/whiteboard.md`) |

View file

@ -7,7 +7,7 @@ Redaksjonen er den daglige arbeidsflaten for Sidelinjas team. Her planlegges epi
## 2. Brukeropplevelse ## 2. Brukeropplevelse
### 2.1 Tema-bassenget ### 2.1 Tema-bassenget
Alle pågående "Saker" vises i en oversikt. PostgreSQL er kilden til sannhet, SpacetimeDB holder aktive temaer i minnet for sanntidsoppdateringer. Alle pågående "Saker" vises i en oversikt. PostgreSQL er kilden til sannhet, med sanntidsoppdateringer via LISTEN/NOTIFY + WebSocket.
### 2.2 Trådet Chat ### 2.2 Trådet Chat
Hver melding tilhører et Tema. Meldinger støtter tråder (svar-på-svar) og rike mentions med `#`-tags og `@`-mentions. Se `docs/features/chat.md` for teknisk spesifikasjon. Hver melding tilhører et Tema. Meldinger støtter tråder (svar-på-svar) og rike mentions med `#`-tags og `@`-mentions. Se `docs/features/chat.md` for teknisk spesifikasjon.
@ -16,7 +16,7 @@ Hver melding tilhører et Tema. Meldinger støtter tråder (svar-på-svar) og ri
Episoder fungerer som containere. Brukerne drar Temaer fra bassenget inn i en episodes kjøreplan med drag-and-drop. Se `docs/features/kanban.md` for teknisk spesifikasjon. Episoder fungerer som containere. Brukerne drar Temaer fra bassenget inn i en episodes kjøreplan med drag-and-drop. Se `docs/features/kanban.md` for teknisk spesifikasjon.
### 2.4 Show Notes ### 2.4 Show Notes
Et kollaborativt tekstfelt koblet til et Tema. Enkle "Operational Transformation"-aktige oppdateringer (eller felt-låsing) håndteres i SpacetimeDB-modulen. Synkes til PostgreSQL for persistens. Et kollaborativt tekstfelt koblet til et Tema. Lagres i PostgreSQL med sanntidssynk via WebSocket.
### 2.5 AI-behandling av tekst ### 2.5 AI-behandling av tekst
Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen (✨) og velger handling (rens, oppsummer, trekk ut fakta). Resultatet publiseres som en ny melding med foreslåtte graf-koblinger. Se `docs/proposals/editor.md` § "AI-behandling — universell knapp". Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen (✨) og velger handling (rens, oppsummer, trekk ut fakta). Resultatet publiseres som en ny melding med foreslåtte graf-koblinger. Se `docs/proposals/editor.md` § "AI-behandling — universell knapp".
@ -32,5 +32,5 @@ Brukere limer inn uformatert tekst fra nettet i editoren, trykker AI-knappen (
## 4. Instruks for Claude Code ## 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. * Frontend mottar sanntidsdata via WebSocket — ikke bygg kompleks lokal state.
* All tilgang er styrt via node_access-matrisen. SpacetimeDB-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder. * All tilgang er styrt via node_access-matrisen. WebSocket-tilkoblinger bærer brukerens identitet, og tilgang avgjøres av edges til samlings-noder.

View file

@ -63,7 +63,7 @@ Synops (collection, visibility: readable)
│ └── (work_items) │ └── (work_items)
└── Synops Erfaringer (collection) └── Synops Erfaringer (collection)
├── SpacetimeDB-integrasjon (content) ├── SpacetimeDB-integrasjon (content, historisk)
├── Authentik OIDC (content) ├── Authentik OIDC (content)
└── ... └── ...
``` ```

View file

@ -9,7 +9,7 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte
2. Høykvalitetslyd streames mellom deltakerne via WebRTC. 2. Høykvalitetslyd streames mellom deltakerne via WebRTC.
3. I bakgrunnen transkriberer Whisper lydstrømmen i chunks (~5 sek) via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`). 3. I bakgrunnen transkriberer Whisper lydstrømmen i chunks (~5 sek) via live transkripsjonspipelinen (se `docs/features/live_transkripsjon.md`).
4. AI-assistenten analyserer transkripsjonen for entiteter (NER) og slår opp i Kunnskapsgrafen. Relevante faktoider popper lydløst opp på skjermen (se `docs/features/live_ai.md`, studio-modus). 4. AI-assistenten analyserer transkripsjonen for entiteter (NER) og slår opp i Kunnskapsgrafen. Relevante faktoider popper lydløst opp på skjermen (se `docs/features/live_ai.md`, studio-modus).
5. Programlederne kan trykke **Aha-markør** for å markere viktige øyeblikk. Tidsstempelet lagres i SpacetimeDB, koblet til episoden. 5. Programlederne kan trykke **Aha-markør** for å markere viktige øyeblikk. Tidsstempelet lagres i PG og synkes til frontend via WebSocket, koblet til episoden.
6. Etter innspilling skyves lydfilen inn i Podcastfabrikken for full transkripsjon og publisering (se `docs/concepts/podcastfabrikken.md`). 6. Etter innspilling skyves lydfilen inn i Podcastfabrikken for full transkripsjon og publisering (se `docs/concepts/podcastfabrikken.md`).
## 3. Komponenter ## 3. Komponenter
@ -17,9 +17,10 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte
| Feature | Rolle i Studioet | | Feature | Rolle i Studioet |
|---|---| |---|---|
| LiveKit | WebRTC lyd/video mellom deltakere | | LiveKit | WebRTC lyd/video mellom deltakere |
| Lydmixer | Volumslidere, mute, sound pads, stemmeeffekter, EQ (se `docs/features/lydmixer.md`) |
| Live transkripsjon | Whisper `small` for lav latens, ~1s forsinkelse (se `docs/features/live_transkripsjon.md`) | | Live transkripsjon | Whisper `small` for lav latens, ~1s forsinkelse (se `docs/features/live_transkripsjon.md`) |
| Live AI (studio-modus) | NER + faktoid-oppslag fra Kunnskapsgrafen (se `docs/features/live_ai.md`) | | Live AI (studio-modus) | NER + faktoid-oppslag fra Kunnskapsgrafen (se `docs/features/live_ai.md`) |
| Aha-markør | Manuell markering av viktige øyeblikk, lagres i SpacetimeDB | | Aha-markør | Manuell markering av viktige øyeblikk, lagres i PG |
## 4. Avgrensning ## 4. Avgrensning
- Studioet er for **innspilling**, ikke redigering. Klipping/postproduksjon skjer utenfor Sidelinja. - Studioet er for **innspilling**, ikke redigering. Klipping/postproduksjon skjer utenfor Sidelinja.
@ -27,6 +28,6 @@ Det virtuelle podcast-studioet er Sidelinjas innspillingsmiljø. LiveKit håndte
- Aha-markøren deles med Møterommet (se `docs/concepts/møterommet.md`), men i studio-konteksten brukes den primært til klippepunkter. - Aha-markøren deles med Møterommet (se `docs/concepts/møterommet.md`), men i studio-konteksten brukes den primært til klippepunkter.
## 5. Utviklingsfaser ## 5. Utviklingsfaser
1. Bygg SpacetimeDB-lytter i frontend + dummy faktoid-push for å verifisere UI. 1. Bygg WebSocket-lytter i frontend + dummy faktoid-push for å verifisere UI.
2. Koble Whisper til et offline lydopptak, kjør NER/oppslag mot PostgreSQL. 2. Koble Whisper til et offline lydopptak, kjør NER/oppslag mot PostgreSQL.
3. Koble LiveKit-strømmen til Whisper for sanntid. 3. Koble LiveKit-strømmen til Whisper for sanntid.

View file

@ -18,14 +18,14 @@ For å unngå "Blank Canvas"-syndromet startes plattformen med anerkjente rammev
Valgomaten er bygget som en to-trinns trakt for å maksimere viral spredning samtidig som dataintegriteten bevares. Valgomaten er bygget som en to-trinns trakt for å maksimere viral spredning samtidig som dataintegriteten bevares.
### 2.1 Forsiden: Friksjonsfri & Anonym ### 2.1 Forsiden: Friksjonsfri & Anonym
* **Mekanikk:** Ingen registrering kreves for å starte. SvelteKit genererer en anonym UUID som lagres i `localStorage` og brukes mot SpacetimeDB. * **Mekanikk:** Ingen registrering kreves for å starte. SvelteKit genererer en anonym UUID som lagres i `localStorage`.
* **UX:** Et rent kortstokk-grensesnitt (Tinder-stil swipe). Påstand på forsiden, brukeren velger Enig/Uenig. * **UX:** Et rent kortstokk-grensesnitt (Tinder-stil swipe). Påstand på forsiden, brukeren velger Enig/Uenig.
* **Tre-trinns kalibrering (OKCupid-inspirert):** * **Tre-trinns kalibrering (OKCupid-inspirert):**
1. Hva mener du? 1. Hva mener du?
2. Hva ønsker du at kandidaten skal mene? 2. Hva ønsker du at kandidaten skal mene?
3. Hvor viktig er dette for deg? 3. Hvor viktig er dette for deg?
* **Dealbreakers (Negative Veto):** Brukere kan sette absolutte grenser (f.eks. "Uansett hvor enige vi er om skatt, matcher jeg aldri med en som er for/mot vindkraft"). * **Dealbreakers (Negative Veto):** Brukere kan sette absolutte grenser (f.eks. "Uansett hvor enige vi er om skatt, matcher jeg aldri med en som er for/mot vindkraft").
* **Sanntid:** SpacetimeDB beregner PCA (Prinsipalkomponentanalyse) og oppdaterer brukerens posisjon lynraskt i minnet for hvert swipe. * **Sanntid:** PCA (Prinsipalkomponentanalyse) beregnes og oppdaterer brukerens posisjon for hvert swipe.
### 2.2 Baksiden: "Snu kortet" (Krever innlogging) ### 2.2 Baksiden: "Snu kortet" (Krever innlogging)
For avansert interaksjon må brukeren logge inn via Authentik (SSO). Den anonyme sesjonen flettes da automatisk med brukerprofilen. På baksiden av kortet finner man: For avansert interaksjon må brukeren logge inn via Authentik (SSO). Den anonyme sesjonen flettes da automatisk med brukerprofilen. På baksiden av kortet finner man:
@ -50,7 +50,7 @@ Et spørsmål i PostgreSQL kan *aldri* endres (`UPDATE`) etter at folk har begyn
### 4.2 Opprettelse av nye spørsmål og akser (Sandkassen) ### 4.2 Opprettelse av nye spørsmål og akser (Sandkassen)
* **Inkubator:** Innloggede brukere kan opprette nye spørsmål eller definere helt nye akser (ved å angi to motpoler). Disse havner i en "Sandkasse" og vises ikke på forsiden før de har fått nok organisk engasjement fra andre innloggede brukere. * **Inkubator:** Innloggede brukere kan opprette nye spørsmål eller definere helt nye akser (ved å angi to motpoler). Disse havner i en "Sandkasse" og vises ikke på forsiden før de har fått nok organisk engasjement fra andre innloggede brukere.
* **Vekting via Graph Edges:** Koblingen mellom et spørsmål og en akse lagres som en relasjon i `graph_edges`. Når brukere "stemmer opp" at et spørsmål tilhører en spesifikk akse, øker feltet `confidence`. SpacetimeDB bruker denne `confidence`-scoren som multiplikator i match-algoritmen. * **Vekting via Graph Edges:** Koblingen mellom et spørsmål og en akse lagres som en relasjon i `graph_edges`. Når brukere "stemmer opp" at et spørsmål tilhører en spesifikk akse, øker feltet `confidence`. Denne `confidence`-scoren brukes som multiplikator i match-algoritmen.
## 5. Visualisering & Sidelinja Explorer ## 5. Visualisering & Sidelinja Explorer
@ -74,9 +74,9 @@ Av kostnads- og ytelseshensyn skjer all AI-bruk asynkront i backend via jobbkøe
|---|---| |---|---|
| **SvelteKit (Klient)** | UX, Anonym UUID-håndtering i `localStorage`, kortstokk-swipe-grensesnitt, "lazy loading" av baksiden ved innlogging, visning av Heatmaps via Sidelinja Explorer. | | **SvelteKit (Klient)** | UX, Anonym UUID-håndtering i `localStorage`, kortstokk-swipe-grensesnitt, "lazy loading" av baksiden ved innlogging, visning av Heatmaps via Sidelinja Explorer. |
| **Authentik** | SSO for brukere, kandidater og redaksjon. Sammenfletting av anonym UUID med ekte bruker-ID. | | **Authentik** | SSO for brukere, kandidater og redaksjon. Sammenfletting av anonym UUID med ekte bruker-ID. |
| **SpacetimeDB** | Sanntids match-kalkulering (Rust Reducers), swiping-logikk, PCA-beregning, og "Multiplayer"-rom ("Sofagruppa"). Holder kun aktive sesjoner i minnet. | | **Sanntid (WebSocket)** | Match-kalkulering, swiping-logikk, PCA-beregning. Sanntidsoppdateringer via PG LISTEN/NOTIFY + WebSocket. |
| **PostgreSQL** | Kunnskapsgrafen (`nodes`, `graph_edges`). Permanent lagring av spørsmål, akser, versjonshistorikk og aggregerte data for Sidelinja Explorer. | | **PostgreSQL** | Kunnskapsgrafen (`nodes`, `graph_edges`). Permanent lagring av spørsmål, akser, versjonshistorikk og aggregerte data for Sidelinja Explorer. |
| **Rust Worker (Sync)** | Synkroniserer batcher av svar fra SpacetimeDB over til PostgreSQL-lagringen via standard sync-mekanismen (se `synkronisering.md`). | | **PostgreSQL** | Eneste datakilde for spørsmål, akser, svar og kunnskapsgraf. |
| **Rust Worker (AI)** | Kjører `valgomat_generate_profile` og `valgomat_moderation` via jobbkøen. | | **Rust Worker (AI)** | Kjører `valgomat_generate_profile` og `valgomat_moderation` via jobbkøen. |
## 8. Dataklassifisering (ref. docs/arkitektur.md 2.2) ## 8. Dataklassifisering (ref. docs/arkitektur.md 2.2)
@ -84,22 +84,20 @@ Av kostnads- og ytelseshensyn skjer all AI-bruk asynkront i backend via jobbkøe
| Data | Kategori | Detaljer | | Data | Kategori | Detaljer |
|---|---|---| |---|---|---|
| Spørsmål, akser, versjonshistorikk | Kritisk (PG) | Brukergenerert innhold, krever backup | | Spørsmål, akser, versjonshistorikk | Kritisk (PG) | Brukergenerert innhold, krever backup |
| Individuelle svar (aggregert) | Kritisk (PG) | Synket fra SpacetimeDB | | Individuelle svar (aggregert) | Kritisk (PG) | Lagret direkte i PG |
| Aktive sesjoner, live PCA-state | Flyktig (SpacetimeDB) | Tåler tap — bruker svarer på nytt |
| AI-genererte kandidatprofiler | Avledet (PG) | Kan regenereres fra partiprogrammer | | AI-genererte kandidatprofiler | Avledet (PG) | Kan regenereres fra partiprogrammer |
## 9. Skaleringsrisiko ## 9. Skaleringsrisiko
### PCA i SpacetimeDB ### PCA-beregning
PCA-beregning i SpacetimeDB er minnekrevende og udokumentert for store datasett. Ved tusenvis av samtidige brukere med 50+ akser kan minnebruken eksplodere. Tiltak: PCA-beregning er potensielt tung ved mange brukere og akser. Tiltak:
- **Materialized Views i PostgreSQL** for Sidelinja Explorer — aldri kjør tunge aggregeringer on-the-fly. Oppdater views via nattlig jobb eller etter batch-sync fra SpacetimeDB. - **Materialized Views i PostgreSQL** for Sidelinja Explorer — aldri kjør tunge aggregeringer on-the-fly. Oppdater views via nattlig jobb.
- **Batch-sync med størrelsesbegrensning** — sync_outbox kan bli bottleneck ved høy svaraktivitet. Vurder dedikert sync-frekvens for valgomat-data. - **PCA i PG** — beregnes i PostgreSQL med caching og oppdateringsintervall.
- **PCA-fallback i PG** — hvis SpacetimeDB-minnebruk overstiger terskel, flytt PCA-beregning til PostgreSQL (via `pg_stat_statements` for kostnadsmåling) med lengre oppdateringsintervall. - **Overvåk tidlig** — legg til valgomat-spesifikke metrikker i `/admin/observability` (antall aktive sesjoner, PCA-beregningstid).
- **Overvåk tidlig** — legg til valgomat-spesifikke metrikker i `/admin/observability` (antall aktive sesjoner, PCA-beregningstid, SpacetimeDB-minnebruk for valgomat-tabeller).
## 10. Instruks for Claude Code ## 10. Instruks for Claude Code
1. **Datamodell:** Utvid enum `node_type` i PostgreSQL med `valgomat_question` og `valgomat_axis`. Bruk `graph_edges` med `relation_type = 'AFFECTS_AXIS'` og oppdater `confidence`-feltet for å håndtere crowdsourcet vekting av spørsmål opp mot ulike akser. 1. **Datamodell:** Utvid enum `node_type` i PostgreSQL med `valgomat_question` og `valgomat_axis`. Bruk `graph_edges` med `relation_type = 'AFFECTS_AXIS'` og oppdater `confidence`-feltet for å håndtere crowdsourcet vekting av spørsmål opp mot ulike akser.
2. **SpacetimeDB Reducers:** Implementer innkommende events som `SubmitSwipe`, `SuggestAxis`, og match-algoritmen i Rust inne i SpacetimeDB. Pass på at reducere støtter anonym `session_id`. 2. **Swiping-logikk:** Implementer `SubmitSwipe`, `SuggestAxis` og match-algoritmen i maskinrommet. Støtt anonym `session_id`.
3. **State Management:** SvelteKit skal ikke kreve innlogging for forsiden. Implementer Auth-guards slik at opprettelse av spørsmål, stemmegivning på andres forslag og visning av kommentarer gir `403 Forbidden` for uautoriserte og trigger Authentik-flyten. 3. **State Management:** SvelteKit skal ikke kreve innlogging for forsiden. Implementer Auth-guards slik at opprettelse av spørsmål, stemmegivning på andres forslag og visning av kommentarer gir `403 Forbidden` for uautoriserte og trigger Authentik-flyten.
4. **Explorer API:** Bygg aggregerte Materialized Views i PostgreSQL for Sidelinja Explorer for å unngå tung on-the-fly kalkulering av Heatmaps over millioner av rader. 4. **Explorer API:** Bygg aggregerte Materialized Views i PostgreSQL for Sidelinja Explorer for å unngå tung on-the-fly kalkulering av Heatmaps over millioner av rader.
5. **Jobbtyper:** Registrer `valgomat_generate_profile` og `valgomat_moderation` som jobbtyper i jobbkøen (se `jobbkø.md`). Bruk `sidelinja/rutine` som modellalias. 5. **Jobbtyper:** Registrer `valgomat_generate_profile` og `valgomat_moderation` som jobbtyper i jobbkøen (se `jobbkø.md`). Bruk `sidelinja/rutine` som modellalias.

View file

@ -150,7 +150,7 @@ Maskinrommet:
6. Logg forbruk i ai_usage_log 6. Logg forbruk i ai_usage_log
Frontend mottar oppdatering via SpacetimeDB Frontend mottar oppdatering via WebSocket (PG LISTEN/NOTIFY)
``` ```
### 6.2 Drag-and-drop integrasjon ### 6.2 Drag-and-drop integrasjon

View file

@ -30,13 +30,13 @@ Canvas-primitiv (felles)
Whiteboard (consumer) Whiteboard (consumer)
├── Tegneverktøy: penn, linje, rektangel, tekst ├── Tegneverktøy: penn, linje, rektangel, tekst
├── Strøk-modell: SVG paths / canvas paths ├── Strøk-modell: SVG paths / canvas paths
└── SpacetimeDB: strøk-synkronisering └── Sanntid: strøk-synkronisering via WebSocket
Storyboard (consumer) Storyboard (consumer)
├── Kort-rendering: <MessageBox> i kompakt modus ├── Kort-rendering: <MessageBox> i kompakt modus
├── Status-modell: Klar / Tatt opp / Droppet / Arkivert ├── Status-modell: Klar / Tatt opp / Droppet / Arkivert
├── Portal-soner: overføringsmekanikk til andre blokker ├── Portal-soner: overføringsmekanikk til andre blokker
└── SpacetimeDB: kort-posisjon + status-synkronisering └── Sanntid: kort-posisjon + status-synkronisering via WebSocket
``` ```
## 2. Kamera-modell ## 2. Kamera-modell
@ -167,9 +167,9 @@ Enhver blokk i `BlockShell` kan gå i fullskjerm. Dette er en generell feature,
- **Escape:** Trykk Esc eller klikk "minimer"-knapp for å gå tilbake - **Escape:** Trykk Esc eller klikk "minimer"-knapp for å gå tilbake
- **URL-state:** Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side - **URL-state:** Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side
## 8. SpacetimeDB-integrasjon ## 8. Sanntidsintegrasjon
Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB: Canvas-primitivet selv har ingen sanntidskobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til WebSocket:
```typescript ```typescript
interface CanvasEvents { interface CanvasEvents {
@ -180,7 +180,7 @@ interface CanvasEvents {
} }
``` ```
Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer for å synkronisere posisjon til andre klienter. Storyboard-consumeren bruker `onObjectMove` til å oppdatere PG via maskinrommet, som propagerer endringen til andre klienter via WebSocket.
## 9. Bygger på ## 9. Bygger på
@ -201,7 +201,7 @@ Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer
### Fase 2: Storyboard som første consumer ### Fase 2: Storyboard som første consumer
- `<StoryboardCard>` rendrer meldingsboks-kort på canvaset - `<StoryboardCard>` rendrer meldingsboks-kort på canvaset
- SpacetimeDB-synk for posisjon og status - Sanntidssynk for posisjon og status via WebSocket
- Portal-soner for overføring - Portal-soner for overføring
### Fase 3: Whiteboard-migrering ### Fase 3: Whiteboard-migrering

View file

@ -2,7 +2,7 @@
**Filsti:** `docs/features/chat.md` **Filsti:** `docs/features/chat.md`
## 1. Konsept ## 1. Konsept
En universell, sanntids meldingskomponent bygget på SpacetimeDB. Chat er ikke bundet til én kontekst — den kan knyttes til enhver node i Kunnskapsgrafen via **channels**. Ulike konsepter bruker chat med ulik konfigurasjon, men all infrastruktur er delt. En universell, sanntids meldingskomponent. Chat er ikke bundet til én kontekst — den kan knyttes til enhver node i Kunnskapsgrafen via **channels**. Ulike konsepter bruker chat med ulik konfigurasjon, men all infrastruktur er delt. Sanntid via PG LISTEN/NOTIFY + WebSocket.
## 2. Channels-modellen ## 2. Channels-modellen
En **channel** er en meldingsstrøm knyttet til en vilkårlig node (parent) i grafen. Channelen er selv en node (`node_type = 'channel'`), noe som betyr at den deltar i grafen og arver tilgangsstyring via `node_access`-matrisen. En **channel** er en meldingsstrøm knyttet til en vilkårlig node (parent) i grafen. Channelen er selv en node (`node_type = 'channel'`), noe som betyr at den deltar i grafen og arver tilgangsstyring via `node_access`-matrisen.
@ -74,14 +74,15 @@ messages (
) )
``` ```
### 3.2 SpacetimeDB (sanntid) ### 3.2 Sanntid
SpacetimeDB holder aktive channels' meldinger i minnet som varm cache foran PG. Ved oppstart gjør worker warmup fra PG → SpacetimeDB (per-kanal konfigurasjon). Nye meldinger sendes via SpacetimeDB-reducers og kringkastes til alle tilkoblede klienter. Synkes til PostgreSQL med ~1 sek forsinkelse (se `docs/infra/synkronisering.md`). Nye meldinger skrives til PG og propageres via LISTEN/NOTIFY + WebSocket
til alle tilkoblede klienter i sanntid.
## 4. Mentions & Autocomplete ## 4. Mentions & Autocomplete
Kun aktive når `config.mentions = true`. Kun aktive når `config.mentions = true`.
* **Trigger-tegn:** `#` (Temaer/Aktører fra Kunnskapsgrafen), `@` (brukere/redaksjonsmedlemmer), `/` (kommandoer). * **Trigger-tegn:** `#` (Temaer/Aktører fra Kunnskapsgrafen), `@` (brukere/redaksjonsmedlemmer), `/` (kommandoer).
* **Filtrering:** Svelte-klienten filtrerer den lokale SpacetimeDB-cachen umiddelbart. Skriver man `#Ha...` vises en klikkbar liste ("Hans Petter Sjøli", "Høyre"). * **Filtrering:** Svelte-klienten filtrerer den lokale cachen umiddelbart. Skriver man `#Ha...` vises en klikkbar liste ("Hans Petter Sjøli", "Høyre").
* **Grafkobling:** Ved `#`-mention opprettes automatisk `MENTIONS`-edges i `graph_edges` mellom meldingen og den nevnte noden. * **Grafkobling:** Ved `#`-mention opprettes automatisk `MENTIONS`-edges i `graph_edges` mellom meldingen og den nevnte noden.
* **Mobil-optimalisert:** Autocomplete-listen er tappbar og tilpasset mindre skjermer. * **Mobil-optimalisert:** Autocomplete-listen er tappbar og tilpasset mindre skjermer.
@ -119,16 +120,10 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s
### Ferdig (mars 2026) ### Ferdig (mars 2026)
- **ChatBlock.svelte:** Adapter-mønster via `createChat()` factory. Bruker `chat.edit()`, `chat.delete()`, `chat.react()` — ingen direkte PG API-kall. - **ChatBlock.svelte:** Adapter-mønster via `createChat()` factory. Bruker `chat.edit()`, `chat.delete()`, `chat.react()` — ingen direkte PG API-kall.
- **SpacetimeDB-adapter (`spacetime.svelte.ts`):** Ren SpacetimeDB-adapter. All data fra SpacetimeDB (historikk via warmup + sanntid). Reaksjoner fra `message_reaction`-tabellen. - **WebSocket-adapter:** Sanntidsdata via PG LISTEN/NOTIFY + WebSocket.
- **PG-adapter (`pg.svelte.ts`):** Polling hvert 3. sek. Readonly fallback når SpacetimeDB ikke er konfigurert.
- **Factory (`create.svelte.ts`):** Velger adapter basert på `VITE_SPACETIMEDB_URL`. SSR-safe med `browser`-guard.
- **Shared types (`types.ts`):** `ChatConnection` interface med `send`, `edit`, `delete`, `react`, `readonly`. - **Shared types (`types.ts`):** `ChatConnection` interface med `send`, `edit`, `delete`, `react`, `readonly`.
- **SpacetimeDB Rust-modul (`spacetimedb/src/lib.rs`):** `ChatMessage`, `MessageReaction`, `SyncOutbox`-tabeller. Reducers: `send_message`, `delete_message`, `edit_message`, `add_reaction`, `remove_reaction`, `load_messages`, `load_reactions`, `clear_channel`, `mark_synced`.
- **Worker warmup (`worker/src/warmup.rs`):** PG → SpacetimeDB ved oppstart. Per-kanal konfig (all/messages/days/none). Trådbasert henting.
- **Worker sync (`worker/src/sync.rs`):** SpacetimeDB → PG hvert sekund. Insert/delete/update meldinger + reaksjoner.
- **Admin-side (`/admin/channels`):** Per-kanal warmup-konfigurasjon.
- **Tråder:** Komplett trådvisning med datogruppering, autoscroll og visuell skillelinje mellom tråder. - **Tråder:** Komplett trådvisning med datogruppering, autoscroll og visuell skillelinje mellom tråder.
- **Reaksjoner:** Via SpacetimeDB-reducers, synket til PG. - **Reaksjoner:** Lagret i PG, propagert via WebSocket.
- **Meldingskollaps:** Lange meldinger begrenses til 2 linjer med "Vis mer"/"Vis mindre". - **Meldingskollaps:** Lange meldinger begrenses til 2 linjer med "Vis mer"/"Vis mindre".
- **AI-behandling:** Meldinger kan AI-behandles (✨-knapp, eldre modell). Revisjons-toggle viser original vs. AI-versjon. Markdown-rendering for AI-output. NB: Erstattes av frittstående AI-verktøy på arbeidsflaten — se `docs/features/ai_verktoy.md`. - **AI-behandling:** Meldinger kan AI-behandles (✨-knapp, eldre modell). Revisjons-toggle viser original vs. AI-versjon. Markdown-rendering for AI-output. NB: Erstattes av frittstående AI-verktøy på arbeidsflaten — se `docs/features/ai_verktoy.md`.
- **Konvertering:** Meldinger kan opprettes som kanban-kort eller kalenderhendelse (dialog sier "Opprett", ikke "Konverter" — meldingen beholdes i chatten). - **Konvertering:** Meldinger kan opprettes som kanban-kort eller kalenderhendelse (dialog sier "Opprett", ikke "Konverter" — meldingen beholdes i chatten).
@ -144,13 +139,12 @@ Channels med `config.ttl_days` satt til et tall får sine meldinger automatisk s
### Gjenstår ### Gjenstår
- **Vedlegg, TTL** — avventer implementering. - **Vedlegg, TTL** — avventer implementering.
- **Tilgangsfiltrering:** SpacetimeDB-laget må filtrere basert på `node_access`-matrisen. - **Pin/konvertering:** Gjenstår.
- **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. * **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.
* **PG er autoritativ** — SpacetimeDB er varm cache. Frontend snakker kun med SpacetimeDB. * **PG er eneste datakilde.** Sanntid via LISTEN/NOTIFY + WebSocket.
* **Tilgang styres via `node_access`-matrisen.** Channels arver tilgang fra sin parent-node via edges. * **Tilgang styres via `node_access`-matrisen.** Channels arver tilgang fra sin parent-node via edges.

View file

@ -7,7 +7,7 @@ som ikke er delt med andre via edges. Fungerer som en kronologisk logg
over tanker, notater og idéer som kun er synlige for eieren. over tanker, notater og idéer som kun er synlige for eieren.
## 2. Status ## 2. Status
**Implementert med nodes+edges (mars 2026).** Sanntid via SpacetimeDB. **Implementert med nodes+edges (mars 2026).** Sanntid via PG LISTEN/NOTIFY + WebSocket.
### Implementert ### Implementert
- Frontend: `/diary` route med dagbok-visning - Frontend: `/diary` route med dagbok-visning
@ -55,9 +55,9 @@ POST /intentions/create_edge
`/diary``frontend/src/routes/diary/+page.svelte` `/diary``frontend/src/routes/diary/+page.svelte`
### Datakilde ### Datakilde
SpacetimeDB sanntidsabonnement via `nodeStore` og `edgeStore`. WebSocket-sanntidsdata via `nodeStore` og `edgeStore`.
Ingen backend-query — all filtrering skjer i frontend basert på Ingen backend-query — all filtrering skjer i frontend basert på
SpacetimeDB-data som allerede er lastet. data mottatt via initial sync + WebSocket.
### UI-struktur ### UI-struktur
- Header med tilbake-lenke til mottak og innlegg-teller - Header med tilbake-lenke til mottak og innlegg-teller

View file

@ -6,7 +6,7 @@ Månedsbasert kalendervisning for redaksjonell planlegging. Hendelser er nodes i
## 2. Status ## 2. Status
**Kalendervisning implementert (mars 2026).** Bruker `scheduled`-edges i stedet for **Kalendervisning implementert (mars 2026).** Bruker `scheduled`-edges i stedet for
separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gjenstår. separat `calendar_events`-tabell. Abonnement og ICS-eksport gjenstår.
### Implementert ### Implementert
- **Fase 1 (v1, mars 2025):** PG-adapter med `calendars` + `calendar_events` (legacy) - **Fase 1 (v1, mars 2025):** PG-adapter med `calendars` + `calendar_events` (legacy)
@ -22,7 +22,7 @@ separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gj
- Hendelsesliste under rutenett for gjeldende måned - Hendelsesliste under rutenett for gjeldende måned
- Lenke fra mottak-siden med hendelsesteller - Lenke fra mottak-siden med hendelsesteller
- Tilgang via `nodeVisibility` (respekterer `node_access`-matrise) - Tilgang via `nodeVisibility` (respekterer `node_access`-matrise)
- Sanntidsoppdatering via SpacetimeDB-subscriptions - Sanntidsoppdatering via WebSocket (PG LISTEN/NOTIFY)
### Gjenstår — Fase 2 ### Gjenstår — Fase 2
- Kobling til kanban-kort (vis deadline på kalender) - Kobling til kanban-kort (vis deadline på kalender)
@ -33,7 +33,6 @@ separat `calendar_events`-tabell. Abonnement, ICS-eksport og SpacetimeDB-sync gj
- Abonnementsmodell (kalender → kalender via graph_edges) - Abonnementsmodell (kalender → kalender via graph_edges)
- Personlige vs. delte kalendere (via samlings-noder) - Personlige vs. delte kalendere (via samlings-noder)
- ICS/CalDAV-eksport - ICS/CalDAV-eksport
- SpacetimeDB-modul + hybrid-adapter
- Varsler/påminnelser via jobbkøen - Varsler/påminnelser via jobbkøen
## 3. Datamodell (implementert) ## 3. Datamodell (implementert)

View file

@ -7,7 +7,7 @@ episodeplanlegging i Redaksjonen, men også mottaker av AI-genererte
action points fra Møterommet. action points fra Møterommet.
## 2. Status ## 2. Status
**Implementert med nodes+edges (mars 2026).** Sanntid via SpacetimeDB. **Implementert med nodes+edges (mars 2026).** Sanntid via PG LISTEN/NOTIFY + WebSocket.
### Implementert ### Implementert
- Board = samlings-node (`node_kind: 'collection'`, `metadata.board: true`) - Board = samlings-node (`node_kind: 'collection'`, `metadata.board: true`)
@ -18,7 +18,7 @@ action points fra Møterommet.
- Backend: `POST /intentions/update_edge` for statusendring - Backend: `POST /intentions/update_edge` for statusendring
- Backend: `GET /query/board?board_id=...` for board-spørring - Backend: `GET /query/board?board_id=...` for board-spørring
- Frontend: `/board/[id]` route med HTML5 drag-and-drop - Frontend: `/board/[id]` route med HTML5 drag-and-drop
- Sanntid via SpacetimeDB edge-subscriptions (ingen polling) - Sanntid via PG LISTEN/NOTIFY + WebSocket (ingen polling)
- Opprett kort direkte i kolonne (tittel-input) - Opprett kort direkte i kolonne (tittel-input)
- Oppretting av nye brett fra mottak-siden - Oppretting av nye brett fra mottak-siden
@ -97,5 +97,5 @@ med `submitted_to`-edge til samlingen, inkludert forfatterinfo.
* Board er en collection-node med `metadata.board: true`. * Board er en collection-node med `metadata.board: true`.
* Status er en `status`-edge (kort → board) med `metadata.value`. * Status er en `status`-edge (kort → board) med `metadata.value`.
* Bruk native HTML5 Drag and Drop, unngå tunge biblioteker. * Bruk native HTML5 Drag and Drop, unngå tunge biblioteker.
* Sanntid via SpacetimeDB edge-subscriptions. * Sanntid via PG LISTEN/NOTIFY + WebSocket.
* Tilgang styres via `node_access`-matrisen. * Tilgang styres via `node_access`-matrisen.

View file

@ -11,7 +11,7 @@ Brukes i Studioet (se `docs/concepts/studioet.md`). En "virtuell co-host" som dy
1. Live transkripsjon (se `docs/features/live_transkripsjon.md`) leverer tekst-chunks. 1. Live transkripsjon (se `docs/features/live_transkripsjon.md`) leverer tekst-chunks.
2. Rust-tjenesten analyserer for egennavn (Named Entity Recognition). 2. Rust-tjenesten analyserer for egennavn (Named Entity Recognition).
3. Lynraskt oppslag i PostgreSQL: `SELECT * FROM factoids JOIN actors... WHERE actor.name = $1`. 3. Lynraskt oppslag i PostgreSQL: `SELECT * FROM factoids JOIN actors... WHERE actor.name = $1`.
4. Treff dytter `LiveFactoidEvent` inn i SpacetimeDB. 4. Treff dytter `LiveFactoidEvent` via WebSocket til frontend.
5. SvelteKit-studio viser faktoiden lydløst i en egen boks. 5. SvelteKit-studio viser faktoiden lydløst i en egen boks.
### 2.2 Lagring ### 2.2 Lagring
@ -55,7 +55,7 @@ Studio-modus har en **kill switch** — en synlig "Stopp AI"-knapp i studio-gren
Nødvendig fordi AI-en kan dytte feil eller irrelevante faktoider under live innspilling. Programlederen må kunne slå den av uten å forlate studio-viewet. Nødvendig fordi AI-en kan dytte feil eller irrelevante faktoider under live innspilling. Programlederen må kunne slå den av uten å forlate studio-viewet.
Kill switch-status (`ai_enabled: bool`) lagres på LiveKit-rommet i SpacetimeDB og synkes til alle klienter i rommet. Kill switch-status (`ai_enabled: bool`) lagres i kommunikasjonsnoden metadata i PG og synkes til alle klienter via WebSocket.
## 5. Instruks for Claude Code ## 5. Instruks for Claude Code
* Begge moduser deler samme Whisper-pipeline — ikke dupliser transkripsjonskode. * Begge moduser deler samme Whisper-pipeline — ikke dupliser transkripsjonskode.

View file

@ -63,14 +63,15 @@ Eksempler på standard-pads: jingle/intro, applaus, latter, dramatisk pause,
### 3.4 Delt mixer-kontroll (flerbruker) ### 3.4 Delt mixer-kontroll (flerbruker)
Alle deltakere i rommet kan se og bruke mixeren samtidig. Mixer-state Alle deltakere i rommet kan se og bruke mixeren samtidig. Mixer-state
synkroniseres i sanntid via SpacetimeDB, slik at volumendringer, mutes, synkroniseres i sanntid via PG LISTEN/NOTIFY + WebSocket, slik at
effekttogles og pad-avspilling reflekteres hos alle klienter umiddelbart. volumendringer, mutes, effekttogles og pad-avspilling reflekteres hos
alle klienter umiddelbart.
| Element | Synkronisering | | Element | Synkronisering |
|---|---| |---|---|
| **Volumslider** | STDB: `MixerChannel`-tabell med `gain`-verdi per kanal | | **Volumslider** | PG: `mixer_channels`-tabell med `gain`-verdi per kanal |
| **Mute** | STDB: `is_muted` boolean per kanal | | **Mute** | PG: `is_muted` boolean per kanal |
| **Effekt av/på** | STDB: `active_effects` JSON per kanal | | **Effekt av/på** | PG: `active_effects` JSON per kanal |
| **Pad-trigger** | LiveKit Data Message (lav latens) | | **Pad-trigger** | LiveKit Data Message (lav latens) |
| **Pad-konfig** | Node metadata (persistent, sjelden endring) | | **Pad-konfig** | Node metadata (persistent, sjelden endring) |
@ -83,35 +84,18 @@ effekttogles og pad-avspilling reflekteres hos alle klienter umiddelbart.
rolle-system (owner/admin/member-edges). rolle-system (owner/admin/member-edges).
**Konflikthåndtering:** Last-write-wins. Volumslidere er kontinuerlige **Konflikthåndtering:** Last-write-wins. Volumslidere er kontinuerlige
verdier som oppdateres via STDB-reducers. Ved samtidig endring av samme verdier som oppdateres via maskinrommet. Ved samtidig endring av samme
kanal vinner siste skriving — i praksis uproblematisk fordi endringer kanal vinner siste skriving — i praksis uproblematisk fordi endringer
er visuelt synlige for alle og deltakerne koordinerer naturlig. er visuelt synlige for alle og deltakerne koordinerer naturlig.
**SpacetimeDB-tabeller:** **PG-tabell:** `mixer_channels` med NOTIFY-trigger for sanntidspropagering via WebSocket.
```rust **API-endepunkter (maskinrommet):**
#[spacetimedb::table(accessor = mixer_channel, public)] - `POST /intentions/create_mixer_channel` — idempotent opprettelse
pub struct MixerChannel { - `POST /intentions/set_gain` — clamped 0.01.5, viewer-sjekk
#[primary_key] - `POST /intentions/set_mute` — viewer-sjekk
pub id: String, // "{room_id}:{target_user_id}" - `POST /intentions/toggle_effect` — JSON-toggle
pub room_id: String, // "communication_{node_uuid}" - `POST /intentions/set_mixer_role` — sett editor/viewer
pub target_user_id: String, // hvem kanalen tilhører
pub gain: f64, // 0.01.5
pub is_muted: bool,
pub active_effects: String, // JSON: {"fat_bottom": true, "robot": false, ...}
pub role: String, // "editor" | "viewer" — tilgangskontroll per kanal
pub updated_by: String, // hvem som sist endret
pub updated_at: Timestamp,
}
```
**STDB Reducers:**
- `create_mixer_channel(room_id, target_user_id, updated_by)` — idempotent opprettelse
- `set_gain(room_id, target_user_id, gain, updated_by)` — clamped 0.01.5, viewer-sjekk
- `set_mute(room_id, target_user_id, is_muted, updated_by)` — viewer-sjekk
- `toggle_effect(room_id, target_user_id, effect_name, updated_by)` — JSON-toggle
- `delete_mixer_channel(room_id, target_user_id)` — opprydding ved disconnect
- `set_mixer_role(room_id, target_user_id, role, updated_by)` — sett editor/viewer
Mixer-kanaler ryddes automatisk ved `close_live_room` og `clear_all`. Mixer-kanaler ryddes automatisk ved `close_live_room` og `clear_all`.
@ -213,7 +197,7 @@ Lydmixeren aktiveres via `mixer`-traitet på en samlings-node. Krever at
- [x] Master fader og master mute - [x] Master fader og master mute
### Fase B: Delt mixer-kontroll ### Fase B: Delt mixer-kontroll
- [x] SpacetimeDB: `MixerChannel`-tabell + reducers (set_gain, set_mute, toggle_effect, set_mixer_role) - [x] PG: `mixer_channels`-tabell + NOTIFY-trigger + maskinrommet-endepunkter
- [x] Frontend abonnerer på mixer-state, oppdaterer Web Audio-graf ved endringer - [x] Frontend abonnerer på mixer-state, oppdaterer Web Audio-graf ved endringer
- [x] Visuell feedback: alle ser sliders bevege seg i sanntid - [x] Visuell feedback: alle ser sliders bevege seg i sanntid
- [x] Tilgangskontroll: eier/admin kan sette deltaker til "viewer" (kun observere) - [x] Tilgangskontroll: eier/admin kan sette deltaker til "viewer" (kun observere)
@ -229,7 +213,7 @@ Lydmixeren aktiveres via `mixer`-traitet på en samlings-node. Krever at
- [x] Fat bottom (lowshelf filter, +8dB @ 200Hz) - [x] Fat bottom (lowshelf filter, +8dB @ 200Hz)
- [x] Sparkle (highshelf filter, +4dB @ 10kHz) - [x] Sparkle (highshelf filter, +4dB @ 10kHz)
- [x] Exciter (WaveShaperNode soft-clip + highshelf @ 3.5kHz) - [x] Exciter (WaveShaperNode soft-clip + highshelf @ 3.5kHz)
- [x] Per-kanal av/på-toggles for hver effekt (synkronisert via STDB `active_effects`) - [x] Per-kanal av/på-toggles for hver effekt (synkronisert via PG `active_effects`)
- [x] Preset-konfigurasjon: "Av", "Podcast-stemme" (bass+luft), "Radio-stemme" (bass+luft+exciter) - [x] Preset-konfigurasjon: "Av", "Podcast-stemme" (bass+luft), "Radio-stemme" (bass+luft+exciter)
- [x] Highpass-filter (80Hz) alltid aktiv for rumble-fjerning - [x] Highpass-filter (80Hz) alltid aktiv for rumble-fjerning

View file

@ -5,7 +5,7 @@
Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike kontekster — show notes, møtenotater, research-notater. Notater er nodes i kunnskapsgrafen og kan kobles til andre noder. Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike kontekster — show notes, møtenotater, research-notater. Notater er nodes i kunnskapsgrafen og kan kobles til andre noder.
## 2. Status ## 2. Status
**PG-adapter ferdig og deployet (mars 2025).** Rich text og SpacetimeDB-sync gjenstår. **PG-adapter ferdig og deployet (mars 2025).** Rich text gjenstår.
### Implementert ### Implementert
- Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes) - Migrering `0004_notes.sql`: `notes`-tabell (FK→nodes)
@ -21,7 +21,6 @@ Et enkelt notatverktøy med automatisk lagring. Brukes som scratchpad i ulike ko
- Versjonering / undo-historikk - Versjonering / undo-historikk
- Kobling til andre noder (temaer, episoder, aktører) - Kobling til andre noder (temaer, episoder, aktører)
- Flerbruker-redigering (conflict resolution) - Flerbruker-redigering (conflict resolution)
- SpacetimeDB-modul + hybrid-adapter
- Eksport (Markdown, PDF) - Eksport (Markdown, PDF)
## 3. Datamodell (implementert) ## 3. Datamodell (implementert)

View file

@ -168,33 +168,10 @@ med forklaring ved inkompatibilitet.
Factory-funksjon `createBlockReceiver(toolType)` oppretter en `BlockReceiver` Factory-funksjon `createBlockReceiver(toolType)` oppretter en `BlockReceiver`
for en gitt verktøy-type. for en gitt verktøy-type.
## 5. SpacetimeDB-integrasjon ## 5. Sanntidsintegrasjon
Plasseringsdata for sanntidskontekster (storyboard, kanban) eies av SpacetimeDB: Plasseringsdata lagres i PG `message_placements`-tabellen. Endringer
propageres via LISTEN/NOTIFY + WebSocket for sanntidsoppdatering i frontend.
```rust
#[table(name = message_placement, public)]
pub struct MessagePlacement {
#[primary_key]
pub id: String,
pub message_id: String,
pub context_type: String,
pub context_id: String,
pub entered_at: Timestamp,
pub position_json: String, // JSON-serialisert posisjon
}
#[reducer]
pub fn place_message(ctx: &ReducerContext, placement: MessagePlacement) { ... }
#[reducer]
pub fn remove_placement(ctx: &ReducerContext, message_id: String, context_type: String, context_id: String) { ... }
#[reducer]
pub fn move_on_canvas(ctx: &ReducerContext, placement_id: String, new_position_json: String) { ... }
```
Sync-workeren persisterer til PG `message_placements`-tabellen.
## 6. Responsivt design ## 6. Responsivt design
@ -207,7 +184,7 @@ Sync-workeren persisterer til PG `message_placements`-tabellen.
- **Meldingsboks** (`meldingsboks.md`): Alle overførte objekter er meldingsbokser - **Meldingsboks** (`meldingsboks.md`): Alle overførte objekter er meldingsbokser
- **Kunnskapsgraf** (`kunnskapsgraf_og_relasjoner.md`): Plasseringer er relasjoner i grafen - **Kunnskapsgraf** (`kunnskapsgraf_og_relasjoner.md`): Plasseringer er relasjoner i grafen
- **BlockShell** / PageGrid: Verktøy-panel-rammen som rendrer mottakssoner - **BlockShell** / PageGrid: Verktøy-panel-rammen som rendrer mottakssoner
- **SpacetimeDB** (`synkronisering.md`): Sanntidssynk av plasseringer - **PG LISTEN/NOTIFY + WebSocket**: Sanntidssynk av plasseringer
## 8. Konsekvenser for eksisterende kode ## 8. Konsekvenser for eksisterende kode
@ -222,9 +199,9 @@ Sync-workeren persisterer til PG `message_placements`-tabellen.
`message_placements` er ny. Eksisterende `kanban_card_view` og `calendar_event_view` lever parallelt inntil migrering. `message_placements` er ny. Eksisterende `kanban_card_view` og `calendar_event_view` lever parallelt inntil migrering.
### 8.3 SpacetimeDB-modul ### 8.3 Sanntid
Ny tabell `message_placement` med reducers for place/remove/move. PG NOTIFY-trigger på `message_placements` for sanntidspropagering via WebSocket.
## 9. Implementasjonsstatus ## 9. Implementasjonsstatus

View file

@ -12,7 +12,7 @@ En interaktiv graf-visning i SvelteKit som gjør Kunnskapsgrafen visuelt naviger
## 3. Datakilde ## 3. Datakilde
* **SvelteKit server-side:** Henter grafdata via Recursive CTE-spørringer mot PostgreSQL og returnerer `{ "nodes": [...], "edges": [...] }` til klienten. * **SvelteKit server-side:** Henter grafdata via Recursive CTE-spørringer mot PostgreSQL og returnerer `{ "nodes": [...], "edges": [...] }` til klienten.
* **SpacetimeDB:** Brukes ikke for graf-visualisering — dette er historiske data som lever i PostgreSQL. * Graf-visualisering er basert på historiske data som lever i PostgreSQL.
## 4. Instruks for Claude Code ## 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).

View file

@ -13,7 +13,7 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst
| **Personlig** | Kun brukeren selv | Privat skisse, kan deles til et Tema senere | | **Personlig** | Kun brukeren selv | Privat skisse, kan deles til et Tema senere |
## 3. Sanntidssynkronisering ## 3. Sanntidssynkronisering
* **SpacetimeDB** synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid. * PG LISTEN/NOTIFY + WebSocket synkroniserer strøk (penseltype, farge, koordinater) mellom alle deltakere i sanntid.
* Hvert whiteboard har en unik ID og er en node i grafen. * 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.
@ -23,12 +23,11 @@ Et delt, sanntids tegnebrett for frihåndsskisser, diagrammer og visuell brainst
* **Implementering:** HTML Canvas eller SVG i SvelteKit. Vurder et lett bibliotek (f.eks. tldraw, Excalidraw) hvis frihåndskvaliteten krever det — men foretrekk egenutviklet for å holde avhengigheter nede. * **Implementering:** HTML Canvas eller SVG i SvelteKit. Vurder et lett bibliotek (f.eks. tldraw, Excalidraw) hvis frihåndskvaliteten krever det — men foretrekk egenutviklet for å holde avhengigheter nede.
## 5. Lagring og Eksport ## 5. Lagring og Eksport
* **Sanntidsdata (SpacetimeDB):** Strøk-historikk holdes i minnet så lenge tavlen er aktiv. * **Sanntidsdata:** Strøk-historikk lagres i PG og propageres via WebSocket.
* **Eksport (PostgreSQL + filsystem):** Når tavlen "lukkes" eller deles, rendres den til PNG eller SVG og lagres som en `media_file`. Referansen knyttes til konteksten (melding, møte) via `message_attachments` eller tilsvarende. * **Eksport (PostgreSQL + filsystem):** Når tavlen "lukkes" eller deles, rendres den til PNG eller SVG og lagres som en `media_file`. Referansen knyttes til konteksten (melding, møte) via `message_attachments` eller tilsvarende.
* **Dataklassifisering:** Strøk-data i SpacetimeDB er flyktig (kategori 4). Eksporterte bilder er avledet (kategori 3) — kan gjenskapes fra strøk-data så lenge tavlen er aktiv, men etter lukking er bildet den permanente kopien.
## 6. Instruks for Claude Code ## 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`. * PG-tabellen for strøk bør være enkel: `whiteboard_id`, `stroke_data` (JSON), `user_id`, `timestamp`. NOTIFY-trigger for sanntid.
* Ikke bygg et fullverdig tegneprogram — start med frihåndstegning, viskelær og farger. Utvid ved behov. * Ikke bygg et fullverdig tegneprogram — start med frihåndstegning, viskelær og farger. Utvid ved behov.
* Tilgang til whiteboards styres via `node_access`-matrisen. * Tilgang til whiteboards styres via `node_access`-matrisen.

View file

@ -107,7 +107,7 @@ Oppdater en spec-node.
} }
``` ```
Oppdaterer noden i STDB + PG. Logger endringen i `ai_usage_log` Oppdaterer noden i PG. Logger endringen i `ai_usage_log`
med `job_type: 'spec_update'`. Oppretter `revision`-node med med `job_type: 'spec_update'`. Oppretter `revision`-node med
forrige versjon for historikk. forrige versjon for historikk.
@ -122,7 +122,7 @@ Send et svar i en samtale (erstatter dagens direkte STDB-skriving).
} }
``` ```
Maskinrommet håndterer node-opprettelse, edges, og STDB-synk. Maskinrommet håndterer node-opprettelse og edges.
Metadata kan inkludere kildereferanser som frontend kan vise. Metadata kan inkludere kildereferanser som frontend kan vise.
#### `POST /agent/suggest_edges` #### `POST /agent/suggest_edges`
@ -144,7 +144,7 @@ godkjenner. Over en viss confidence-terskel kan de auto-godkjennes.
## Autentisering ## Autentisering
Agent-endepunktene bruker agent-token fra `agent_identities`-tabellen. Agent-endepunktene bruker agent-token fra `agent_identities`-tabellen.
Samme token som brukes for STDB-tilkobling. Header: Header:
``` ```
Authorization: Bearer <agent_token> Authorization: Bearer <agent_token>
@ -161,7 +161,7 @@ Med Agent API endres flyten:
### Før (nåværende) ### Før (nåværende)
``` ```
melding → agent_respond-jobb → bygg prompt (SQL) → claude -p → parse svar → skriv STDB+PG melding → agent_respond-jobb → bygg prompt (SQL) → claude -p → parse svar → skriv PG
``` ```
### Etter (med Agent API) ### Etter (med Agent API)

View file

@ -7,7 +7,7 @@ Maskinrommet eier alle skrivinger: det validerer, skriver til PG,
og orkestrerer konsekvenser. og orkestrerer konsekvenser.
Sanntid: PG LISTEN/NOTIFY → maskinrommet → WebSocket `/ws` → frontend. Sanntid: PG LISTEN/NOTIFY → maskinrommet → WebSocket `/ws` → frontend.
SpacetimeDB er under utfasing — frontend bruker kun portvokterens WebSocket (Fase M2). Frontend bruker portvokterens WebSocket for sanntid.
Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → PG. Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet → PG.
## 2. Kommunikasjonskart ## 2. Kommunikasjonskart
@ -57,7 +57,7 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
## 5. Implementerte endepunkter ## 5. Implementerte endepunkter
### Offentlige ### Offentlige
- `GET /health` — Helsesjekk. Verifiserer PG- og STDB-tilkobling. - `GET /health` — Helsesjekk. Verifiserer PG-tilkobling.
### WebSocket (sanntid, oppgave 22.122.2) ### WebSocket (sanntid, oppgave 22.122.2)
- `GET /ws?token=<JWT>` — WebSocket-oppgradering for sanntidsstrøm. - `GET /ws?token=<JWT>` — WebSocket-oppgradering for sanntidsstrøm.
@ -73,8 +73,8 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
### Autentiserte (krever `Authorization: Bearer <JWT>`) ### Autentiserte (krever `Authorization: Bearer <JWT>`)
- `GET /me` — Returnerer autentisert brukers `node_id` og `authentik_sub`. - `GET /me` — Returnerer autentisert brukers `node_id` og `authentik_sub`.
- `POST /intentions/create_node` — Opprett node. Skriv til STDB (instant), - `POST /intentions/create_node` — Opprett node. Skriv til PG,
spawn async PG-skriving, returner `node_id` umiddelbart. returner `node_id` umiddelbart.
- Body (JSON): `{ node_kind?, title?, content?, visibility?, metadata? }` - Body (JSON): `{ node_kind?, title?, content?, visibility?, metadata? }`
- Defaults: `node_kind="content"`, `visibility="hidden"`, andre felter tomme - Defaults: `node_kind="content"`, `visibility="hidden"`, andre felter tomme
- Respons: `{ node_id: "<uuid>" }` - Respons: `{ node_id: "<uuid>" }`
@ -110,12 +110,12 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
### LiveKit / Sanntidslyd (oppgave 11.2) ### LiveKit / Sanntidslyd (oppgave 11.2)
- `POST /intentions/join_communication` — Koble til sanntidslyd i en kommunikasjonsnode. - `POST /intentions/join_communication` — Koble til sanntidslyd i en kommunikasjonsnode.
Validerer deltaker-tilgang (owner/member_of/host_of-edge eller via alias). Validerer deltaker-tilgang (owner/member_of/host_of-edge eller via alias).
Genererer LiveKit access token (JWT), oppretter rom i STDB, oppdaterer node-metadata. Genererer LiveKit access token (JWT), oppdaterer node-metadata.
- Body (JSON): `{ communication_id, role? }` (role: "publisher" | "subscriber", default "publisher") - Body (JSON): `{ communication_id, role? }` (role: "publisher" | "subscriber", default "publisher")
- Respons: `{ livekit_room_name, livekit_token, livekit_url, identity, participants[] }` - Respons: `{ livekit_room_name, livekit_token, livekit_url, identity, participants[] }`
- Frontend bruker `livekit_token` + `livekit_url` til å koble livekit-client SDK. - Frontend bruker `livekit_token` + `livekit_url` til å koble livekit-client SDK.
- `POST /intentions/leave_communication` — Forlat sanntidsrom. - `POST /intentions/leave_communication` — Forlat sanntidsrom.
Fjerner deltaker fra STDB live-rom. Fjerner deltaker fra live-rom.
- Body (JSON): `{ communication_id }` - Body (JSON): `{ communication_id }`
- Respons: `{ status: "left" }` - Respons: `{ status: "left" }`
- `POST /intentions/close_communication` — Steng sanntidsrom (krever owner/admin). - `POST /intentions/close_communication` — Steng sanntidsrom (krever owner/admin).
@ -124,7 +124,7 @@ Tunge spørringer (søk, statistikk, graftraversering) går via maskinrommet →
- Respons: `{ status: "closed" }` - Respons: `{ status: "closed" }`
### Mixer-kanaler (oppgave 22.2) ### Mixer-kanaler (oppgave 22.2)
Erstatter SpacetimeDB-reducers for delt mixer-tilstand i LiveKit-rom. Delt mixer-tilstand i LiveKit-rom.
Skriver til PG `mixer_channels`-tabell; NOTIFY-trigger propagerer til WS. Skriver til PG `mixer_channels`-tabell; NOTIFY-trigger propagerer til WS.
- `POST /intentions/create_mixer_channel` — Opprett mixer-kanal for deltaker i rom. - `POST /intentions/create_mixer_channel` — Opprett mixer-kanal for deltaker i rom.
- Body: `{ room_id, target_user_id }` - Body: `{ room_id, target_user_id }`

View file

@ -3,8 +3,7 @@
**Filsti:** `docs/infra/backup.md` **Filsti:** `docs/infra/backup.md`
Synops sin backup-strategi bygger på én innsikt: **PostgreSQL er den Synops sin backup-strategi bygger på én innsikt: **PostgreSQL er den
eneste autoriteten.** SpacetimeDB er en sanntidscache som gjenoppbygges eneste autoriteten.** Media-filer i CAS er innholdsadresserte og immutable.
fra PG ved behov. Media-filer i CAS er innholdsadresserte og immutable.
## Arkitektur ## Arkitektur
@ -15,8 +14,7 @@ PostgreSQL (autoritativ kilde)
│ └──→ /srv/synops/backup/pg/sidelinja_YYYYMMDD_HHMMSS.dump │ └──→ /srv/synops/backup/pg/sidelinja_YYYYMMDD_HHMMSS.dump
│ └──→ Rotasjon: 30 dager │ └──→ Rotasjon: 30 dager
└──→ SpacetimeDB (sanntidscache) └──→ Sanntid via PG LISTEN/NOTIFY → WebSocket
└──→ Gjenoppbygges fra PG ved krasj (warmup)
``` ```
## 1. PG-dump (daglig) ## 1. PG-dump (daglig)
@ -44,28 +42,10 @@ docker exec sidelinja-postgres-1 pg_restore --list /tmp/test.dump
docker exec sidelinja-postgres-1 rm /tmp/test.dump docker exec sidelinja-postgres-1 rm /tmp/test.dump
``` ```
## 2. STDB-gjenoppbygging ved krasj ## 2. Sanntid
**Modul:** `maskinrommet/src/stdb_monitor.rs` Sanntid leveres via PG LISTEN/NOTIFY + WebSocket i portvokteren.
Ingen separat sanntidstjeneste å gjenoppbygge — PG er eneste datakilde.
SpacetimeDB er en sanntidscache. Hvis den krasjer, tapes ingen data
fordi all skriving går gjennom maskinrommet som skriver til PG først
(asynkront, men alltid). Gjenoppbygging skjer automatisk:
### Ved oppstart
Maskinrommet kjører `warmup::run()` i `main.rs` — laster alle noder,
edges og node_access fra PG til STDB.
### Ved krasj under drift
`stdb_monitor` kjører i bakgrunnen og sjekker STDB hvert 30. sekund:
1. **Oppdager** at STDB ikke svarer (var oppe, nå nede)
2. **Venter** opptil 10 minutter på at containeren restarter
3. **Kjører warmup** (PG → STDB) når STDB svarer igjen
4. **Logger** hele hendelsesforløpet
Prosessen er automatisk og krever ingen manuell inngripen så lenge
Docker restarter containeren (restart-policy: `unless-stopped`).
## 3. Restore fra backup ## 3. Restore fra backup
@ -79,7 +59,7 @@ docker cp /srv/synops/backup/pg/sidelinja_YYYYMMDD.dump sidelinja-postgres-1:/tm
docker exec sidelinja-postgres-1 pg_restore -U sidelinja -d sidelinja --clean /tmp/restore.dump docker exec sidelinja-postgres-1 pg_restore -U sidelinja -d sidelinja --clean /tmp/restore.dump
docker exec sidelinja-postgres-1 rm /tmp/restore.dump docker exec sidelinja-postgres-1 rm /tmp/restore.dump
# Start maskinrommet (warmup laster PG → STDB automatisk) # Start maskinrommet
sudo systemctl start maskinrommet sudo systemctl start maskinrommet
``` ```
@ -88,7 +68,7 @@ Ved total serversvikt (ny VPS):
1. Installer OS og Docker (se `docs/setup/produksjon.md`) 1. Installer OS og Docker (se `docs/setup/produksjon.md`)
2. Start PG-container 2. Start PG-container
3. Restore dump (se over) 3. Restore dump (se over)
4. Start maskinrommet (warmup håndterer STDB) 4. Start maskinrommet
5. Avledede data (segmenter, søkeindeks) regenereres fra kildene 5. Avledede data (segmenter, søkeindeks) regenereres fra kildene
## 4. Overvåking ## 4. Overvåking
@ -98,12 +78,11 @@ Health-dashboardet (`/admin/health`) viser backup-status:
- **stale** — dump-fil er eldre enn 25 timer - **stale** — dump-fil er eldre enn 25 timer
- **missing** — ingen dump-filer funnet - **missing** — ingen dump-filer funnet
Metrikk-endepunktet (`/metrics`) inkluderer STDB-status som del av Metrikk-endepunktet (`/metrics`) inkluderer tjeneste-status som del av
helsesjekken. helsesjekken.
## 5. Hva som IKKE backupes (bevisst) ## 5. Hva som IKKE backupes (bevisst)
- **SpacetimeDB** — sanntidscache, gjenoppbygges fra PG
- **Redis** — cache, regenereres automatisk - **Redis** — cache, regenereres automatisk
- **Caddy-data** — sertifikater regenereres av Let's Encrypt - **Caddy-data** — sertifikater regenereres av Let's Encrypt
- **Whisper-modeller** — re-download fra HuggingFace - **Whisper-modeller** — re-download fra HuggingFace

View file

@ -21,12 +21,12 @@ Bruker sender melding (via frontend)
→ kaller: claude -p "<prompt>" --output-format json → kaller: claude -p "<prompt>" --output-format json
→ oppretter svar-node i PG, logger ai_usage + resource_usage → oppretter svar-node i PG, logger ai_usage + resource_usage
→ returnerer JSON med reply_node_id + response_text → returnerer JSON med reply_node_id + response_text
maskinrommet: skriver til STDB (sanntidsvisning) PG NOTIFY propagerer til frontend via WebSocket
→ frontend viser melding i sanntid via STDB WebSocket → frontend viser melding i sanntid
``` ```
Ansvarsdeling (unix-filosofi): Ansvarsdeling (unix-filosofi):
- **Maskinrommet:** Auth, kill switch, rate limiting, loop-prevensjon, STDB-skriving - **Maskinrommet:** Auth, kill switch, rate limiting, loop-prevensjon
- **synops-respond:** Kontekst-henting, prompt-bygging, claude-kall, PG-skriving - **synops-respond:** Kontekst-henting, prompt-bygging, claude-kall, PG-skriving
Latens: ~3-5 sekunder fra melding til svar. Latens: ~3-5 sekunder fra melding til svar.

View file

@ -53,17 +53,17 @@ CLI-verktøy (`Command::new("synops-X")`) som gjør selve jobben.
- Payload-parsing og validering - Payload-parsing og validering
- Sikkerhetskontroller (kill switch, rate limiting, loop-prevensjon for agent) - Sikkerhetskontroller (kill switch, rate limiting, loop-prevensjon for agent)
- Voice/model-oppslag fra node metadata (orchestrator-logikk) - Voice/model-oppslag fra node metadata (orchestrator-logikk)
- STDB-synk etter CLI fullført (sanntidsvisning) - PG NOTIFY etter CLI fullført (sanntidsvisning via WebSocket)
- Jobbstatus-håndtering (complete/fail/retry) - Jobbstatus-håndtering (complete/fail/retry)
| Jobbtype | CLI-verktøy | Maskinrommet beholder | | Jobbtype | CLI-verktøy | Maskinrommet beholder |
|---|---|---| |---|---|---|
| `whisper_transcribe` | `synops-transcribe` | — | | `whisper_transcribe` | `synops-transcribe` | — |
| `agent_respond` | `synops-respond` | Kill switch, rate limit, loop-prevensjon, STDB-synk | | `agent_respond` | `synops-respond` | Kill switch, rate limit, loop-prevensjon |
| `suggest_edges` | `synops-suggest-edges` | — | | `suggest_edges` | `synops-suggest-edges` | — |
| `summarize_communication` | `synops-summarize` | — | | `summarize_communication` | `synops-summarize` | — |
| `tts_generate` | `synops-tts` | Voice-oppslag fra node metadata, STDB-synk | | `tts_generate` | `synops-tts` | Voice-oppslag fra node metadata |
| `audio_process` | `synops-audio` | STDB-synk | | `audio_process` | `synops-audio` | |
| `render_article` | `synops-render` | — | | `render_article` | `synops-render` | — |
| `render_index` | `synops-render` | — | | `render_index` | `synops-render` | — |
| `ai_process` | *(inline — mangler CLI)* | Alt (planlagt migrasjon) | | `ai_process` | *(inline — mangler CLI)* | Alt (planlagt migrasjon) |
@ -223,7 +223,7 @@ tabell med alle felter, retry/avbryt-knapper. Poller hvert 5. sekund.
**Migrasjon:** `014_resource_governor.sql``job_priority_rules` + `disk_status_log` **Migrasjon:** `014_resource_governor.sql``job_priority_rules` + `disk_status_log`
- Valgfritt: SpacetimeDB-event ved statusendring slik at UI kan vise fremdrift i sanntid (f.eks. "Transkriberer... 2/3 forsøk") - Jobbstatus-endringer propageres til frontend via PG NOTIFY → WebSocket (f.eks. "Transkriberer... 2/3 forsøk")
## 8. Instruks for Claude Code ## 8. Instruks for Claude Code
- Én binær: `sidelinja-worker`. Én Rust-crate med polling-loop + handler-dispatch - Én binær: `sidelinja-worker`. Én Rust-crate med polling-loop + handler-dispatch

View file

@ -33,7 +33,6 @@ kan den prosessere ubesvarte meldinger.
Portvokteren eksponerer `/health` som sjekker: Portvokteren eksponerer `/health` som sjekker:
- PG-tilkobling - PG-tilkobling
- STDB-tilkobling
- LiteLLM-tilgjengelighet - LiteLLM-tilgjengelighet
- Disk-status - Disk-status
@ -107,7 +106,7 @@ En `reader` kan spørre `@bot` om informasjon, men ikke trigge
| Alle LLM-er nede | synops-respond exit != 0 | Statisk "utilgjengelig" + work_item | Vet at meldingen er mottatt | | Alle LLM-er nede | synops-respond exit != 0 | Statisk "utilgjengelig" + work_item | Vet at meldingen er mottatt |
| Portvokteren nede | Systemd healthcheck → restart | CLI fungerer for Claude Code | Web-brukere venter, terminal funker | | Portvokteren nede | Systemd healthcheck → restart | CLI fungerer for Claude Code | Web-brukere venter, terminal funker |
| PG nede | Connection refused | Alt stopper | Eneste reelle SPOF | | PG nede | Connection refused | Alt stopper | Eneste reelle SPOF |
| STDB nede | Reconnect-loop | PG-fallback for lesing, skriving bufres | Sanntid borte, data trygt | | WebSocket nede | Portvokteren restarter | Frontend rekobler automatisk | Sanntid midlertidig borte, data trygt |
## PG som eneste SPOF ## PG som eneste SPOF
@ -118,7 +117,6 @@ gjenoppbygging fra backup er veldokumentert
Mitigering: Mitigering:
- Automatisk PG-dump (se oppgave 12.2) - Automatisk PG-dump (se oppgave 12.2)
- STDB kan gjenoppbygges fra PG
- `synops-snapshot` sikrer at docs-fallback finnes i repo - `synops-snapshot` sikrer at docs-fallback finnes i repo
- CAS-filer er uavhengig av PG (filsystem) - CAS-filer er uavhengig av PG (filsystem)

View file

@ -1,5 +1,10 @@
# Synkronisering: SpacetimeDB ↔ PostgreSQL # Synkronisering: SpacetimeDB ↔ PostgreSQL
> **UTGÅTT (mars 2026).** SpacetimeDB er fjernet fra prosjektet.
> Sanntid leveres nå via PG LISTEN/NOTIFY + WebSocket i portvokteren.
> Se `docs/infra/api_grensesnitt.md` for gjeldende arkitektur.
> Dokumentet beholdes som historisk referanse.
## Konsept ## Konsept
SpacetimeDB holder hele grafen (alle noder og edges) i minne. SpacetimeDB holder hele grafen (alle noder og edges) i minne.

View file

@ -78,7 +78,7 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean
|---|---|---| |---|---|---|
| `editor` | TipTap med presets (longform, note, chat, code) | Validering av dokumentstruktur | | `editor` | TipTap med presets (longform, note, chat, code) | Validering av dokumentstruktur |
| `versioning` | Revisjonshistorikk, diff, rollback-knapp | Snapshot ved signifikante endringer | | `versioning` | Revisjonshistorikk, diff, rollback-knapp | Snapshot ved signifikante endringer |
| `collaboration` | Samtidig redigering, markører, inline-kommentarer | OT/CRDT via STDB | | `collaboration` | Samtidig redigering, markører, inline-kommentarer | OT/CRDT via WebSocket |
| `translation` | Språkvelger, side-ved-side-visning | AI-oversettelse via jobbkø | | `translation` | Språkvelger, side-ved-side-visning | AI-oversettelse via jobbkø |
| `templates` | Mal-velger ved ny node | Mal-noder i samlingen | | `templates` | Mal-velger ved ny node | Mal-noder i samlingen |
@ -111,7 +111,7 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean
| Trait | Frontend | Backend | | Trait | Frontend | Backend |
|---|---|---| |---|---|---|
| `chat` | Sanntidsmeldinger, tråder, reaksjoner | STDB-synk, TTL-håndtering | | `chat` | Sanntidsmeldinger, tråder, reaksjoner | WebSocket-synk, TTL-håndtering |
| `forum` | Trådet diskusjon, sortering (nyeste/populære/ubesvarte) | Tråd-indeksering | | `forum` | Trådet diskusjon, sortering (nyeste/populære/ubesvarte) | Tråd-indeksering |
| `comments` | Kommentarfelt under publisert innhold | Moderasjonskø, evt. anonym input | | `comments` | Kommentarfelt under publisert innhold | Moderasjonskø, evt. anonym input |
| `guest_input` | Gjeste-lenke-generering, svar-oversikt | Token-generering, CAS-upload, rate limiting | | `guest_input` | Gjeste-lenke-generering, svar-oversikt | Token-generering, CAS-upload, rate limiting |

View file

@ -22,7 +22,7 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe
| [Podcast Time Machine](podcast_time_machine.md) | Lav | Høy | Segmenter, Caddy byte-range, Live AI | | [Podcast Time Machine](podcast_time_machine.md) | Lav | Høy | Segmenter, Caddy byte-range, Live AI |
| [Meme Generator](meme_generator.md) | Lav | Høy | Whiteboard, transkripsjon, AI Gateway | | [Meme Generator](meme_generator.md) | Lav | Høy | Whiteboard, transkripsjon, AI Gateway |
| [Valgomat Roast](valgomat_roast.md) | Lav | Middels | Valgomat, kunnskapsgraf | | [Valgomat Roast](valgomat_roast.md) | Lav | Middels | Valgomat, kunnskapsgraf |
| [Live Audience Q&A](live_audience_qa.md) | Middels | Høy | Valgomat, LiveKit, SpacetimeDB | | [Live Audience Q&A](live_audience_qa.md) | Middels | Høy | Valgomat, LiveKit, WebSocket |
| [Guest Prep Simulator](guest_prep_simulator.md) | Middels | Høy | Kunnskapsgraf, AI Gateway | | [Guest Prep Simulator](guest_prep_simulator.md) | Middels | Høy | Kunnskapsgraf, AI Gateway |
| [Debate Club](debate_club.md) | Middels | Middels | Kunnskapsgraf, AI Gateway, jobbkø | | [Debate Club](debate_club.md) | Middels | Middels | Kunnskapsgraf, AI Gateway, jobbkø |
| [Ghost Host TTS](ghost_host_tts.md) | Stor | Høy | LiveKit, AI Gateway, ny TTS-infra | | [Ghost Host TTS](ghost_host_tts.md) | Stor | Høy | LiveKit, AI Gateway, ny TTS-infra |
@ -48,7 +48,7 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe
| [Flow Meter](flow_meter.md) | Lav | Middels | Storyboard | | [Flow Meter](flow_meter.md) | Lav | Middels | Storyboard |
| [Emotion Tags](emotion_tags.md) | Lav | Middels | Meldingsboks, kanban, storyboard | | [Emotion Tags](emotion_tags.md) | Lav | Middels | Meldingsboks, kanban, storyboard |
| **Samarbeid** | | | | | **Samarbeid** | | | |
| [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | SpacetimeDB, Svelte | | [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | WebSocket, Svelte |
| [Card Heat Map](card_heat_map.md) | Lav | Middels | Meldingsboks, kanban/storyboard | | [Card Heat Map](card_heat_map.md) | Lav | Middels | Meldingsboks, kanban/storyboard |
**Forfremmet til feature:** [Meldingsboks](../features/meldingsboks.md) — universell diskusjonsprimitiv. [Artikkel-publisering](artikkel_publisering.md) → Fase 14 / `docs/concepts/publisering.md`. [Tekst-primitiv](tekst_primitiv.md) — realisert i nodearkitekturen. **Forfremmet til feature:** [Meldingsboks](../features/meldingsboks.md) — universell diskusjonsprimitiv. [Artikkel-publisering](artikkel_publisering.md) → Fase 14 / `docs/concepts/publisering.md`. [Tekst-primitiv](tekst_primitiv.md) — realisert i nodearkitekturen.

View file

@ -1,5 +1,12 @@
# Komponerbare sider (Dashboard-komposisjon) # Komponerbare sider (Dashboard-komposisjon)
> **Superseded:** Konseptet er videreført og utvidet i retningen
> "Arbeidsflaten" (`docs/retninger/arbeidsflaten.md`). Den spatial
> workspace-modellen med frie verktøy-paneler og drag-and-drop erstatter
> grid-baserte dashboard-komposisjoner. Dokumentet er bevart som
> historisk referanse — mye av tenkningen rundt blokker, resize og
> maximize er videreført i arbeidsflate-retningen.
## Idé ## Idé
Brukere ser ferdige sider (Redaksjonen, Studioet, etc.), men admin kan komponere egne sider fra tilgjengelige byggeklosser — chat, kanban, statistikk, graf-visning, whiteboard, osv. Brukere ser ferdige sider (Redaksjonen, Studioet, etc.), men admin kan komponere egne sider fra tilgjengelige byggeklosser — chat, kanban, statistikk, graf-visning, whiteboard, osv.

View file

@ -26,7 +26,7 @@ andre dokumenter. En retning kan også forkastes eller parkeres.
| [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. | | [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. |
| [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. | | [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. |
| [Noder er sentrum](bruker_ikke_workspace.md) | **Besluttet** | Alt er noder (brukere, team, innhold). Edges definerer relasjoner og tilgang. Materialisert tilgangsmatrise for RLS. | | [Noder er sentrum](bruker_ikke_workspace.md) | **Besluttet** | Alt er noder (brukere, team, innhold). Edges definerer relasjoner og tilgang. Materialisert tilgangsmatrise for RLS. |
| [Datalaget](datalaget.md) | **Revidert** | PG er eneste datakilde. Sanntid via LISTEN/NOTIFY + WebSocket. SpacetimeDB fases ut. CAS for binærdata, AGE ved behov. | | [Datalaget](datalaget.md) | **Revidert** | PG er eneste datakilde. Sanntid via LISTEN/NOTIFY + WebSocket. CAS for binærdata, AGE ved behov. |
| [Arbeidsflaten](arbeidsflaten.md) | **Besluttet** | Spatial canvas med verktøy-paneler. Drag-and-drop skaper nye noder med edges. | | [Arbeidsflaten](arbeidsflaten.md) | **Besluttet** | Spatial canvas med verktøy-paneler. Drag-and-drop skaper nye noder med edges. |
| [Unix-filosofi](unix_filosofi.md) | **Besluttet** | Maskinrommet orkestrerer, CLI-verktøy gjør jobben. Claude deler verktøykasse. | | [Unix-filosofi](unix_filosofi.md) | **Besluttet** | Maskinrommet orkestrerer, CLI-verktøy gjør jobben. Claude deler verktøykasse. |

View file

@ -152,7 +152,7 @@ spesifikk samling.
- **Navigasjon:** Tilgjengelig via "Min flate"-knapp på mottak, og i - **Navigasjon:** Tilgjengelig via "Min flate"-knapp på mottak, og i
kontekst-velger-dropdown på samlingssider kontekst-velger-dropdown på samlingssider
- **Provisjonering:** Backend oppretter workspace-node + owner-edge ved - **Provisjonering:** Backend oppretter workspace-node + owner-edge ved
første forespørsel. STDB for instant synk, async PG for persistens. første forespørsel. PG-skriving med NOTIFY for sanntidsoppdatering.
## `source_material`-edge ## `source_material`-edge
@ -185,8 +185,9 @@ Gir artikler sporbar lineage: du kan alltid se *hvor* materialet kom fra.
- **[Noder er sentrum](bruker_ikke_workspace.md):** Verktøy er visninger - **[Noder er sentrum](bruker_ikke_workspace.md):** Verktøy er visninger
av grafen. Arbeidsflaten er brukerens personlige arrangement av disse av grafen. Arbeidsflaten er brukerens personlige arrangement av disse
visningene. visningene.
- **[Datalaget](datalaget.md):** SpacetimeDB driver sanntidsoppdatering - **[Datalaget](datalaget.md):** PG LISTEN/NOTIFY + WebSocket driver
mellom paneler. Drag-and-drop oppretter noder/edges som synkes instant. sanntidsoppdatering mellom paneler. Drag-and-drop oppretter noder/edges
som propageres via sanntidsstrømmen.
## Hva ville vært annerledes ## Hva ville vært annerledes

View file

@ -1,11 +1,10 @@
# Datalaget # Datalaget
**Status: Besluttet. Revidert mars 2026 — SpacetimeDB fases ut.** **Status: Besluttet. Revidert mars 2026 — SpacetimeDB fjernet.**
> PostgreSQL er eneste datakilde. Sanntid via PG `LISTEN/NOTIFY` > PostgreSQL er eneste datakilde. Sanntid via PG `LISTEN/NOTIFY`
> og WebSocket i portvokteren. CAS lagrer binærdata. > og WebSocket i portvokteren. CAS lagrer binærdata.
> Apache AGE legges til ved behov for Cypher-traverseringer. > Apache AGE legges til ved behov for Cypher-traverseringer.
> SpacetimeDB fases ut — se migrasjonsplan.
## Lagmodell ## Lagmodell
@ -105,61 +104,25 @@ Portvokteren holder:
- Map av node_id → synlige noder (fra node_access) - Map av node_id → synlige noder (fra node_access)
- Ved NOTIFY: filtrer på tilgang, push til relevante klienter - Ved NOTIFY: filtrer på tilgang, push til relevante klienter
Enklere enn STDB-synk: én retning (PG → klient), ingen Én retning (PG → klient), ingen reducer-logikk, ingen
reducer-logikk, ingen konsistensproblemer. konsistensproblemer.
## Hvorfor SpacetimeDB fases ut ## Historikk: SpacetimeDB (fjernet mars 2026)
SpacetimeDB var et godt eksperiment. Det løste sanntid elegant SpacetimeDB ble brukt som sanntidslag i v1/prototype. Det løste
i prototype-fasen. Men for produksjon på én server: sanntid elegant, men for produksjon på én server skapte det
unødvendig kompleksitet:
- **Synk-kompleksitet.** PG ↔ STDB synk er en egen feilkategori. - **Synk-kompleksitet.** PG ↔ STDB synk var en egen feilkategori.
Erfaringsdocs (adapter_moenster.md, spacetimedb_integrasjon.md) - **Dobbelt vedlikehold.** STDB-modul med reducers måtte holdes i
dokumenterer smerten. synk med PG-skjema.
- **Dobbelt vedlikehold.** STDB-modul med reducers må holdes i - **Ekstra SPOF.** Enda en tjeneste å overvåke og restarte.
synk med PG-skjema. Endring i nodes-tabellen → to steder.
- **Ekstra SPOF.** Enda en tjeneste å overvåke, restarte, debugge.
- **Unødvendig for skalaen.** PG LISTEN/NOTIFY + WebSocket gir - **Unødvendig for skalaen.** PG LISTEN/NOTIFY + WebSocket gir
~5ms latency. STDB ga ~0.01ms. Forskjellen er umerkelig for ~5ms latency — umerkelig forskjell for brukere.
brukere.
- **CLI-verktøy forenkles.** Bare PG-tilkobling, ingen STDB-klient.
## Migrasjonsplan: STDB → PG LISTEN/NOTIFY SpacetimeDB ble faset ut i fire steg: WebSocket-lag, frontend-
migrering, fjern skrivestien, fjern alt. Se erfaringsdocs for
### Fase M1: WebSocket-lag i portvokteren ✅ lærdommer: `docs/erfaringer/spacetimedb_integrasjon.md`.
Implementert LISTEN/NOTIFY-lytter og WebSocket-endepunkt i
portvokteren. PG-triggers for nodes, edges og access.
Frontend koblet til begge (STDB + nytt WS) i parallell.
### Fase M2: Frontend-migrering ✅
Frontend bruker nå kun portvokterens WebSocket. SpacetimeDB-klient
fjernet. Reactive stores oppdateres direkte fra WS-meldinger.
Berikede events: portvokteren henter full raddata fra PG etter
NOTIFY (ikke bare ID) slik at stores kan oppdateres uten ekstra
API-kall. Mixer-kanaler migrert fra STDB til PG-tabell med
tilhørende NOTIFY-trigger og HTTP API-endepunkter.
### Fase M3: Fjern skrivestien til STDB ✅
Portvokteren skriver kun til PG. STDB-skrivestien er fjernet.
Alle intensjoner (create/update/delete node/edge) skriver
synkront til PG. NOTIFY-triggere er eneste push-mekanisme.
Warmup (PG→STDB) og STDB-monitor er fjernet. StdbClient er
fjernet fra AppState. Job-handlere (agent, audio, tts, ai_process)
synker ikke lenger til STDB — PG NOTIFY dekker sanntid.
### Fase M4: Fjern STDB
- Stopp SpacetimeDB Docker-container
- Fjern STDB-modul (spacetimedb/)
- Fjern STDB-klient fra portvokteren
- Fjern STDB-avhengigheter fra frontend
- Fjern synkroniserings-kode
- Oppdater docs (synkronisering.md → arkiver)
- Oppdater CLAUDE.md
### Fase M5: Opprydding
- Slett erfaringsdocs som kun gjelder STDB
- Oppdater alle docs-referanser til STDB
- Fjern Docker-konfig for SpacetimeDB
## Forhold til andre retninger ## Forhold til andre retninger

View file

@ -32,7 +32,7 @@ Lever resultat i riktig modalitet til riktig mottaker:
- Lyd (TTS-opplesning, lydstream) - Lyd (TTS-opplesning, lydstream)
- Video/bilde (stream, thumbnail) - Video/bilde (stream, thumbnail)
- Strukturert data (noder, edges tilbake i grafen) - Strukturert data (noder, edges tilbake i grafen)
- Push (SpacetimeDB-reducer) - Push (WebSocket via PG LISTEN/NOTIFY)
## Maskinrommet eier all skriving ## Maskinrommet eier all skriving
@ -41,9 +41,9 @@ Frontend sender intensjoner. Maskinrommet utfører.
``` ```
Frontend: "legg til Trond i møtet" Frontend: "legg til Trond i møtet"
→ Maskinrommet validerer → Maskinrommet validerer
→ Skriver edge til SpacetimeDB (instant) → Skriver edge til PG
→ Oppdaterer tilgangsmatrise → Oppdaterer tilgangsmatrise
→ Persisterer til PG (asynk) → PG NOTIFY → WebSocket → frontend oppdateres i sanntid
→ Reagerer på konsekvensene (koble inn LiveKit, starte transkripsjon) → Reagerer på konsekvensene (koble inn LiveKit, starte transkripsjon)
``` ```
@ -51,8 +51,7 @@ Alt i én operasjon. Maskinrommet er ikke reaktivt i en pub/sub-
forstand — det orkestrerer hele sekvensen. Enklere å forstå, forstand — det orkestrerer hele sekvensen. Enklere å forstå,
enklere å debugge. enklere å debugge.
Skrivestien: validering → SpacetimeDB (instant) → PG (asynk). Skrivestien: validering → PG → NOTIFY → WebSocket (sanntid).
Se [synkronisering](../infra/synkronisering.md).
## Edge-drevet ressursorkestrering ## Edge-drevet ressursorkestrering
@ -185,13 +184,13 @@ Compute-separasjon er en konfigurasjon, ikke en arkitekturendring.
## Evolusjon: Maskinrommet → Portvokteren ## Evolusjon: Maskinrommet → Portvokteren
Maskinrommet ble bygget som en monolitt — auth, validering, Maskinrommet ble bygget som en monolitt — auth, validering,
prosessering, jobbkø, STDB-synk i én binær. Med unix-filosofi- prosessering, jobbkø i én binær. Med unix-filosofi-
retningen (se `docs/retninger/unix_filosofi.md`) flyttes all retningen (se `docs/retninger/unix_filosofi.md`) flyttes all
prosessering til CLI-verktøy. Det som blir igjen er: prosessering til CLI-verktøy. Det som blir igjen er:
1. **Auth** — JWT-validering, "hvem er denne requesten?" 1. **Auth** — JWT-validering, "hvem er denne requesten?"
2. **HTTP-ruting** — frontend → riktig CLI-verktøy 2. **HTTP-ruting** — frontend → riktig CLI-verktøy
3. **STDB-synk** — push PG-endringer til SpacetimeDB 3. **Sanntid** — PG LISTEN/NOTIFY → WebSocket til frontend
4. **Jobbkø-dispatch** — poll PG, spawn CLI-verktøy 4. **Jobbkø-dispatch** — poll PG, spawn CLI-verktøy
Dette er en **portvokter**, ikke et maskinrom. Når uttynningen er Dette er en **portvokter**, ikke et maskinrom. Når uttynningen er
@ -219,5 +218,5 @@ Maskinrommet er infrastrukturen *under* de tre primitivene i
- [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet - [Noder er sentrum](bruker_ikke_workspace.md) — maskinrommet
eier tilgangsmatrise-oppdatering eier tilgangsmatrise-oppdatering
- [Datalaget](datalaget.md) — maskinrommet skriver SpacetimeDB - [Datalaget](datalaget.md) — maskinrommet skriver PG,
først, PG asynk sanntid via LISTEN/NOTIFY + WebSocket

View file

@ -6,15 +6,14 @@
## Observasjoner ## Observasjoner
Sidelinja har vokst organisk. Vi har bygget chat, kanban, kalender, notater, Sidelinja har vokst organisk. Vi har bygget chat, kanban, kalender, notater,
kunnskapsgraf — hver som sin feature, hver med sin spec. SpacetimeDB ble lagt til kunnskapsgraf — hver som sin feature, hver med sin spec. Sanntid ble lagt til,
for sanntid, men arkitekturen er fortsatt "PostgreSQL-app med sanntidskrydder." men arkitekturen var fortsatt "PostgreSQL-app med sanntidskrydder."
Resultatet: Resultatet:
- **Forum-følelsen.** Ting er organisert i tråder, kort, lister. Brukeren - **Forum-følelsen.** Ting er organisert i tråder, kort, lister. Brukeren
navigerer mellom sider. Det føles som et tradisjonelt verktøy med litt polish. navigerer mellom sider. Det føles som et tradisjonelt verktøy med litt polish.
- **Databasespenning.** PG og SpacetimeDB har et komplisert eierforhold. - **Arkitekturspenning.** Spørsmålet om hva *primæropplevelsen* er
SpacetimeDB-loven løser grensesnittet, men ikke det underliggende spørsmålet: — sanntid eller tradisjonell webapp — forblir åpent.
hva *er* primæropplevelsen?
- **Feature-fragmentering.** Chat, kanban, whiteboard, notater — hver lever i sin - **Feature-fragmentering.** Chat, kanban, whiteboard, notater — hver lever i sin
boks. "Universell overføring" og "meldingsboks" prøver å lime dem sammen, men boks. "Universell overføring" og "meldingsboks" prøver å lime dem sammen, men
utgangspunktet er fortsatt separate primitiver. utgangspunktet er fortsatt separate primitiver.
@ -34,34 +33,22 @@ Forskjellen er subtil men fundamental:
## Hva ville vært annerledes? ## Hva ville vært annerledes?
### SpacetimeDB som verden, PG som arkiv ### Sanntidslaget og arkivet
SpacetimeDB er ikke en "sanntidskache foran PG" — det er verdenen brukerne PG er eneste datakilde. Sanntid leveres via PG LISTEN/NOTIFY + WebSocket
lever i. PG er arkivet som husker hva som har skjedd. i portvokteren. Arkivet og sanntidslaget er ikke to separate systemer —
PG er begge deler.
Rollene blir klare og adskilte: Ikke alt trenger sanntid. En kunnskapsgraf-utforsker, et søk i gamle
- **SpacetimeDB** = sanntidslaget. Aktivt samarbeid, live interaksjon, episoder, en statistikkside, en offentlig publisert artikkel — disse
ting som skjer *nå*. bruker tradisjonelle API-kall mot PG. Sanntidsstrømmen er for det som
- **PostgreSQL** = arkivet. Alt som noensinne har skjedd. Søk, historikk, er levende: chat, whiteboard, live redigering.
statistikk, revisjon.
Men viktig: sanntidslaget er *bare* et sanntidslag. Ikke alt trenger To overflater, én datakilde:
sanntid. En kunnskapsgraf-utforsker, et søk i gamle episoder, en - **Sanntidsopplevelsen** (via WebSocket) — for alt som er levende,
statistikkside, en offentlig publisert artikkel — disse snakker rett med aktivt, samarbeidende.
PG-arkivet som tradisjonelle nettsider. De trenger ikke gå gjennom - **Tradisjonelt lag** (API-kall mot PG) — for alt som er retrospektivt,
SpacetimeDB. Begge lagene leser og skriver til det samme arkivet.
Dermed har vi to parallelle overflater:
- **Sanntidsopplevelsen** (via SpacetimeDB) — for alt som er levende,
aktivt, samarbeidende. Chat, whiteboard, live redigering.
- **Tradisjonelt lag** (rett mot PG) — for alt som er retrospektivt,
utforskende, statisk. Arkiv, søk, publisering, statistikk. utforskende, statisk. Arkiv, søk, publisering, statistikk.
Dataflyt mellom dem: ting som oppstår i sanntidslaget synkes til PG.
Ting i PG kan løftes inn i sanntidslaget når de blir aktive igjen.
Men det er ingen eierskapskonflikt — de to lagene har fundamentalt
forskjellige roller. Det er ikke to konkurrerende sannheter, det er
*nåtid* og *arkiv*, med to overflater som passer til hver sin rolle.
### Rommet som primitiv, ikke siden ### Rommet som primitiv, ikke siden
I dag navigerer brukeren mellom `/chat`, `/kanban`, `/kalender`. I "rom"-modellen I dag navigerer brukeren mellom `/chat`, `/kanban`, `/kalender`. I "rom"-modellen
er brukeren alltid *et sted*, og funksjonalitet er lag som kan slås av og på: er brukeren alltid *et sted*, og funksjonalitet er lag som kan slås av og på:
@ -121,22 +108,17 @@ systemet tar vare på det riktig sted basert på kontekst og synlighet.
Input-metode (tekst, voice, tegning) og synlighet (privat, delt, publisert) Input-metode (tekst, voice, tegning) og synlighet (privat, delt, publisert)
er ortogonale egenskaper. Ingen av dem bør diktere *hva* innholdet blir. er ortogonale egenskaper. Ingen av dem bør diktere *hva* innholdet blir.
### SpacetimeDB som naturlig motor ### PG som eneste kilde, WebSocket som sanntidslag
SpacetimeDB tenker "dette eksisterer i verden nå" — ikke "lagre dette i PG briljerer som både arkiv og sanntidskilde: relasjonelle spørringer
riktig tabell." Å trylle frem et whiteboard er naturlig i en verden-modell: over historikk, fulltekstsøk, pgvector for semantisk søk, aggregeringer
det er bare et nytt objekt med en tilstand. I PG-modellen må du opprette og statistikk. LISTEN/NOTIFY + WebSocket gir sanntidsoppdatering uten
rader, definere relasjoner, sette opp persistens. SpacetimeDB låner seg et ekstra system. Å trylle frem et whiteboard er bare å opprette en node
til flytende, formløs interaksjon på en måte PG ikke gjør. — WebSocket-strømmen sørger for at alle ser det umiddelbart.
PG briljerer i rollen som arkiv: relasjonelle spørringer over historikk,
fulltekstsøk, pgvector for semantisk søk, aggregeringer og statistikk.
Når du spør "hva snakket vi om i mars?" er det PG som svarer. Når du
spør "hva skjer nå?" er det SpacetimeDB.
## Spenninger og åpne spørsmål ## Spenninger og åpne spørsmål
- **Ytelse.** En alltid-på sanntidsopplevelse krever mer av både klient og - **Ytelse.** En alltid-på sanntidsopplevelse krever mer av både klient og
server enn tradisjonelle sideinnlastinger. Er SpacetimeDB klar for dette? server enn tradisjonelle sideinnlastinger.
- **Kompleksitet.** "Alt er et rom" høres elegant ut, men kan bli kaotisk. - **Kompleksitet.** "Alt er et rom" høres elegant ut, men kan bli kaotisk.
Hvordan unngår vi at det blir uoversiktlig? Hvordan unngår vi at det blir uoversiktlig?
- **Discovery vs fokus.** "Alt kan bli hva som helst" er kraftig, men kan - **Discovery vs fokus.** "Alt kan bli hva som helst" er kraftig, men kan
@ -146,9 +128,9 @@ spør "hva skjer nå?" er det SpacetimeDB.
- **~~Gradvis overgang.~~** *(Løst — se "Innebygd utviklingsstrategi" nedenfor.)* - **~~Gradvis overgang.~~** *(Løst — se "Innebygd utviklingsstrategi" nedenfor.)*
- **Solo-bruk.** Mye av verdien i "rom" kommer fra å være der sammen. Hvordan - **Solo-bruk.** Mye av verdien i "rom" kommer fra å være der sammen. Hvordan
føles det for én person som jobber alene? føles det for én person som jobber alene?
- **Er det vi allerede har?** Meldingsboks-konseptet, universell overføring og - **Er det vi allerede har?** Meldingsboks-konseptet og universell overføring
SpacetimeDB-loven peker allerede i denne retningen. Kanskje dette ikke er en peker allerede i denne retningen. Kanskje dette ikke er en ny retning,
ny retning, men en artikulering av det vi ubevisst har bygget mot? men en artikulering av det vi ubevisst har bygget mot?
- **Inspirasjon.** Spillverdener (MMO-lobbyer, shared spaces), Figma (alle i - **Inspirasjon.** Spillverdener (MMO-lobbyer, shared spaces), Figma (alle i
samme canvas), tldraw, Gather.town. Hva kan vi lære fra disse? samme canvas), tldraw, Gather.town. Hva kan vi lære fra disse?
@ -157,31 +139,24 @@ spør "hva skjer nå?" er det SpacetimeDB.
To-lags-modellen gir en viktig implikasjon for utviklingen: **innebygd fallback To-lags-modellen gir en viktig implikasjon for utviklingen: **innebygd fallback
og to veier inn til alt.** og to veier inn til alt.**
Ny funksjonalitet kan alltid starte i det tradisjonelle laget — rett mot PG, Ny funksjonalitet kan alltid starte som tradisjonell request/response mot PG.
vanlig request/response, kjent terreng. Den fungerer med en gang. Når det Sanntidsoppdatering kommer gratis via LISTEN/NOTIFY + WebSocket — ingen
senere gir verdi (samarbeid, sanntid, live interaksjon) kan den løftes inn separat "løfting" til et sanntidslag nødvendig.
i sanntidslaget. Men den trenger ikke det for å være nyttig.
Dette betyr: Dette betyr:
- **Ingenting vi har bygget er bortkastet.** Eksisterende PG-baserte features - **Ingenting vi har bygget er bortkastet.** Eksisterende PG-baserte features
er ikke "gammel arkitektur" — de er det tradisjonelle laget, og det er et er fullverdige og har sanntid via WebSocket.
fullverdig lag. - **Ingen stor omskriving.** Retningen er en utviklingsstrategi: bygg mot PG,
- **Ingen stor omskriving.** Retningen er ikke en migrasjon med en frist. Den sanntid følger automatisk.
er en utviklingsstrategi: bygg tradisjonelt, løft til sanntid ved behov. - **Risikoen er lav.** PG er stabil og velprøvd.
- **Risikoen er lav.** Hvis SpacetimeDB skuffer, har vi fortsatt et komplett - **Primitivene er nøkkelen.** Så lenge noder, edges og kunnskapsgrafen er
tradisjonelt system. Hvis det leverer, får ting gradvis en rikere opplevelse. fleksible nok, holder arkitekturen uavhengig av kontekst.
- **Primitivene er nøkkelen.** Så lenge meldingsboksen og kunnskapsgrafen er
fleksible nok, kan begge lag bruke dem. Arkitekturen holder uavhengig av
hvilket lag en feature lever i.
## Kritisk vurdering ## Kritisk vurdering
### SpacetimeDB er en ung teknologi ### Sanntidslaget er PG-basert
Å lene seg tungt på SpacetimeDB er et veddemål. Hvis prosjektet stagnerer, SpacetimeDB ble fjernet (mars 2026). Sanntid leveres nå via PG
endrer API, eller har skaleringstak vi ikke ser ennå — er vi eksponert. LISTEN/NOTIFY + WebSocket. Én datakilde, ingen synkroniseringskompleksitet.
*Mildnet* av at det tradisjonelle laget mot PG alltid finnes som fallback:
SpacetimeDB er ikke alt-eller-ingenting, men et lag for det som trenger
sanntid. Resten lever trygt mot PG uansett.
### "Formløs input" er vanskelig i praksis ### "Formløs input" er vanskelig i praksis
Det høres elegant ut, men noen må bestemme hva noe *blir*. AI? Brukeren Det høres elegant ut, men noen må bestemme hva noe *blir*. AI? Brukeren
@ -195,20 +170,17 @@ hverdagen.
En samtale fra i går som fortsatt er relevant — er den "nå" eller "arkiv"? En samtale fra i går som fortsatt er relevant — er den "nå" eller "arkiv"?
Et kanban-kort som har stått stille i to uker? Vi trenger regler for når ting Et kanban-kort som har stått stille i to uker? Vi trenger regler for når ting
flyttes mellom lagene, og de reglene kan bli like komplekse som flyttes mellom lagene, og de reglene kan bli like komplekse som
SpacetimeDB-loven de erstatter. "Tid og arkiv" er renere *i prinsippet*, reglene de erstatter. "Tid og arkiv" er renere *i prinsippet*,
men i praksis er "aktiv" et spektrum, ikke en binær tilstand. men i praksis er "aktiv" et spektrum, ikke en binær tilstand.
### Solo-bruk er underadressert ### Solo-bruk er underadressert
Vegard er primærbruker. "Rom"-konseptet henter mye av sin kraft fra Vegard er primærbruker. "Rom"-konseptet henter mye av sin kraft fra
tilstedeværelse og samarbeid. For én person som produserer podcast er det tilstedeværelse og samarbeid. For én person som produserer podcast er det
en reell risiko at sanntidslaget er overhead sammenlignet med et godt en reell risiko at sanntidslaget er overhead sammenlignet med et godt
organisert tradisjonelt verktøy. *Mildnet* av to-lags-modellen: solo-bruk organisert tradisjonelt verktøy. *Mildnet* av at PG-basert sanntid har null overhead — WebSocket
kan lene seg tyngre på det tradisjonelle PG-laget, og sanntidslaget leverer oppdateringer uten ekstra lag å vedlikeholde.
aktiveres når det faktisk gir verdi (live innspilling, samarbeid).
### Omfanget ### Omfanget
*Betydelig mildnet* av utviklingsstrategien ovenfor. Retningen krever ikke *Betydelig mildnet* av at sanntid nå er innebygd i arkitekturen via
en omskriving — den er kompatibel med inkrementell utvikling. Den PG LISTEN/NOTIFY + WebSocket. Ingen vurdering "bør dette være sanntid?"
gjenværende risikoen er mer subtil: at vi bruker mental energi på å nødvendig — alt som skrives til PG propageres automatisk.
vurdere "bør dette være sanntid?" for hver feature i stedet for å bare
bygge. Pragmatisk default bør være: bygg tradisjonelt, løft senere.

View file

@ -44,17 +44,17 @@ er form-basert og eksplisitt. Brukeren "administrerer innhold" mer enn
de "jobber sammen i et miljø." de "jobber sammen i et miljø."
### Sanntid som tillegg ### Sanntid som tillegg
SpacetimeDB er lagt til for å gi sanntidsoppdatering, men arkitekturen Sanntid (opprinnelig via SpacetimeDB, nå PG LISTEN/NOTIFY + WebSocket)
er PostgreSQL-først. Sanntid er noe som *skjer med* tradisjonelle er lagt til, men arkitekturen er PostgreSQL-først. Sanntid er noe som
operasjoner, ikke noe som er *grunnlaget* for opplevelsen. *skjer med* tradisjonelle operasjoner, ikke noe som er *grunnlaget*
for opplevelsen.
## Spenninger ## Spenninger
### To sannhetskilder ### To sannhetskilder (historisk, nå løst)
PG og SpacetimeDB har et komplisert forhold. SpacetimeDB-loven definerer PG og SpacetimeDB hadde et komplisert forhold. Denne spenningen er
klare regler for hvem som eier hva, men selve eksistensen av loven vitner løst ved fjerning av SpacetimeDB (mars 2026) — PG er nå eneste
om en arkitektonisk spenning: vi har to systemer som begge vil være datakilde, med sanntid via LISTEN/NOTIFY + WebSocket.
primærkilde, og vi bruker konvensjoner for å holde dem fra å kollidere.
### Ambisiøs bunn, forsiktig topp ### Ambisiøs bunn, forsiktig topp
Meldingsboksen og kunnskapsgrafen åpner for opplevelser vi ikke leverer Meldingsboksen og kunnskapsgrafen åpner for opplevelser vi ikke leverer

View file

@ -24,7 +24,7 @@ som knyttes til resultatet.
### Én pipeline ### Én pipeline
All input går gjennom samme tekniske pipeline: All input går gjennom samme tekniske pipeline:
maskinrommet → SpacetimeDB (instant) → PG (asynk). maskinrommet → PG → NOTIFY → WebSocket (sanntid).
Konteksten bestemmer routing, ikke en teknisk modus: Konteksten bestemmer routing, ikke en teknisk modus:
@ -260,8 +260,8 @@ Alle leser fra samme graf. Ingen har "sin egen" data.
- [Noder er sentrum](bruker_ikke_workspace.md) — visibility, - [Noder er sentrum](bruker_ikke_workspace.md) — visibility,
tilgangsmatrise, aliaser tilgangsmatrise, aliaser
- [Datalaget](datalaget.md) — SpacetimeDB holder hele grafen, - [Datalaget](datalaget.md) — PG er eneste datakilde,
PG persisterer asynkront sanntid via LISTEN/NOTIFY + WebSocket
- [Maskinrommet](maskinrommet.md) — validering, routing, CAS, - [Maskinrommet](maskinrommet.md) — validering, routing, CAS,
tunge jobber (Whisper, TTS, AI) tunge jobber (Whisper, TTS, AI)
- [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden - [Rom, ikke forum](rom_ikke_forum.md) — kommunikasjonsnoden

View file

@ -52,7 +52,7 @@ Maskinrommet (Rust)
│ └── ... │ └── ...
▼ direkte ▼ direkte
PG, STDB, CAS PG, CAS
``` ```
Claude har tilgang til hele `tools/`-katalogen og kan kjøre alt direkte: Claude har tilgang til hele `tools/`-katalogen og kan kjøre alt direkte:
@ -109,7 +109,7 @@ Ikke en big-bang refaktor. Gradvis utbryting:
Kjernen som ikke bør brytes ut: Kjernen som ikke bør brytes ut:
- Auth-middleware (JWT-validering, node-oppslag) - Auth-middleware (JWT-validering, node-oppslag)
- Intentions (validering, STDB+PG-skriving, edge-logikk) - Intentions (validering, PG-skriving, edge-logikk)
- Jobbkø (polling, retry, dead letter) - Jobbkø (polling, retry, dead letter)
- Tilgangskontroll (node_access, recompute_access) - Tilgangskontroll (node_access, recompute_access)
- Health-endepunkt - Health-endepunkt

View file

@ -5,7 +5,7 @@
> produksjonsserveren via Claude Code. Dokumentet beholdes som referanse > produksjonsserveren via Claude Code. Dokumentet beholdes som referanse
> i tilfelle lokalt utviklingsmiljø gjeninnføres. > i tilfelle lokalt utviklingsmiljø gjeninnføres.
Det lokale miljøet var et **utviklingsmiljø for kode**. Frontend (SvelteKit) ble kjørt lokalt med HMR, Rust ble bygd lokalt. Alle tjenester (PG, SpacetimeDB, AI Gateway, etc.) kjørte på produksjonsserveren — ingen lokal Docker-replika. Det lokale miljøet var et **utviklingsmiljø for kode**. Frontend (SvelteKit) ble kjørt lokalt med HMR, Rust ble bygd lokalt. Alle tjenester (PG, AI Gateway, etc.) kjørte på produksjonsserveren — ingen lokal Docker-replika.
## Hva som gjøres hvor ## Hva som gjøres hvor

View file

@ -63,7 +63,7 @@ newgrp docker
```bash ```bash
sudo mkdir -p /srv/synops/{config,data,media,logs} sudo mkdir -p /srv/synops/{config,data,media,logs}
sudo mkdir -p /srv/synops/config/{caddy,authentik} sudo mkdir -p /srv/synops/config/{caddy,authentik}
sudo mkdir -p /srv/synops/data/{postgres,spacetimedb,forgejo,authentik} sudo mkdir -p /srv/synops/data/{postgres,forgejo,authentik}
sudo mkdir -p /srv/synops/data/whisper-models sudo mkdir -p /srv/synops/data/whisper-models
sudo mkdir -p /srv/synops/media/podcast sudo mkdir -p /srv/synops/media/podcast
sudo mkdir -p /srv/synops/logs/caddy sudo mkdir -p /srv/synops/logs/caddy
@ -80,7 +80,6 @@ Resultat:
│ └── authentik/ │ └── authentik/
├── data/ ├── data/
│ ├── postgres/ │ ├── postgres/
│ ├── spacetimedb/
│ ├── forgejo/ │ ├── forgejo/
│ ├── whisper-models/ │ ├── whisper-models/
│ └── authentik/ │ └── authentik/
@ -128,11 +127,6 @@ AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/
AUTHENTIK_CLIENT_ID=<fra Authentik OIDC-provider> AUTHENTIK_CLIENT_ID=<fra Authentik OIDC-provider>
AUTHENTIK_CLIENT_SECRET=<fra Authentik OIDC-provider> AUTHENTIK_CLIENT_SECRET=<fra Authentik OIDC-provider>
# === SpacetimeDB ===
SPACETIMEDB_URL=http://spacetimedb:3000
SPACETIMEDB_DATABASE=synops
SPACETIMEDB_TOKEN=<generert av spacetime identity token>
# === Whisper (STT) === # === Whisper (STT) ===
# Modell lastes ned automatisk ved oppstart. large-v3 gir best norsk kvalitet. # Modell lastes ned automatisk ved oppstart. large-v3 gir best norsk kvalitet.
# Ved GPU: bytt image til fedirz/faster-whisper-server:latest-cuda og WHISPER__COMPUTE_TYPE=float16 # Ved GPU: bytt image til fedirz/faster-whisper-server:latest-cuda og WHISPER__COMPUTE_TYPE=float16
@ -156,8 +150,7 @@ Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i
5. **Forgejo:** Start med Authentik som OAuth2-provider, opprett organisasjon og repo 5. **Forgejo:** Start med Authentik som OAuth2-provider, opprett organisasjon og repo
### Lag B: Sanntid (krever nettverk) ### Lag B: Sanntid (krever nettverk)
6. **SpacetimeDB:** Start, verifiser tilkobling 6. **LiveKit:** Start, verifiser at WebRTC fungerer
7. **LiveKit:** Start, verifiser at WebRTC fungerer
### Lag C: Applikasjon (krever alt over) ### Lag C: Applikasjon (krever alt over)
8. **SvelteKit:** Bygg og start container, verifiser at frontenden laster 8. **SvelteKit:** Bygg og start container, verifiser at frontenden laster
@ -187,7 +180,6 @@ services:
postgres: # data:/srv/synops/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/synops/data/forgejo, på git.sidelinja.org forgejo: # data:/srv/synops/data/forgejo, på git.sidelinja.org
spacetimedb: # data:/srv/synops/data/spacetimedb
maskinrommet: # Rust/axum API, intern port 3100, proxyet via Caddy maskinrommet: # Rust/axum API, intern port 3100, proxyet via Caddy
livekit: # Intern port, proxyet via Caddy livekit: # Intern port, proxyet via Caddy
sveltekit: # Intern port, proxyet via Caddy sveltekit: # Intern port, proxyet via Caddy
@ -205,11 +197,6 @@ auth.sidelinja.org {
# === Sidelinja (hovedapplikasjon) === # === Sidelinja (hovedapplikasjon) ===
sidelinja.org { sidelinja.org {
# SpacetimeDB (WebSocket) — handle_path stripper prefix
handle_path /spacetime/* {
reverse_proxy spacetimedb:3000
}
# LiveKit signaling (WebSocket upgrade) # LiveKit signaling (WebSocket upgrade)
handle_path /livekit/* { handle_path /livekit/* {
reverse_proxy livekit:7880 reverse_proxy livekit:7880
@ -401,7 +388,6 @@ echo "$(date): Off-site backup ferdig" >> /srv/synops/logs/backup-offsite.log
- **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git - **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git
- **Logger** — rulleres med logrotate, arkiveres separat ved behov - **Logger** — rulleres med logrotate, arkiveres separat ved behov
- **Whisper-modeller** — re-download fra HuggingFace - **Whisper-modeller** — re-download fra HuggingFace
- **SpacetimeDB** — sanntidsdata synkes til PG, in-memory state er flyktig
### 11.8 Restore-prosedyre ### 11.8 Restore-prosedyre
```bash ```bash
@ -454,8 +440,7 @@ sudo journalctl -u maskinrommet -f
``` ```
Env-filen (`/tmp/maskinrommet.env`) genereres automatisk av Env-filen (`/tmp/maskinrommet.env`) genereres automatisk av
`scripts/maskinrommet-env.sh` med Docker container-IPs for PG og STDB. `scripts/maskinrommet-env.sh` med Docker container-IPs for PG.
Ved oppstart laster maskinrommet hele grafen fra PG inn i STDB (warmup).
Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`. Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`.
Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes
@ -470,7 +455,7 @@ ikke i produksjon.
- [ ] Git push til Forgejo fungerer - [ ] Git push til Forgejo fungerer
### Lag B-C ### Lag B-C
- [x] `https://api.sidelinja.org/health` returnerer `{"status":"ok"}` med PG og STDB tilkoblet (verifisert 2026-03-17) - [x] `https://api.sidelinja.org/health` returnerer `{"status":"ok"}` med PG tilkoblet (verifisert 2026-03-17)
- [x] `https://api.sidelinja.org/me` returnerer 401 uten token (verifisert 2026-03-17) - [x] `https://api.sidelinja.org/me` returnerer 401 uten token (verifisert 2026-03-17)
- [x] `https://sidelinja.org` laster SvelteKit-appen (deployet 2025-03-15) - [x] `https://sidelinja.org` laster SvelteKit-appen (deployet 2025-03-15)
- [x] `https://sidelinja.org/api/health` returnerer 200 - [x] `https://sidelinja.org/api/health` returnerer 200
@ -478,6 +463,5 @@ ikke i produksjon.
- [x] Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15) - [x] Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15)
- [ ] `https://synops.no` viser placeholder - [ ] `https://synops.no` viser placeholder
- [ ] `https://vegard.info` svarer - [ ] `https://vegard.info` svarer
- [ ] SpacetimeDB: WebSocket-tilkobling fra nettleser fungerer
- [x] LiveKit: Container kjører, signaling proxyet via Caddy (verifisert 2026-03-17) - [x] LiveKit: Container kjører, signaling proxyet via Caddy (verifisert 2026-03-17)
- [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes` - [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes`

View file

@ -7,7 +7,3 @@ AUTH_TRUST_HOST=true
# Maskinrommet API # Maskinrommet API
MASKINROMMET_URL=https://api.sidelinja.org MASKINROMMET_URL=https://api.sidelinja.org
# SpacetimeDB (sanntids WebSocket-tilkobling)
VITE_SPACETIMEDB_URL=wss://sidelinja.org/spacetime
VITE_SPACETIMEDB_MODULE=synops

View file

@ -18,7 +18,6 @@
"@tiptap/starter-kit": "^3.20.4", "@tiptap/starter-kit": "^3.20.4",
"d3": "^7.9.0", "d3": "^7.9.0",
"livekit-client": "^2.17.3", "livekit-client": "^2.17.3",
"spacetimedb": "^2.0.4",
"wavesurfer.js": "^7.12.4" "wavesurfer.js": "^7.12.4"
}, },
"devDependencies": { "devDependencies": {
@ -2205,26 +2204,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -2888,12 +2867,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/headers-polyfill": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
"integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
"license": "MIT"
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3357,18 +3330,6 @@
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
}, },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/obug": { "node_modules/obug": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@ -3457,21 +3418,6 @@
"preact": ">=10" "preact": ">=10"
} }
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prosemirror-changeset": { "node_modules/prosemirror-changeset": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
@ -3676,22 +3622,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pure-rand": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
"integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@ -3812,15 +3742,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -3871,59 +3792,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/spacetimedb": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/spacetimedb/-/spacetimedb-2.0.4.tgz",
"integrity": "sha512-7GiZerC9SKXvHvcaOLCgLEjFL4XOcG30k5f/ogA9QqBuD+tO/om6DfhIplo5dUg976gfqxr3Hsp5XtY2pPSCKw==",
"license": "ISC",
"dependencies": {
"base64-js": "^1.5.1",
"headers-polyfill": "^4.0.3",
"object-inspect": "^1.13.4",
"prettier": "^3.3.3",
"pure-rand": "^7.0.1",
"safe-stable-stringify": "^2.5.0",
"statuses": "^2.0.2",
"url-polyfill": "^1.1.14"
},
"peerDependencies": {
"@angular/core": ">=17.0.0",
"@tanstack/react-query": "^5.0.0",
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"undici": "^6.19.2",
"vue": "^3.3.0"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
},
"@tanstack/react-query": {
"optional": true
},
"react": {
"optional": true
},
"svelte": {
"optional": true
},
"undici": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@ -4069,12 +3937,6 @@
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/url-polyfill": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz",
"integrity": "sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==",
"license": "MIT"
},
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",

View file

@ -34,7 +34,6 @@
"@tiptap/starter-kit": "^3.20.4", "@tiptap/starter-kit": "^3.20.4",
"d3": "^7.9.0", "d3": "^7.9.0",
"livekit-client": "^2.17.3", "livekit-client": "^2.17.3",
"spacetimedb": "^2.0.4",
"wavesurfer.js": "^7.12.4" "wavesurfer.js": "^7.12.4"
} }
} }

View file

@ -629,7 +629,7 @@ export interface CreateAnnouncementResponse {
node_id: string; node_id: string;
} }
/** Opprett et systemvarsel (vises for alle klienter umiddelbart via STDB). */ /** Opprett et systemvarsel (vises for alle klienter umiddelbart via WebSocket). */
export function createAnnouncement( export function createAnnouncement(
accessToken: string, accessToken: string,
data: CreateAnnouncementRequest data: CreateAnnouncementRequest
@ -1283,7 +1283,7 @@ export async function fetchNodeUsage(
} }
// ============================================================================= // =============================================================================
// Mixer-kanaler (oppgave 22.2 — erstatter STDB-reducers) // Mixer-kanaler
// ============================================================================= // =============================================================================
export async function createMixerChannel( export async function createMixerChannel(

View file

@ -55,7 +55,7 @@
let shareError = $state(''); let shareError = $state('');
let shareSaving = $state(false); let shareSaving = $state(false);
// --- Derived: AI-preset noder fra STDB --- // --- Derived: AI-preset noder fra store ---
const presets = $derived.by(() => { const presets = $derived.by(() => {
const all = nodeStore.byKind('ai_preset'); const all = nodeStore.byKind('ai_preset');
return all.sort((a, b) => { return all.sort((a, b) => {

View file

@ -12,7 +12,7 @@
collectionNode: Node | undefined; collectionNode: Node | undefined;
/** Current user's node ID */ /** Current user's node ID */
userId: string | undefined; userId: string | undefined;
/** Whether STDB is connected */ /** Whether WebSocket is connected */
connected: boolean; connected: boolean;
/** Active trait names on this collection */ /** Active trait names on this collection */
traitNames: string[]; traitNames: string[];
@ -259,7 +259,7 @@
</div> </div>
{#if connected} {#if connected}
<span class="context-status context-status-ok" title="Tilkoblet SpacetimeDB">&#9679;</span> <span class="context-status context-status-ok" title="Tilkoblet sanntid">&#9679;</span>
{:else} {:else}
<span class="context-status" title="{connectionState.current}">&#9679;</span> <span class="context-status" title="{connectionState.current}">&#9679;</span>
{/if} {/if}

View file

@ -27,7 +27,7 @@ export interface Rect {
height: number; height: number;
} }
/** Events emitted by the canvas for consumer integration (e.g. SpacetimeDB sync) */ /** Events emitted by the canvas for consumer integration (e.g. WebSocket sync) */
export interface CanvasEvents { export interface CanvasEvents {
onObjectMove?: (id: string, x: number, y: number) => void; onObjectMove?: (id: string, x: number, y: number) => void;
onObjectResize?: (id: string, w: number, h: number) => void; onObjectResize?: (id: string, w: number, h: number) => void;

View file

@ -115,7 +115,7 @@
}); });
// ========================================================================= // =========================================================================
// Scheduled events from SpacetimeDB // Scheduled events from WebSocket store
// ========================================================================= // =========================================================================
interface ScheduledEvent { interface ScheduledEvent {

View file

@ -1,7 +1,7 @@
/** /**
* WebSocket-tilkobling til portvokteren (maskinrommet). * WebSocket-tilkobling til portvokteren (maskinrommet).
* *
* Erstatter SpacetimeDB-klient i Fase M2. Kobler til /ws-endepunktet, * Kobler til /ws-endepunktet,
* mottar initial_sync og inkrementelle events, og oppdaterer reactive stores. * mottar initial_sync og inkrementelle events, og oppdaterer reactive stores.
* *
* Usage: * Usage:

View file

@ -1,13 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {};

View file

@ -1,21 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
sourceId: __t.string(),
targetId: __t.string(),
edgeType: __t.string(),
metadata: __t.string(),
system: __t.bool(),
createdBy: __t.string(),
};

View file

@ -1,17 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
updatedBy: __t.string(),
};

View file

@ -1,21 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
nodeKind: __t.string(),
title: __t.string(),
content: __t.string(),
visibility: __t.string(),
metadata: __t.string(),
createdBy: __t.string(),
};

View file

@ -1,15 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
};

View file

@ -1,16 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
};

View file

@ -1,15 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
subjectId: __t.string(),
};

View file

@ -1,16 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
subjectId: __t.string(),
objectId: __t.string(),
};

View file

@ -1,15 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
};

View file

@ -1,22 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default __t.row({
id: __t.string().primaryKey(),
sourceId: __t.string().name("source_id"),
targetId: __t.string().name("target_id"),
edgeType: __t.string().name("edge_type"),
metadata: __t.string(),
system: __t.bool(),
createdAt: __t.timestamp().name("created_at"),
createdBy: __t.string().name("created_by"),
});

View file

@ -1,203 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
// This was generated using spacetimedb cli version 2.0.5 (commit d60138999206c06c776829072f46b5d1c1101f7e).
/* eslint-disable */
/* tslint:disable */
import {
DbConnectionBuilder as __DbConnectionBuilder,
DbConnectionImpl as __DbConnectionImpl,
SubscriptionBuilderImpl as __SubscriptionBuilderImpl,
TypeBuilder as __TypeBuilder,
Uuid as __Uuid,
convertToAccessorMap as __convertToAccessorMap,
makeQueryBuilder as __makeQueryBuilder,
procedureSchema as __procedureSchema,
procedures as __procedures,
reducerSchema as __reducerSchema,
reducers as __reducers,
schema as __schema,
t as __t,
table as __table,
type AlgebraicTypeType as __AlgebraicTypeType,
type DbConnectionConfig as __DbConnectionConfig,
type ErrorContextInterface as __ErrorContextInterface,
type Event as __Event,
type EventContextInterface as __EventContextInterface,
type Infer as __Infer,
type QueryBuilder as __QueryBuilder,
type ReducerEventContextInterface as __ReducerEventContextInterface,
type RemoteModule as __RemoteModule,
type SubscriptionEventContextInterface as __SubscriptionEventContextInterface,
type SubscriptionHandleImpl as __SubscriptionHandleImpl,
} from "spacetimedb";
// Import all reducer arg schemas
import ClearAllReducer from "./clear_all_reducer";
import CreateEdgeReducer from "./create_edge_reducer";
import CreateNodeReducer from "./create_node_reducer";
import DeleteEdgeReducer from "./delete_edge_reducer";
import DeleteNodeReducer from "./delete_node_reducer";
import DeleteNodeAccessReducer from "./delete_node_access_reducer";
import DeleteNodeAccessForSubjectReducer from "./delete_node_access_for_subject_reducer";
import UpdateEdgeReducer from "./update_edge_reducer";
import UpdateNodeReducer from "./update_node_reducer";
import UpsertNodeAccessReducer from "./upsert_node_access_reducer";
import CreateMixerChannelReducer from "./create_mixer_channel_reducer";
import SetGainReducer from "./set_gain_reducer";
import SetMuteReducer from "./set_mute_reducer";
import ToggleEffectReducer from "./toggle_effect_reducer";
import DeleteMixerChannelReducer from "./delete_mixer_channel_reducer";
import SetMixerRoleReducer from "./set_mixer_role_reducer";
// Import all procedure arg schemas
// Import all table schema definitions
import EdgeRow from "./edge_table";
import MixerChannelRow from "./mixer_channel_table";
import NodeRow from "./node_table";
import NodeAccessRow from "./node_access_table";
/** Type-only namespace exports for generated type groups. */
/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */
const tablesSchema = __schema({
edge: __table({
name: 'edge',
indexes: [
{ accessor: 'id', name: 'edge_id_idx_btree', algorithm: 'btree', columns: [
'id',
] },
{ accessor: 'source_id', name: 'edge_source_id_idx_btree', algorithm: 'btree', columns: [
'sourceId',
] },
{ accessor: 'target_id', name: 'edge_target_id_idx_btree', algorithm: 'btree', columns: [
'targetId',
] },
],
constraints: [
{ name: 'edge_id_key', constraint: 'unique', columns: ['id'] },
],
}, EdgeRow),
mixer_channel: __table({
name: 'mixer_channel',
indexes: [
{ accessor: 'id', name: 'mixer_channel_id_idx_btree', algorithm: 'btree', columns: [
'id',
] },
{ accessor: 'room_id', name: 'mixer_channel_room_id_idx_btree', algorithm: 'btree', columns: [
'roomId',
] },
{ accessor: 'target_user_id', name: 'mixer_channel_target_user_id_idx_btree', algorithm: 'btree', columns: [
'targetUserId',
] },
],
constraints: [
{ name: 'mixer_channel_id_key', constraint: 'unique', columns: ['id'] },
],
}, MixerChannelRow),
node: __table({
name: 'node',
indexes: [
{ accessor: 'id', name: 'node_id_idx_btree', algorithm: 'btree', columns: [
'id',
] },
],
constraints: [
{ name: 'node_id_key', constraint: 'unique', columns: ['id'] },
],
}, NodeRow),
node_access: __table({
name: 'node_access',
indexes: [
{ accessor: 'id', name: 'node_access_id_idx_btree', algorithm: 'btree', columns: [
'id',
] },
{ accessor: 'object_id', name: 'node_access_object_id_idx_btree', algorithm: 'btree', columns: [
'objectId',
] },
{ accessor: 'subject_id', name: 'node_access_subject_id_idx_btree', algorithm: 'btree', columns: [
'subjectId',
] },
],
constraints: [
{ name: 'node_access_id_key', constraint: 'unique', columns: ['id'] },
],
}, NodeAccessRow),
});
/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */
const reducersSchema = __reducers(
__reducerSchema("clear_all", ClearAllReducer),
__reducerSchema("create_edge", CreateEdgeReducer),
__reducerSchema("create_node", CreateNodeReducer),
__reducerSchema("delete_edge", DeleteEdgeReducer),
__reducerSchema("delete_node", DeleteNodeReducer),
__reducerSchema("delete_node_access", DeleteNodeAccessReducer),
__reducerSchema("delete_node_access_for_subject", DeleteNodeAccessForSubjectReducer),
__reducerSchema("update_edge", UpdateEdgeReducer),
__reducerSchema("update_node", UpdateNodeReducer),
__reducerSchema("upsert_node_access", UpsertNodeAccessReducer),
__reducerSchema("create_mixer_channel", CreateMixerChannelReducer),
__reducerSchema("set_gain", SetGainReducer),
__reducerSchema("set_mute", SetMuteReducer),
__reducerSchema("toggle_effect", ToggleEffectReducer),
__reducerSchema("delete_mixer_channel", DeleteMixerChannelReducer),
__reducerSchema("set_mixer_role", SetMixerRoleReducer),
);
/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */
const proceduresSchema = __procedures(
);
/** The remote SpacetimeDB module schema, both runtime and type information. */
const REMOTE_MODULE = {
versionInfo: {
cliVersion: "2.0.5" as const,
},
tables: tablesSchema.schemaType.tables,
reducers: reducersSchema.reducersType.reducers,
...proceduresSchema,
} satisfies __RemoteModule<
typeof tablesSchema.schemaType,
typeof reducersSchema.reducersType,
typeof proceduresSchema
>;
/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */
export const tables: __QueryBuilder<typeof tablesSchema.schemaType> = __makeQueryBuilder(tablesSchema.schemaType);
/** The reducers available in this remote SpacetimeDB module. */
export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers);
/** The context type returned in callbacks for all possible events. */
export type EventContext = __EventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for reducer events. */
export type ReducerEventContext = __ReducerEventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for subscription events. */
export type SubscriptionEventContext = __SubscriptionEventContextInterface<typeof REMOTE_MODULE>;
/** The context type returned in callbacks for error events. */
export type ErrorContext = __ErrorContextInterface<typeof REMOTE_MODULE>;
/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */
export type SubscriptionHandle = __SubscriptionHandleImpl<typeof REMOTE_MODULE>;
/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */
export class SubscriptionBuilder extends __SubscriptionBuilderImpl<typeof REMOTE_MODULE> {}
/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */
export class DbConnectionBuilder extends __DbConnectionBuilder<DbConnection> {}
/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */
export class DbConnection extends __DbConnectionImpl<typeof REMOTE_MODULE> {
/** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */
static builder = (): DbConnectionBuilder => {
return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig<typeof REMOTE_MODULE>) => new DbConnection(config));
};
/** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */
override subscriptionBuilder = (): SubscriptionBuilder => {
return new SubscriptionBuilder(this);
};
}

View file

@ -1,23 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default __t.row({
id: __t.string().primaryKey(),
roomId: __t.string().name("room_id"),
targetUserId: __t.string().name("target_user_id"),
gain: __t.f64(),
isMuted: __t.bool().name("is_muted"),
activeEffects: __t.string().name("active_effects"),
role: __t.string(),
updatedBy: __t.string().name("updated_by"),
updatedAt: __t.timestamp().name("updated_at"),
});

View file

@ -1,19 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default __t.row({
id: __t.string().primaryKey(),
subjectId: __t.string().name("subject_id"),
objectId: __t.string().name("object_id"),
access: __t.string(),
viaEdge: __t.string().name("via_edge"),
});

View file

@ -1,22 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default __t.row({
id: __t.string().primaryKey(),
nodeKind: __t.string().name("node_kind"),
title: __t.string(),
content: __t.string(),
visibility: __t.string(),
metadata: __t.string(),
createdAt: __t.timestamp().name("created_at"),
createdBy: __t.string().name("created_by"),
});

View file

@ -1,18 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
gain: __t.f64(),
updatedBy: __t.string(),
};

View file

@ -1,18 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
role: __t.string(),
updatedBy: __t.string(),
};

View file

@ -1,18 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
isMuted: __t.bool(),
updatedBy: __t.string(),
};

View file

@ -1,18 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
roomId: __t.string(),
targetUserId: __t.string(),
effectName: __t.string(),
updatedBy: __t.string(),
};

View file

@ -1,58 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export const Edge = __t.object("Edge", {
id: __t.string(),
sourceId: __t.string(),
targetId: __t.string(),
edgeType: __t.string(),
metadata: __t.string(),
system: __t.bool(),
createdAt: __t.timestamp(),
createdBy: __t.string(),
});
export type Edge = __Infer<typeof Edge>;
export const Node = __t.object("Node", {
id: __t.string(),
nodeKind: __t.string(),
title: __t.string(),
content: __t.string(),
visibility: __t.string(),
metadata: __t.string(),
createdAt: __t.timestamp(),
createdBy: __t.string(),
});
export type Node = __Infer<typeof Node>;
export const MixerChannel = __t.object("MixerChannel", {
id: __t.string(),
roomId: __t.string(),
targetUserId: __t.string(),
gain: __t.f64(),
isMuted: __t.bool(),
activeEffects: __t.string(),
role: __t.string(),
updatedBy: __t.string(),
updatedAt: __t.timestamp(),
});
export type MixerChannel = __Infer<typeof MixerChannel>;
export const NodeAccess = __t.object("NodeAccess", {
id: __t.string(),
subjectId: __t.string(),
objectId: __t.string(),
access: __t.string(),
viaEdge: __t.string(),
});
export type NodeAccess = __Infer<typeof NodeAccess>;

View file

@ -1,10 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import { type Infer as __Infer } from "spacetimedb";
// Import all procedure arg schemas

View file

@ -1,42 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import { type Infer as __Infer } from "spacetimedb";
// Import all reducer arg schemas
import ClearAllReducer from "../clear_all_reducer";
import CreateEdgeReducer from "../create_edge_reducer";
import CreateNodeReducer from "../create_node_reducer";
import DeleteEdgeReducer from "../delete_edge_reducer";
import DeleteNodeReducer from "../delete_node_reducer";
import DeleteNodeAccessReducer from "../delete_node_access_reducer";
import DeleteNodeAccessForSubjectReducer from "../delete_node_access_for_subject_reducer";
import UpdateEdgeReducer from "../update_edge_reducer";
import UpdateNodeReducer from "../update_node_reducer";
import UpsertNodeAccessReducer from "../upsert_node_access_reducer";
import CreateMixerChannelReducer from "../create_mixer_channel_reducer";
import SetGainReducer from "../set_gain_reducer";
import SetMuteReducer from "../set_mute_reducer";
import ToggleEffectReducer from "../toggle_effect_reducer";
import DeleteMixerChannelReducer from "../delete_mixer_channel_reducer";
import SetMixerRoleReducer from "../set_mixer_role_reducer";
export type ClearAllParams = __Infer<typeof ClearAllReducer>;
export type CreateEdgeParams = __Infer<typeof CreateEdgeReducer>;
export type CreateNodeParams = __Infer<typeof CreateNodeReducer>;
export type DeleteEdgeParams = __Infer<typeof DeleteEdgeReducer>;
export type DeleteNodeParams = __Infer<typeof DeleteNodeReducer>;
export type DeleteNodeAccessParams = __Infer<typeof DeleteNodeAccessReducer>;
export type DeleteNodeAccessForSubjectParams = __Infer<typeof DeleteNodeAccessForSubjectReducer>;
export type UpdateEdgeParams = __Infer<typeof UpdateEdgeReducer>;
export type UpdateNodeParams = __Infer<typeof UpdateNodeReducer>;
export type UpsertNodeAccessParams = __Infer<typeof UpsertNodeAccessReducer>;
export type CreateMixerChannelParams = __Infer<typeof CreateMixerChannelReducer>;
export type SetGainParams = __Infer<typeof SetGainReducer>;
export type SetMuteParams = __Infer<typeof SetMuteReducer>;
export type ToggleEffectParams = __Infer<typeof ToggleEffectReducer>;
export type DeleteMixerChannelParams = __Infer<typeof DeleteMixerChannelReducer>;
export type SetMixerRoleParams = __Infer<typeof SetMixerRoleReducer>;

View file

@ -1,17 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
edgeType: __t.string(),
metadata: __t.string(),
};

View file

@ -1,20 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
id: __t.string(),
nodeKind: __t.string(),
title: __t.string(),
content: __t.string(),
visibility: __t.string(),
metadata: __t.string(),
};

View file

@ -1,18 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
/* eslint-disable */
/* tslint:disable */
import {
TypeBuilder as __TypeBuilder,
t as __t,
type AlgebraicTypeType as __AlgebraicTypeType,
type Infer as __Infer,
} from "spacetimedb";
export default {
subjectId: __t.string(),
objectId: __t.string(),
access: __t.string(),
viaEdge: __t.string(),
};

View file

@ -1,7 +1,6 @@
/** /**
* Lokale type-definisjoner for noder, edges, access og mixer-kanaler. * Lokale type-definisjoner for noder, edges, access og mixer-kanaler.
* *
* Erstatter SpacetimeDB module_bindings/types.ts.
* Feltnavnene matcher JSON-formatet fra portvokterens WebSocket (camelCase). * Feltnavnene matcher JSON-formatet fra portvokterens WebSocket (camelCase).
*/ */

View file

@ -94,7 +94,7 @@
return 'text-neutral-500'; return 'text-neutral-500';
} }
const allServices = ['maskinrommet', 'caddy', 'postgres', 'spacetimedb', 'authentik', 'litellm', 'whisper', 'livekit']; const allServices = ['maskinrommet', 'caddy', 'postgres', 'authentik', 'litellm', 'whisper', 'livekit'];
</script> </script>
<div class="min-h-screen bg-neutral-950 text-neutral-100 p-4 sm:p-8"> <div class="min-h-screen bg-neutral-950 text-neutral-100 p-4 sm:p-8">

View file

@ -94,7 +94,7 @@
} }
// ========================================================================= // =========================================================================
// Scheduled events from SpacetimeDB // Scheduled events from WebSocket store
// ========================================================================= // =========================================================================
interface ScheduledEvent { interface ScheduledEvent {

View file

@ -41,7 +41,7 @@
}; };
// ========================================================================= // =========================================================================
// Kort fra SpacetimeDB — noder med submitted_to-edge til samlingen // Kort fra WebSocket store — noder med submitted_to-edge til samlingen
// ========================================================================= // =========================================================================
interface CardData { interface CardData {

View file

@ -25,7 +25,7 @@
const connected = $derived(connectionState.current === 'connected'); const connected = $derived(connectionState.current === 'connected');
const mediaNodeId = $derived($page.params.id ?? ''); const mediaNodeId = $derived($page.params.id ?? '');
// Media node from STDB // Media node from store
const mediaNode = $derived(connected ? nodeStore.get(mediaNodeId) : undefined); const mediaNode = $derived(connected ? nodeStore.get(mediaNodeId) : undefined);
const metadata = $derived.by(() => { const metadata = $derived.by(() => {
if (!mediaNode?.metadata) return null; if (!mediaNode?.metadata) return null;

View file

@ -78,7 +78,7 @@
}); });
}); });
// Also try to read layout from STDB node (for real-time sync) // Also try to read layout from WS node store (for real-time sync)
const workspaceNode = $derived( const workspaceNode = $derived(
workspaceNodeId && connected ? nodeStore.get(workspaceNodeId) : undefined workspaceNodeId && connected ? nodeStore.get(workspaceNodeId) : undefined
); );
@ -90,7 +90,7 @@
let layout = $state<WorkspaceLayout>({ panels: [] }); let layout = $state<WorkspaceLayout>({ panels: [] });
let layoutInitialized = $state(false); let layoutInitialized = $state(false);
// When workspace node appears in STDB (after creation), load its layout // When workspace node appears in store (after creation), load its layout
$effect(() => { $effect(() => {
if (!workspaceNode || layoutInitialized) return; if (!workspaceNode || layoutInitialized) return;
try { try {
@ -124,7 +124,7 @@
clearTimeout(saveTimeout); clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => { saveTimeout = setTimeout(async () => {
try { try {
// Read current metadata from STDB node // Read current metadata from node store
const currentMeta = workspaceNode const currentMeta = workspaceNode
? JSON.parse(workspaceNode.metadata ?? '{}') ? JSON.parse(workspaceNode.metadata ?? '{}')
: {}; : {};
@ -460,7 +460,7 @@
</div> </div>
{#if connected} {#if connected}
<span class="context-status context-status-ok" title="Tilkoblet SpacetimeDB">&#9679;</span> <span class="context-status context-status-ok" title="Tilkoblet sanntid">&#9679;</span>
{:else} {:else}
<span class="context-status" title="{connectionState.current}">&#9679;</span> <span class="context-status" title="{connectionState.current}">&#9679;</span>
{/if} {/if}

View file

@ -758,8 +758,8 @@ async fn resolve_silence_cuts(
// ─── Jobbhåndterer — delegerer til synops-audio CLI ──────────────── // ─── Jobbhåndterer — delegerer til synops-audio CLI ────────────────
// //
// Maskinrommet beholder: STDB-synk for sanntidsvisning.
// CLI gjør: FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging. // CLI gjør: FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging.
// PG NOTIFY-triggere sender sanntidsoppdateringer.
/// Synops-audio binary path. /// Synops-audio binary path.
fn audio_bin() -> String { fn audio_bin() -> String {
@ -771,7 +771,7 @@ fn audio_bin() -> String {
/// ///
/// Spawner synops-audio med --write for å gjøre alt arbeidet: /// Spawner synops-audio med --write for å gjøre alt arbeidet:
/// FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging. /// FFmpeg-prosessering, CAS-lagring, PG-skriving, ressurslogging.
/// Maskinrommet gjør etterpå STDB-synk for sanntidsvisning. /// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter.
/// ///
/// Payload: /// Payload:
/// ```json /// ```json

View file

@ -1,7 +1,7 @@
// Felles hjelpefunksjoner for å spawne CLI-verktøy fra jobbkø-handlere. // Felles hjelpefunksjoner for å spawne CLI-verktøy fra jobbkø-handlere.
// //
// Mønsteret: maskinrommet orkestrerer (payload-parsing, sikkerhetskontroller, // Mønsteret: maskinrommet orkestrerer (payload-parsing, sikkerhetskontroller),
// STDB-synk), CLI-verktøyet gjør jobben (API-kall, DB-skriving, prosessering). // CLI-verktøyet gjør jobben (API-kall, DB-skriving, prosessering).
// Stdout → jobbresultat (JSON), stderr → feillogg, exitkode → status. // Stdout → jobbresultat (JSON), stderr → feillogg, exitkode → status.
// //
// Ref: docs/retninger/unix_filosofi.md // Ref: docs/retninger/unix_filosofi.md

View file

@ -1,6 +1,6 @@
// Serverhelse-dashboard — tjeneste-status, metrikker, backup-status, logg-tilgang. // Serverhelse-dashboard — tjeneste-status, metrikker, backup-status, logg-tilgang.
// //
// Sjekker alle tjenester i stacken (PG, STDB, Caddy, Authentik, LiteLLM, // Sjekker alle tjenester i stacken (PG, Caddy, Authentik, LiteLLM,
// Whisper, LiveKit) og samler system-metrikker (CPU, minne, disk). // Whisper, LiveKit) og samler system-metrikker (CPU, minne, disk).
// //
// Ref: docs/concepts/adminpanelet.md § 4 "Serverhelse", oppgave 15.6 // Ref: docs/concepts/adminpanelet.md § 4 "Serverhelse", oppgave 15.6
@ -373,10 +373,9 @@ fn read_service_logs(service: &str, max_lines: usize) -> Vec<LogEntry> {
"maskinrommet" | "caddy" | "sveltekit" => { "maskinrommet" | "caddy" | "sveltekit" => {
format!("journalctl -u {service} --no-pager -n {max_lines} --output=short-iso 2>/dev/null") format!("journalctl -u {service} --no-pager -n {max_lines} --output=short-iso 2>/dev/null")
} }
"postgres" | "spacetimedb" | "authentik" | "litellm" | "whisper" | "livekit" => { "postgres" | "authentik" | "litellm" | "whisper" | "livekit" => {
let container = match service { let container = match service {
"postgres" => "sidelinja-postgres-1", "postgres" => "sidelinja-postgres-1",
"spacetimedb" => "sidelinja-spacetimedb-1",
"authentik" => "sidelinja-authentik-server-1", "authentik" => "sidelinja-authentik-server-1",
"litellm" => "sidelinja-ai-gateway-1", "litellm" => "sidelinja-ai-gateway-1",
"whisper" => "sidelinja-faster-whisper-1", "whisper" => "sidelinja-faster-whisper-1",
@ -473,7 +472,7 @@ pub async fn health_logs(
read_service_logs(service, max_lines) read_service_logs(service, max_lines)
} else { } else {
// Alle tjenester, siste linjer fra hver // Alle tjenester, siste linjer fra hver
let services = ["maskinrommet", "caddy", "postgres", "spacetimedb", "authentik", "litellm", "whisper", "livekit"]; let services = ["maskinrommet", "caddy", "postgres", "authentik", "litellm", "whisper", "livekit"];
let per_service = (max_lines / services.len()).max(10); let per_service = (max_lines / services.len()).max(10);
let mut all = Vec::new(); let mut all = Vec::new();
for svc in &services { for svc in &services {

View file

@ -457,8 +457,7 @@ pub struct CreateNodeResponse {
/// POST /intentions/create_node /// POST /intentions/create_node
/// ///
/// Validerer input, skriver til STDB (instant), spawner async PG-skriving. /// Validerer input, skriver til PG. NOTIFY-triggere sender sanntidsoppdateringer.
/// Returnerer node_id umiddelbart.
/// ///
/// Hvis `context_id` er satt, opprettes automatisk en `belongs_to`-edge /// Hvis `context_id` er satt, opprettes automatisk en `belongs_to`-edge
/// fra den nye noden til kontekstnoden. Kontekstnoden må eksistere og /// fra den nye noden til kontekstnoden. Kontekstnoden må eksistere og
@ -1179,7 +1178,7 @@ pub struct DeleteNodeResponse {
/// POST /intentions/delete_node /// POST /intentions/delete_node
/// ///
/// Sletter en node og alle dens edges (CASCADE i PG, eksplisitt i STDB). /// Sletter en node og alle dens edges (CASCADE i PG).
/// Krever at brukeren er created_by eller har owner/admin-edge til noden. /// Krever at brukeren er created_by eller har owner/admin-edge til noden.
pub async fn delete_node( pub async fn delete_node(
State(state): State<AppState>, State(state): State<AppState>,
@ -3264,7 +3263,7 @@ pub struct RoomParticipantInfo {
/// ///
/// Kobler en bruker til sanntidslyd i en kommunikasjonsnode. /// Kobler en bruker til sanntidslyd i en kommunikasjonsnode.
/// Validerer tilgang (bruker må ha member_of/owner/host_of-edge), /// Validerer tilgang (bruker må ha member_of/owner/host_of-edge),
/// genererer LiveKit-token, oppdaterer STDB med live-status. /// genererer LiveKit-token, oppdaterer PG med live-status.
pub async fn join_communication( pub async fn join_communication(
State(state): State<AppState>, State(state): State<AppState>,
user: AuthUser, user: AuthUser,
@ -3521,7 +3520,7 @@ pub struct CloseCommunicationResponse {
/// POST /intentions/close_communication /// POST /intentions/close_communication
/// ///
/// Stenger et sanntidsrom. Krever owner/admin-tilgang. /// Stenger et sanntidsrom. Krever owner/admin-tilgang.
/// Fjerner alle deltakere fra STDB, oppdaterer metadata til "ended". /// Oppdaterer metadata til "ended".
pub async fn close_communication( pub async fn close_communication(
State(state): State<AppState>, State(state): State<AppState>,
user: AuthUser, user: AuthUser,
@ -3734,7 +3733,7 @@ pub struct CreateAnnouncementResponse {
/// POST /intentions/create_announcement /// POST /intentions/create_announcement
/// ///
/// Oppretter en systemvarslingsnode med `visibility: open` slik at alle /// Oppretter en systemvarslingsnode med `visibility: open` slik at alle
/// aktive klienter ser varselet via STDB umiddelbart. /// aktive klienter ser varselet via WebSocket umiddelbart.
/// ///
/// Kun autentiserte brukere kan opprette varsler (MVP — full admin-sjekk /// Kun autentiserte brukere kan opprette varsler (MVP — full admin-sjekk
/// legges til når admin-rollesystemet er på plass). /// legges til når admin-rollesystemet er på plass).
@ -3817,8 +3816,8 @@ pub struct ExpireAnnouncementResponse {
/// POST /intentions/expire_announcement /// POST /intentions/expire_announcement
/// ///
/// Fjerner (sletter) en systemvarslingsnode. Sletter fra STDB først /// Fjerner (sletter) en systemvarslingsnode fra PG.
/// (umiddelbar fjerning fra alle klienter), deretter fra PG. /// WebSocket NOTIFY sørger for umiddelbar fjerning fra alle klienter.
/// ///
/// Kun eier (created_by) eller admin kan fjerne varsler. /// Kun eier (created_by) eller admin kan fjerne varsler.
pub async fn expire_announcement( pub async fn expire_announcement(

View file

@ -22,8 +22,6 @@ pub mod resource_usage;
pub mod resources; pub mod resources;
mod rss; mod rss;
mod serving; mod serving;
#[allow(dead_code)]
mod stdb; // Beholdt som død kode — fjernes i oppgave 22.4
pub mod summarize; pub mod summarize;
pub mod ws; pub mod ws;
pub mod mixer; pub mod mixer;
@ -257,7 +255,7 @@ async fn main() {
.route("/custom-domain/sok", get(custom_domain::serve_custom_domain_search)) .route("/custom-domain/sok", get(custom_domain::serve_custom_domain_search))
.route("/custom-domain/om", get(custom_domain::serve_custom_domain_about)) .route("/custom-domain/om", get(custom_domain::serve_custom_domain_about))
.route("/custom-domain/{article_id}", get(custom_domain::serve_custom_domain_article)) .route("/custom-domain/{article_id}", get(custom_domain::serve_custom_domain_article))
// Mixer-kanaler (oppgave 22.2 — erstatter STDB-reducers) // Mixer-kanaler
.route("/intentions/create_mixer_channel", post(mixer::create_mixer_channel)) .route("/intentions/create_mixer_channel", post(mixer::create_mixer_channel))
.route("/intentions/set_gain", post(mixer::set_gain)) .route("/intentions/set_gain", post(mixer::set_gain))
.route("/intentions/set_mute", post(mixer::set_mute)) .route("/intentions/set_mute", post(mixer::set_mute))

View file

@ -1,10 +1,7 @@
//! Mixer-kanaler — HTTP API for delt lydmixer-tilstand. //! Mixer-kanaler — HTTP API for delt lydmixer-tilstand.
//! //!
//! Erstatter SpacetimeDB-reducers for mixer (createMixerChannel, setGain, //! Skriver direkte til PG; NOTIFY-trigger propagerer endringer til
//! setMute, toggleEffect, setMixerRole). Skriver direkte til PG; //! WebSocket-klienter.
//! NOTIFY-trigger propagerer endringer til WebSocket-klienter.
//!
//! Ref: oppgave 22.2 (SpacetimeDB-migrering)
use axum::{extract::State, http::StatusCode, Json}; use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -202,7 +202,7 @@ fn edge_type_to_access_level(edge_type: &str) -> Option<&'static str> {
/// Handler: pg_insert_edge /// Handler: pg_insert_edge
/// ///
/// Håndterer tilgangsgivende edges (owner/admin/member_of/reader) med /// Håndterer tilgangsgivende edges (owner/admin/member_of/reader) med
/// recompute_access i transaksjon, og synker til STDB. For belongs_to-edges /// recompute_access i transaksjon. For belongs_to-edges
/// trigges artikkelrendering hvis target er en publiseringssamling. /// trigges artikkelrendering hvis target er en publiseringssamling.
pub async fn handle_insert_edge( pub async fn handle_insert_edge(
job: &JobRow, job: &JobRow,

View file

@ -1,6 +1,6 @@
// Tunge spørringer — lesestien via PostgreSQL med RLS. // Tunge spørringer — lesestien via PostgreSQL med RLS.
// //
// For søk, statistikk, og graf-traversering brukes PG direkte (ikke STDB). // For søk, statistikk, og graf-traversering brukes PG direkte.
// Alle spørringer kjøres med SET LOCAL ROLE synops_reader, som er underlagt // Alle spørringer kjøres med SET LOCAL ROLE synops_reader, som er underlagt
// RLS-policies. Brukerens node_id settes som sesjonsvariabel. // RLS-policies. Brukerens node_id settes som sesjonsvariabel.
// //

View file

@ -1,466 +0,0 @@
// SpacetimeDB HTTP-klient for maskinrommet.
//
// Kaller STDB-reducere via HTTP JSON API.
// Maskinrommet eier all skriving — denne klienten er eneste vei inn.
//
// API-format: POST /v1/database/{db}/call/{reducer}
// Body: JSON-objekt med navngitte parametre.
// Auth: Bearer-token fra STDB-identitet.
//
// Ref: docs/retninger/datalaget.md, docs/infra/synkronisering.md
use reqwest::Client;
use serde::Serialize;
/// SpacetimeDB-klient som kaller reducere via HTTP.
#[derive(Clone)]
pub struct StdbClient {
client: Client,
base_url: String,
database: String,
token: String,
}
impl StdbClient {
/// Opprett ny klient. `base_url` er STDB-serverens URL (f.eks. "http://spacetimedb:3000").
pub fn new(base_url: &str, database: &str, token: &str) -> Self {
Self {
client: Client::new(),
base_url: base_url.trim_end_matches('/').to_string(),
database: database.to_string(),
token: token.to_string(),
}
}
/// Hent en ny identitet og token fra STDB-serveren.
/// Brukes ved oppstart hvis ingen token er konfigurert.
pub async fn create_identity(base_url: &str) -> Result<(String, String), StdbError> {
let client = Client::new();
let url = format!("{}/v1/identity", base_url.trim_end_matches('/'));
let resp = client.post(&url).send().await?;
if !resp.status().is_success() {
return Err(StdbError::Http(format!(
"Kunne ikke opprette identitet: {}",
resp.status()
)));
}
let body: serde_json::Value = resp.json().await?;
let identity = body["identity"]
.as_str()
.ok_or_else(|| StdbError::Http("Mangler identity i respons".into()))?
.to_string();
let token = body["token"]
.as_str()
.ok_or_else(|| StdbError::Http("Mangler token i respons".into()))?
.to_string();
Ok((identity, token))
}
/// Kall en reducer med navngitte parametre (JSON-objekt).
async fn call_reducer<T: Serialize>(&self, reducer: &str, args: &T) -> Result<(), StdbError> {
let url = format!(
"{}/v1/database/{}/call/{}",
self.base_url, self.database, reducer
);
let resp = self
.client
.post(&url)
.bearer_auth(&self.token)
.json(args)
.send()
.await?;
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
Err(StdbError::Reducer {
reducer: reducer.to_string(),
status: status.as_u16(),
message: body,
})
}
}
// =========================================================================
// Node-operasjoner
// =========================================================================
pub async fn create_node(
&self,
id: &str,
node_kind: &str,
title: &str,
content: &str,
visibility: &str,
metadata: &str,
created_by: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
node_kind: &'a str,
title: &'a str,
content: &'a str,
visibility: &'a str,
metadata: &'a str,
created_by: &'a str,
}
self.call_reducer(
"create_node",
&Args {
id,
node_kind,
title,
content,
visibility,
metadata,
created_by,
},
)
.await
}
pub async fn update_node(
&self,
id: &str,
node_kind: &str,
title: &str,
content: &str,
visibility: &str,
metadata: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
node_kind: &'a str,
title: &'a str,
content: &'a str,
visibility: &'a str,
metadata: &'a str,
}
self.call_reducer(
"update_node",
&Args {
id,
node_kind,
title,
content,
visibility,
metadata,
},
)
.await
}
pub async fn delete_node(&self, id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
}
self.call_reducer("delete_node", &Args { id }).await
}
// =========================================================================
// Edge-operasjoner
// =========================================================================
pub async fn create_edge(
&self,
id: &str,
source_id: &str,
target_id: &str,
edge_type: &str,
metadata: &str,
system: bool,
created_by: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
source_id: &'a str,
target_id: &'a str,
edge_type: &'a str,
metadata: &'a str,
system: bool,
created_by: &'a str,
}
self.call_reducer(
"create_edge",
&Args {
id,
source_id,
target_id,
edge_type,
metadata,
system,
created_by,
},
)
.await
}
pub async fn update_edge(
&self,
id: &str,
edge_type: &str,
metadata: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
edge_type: &'a str,
metadata: &'a str,
}
self.call_reducer("update_edge", &Args { id, edge_type, metadata })
.await
}
pub async fn delete_edge(&self, id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
}
self.call_reducer("delete_edge", &Args { id }).await
}
// =========================================================================
// NodeAccess-operasjoner
// =========================================================================
pub async fn upsert_node_access(
&self,
subject_id: &str,
object_id: &str,
access: &str,
via_edge: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
subject_id: &'a str,
object_id: &'a str,
access: &'a str,
via_edge: &'a str,
}
self.call_reducer(
"upsert_node_access",
&Args {
subject_id,
object_id,
access,
via_edge,
},
)
.await
}
pub async fn delete_node_access(
&self,
subject_id: &str,
object_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
subject_id: &'a str,
object_id: &'a str,
}
self.call_reducer("delete_node_access", &Args { subject_id, object_id })
.await
}
// =========================================================================
// Live-rom (LiveKit)
// =========================================================================
pub async fn create_live_room(
&self,
room_id: &str,
communication_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
communication_id: &'a str,
}
self.call_reducer("create_live_room", &Args { room_id, communication_id })
.await
}
pub async fn add_room_participant(
&self,
room_id: &str,
user_id: &str,
display_name: &str,
role: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
user_id: &'a str,
display_name: &'a str,
role: &'a str,
}
self.call_reducer(
"add_room_participant",
&Args { room_id, user_id, display_name, role },
)
.await
}
pub async fn remove_room_participant(
&self,
room_id: &str,
user_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
user_id: &'a str,
}
self.call_reducer("remove_room_participant", &Args { room_id, user_id })
.await
}
pub async fn close_live_room(&self, room_id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
}
self.call_reducer("close_live_room", &Args { room_id }).await
}
// =========================================================================
// Placement-operasjoner (message_placements)
// =========================================================================
/// Plasser en melding i en kontekst. Idempotent (upsert).
pub async fn place_message(
&self,
id: &str,
message_id: &str,
context_type: &str,
context_id: &str,
position_json: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
message_id: &'a str,
context_type: &'a str,
context_id: &'a str,
position_json: &'a str,
}
self.call_reducer(
"place_message",
&Args { id, message_id, context_type, context_id, position_json },
)
.await
}
/// Fjern en meldings plassering fra en kontekst.
pub async fn remove_placement(
&self,
message_id: &str,
context_type: &str,
context_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
message_id: &'a str,
context_type: &'a str,
context_id: &'a str,
}
self.call_reducer(
"remove_placement",
&Args { message_id, context_type, context_id },
)
.await
}
/// Flytt en plassering (oppdater posisjon).
pub async fn move_on_canvas(
&self,
placement_id: &str,
new_position_json: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
placement_id: &'a str,
new_position_json: &'a str,
}
self.call_reducer(
"move_on_canvas",
&Args { placement_id, new_position_json },
)
.await
}
// =========================================================================
// Vedlikehold
// =========================================================================
/// Tøm alle noder og edges. Brukes ved warmup for å unngå duplikater.
pub async fn clear_all(&self) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Empty {}
self.call_reducer("clear_all", &Empty {}).await
}
}
// =============================================================================
// Feilhåndtering
// =============================================================================
#[derive(Debug)]
pub enum StdbError {
/// HTTP-transportfeil (nettverk, timeout)
Http(String),
/// Reducer returnerte feil (400, 500, etc.)
Reducer {
reducer: String,
status: u16,
message: String,
},
}
impl std::fmt::Display for StdbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StdbError::Http(msg) => write!(f, "STDB HTTP-feil: {msg}"),
StdbError::Reducer {
reducer,
status,
message,
} => write!(f, "STDB reducer {reducer} feilet ({status}): {message}"),
}
}
}
impl std::error::Error for StdbError {}
impl From<reqwest::Error> for StdbError {
fn from(e: reqwest::Error) -> Self {
StdbError::Http(e.to_string())
}
}

View file

@ -25,7 +25,7 @@ fn tts_bin() -> String {
/// ///
/// Spawner synops-tts med --write for å gjøre alt arbeidet: /// Spawner synops-tts med --write for å gjøre alt arbeidet:
/// ElevenLabs-kall, CAS-lagring, PG-skriving, ressurslogging. /// ElevenLabs-kall, CAS-lagring, PG-skriving, ressurslogging.
/// Maskinrommet gjør etterpå STDB-synk for sanntidsvisning. /// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter.
pub async fn handle_tts_job( pub async fn handle_tts_job(
job: &JobRow, job: &JobRow,
db: &PgPool, db: &PgPool,

View file

@ -4,7 +4,6 @@
//! og `mixer_channel_changed` kanaler i PostgreSQL og videresender relevante //! og `mixer_channel_changed` kanaler i PostgreSQL og videresender relevante
//! endringer via WebSocket til tilkoblede klienter, filtrert på tilgangsmatrisen. //! endringer via WebSocket til tilkoblede klienter, filtrert på tilgangsmatrisen.
//! //!
//! Fase M2: Frontend bruker kun denne WebSocket-tilkoblingen (SpacetimeDB fjernet).
//! Events berikes med full raddata fra PG slik at klienten kan oppdatere stores direkte. //! Events berikes med full raddata fra PG slik at klienten kan oppdatere stores direkte.
//! Ref: docs/retninger/datalaget.md //! Ref: docs/retninger/datalaget.md

View file

@ -24,7 +24,7 @@ faktisk kode. Mer fokusert enn ryddejobben — kun docs.
### 3. Tekniske detaljer ### 3. Tekniske detaljer
- [ ] Stemmer database-skjema beskrevet i docs med faktiske migrasjoner? - [ ] Stemmer database-skjema beskrevet i docs med faktiske migrasjoner?
- [ ] Stemmer API-endepunkter beskrevet i docs med faktiske routes? - [ ] Stemmer API-endepunkter beskrevet i docs med faktiske routes?
- [ ] Stemmer SpacetimeDB-tabeller/reducere i docs med modulen? - [ ] Stemmer WebSocket-/sanntids-beskrivelser i docs med implementasjonen?
### 4. Setup-docs ### 4. Setup-docs
- [ ] `docs/setup/lokal.md` — fungerer stegene fortsatt? - [ ] `docs/setup/lokal.md` — fungerer stegene fortsatt?

View file

@ -29,10 +29,7 @@ men ikke implementert.
- [ ] Er det env-vars i `.env` som er utdaterte? - [ ] Er det env-vars i `.env` som er utdaterte?
- [ ] Er secrets rotert der de bør være? - [ ] Er secrets rotert der de bør være?
### 5. SpacetimeDB-modul ### 5. Caddy / reverse proxy
- [ ] Er SpacetimeDB-modulen publisert med siste endringer?
### 6. Caddy / reverse proxy
- [ ] Er Caddyfile i repo synk med `/srv/synops/config/caddy/Caddyfile`? - [ ] Er Caddyfile i repo synk med `/srv/synops/config/caddy/Caddyfile`?
- [ ] Er det nye subdomener eller routes som mangler? - [ ] Er det nye subdomener eller routes som mangler?

View file

@ -29,7 +29,6 @@ fjerne utdaterte referanser, og sikre at dokumentasjon stemmer med virkeligheten
- [ ] Ubrukte SvelteKit-routes (mapper i `web/src/routes/` uten innhold eller med stub) - [ ] Ubrukte SvelteKit-routes (mapper i `web/src/routes/` uten innhold eller med stub)
- [ ] Ubrukte komponenter (filer i `web/src/lib/components/` som ikke importeres) - [ ] Ubrukte komponenter (filer i `web/src/lib/components/` som ikke importeres)
- [ ] Ubrukte Rust-moduler i worker - [ ] Ubrukte Rust-moduler i worker
- [ ] Ubrukte SpacetimeDB-reducere eller tabeller
- [ ] Gamle migrations som bør dokumenteres eller konsolideres - [ ] Gamle migrations som bør dokumenteres eller konsolideres
- [ ] `package.json` / `Cargo.toml` — ubrukte dependencies - [ ] `package.json` / `Cargo.toml` — ubrukte dependencies

View file

@ -13,7 +13,7 @@ Slettes når Synops er oppe og stabilt.
| authentik-worker | goauthentik/server:latest | SSO bakgrunn | JA | | authentik-worker | goauthentik/server:latest | SSO bakgrunn | JA |
| redis | redis:7-alpine | Cache for Authentik | JA | | redis | redis:7-alpine | Cache for Authentik | JA |
| forgejo | forgejo:10 | Git | JA | | forgejo | forgejo:10 | Git | JA |
| spacetimedb | clockworklabs/spacetime:latest | Sanntid | FJERNES (tom v1-modul) | | ~~spacetimedb~~ | ~~clockworklabs/spacetime:latest~~ | ~~Sanntid~~ | FJERNET (mars 2026) |
| web | sidelinja-web (bygget) | SvelteKit v1 | FJERNES | | web | sidelinja-web (bygget) | SvelteKit v1 | FJERNES |
| worker | sidelinja-worker (bygget) | Rust worker v1 | FJERNES | | worker | sidelinja-worker (bygget) | Rust worker v1 | FJERNES |
| ai-gateway | litellm:main-stable | AI-ruter | BEHOLDES (uavhengig av app) | | ai-gateway | litellm:main-stable | AI-ruter | BEHOLDES (uavhengig av app) |
@ -23,7 +23,7 @@ Slettes når Synops er oppe og stabilt.
- git.sidelinja.org → forgejo:3000 - git.sidelinja.org → forgejo:3000
- sidelinja.org → web:3000 + /media/* (filservering) + JSON access log - sidelinja.org → web:3000 + /media/* (filservering) + JSON access log
- vegard.info → placeholder - vegard.info → placeholder
- rt.sidelinja.org → spacetimedb:3000 - ~~rt.sidelinja.org → spacetimedb:3000~~ (fjernet)
## PG init-script ## PG init-script
`/srv/sidelinja/config/postgres/init/01-create-databases.sql` `/srv/sidelinja/config/postgres/init/01-create-databases.sql`
@ -34,7 +34,7 @@ kjøres kun ved *første* PG-oppstart.
## Dockerfiles (referanse) ## Dockerfiles (referanse)
### web/Dockerfile ### web/Dockerfile
- node:20-alpine, to-stegs bygg - node:20-alpine, to-stegs bygg
- VITE_SPACETIMEDB_URL som build-arg - ~~VITE_SPACETIMEDB_URL som build-arg~~ (fjernet)
- `npm run build``node build` på port 3000 - `npm run build``node build` på port 3000
### worker/Dockerfile ### worker/Dockerfile

View file

@ -9,16 +9,12 @@ read_env() { grep "^$1=" "$ENV_FILE" | head -1 | cut -d= -f2; }
container_ip() { docker inspect "$1" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'; } container_ip() { docker inspect "$1" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'; }
PG_IP=$(container_ip sidelinja-postgres-1) PG_IP=$(container_ip sidelinja-postgres-1)
STDB_IP=$(container_ip sidelinja-spacetimedb-1)
WHISPER_IP=$(container_ip sidelinja-faster-whisper-1 2>/dev/null || echo "") WHISPER_IP=$(container_ip sidelinja-faster-whisper-1 2>/dev/null || echo "")
AI_GW_IP=$(container_ip sidelinja-ai-gateway-1 2>/dev/null || echo "") AI_GW_IP=$(container_ip sidelinja-ai-gateway-1 2>/dev/null || echo "")
LIVEKIT_IP=$(container_ip sidelinja-livekit-1 2>/dev/null || echo "") LIVEKIT_IP=$(container_ip sidelinja-livekit-1 2>/dev/null || echo "")
cat > /tmp/maskinrommet.env <<EOF cat > /tmp/maskinrommet.env <<EOF
DATABASE_URL=postgres://$(read_env POSTGRES_USER):$(read_env POSTGRES_PASSWORD)@${PG_IP}:5432/synops DATABASE_URL=postgres://$(read_env POSTGRES_USER):$(read_env POSTGRES_PASSWORD)@${PG_IP}:5432/synops
SPACETIMEDB_URL=http://${STDB_IP}:3000
SPACETIMEDB_DATABASE=$(read_env SPACETIMEDB_DATABASE)
SPACETIMEDB_TOKEN=$(read_env SPACETIMEDB_TOKEN)
AUTHENTIK_ISSUER=$(read_env AUTHENTIK_ISSUER) AUTHENTIK_ISSUER=$(read_env AUTHENTIK_ISSUER)
AUTHENTIK_CLIENT_ID=$(read_env AUTHENTIK_CLIENT_ID) AUTHENTIK_CLIENT_ID=$(read_env AUTHENTIK_CLIENT_ID)
BIND_ADDR=0.0.0.0:3100 BIND_ADDR=0.0.0.0:3100

View file

@ -37,7 +37,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
- `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` — Noder er sentrum: brukere, team, innhold er noder. Tilgangsmatrise fra edges. - `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, sanntid via LISTEN/NOTIFY + WebSocket
- `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene: - `docs/primitiver/` — Spesifikasjoner for kjerneprimitivene:
- `nodes.md` — Node-skjema, node_kind, visibility, CAS-noder, eierskap - `nodes.md` — Node-skjema, node_kind, visibility, CAS-noder, eierskap
- `edges.md` — Edge-skjema, typer, metadata, systemedges - `edges.md` — Edge-skjema, typer, metadata, systemedges
@ -57,7 +57,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
- `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
- `jobbkø.md` — PostgreSQL-basert køsystem for bakgrunnsjobber - `jobbkø.md` — PostgreSQL-basert køsystem for bakgrunnsjobber
- `synkronisering.md`PostgreSQL ↔ SpacetimeDB dataflyt og eierskapsmodell - `synkronisering.md`Historisk: PostgreSQL ↔ SpacetimeDB (utgått, STDB fjernet mars 2026)
- `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, SpacetimeDB, Authentik) - `docs/erfaringer/` — Lærdommer fra v1 (adapter-mønster, Svelte 5, SpacetimeDB, Authentik)
- `reference/` — Kode fra v1 med gjenbruksverdi (Editor.svelte) - `reference/` — Kode fra v1 med gjenbruksverdi (Editor.svelte)
- `ops/` — Repeterbare vedlikeholdsjobber (ryddejobb, doc-audit, drift-sjekk) - `ops/` — Repeterbare vedlikeholdsjobber (ryddejobb, doc-audit, drift-sjekk)
@ -70,7 +70,7 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
## Stack ## Stack
- **Orkestrator/Backend:** Rust (maskinrommet) - **Orkestrator/Backend:** Rust (maskinrommet)
- **Frontend:** SvelteKit (TypeScript, PWA) - **Frontend:** SvelteKit (TypeScript, PWA)
- **Sanntid:** SpacetimeDB - **Sanntid:** PG LISTEN/NOTIFY + WebSocket
- **Database/Graf:** PostgreSQL (+Apache AGE ved behov) - **Database/Graf:** PostgreSQL (+Apache AGE ved behov)
- **Binærlagring:** CAS (content-addressable store) - **Binærlagring:** CAS (content-addressable store)
- **AI:** LiteLLM (AI Gateway), faster-whisper (STT), ElevenLabs (TTS) - **AI:** LiteLLM (AI Gateway), faster-whisper (STT), ElevenLabs (TTS)
@ -109,10 +109,10 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
GUI (SvelteKit) GUI (SvelteKit)
│ skriv │ les (sanntid, direkte WebSocket) │ skriv │ les (sanntid, direkte WebSocket)
▼ ▼ ▼ ▼
Maskinrommet (Rust) SpacetimeDB ──→ GUI Maskinrommet (Rust) ──→ WebSocket ──→ GUI
Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ... Tjenester: PG+AGE, CAS, Whisper, LiteLLM, LiveKit ...
``` ```
## Kjerneprinsipper ## Kjerneprinsipper
@ -126,8 +126,7 @@ Tjenester: PG+AGE, SpacetimeDB, CAS, Whisper, LiteLLM, LiveKit ...
Du ser dine edges. Tilgang via materialisert tilgangsmatrise. 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 eneste datakilde.** Sanntid via LISTEN/NOTIFY + WebSocket.
To lag, to roller.
================================================================ ================================================================
@ -146,52 +145,50 @@ Synops.
Alt er noder og edges i en graf. En bruker er en node. Et team er en Alt er noder og edges i en graf. En bruker er en node. Et team er en
node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke node. En mediefil er en node. Hva noe "er" bestemmes av edges, ikke
av noden selv. Maskinrommet eier alle skrivinger. Frontend er et tynt av noden selv. Maskinrommet eier alle skrivinger. Frontend er et tynt
lag som leser grafen fra SpacetimeDB. lag som mottar sanntidsdata via WebSocket.
## Lagmodell ## Lagmodell
``` ```
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ GUI (SvelteKit) │ │ GUI (SvelteKit) │
│ Visninger: spørringer mot STDB │ Visninger: sanntid via WebSocket
└────────┬──────────────────┬─────────┘ └────────┬──────────────────┬─────────┘
│ intensjoner │ les (sanntid) │ intensjoner │ les (sanntid)
│ │ direkte WebSocket │ │ WebSocket
┌────────▼────────┐ ┌──────▼──────────┐ ┌────────▼──────────────────▼─────────┐
│ Maskinrommet │ │ SpacetimeDB │ │ Maskinrommet / Portvokteren (Rust) │
│ (Rust) │ │ Hele grafen │ │ Eier alle skrivinger │
│ Eier alle │ │ (noder+edges) │ │ PG LISTEN/NOTIFY → WebSocket │
│ skrivinger │ └─────────────────┘ └──┬─────────┬──────────────────┬─────┘
└──┬─────┬─────┬──┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌─────┐┌─────┐┌─────┐┌─────────────┐ ┌─────┐ ┌─────┐ ┌─────────────┐
│ PG ││STDB ││ CAS ││ Whisper, │ │ PG │ │ CAS │ │ Whisper, │
(bak)││(skr)││ ││ LiteLLM, │ │ │ │ │ LiteLLM, │
│ │ │ ││ LiveKit ... │ │ │ │ │ │ LiveKit ... │
└─────┘└─────┘└─────┘└─────────────┘ └─────┘ └─────┘ └─────────────┘
``` ```
### Skrivestien ### Skrivestien
GUI → intensjon → Maskinrommet (Rust) → SpacetimeDB (instant) → PG (async) GUI → intensjon → Maskinrommet (Rust) → PG → NOTIFY → WebSocket → GUI
Frontend sender intensjoner (ikke data). Maskinrommet validerer, Frontend sender intensjoner (ikke data). Maskinrommet validerer,
skriver til SpacetimeDB først for umiddelbar oppdatering, deretter skriver til PG, og NOTIFY-triggere propagerer endringen til frontend
persisterer til PG asynkront. Maskinrommet leser edges og bestemmer via WebSocket i sanntid. Maskinrommet leser edges og bestemmer
hvilke tjenester som trigges. hvilke tjenester som trigges.
### Lesestien (sanntid) ### Lesestien (sanntid)
SpacetimeDB → GUI (direkte WebSocket) PG LISTEN/NOTIFY → Maskinrommet → WebSocket → GUI
SpacetimeDB holder hele grafen — alle noder og edges. Frontend PG er eneste datakilde. Frontend mottar sanntidsoppdateringer via
abonnerer via WebSocket med edge-filtre. Visninger er spørringer WebSocket. Initial sync ved tilkobling, deretter inkrementelle events.
mot STDB, ikke forhåndsdefinerte API-endepunkter.
### Lesestien (tunge spørringer) ### Lesestien (tunge spørringer)
GUI → Maskinrommet (Rust) → PG GUI → Maskinrommet (Rust) → PG
Søk, statistikk, semantisk søk (pgvector), graftraversering Søk, statistikk, semantisk søk (pgvector), graftraversering
(AGE/Cypher). For operasjoner der STDB ikke er egnet. (AGE/Cypher). For tunge operasjoner som ikke passer sanntidsstrømmen.
## Datamodell ## Datamodell
@ -214,7 +211,7 @@ bestemmes utelukkende av dens edges:
- Node uten edges = løs tanke - Node uten edges = løs tanke
### Visninger er spørringer ### Visninger er spørringer
Visninger er spørringer mot SpacetimeDB med edge-filtre: Visninger er spørringer mot grafen 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
@ -276,10 +273,9 @@ Deling er å legge til edges.
Persistent backup og arkiv. Alle noder og edges. Fulltekstsøk, Persistent backup og arkiv. Alle noder og edges. Fulltekstsøk,
pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov. pgvector (semantisk søk), JSONB. Apache AGE for Cypher ved behov.
### SpacetimeDB ### Sanntid (PG LISTEN/NOTIFY + WebSocket)
Holder hele grafen — alle noder og edges. Frontend abonnerer via PG NOTIFY-triggere på noder, edges og node_access propagerer endringer
WebSocket. Maskinrommet skriver hit først for umiddelbar oppdatering, til frontend via WebSocket i portvokteren. Ingen separat sanntidstjeneste.
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å
@ -291,9 +287,9 @@ er en cache som regenereres on-demand.
| Rolle | Teknologi | Begrunnelse | | Rolle | Teknologi | Begrunnelse |
|-------|-----------|-------------| |-------|-----------|-------------|
| Orkestrator | Rust | Ytelse, typesikkerhet, eier alle skrivinger | | Orkestrator | Rust | Ytelse, typesikkerhet, eier alle skrivinger |
| Frontend | SvelteKit | PWA, SSR, tynt lag mot STDB | | Frontend | SvelteKit | PWA, SSR, tynt lag mot WebSocket |
| Database | PostgreSQL | Persistent backup, pgvector, fulltekstsøk, AGE | | Database | PostgreSQL | Eneste datakilde, pgvector, fulltekstsøk, AGE |
| Sanntid | SpacetimeDB | Hele grafen, WebSocket-subscriptions, ~10μs | | Sanntid | PG LISTEN/NOTIFY + WebSocket | Triggere på PG-tabeller, portvokteren relay-er |
| 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 |
@ -340,7 +336,7 @@ andre dokumenter. En retning kan også forkastes eller parkeres.
| [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. | | [Universell input og mottak](universell_input.md) | **Besluttet** | Én multimodal input-primitiv, én mottaksflate, kommunikasjonsnoder. Edges definerer alt. |
| [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. | | [Maskinrommet](maskinrommet.md) | **Besluttet** | Én Rust-tjeneste: fang, prosesser, lever. Eier all skriving. Edge-drevet ressursorkestrering. |
| [Noder er sentrum](bruker_ikke_workspace.md) | **Besluttet** | Alt er noder (brukere, team, innhold). Edges definerer relasjoner og tilgang. Materialisert tilgangsmatrise for RLS. | | [Noder er sentrum](bruker_ikke_workspace.md) | **Besluttet** | Alt er noder (brukere, team, innhold). Edges definerer relasjoner og tilgang. Materialisert tilgangsmatrise for RLS. |
| [Datalaget](datalaget.md) | **Besluttet** | SpacetimeDB holder hele grafen, PG er persistent arkiv, CAS for binærdata, AGE ved behov | | [Datalaget](datalaget.md) | **Revidert** | PG er eneste datakilde, sanntid via LISTEN/NOTIFY + WebSocket, CAS for binærdata, AGE ved behov |
## Format ## Format
- Hva er tesen? - Hva er tesen?

View file

@ -1,141 +0,0 @@
#!/usr/bin/env bash
# Test sanntidsflyt: STDB node/edge CRUD via HTTP API
#
# Verifiserer at maskinrommet kan skrive til STDB, og at data
# er tilgjengelig for subscribers (grunnlaget for at to faner
# kan se hverandres endringer i sanntid).
#
# Kjøres fra lokal maskin. SSH-er til serveren for å nå Docker-nettverket.
#
# Bruk: ./scripts/test-sanntid.sh
set -euo pipefail
SERVER="claude@157.180.81.26"
NETWORK="sidelinja_sidelinja-net"
STDB_URL="http://spacetimedb:3000/v1/database/synops"
VEGARD_NODE="a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
# Les STDB-token fra .env
STDB_TOKEN=$(ssh "$SERVER" "grep SPACETIMEDB_TOKEN /srv/synops/.env | cut -d= -f2-")
if [ -z "$STDB_TOKEN" ]; then
echo "FEIL: Fant ikke SPACETIMEDB_TOKEN i .env"
exit 1
fi
TEST_NODE_ID="test-sanntid-$(date +%s)"
TEST_EDGE_ID="test-edge-$(date +%s)"
# Hjelpefunksjon: kall STDB reducer via curl i Docker-nettverket
stdb_call() {
local reducer=$1
local data=$2
ssh "$SERVER" "docker run --rm --network $NETWORK curlimages/curl:latest \
-s -w '%{http_code}' -X POST $STDB_URL/call/$reducer \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $STDB_TOKEN' \
-d '$data'" 2>/dev/null
}
# Hjelpefunksjon: spør STDB via SQL
stdb_sql() {
ssh "$SERVER" "docker exec sidelinja-spacetimedb-1 spacetime sql synops \"$1\"" 2>/dev/null | grep -v WARNING
}
echo "=== Sanntidstest: STDB node/edge CRUD ==="
echo ""
# 1. Opprett testnode
echo -n "1. Oppretter testnode ($TEST_NODE_ID)... "
RESULT=$(stdb_call "create_node" "{\"id\": \"$TEST_NODE_ID\", \"node_kind\": \"content\", \"title\": \"Sanntidstest\", \"content\": \"Automatisk test av sanntidsflyt\", \"visibility\": \"hidden\", \"metadata\": \"{}\", \"created_by\": \"$VEGARD_NODE\"}")
if [ "$RESULT" = "200" ]; then
echo "OK"
else
echo "FEIL (HTTP $RESULT)"
exit 1
fi
# 2. Verifiser node finnes i STDB
echo -n "2. Verifiserer node i STDB... "
NODE_RESULT=$(stdb_sql "SELECT id FROM node WHERE id = '$TEST_NODE_ID'")
if echo "$NODE_RESULT" | grep -q "$TEST_NODE_ID"; then
echo "OK"
else
echo "FEIL: Node ikke funnet"
exit 1
fi
# 3. Opprett owner-edge (Vegard → testnode)
echo -n "3. Oppretter owner-edge... "
RESULT=$(stdb_call "create_edge" "{\"id\": \"$TEST_EDGE_ID\", \"source_id\": \"$VEGARD_NODE\", \"target_id\": \"$TEST_NODE_ID\", \"edge_type\": \"owner\", \"metadata\": \"{}\", \"system\": false, \"created_by\": \"$VEGARD_NODE\"}")
if [ "$RESULT" = "200" ]; then
echo "OK"
else
echo "FEIL (HTTP $RESULT)"
exit 1
fi
# 4. Verifiser edge finnes
echo -n "4. Verifiserer edge i STDB... "
EDGE_RESULT=$(stdb_sql "SELECT id FROM edge WHERE id = '$TEST_EDGE_ID'")
if echo "$EDGE_RESULT" | grep -q "$TEST_EDGE_ID"; then
echo "OK"
else
echo "FEIL: Edge ikke funnet"
exit 1
fi
# 5. Oppdater noden
echo -n "5. Oppdaterer nodetittel... "
RESULT=$(stdb_call "update_node" "{\"id\": \"$TEST_NODE_ID\", \"node_kind\": \"content\", \"title\": \"Sanntidstest (oppdatert)\", \"content\": \"Automatisk test av sanntidsflyt\", \"visibility\": \"hidden\", \"metadata\": \"{}\"}")
if [ "$RESULT" = "200" ]; then
echo "OK"
else
echo "FEIL (HTTP $RESULT)"
exit 1
fi
# 6. Verifiser oppdateringen
echo -n "6. Verifiserer oppdatert tittel... "
TITLE_RESULT=$(stdb_sql "SELECT title FROM node WHERE id = '$TEST_NODE_ID'")
if echo "$TITLE_RESULT" | grep -q "oppdatert"; then
echo "OK"
else
echo "FEIL: Tittel ikke oppdatert"
exit 1
fi
# 7. Slett testnode (kaskade-sletter edges)
echo -n "7. Sletter testnode (kaskaderer edges)... "
RESULT=$(stdb_call "delete_node" "{\"id\": \"$TEST_NODE_ID\"}")
if [ "$RESULT" = "200" ]; then
echo "OK"
else
echo "FEIL (HTTP $RESULT)"
exit 1
fi
# 8. Verifiser at node og edge er borte
echo -n "8. Verifiserer opprydding... "
NODE_GONE=$(stdb_sql "SELECT id FROM node WHERE id = '$TEST_NODE_ID'" | grep -c "$TEST_NODE_ID" || true)
EDGE_GONE=$(stdb_sql "SELECT id FROM edge WHERE id = '$TEST_EDGE_ID'" | grep -c "$TEST_EDGE_ID" || true)
if [ "$NODE_GONE" = "0" ] && [ "$EDGE_GONE" = "0" ]; then
echo "OK"
else
echo "FEIL: Data ikke ryddet opp"
exit 1
fi
echo ""
echo "=== Alle 8 tester bestått ==="
echo ""
echo "Backend-sanntidsflyt fungerer: STDB mottar og serverer"
echo "node/edge-operasjoner korrekt. WebSocket-subscribers"
echo "(frontend-faner) vil motta disse endringene i sanntid."
echo ""
echo "For å teste i browser:"
echo " 1. Start frontend: cd frontend && npm run dev"
echo " 2. Åpne to faner: http://localhost:5173"
echo " 3. Logg inn i begge"
echo " 4. Skriv en node i fane 1, se den dukke opp i fane 2"

View file

@ -1,5 +0,0 @@
{
"server": "synops-server",
"database": "synops",
"module-path": "./spacetimedb"
}

Some files were not shown because too many files have changed in this diff Show more