PostgreSQL-skjema: kunnskapsgraf, meldinger, jobbkø og mediefiler

Første migrasjon (0001) med komplett databasearkitektur:
- nodes/graph_edges grafmodell med relasjonstyper
- Faktoider og meldinger med stemme-system (opp/ned)
- Chat med redigeringshistorikk, tråding og research-clips
- Content-addressable mediefiler (SHA-256 deduplisering)
- PostgreSQL-basert jobbkø med SKIP LOCKED
- Brukertabell som tynn cache fra Authentik

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-13 15:37:15 +01:00
parent 4b56560bf9
commit 2100184f4e
3 changed files with 225 additions and 1 deletions

View file

@ -59,7 +59,8 @@
"Bash(git config:*)",
"Bash(git remote:*)",
"Bash(ssh sidelinja@157.180.81.26 bash << 'REMOTE'\ncd /tmp\ngit clone ssh://git@git.sidelinja.org:222/sidelinja/sidelinja.git test-clone 2>&1\nls test-clone/\nrm -rf test-clone\necho \"DONE\"\nREMOTE)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(git commit:*)"
]
}
}

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
# Docker-volumer (flyktige, ikke i Git)
.docker-data/
# Scratch (testfiler, notater, midlertidig)
.scratch/
# Miljovariabler
.env.local
.env

View file

@ -0,0 +1,220 @@
-- Sidelinja: Initial database schema
-- Kunnskapsgraf, jobbkø, meldinger, mediefiler
--
-- Kjøres mot en tom PostgreSQL-database.
-- Krever: PostgreSQL 15+ med pgcrypto (gen_random_uuid)
BEGIN;
-- ============================================================
-- Extensions
-- ============================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- uuid_generate_v5() for deterministiske segment-UUIDs
-- ============================================================
-- 1. Brukere (tynn cache fra Authentik)
-- ============================================================
CREATE TABLE users (
authentik_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
avatar_url TEXT,
cached_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 2. Nodes — supertabell for alle grafentiteter
-- ============================================================
CREATE TYPE node_type AS ENUM (
'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding'
);
CREATE TABLE nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_type node_type NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 3. Relasjonstyper (seeded + utvidbar)
-- ============================================================
CREATE TABLE relation_types (
name TEXT PRIMARY KEY,
label TEXT NOT NULL,
description TEXT,
system BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO relation_types (name, label, description, system) VALUES
('MENTIONS', 'Nevner', 'Refererer til denne aktøren/temaet', true),
('ABOUT', 'Handler om', 'Faktoide eller klipp som omhandler', true),
('DISCUSSED_IN', 'Diskutert i', 'Tema diskutert i dette segmentet', true),
('WORKS_FOR', 'Jobber for', 'Person tilknyttet organisasjon', true),
('CONTRADICTS', 'Motsier', 'Står i motsetning til', true),
('PART_OF', 'Del av', 'Tilhører / er underordnet', true);
-- ============================================================
-- 4. Graph edges
-- ============================================================
CREATE TABLE graph_edges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL REFERENCES relation_types(name),
context_id UUID REFERENCES nodes(id) ON DELETE SET NULL,
confidence REAL CHECK (confidence BETWEEN 0.0 AND 1.0),
created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL,
origin TEXT NOT NULL DEFAULT 'system',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT no_self_reference CHECK (source_id != target_id)
);
CREATE INDEX idx_edges_source ON graph_edges(source_id);
CREATE INDEX idx_edges_target ON graph_edges(target_id);
CREATE INDEX idx_edges_relation ON graph_edges(relation_type);
-- ============================================================
-- 5. Detailtabeller — aktører, temaer, episoder, segmenter
-- ============================================================
CREATE TABLE actors (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT -- 'person', 'organisasjon', etc.
);
CREATE TABLE topics (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
name TEXT NOT NULL
);
CREATE TABLE episodes (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
title TEXT NOT NULL,
published_at TIMESTAMPTZ,
guid TEXT UNIQUE NOT NULL
);
CREATE TABLE segments (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE,
start_time INTERVAL NOT NULL,
end_time INTERVAL NOT NULL,
transcript TEXT,
CONSTRAINT valid_timerange CHECK (end_time > start_time)
);
CREATE INDEX idx_segments_episode ON segments(episode_id);
CREATE INDEX idx_segments_fts ON segments USING GIN (to_tsvector('norwegian', transcript));
-- ============================================================
-- 6. Faktoider
-- ============================================================
CREATE TABLE factoids (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
body TEXT NOT NULL,
source_url TEXT,
created_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE factoid_votes (
factoid_id UUID NOT NULL REFERENCES factoids(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE,
vote SMALLINT NOT NULL CHECK (vote IN (-1, 1)),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (factoid_id, user_id)
);
-- ============================================================
-- 7. Meldinger (chat)
-- ============================================================
CREATE TYPE message_type AS ENUM (
'text', -- Vanlig Markdown-melding
'research_clip', -- Innlimt artikkel (Ctrl+A)
'factoid', -- Faktoid delt i chat
'system' -- Automatisk systemmelding
);
CREATE TABLE messages (
id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
topic_id UUID NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
reply_to UUID REFERENCES messages(id) ON DELETE SET NULL,
author_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL,
message_type message_type NOT NULL DEFAULT 'text',
body TEXT NOT NULL,
metadata JSONB,
edited_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_messages_topic ON messages(topic_id, created_at);
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)
);
CREATE TABLE message_votes (
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE,
vote SMALLINT NOT NULL CHECK (vote IN (-1, 1)),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (message_id, user_id)
);
-- ============================================================
-- 8. Mediefiler (content-addressable)
-- ============================================================
CREATE TABLE media_files (
sha256 TEXT PRIMARY KEY,
mime_type TEXT NOT NULL,
file_size BIGINT NOT NULL,
uploaded_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE message_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
sha256 TEXT NOT NULL REFERENCES media_files(sha256),
sort_order SMALLINT NOT NULL DEFAULT 0
);
-- ============================================================
-- 9. Jobbkø
-- ============================================================
CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry');
CREATE TABLE job_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_type TEXT NOT NULL,
payload JSONB NOT NULL,
status job_status NOT NULL DEFAULT 'pending',
priority SMALLINT NOT NULL DEFAULT 0,
result JSONB,
error_msg TEXT,
attempts SMALLINT NOT NULL DEFAULT 0,
max_attempts SMALLINT NOT NULL DEFAULT 3,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC)
WHERE status IN ('pending', 'retry');
COMMIT;