diff --git a/.claude/settings.local.json b/.claude/settings.local.json index adf4a92..aee991a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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:*)" ] } } diff --git a/.gitignore b/.gitignore index 48b8375..5680e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Docker-volumer (flyktige, ikke i Git) .docker-data/ +# Scratch (testfiler, notater, midlertidig) +.scratch/ + # Miljovariabler .env.local .env diff --git a/migrations/0001_initial_schema.sql b/migrations/0001_initial_schema.sql new file mode 100644 index 0000000..06f60be --- /dev/null +++ b/migrations/0001_initial_schema.sql @@ -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;