server/docs/features/meldingsboks.md
vegard 1faef972dd Meldingsboks-migrasjon: universell diskusjonsprimitiv + entities
Migrering 0005 samler kanban-kort, kalenderhendelser, faktoider og
notater til én felles messages-tabell med view-config-tabeller.
Actors og topics erstattes av unified entities-tabell.

- 0005_meldingsboks.sql: messages utvides med title/pinned/visibility,
  kanban_card_view + calendar_event_view + message_reactions opprettes,
  entities erstatter actors+topics, gamle tabeller droppes
- seed_dev.sql: oppdatert til meldingsboks-modell + 5 test-entiteter
  med graf-relasjoner
- API-ruter: kanban/kalender/notater bruker messages + view-config
- Dokumentasjon: meldingsboks feature-spec, oppdatert arkitektur,
  kunnskapsgraf, jobbkø, konseptdokumenter og proposals

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 15:32:15 +01:00

305 lines
16 KiB
Markdown

# Feature: Meldingsboks — universell diskusjonsprimitiv
**Filsti:** `docs/features/meldingsboks.md`
## 1. Konsept
Meldingsboksen er den sentrale byggeklossen for alt ustrukturert innhold i Sidelinja. Én datamodell som erstatter separate tabeller for chat-meldinger, kanban-kort, kalenderhendelser, faktoider og notater. Samme objekt, samme diskusjon, vist i flere kontekster via view-config.
### 1.1 Hva meldingsboksen erstatter
| Tidligere modell | Egen tabell | Blir nå |
|---|---|---|
| Chat-melding | `messages` | Meldingsboks (node) |
| Kanban-kort | `kanban_cards` | Meldingsboks + `kanban_card_view` |
| Kalenderhendelse | `calendar_events` | Meldingsboks + `calendar_event_view` |
| Faktoide | `factoids` | Meldingsboks med `ABOUT`-edge |
| Notat | `notes` | Meldingsboks med tittel |
### 1.2 Hva som IKKE er meldingsbokser
Typed nodes med strukturelt unike skjemaer forblir egne detailtabeller:
| Type | Hvorfor egen tabell |
|---|---|
| Entitet | `name`, `type`, `aliases`, `avatar_url` — autocomplete, Whisper-prompt, autoritativ navngiving |
| Episode | `title`, `guid` (immutabel RSS-krav), `published_at` |
| Segment | `episode_id`, `start_time`/`end_time`, `transcript`, FTS-indeks |
**Entiteter** (erstatter `actors` + `topics`) er alt som kan nevnes med `#`: personer, organisasjoner, steder, temaer, konsepter. Se `docs/features/kunnskapsgraf_og_relasjoner.md` §3.2.
Typed nodes kan **kobles til meldingsbokser** via edges for diskusjon. En entitet har ikke innebygd diskusjon, men en meldingsboks kan knyttes til den med en `DISCUSSED_IN`-edge.
## 2. Alle meldinger er noder
Hver meldingsboks er en fullverdig node i kunnskapsgrafen (`node_type = 'melding'`). Ingen vektklasser, ingen promoteringslogikk. Opprettelse er alltid: `INSERT INTO nodes` + `INSERT INTO messages` i én transaksjon.
**Hvorfor:** De fleste meldinger i en aktiv redaksjon ender opp med å trenge graf-tilkobling uansett (mentions, svar, stemmer). Promoteringslogikk legger til kompleksitet uten reell gevinst. `nodes`-tabellen tåler volumet — TTL rydder opp i flyktige meldinger, og `node_type`-filter sikrer at spørringer aldri treffer hele tabellen.
**Konsekvens:** Ethvert svar er en rik entitet som kan kobles til kanban, kalender, graf — full fleksibilitet uten spesialtilfeller. Et svar på tredje nivå i en diskusjon kan bli en kalenderoppføring, og konteksten forsvinner ikke.
## 3. Datamodell
### 3.1 Messages (erstatter `messages`, `kanban_cards`, `calendar_events`, `factoids`, `notes`)
```sql
CREATE TABLE messages (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
channel_id UUID REFERENCES nodes(id) ON DELETE CASCADE,
reply_to UUID REFERENCES messages(id) ON DELETE SET NULL,
author_id TEXT REFERENCES users(authentik_id) ON DELETE SET NULL,
message_type message_type NOT NULL DEFAULT 'text',
title TEXT, -- Kanban-kort, notater, kalenderhendelser, faktoider
body TEXT NOT NULL,
metadata JSONB, -- Ekstra data per message_type
pinned BOOLEAN NOT NULL DEFAULT false,
visibility TEXT NOT NULL DEFAULT 'workspace', -- 'workspace' | 'private'
edited_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_messages_channel ON messages(channel_id, created_at);
CREATE INDEX idx_messages_reply ON messages(reply_to) WHERE reply_to IS NOT NULL;
CREATE TRIGGER trg_messages_updated_at BEFORE UPDATE ON messages
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
```
**Forskjeller fra gammel `messages`-tabell:**
- `id` er FK til `nodes(id)`**alle meldinger er noder** i kunnskapsgrafen
- Ingen `workspace_id` — arves via `nodes.workspace_id` (RLS på nodes gjelder)
- `channel_id` er nullable — notater og standalone-bokser trenger ikke en channel
- `title` er førsteklasses felt — brukes av kanban-kort, notater, kalenderhendelser
- `pinned` for manuell fritak fra TTL-sletting
- `visibility` styrer synlighet — `'workspace'` (alle i workspacet) eller `'private'` (kun forfatter). Private meldingsbokser kan brukes som kladd for notater, kanban-kort og kalenderhendelser. Endre til `'workspace'` for å dele.
### 3.1.1 Kontekst-navigering via `reply_to`
Når en meldingsboks har roller i flere kontekster (f.eks. et svar som også er en kalenderoppføring), gir `reply_to`-kjeden alltid vei tilbake til opprinnelig diskusjon:
```
📅 Kalenderen: "Planleggingsmøte for konferanse"
↩ Fra diskusjon i #Mediepolitikk → "Vi burde kanskje..."
```
UI-et følger `reply_to` → forelder → `channel_id` → parent node for å bygge brødsmulesti. Ingen ekstra data nødvendig — `reply_to` + view-config gir hele bildet i begge retninger:
- **Fra kalenderen:** "Hvor kom denne fra?" → følg `reply_to` oppover
- **Fra chatten:** "Hva ble dette?" → se at svaret har `calendar_event_view` → vis kalender-badge
### 3.2 Kanban view-config (erstatter `kanban_cards`)
```sql
CREATE TABLE kanban_card_view (
message_id UUID PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
column_id UUID NOT NULL REFERENCES kanban_columns(id) ON DELETE CASCADE,
position REAL NOT NULL DEFAULT 0,
color TEXT,
assignee_id TEXT REFERENCES users(authentik_id) ON DELETE SET NULL
);
CREATE INDEX idx_kanban_card_view_column ON kanban_card_view(column_id, position);
```
Et kanban-kort er en meldingsboks + en rad i `kanban_card_view`. Tittel og beskrivelse lever i `messages.title`/`messages.body`. Flytt mellom kolonner = `UPDATE kanban_card_view SET column_id = ...`.
`kanban_boards` og `kanban_columns` forblir uendret — de er strukturelle tabeller, ikke grafnoder (kolonner er intern organisering).
### 3.3 Kalender view-config (erstatter `calendar_events`)
```sql
CREATE TABLE calendar_event_view (
message_id UUID PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,
calendar_id UUID NOT NULL REFERENCES calendars(id) ON DELETE CASCADE,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ,
all_day BOOLEAN NOT NULL DEFAULT false,
color TEXT
);
CREATE INDEX idx_calendar_event_view_calendar ON calendar_event_view(calendar_id, starts_at);
```
`calendars`-tabellen forblir uendret.
### 3.4 Reaksjoner (erstatter `factoid_votes` og `message_votes`)
```sql
CREATE TABLE message_reactions (
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE,
reaction TEXT NOT NULL, -- 'upvote', 'downvote', '👍', '🔥', etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (message_id, user_id, reaction)
);
```
Én tabell for alle interaksjoner — opp/ned-stemmer (`'upvote'`/`'downvote'`) og emoji-reaksjoner (`'👍'`, `'🔥'`) i samme modell. Ikke graf-edges — reaksjoner er høyfrekvente, lav-semantiske operasjoner.
Sortering etter stemmer: `SELECT COUNT(*) FILTER (WHERE reaction = 'upvote') - COUNT(*) FILTER (WHERE reaction = 'downvote') AS score`.
### 3.5 Message revisions (uendret)
```sql
CREATE TABLE message_revisions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
body TEXT NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT revision_order UNIQUE (message_id, edited_at)
);
```
### 3.6 Nye relasjonstyper
Eksisterende relasjonstyper dekker behovene — ingen nye nødvendig:
- `DISCUSSED_IN` — kobler typed nodes til meldingsbokser (diskusjon på en aktør/tema)
- `ABOUT` — kobler faktoide-meldinger til aktører/temaer
- `MENTIONS` — chat-mentions via `#`-tags
### 3.7 node_type enum (opprydding)
Aktive verdier etter migrering:
- `'entitet'` — person, organisasjon, sted, tema, konsept (erstatter `'aktør'` og `'tema'`)
- `'episode'`, `'segment'` — typed nodes
- `'melding'` — meldingsboks (chat, kanban-kort, kalenderhendelse, faktoide, notat)
- `'channel'` — gruppering av meldinger
- `'kanban_board'`, `'calendar'`, `'meeting'` — strukturelle
Utfasede verdier (kan ikke fjernes fra PG ENUM, men skal aldri brukes i ny kode):
`'aktør'`, `'tema'`, `'faktoide'`, `'note'`, `'kanban_card'`, `'calendar_event'`
## 4. Multi-rolle via view-config
Konvertering = legg til view-config. Ingen data flyttes:
```
Meldingsboks (messages + nodes)
├── kanban_card_view → vises på kanban-brettet
├── calendar_event_view → vises i kalenderen
├── edge: ABOUT → aktør/tema → vises som faktoide
├── edge: DISCUSSED_IN ← typed node → diskusjon på aktør/tema/episode
├── reply_to → parent melding → tråd-kontekst
└── reply_to ← svar → diskusjonstråd følger med i alle kontekster
```
En meldingsboks kan ha **flere roller samtidig**: et kanban-kort som også er en kalenderhendelse, med en diskusjonstråd under seg. Svar på meldingsboksen følger med uansett kontekst — diskusjonen er alltid tilgjengelig.
## 5. Channels
Channels forblir som grupperingsmekanisme. En channel samler meldingsbokser under ett scope — typisk knyttet til en typed node (tema, episode, møte).
Channels **opprettes ved behov**, ikke automatisk for alle noder. Når en bruker starter en diskusjon på en aktør, opprettes en channel i samme transaksjon.
Channel-config (`threads`, `mentions`, `attachments`, `ttl_days`) arves fra kontekst (se `docs/features/chat.md` §2.2).
## 6. Nesting og utskilling
Maks 3 nivåer visuelt (via `reply_to`-kjeding):
1. Boks (trådstart)
2. Svar på boks
3. Svar på svar
Ved nivå 3 tilbyr systemet: **"Skill ut som egen diskusjon?"**
- Svaret promoteres til boks (ny node)
- Originaltråden får en lenke-melding: "→ Diskusjonen fortsetter her"
- Den nye boksen lever sitt eget liv
## 7. Eierskap, kurasjon og prominens
### 7.1 Eierskap
Trådstarter og workspace-admin deler eierskap over en diskusjonstråd. Eierskap gir tilgang til kurasjonsverktøy (se 7.2).
### 7.2 Kurasjon (TODO — UI-features, bygges inkrementelt)
Datamodellen trenger ikke endres for disse — alt håndteres via `messages.metadata` (JSONB):
- **Absorber svar** — `metadata.absorbed = true`. Svaret kollapses visuelt, ikke slettet.
- **Kollapser utdaterte svar** — `metadata.collapsed = true`. Alltid tilgjengelig med klikk.
- **Fest viktige svar** — `metadata.featured = true`. Vises prominent i lang tråd.
### 7.3 Prominens (avledet, ikke lagret)
Hvor viktig en meldingsboks er, beregnes fra eksisterende data — aldri lagret som en score:
- Antall svar (`COUNT``reply_to`)
- Stemmer (`SUM` fra `message_votes`)
- Antall roller (finnes i `kanban_card_view`, `calendar_event_view`?)
- Graf-koblinger (`COUNT` fra `graph_edges`)
- Alder på siste aktivitet (`MAX(created_at)` fra svar)
Beregnes ved visning eller caches i materialized view ved behov. Algoritmen kan justeres uten migrasjoner eller skjemaendringer.
## 8. TTL og livsløp
### 8.1 To-trinns fading
1. **Skjult fra visning** — meldingen forsvinner fra default UI etter TTL (arvet fra channel/workspace). `messages.metadata.hidden_at` settes.
2. **Slettet** — etter tilleggsperiode (dobbel TTL) fjernes raden permanent.
### 8.2 Alder som dynamisk faktor
Tid bidrar til utfasing — eldre meldinger uten aktivitet eller koblinger fader naturlig. Prominens-scoren (§7.3) synker med alder, og TTL-jobben bruker den til å avgjøre hva som skjules og slettes.
### 8.3 Fritak-regler
En melding slettes **ikke** hvis:
- Den har **graf-edge(s)** (`ABOUT`, `MENTIONS`, `DISCUSSED_IN`, etc.) — koblet til noe varig i kunnskapsgrafen. Dette er det som gjør faktoider immune: en `ABOUT`-edge til en aktør/tema betyr at informasjonen har verdi utover konteksten den ble skrevet i.
- Den har `kanban_card_view`-rad i en aktiv kolonne
- Den har `calendar_event_view`-rad med fremtidig tidspunkt
- Den har aktive svar (siste svar innenfor TTL)
- `pinned = true`
**Prinsippet:** Grafen bestemmer hva som er varig. Ingen spesialhåndtering for faktoider — enhver melding med en graf-kobling overlever. Meldinger uten koblinger fader med tid.
Lever boksen, lever alt under den — svar beholdes uansett alder.
### 8.4 Konfigurerbarhet
```
Workspace-default TTL: 30 dager (workspaces.settings.default_ttl_days)
└── Channel kan overstyre: config.ttl_days
└── Individuelle meldinger frittes via reglene over
```
## 9. `<MessageBox>` Svelte-komponent
Én komponent som rendrer en meldingsboks i alle kontekster:
- **Kompakt modus** — kanban: tittel + "3 svar" + fargekode
- **Kalender-modus** — tittel + tidspunkt + fargekode
- **Utvidet modus** — full diskusjon med innrykk (maks 3 nivåer)
- Leser kontekst og tilpasser capabilities (stemmer, mentions, vedlegg)
- Lazy-loader tråd ved expand (ytelse)
## 10. Konsekvenser for eksisterende kode
### 10.1 Tabeller som fjernes
- `kanban_cards` → erstattes av `kanban_card_view`
- `calendar_events` → erstattes av `calendar_event_view`
- `factoids` + `factoid_votes` → erstattes av `messages` + `message_reactions`
- `message_votes` → erstattes av `message_reactions`
- `notes` → erstattes av `messages` med tittel
### 10.2 Tabeller som forblir uendret
- `kanban_boards`, `kanban_columns` — strukturelle
- `calendars` — strukturell
- `channels` — gruppering
- `message_revisions` — revisjonshistorikk
- `message_attachments`, `media_files` — vedlegg
### 10.3 API-endringer
Eksisterende API-ruter (`/api/kanban/`, `/api/calendar/`, `/api/notes/`) refaktoreres til å bruke den nye datamodellen. Grensesnittet mot frontend kan holdes stabilt — endringene er i datalaget.
## 11. Migrering (0005_meldingsboks.sql)
Migrasjonen konverterer eksisterende data:
1. **Endre `messages`-tabellen:** Legg til `title`, `pinned`. `id → nodes(id)` beholdes (alle meldinger er allerede noder).
2. **Migrer kanban-kort:** For hver `kanban_cards`-rad: opprett `messages`-rad (med `title`, `body` fra description, gjenbruk eksisterende node-id) + `kanban_card_view`-rad.
3. **Migrer kalenderhendelser:** For hver `calendar_events`-rad: opprett `messages`-rad + `calendar_event_view`-rad.
4. **Migrer faktoider:** For hver `factoids`-rad: opprett `messages`-rad (med `body`, `message_type = 'factoid'`). Flytt `factoid_votes` til `message_votes`. Opprett `ABOUT`-edges i `graph_edges`.
5. **Migrer notater:** For hver `notes`-rad: opprett `messages`-rad (med `title`, `body` fra content).
6. **Drop gamle tabeller:** `kanban_cards`, `calendar_events`, `factoids`, `factoid_votes`, `notes`.
**Merk:** Migrasjonen bør ha en tilhørende down-migrering som gjenskaper de gamle tabellene og flytter data tilbake.
## 12. Instruks for Claude Code
- **Opprettelse av meldingsboks:** Alltid INSERT i `nodes` (type 'melding') + INSERT i `messages` med samme id. Alt i én transaksjon.
- **Kanban-kort:** INSERT i `nodes` + `messages` (med tittel) + `kanban_card_view`. Én transaksjon.
- **Kalenderhendelse:** INSERT i `nodes` + `messages` (med tittel) + `calendar_event_view`. Én transaksjon.
- **Faktoide:** INSERT i `nodes` + `messages` (`message_type = 'factoid'`) + `graph_edges` med `ABOUT`-relasjon til aktør/tema. Én transaksjon.
- **Notat:** INSERT i `nodes` + `messages` (med tittel + body). `channel_id` peker på en personal/workspace channel eller er NULL.
- **Konvertering mellom roller:** Legg til view-config-rad (kanban/kalender). Aldri kopier data.
- **Kontekst-navigering:** Bruk `reply_to`-kjeden for å bygge brødsmulesti tilbake til opprinnelig diskusjonskontekst.
- **TTL:** Implementér som nattlig jobbkø-jobb (`message_ttl_cleanup`). Sjekk fritak-regler før sletting. Ved sletting: slett fra `messages` → cascade til `nodes` via FK.
- **RLS:** Workspace-isolasjon arves fra `nodes.workspace_id`. Visibility håndteres i applikasjonskode (SvelteKit): `WHERE visibility = 'workspace' OR author_id = $current_user`. Ikke i RLS — RLS håndterer kun workspace-grenser.
- **Visibility:** Default `'workspace'`. Sett `'private'` for personlige kladder. Endre til `'workspace'` for å dele — ingen kopiering nødvendig.