# 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. 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 workspace-admin 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/workspace). `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 ``` Workspace-default TTL: 30 dager (workspaces.settings.default_ttl_days) └── Channel kan overstyre: config.ttl_days └── Individuelle meldinger frittes via reglene over ``` ## 10. `` 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 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.