7b4d7ccf68
Provide a unified ops console for inspecting, retrying and cancelling jobs across generation, template/kol assist, knowledge parse, desktop publish/task, compliance review and monitoring collect sources. Wires RabbitMQ for retry republish and consolidates the desktop_publish_jobs columns into the base migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
216 lines
10 KiB
SQL
216 lines
10 KiB
SQL
ALTER TABLE article_versions
|
|
ADD COLUMN IF NOT EXISTS plaintext_snapshot TEXT,
|
|
ADD COLUMN IF NOT EXISTS manual_review_status VARCHAR(16) NOT NULL DEFAULT 'none',
|
|
ADD COLUMN IF NOT EXISTS manual_review_id BIGINT,
|
|
ADD COLUMN IF NOT EXISTS manual_review_updated_at TIMESTAMPTZ;
|
|
|
|
ALTER TABLE article_versions
|
|
DROP CONSTRAINT IF EXISTS ck_article_versions_manual_review_status;
|
|
|
|
ALTER TABLE article_versions
|
|
ADD CONSTRAINT ck_article_versions_manual_review_status
|
|
CHECK (manual_review_status IN ('none', 'pending', 'approved', 'rejected'));
|
|
|
|
COMMENT ON COLUMN article_versions.plaintext_snapshot IS
|
|
'Plaintext extracted at save time, used by compliance gate to skip diff reconstruction.';
|
|
|
|
COMMENT ON COLUMN article_versions.manual_review_status IS
|
|
'Hot-path manual review stamp: none|pending|approved|rejected.';
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_check_records (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
article_id BIGINT REFERENCES articles(id) ON DELETE SET NULL,
|
|
article_version_id BIGINT REFERENCES article_versions(id) ON DELETE SET NULL,
|
|
target_platforms TEXT[] NOT NULL DEFAULT '{}',
|
|
content_hash CHAR(64) NOT NULL,
|
|
policy_fingerprint VARCHAR(64) NOT NULL,
|
|
trigger_source VARCHAR(32) NOT NULL,
|
|
enforcement_mode VARCHAR(24) NOT NULL,
|
|
highest_level VARCHAR(16),
|
|
passed BOOLEAN NOT NULL,
|
|
hit_count INT NOT NULL DEFAULT 0,
|
|
stored_hit_count INT NOT NULL DEFAULT 0,
|
|
truncated BOOLEAN NOT NULL DEFAULT FALSE,
|
|
dict_versions JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
llm_used BOOLEAN NOT NULL DEFAULT FALSE,
|
|
duration_ms INT NOT NULL DEFAULT 0,
|
|
checked_by BIGINT,
|
|
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
manual_review_status VARCHAR(16) NOT NULL DEFAULT 'none',
|
|
manual_review_id BIGINT,
|
|
CONSTRAINT ck_compliance_check_trigger_source CHECK (
|
|
trigger_source IN (
|
|
'editor_manual',
|
|
'publish_gate',
|
|
'auto_draft',
|
|
'api',
|
|
'scheduler_recheck',
|
|
'manual_review_bypass'
|
|
)
|
|
),
|
|
CONSTRAINT ck_compliance_check_mode CHECK (enforcement_mode IN ('mandatory', 'advisory', 'disabled')),
|
|
CONSTRAINT ck_compliance_check_highest_level CHECK (highest_level IS NULL OR highest_level IN ('block', 'high', 'medium', 'info')),
|
|
CONSTRAINT ck_compliance_check_manual_status CHECK (manual_review_status IN ('none', 'pending', 'approved', 'rejected'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_check_records_article_checked
|
|
ON compliance_check_records (tenant_id, article_id, checked_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_check_records_version_checked
|
|
ON compliance_check_records (article_version_id, checked_at DESC)
|
|
WHERE article_version_id IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_violations (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
record_id BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
|
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
platform_code TEXT,
|
|
source VARCHAR(24) NOT NULL,
|
|
dictionary_code VARCHAR(64),
|
|
dictionary_name VARCHAR(128),
|
|
term_pattern TEXT,
|
|
matched_text TEXT NOT NULL,
|
|
start_offset INT NOT NULL DEFAULT 0,
|
|
end_offset INT NOT NULL DEFAULT 0,
|
|
dom_path TEXT,
|
|
level VARCHAR(16) NOT NULL,
|
|
hint TEXT,
|
|
reference_law TEXT,
|
|
acknowledged_in_ack_id BIGINT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT ck_compliance_violation_source CHECK (source IN ('dict', 'regex', 'llm')),
|
|
CONSTRAINT ck_compliance_violation_level CHECK (level IN ('block', 'high', 'medium', 'info')),
|
|
CONSTRAINT ck_compliance_violation_offsets CHECK (start_offset >= 0 AND end_offset >= start_offset),
|
|
CONSTRAINT ck_violations_matched_text_len CHECK (char_length(matched_text) <= 80)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_violations_record
|
|
ON compliance_violations (record_id, id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_violations_tenant_level
|
|
ON compliance_violations (tenant_id, level, id DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_ack_records (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
article_id BIGINT NOT NULL REFERENCES articles(id),
|
|
article_version_id BIGINT NOT NULL REFERENCES article_versions(id),
|
|
check_record_id BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
|
|
content_hash CHAR(64) NOT NULL,
|
|
policy_fingerprint VARCHAR(64) NOT NULL,
|
|
acknowledged_by BIGINT NOT NULL REFERENCES users(id),
|
|
ack_violation_ids BIGINT[] NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
consumed_at TIMESTAMPTZ,
|
|
consumed_by_job_id UUID,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ack_lookup
|
|
ON compliance_ack_records (tenant_id, article_id, article_version_id, check_record_id)
|
|
WHERE consumed_at IS NULL;
|
|
|
|
ALTER TABLE compliance_violations
|
|
DROP CONSTRAINT IF EXISTS fk_compliance_violations_ack;
|
|
|
|
ALTER TABLE compliance_violations
|
|
ADD CONSTRAINT fk_compliance_violations_ack
|
|
FOREIGN KEY (acknowledged_in_ack_id)
|
|
REFERENCES compliance_ack_records(id)
|
|
ON DELETE SET NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_review_jobs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
message_id UUID NOT NULL,
|
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
article_id BIGINT NOT NULL REFERENCES articles(id),
|
|
article_version_id BIGINT NOT NULL REFERENCES article_versions(id),
|
|
check_record_id BIGINT NOT NULL REFERENCES compliance_check_records(id) ON DELETE CASCADE,
|
|
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
attempts INT NOT NULL DEFAULT 0,
|
|
available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
locked_at TIMESTAMPTZ,
|
|
locked_by TEXT,
|
|
last_error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT uk_compliance_review_jobs_record UNIQUE (check_record_id),
|
|
CONSTRAINT uk_compliance_review_jobs_message UNIQUE (message_id),
|
|
CONSTRAINT ck_compliance_review_job_status CHECK (status IN ('pending', 'running', 'done', 'failed'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_review_jobs_pending
|
|
ON compliance_review_jobs (available_at, id)
|
|
WHERE status = 'pending';
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_manual_reviews (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
|
article_id BIGINT NOT NULL REFERENCES articles(id),
|
|
article_version_id BIGINT NOT NULL REFERENCES article_versions(id),
|
|
original_check_record_id BIGINT REFERENCES compliance_check_records(id) ON DELETE SET NULL,
|
|
original_content_hash CHAR(64) NOT NULL,
|
|
original_policy_fingerprint VARCHAR(64) NOT NULL,
|
|
original_target_platforms TEXT[] NOT NULL DEFAULT '{}',
|
|
original_violation_summary JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
requested_by BIGINT NOT NULL REFERENCES users(id),
|
|
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
request_note TEXT,
|
|
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
cancelled_by BIGINT REFERENCES users(id),
|
|
cancelled_at TIMESTAMPTZ,
|
|
cancel_reason TEXT,
|
|
reviewed_by BIGINT,
|
|
reviewed_at TIMESTAMPTZ,
|
|
decision_reason TEXT,
|
|
CONSTRAINT ck_manual_review_request_note_len CHECK (request_note IS NULL OR char_length(request_note) <= 500),
|
|
CONSTRAINT ck_manual_review_cancel_reason_len CHECK (cancel_reason IS NULL OR char_length(cancel_reason) <= 500),
|
|
CONSTRAINT ck_manual_review_decision_reason_len CHECK (decision_reason IS NULL OR char_length(decision_reason) <= 1000),
|
|
CONSTRAINT ck_manual_review_status CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')),
|
|
CONSTRAINT ck_manual_review_decision CHECK (
|
|
(status = 'pending' AND reviewed_by IS NULL AND reviewed_at IS NULL AND decision_reason IS NULL
|
|
AND cancelled_by IS NULL AND cancelled_at IS NULL AND cancel_reason IS NULL) OR
|
|
(status IN ('approved','rejected') AND reviewed_by IS NOT NULL AND reviewed_at IS NOT NULL AND decision_reason IS NOT NULL
|
|
AND cancelled_by IS NULL AND cancelled_at IS NULL AND cancel_reason IS NULL) OR
|
|
(status = 'cancelled' AND reviewed_by IS NULL AND reviewed_at IS NULL AND decision_reason IS NULL
|
|
AND cancelled_by IS NOT NULL AND cancelled_at IS NOT NULL)
|
|
)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_manual_review_active_per_version
|
|
ON compliance_manual_reviews (article_version_id)
|
|
WHERE status IN ('pending', 'approved', 'rejected');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_manual_review_pending_queue
|
|
ON compliance_manual_reviews (requested_at)
|
|
WHERE status = 'pending';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_manual_review_lookup_by_version
|
|
ON compliance_manual_reviews (article_version_id, status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_manual_review_tenant_status
|
|
ON compliance_manual_reviews (tenant_id, status, requested_at DESC);
|
|
|
|
ALTER TABLE article_versions
|
|
DROP CONSTRAINT IF EXISTS fk_article_versions_manual_review;
|
|
|
|
ALTER TABLE article_versions
|
|
ADD CONSTRAINT fk_article_versions_manual_review
|
|
FOREIGN KEY (manual_review_id)
|
|
REFERENCES compliance_manual_reviews(id)
|
|
ON DELETE SET NULL;
|
|
|
|
ALTER TABLE desktop_publish_jobs
|
|
DROP CONSTRAINT IF EXISTS desktop_publish_jobs_compliance_blocked_record_id_fkey;
|
|
|
|
ALTER TABLE desktop_publish_jobs
|
|
ADD CONSTRAINT desktop_publish_jobs_compliance_blocked_record_id_fkey
|
|
FOREIGN KEY (compliance_blocked_record_id)
|
|
REFERENCES compliance_check_records(id)
|
|
ON DELETE SET NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_desktop_publish_jobs_status_scheduled
|
|
ON desktop_publish_jobs (status, scheduled_at, created_at)
|
|
WHERE status IN ('queued', 'blocked_by_compliance');
|