Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
93
apps/api/migrations/020_analytics.sql
Normal file
93
apps/api/migrations/020_analytics.sql
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
-- 020: WP3 analytics snapshots — monthly per-workspace rollups.
|
||||
--
|
||||
-- One row per (workspace, month), refreshed in place by the snapshot writer
|
||||
-- (apps/api/src/analytics/snapshot.service.ts) with set-based SQL and an
|
||||
-- idempotent upsert on (workspace_id, month). Months are materialized as
|
||||
-- range partitions of a `month date` key, mirroring 014_live_chat.sql.
|
||||
--
|
||||
-- Creating future partitions: RLS on the partitioned parent does NOT apply to
|
||||
-- direct leaf access, so every leaf carries its own ENABLE/FORCE RLS and
|
||||
-- tenant_isolation policy. `analytics_ensure_month(date)` creates a month's
|
||||
-- partition idempotently with those controls; this migration precreates the
|
||||
-- previous, current and next month. Administrators run
|
||||
-- SELECT analytics_ensure_month(date '2025-09-01');
|
||||
-- ahead of each new month (cron it monthly, one statement, no downtime). The
|
||||
-- DEFAULT partition below catches any month whose partition was not created,
|
||||
-- so a missed call never drops data — it only loses pruning until the
|
||||
-- partition is created and the rows moved into it.
|
||||
--
|
||||
-- Partitioned-leaf register entry (proposal §4.4, exception (a)): PK carries
|
||||
-- the partition key, the table is never the target of an FK, and rows are
|
||||
-- referenced only by (workspace_id, month) / id soft links.
|
||||
--
|
||||
-- Query catalog (owning WP: WP3 — packages/db does not exist in this repo, so
|
||||
-- the catalog entries live here and in snapshot.service.ts):
|
||||
-- analytics.snapshot.refresh INSERT .. ON CONFLICT (workspace_id, month) —
|
||||
-- served by the PK (workspace_id, month, id); write path, p95 <= 250 ms.
|
||||
-- analytics.snapshot.list keyset page ORDER BY month DESC — same PK,
|
||||
-- backward index scan, p95 <= 20 ms.
|
||||
-- analytics.snapshot.summary two PK point lookups (current + previous
|
||||
-- month), p95 <= 20 ms.
|
||||
CREATE TABLE analytics_snapshots (
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
-- Always a month start; the CHECK keeps the value aligned with its partition.
|
||||
month date NOT NULL CHECK (month = date_trunc('month', month)::date),
|
||||
publications_total bigint NOT NULL DEFAULT 0 CHECK (publications_total >= 0),
|
||||
-- {provider: count} for publications published in the month.
|
||||
publications_by_provider jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(publications_by_provider) = 'object'),
|
||||
posts_archived_total bigint NOT NULL DEFAULT 0 CHECK (posts_archived_total >= 0),
|
||||
-- Point-in-time current state at snapshot time (not a monthly delta):
|
||||
-- connections not disconnected when the snapshot was taken.
|
||||
active_connections int NOT NULL DEFAULT 0 CHECK (active_connections >= 0),
|
||||
drafts_created_total bigint NOT NULL DEFAULT 0 CHECK (drafts_created_total >= 0),
|
||||
publishing_attempts_total bigint NOT NULL DEFAULT 0 CHECK (publishing_attempts_total >= 0),
|
||||
-- Attempts whose terminal status is 'failed', counted by their creation month.
|
||||
publish_failures_total bigint NOT NULL DEFAULT 0 CHECK (publish_failures_total >= 0),
|
||||
snapshot_started_at timestamptz NOT NULL DEFAULT now(),
|
||||
snapshot_completed_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (workspace_id, month, id),
|
||||
UNIQUE (workspace_id, month)
|
||||
) PARTITION BY RANGE (month);
|
||||
|
||||
-- Idempotent: safe to re-run for the same month.
|
||||
CREATE FUNCTION analytics_ensure_month(p_month date) RETURNS void
|
||||
LANGUAGE plpgsql SET search_path = public AS $$
|
||||
DECLARE v_start date := date_trunc('month', p_month)::date;
|
||||
v_table text := 'analytics_snapshots_' || to_char(v_start, 'YYYYMM');
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF analytics_snapshots FOR VALUES FROM (%L) TO (%L)',
|
||||
v_table, v_start, (v_start + interval '1 month')::date);
|
||||
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', v_table);
|
||||
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', v_table);
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = v_table AND policyname = 'tenant_isolation') THEN
|
||||
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (workspace_id = NULLIF(current_setting(''app.workspace_id'', true), '''')::uuid) WITH CHECK (workspace_id = NULLIF(current_setting(''app.workspace_id'', true), '''')::uuid)', v_table);
|
||||
END IF;
|
||||
END $$;
|
||||
REVOKE ALL ON FUNCTION analytics_ensure_month(date) FROM PUBLIC;
|
||||
|
||||
SELECT analytics_ensure_month((current_date - interval '1 month')::date);
|
||||
SELECT analytics_ensure_month(current_date);
|
||||
SELECT analytics_ensure_month((current_date + interval '1 month')::date);
|
||||
|
||||
-- Safety net for months nobody pre-created (see header).
|
||||
CREATE TABLE analytics_snapshots_default PARTITION OF analytics_snapshots DEFAULT;
|
||||
ALTER TABLE analytics_snapshots_default ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE analytics_snapshots_default FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON analytics_snapshots_default
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
||||
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
|
||||
CREATE TRIGGER trg_analytics_snapshot_ws_immutable BEFORE UPDATE ON analytics_snapshots
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
||||
|
||||
ALTER TABLE analytics_snapshots ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE analytics_snapshots FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON analytics_snapshots
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
||||
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
|
||||
-- Grants on the parent cover all current and future partitions for queries
|
||||
-- routed through it (014 style); direct leaf access also inherits from the
|
||||
-- owner's default privileges (003).
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON analytics_snapshots TO sp_app;
|
||||
Loading…
Add table
Add a link
Reference in a new issue