Commit graph

73 commits

Author SHA1 Message Date
148e5c222c Starter oppgave 7.6 2026-03-17 18:21:54 +01:00
7eae02eeb5 Fullfør oppgave 7.5: Segmenttabell-migrasjon og SRT-pipeline
Oppretter transcription_segments-tabellen i PostgreSQL som master-kopi
for alle transkripsjoner. transcribe.rs er oppdatert fra verbose_json
til SRT-format med full parse → segment-innsetting pipeline.

Endringer:
- Migration 005: transcription_segments med GIN fulltekstsøk (norsk)
- transcribe.rs: SRT-parser, segment-innsetting, node-oppdatering
- Miljøvariabler: WHISPER_MODEL (default "medium"), WHISPER_INITIAL_PROMPT
- Docker-compose: nye env vars for maskinrommet-containeren
- Docs: oppdatert podcastfabrikken, arkitektur, primitiver, CLAUDE.md

Tabellen kjørt på server, maskinrommet restartet med nye env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:19:00 +01:00
edafbc9a12 Starter oppgave 7.5 2026-03-17 18:10:20 +01:00
63d4bbd41b Fullfør oppgave 7.4: Lyd-avspilling med waveform-visning
Legger til AudioPlayer-komponent som spiller av lyd fra CAS-noder
med waveform-visualisering via wavesurfer.js. Komponenten viser
play/pause, tidslinje, og kan ekspandere transkripsjonen.

Chat-visningen inkluderer nå media-noder (has_media-edges) sammen
med tekstmeldinger, sortert kronologisk. Talenotater vises med
mikrofon-ikon, waveform og transkripsjon.

Mottak-siden viser også AudioPlayer for media-noder med lyd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 18:07:29 +01:00
8059d0f84f Starter oppgave 7.4 2026-03-17 17:58:32 +01:00
ebbec982b3 Fullfør oppgave 7.3: Voice memo — opptak-knapp i frontend
Legger til VoiceRecorder-komponent som bruker MediaRecorder API for
lydopptak i nettleseren. Opptaket lastes opp til CAS via eksisterende
uploadMedia-endepunkt, som automatisk trigger Whisper-transkripsjon.

Komponenten er integrert i:
- ChatInput: mikrofon-knapp mellom tekstfelt og send-knapp
- NodeEditor: mikrofon-knapp i verktøylinjen

Flyten: opptak → webm/opus blob → upload → CAS → whisper_transcribe-jobb.
Ingen backend-endringer nødvendig — hele transkripsjons-pipelinen fra
oppgave 7.2 gjenbrukes uendret.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:51:40 +01:00
79c681dbf6 Starter oppgave 7.3 2026-03-17 17:47:13 +01:00
9768a24693 Fullfør oppgave 7.2: Transkripsjons-pipeline (CAS → Whisper → content)
Implementerer komplett pipeline for automatisk transkripsjon av lydfiler:

- PostgreSQL jobbkø (job_queue-tabell med status, retry, backoff)
- Worker-loop i maskinrommet som poller hvert 2. sekund
- Whisper-integrasjon: leser CAS-fil, sender multipart til faster-whisper API
- Postprosessering: filtrerer hallusinerte segmenter (no_speech_prob > 0.6)
- Oppdaterer media-nodens content-felt med transkripsjon og metadata
- Automatisk trigger: upload_media enqueuer jobb for audio/*-filer

Testet ende-til-ende på server: jobb plukkes opp, Whisper prosesserer,
node oppdateres. Retry med eksponentiell backoff ved feil.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:44:54 +01:00
f6d1c5f563 Starter oppgave 7.2 2026-03-17 17:25:06 +01:00
443f60a518 Fullfør oppgave 7.1: faster-whisper Docker-oppsett for norsk STT
Satt opp faster-whisper-server (fedirz/faster-whisper-server:latest-cpu)
som Docker-tjeneste på produksjonsserveren. Ingen GPU tilgjengelig —
bruker CPU med int8-kvantisering og large-v3 modell for best norsk kvalitet.

Verifisert:
- Transkripsjon fungerer via OpenAI-kompatibelt API
- verbose_json med segmenter og tidskoder OK
- Docker DNS-oppslag fra sidelinja-net fungerer
- Maskinrommet har WHISPER_URL=http://faster-whisper:8000
- RAM-bruk ~2.5 GB med modell lastet

Konfigurasjon:
- Image: fedirz/faster-whisper-server:latest-cpu
- Modell: large-v3 (norsk), int8, CPU
- CAS montert read-only for direkte filtilgang
- Healthcheck via python3 (curl ikke tilgjengelig i image)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:21:29 +01:00
8613b90f2c Starter oppgave 7.1 2026-03-17 17:13:44 +01:00
dee9d5bf3a Fullfør oppgave 6.4: Bilder i TipTap via drag-and-drop/paste
Brukeren kan nå dra eller lime inn bilder i TipTap-editoren.
Bildet lastes opp til CAS via upload_media-endepunktet, og settes
inn som <img> med CAS-URL i metadata.document (HTML).

Endringer:
- Ny uploadMedia() og casUrl() i api.ts for multipart upload
- @tiptap/extension-image med CasImage-utvidelse (data-node-id attr)
- handleDrop/handlePaste i editor intercepter bildefiler
- Upload-status vises i editoren mens bilder lastes opp
- accessToken sendes ned til NodeEditor fra +page.svelte

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:07:17 +01:00
dcab564f74 Starter oppgave 6.4 2026-03-17 17:01:55 +01:00
9566ba8dfe Fullfør oppgave 6.3: CAS-serving med GET /cas/{hash}
Nytt endepunkt streamer CAS-filer fra disk med riktig Content-Type
(oppslått fra media-nodens metadata i PG) og Cache-Control: immutable.
Hash-validering (64 hex-tegn) hindrer path traversal.
Tokio-streaming for effektiv håndtering av store filer.

Docker-compose oppdatert med CAS-volum for maskinrommet-containeren.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:58:21 +01:00
5de9e88c51 Starter oppgave 6.3 2026-03-17 16:50:12 +01:00
924ac1b6d0 Fullfør oppgave 6.2: Upload-endepunkt for mediefiler
POST /intentions/upload_media mottar multipart form data, lagrer filen
i CAS med SHA-256 hashing, og oppretter en media-node. Valgfri
source_id oppretter en has_media-edge fra kildenoden til media-noden.

Endepunktet følger etablert skrivestimønster: STDB først (instant),
async PG-persistering i bakgrunnen. Maks filstørrelse 100 MB.
Deduplisering via CAS — identiske filer gir ingen ekstra diskbruk.

Verifisert med curl mot produksjonsserver: upload uten og med
source_id, deduplisering, og PG-persistering fungerer korrekt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:46:54 +01:00
ae3d6739be Starter oppgave 6.2 2026-03-17 16:38:52 +01:00
b7a6f8e7ef Fullfør oppgave 6.1: CAS-lagring med SHA-256 og deduplisering
Implementerer content-addressable store som Rust-modul i maskinrommet:

- CasStore med SHA-256 hashing (sha2 + hex crates)
- Katalogstruktur: {root}/{hash[0..2]}/{hash[2..4]}/{hash}
- Atomisk skriving via temp-fil + rename (hindrer korrupt read)
- Innebygd deduplisering — identisk hash = ingen skriving
- Konfigurerbar rot via CAS_ROOT env (default: /srv/synops/media/cas)
- 5 enhetstester: store, dedup, exists, ulike filer, path-struktur

Neste steg: 6.2 (upload-endepunkt) bruker CasStore for filmottak.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:31:23 +01:00
c85f0d4c92 Starter oppgave 6.1 2026-03-17 16:27:32 +01:00
5d2581710f Fullfør oppgave 5.4: én-til-én chat med deltaker-velger
Implementerer full 1:1 chat-loop:
- NewChatDialog: velg person å starte samtale med
- Fikser API-felt (participant_ids → participants) for korrekt
  kommunikasjon med maskinrommets create_communication-endepunkt
- Opprett kommunikasjonsnode med to deltakere (owner + member_of)
- Dedupliserer: finner eksisterende samtale før ny opprettes
- Chat-header viser den andre deltakerens navn i 1:1-samtaler
- Testbruker-node opprettet på server for verifisering

Full loop verifisert via STDB: node + edges + melding + belongs_to
fungerer, WebSocket-subscribers ser endringer i sanntid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:25:44 +01:00
0a9e2cf814 Starter oppgave 5.4 2026-03-17 16:15:23 +01:00
2af06111bb Fullfør oppgave 5.3: chat-visning med sanntid via STDB
Chat-visning i frontend som viser noder med belongs_to-edge til en
kommunikasjonsnode, sortert på tid, med sanntidsoppdatering via
SpacetimeDB.

Nye filer:
- frontend/src/routes/chat/[id]/+page.svelte — Chat-side som viser
  meldinger (noder med belongs_to-edge), deltakere, auto-scroll,
  og avsender-info. Bruker edgeStore.byTarget() for reaktive
  oppdateringer når nye meldinger kommer via STDB.
- frontend/src/lib/components/ChatInput.svelte — Enkel meldings-input
  med Enter-for-send, auto-resize textarea.

Endringer:
- frontend/src/lib/api.ts — Lagt til createCommunication()-funksjon
  for å opprette kommunikasjonsnoder fra frontend.
- frontend/src/routes/+page.svelte — Kommunikasjonsnoder i mottaket
  er nå klikkbare lenker til chat-visningen. "Ny samtale"-knapp.
- tasks.md — Oppgave 5.3 markert som ferdig.

Arkitektur: Chat-visningen bruker context_id-parameteren i
create_node-intensjonen (implementert i 5.2) for automatisk
belongs_to-edge. Meldinger hentes reaktivt fra STDB-stores —
ingen polling, ingen ekstra API-kall.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:15:15 +01:00
9e3f5de2bd Starter oppgave 5.3 2026-03-17 16:08:50 +01:00
cc1f9275ef Fullfør oppgave 5.2: kontekst-arv med automatisk belongs_to-edge
create_node-intensjonen støtter nå context_id-parameter. Når satt:
- Validerer at kontekstnoden eksisterer og er en kommunikasjonsnode
- Oppretter automatisk belongs_to-edge fra ny node → kontekstnode
- Returnerer belongs_to_edge_id i responsen

Verifisert med curl-tester: feilvalidering for ugyldig/feil node_kind,
og korrekt edge-opprettelse i både STDB og PG for gyldig context_id.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:04:14 +01:00
4ef0b31b3c Starter oppgave 5.2 2026-03-17 15:52:29 +01:00
7189925d08 Fullfør oppgave 5.1: create_communication-intensjon
Ny intensjon POST /intentions/create_communication som oppretter en
kommunikasjonsnode (node_kind='communication') med:
- metadata.started_at satt til opprettelsestidspunkt
- owner-edge fra innlogget bruker til noden
- member_of-edges for alle angitte deltakere
- Validering av deltaker-noder og visibility
- Samme to-lags skriveflyt som andre intensjoner (STDB instant, PG async)

Testet med curl mot produksjonsserver — node, edges og node_access
opprettes korrekt i både STDB og PG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:44:02 +01:00
db98935182 Starter oppgave 5.1 2026-03-17 15:35:59 +01:00
1355d189b2 Fullfør oppgave 4.4: RLS-policies på PG med node_access-filtrering
Implementerer Row Level Security for tunge PostgreSQL-spørringer.
Maskinrommet skriver som superuser (sidelinja), men leser med
SET LOCAL ROLE synops_reader som er underlagt RLS-policies.

Endringer:
- Migration 004: synops_reader rolle, current_node_id() funksjon,
  RLS-policies på nodes (created_by/node_access/visibility),
  edges (endepunkt-tilgang + system-edge-skjuling),
  og node_access (kun egne rader)
- queries.rs: RLS-kontekst-helper (set_rls_context) og
  GET /query/nodes endepunkt med søk, filtrering og paginering
- migration_safety.md: omskrevet fra v1 workspace-RLS til
  node_access-basert RLS med oppdaterte leak hunter-tester

Verifisert på server: hidden noder filtrert for ukjente brukere,
synlige for eiere. Edges filtrert tilsvarende.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:30:29 +01:00
50a6934f05 Starter oppgave 4.4 2026-03-17 15:19:35 +01:00
20ec437c62 Fullfør oppgave 4.3: visibility-filtrering i STDB og frontend
Implementert visibility-filtrering som gjør at frontend kun viser
noder brukeren har tilgang til, basert på visibility-enum og
node_access-matrisen.

Endringer:
- STDB: node_access-tabell + reducers (upsert, delete, clear)
- Maskinrommet: synker node_access til STDB ved warmup og edge-endring
- Frontend: nodeAccessStore, nodeVisibility()-filter, oppdatert mottak
- Discoverable noder: viser tittel men skjuler innhold
- Hidden noder: kun synlige med eksplisitt tilgang eller created_by

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:16:42 +01:00
68bcd57d84 Starter oppgave 4.3 2026-03-17 15:03:07 +01:00
e0f30bba27 Fullfør oppgave 4.2: team-transitivitet i recompute_access
Legger til steg 4 i recompute_access: når en bruker melder seg inn i
et team (member_of-edge), arver brukeren all tilgang teamet allerede
har. Tidligere håndterte funksjonen kun retningen "team får ny tilgang
→ propager til eksisterende medlemmer" (steg 3). Nå håndteres begge
retninger:

- Steg 3: Team får tilgang → alle eksisterende medlemmer arver
- Steg 4: Ny bruker melder seg inn → arver teamets eksisterende tilgang

Testet med scenario: Trond → Podcastteamet → Sidelinja → Episode 42.
Trond arver member-tilgang til alle tre noder via team-transitivitet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:01:51 +01:00
28e6056644 Starter oppgave 4.2 2026-03-17 14:57:01 +01:00
c20fc9149b Fullfør oppgave 4.1: recompute_access ved edge-endring
Når en tilgangsgivende edge (owner, admin, member_of, reader)
opprettes, kalles nå recompute_access() i samme PG-transaksjon
som edge-insertet. Dette sikrer at node_access-matrisen alltid
er oppdatert — ingen vindu med stale tilgang.

Implementasjon:
- edge_type_to_access_level() mapper edge-typer til access_level enum
- insert_edge_with_access() wrapper edge-insert + recompute_access i tx
- Vanlige edges (belongs_to, mentions, etc.) skrives som før (fire-and-forget)

Verifisert med SQL-tester: direkte tilgang + transitiv tilgang via
belongs_to-edges fungerer korrekt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:51:30 +01:00
03211e59a5 Starter oppgave 4.1 2026-03-17 14:45:32 +01:00
83cc04937c Fullfør oppgave 3.6: Sanntidstest verifisert
Backend-verifisering av sanntidsflyt (STDB CRUD) bestått:
- Node opprettelse, oppdatering, sletting via STDB HTTP API
- Edge opprettelse med kaskadesletting
- Data synkronisert korrekt mellom PG og STDB

Frontend klar for browser-test (to faner):
- Arkitekturen støtter sanntid: STDB WebSocket → onInsert → stores → $derived
- Lagt til console.log i stores for å spore sanntidshendelser
- Alle builds (svelte-check, vite build, cargo check) grønne

Testskript: scripts/test-sanntid.sh (8 backend-tester)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:40:18 +01:00
4d652d5816 Starter oppgave 3.6 2026-03-17 14:29:08 +01:00
0756e17069 Fullfør oppgave 3.5: TipTap-editor med create_node + owner-edge
Legger til owner-edge etter node-opprettelse slik at noden dukker opp
i brukerens mottak. Gjør API-klienten konfigurerbar via VITE_API_URL
for fremtidig produksjonsbruk. Legger til createEdge i API-klienten.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:26:59 +01:00
76b937466b Starter oppgave 3.5 2026-03-17 14:19:49 +01:00
08d2933788 Mottaksflaten v0: vis noder koblet til innlogget bruker (oppgave 3.4)
Henter brukerens node_id fra maskinrommets /me-endepunkt ved innlogging
(via Authentik access_token), cacher i JWT-sesjon. Mottaksflaten viser
alle noder brukeren har edges til (begge retninger), sortert nyeste først,
med tittel, utdrag, node_kind-merke og edge-type-merker.

Endringer:
- auth.ts: fetchNodeId() kaller maskinrommet /me ved sign-in
- app.d.ts: utvider JWT og Session med node_id/nodeId
- +page.svelte: erstatter dashboard med mottaksflate-visning
- .env.example: MASKINROMMET_URL for server-side API-kall

Krever at brukeren logger ut og inn igjen for å hente node_id.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:13:36 +01:00
3f81ac1865 Starter oppgave 3.4 2026-03-17 14:03:14 +01:00
a91d358263 STDB WebSocket-klient med reaktive Svelte-stores (oppgave 3.3)
Frontend kobler til SpacetimeDB via WebSocket og abonnerer på
node- og edge-tabellene. Data eksponeres som reaktive Svelte 5-stores
(runes) som oppdateres automatisk ved insert/update/delete.

Implementering:
- spacetimedb SDK (npm) + genererte TypeScript-bindings
- connection.svelte.ts: tilkoblingsmanager med reaktiv state
- stores.svelte.ts: nodeStore og edgeStore med sekundærindekser
  (bySource, byTarget, byKind, byType osv.)
- Layout initialiserer tilkobling ved autentisering
- Hjemmesiden viser tilkoblingsstatus og antall noder/edges
- .env.example med VITE_SPACETIMEDB_URL og VITE_SPACETIMEDB_MODULE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:55:37 +01:00
6829731525 Starter oppgave 3.3 2026-03-17 13:46:39 +01:00
6bc742cc8d Authentik OIDC login (oppgave 3.2)
Implementerer autentisering med Authentik via @auth/sveltekit:
- OIDC authorization code flow med PKCE og state-verifisering
- JWT-callback lagrer authentik_sub (SHA256-hash, ikke UUID) for
  konsistens med maskinrommets auth_identities-tabell
- Server hooks: alle ruter unntatt /signin og /auth/* krever sesjon
- Uautentiserte brukere redirectes til /signin (303)
- Innloggingsside med client-side signIn('authentik')
- Hovedside viser innlogget bruker med logg ut-knapp
- TypeScript-typer utvidet med JWT.authentik_sub

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:45:33 +01:00
073de4b148 Starter oppgave 3.2 2026-03-17 13:40:40 +01:00
d43365d5ea SvelteKit frontend-skjelett (oppgave 3.1)
Oppretter frontend/ med SvelteKit, TypeScript, TailwindCSS v4 og
adapter-node. PWA-manifest med SVG-ikon. Vite proxy til
api.sidelinja.org for lokal utvikling med HMR.

Oppdaterer docs/setup/lokal.md med riktige stier (frontend/ og
maskinrommet/ i stedet for web/ og rust/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:37:41 +01:00
5fafa06139 Starter oppgave 3.1 2026-03-17 13:32:41 +01:00
76f386ac84 Maskinrommet i Docker Compose (oppgave 2.6)
Maskinrommet kjører nå som Docker-service i server-stacken:
- Lagt til maskinrommet-service i docker-compose.yml på server
- Intern nettverkstilgang til PG (postgres:5432) og STDB (spacetimedb:3000)
- Caddy proxyer api.sidelinja.org → maskinrommet:3100
- Verifisert: health-endpoint, PG-tilkobling, STDB-tilkobling, warmup,
  auth-middleware (401 uten token)

Oppdatert docs/setup/produksjon.md med:
- Maskinrommet i service-oversikt og Caddyfile
- SpacetimeDB-variabler i .env-template
- Deploy-instruksjoner for bygging av Docker-image
- Verifiseringssjekkliste

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:32:11 +01:00
79477a24bf Starter oppgave 2.6 2026-03-17 13:26:36 +01:00
1e9a4c83c3 Flere intensjoner: create_edge, update_node, delete_node (oppgave 2.5)
Implementerer tre nye skrivestier i maskinrommet med tilgangskontroll:

- POST /intentions/create_edge — opprett retningsbestemt edge mellom
  to noder. Validerer at begge noder eksisterer og edge_type er satt.
- POST /intentions/update_node — partial update av eksisterende node.
  Kun oppgitte felter endres, resten beholdes fra PG.
- POST /intentions/delete_node — slett node med cascade av edges.

Tilgangskontroll for update/delete: brukeren må enten være created_by
på noden, eller ha en owner/admin-edge til den. Sjekkes mot PG som
autoritativ kilde.

Alle endepunkter følger samme mønster som create_node:
STDB-skriving (instant) → async PG-persistering → umiddelbar respons.

Verifisert på server med 10 testcaser:
1. /me med gyldig token → 200
2. create_node → 200 med node_id
3. create_edge (gyldig) → 200 med edge_id
4. create_edge (ugyldig source) → 400
5. create_edge (tom edge_type) → 400
6. update_node (partial, eier) → 200
7. update_node (ingen tilgang) → 403
8. delete_node (eier) → 200
9. delete_node (ingen tilgang) → 403
10. update via owner-edge (Sidelinja) → 200

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:25:16 +01:00