Oppdatert basert på ekstern tilbakemelding. Nye proposals for kildevern, podcasting 2.0, web clipper, waveforms, editor, tekst-primitiv og avisvisning. Oppdatert meldingsboks med slette-semantikk, entity resolution i kunnskapsgrafen, og AI gateway med kildevern-modus. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
319 lines
17 KiB
Markdown
319 lines
17 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. 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. `<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 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.
|