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

16 KiB

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)

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)

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)

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)

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)

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 svarmetadata.absorbed = true. Svaret kollapses visuelt, ikke slettet.
  • Kollapser utdaterte svarmetadata.collapsed = true. Alltid tilgjengelig med klikk.
  • Fest viktige svarmetadata.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 (COUNTreply_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.