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
72
apps/api/migrations/025_chatgov_p2.sql
Normal file
72
apps/api/migrations/025_chatgov_p2.sql
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
-- P2 competitive short-post variants. 022/023/024 exist. The runner keys
|
||||
-- migrations by filename; 025_archive_search.sql belongs to another slice.
|
||||
ALTER TABLE polls
|
||||
ADD COLUMN kind text NOT NULL DEFAULT 'yes_no' CHECK (kind IN ('yes_no', 'variant_selection')),
|
||||
ADD COLUMN winner_option_id uuid,
|
||||
ADD COLUMN draft_revision bigint,
|
||||
ADD COLUMN content_kind text NOT NULL DEFAULT 'short_post' CHECK (content_kind IN ('short_post', 'document')),
|
||||
ADD COLUMN eligible_user_ids uuid[];
|
||||
-- document is a schema seam only; P2 accepts short_post exclusively.
|
||||
ALTER TABLE polls DROP CONSTRAINT polls_state_check;
|
||||
ALTER TABLE polls ADD CONSTRAINT polls_state_check CHECK
|
||||
(state IN ('open', 'passed', 'failed', 'vetoed', 'quorum_unreachable', 'cancelled', 'stale'));
|
||||
ALTER TABLE polls DROP CONSTRAINT polls_close_reason_check;
|
||||
ALTER TABLE polls ADD CONSTRAINT polls_close_reason_check CHECK
|
||||
(close_reason IN ('quorum_early', 'timebox', 'veto', 'cancelled', 'quorum_unreachable', 'reset_on_change'));
|
||||
|
||||
CREATE TABLE poll_options (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
poll_id uuid NOT NULL,
|
||||
seq integer NOT NULL CHECK (seq > 0),
|
||||
-- Future document revision chains can anchor here. Short posts use the
|
||||
-- poll's numeric draft_revision plus this immutable text snapshot.
|
||||
draft_revision_id uuid,
|
||||
text text NOT NULL CHECK (char_length(text) BETWEEN 1 AND 5000),
|
||||
submitted_by uuid NOT NULL REFERENCES users(id) ON DELETE NO ACTION ON UPDATE RESTRICT,
|
||||
submitted_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (poll_id, seq),
|
||||
UNIQUE (id, poll_id, workspace_id),
|
||||
FOREIGN KEY (poll_id, workspace_id) REFERENCES polls(id, workspace_id) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
);
|
||||
CREATE TRIGGER trg_poll_options_ws_immutable BEFORE UPDATE ON poll_options
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
||||
-- Artifacts cannot be edited or individually removed, even by SQL callers.
|
||||
-- Cascading workspace/poll retention remains possible.
|
||||
CREATE FUNCTION chatgov_option_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' OR pg_trigger_depth() = 1 THEN
|
||||
RAISE EXCEPTION 'poll options are immutable' USING ERRCODE = '55000';
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END $$;
|
||||
CREATE TRIGGER trg_poll_options_immutable BEFORE UPDATE OR DELETE ON poll_options
|
||||
FOR EACH ROW EXECUTE FUNCTION chatgov_option_immutable();
|
||||
ALTER TABLE poll_options ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE poll_options FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON poll_options
|
||||
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
||||
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON poll_options TO sp_app;
|
||||
|
||||
ALTER TABLE poll_votes ADD COLUMN option_id uuid;
|
||||
ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_fk
|
||||
FOREIGN KEY (option_id, poll_id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id)
|
||||
ON DELETE NO ACTION ON UPDATE RESTRICT;
|
||||
ALTER TABLE poll_votes ADD CONSTRAINT poll_votes_option_ballot_check CHECK (option_id IS NULL OR ballot = 'yes');
|
||||
ALTER TABLE polls ADD CONSTRAINT polls_winner_option_fk
|
||||
FOREIGN KEY (winner_option_id, id, workspace_id) REFERENCES poll_options(id, poll_id, workspace_id)
|
||||
ON DELETE NO ACTION ON UPDATE RESTRICT;
|
||||
ALTER TABLE polls ADD CONSTRAINT polls_winner_state_check CHECK
|
||||
(winner_option_id IS NULL OR (kind = 'variant_selection' AND state = 'passed'));
|
||||
ALTER TABLE poll_votes ADD COLUMN voter_identity_id uuid REFERENCES chat_identities(id) ON DELETE NO ACTION ON UPDATE RESTRICT;
|
||||
ALTER TABLE polls ADD COLUMN final_evaluation jsonb;
|
||||
-- Narrow expiry discovery follows the publishing due-schedule house pattern:
|
||||
-- returns references only, never ballots/content. Mutations still use RLS.
|
||||
CREATE FUNCTION chatgov_due_polls() RETURNS TABLE(workspace_id uuid, id uuid)
|
||||
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
SELECT p.workspace_id, p.id FROM polls p WHERE p.state = 'open' AND p.closes_at <= now()
|
||||
ORDER BY p.closes_at LIMIT 500
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION chatgov_due_polls() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION chatgov_due_polls() TO sp_app;
|
||||
Loading…
Add table
Add a link
Reference in a new issue