-- Reports page foundation. Part of the locked /reports page design -- (docs/reports-page-design.md). Three pieces: -- 1. Extend the existing report_templates with visibility + archive flag. -- 2. Append-only report_runs audit log. -- 3. report_schedules driving the BullMQ recurring scheduler. -- ─── 1. Extend report_templates ───────────────────────────────────────────── ALTER TABLE report_templates ADD COLUMN IF NOT EXISTS description text, ADD COLUMN IF NOT EXISTS visibility text NOT NULL DEFAULT 'private', ADD COLUMN IF NOT EXISTS archived_at timestamptz; CREATE INDEX IF NOT EXISTS idx_report_templates_port_visibility ON report_templates(port_id, visibility); -- Soften the unique-name constraint so archived templates don't block reuse -- of a name. Drop the existing index + recreate with a WHERE clause. DROP INDEX IF EXISTS uniq_report_templates_port_kind_name; CREATE UNIQUE INDEX uniq_report_templates_port_kind_name ON report_templates(port_id, kind, LOWER(name)) WHERE archived_at IS NULL; -- ─── 2. report_runs (append-only audit log) ───────────────────────────────── CREATE TABLE IF NOT EXISTS report_runs ( id text PRIMARY KEY DEFAULT gen_random_uuid()::text, port_id text NOT NULL REFERENCES ports(id) ON DELETE CASCADE, -- Nullable: ad-hoc runs (no template) still get logged. template_id text REFERENCES report_templates(id) ON DELETE SET NULL, -- Forward-declared FK; populated once report_schedules is created below. schedule_id text, kind text NOT NULL, -- Snapshotted at run time so re-runs reproduce identically even if the -- source template has since been edited / archived. config jsonb NOT NULL, output_format text NOT NULL, -- 'pdf' | 'csv' | 'png' | 'jpg' -- Storage key of the rendered artefact. Same backend as files (s3 or fs). storage_key text, size_bytes integer, status text NOT NULL DEFAULT 'pending', -- pending | rendering | complete | failed error_message text, triggered_by text NOT NULL, -- 'user' | 'schedule' triggered_by_user_id text REFERENCES "user"(id) ON DELETE SET NULL, -- When non-null, this run was emailed to these recipients on completion. emailed_to jsonb, -- Array<{ name?: string, email: string }> emailed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), completed_at timestamptz ); CREATE INDEX IF NOT EXISTS report_runs_port_created_idx ON report_runs(port_id, created_at DESC); CREATE INDEX IF NOT EXISTS report_runs_port_user_idx ON report_runs(port_id, triggered_by_user_id); CREATE INDEX IF NOT EXISTS report_runs_port_template_idx ON report_runs(port_id, template_id) WHERE template_id IS NOT NULL; -- ─── 3. report_schedules (BullMQ recurring scheduler) ──────────────────────── CREATE TABLE IF NOT EXISTS report_schedules ( id text PRIMARY KEY DEFAULT gen_random_uuid()::text, port_id text NOT NULL REFERENCES ports(id) ON DELETE CASCADE, template_id text NOT NULL REFERENCES report_templates(id) ON DELETE CASCADE, -- Enum cadence at v1; cron strings can layer on top later. cadence text NOT NULL, recipients jsonb NOT NULL, -- Array<{ name?: string, email: string }> output_format text NOT NULL DEFAULT 'pdf', enabled boolean NOT NULL DEFAULT true, last_run_at timestamptz, next_run_at timestamptz NOT NULL, created_by text NOT NULL REFERENCES "user"(id) ON DELETE RESTRICT, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS report_schedules_port_enabled_next_idx ON report_schedules(port_id, enabled, next_run_at); -- Now wire the forward-declared schedule_id FK from report_runs back to the -- schedules table. ALTER TABLE report_runs ADD CONSTRAINT report_runs_schedule_id_fkey FOREIGN KEY (schedule_id) REFERENCES report_schedules(id) ON DELETE SET NULL;