# 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 'shared',  -- 'shared' | '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` — tilgang styres via `node_access`-matrisen
- `channel_id` er nullable — notater og standalone-bokser trenger ikke en channel
- `title` er førsteklasses felt — brukes av kanban-kort, notater, kalenderhendelser
- `pinned` for manuell fritak fra TTL-sletting
- `visibility` styrer synlighet — `'shared'` (alle med tilgang via `node_access`) eller `'private'` (kun forfatter). Private meldingsbokser kan brukes som kladd for notater, kanban-kort og kalenderhendelser. Endre til `'shared'` for å dele.

### 3.1.1 Kontekst-navigering via `reply_to`

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. Visning i flere kontekster

En node kan vises i flere verktøy samtidig via edges og view-configs.
For lettvekts-triage (node → kanban, node → kalender) legges en edge/
view-config til. For innholdstransformasjon (chatboble → artikkel)
opprettes en ny node med `source_material`-edge. Se
[arbeidsflaten](../retninger/arbeidsflaten.md).

Multi-visning via view-config (samme node, flere kontekster):

```
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. Slette-semantikk: "Fjern" vs "Slett"

En meldingsboks kan ha flere roller (kanban-kort, kalenderhendelse, diskusjonstråd). UI-et må ha et skarpt, eksplisitt skille:

| Handling | Hva skjer | Konsekvens |
|---|---|---|
| **Fjern fra brett/kalender** | `DELETE FROM kanban_card_view` / `calendar_event_view` | Meldingen lever videre i chatten og grafen. Kun visningen forsvinner. |
| **Slett innhold** | `DELETE FROM messages` → cascader til `nodes` | Alt borte: diskusjonstråd, view-configs, graf-edges. Irreversibelt. |

**UI-regler:**
- "Fjern fra brett" / "Fjern fra kalender" — standardhandling i kontekstmeny på kanban/kalender. Trygt.
- "Slett permanent" — bak en bekreftelsesdialog ("Denne meldingen har 3 svar og er koblet til 2 entiteter. Slett alt?"). Viser konsekvensene eksplisitt.
- En melding med aktive roller i andre views bør vise en advarsel: "Denne meldingen er også et kanban-kort i [brett]. Fjern fra brettet først, eller slett alt?"

## 7. 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

## 8. Eierskap, kurasjon og prominens

### 8.1 Eierskap
Trådstarter og admin for samlings-noden deler eierskap over en diskusjonstråd. Eierskap gir tilgang til kurasjonsverktøy (se 7.2).

### 8.2 Kurasjon (TODO — UI-features, bygges inkrementelt)
Datamodellen trenger ikke endres for disse — alt håndteres via `messages.metadata` (JSONB):
- **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.

### 8.3 Prominens (avledet, ikke lagret)
Hvor viktig en meldingsboks er, beregnes fra eksisterende data — aldri lagret som en score:
- Antall svar (`COUNT` på `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.

## 9. TTL og livsløp

### 9.1 To-trinns fading
1. **Skjult fra visning** — meldingen forsvinner fra default UI etter TTL (arvet fra channel eller samlings-node). `messages.metadata.hidden_at` settes.
2. **Slettet** — etter tilleggsperiode (dobbel TTL) fjernes raden permanent.

### 9.2 Alder som dynamisk faktor
Tid bidrar til utfasing — eldre meldinger uten aktivitet eller koblinger fader naturlig. Prominens-scoren (§8.3) synker med alder, og TTL-jobben bruker den til å avgjøre hva som skjules og slettes.

### 9.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.

### 9.4 Konfigurerbarhet
```
Samlings-node default TTL: 30 dager (samlings-node metadata.default_ttl_days)
  └── Channel kan overstyre: config.ttl_days
        └── Individuelle meldinger frittes via reglene over
```

## 10. `<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)

## 11. Konsekvenser for eksisterende kode

### 11.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

### 11.2 Tabeller som forblir uendret
- `kanban_boards`, `kanban_columns` — strukturelle
- `calendars` — strukturell
- `channels` — gruppering
- `message_revisions` — revisjonshistorikk
- `message_attachments`, `media_files` — vedlegg

### 11.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.

## 12. 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.

## 13. 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 personlig eller delt channel, eller er NULL.
- **Visning i ny kontekst:** Legg til view-config-rad (kanban/kalender) for lettvekts-triage. For innholdstransfer: ny node med `source_material`-edge.
- **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.
- **Tilgang:** Styres via `node_access`-matrisen. Visibility håndteres i applikasjonskode (SvelteKit): `WHERE visibility = 'shared' OR author_id = $current_user`.
- **Visibility:** Default `'shared'`. Sett `'private'` for personlige kladder. Endre til `'shared'` for å dele — ingen kopiering nødvendig.
