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
103
apps/api/migrations/019_publishing_integration.sql
Normal file
103
apps/api/migrations/019_publishing_integration.sql
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
-- 018 is retained unchanged: local anp_publishing_it records its application.
|
||||
-- These corrections use independently tracked resumable stages.
|
||||
-- wp3:phase expand transaction
|
||||
SET LOCAL lock_timeout = '5s';
|
||||
ALTER TABLE publishing_attempts ADD COLUMN first_dispatch_started_at timestamptz;
|
||||
CREATE OR REPLACE FUNCTION publishing_attempts_guard() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF (NEW.schedule_id, NEW.draft_id, NEW.connection_id, NEW.attempt_no, NEW.idempotency_key,
|
||||
NEW.schedule_generation, NEW.post_snapshot, NEW.post_hash, NEW.scheduled_by_user_id,
|
||||
NEW.required_permission, NEW.approved_revision, NEW.reviewer_user_id,
|
||||
NEW.credential_id, NEW.credential_generation, NEW.created_at)
|
||||
IS DISTINCT FROM
|
||||
(OLD.schedule_id, OLD.draft_id, OLD.connection_id, OLD.attempt_no, OLD.idempotency_key,
|
||||
OLD.schedule_generation, OLD.post_snapshot, OLD.post_hash, OLD.scheduled_by_user_id,
|
||||
OLD.required_permission, OLD.approved_revision, OLD.reviewer_user_id,
|
||||
OLD.credential_id, OLD.credential_generation, OLD.created_at) THEN
|
||||
RAISE EXCEPTION 'publishing attempt snapshot is immutable' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF OLD.status IN ('completed', 'abandoned') AND NEW.status IS DISTINCT FROM OLD.status THEN
|
||||
RAISE EXCEPTION 'publishing attempt % is terminal', OLD.status USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF OLD.first_dispatch_started_at IS NOT NULL AND NEW.first_dispatch_started_at IS DISTINCT FROM OLD.first_dispatch_started_at THEN
|
||||
RAISE EXCEPTION 'first dispatch time is immutable' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.dispatch_count < OLD.dispatch_count THEN
|
||||
RAISE EXCEPTION 'dispatch_count cannot decrease' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.provider_retry_deadline IS DISTINCT FROM OLD.provider_retry_deadline
|
||||
AND OLD.provider_retry_deadline IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'provider_retry_deadline is fixed at first dispatch' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.owner_token IS NOT NULL AND NEW.owner_token IS DISTINCT FROM OLD.owner_token THEN
|
||||
NEW.claim_epoch := OLD.claim_epoch + 1;
|
||||
ELSE
|
||||
NEW.claim_epoch := OLD.claim_epoch;
|
||||
END IF;
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION drafts_revision_fence() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.publication_attempt < OLD.publication_attempt THEN
|
||||
RAISE EXCEPTION 'publication_attempt cannot decrease' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
IF NEW.post IS DISTINCT FROM OLD.post THEN
|
||||
IF EXISTS (SELECT 1 FROM publishing_attempts a
|
||||
WHERE a.workspace_id = OLD.workspace_id AND a.draft_id = OLD.id
|
||||
AND (a.status = 'unknown'
|
||||
OR (a.status IN ('ready', 'in_progress') AND a.dispatch_started_at IS NOT NULL))) THEN
|
||||
RAISE EXCEPTION 'draft content is locked while a dispatch is unsettled' USING ERRCODE = '55000';
|
||||
END IF;
|
||||
NEW.revision := OLD.revision + 1;
|
||||
NEW.approved_revision := NULL;
|
||||
NEW.approved_hash := NULL;
|
||||
NEW.approved_at := NULL;
|
||||
NEW.reviewer_user_id := NULL;
|
||||
ELSE
|
||||
NEW.revision := OLD.revision;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
-- wp3:phase dispatch_backfill batch
|
||||
WITH batch AS (
|
||||
SELECT id FROM publishing_attempts WHERE first_dispatch_started_at IS NULL AND dispatch_count > 0 LIMIT 1000 FOR UPDATE
|
||||
)
|
||||
UPDATE publishing_attempts a SET first_dispatch_started_at = CASE WHEN a.dispatch_count > 1 OR a.dispatch_started_at IS NULL THEN a.created_at ELSE a.dispatch_started_at END FROM batch WHERE a.id = batch.id;
|
||||
-- wp3:phase receipt_recovery_index concurrent
|
||||
CREATE INDEX CONCURRENTLY publishing_receipts_recovery ON publishing_receipts (workspace_id, attempt_id, received_at);
|
||||
-- wp3:phase cutover transaction
|
||||
SET LOCAL lock_timeout = '5s';
|
||||
DROP FUNCTION publishing_due_schedules(integer);
|
||||
CREATE FUNCTION publishing_due_schedules(p_limit integer DEFAULT 100)
|
||||
RETURNS TABLE(workspace_id uuid, schedule_id uuid, idempotency_key text, generation bigint, publish_at timestamptz, evidence_only boolean)
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT due.workspace_id, due.schedule_id, due.idempotency_key, due.generation, due.publish_at, due.evidence_only
|
||||
FROM (
|
||||
SELECT sp.workspace_id, sp.id AS schedule_id, sp.idempotency_key, sp.generation,
|
||||
sp.publish_at, sp.next_attempt_at AS due_at, false AS evidence_only
|
||||
FROM scheduled_publications sp
|
||||
WHERE sp.state = 'pending' AND sp.execution_version = 1 AND sp.next_attempt_at <= now() AND NOT EXISTS (SELECT 1 FROM publishing_attempts a JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id WHERE a.idempotency_key = sp.idempotency_key AND a.workspace_id = sp.workspace_id)
|
||||
UNION ALL
|
||||
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
|
||||
sp.publish_at, a.lease_until AS due_at, false AS evidence_only
|
||||
FROM publishing_attempts a
|
||||
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
|
||||
WHERE a.status = 'in_progress' AND a.lease_until < now() AND NOT EXISTS (SELECT 1 FROM publishing_receipts r WHERE r.attempt_id = a.id AND r.workspace_id = a.workspace_id)
|
||||
UNION ALL
|
||||
SELECT a.workspace_id, a.schedule_id, a.idempotency_key, a.schedule_generation,
|
||||
sp.publish_at, r.received_at AS due_at, true AS evidence_only
|
||||
FROM publishing_attempts a
|
||||
JOIN publishing_receipts r ON r.attempt_id = a.id AND r.workspace_id = a.workspace_id
|
||||
JOIN scheduled_publications sp ON sp.id = a.schedule_id AND sp.workspace_id = a.workspace_id
|
||||
WHERE (a.status IN ('ready', 'failed', 'unknown') OR (a.status = 'in_progress' AND a.lease_until < now()))
|
||||
) due
|
||||
ORDER BY due.due_at, due.schedule_id
|
||||
LIMIT LEAST(GREATEST(COALESCE(p_limit, 100), 1), 100)
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION publishing_due_schedules(integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION publishing_due_schedules(integer) TO sp_app;
|
||||
Loading…
Add table
Add a link
Reference in a new issue