Migrating to version 20260908000000 (145 migrations in total): -- migrating version 20260527115454 -> CREATE TABLE "public"."tenants" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" text NOT NULL, "slug" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "tenants_slug_key" UNIQUE ("slug") ); -> CREATE TABLE "public"."sites" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "url" text NOT NULL, "name" text NOT NULL, "status" text NOT NULL DEFAULT 'pending', "wp_version" text NOT NULL DEFAULT '', "php_version" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "sites_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "sites_tenant_id_idx" ON "public"."sites" ("tenant_id"); -> CREATE UNIQUE INDEX "sites_tenant_id_url_key" ON "public"."sites" ("tenant_id", "url"); -> ALTER TABLE "public"."sites" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."sites" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "sites_tenant_isolation" ON "public"."sites" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -- ok (747.137ms) -- migrating version 20260527130000 -> DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'wpmgr_app') THEN CREATE ROLE wpmgr_app NOLOGIN NOSUPERUSER NOBYPASSRLS; END IF; END $$; -> CREATE TABLE "public"."users" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "email" text NOT NULL, "password_hash" text NULL, "oidc_subject" text NULL, "oidc_issuer" text NULL, "name" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "last_login_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "users_email_key" UNIQUE ("email") ); -> CREATE UNIQUE INDEX "users_oidc_identity_key" ON "public"."users" ("oidc_issuer", "oidc_subject") WHERE (oidc_issuer IS NOT NULL AND oidc_subject IS NOT NULL); -> CREATE TABLE "public"."memberships" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "role" text NOT NULL DEFAULT 'viewer', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "memberships_user_tenant_key" ON "public"."memberships" ("user_id", "tenant_id"); -> CREATE INDEX "memberships_tenant_id_idx" ON "public"."memberships" ("tenant_id"); -> CREATE INDEX "memberships_user_id_idx" ON "public"."memberships" ("user_id"); -> CREATE TABLE "public"."api_keys" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "name" text NOT NULL, "prefix" text NOT NULL, "key_hash" text NOT NULL, "role" text NOT NULL DEFAULT 'operator', "created_at" timestamptz NOT NULL DEFAULT now(), "last_used_at" timestamptz NULL, "revoked_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "api_keys_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "api_keys_prefix_key" ON "public"."api_keys" ("prefix"); -> CREATE INDEX "api_keys_tenant_id_idx" ON "public"."api_keys" ("tenant_id"); -> CREATE TABLE "public"."audit_log" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "actor_type" text NOT NULL, "actor_id" text NOT NULL DEFAULT '', "action" text NOT NULL, "target_type" text NOT NULL DEFAULT '', "target_id" text NOT NULL DEFAULT '', "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb, "prev_hash" text NOT NULL DEFAULT '', "hash" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "audit_log_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "audit_log_tenant_id_created_at_idx" ON "public"."audit_log" ("tenant_id", "created_at"); -> ALTER TABLE "public"."memberships" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."memberships" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "memberships_tenant_isolation" ON "public"."memberships" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "memberships_self_read" ON "public"."memberships" FOR SELECT USING ("user_id" = nullif(current_setting('app.user_id', true), '')::uuid); -> ALTER TABLE "public"."api_keys" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."api_keys" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "api_keys_tenant_isolation" ON "public"."api_keys" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "api_keys_prefix_lookup" ON "public"."api_keys" FOR SELECT USING (current_setting('app.apikey_lookup', true) = 'on'); -> ALTER TABLE "public"."audit_log" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."audit_log" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "audit_log_tenant_isolation" ON "public"."audit_log" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> GRANT USAGE ON SCHEMA "public" TO wpmgr_app; -> GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "public" TO wpmgr_app; -> GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "public" TO wpmgr_app; -> ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO wpmgr_app; -> ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT USAGE, SELECT ON SEQUENCES TO wpmgr_app; -> REVOKE UPDATE, DELETE, TRUNCATE ON "public"."audit_log" FROM wpmgr_app; -- ok (2.297501042s) -- migrating version 20260527172114 -> ALTER TABLE "public"."sites" ADD COLUMN "agent_public_key" text NOT NULL DEFAULT '', ADD COLUMN "enrolled_at" timestamptz NULL, ADD COLUMN "last_seen_at" timestamptz NULL, ADD COLUMN "health_status" text NOT NULL DEFAULT 'unknown', ADD COLUMN "server_info" text NOT NULL DEFAULT '', ADD COLUMN "multisite" boolean NOT NULL DEFAULT false, ADD COLUMN "active_theme" text NOT NULL DEFAULT '', ADD COLUMN "components" jsonb NOT NULL DEFAULT '{}', ADD COLUMN "tags" text[] NOT NULL DEFAULT '{}'; -> CREATE UNIQUE INDEX "sites_agent_public_key_key" ON "public"."sites" ("agent_public_key") WHERE (agent_public_key <> ''::text); -> CREATE INDEX "sites_tags_idx" ON "public"."sites" USING GIN ("tags"); -> CREATE TABLE "public"."agent_nonces" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "site_id" uuid NOT NULL, "nonce" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "agent_nonces_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "agent_nonces_created_at_idx" ON "public"."agent_nonces" ("created_at"); -> CREATE UNIQUE INDEX "agent_nonces_site_nonce_key" ON "public"."agent_nonces" ("site_id", "nonce"); -> CREATE TABLE "public"."pairing_codes" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "code_hash" text NOT NULL, "created_by" uuid NULL, "site_name" text NOT NULL DEFAULT '', "tags" text[] NOT NULL DEFAULT '{}', "expires_at" timestamptz NOT NULL, "consumed_at" timestamptz NULL, "attempts" integer NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "pairing_codes_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "pairing_codes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "pairing_codes_code_hash_key" ON "public"."pairing_codes" ("code_hash"); -> CREATE INDEX "pairing_codes_tenant_id_idx" ON "public"."pairing_codes" ("tenant_id"); -> CREATE POLICY "sites_enroll" ON "public"."sites" USING (current_setting('app.enroll', true) = 'on') WITH CHECK (current_setting('app.enroll', true) = 'on'); -> CREATE POLICY "sites_agent" ON "public"."sites" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -> ALTER TABLE "public"."pairing_codes" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."pairing_codes" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "pairing_codes_tenant_isolation" ON "public"."pairing_codes" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "pairing_codes_enroll" ON "public"."pairing_codes" USING (current_setting('app.enroll', true) = 'on') WITH CHECK (current_setting('app.enroll', true) = 'on'); -> ALTER TABLE "public"."agent_nonces" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."agent_nonces" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "agent_nonces_agent" ON "public"."agent_nonces" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (1.521227291s) -- migrating version 20260527195407 -> CREATE TABLE "update_runs" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "created_by" uuid NULL, "status" text NOT NULL DEFAULT 'pending', "dry_run" boolean NOT NULL DEFAULT false, "scheduled_at" timestamptz NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "update_runs_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "update_runs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "update_runs_tenant_id_created_at_idx" ON "update_runs" ("tenant_id", "created_at" DESC); -> CREATE TABLE "update_tasks" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "run_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "target_type" text NOT NULL, "target_slug" text NOT NULL, "desired_version" text NOT NULL DEFAULT 'latest', "from_version" text NOT NULL DEFAULT '', "to_version" text NOT NULL DEFAULT '', "status" text NOT NULL DEFAULT 'pending', "detail" text NOT NULL DEFAULT '', "error" text NOT NULL DEFAULT '', "started_at" timestamptz NULL, "finished_at" timestamptz NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "update_tasks_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "update_runs" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "update_tasks_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "update_tasks_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "update_tasks_run_id_idx" ON "update_tasks" ("run_id"); -> CREATE INDEX "update_tasks_site_id_idx" ON "update_tasks" ("site_id"); -> CREATE INDEX "update_tasks_tenant_id_idx" ON "update_tasks" ("tenant_id"); -> ALTER TABLE "public"."update_runs" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."update_runs" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "update_runs_tenant_isolation" ON "public"."update_runs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> ALTER TABLE "public"."update_tasks" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."update_tasks" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "update_tasks_tenant_isolation" ON "public"."update_tasks" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -- ok (1.101214042s) -- migrating version 20260528020000 -> ALTER TABLE "public"."sites" ADD COLUMN "age_recipient" text NOT NULL DEFAULT ''; -> CREATE TABLE "public"."backup_chunks" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "blake3" text NOT NULL, "s3_key" text NOT NULL, "size" bigint NOT NULL DEFAULT 0, "refcount" bigint NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "backup_chunks_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "backup_chunks_tenant_blake3_key" ON "public"."backup_chunks" ("tenant_id", "blake3"); -> CREATE INDEX "backup_chunks_tenant_id_idx" ON "public"."backup_chunks" ("tenant_id"); -> CREATE TABLE "public"."backup_snapshots" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "created_by" uuid NULL, "kind" text NOT NULL, "status" text NOT NULL DEFAULT 'pending', "age_recipient" text NOT NULL DEFAULT '', "total_size" bigint NOT NULL DEFAULT 0, "chunk_count" bigint NOT NULL DEFAULT 0, "error" text NOT NULL DEFAULT '', "archived" boolean NOT NULL DEFAULT false, "started_at" timestamptz NULL, "finished_at" timestamptz NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "backup_snapshots_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "backup_snapshots_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_snapshots_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "backup_snapshots_tenant_site_idx" ON "public"."backup_snapshots" ("tenant_id", "site_id", "created_at" DESC); -> CREATE INDEX "backup_snapshots_tenant_created_idx" ON "public"."backup_snapshots" ("tenant_id", "created_at" DESC); -> CREATE TABLE "public"."backup_manifest_entries" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "snapshot_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "path" text NOT NULL, "entry_kind" text NOT NULL DEFAULT 'file', "table_name" text NOT NULL DEFAULT '', "chunk_hashes" text[] NOT NULL DEFAULT '{}', "size" bigint NOT NULL DEFAULT 0, "mode" integer NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "backup_manifest_entries_snapshot_id_fkey" FOREIGN KEY ("snapshot_id") REFERENCES "public"."backup_snapshots" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_manifest_entries_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "backup_manifest_entries_snapshot_idx" ON "public"."backup_manifest_entries" ("snapshot_id"); -> CREATE INDEX "backup_manifest_entries_tenant_id_idx" ON "public"."backup_manifest_entries" ("tenant_id"); -> CREATE TABLE "public"."backup_schedules" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "cadence" text NOT NULL DEFAULT 'daily', "kind" text NOT NULL DEFAULT 'full', "enabled" boolean NOT NULL DEFAULT true, "retention_days" integer NOT NULL DEFAULT 30, "monthly_archive_keep" integer NOT NULL DEFAULT 12, "next_run_at" timestamptz NOT NULL DEFAULT now(), "last_run_at" timestamptz NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "backup_schedules_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_schedules_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "backup_schedules_site_key" ON "public"."backup_schedules" ("site_id"); -> CREATE INDEX "backup_schedules_tenant_id_idx" ON "public"."backup_schedules" ("tenant_id"); -> CREATE INDEX "backup_schedules_due_idx" ON "public"."backup_schedules" ("next_run_at") WHERE enabled; -> ALTER TABLE "public"."backup_chunks" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."backup_chunks" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_chunks_tenant_isolation" ON "public"."backup_chunks" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> ALTER TABLE "public"."backup_snapshots" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."backup_snapshots" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_snapshots_tenant_isolation" ON "public"."backup_snapshots" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "backup_snapshots_gc" ON "public"."backup_snapshots" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -> ALTER TABLE "public"."backup_manifest_entries" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."backup_manifest_entries" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_manifest_entries_tenant_isolation" ON "public"."backup_manifest_entries" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> ALTER TABLE "public"."backup_schedules" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."backup_schedules" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_schedules_tenant_isolation" ON "public"."backup_schedules" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "backup_schedules_scheduler" ON "public"."backup_schedules" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -- ok (2.242236833s) -- migrating version 20260528060000 -> CREATE TABLE "public"."alert_configs" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "email_recipients" text[] NOT NULL DEFAULT '{}', "webhook_url" text NOT NULL DEFAULT '', "webhook_secret" text NOT NULL DEFAULT '', "enabled" boolean NOT NULL DEFAULT true, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "alert_configs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "alert_configs_tenant_key" ON "public"."alert_configs" ("tenant_id"); -> CREATE TABLE "public"."site_alert_state" ( "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "last_status" text NOT NULL DEFAULT 'unknown', "consecutive_down" integer NOT NULL DEFAULT 0, "in_incident" boolean NOT NULL DEFAULT false, "last_alert_at" timestamptz NULL, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_alert_state_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_alert_state_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "site_alert_state_tenant_id_idx" ON "public"."site_alert_state" ("tenant_id"); -> ALTER TABLE "public"."alert_configs" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."alert_configs" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "alert_configs_tenant_isolation" ON "public"."alert_configs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "alert_configs_evaluator" ON "public"."alert_configs" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -> ALTER TABLE "public"."site_alert_state" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_alert_state" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "site_alert_state_tenant_isolation" ON "public"."site_alert_state" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "site_alert_state_agent" ON "public"."site_alert_state" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (1.102796958s) -- migrating version 20260528100000 -> CREATE TABLE "public"."autologin_policies" ( "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "enabled" boolean NOT NULL DEFAULT true, "allowed_wp_roles" text[] NOT NULL DEFAULT ARRAY['administrator'], "require_2fa_step_up" boolean NOT NULL DEFAULT false, "max_session_age_minutes" integer NOT NULL DEFAULT 30, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "autologin_policies_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "autologin_policies_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "autologin_policies_tenant_id_idx" ON "public"."autologin_policies" ("tenant_id"); -> CREATE TABLE "public"."autologin_tokens" ( "id" text NOT NULL, "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "initiator_user_id" uuid NOT NULL, "target_wp_user_login" text NOT NULL DEFAULT '', "initiator_ip" inet NULL, "initiator_user_agent" text NOT NULL DEFAULT '', "expires_at" timestamptz NOT NULL, "consumed_at" timestamptz NULL, "consumed_from_ip" inet NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "autologin_tokens_initiator_user_id_fkey" FOREIGN KEY ("initiator_user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "autologin_tokens_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "autologin_tokens_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "autologin_tokens_tenant_id_idx" ON "public"."autologin_tokens" ("tenant_id"); -> CREATE INDEX "autologin_tokens_pending_expiry_idx" ON "public"."autologin_tokens" ("expires_at") WHERE (consumed_at IS NULL); -> ALTER TABLE "public"."autologin_tokens" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."autologin_tokens" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "autologin_tokens_tenant_isolation" ON "public"."autologin_tokens" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "autologin_tokens_agent" ON "public"."autologin_tokens" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -> CREATE POLICY "autologin_tokens_agent_consume" ON "public"."autologin_tokens" FOR UPDATE USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -> ALTER TABLE "public"."autologin_policies" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."autologin_policies" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "autologin_policies_tenant_isolation" ON "public"."autologin_policies" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "autologin_policies_agent" ON "public"."autologin_policies" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -- ok (1.2141575s) -- migrating version 20260528120000 -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN IF NOT EXISTS "progress" jsonb NOT NULL DEFAULT '{}'::jsonb; -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN IF NOT EXISTS "progress_updated_at" timestamptz NULL; -> CREATE INDEX IF NOT EXISTS "backup_snapshots_running_progress_idx" ON "public"."backup_snapshots" ("progress_updated_at") WHERE status = 'running'; -- ok (460.888875ms) -- migrating version 20260529120000 -> CREATE TABLE IF NOT EXISTS "public"."site_uptime_probes" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "probed_at" timestamptz NOT NULL DEFAULT now(), "up" boolean NOT NULL, "http_status" integer NOT NULL DEFAULT 0, "dns_ms" double precision NOT NULL DEFAULT 0, "connect_ms" double precision NOT NULL DEFAULT 0, "tls_ms" double precision NOT NULL DEFAULT 0, "ttfb_ms" double precision NOT NULL DEFAULT 0, "total_ms" double precision NOT NULL DEFAULT 0, "tls_expiry" timestamptz NULL, "tls_issuer" text NOT NULL DEFAULT '', "tls_subject" text NOT NULL DEFAULT '', "error_text" text NOT NULL DEFAULT '', PRIMARY KEY ("id"), CONSTRAINT "site_uptime_probes_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON DELETE CASCADE, CONSTRAINT "site_uptime_probes_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "site_uptime_probes_site_time_idx" ON "public"."site_uptime_probes" ("site_id", "probed_at" DESC); -> CREATE INDEX IF NOT EXISTS "site_uptime_probes_tenant_time_idx" ON "public"."site_uptime_probes" ("tenant_id", "probed_at" DESC); -> ALTER TABLE "public"."site_uptime_probes" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_uptime_probes" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "site_uptime_probes_tenant_isolation" ON "public"."site_uptime_probes" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "site_uptime_probes_agent" ON "public"."site_uptime_probes" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (764.8355ms) -- migrating version 20260530000000 -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN "sql_inspection_cached_at" timestamptz; -> COMMENT ON COLUMN "public"."backup_snapshots"."sql_inspection_cached_at" IS 'When the CP wrote a legacy inspection cache for this snapshot. NULL means no cache; manifest-based inspection has its own resolution path.'; -- ok (394.758417ms) -- migrating version 20260530000001 -> CREATE TABLE "public"."site_destinations" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, -- Nullable so a future flow can introduce tenant-wide defaults (V1 always -- writes a non-null site_id). "site_id" uuid NULL, "kind" text NOT NULL CHECK (kind IN ('cp', 'local', 's3_compat')), "label" text NOT NULL, "endpoint" text NOT NULL DEFAULT '', "region" text NOT NULL DEFAULT '', "bucket" text NOT NULL DEFAULT '', "path_prefix" text NOT NULL DEFAULT '', "access_key_id" text NOT NULL DEFAULT '', "secret_key_enc" bytea NULL, "force_path_style" boolean NOT NULL DEFAULT FALSE, "is_default" boolean NOT NULL DEFAULT FALSE, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "site_destinations_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_destinations_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE INDEX "site_destinations_site_idx" ON "public"."site_destinations" ("site_id") WHERE site_id IS NOT NULL; -> CREATE UNIQUE INDEX "site_destinations_default_idx" ON "public"."site_destinations" ("tenant_id", "site_id") WHERE is_default = TRUE AND site_id IS NOT NULL; -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN "destination_id" uuid NULL REFERENCES "public"."site_destinations" ("id") ON DELETE SET NULL; -> ALTER TABLE "public"."site_destinations" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_destinations" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "site_destinations_tenant_isolation" ON "public"."site_destinations" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "site_destinations_agent" ON "public"."site_destinations" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -- ok (820.541417ms) -- migrating version 20260530010000 -> ALTER TABLE backup_snapshots ADD COLUMN source_site_url TEXT, ADD COLUMN source_home_url TEXT, ADD COLUMN source_content_url TEXT, ADD COLUMN source_upload_url TEXT; -> COMMENT ON COLUMN backup_snapshots.source_site_url IS 'siteurl recorded at backup time. Used by restore to compute URL rewrites when restoring to a different environment (dev->prod, staging->prod).'; -> COMMENT ON COLUMN backup_snapshots.source_home_url IS 'home_url recorded at backup time. See source_site_url.'; -> COMMENT ON COLUMN backup_snapshots.source_content_url IS 'WP_CONTENT_URL recorded at backup time. See source_site_url.'; -> COMMENT ON COLUMN backup_snapshots.source_upload_url IS 'wp_upload_dir()[''baseurl''] recorded at backup time. See source_site_url.'; -- ok (598.642458ms) -- migrating version 20260530020000 -> CREATE TABLE "public"."agent_diagnostics" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- One of: identity / php / mysql / filesystem / http / cron / themes / -- plugins / users / security / https / mail / performance / hosting "category" text NOT NULL, "payload" jsonb NOT NULL DEFAULT '{}'::jsonb, "collected_at" timestamptz NOT NULL DEFAULT now(), "received_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "agent_diagnostics_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "agent_diagnostics_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "agent_diagnostics_site_category_idx" ON "public"."agent_diagnostics" ("tenant_id", "site_id", "category"); -> CREATE INDEX "agent_diagnostics_received_idx" ON "public"."agent_diagnostics" ("tenant_id", "received_at" DESC); -> CREATE TABLE "public"."agent_php_errors" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- md5(code:file:line:message) — the agent-side dedup fingerprint. "md5" text NOT NULL, "code" integer NOT NULL, "severity" text NOT NULL DEFAULT 'warning', "message" text NOT NULL, "file" text NOT NULL DEFAULT '', "line" integer NOT NULL DEFAULT 0, "request_path" text NOT NULL DEFAULT '', -- Agent-supplied first_seen / last_seen timestamps (Unix seconds → tz). "first_seen_at" timestamptz NOT NULL DEFAULT now(), "last_seen_at" timestamptz NOT NULL DEFAULT now(), "occurrence_count" bigint NOT NULL DEFAULT 1, "silenced" boolean NOT NULL DEFAULT false, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "agent_php_errors_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "agent_php_errors_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "agent_php_errors_site_md5_idx" ON "public"."agent_php_errors" ("tenant_id", "site_id", "md5"); -> CREATE INDEX "agent_php_errors_site_lastseen_idx" ON "public"."agent_php_errors" ("tenant_id", "site_id", "last_seen_at" DESC); -> CREATE INDEX "agent_php_errors_silenced_idx" ON "public"."agent_php_errors" ("tenant_id", "site_id") WHERE silenced = false; -> ALTER TABLE "public"."agent_diagnostics" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."agent_diagnostics" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "agent_diagnostics_tenant_isolation" ON "public"."agent_diagnostics" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "agent_diagnostics_agent" ON "public"."agent_diagnostics" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -> ALTER TABLE "public"."agent_php_errors" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."agent_php_errors" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "agent_php_errors_tenant_isolation" ON "public"."agent_php_errors" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "agent_php_errors_agent" ON "public"."agent_php_errors" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (1.278621583s) -- migrating version 20260530030000 -> COMMENT ON COLUMN "public"."agent_diagnostics"."category" IS 'One of: identity / php / mysql / filesystem / http / cron / themes / ' 'plugins / users / security / https / mail / performance / hosting / ' 'wp_native. The 14 legacy categories are the WPMgr-extra leapfrog ' 'collector; wp_native is the verbatim WP_Debug_Data::debug_data() dump ' 'introduced in agent v0.9.14 (Site-Health-Full).'; -- ok (316.931875ms) -- migrating version 20260530040000 -> ALTER TABLE "public"."alert_configs" ADD COLUMN "notify_security" boolean NOT NULL DEFAULT false; -> CREATE TABLE "public"."agent_activity_log" ( "id" bigserial NOT NULL, "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- Agent-assigned monotonic sequence within the (tenant, site) chain. "seq" bigint NOT NULL, "event_type" text NOT NULL, "object_type" text NOT NULL, "object_id" text NOT NULL DEFAULT '', "object_label" text NOT NULL DEFAULT '', "actor_user_id" bigint NOT NULL DEFAULT 0, "actor_login" text NOT NULL DEFAULT '', "actor_ip" text NOT NULL DEFAULT '', "summary" text NOT NULL DEFAULT '', "meta" jsonb, -- Verbatim agent-serialized meta bytes — the EXACT preimage the agent -- hashed (wp_json_encode key order + slash/unicode escaping). Chain -- re-verification hashes THIS, not the jsonb "meta" column: Postgres JSONB -- normalizes key order + whitespace and Go's json.Marshal sorts keys + -- HTML-escapes, either of which diverges from PHP's encoding and would -- false-flag every multi-key event (e.g. {version,severity}) as a break. "meta_raw" text NOT NULL DEFAULT '{}', -- Extracted from meta.severity (high|medium|low); drives the alert decision. "severity" text NOT NULL DEFAULT 'low', -- Hash chain (see SHARED WIRE CONTRACT). prev_hash of the first event is 64 -- zero chars; this_hash = sha256(canonical preimage). chain_valid is set by -- the CP's server-side re-verification at ingest. "prev_hash" text NOT NULL, "this_hash" text NOT NULL, "chain_valid" boolean NOT NULL DEFAULT true, "occurred_at" timestamptz NOT NULL, "received_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "agent_activity_log_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "agent_activity_log_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, -- Idempotent agent retry: one row per (tenant, site, seq). CONSTRAINT "agent_activity_log_tenant_site_seq_key" UNIQUE ("tenant_id", "site_id", "seq") ); -> CREATE INDEX "activity_site_occurred_idx" ON "public"."agent_activity_log" ("site_id", "occurred_at" DESC); -> CREATE INDEX "activity_site_severity_idx" ON "public"."agent_activity_log" ("site_id", "severity") WHERE severity = 'high'; -> ALTER TABLE "public"."agent_activity_log" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."agent_activity_log" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "agent_activity_log_tenant_isolation" ON "public"."agent_activity_log" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "agent_activity_log_agent" ON "public"."agent_activity_log" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (796.4745ms) -- migrating version 20260530050000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'agent_php_errors' AND column_name = 'backtrace' ) THEN ALTER TABLE "public"."agent_php_errors" ADD COLUMN "backtrace" jsonb NOT NULL DEFAULT '[]'::jsonb; END IF; END; $$; -- ok (316.701792ms) -- migrating version 20260530060000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_error_config" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- error_level is the PHP E_* bitmask (e.g. 6143 = E_ALL & ~E_STRICT, -- the WP default). >0, fits in int32. "error_level" integer NOT NULL DEFAULT 6143, -- ignore_md5s is the list of md5 fingerprints the agent must suppress -- without counting; empty by default. "ignore_md5s" text[] NOT NULL DEFAULT '{}', "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_error_config_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_error_config_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_error_config' AND indexname = 'site_error_config_tenant_idx' ) THEN CREATE INDEX "site_error_config_tenant_idx" ON "public"."site_error_config" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN -- Enable RLS (idempotent; ALTER TABLE … ENABLE ROW LEVEL SECURITY is safe -- to run multiple times). ALTER TABLE "public"."site_error_config" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_error_config" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_error_config' AND policyname = 'site_error_config_tenant_isolation' ) THEN CREATE POLICY "site_error_config_tenant_isolation" ON "public"."site_error_config" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_error_config' AND policyname = 'site_error_config_agent' ) THEN CREATE POLICY "site_error_config_agent" ON "public"."site_error_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (590.38025ms) -- migrating version 20260530070000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_security_config" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- mode controls what the agent does with login attempts: -- "disabled" — no login protection active. -- "audit" — record events but do not block. -- "protect" — record events AND block based on thresholds. "mode" text NOT NULL DEFAULT 'protect', -- thresholds is a JSONB map that the agent uses to decide when to -- challenge, temporarily block, or permanently block an IP. -- Default matches the agent's built-in defaults. "thresholds" jsonb NOT NULL DEFAULT '{"captcha_limit":3,"temp_block_limit":10,"block_all_limit":100,"failed_login_gap":1800,"success_login_gap":1800,"all_blocked_gap":1800}', -- ip_header is the HTTP header the agent reads to extract the real -- client IP (e.g. "REMOTE_ADDR", "HTTP_X_FORWARDED_FOR"). "ip_header" text NOT NULL DEFAULT 'REMOTE_ADDR', -- allow_cidrs / deny_cidrs are CIDR strings the agent applies before -- threshold evaluation. Stored as text[] for simple equality queries. "allow_cidrs" text[] NOT NULL DEFAULT '{}', "deny_cidrs" text[] NOT NULL DEFAULT '{}', "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_security_config_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_security_config_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_config' AND indexname = 'site_security_config_tenant_idx' ) THEN CREATE INDEX "site_security_config_tenant_idx" ON "public"."site_security_config" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_security_config" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_security_config" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_config' AND policyname = 'site_security_config_tenant_isolation' ) THEN CREATE POLICY "site_security_config_tenant_isolation" ON "public"."site_security_config" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_config' AND policyname = 'site_security_config_agent' ) THEN CREATE POLICY "site_security_config_agent" ON "public"."site_security_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."agent_login_events" ( "id" bigserial PRIMARY KEY, "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- agent_event_id is the agent's local row id for dedup cursor tracking. "agent_event_id" bigint NOT NULL, "ip" text, -- status: 1=failure, 2=success, 3=blocked. "status" smallint, "category" text, "username" text, "request_id" text, "occurred_at" timestamptz, "ingested_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "agent_login_events_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "agent_login_events_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'agent_login_events' AND indexname = 'agent_login_events_tenant_site_time_idx' ) THEN CREATE INDEX "agent_login_events_tenant_site_time_idx" ON "public"."agent_login_events" ("tenant_id", "site_id", "occurred_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'agent_login_events' AND indexname = 'agent_login_events_dedup_idx' ) THEN CREATE UNIQUE INDEX "agent_login_events_dedup_idx" ON "public"."agent_login_events" ("tenant_id", "site_id", "agent_event_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."agent_login_events" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."agent_login_events" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_login_events' AND policyname = 'agent_login_events_tenant_isolation' ) THEN CREATE POLICY "agent_login_events_tenant_isolation" ON "public"."agent_login_events" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_login_events' AND policyname = 'agent_login_events_agent' ) THEN CREATE POLICY "agent_login_events_agent" ON "public"."agent_login_events" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.025076125s) -- migrating version 20260531000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_login_brand" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- logo_url is the full URL of the image shown on the WP login page. -- Empty string = no override (WordPress default logo). "logo_url" text NOT NULL DEFAULT '', -- logo_link is the URL the logo links to. Empty = no override. "logo_link" text NOT NULL DEFAULT '', -- message is the text shown below the logo on the login page. -- Empty = no override. Max 2000 characters enforced at the CP layer. "message" text NOT NULL DEFAULT '', "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_login_brand_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_login_brand_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_login_brand' AND indexname = 'site_login_brand_tenant_idx' ) THEN CREATE INDEX "site_login_brand_tenant_idx" ON "public"."site_login_brand" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_login_brand" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_login_brand" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_login_brand' AND policyname = 'site_login_brand_tenant_isolation' ) THEN CREATE POLICY "site_login_brand_tenant_isolation" ON "public"."site_login_brand" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_login_brand' AND policyname = 'site_login_brand_agent' ) THEN CREATE POLICY "site_login_brand_agent" ON "public"."site_login_brand" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (594.394167ms) -- migrating version 20260531010000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."scan_runs" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "kind" text NOT NULL DEFAULT 'core', -- 'core'|'files'|'full' "status" text NOT NULL DEFAULT 'queued', -- 'queued'|'scanning'|'diffing'|'done'|'failed' -- cursor is the resume_cursor JSON the agent returned in the last batch. -- NULL means we have not started the first scan command yet. "cursor" jsonb, "files_scanned" bigint NOT NULL DEFAULT 0, "wp_version" text, "locale" text, "error" text, -- finding_counts is a JSONB map: {core_modified:N, core_missing:N, core_unknown_injected:N} "finding_counts" jsonb, "created_at" timestamptz NOT NULL DEFAULT now(), "started_at" timestamptz, "finished_at" timestamptz, CONSTRAINT "scan_runs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "scan_runs_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'scan_runs' AND indexname = 'scan_runs_tenant_site_created_idx' ) THEN CREATE INDEX "scan_runs_tenant_site_created_idx" ON "public"."scan_runs" ("tenant_id", "site_id", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."scan_runs" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."scan_runs" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_runs' AND policyname = 'scan_runs_tenant_isolation' ) THEN CREATE POLICY "scan_runs_tenant_isolation" ON "public"."scan_runs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_runs' AND policyname = 'scan_runs_agent' ) THEN CREATE POLICY "scan_runs_agent" ON "public"."scan_runs" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."scan_run_hashes" ( "id" bigserial PRIMARY KEY, "tenant_id" uuid NOT NULL, "run_id" uuid NOT NULL, "path" text NOT NULL, "size" bigint, "md5" text, -- 32 hex chars or '' for unreadable "mtime" bigint, -- Unix seconds from agent "is_link" boolean NOT NULL DEFAULT false, UNIQUE ("run_id", "path"), CONSTRAINT "scan_run_hashes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "scan_run_hashes_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "public"."scan_runs" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'scan_run_hashes' AND indexname = 'scan_run_hashes_run_idx' ) THEN CREATE INDEX "scan_run_hashes_run_idx" ON "public"."scan_run_hashes" ("run_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."scan_run_hashes" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."scan_run_hashes" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_run_hashes' AND policyname = 'scan_run_hashes_tenant_isolation' ) THEN CREATE POLICY "scan_run_hashes_tenant_isolation" ON "public"."scan_run_hashes" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_run_hashes' AND policyname = 'scan_run_hashes_agent' ) THEN CREATE POLICY "scan_run_hashes_agent" ON "public"."scan_run_hashes" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."scan_findings" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "run_id" uuid NOT NULL, -- finding_type: 'core_modified'|'core_missing'|'core_unknown_injected' "finding_type" text NOT NULL, "path" text NOT NULL, -- severity: 'high'|'medium' "severity" text NOT NULL, "expected_md5" text, "actual_md5" text, -- dedup_key = md5(site_id || ':' || finding_type || ':' || path) "dedup_key" text NOT NULL, "ignored" boolean NOT NULL DEFAULT false, "ignored_by" text, "created_at" timestamptz NOT NULL DEFAULT now(), "last_seen_run" uuid NOT NULL, UNIQUE ("tenant_id", "site_id", "dedup_key"), CONSTRAINT "scan_findings_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "scan_findings_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'scan_findings' AND indexname = 'scan_findings_tenant_site_idx' ) THEN CREATE INDEX "scan_findings_tenant_site_idx" ON "public"."scan_findings" ("tenant_id", "site_id", "ignored", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."scan_findings" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."scan_findings" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_findings' AND policyname = 'scan_findings_tenant_isolation' ) THEN CREATE POLICY "scan_findings_tenant_isolation" ON "public"."scan_findings" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_findings' AND policyname = 'scan_findings_agent' ) THEN CREATE POLICY "scan_findings_agent" ON "public"."scan_findings" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wporg_core_checksums" ( "version" text NOT NULL, "locale" text NOT NULL, "path" text NOT NULL, "md5" text NOT NULL, "fetched_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("version", "locale", "path") ); END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wporg_core_checksums_meta" ( "version" text NOT NULL, "locale" text NOT NULL, "fetched_at" timestamptz NOT NULL DEFAULT now(), -- ok=false means the fetch failed (404/empty); used for negative-cache TTL. "ok" boolean NOT NULL DEFAULT true, PRIMARY KEY ("version", "locale") ); END; $$; -- ok (1.425058833s) -- migrating version 20260531020000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."restore_runs" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "snapshot_id" uuid NOT NULL, "mode" text NOT NULL DEFAULT '', "components" text[] NOT NULL DEFAULT '{}', "selection" jsonb NOT NULL DEFAULT '{}', "status" text NOT NULL DEFAULT 'queued', "current_phase" text, "error" text, "triggered_by" text, "created_at" timestamptz NOT NULL DEFAULT now(), "started_at" timestamptz, "finished_at" timestamptz, "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "restore_runs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "restore_runs_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'restore_runs' AND indexname = 'restore_runs_tenant_site_created_idx' ) THEN CREATE INDEX "restore_runs_tenant_site_created_idx" ON "public"."restore_runs" ("tenant_id", "site_id", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'restore_runs' AND indexname = 'restore_runs_snapshot_status_idx' ) THEN CREATE INDEX "restore_runs_snapshot_status_idx" ON "public"."restore_runs" ("snapshot_id", "status"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."restore_runs" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."restore_runs" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_runs' AND policyname = 'restore_runs_tenant_isolation' ) THEN CREATE POLICY "restore_runs_tenant_isolation" ON "public"."restore_runs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_runs' AND policyname = 'restore_runs_agent' ) THEN CREATE POLICY "restore_runs_agent" ON "public"."restore_runs" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."restore_run_events" ( "id" bigserial PRIMARY KEY, "tenant_id" uuid NOT NULL, "restore_run_id" uuid NOT NULL, "phase" text NOT NULL, "status" text NOT NULL DEFAULT '', "message" text NOT NULL DEFAULT '', "detail" jsonb, "occurred_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "restore_run_events_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "restore_run_events_restore_run_id_fkey" FOREIGN KEY ("restore_run_id") REFERENCES "public"."restore_runs" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'restore_run_events' AND indexname = 'restore_run_events_run_id_idx' ) THEN CREATE INDEX "restore_run_events_run_id_idx" ON "public"."restore_run_events" ("restore_run_id", "id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."restore_run_events" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."restore_run_events" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_run_events' AND policyname = 'restore_run_events_tenant_isolation' ) THEN CREATE POLICY "restore_run_events_tenant_isolation" ON "public"."restore_run_events" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_run_events' AND policyname = 'restore_run_events_agent' ) THEN CREATE POLICY "restore_run_events_agent" ON "public"."restore_run_events" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.002558125s) -- migrating version 20260531030000 -> DO $$ BEGIN ALTER TABLE "public"."backup_schedules" ADD COLUMN IF NOT EXISTS "run_hour" smallint NOT NULL DEFAULT 2, ADD COLUMN IF NOT EXISTS "run_minute" smallint NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS "day_of_week" smallint NULL, ADD COLUMN IF NOT EXISTS "day_of_month" smallint NULL, ADD COLUMN IF NOT EXISTS "frequency_hours" smallint NULL, ADD COLUMN IF NOT EXISTS "keep_last" integer NOT NULL DEFAULT 7; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_run_hour_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_run_hour_check" CHECK ("run_hour" BETWEEN 0 AND 23); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_run_minute_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_run_minute_check" CHECK ("run_minute" BETWEEN 0 AND 59); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_day_of_week_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_day_of_week_check" CHECK ("day_of_week" BETWEEN 0 AND 6); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_day_of_month_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_day_of_month_check" CHECK ("day_of_month" BETWEEN 1 AND 28); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_frequency_hours_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_frequency_hours_check" CHECK ("frequency_hours" BETWEEN 1 AND 24); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_keep_last_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_keep_last_check" CHECK ("keep_last" >= 0); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_cadence_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_cadence_check" CHECK ("cadence" IN ('hourly','every_n_hours','daily','weekly','monthly')); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'backup_schedules_kind_check' AND conrelid = 'public.backup_schedules'::regclass ) THEN ALTER TABLE "public"."backup_schedules" ADD CONSTRAINT "backup_schedules_kind_check" CHECK ("kind" IN ('files','db','full')); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "wp_timezone" text NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS "wp_gmt_offset" real NOT NULL DEFAULT 0; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."backup_schedule_runs" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "schedule_id" uuid NOT NULL, "snapshot_id" uuid NULL, "scheduled_for" timestamptz NOT NULL, "status" text NOT NULL DEFAULT 'scheduled', "kind" text NOT NULL DEFAULT 'full', "error" text NULL, "triggered_by" text NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "started_at" timestamptz NULL, "finished_at" timestamptz NULL, "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "backup_schedule_runs_status_check" CHECK ("status" IN ('scheduled','queued','running','completed','failed','skipped','canceled')), CONSTRAINT "backup_schedule_runs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_schedule_runs_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_schedule_runs_schedule_id_fkey" FOREIGN KEY ("schedule_id") REFERENCES "public"."backup_schedules" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_schedule_runs_snapshot_id_fkey" FOREIGN KEY ("snapshot_id") REFERENCES "public"."backup_snapshots" ("id") ON UPDATE NO ACTION ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND indexname = 'backup_schedule_runs_tenant_site_for_idx' ) THEN CREATE INDEX "backup_schedule_runs_tenant_site_for_idx" ON "public"."backup_schedule_runs" ("tenant_id", "site_id", "scheduled_for" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND indexname = 'backup_schedule_runs_status_for_idx' ) THEN CREATE INDEX "backup_schedule_runs_status_for_idx" ON "public"."backup_schedule_runs" ("status", "scheduled_for"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND indexname = 'backup_schedule_runs_schedule_id_idx' ) THEN CREATE INDEX "backup_schedule_runs_schedule_id_idx" ON "public"."backup_schedule_runs" ("schedule_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND indexname = 'backup_schedule_runs_schedule_for_key' ) THEN CREATE UNIQUE INDEX "backup_schedule_runs_schedule_for_key" ON "public"."backup_schedule_runs" ("schedule_id", "scheduled_for"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."backup_schedule_runs" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."backup_schedule_runs" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND policyname = 'backup_schedule_runs_tenant_isolation' ) THEN CREATE POLICY "backup_schedule_runs_tenant_isolation" ON "public"."backup_schedule_runs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND policyname = 'backup_schedule_runs_agent' ) THEN CREATE POLICY "backup_schedule_runs_agent" ON "public"."backup_schedule_runs" FOR ALL USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.563207291s) -- migrating version 20260531040000 -> DO $$ BEGIN ALTER TABLE "public"."sites" DISABLE ROW LEVEL SECURITY; UPDATE "public"."sites" s SET wp_timezone = COALESCE(NULLIF(d.payload->>'timezone', ''), s.wp_timezone), wp_gmt_offset = COALESCE(NULLIF(d.payload->>'gmt_offset', '')::real, s.wp_gmt_offset) FROM "public"."agent_diagnostics" d WHERE d.site_id = s.id AND d.tenant_id = s.tenant_id AND d.category = 'identity'; ALTER TABLE "public"."sites" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."sites" FORCE ROW LEVEL SECURITY; END; $$; -- ok (332.618834ms) -- migrating version 20260531050000 -> CREATE EXTENSION IF NOT EXISTS citext; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sites_id_tenant_key' AND conrelid = 'public.sites'::regclass ) THEN ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_id_tenant_key" UNIQUE ("id", "tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'memberships_role_check' AND conrelid = 'public.memberships'::regclass ) THEN ALTER TABLE "public"."memberships" ADD CONSTRAINT "memberships_role_check" CHECK (role IN ('owner', 'admin', 'operator', 'viewer')); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_shares" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "user_id" uuid NOT NULL, "role" text NOT NULL DEFAULT 'viewer' CHECK (role IN ('viewer', 'operator', 'admin')), "granted_by" uuid NULL, "expires_at" timestamptz NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "site_shares_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE, CONSTRAINT "site_shares_granted_by_fkey" FOREIGN KEY ("granted_by") REFERENCES "public"."users" ("id") ON DELETE SET NULL, CONSTRAINT "site_shares_site_tenant_fkey" FOREIGN KEY ("site_id", "tenant_id") REFERENCES "public"."sites" ("id", "tenant_id") ON DELETE CASCADE, CONSTRAINT "site_shares_site_user_key" UNIQUE ("site_id", "user_id") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_shares' AND indexname = 'site_shares_user_id_idx' ) THEN CREATE INDEX "site_shares_user_id_idx" ON "public"."site_shares" ("user_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_shares' AND indexname = 'site_shares_tenant_id_idx' ) THEN CREATE INDEX "site_shares_tenant_id_idx" ON "public"."site_shares" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_shares" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_shares" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_shares' AND policyname = 'site_shares_tenant_isolation' ) THEN CREATE POLICY "site_shares_tenant_isolation" ON "public"."site_shares" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_shares' AND policyname = 'site_shares_self_read' ) THEN CREATE POLICY "site_shares_self_read" ON "public"."site_shares" FOR SELECT USING ("user_id" = nullif(current_setting('app.user_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."invitations" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "email" citext NOT NULL, "scope" text NOT NULL CHECK (scope IN ('org', 'site')), "site_id" uuid NULL, "role" text NOT NULL, "token_hash" text NOT NULL, "invited_by" uuid NULL, "expires_at" timestamptz NOT NULL, "attempts" integer NOT NULL DEFAULT 0, "accepted_at" timestamptz NULL, "accepted_user_id" uuid NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "invitations_token_hash_key" UNIQUE ("token_hash"), CONSTRAINT "invitations_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON DELETE CASCADE, CONSTRAINT "invitations_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON DELETE CASCADE, CONSTRAINT "invitations_invited_by_fkey" FOREIGN KEY ("invited_by") REFERENCES "public"."users" ("id") ON DELETE SET NULL, CONSTRAINT "invitations_accepted_user_id_fkey" FOREIGN KEY ("accepted_user_id") REFERENCES "public"."users" ("id") ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'invitations' AND indexname = 'invitations_tenant_id_idx' ) THEN CREATE INDEX "invitations_tenant_id_idx" ON "public"."invitations" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'invitations' AND indexname = 'invitations_email_idx' ) THEN CREATE INDEX "invitations_email_idx" ON "public"."invitations" ("email"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."invitations" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."invitations" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'invitations' AND policyname = 'invitations_tenant_isolation' ) THEN CREATE POLICY "invitations_tenant_isolation" ON "public"."invitations" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'invitations' AND policyname = 'invitations_token_lookup' ) THEN CREATE POLICY "invitations_token_lookup" ON "public"."invitations" FOR SELECT USING (current_setting('app.invite_lookup', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'sites' AND policyname = 'sites_site_scope' ) THEN CREATE POLICY "sites_site_scope" ON "public"."sites" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'update_tasks' AND policyname = 'update_tasks_site_scope' ) THEN CREATE POLICY "update_tasks_site_scope" ON "public"."update_tasks" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_snapshots' AND policyname = 'backup_snapshots_site_scope' ) THEN CREATE POLICY "backup_snapshots_site_scope" ON "public"."backup_snapshots" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_schedules' AND policyname = 'backup_schedules_site_scope' ) THEN CREATE POLICY "backup_schedules_site_scope" ON "public"."backup_schedules" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_schedule_runs' AND policyname = 'backup_schedule_runs_site_scope' ) THEN CREATE POLICY "backup_schedule_runs_site_scope" ON "public"."backup_schedule_runs" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_runs' AND policyname = 'restore_runs_site_scope' ) THEN CREATE POLICY "restore_runs_site_scope" ON "public"."restore_runs" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_alert_state' AND policyname = 'site_alert_state_site_scope' ) THEN CREATE POLICY "site_alert_state_site_scope" ON "public"."site_alert_state" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_probes' AND policyname = 'site_uptime_probes_site_scope' ) THEN CREATE POLICY "site_uptime_probes_site_scope" ON "public"."site_uptime_probes" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'autologin_tokens' AND policyname = 'autologin_tokens_site_scope' ) THEN CREATE POLICY "autologin_tokens_site_scope" ON "public"."autologin_tokens" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'autologin_policies' AND policyname = 'autologin_policies_site_scope' ) THEN CREATE POLICY "autologin_policies_site_scope" ON "public"."autologin_policies" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_activity_log' AND policyname = 'agent_activity_log_site_scope' ) THEN CREATE POLICY "agent_activity_log_site_scope" ON "public"."agent_activity_log" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_diagnostics' AND policyname = 'agent_diagnostics_site_scope' ) THEN CREATE POLICY "agent_diagnostics_site_scope" ON "public"."agent_diagnostics" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_php_errors' AND policyname = 'agent_php_errors_site_scope' ) THEN CREATE POLICY "agent_php_errors_site_scope" ON "public"."agent_php_errors" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_login_events' AND policyname = 'agent_login_events_site_scope' ) THEN CREATE POLICY "agent_login_events_site_scope" ON "public"."agent_login_events" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'agent_nonces' AND policyname = 'agent_nonces_site_scope' ) THEN CREATE POLICY "agent_nonces_site_scope" ON "public"."agent_nonces" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_destinations' AND policyname = 'site_destinations_site_scope' ) THEN CREATE POLICY "site_destinations_site_scope" ON "public"."site_destinations" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_error_config' AND policyname = 'site_error_config_site_scope' ) THEN CREATE POLICY "site_error_config_site_scope" ON "public"."site_error_config" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_login_brand' AND policyname = 'site_login_brand_site_scope' ) THEN CREATE POLICY "site_login_brand_site_scope" ON "public"."site_login_brand" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_config' AND policyname = 'site_security_config_site_scope' ) THEN CREATE POLICY "site_security_config_site_scope" ON "public"."site_security_config" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_runs' AND policyname = 'scan_runs_site_scope' ) THEN CREATE POLICY "scan_runs_site_scope" ON "public"."scan_runs" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_findings' AND policyname = 'scan_findings_site_scope' ) THEN CREATE POLICY "scan_findings_site_scope" ON "public"."scan_findings" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'scan_run_hashes' AND policyname = 'scan_run_hashes_site_scope' ) THEN CREATE POLICY "scan_run_hashes_site_scope" ON "public"."scan_run_hashes" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "run_id" IN ( SELECT "id" FROM "public"."scan_runs" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "run_id" IN ( SELECT "id" FROM "public"."scan_runs" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_manifest_entries' AND policyname = 'backup_manifest_entries_site_scope' ) THEN CREATE POLICY "backup_manifest_entries_site_scope" ON "public"."backup_manifest_entries" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "snapshot_id" IN ( SELECT "id" FROM "public"."backup_snapshots" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "snapshot_id" IN ( SELECT "id" FROM "public"."backup_snapshots" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'restore_run_events' AND policyname = 'restore_run_events_site_scope' ) THEN CREATE POLICY "restore_run_events_site_scope" ON "public"."restore_run_events" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "restore_run_id" IN ( SELECT "id" FROM "public"."restore_runs" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "restore_run_id" IN ( SELECT "id" FROM "public"."restore_runs" WHERE "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) ); END IF; END; $$; -- ok (3.008789709s) -- migrating version 20260531060000 -> ALTER TABLE invitations ADD COLUMN IF NOT EXISTS revoked_at timestamptz; -> ALTER TABLE invitations ADD COLUMN IF NOT EXISTS revoked_by uuid REFERENCES users (id) ON DELETE SET NULL; -> CREATE INDEX IF NOT EXISTS invitations_site_id_idx ON invitations (site_id, created_at DESC) WHERE scope = 'site'; -- ok (450.843875ms) -- migrating version 20260531070000 -> ALTER TABLE sites ADD COLUMN IF NOT EXISTS connection_state text NOT NULL DEFAULT 'pending_enrollment' CHECK (connection_state IN ('pending_enrollment','connected','degraded','disconnected','revoked','archived')); -> ALTER TABLE sites ADD COLUMN IF NOT EXISTS connection_generation integer NOT NULL DEFAULT 0; -> ALTER TABLE sites ADD COLUMN IF NOT EXISTS disconnected_at timestamptz; -> ALTER TABLE sites ADD COLUMN IF NOT EXISTS disconnected_reason text; -> ALTER TABLE sites ADD COLUMN IF NOT EXISTS archived_at timestamptz; -> UPDATE sites SET connection_state = 'connected' WHERE connection_state = 'pending_enrollment' AND status = 'active'; -> CREATE INDEX IF NOT EXISTS idx_sites_connection_state ON sites (tenant_id, connection_state); -> CREATE INDEX IF NOT EXISTS idx_sites_last_seen ON sites (last_seen_at) WHERE connection_state IN ('connected','degraded'); -> ALTER TABLE pairing_codes ADD COLUMN IF NOT EXISTS site_id uuid REFERENCES sites (id) ON DELETE CASCADE; -> ALTER TABLE pairing_codes ADD COLUMN IF NOT EXISTS consumed_from_ip inet; -> CREATE INDEX IF NOT EXISTS idx_pairing_codes_site ON pairing_codes (site_id) WHERE site_id IS NOT NULL; -> CREATE TABLE IF NOT EXISTS site_connection_history ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, site_id uuid NOT NULL REFERENCES sites (id) ON DELETE CASCADE, from_state text NOT NULL, to_state text NOT NULL, reason text, actor_user_id uuid REFERENCES users (id) ON DELETE SET NULL, generation integer NOT NULL DEFAULT 0, occurred_at timestamptz NOT NULL DEFAULT now(), metadata jsonb NOT NULL DEFAULT '{}'::jsonb ); -> CREATE INDEX IF NOT EXISTS idx_conn_history_site ON site_connection_history (site_id, occurred_at DESC); -> ALTER TABLE site_connection_history ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_connection_history FORCE ROW LEVEL SECURITY; -> CREATE POLICY conn_history_tenant_isolation ON site_connection_history USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE TABLE IF NOT EXISTS site_events ( event_id text PRIMARY KEY, tenant_id uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, site_id uuid, -- nullable: some events are tenant-level (e.g. created) type text NOT NULL, data jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now() ); -> CREATE INDEX IF NOT EXISTS idx_site_events_tenant ON site_events (tenant_id, event_id); -> CREATE INDEX IF NOT EXISTS idx_site_events_created ON site_events (created_at); -> ALTER TABLE site_events ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_events FORCE ROW LEVEL SECURITY; -> CREATE POLICY site_events_tenant_isolation ON site_events USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); -- ok (1.762408167s) -- migrating version 20260531090000 -> DROP POLICY IF EXISTS conn_history_enroll ON site_connection_history; -> CREATE POLICY conn_history_enroll ON site_connection_history USING (current_setting('app.enroll', true) = 'on') WITH CHECK (current_setting('app.enroll', true) = 'on'); -> DROP POLICY IF EXISTS site_events_agent ON site_events; -> CREATE POLICY site_events_agent ON site_events USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (554.564042ms) -- migrating version 20260531100000 -> DROP POLICY IF EXISTS sites_shared_read ON sites; -> CREATE POLICY sites_shared_read ON sites FOR SELECT USING (EXISTS ( SELECT 1 FROM site_shares s WHERE s.site_id = sites.id AND s.user_id = nullif(current_setting('app.user_id', true), '')::uuid AND (s.expires_at IS NULL OR s.expires_at > now()) )); -- ok (384.840458ms) -- migrating version 20260531110000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_media_assets" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "wp_attachment_id" bigint NOT NULL, "title" text NOT NULL, "original_path" text NOT NULL, "original_url" text NOT NULL, "original_mime" text NOT NULL, "original_width" int, "original_height" int, "original_size_bytes" bigint NOT NULL, -- 'original'|'webp'|'avif' — the format the optimized variants are in now. "current_format" text NOT NULL DEFAULT 'original', "current_size_bytes" bigint NOT NULL, -- pending|optimizing|optimized|failed|restoring|restored|excluded|originals_deleted "status" text NOT NULL DEFAULT 'pending', "generation" int NOT NULL DEFAULT 0, "compression_level" text, -- 'lossy'|'lossless' at last optimization "target_format" text, -- requested format at last optimization "sizes_optimized" jsonb NOT NULL DEFAULT '[]'::jsonb, "sizes_unoptimized" jsonb NOT NULL DEFAULT '{}'::jsonb, -- map "last_optimized_at" timestamptz, "last_synced_at" timestamptz NOT NULL DEFAULT now(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), -- set by app code, NOT a trigger CONSTRAINT "site_media_assets_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_media_assets_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_media_assets_site_attachment_uniq" UNIQUE ("site_id", "wp_attachment_id") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_media_assets' AND indexname = 'site_media_assets_site_status_idx' ) THEN CREATE INDEX "site_media_assets_site_status_idx" ON "public"."site_media_assets" ("site_id", "status"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_media_assets' AND indexname = 'site_media_assets_tenant_idx' ) THEN CREATE INDEX "site_media_assets_tenant_idx" ON "public"."site_media_assets" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_media_assets" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_media_assets" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_media_assets' AND policyname = 'site_media_assets_tenant_isolation' ) THEN CREATE POLICY "site_media_assets_tenant_isolation" ON "public"."site_media_assets" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_media_assets' AND policyname = 'site_media_assets_agent' ) THEN CREATE POLICY "site_media_assets_agent" ON "public"."site_media_assets" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."media_optimization_jobs" ( "id" text PRIMARY KEY, -- ULID; the agent's wpmgr_job_id "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "asset_id" uuid, "wp_attachment_id" bigint NOT NULL, "kind" text NOT NULL, -- 'optimize'|'restore'|'delete_originals'|'sync' "target_format" text, -- 'avif'|'webp'|'original' "target_quality" text, -- 'lossy'|'lossless' -- queued|in_progress|succeeded|partially_succeeded|failed|cancelled "state" text NOT NULL DEFAULT 'queued', "bytes_before" bigint, "bytes_after" bigint, "variants_total" int NOT NULL DEFAULT 0, "variants_succeeded" int NOT NULL DEFAULT 0, "variants_failed" int NOT NULL DEFAULT 0, "error_reason" text, "initiator_user_id" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "started_at" timestamptz, "completed_at" timestamptz, CONSTRAINT "media_optimization_jobs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "media_optimization_jobs_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "media_optimization_jobs_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "public"."site_media_assets" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "media_optimization_jobs_initiator_fkey" FOREIGN KEY ("initiator_user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'media_optimization_jobs' AND indexname = 'media_optimization_jobs_site_state_idx' ) THEN CREATE INDEX "media_optimization_jobs_site_state_idx" ON "public"."media_optimization_jobs" ("site_id", "state"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'media_optimization_jobs' AND indexname = 'media_optimization_jobs_tenant_created_idx' ) THEN CREATE INDEX "media_optimization_jobs_tenant_created_idx" ON "public"."media_optimization_jobs" ("tenant_id", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."media_optimization_jobs" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."media_optimization_jobs" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'media_optimization_jobs' AND policyname = 'media_optimization_jobs_tenant_isolation' ) THEN CREATE POLICY "media_optimization_jobs_tenant_isolation" ON "public"."media_optimization_jobs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'media_optimization_jobs' AND policyname = 'media_optimization_jobs_agent' ) THEN CREATE POLICY "media_optimization_jobs_agent" ON "public"."media_optimization_jobs" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."media_variant_results" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "job_id" text NOT NULL, "tenant_id" uuid NOT NULL, "variant_name" text NOT NULL, -- 'full'|'thumbnail'|'medium'|'large'|... "source_size_bytes" bigint NOT NULL, "optimized_size_bytes" bigint, "source_mime" text NOT NULL, "optimized_mime" text, "encode_ms" int, "state" text NOT NULL, -- 'succeeded'|'failed'|'skipped' "reason" text, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "media_variant_results_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "public"."media_optimization_jobs" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "media_variant_results_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'media_variant_results' AND indexname = 'media_variant_results_job_idx' ) THEN CREATE INDEX "media_variant_results_job_idx" ON "public"."media_variant_results" ("job_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."media_variant_results" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."media_variant_results" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'media_variant_results' AND policyname = 'media_variant_results_tenant_isolation' ) THEN CREATE POLICY "media_variant_results_tenant_isolation" ON "public"."media_variant_results" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'media_variant_results' AND policyname = 'media_variant_results_agent' ) THEN CREATE POLICY "media_variant_results_agent" ON "public"."media_variant_results" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.415908292s) -- migrating version 20260601120000 -> ALTER TABLE "public"."site_media_assets" ADD COLUMN IF NOT EXISTS "sync_generation" bigint NOT NULL DEFAULT 0; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_media_assets' AND indexname = 'site_media_assets_site_syncgen_idx' ) THEN CREATE INDEX "site_media_assets_site_syncgen_idx" ON "public"."site_media_assets" ("site_id", "sync_generation"); END IF; END; $$; -> ALTER TABLE "public"."media_optimization_jobs" ADD COLUMN IF NOT EXISTS "sync_generation" bigint; -- ok (448.135583ms) -- migrating version 20260601130000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_media_settings" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- auto_optimize_enabled: opt-in toggle. Off by default — no existing -- behaviour changes until an operator explicitly enables it. "auto_optimize_enabled" boolean NOT NULL DEFAULT false, -- auto_target_format: avif | webp | original. "auto_target_format" text NOT NULL DEFAULT 'webp', -- auto_target_quality: lossy | lossless. "auto_target_quality" text NOT NULL DEFAULT 'lossy', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_media_settings_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_media_settings_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_media_settings' AND indexname = 'site_media_settings_tenant_idx' ) THEN CREATE INDEX "site_media_settings_tenant_idx" ON "public"."site_media_settings" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_media_settings" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_media_settings" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_media_settings' AND policyname = 'site_media_settings_tenant_isolation' ) THEN CREATE POLICY "site_media_settings_tenant_isolation" ON "public"."site_media_settings" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_media_settings' AND policyname = 'site_media_settings_agent' ) THEN CREATE POLICY "site_media_settings_agent" ON "public"."site_media_settings" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (586.382333ms) -- migrating version 20260601140000 -> DO $$ BEGIN ALTER TABLE "public"."site_media_assets" ADD COLUMN IF NOT EXISTS "variant_count" integer NOT NULL DEFAULT 0; ALTER TABLE "public"."site_media_assets" ADD COLUMN IF NOT EXISTS "saved_bytes" bigint NOT NULL DEFAULT 0; END; $$; -- ok (317.624375ms) -- migrating version 20260602000000 -> DO $$ BEGIN ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "agent_version" text NOT NULL DEFAULT ''; END; $$; -- ok (317.256458ms) -- migrating version 20260603000000 -> DO $$ BEGIN ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "host_provider" text NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS "host_provider_ip" text NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS "host_provider_checked_at" timestamptz; END; $$; -- ok (314.271ms) -- migrating version 20260603010000 -> DO $$ BEGIN ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "host_provider_org" text NOT NULL DEFAULT ''; END; $$; -- ok (313.567042ms) -- migrating version 20260603020000 -> CREATE TABLE IF NOT EXISTS "public"."smtp_settings" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "singleton" boolean NOT NULL DEFAULT true, "enabled" boolean NOT NULL DEFAULT false, "host" text NOT NULL DEFAULT '', "port" integer NOT NULL DEFAULT 587, "username" text NOT NULL DEFAULT '', "password_enc" bytea, "from_address" text NOT NULL DEFAULT '', "from_name" text NOT NULL DEFAULT '', "tls_mode" text NOT NULL DEFAULT 'starttls', "allow_insecure_tls" boolean NOT NULL DEFAULT false, "updated_by" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "smtp_settings_tls_mode_check" CHECK (tls_mode IN ('starttls', 'tls', 'none')), CONSTRAINT "smtp_settings_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "public"."users" ("id") ON DELETE SET NULL ); -> CREATE UNIQUE INDEX IF NOT EXISTS "smtp_settings_singleton_key" ON "public"."smtp_settings" ("singleton"); -> ALTER TABLE "public"."smtp_settings" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."smtp_settings" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "smtp_settings_agent" ON "public"."smtp_settings" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -> CREATE TABLE IF NOT EXISTS "public"."email_log" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid, "to_addresses" text[] NOT NULL, "subject" text NOT NULL, "template" text NOT NULL, "status" text NOT NULL DEFAULT 'pending', "error" text, "attempts" integer NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), "sent_at" timestamptz, PRIMARY KEY ("id"), CONSTRAINT "email_log_status_check" CHECK (status IN ('pending', 'sent', 'failed')), CONSTRAINT "email_log_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "email_log_tenant_created_idx" ON "public"."email_log" ("tenant_id", "created_at" DESC); -> CREATE INDEX IF NOT EXISTS "email_log_status_failed_idx" ON "public"."email_log" ("status") WHERE status = 'failed'; -> ALTER TABLE "public"."email_log" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_log" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "email_log_tenant_isolation" ON "public"."email_log" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "email_log_agent" ON "public"."email_log" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (1.089511042s) -- migrating version 20260603030000 -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "password_changed_at" timestamptz; -> CREATE TABLE IF NOT EXISTS "public"."password_reset_tokens" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "token_hash" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "used_at" timestamptz, "attempts" integer NOT NULL DEFAULT 0, "requested_ip" inet, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "password_reset_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE UNIQUE INDEX IF NOT EXISTS "password_reset_tokens_token_hash_key" ON "public"."password_reset_tokens" ("token_hash"); -> CREATE INDEX IF NOT EXISTS "password_reset_tokens_user_active_idx" ON "public"."password_reset_tokens" ("user_id") WHERE used_at IS NULL; -> ALTER TABLE "public"."password_reset_tokens" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."password_reset_tokens" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "password_reset_tokens_agent" ON "public"."password_reset_tokens" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (724.121083ms) -- migrating version 20260603040000 -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "status" text NOT NULL DEFAULT 'active'; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'users_status_check' ) THEN ALTER TABLE "public"."users" ADD CONSTRAINT "users_status_check" CHECK (status IN ('active', 'pending', 'disabled')); END IF; END$$; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "email_verified_at" timestamptz; -> UPDATE "public"."users" SET email_verified_at = now() WHERE email_verified_at IS NULL; -> CREATE TABLE IF NOT EXISTS "public"."email_verification_tokens" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "token_hash" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "used_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "email_verification_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE UNIQUE INDEX IF NOT EXISTS "email_verification_tokens_token_hash_key" ON "public"."email_verification_tokens" ("token_hash"); -> CREATE INDEX IF NOT EXISTS "email_verification_tokens_user_active_idx" ON "public"."email_verification_tokens" ("user_id") WHERE used_at IS NULL; -> ALTER TABLE "public"."email_verification_tokens" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_verification_tokens" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "email_verification_tokens_agent" ON "public"."email_verification_tokens" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -- ok (930.076083ms) -- migrating version 20260603050000 -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "is_superadmin" boolean NOT NULL DEFAULT false; -- ok (309.148292ms) -- migrating version 20260603060000 -> CREATE POLICY "memberships_agent" ON "public"."memberships" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -- ok (315.88975ms) -- migrating version 20260603070000 -> CREATE OR REPLACE FUNCTION "public"."admin_delete_empty_tenant"(p_tenant_id uuid) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_count integer; BEGIN PERFORM set_config('app.agent', 'on', true); IF EXISTS (SELECT 1 FROM memberships m WHERE m.tenant_id = p_tenant_id) OR EXISTS (SELECT 1 FROM sites s WHERE s.tenant_id = p_tenant_id) THEN RETURN false; END IF; PERFORM set_config('app.tenant_id', p_tenant_id::text, true); DELETE FROM audit_log WHERE tenant_id = p_tenant_id; PERFORM set_config('app.tenant_id', '', true); DELETE FROM tenants t WHERE t.id = p_tenant_id; GET DIAGNOSTICS v_count = ROW_COUNT; RETURN v_count > 0; END; $$; -> REVOKE ALL ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) FROM PUBLIC; -> GRANT EXECUTE ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) TO "wpmgr_app"; -- ok (451.894583ms) -- migrating version 20260603080000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_perf_config" ( "site_id" uuid PRIMARY KEY, "tenant_id" uuid NOT NULL, -- Caching "cache_enabled" boolean NOT NULL DEFAULT false, "cache_logged_in" boolean NOT NULL DEFAULT false, "cache_mobile" boolean NOT NULL DEFAULT false, "cache_refresh" boolean NOT NULL DEFAULT false, "cache_refresh_interval" text NOT NULL DEFAULT '2hours', "cache_link_prefetch" boolean NOT NULL DEFAULT true, "cache_bypass_urls" text[] NOT NULL DEFAULT '{}', "cache_bypass_cookies" text[] NOT NULL DEFAULT '{}', "cache_include_queries" text[] NOT NULL DEFAULT '{}', "cache_include_cookies" text[] NOT NULL DEFAULT '{}', -- CSS / JS "css_js_minify" boolean NOT NULL DEFAULT true, "css_rucss" boolean NOT NULL DEFAULT false, "css_rucss_include_selectors" text[] NOT NULL DEFAULT '{}', "css_js_self_host_third_party" boolean NOT NULL DEFAULT false, "js_delay" boolean NOT NULL DEFAULT false, "js_delay_method" text NOT NULL DEFAULT 'defer', "js_delay_excludes" text[] NOT NULL DEFAULT '{}', "js_delay_third_party" boolean NOT NULL DEFAULT false, "js_delay_third_party_excludes" text[] NOT NULL DEFAULT '{}', -- Fonts "fonts_display_swap" boolean NOT NULL DEFAULT true, "fonts_optimize_google" boolean NOT NULL DEFAULT false, "fonts_preload" boolean NOT NULL DEFAULT false, -- Media / lazy-load "lazy_load" boolean NOT NULL DEFAULT true, "lazy_load_exclusions" text[] NOT NULL DEFAULT '{}', "properly_size_images" boolean NOT NULL DEFAULT true, "youtube_placeholder" boolean NOT NULL DEFAULT false, "self_host_gravatars" boolean NOT NULL DEFAULT false, -- CDN "cdn_enabled" boolean NOT NULL DEFAULT false, "cdn_url" text, "cdn_file_types" text NOT NULL DEFAULT 'all', "cdn_provider" text, "cdn_credentials_encrypted" bytea, -- Database cleanup "db_auto_clean" boolean NOT NULL DEFAULT false, "db_auto_clean_interval" text NOT NULL DEFAULT 'weekly', "db_post_revisions" boolean NOT NULL DEFAULT false, "db_post_auto_drafts" boolean NOT NULL DEFAULT false, "db_post_trashed" boolean NOT NULL DEFAULT false, "db_comments_spam" boolean NOT NULL DEFAULT false, "db_comments_trashed" boolean NOT NULL DEFAULT false, "db_transients_expired" boolean NOT NULL DEFAULT false, "db_optimize_tables" boolean NOT NULL DEFAULT false, -- Bloat removal "bloat_disable_block_css" boolean NOT NULL DEFAULT false, "bloat_disable_dashicons" boolean NOT NULL DEFAULT false, "bloat_disable_emojis" boolean NOT NULL DEFAULT false, "bloat_disable_jquery_migrate" boolean NOT NULL DEFAULT false, "bloat_disable_xml_rpc" boolean NOT NULL DEFAULT false, "bloat_disable_rss_feed" boolean NOT NULL DEFAULT false, "bloat_disable_oembeds" boolean NOT NULL DEFAULT false, "bloat_heartbeat_control" boolean NOT NULL DEFAULT false, "bloat_post_revisions_control" boolean NOT NULL DEFAULT false, -- Server / install state (agent-reported) "server_software" text, "dropin_installed" boolean NOT NULL DEFAULT false, "wp_cache_constant_set" boolean NOT NULL DEFAULT false, "htaccess_managed" boolean NOT NULL DEFAULT false, "config_version" int NOT NULL DEFAULT 1, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), -- set by app code, NOT a trigger CONSTRAINT "site_perf_config_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_perf_config_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_perf_config' AND indexname = 'site_perf_config_tenant_idx' ) THEN CREATE INDEX "site_perf_config_tenant_idx" ON "public"."site_perf_config" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_perf_config" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_perf_config" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_perf_config' AND policyname = 'site_perf_config_tenant_isolation' ) THEN CREATE POLICY "site_perf_config_tenant_isolation" ON "public"."site_perf_config" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_perf_config' AND policyname = 'site_perf_config_agent' ) THEN CREATE POLICY "site_perf_config_agent" ON "public"."site_perf_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_cache_stats" ( "site_id" uuid PRIMARY KEY, "tenant_id" uuid NOT NULL, "cached_pages_count" int NOT NULL DEFAULT 0, "cache_size_bytes" bigint NOT NULL DEFAULT 0, "last_purged_at" timestamptz, "last_purge_kind" text, "last_preload_at" timestamptz, "preload_pending" int NOT NULL DEFAULT 0, "preload_total" int NOT NULL DEFAULT 0, "reported_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_cache_stats_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_cache_stats_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_cache_stats' AND indexname = 'site_cache_stats_tenant_idx' ) THEN CREATE INDEX "site_cache_stats_tenant_idx" ON "public"."site_cache_stats" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_cache_stats" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_cache_stats" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_stats' AND policyname = 'site_cache_stats_tenant_isolation' ) THEN CREATE POLICY "site_cache_stats_tenant_isolation" ON "public"."site_cache_stats" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_stats' AND policyname = 'site_cache_stats_agent' ) THEN CREATE POLICY "site_cache_stats_agent" ON "public"."site_cache_stats" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."cache_purge_audit" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "kind" text NOT NULL, -- 'all'|'url'|'post'|'preload'|'auto' "initiator_user_id" uuid, "target_urls" text[] NOT NULL DEFAULT '{}', "urls_count" int NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "cache_purge_audit_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "cache_purge_audit_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "cache_purge_audit_initiator_fkey" FOREIGN KEY ("initiator_user_id") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'cache_purge_audit' AND indexname = 'idx_cache_purge_site' ) THEN CREATE INDEX "idx_cache_purge_site" ON "public"."cache_purge_audit" ("site_id", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'cache_purge_audit' AND indexname = 'cache_purge_audit_tenant_idx' ) THEN CREATE INDEX "cache_purge_audit_tenant_idx" ON "public"."cache_purge_audit" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."cache_purge_audit" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."cache_purge_audit" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'cache_purge_audit' AND policyname = 'cache_purge_audit_tenant_isolation' ) THEN CREATE POLICY "cache_purge_audit_tenant_isolation" ON "public"."cache_purge_audit" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'cache_purge_audit' AND policyname = 'cache_purge_audit_agent' ) THEN CREATE POLICY "cache_purge_audit_agent" ON "public"."cache_purge_audit" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."rucss_results" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "structure_hash" text NOT NULL, -- hash of the page's structural signature "url" text, -- a representative URL that produced this structure "original_css_bytes" int, "used_css_bytes" int, "reduction_pct" numeric(5,2), "used_css_s3_key" text NOT NULL, -- object-storage key of the computed used CSS "selectors_total" int, "selectors_kept" int, "selectors_dropped" int, "compute_ms" int, "created_at" timestamptz NOT NULL DEFAULT now(), "last_used_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "rucss_results_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "rucss_results_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "rucss_results_site_hash_uniq" UNIQUE ("site_id", "structure_hash") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'rucss_results' AND indexname = 'idx_rucss_results_site' ) THEN CREATE INDEX "idx_rucss_results_site" ON "public"."rucss_results" ("site_id", "last_used_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'rucss_results' AND indexname = 'rucss_results_tenant_idx' ) THEN CREATE INDEX "rucss_results_tenant_idx" ON "public"."rucss_results" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."rucss_results" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."rucss_results" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_results' AND policyname = 'rucss_results_tenant_isolation' ) THEN CREATE POLICY "rucss_results_tenant_isolation" ON "public"."rucss_results" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_results' AND policyname = 'rucss_results_agent' ) THEN CREATE POLICY "rucss_results_agent" ON "public"."rucss_results" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."rucss_jobs" ( "id" text PRIMARY KEY, -- ULID "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "structure_hash" text, "url" text, -- queued|running|done|failed "state" text NOT NULL DEFAULT 'queued', "error_reason" text, "result_id" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "completed_at" timestamptz, CONSTRAINT "rucss_jobs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "rucss_jobs_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "rucss_jobs_result_id_fkey" FOREIGN KEY ("result_id") REFERENCES "public"."rucss_results" ("id") ON UPDATE NO ACTION ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'rucss_jobs' AND indexname = 'idx_rucss_jobs_site_state' ) THEN CREATE INDEX "idx_rucss_jobs_site_state" ON "public"."rucss_jobs" ("site_id", "state"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'rucss_jobs' AND indexname = 'rucss_jobs_tenant_idx' ) THEN CREATE INDEX "rucss_jobs_tenant_idx" ON "public"."rucss_jobs" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."rucss_jobs" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."rucss_jobs" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_jobs' AND policyname = 'rucss_jobs_tenant_isolation' ) THEN CREATE POLICY "rucss_jobs_tenant_isolation" ON "public"."rucss_jobs" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_jobs' AND policyname = 'rucss_jobs_agent' ) THEN CREATE POLICY "rucss_jobs_agent" ON "public"."rucss_jobs" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (2.301435959s) -- migrating version 20260604000000 -> DO $$ BEGIN ALTER TABLE "public"."site_perf_config" ADD COLUMN IF NOT EXISTS "preload_concurrency" integer NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS "preload_delay_ms" integer NOT NULL DEFAULT 500, ADD COLUMN IF NOT EXISTS "preload_batch_size" integer NOT NULL DEFAULT 50, ADD COLUMN IF NOT EXISTS "preload_max_load" real NOT NULL DEFAULT 0; END; $$; -- ok (368.986292ms) -- migrating version 20260604010000 -> DO $$ BEGIN ALTER TABLE "public"."site_perf_config" ADD COLUMN IF NOT EXISTS "next_db_clean_at" timestamptz; END; $$; -- ok (318.457958ms) -- migrating version 20260604020000 -> CREATE TABLE IF NOT EXISTS site_db_scan_results ( site_id uuid NOT NULL, tenant_id uuid NOT NULL, job_id text NOT NULL, categories_json jsonb NOT NULL DEFAULT '{}', db_size_bytes bigint NOT NULL DEFAULT 0, table_count int NOT NULL DEFAULT 0, scanned_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT site_db_scan_results_pkey PRIMARY KEY (site_id) ); -> CREATE INDEX IF NOT EXISTS site_db_scan_results_tenant_idx ON site_db_scan_results (tenant_id); -> ALTER TABLE site_db_scan_results ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_db_scan_results FORCE ROW LEVEL SECURITY; -> CREATE POLICY site_db_scan_results_tenant_isolation ON site_db_scan_results USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY site_db_scan_results_agent ON site_db_scan_results USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); -> DO $$ BEGIN ALTER TABLE "public"."site_perf_config" ADD COLUMN IF NOT EXISTS "active_db_clean_job_id" text, ADD COLUMN IF NOT EXISTS "active_db_clean_started" timestamptz, ADD COLUMN IF NOT EXISTS "active_db_scan_job_id" text, ADD COLUMN IF NOT EXISTS "active_db_scan_started" timestamptz; END; $$; -- ok (741.089667ms) -- migrating version 20260604030000 -> ALTER TABLE site_db_scan_results ADD COLUMN IF NOT EXISTS tables_json jsonb NOT NULL DEFAULT '[]'; -- ok (317.009167ms) -- migrating version 20260605000000 -> CREATE TABLE plugin_signatures ( slug text NOT NULL, corpus_version integer NOT NULL DEFAULT 1, option_patterns jsonb NOT NULL DEFAULT '[]', transient_patterns jsonb NOT NULL DEFAULT '[]', table_patterns jsonb NOT NULL DEFAULT '[]', cron_hook_patterns jsonb NOT NULL DEFAULT '[]', updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT plugin_signatures_pkey PRIMARY KEY (slug) ); -> CREATE INDEX plugin_signatures_corpus_version_idx ON plugin_signatures (corpus_version); -> ALTER TABLE plugin_signatures ENABLE ROW LEVEL SECURITY; -> CREATE POLICY plugin_signatures_read ON plugin_signatures FOR SELECT USING (true); -- ok (561.098166ms) -- migrating version 20260605010000 -> INSERT INTO plugin_signatures (slug, corpus_version, option_patterns, transient_patterns, table_patterns, cron_hook_patterns, updated_at) VALUES ('contact-form-7', 1, '["^wpcf7_","wpcf7_contact_form_properties"]', '["^wpcf7_"]', '[]', '["wpcf7_cleanup_post_meta","wpcf7_autosave"]', now()), ('woocommerce', 1, '["^woocommerce_","woocommerce_version","woocommerce_db_version","woocommerce_installed_on"]', '["^woocommerce_"]', '["^wp_woocommerce","^wp_wc_"]', '["^woocommerce_","woocommerce_cleanup_sessions","woocommerce_delete_product_transients","wc_delete_expired_transients","wc_tracker_send_event","woocommerce_run_update_callback"]', now()), ('elementor', 1, '["^elementor_","elementor_version","elementor_db_version","elementor_experiment_","elementor_pro_license"]', '["^elementor_"]', '["^wp_e_","^wp_elementor"]', '["^elementor_","elementor_background_library_batch_process","elementor_page_on_load","elementor_clear_css"]', now()), ('wordfence', 1, '["^wordfence","wordfenceActivated","wordfence_version"]', '["^wordfence"]', '["^wp_wfblockediplog","^wp_wfconfig","^wp_wfcrawlers","^wp_wffilechanges","^wp_wffilemods","^wp_wfhits","^wp_wfhoover","^wp_wfissues","^wp_wfknownfilelist","^wp_wflivetraffichuman","^wp_wfls_","^wp_wfnet","^wp_wfnotifications","^wp_wfpendingissues","^wp_wfreversecache","^wp_wfsnipcache","^wp_wfstatus","^wp_wftrafficrates","^wp_wfvulnscanners"]', '["^wordfence_","wfLiveTraffic","wflsRemediateSettings","wf_activity_log_send_data","wfpUpdateIPRanges"]', now()), ('yoast-seo', 1, '["^wpseo_","^yoast_","wpseo","wpseo_version","yoast_seo_installation_success_page"]', '["^wpseo_","^yoast_"]', '["^wp_yoast_","^wp_seo_"]', '["^wpseo_","^yoast_","yoast_global_option_save","yoast_update_license_info_remote"]', now()), ('akismet', 1, '["akismet_available_servers","akismet_connectivity_check_failed","akismet_delay_spam_check","akismet_debuglog","akismet_saved_queries","akismet_spam_count","akismet_validate_key","akismet_version","wordpress_api_key","akismet_strictness","akismet_show_user_comments_approved"]', '["^akismet_"]', '[]', '["akismet_schedule_cron_recheck","akismet_scheduled_delete"]', now()), ('jetpack', 1, '["^jetpack_","jetpack_activated","jetpack_options","jetpack_site_products","jetpack_sync_","jetpack_private_options"]', '["^jetpack_"]', '[]', '["^jetpack_","jetpack_clean_nonces","jetpack_v2_heartbeat","jetpack_plugin_update_check","jetpack_sync_process_queue"]', now()), ('all-in-one-seo-pack', 1, '["^aioseo_","^aioseop_","all_in_one_seo_pack","aioseo_version"]', '["^aioseo_","^aioseop_"]', '["^wp_aioseo_"]', '["^aioseo_","aioseo_cron_activate_license","aioseo_cron_check_updates"]', now()), ('duplicate-post', 1, '["^duplicate_post_","duplicate_post_version"]', '["^duplicate_post_"]', '[]', '[]', now()), ('wpforms-lite', 1, '["^wpforms_","wpforms_version","wpforms_db_version","wpforms_activation_date","wpforms_license"]', '["^wpforms_"]', '["^wp_wpforms"]', '["^wpforms_","wpforms_email_summaries_cron","wpforms_entry_purge_old","wpforms_weekly_entries_count"]', now()), ('wp-super-cache', 1, '["wp_super_cache_hash","wp_cache_config","supercachedir","wp_cache_clear_on_post_edit","wp_cache_enabled","wp_cache_mobile_groups","wp_cache_no_cache_for_get"]', '["^wp_super_cache","^wp_cache_"]', '[]', '["^wp_cache_","wp_super_cache_prune_log"]', now()), ('w3-total-cache', 1, '["^w3tc_","w3tc_compat","w3-total-cache-config"]', '["^w3tc_"]', '["^wp_w3tc_"]', '["^w3tc_","w3tc_flush_all","w3tc_pgcache_prime","w3tc_objectcache_prime"]', now()), ('updraftplus', 1, '["^updraft","updraftplus","updraftplus_version","updraftplus_lastmessage","updraftplus_backup_history","updraftplus_schedule_backup_interval","updraftplus_retention","updraftplus_s3_settings","updraftplus_googledrive"]', '["^updraft"]', '[]', '["^updraftplus_","updraftplus_backup_resume","updraftplus_checklogin","updraftplus_delete_old_dirs","updraftplus_disk_space_check"]', now()), ('bbpress', 1, '["^_bbpress","bbpress_db_version"]', '["^_bbpress"]', '["^wp_bbp_"]', '["bbp_check_for_updates","bbp_make_ham_spammer"]', now()), ('buddypress', 1, '["^buddypress","buddypress-db-version","bp_db_version"]', '["^buddypress"]', '["^wp_bp_","^wp_buddypress"]', '["^buddypress","bp_mail_send","bp_xprofile_sync_bp_latest_update"]', now()), ('woocommerce-subscriptions', 1, '["^wc_subscriptions_","woocommerce_subscriptions_version","wc_subscriptions_stripe_settings"]', '["^wc_subscriptions_"]', '["^wp_wcs_"]', '["wc_subscriptions_maybe_log_subscription_payment_failure","woocommerce_scheduled_subscription_"]', now()), ('the-events-calendar', 1, '["^tribe_","^tribe-events","tribe_events_version","tribe_db_version","tribe_options"]', '["^tribe_","^tribe-"]', '["^wp_tribe_","^wp_tec_"]', '["^tribe_","tribe-cron","tribe_events_update_version"]', now()), ('wpml-multilingual-cms', 1, '["^wpml_","icl_sitepress_version","wpml_version","sitepress_settings"]', '["^wpml_"]', '["^wp_icl_","^wp_wpml_"]', '["^wpml_","wpml_cache_cleanup","wpml_jobs_cleanup"]', now()), ('gravityforms', 1, '["^gform_","^gravityforms","gform_version","gravity_forms_version","gravityformsaddon_","gform_enable_logging"]', '["^gform_","^gravityforms"]', '["^wp_rg_form","^wp_gf_"]', '["^gravityforms_","gforms_cron","gravity_forms_send_usage_data"]', now()), ('ninja-forms', 1, '["^ninja_forms_","ninja_forms_version","nf_db_upgrade_version","ninja_forms_license"]', '["^ninja_forms_"]', '["^wp_nf_"]', '["^ninja_forms_","nf_send_usage_data","nf_cron_queue_runner"]', now()), ('wp-migrate-db', 1, '["^wpmdb_","wpmdb_version","wpmdb_licence","wpmdb_settings","wpmdb_upgrade_type"]', '["^wpmdb_"]', '[]', '["^wpmdb_","wpmdb_cleanup_temp_tables"]', now()), ('backwpup', 1, '["^backwpup_","backwpup_version"]', '["^backwpup_"]', '[]', '["^backwpup_","backwpup_cron"]', now()), ('all-in-one-wp-migration', 1, '["^ai1wm_","^servmask_","ai1wm_version","ai1wm_secret_key"]', '["^ai1wm_","^servmask_"]', '[]', '["^ai1wm_"]', now()), ('mailchimp-for-wp', 1, '["^mc4wp_","mc4wp_version","mc4wp_lite_version","mc4wp_license_key","mc4wp_checkbox_enabled","mc4wp_integration_"]', '["^mc4wp_"]', '[]', '["^mc4wp_","mc4wp_daily_digest"]', now()), ('really-simple-ssl', 1, '["^rsssl_","rlrsssl_version","really_simple_ssl_version","rsssl_activated","rsssl_options","rsssl_site_options"]', '["^rsssl_","^rlrsssl_"]', '[]', '["^rsssl_","rsssl_daily_cron","rsssl_task_runner"]', now()), ('redirection', 1, '["^redirection_","redirection_version","redirection_db_version","red_current_version"]', '["^redirection_"]', '["^wp_redirection"]', '["^redirection_","redirection_prune"]', now()), ('social-warfare', 1, '["^swfw_","swp_version","swfw_version"]', '["^swfw_"]', '[]', '["swp_cache_rebuild"]', now()), ('advanced-custom-fields', 1, '["acf_version","acf_db_version","acf_pro_license"]', '[]', '[]', '["acf_cleanup","acf_update_check"]', now()), ('post-smtp', 1, '["^postman_","^post_smtp_","postSMTP","postman_version","postman_options","^post_smtp_log_"]', '["^postman_","^post_smtp_"]', '[]', '["^post_smtp_","postman_send_test_email"]', now()), ('wp-rocket', 1, '["^wp_rocket_","wp_rocket_version","rocket_version","wp_rocket_settings","rocket_cache_reject_uri","rocket_pre_get_posts"]', '["^wp_rocket_","^rocket_"]', '[]', '["^rocket_","rocket_cron_deactivation_lock","rocket_database_optimization_process","rocket_facebook_pixel_beacon"]', now()), ('autoptimize', 1, '["^autoptimize_","autoptimize_version","autoptimize_feed","autoptimize_css","autoptimize_js"]', '["^autoptimize_"]', '[]', '["^autoptimize_","autoptimize_stats","ao_ccss_queue"]', now()), ('imagify', 1, '["^imagify_","imagify_version","imagify_settings","imagify_api_key","imagify_user_account"]', '["^imagify_"]', '[]', '["^imagify_","imagify_async_optimize_all_images"]', now()), ('ewww-image-optimizer', 1, '["^ewww_image_optimizer_","ewww_image_optimizer_version"]', '["^ewww_image_optimizer_"]', '["^wp_ewwwio_"]', '["^ewww_","ewww_image_optimizer_auto","ewww_image_optimizer_background_optimization"]', now()), ('smush', 1, '["^wp_smush_","wp_smushit_","^smush_","smush_version","smush_stats","smush_settings","wp_smush_api_key"]', '["^wp_smush_","^smush_"]', '[]', '["^wp_smush_","smush_send_smush_data","wp_smush_scan"]', now()), ('shortpixel-image-optimiser', 1, '["^wp-short-pixel","^_spio_","shotpixel_api_key","shortPixelNoticeReviewedAt","wp_short_pixel_","shortpixel_settings"]', '["^wp-short-pixel","^_spio_","^shortpixel_"]', '[]', '["^shortpixel_","wp_shortpixel_processes_start"]', now()), ('wp-optimize', 1, '["wp-optimize-version","wpoptimize_version","wpo_settings","wpo_version","wpo_cache_config"]', '["^wp-optimize"]', '[]', '["wpo_cron_event","wpo_cache_cron","wpo_scheduled_optimization"]', now()), ('clearfy', 1, '["^wbcr_clearfy_","^wbcr_inp_","clearfy_version"]', '["^wbcr_clearfy_","^wbcr_inp_"]', '[]', '["^wbcr_clearfy_"]', now()), ('cloudflare', 1, '["^cloudflare_","cloudflare_api_key","cloudflare_api_email","cloudflare_cached_domain_info","cloudflare_ips","cloudflare_version"]', '["^cloudflare_"]', '[]', '["^cloudflare_","cloudflare_purge_all_cache"]', now()), ('wp-fastest-cache', 1, '["^wpfc_","WpFastestCache","wpFastestCacheOptions","WpFastestCacheOptions","wpfc_exclude_keywords","wpfc_wpml_languages"]', '["^wpfc_","^WpFastestCache"]', '[]', '["^WpFastestCache","^wpfc_","wp_fastest_cache_","WpFastestCacheCronJob"]', now()), ('loco-translate', 1, '["^loco_","loco_translate_version","loco_options","loco_config_src"]', '["^loco_"]', '[]', '[]', now()), ('nextgen-gallery', 1, '["^nggallery_","ngg_version","ngg_db_version","nggallery_options","nextgen-gallery-version"]', '["^nggallery_"]', '["^wp_ngg_"]', '["nextgen-gallery-cache-flusher"]', now()), ('tablepress', 1, '["tablepress_db_version","tablepress_plugin_options","tablepress_tables_options","tablepress_version"]', '["^tablepress_"]', '["^wp_tablepress"]', '[]', now()), ('easy-digital-downloads', 1, '["easy_digital_downloads_version","edd_version","edd_db_version","edd_settings","edd_activation_date","edd_license_"]', '[]', '["^wp_edd_"]', '["edd_cleanup_file_symlinks","edd_weekly_scheduled_events","edd_daily_scheduled_events"]', now()), ('learndash', 1, '["^learndash_","^sfwd_","learndash_settings_","learndash_profile_course_","learndash_version"]', '["^learndash_","^sfwd_"]', '["^wp_sfwd_","^wp_learndash_"]', '["^learndash_","^sfwd_cron_","learndash_daily_digest"]', now()), ('tutor', 1, '["^tutor_","tutor_version","tutor_db_version","tutor_option"]', '["^tutor_"]', '["^wp_tutor"]', '["^tutor_","tutor_scheduled_delete_incomplete_attempts"]', now()), ('lifterlms', 1, '["^llms_","^lifterlms_","lifterlms_version","llms_version","llms_db_version"]', '["^llms_","^lifterlms_"]', '["^wp_lifterlms"]', '["^llms_","llms_send_tracking_data"]', now()), ('wc-memberships', 1, '["^wc_memberships_","woocommerce_memberships_version"]', '["^wc_memberships_"]', '[]', '["^wc_memberships_","wc_memberships_delete_expired_tokens"]', now()), ('slimstat-analytics', 1, '["^wp_slim_stat_","slimstat_version","slimstat_db_version","slim_stat_options"]', '["^slim_stat_","^wp_slim_stat_"]', '["^wp_slim_stats"]', '["^slim_stat_","slim_stat_cleanup","slim_stat_daily_cleanup"]', now()), ('google-analytics-for-wordpress', 1, '["^monsterinsights_","^gadwp_","^googleanalytics_","monsterinsights_version","monsterinsights_settings","monsterinsights_license"]', '["^monsterinsights_","^gadwp_"]', '[]', '["^monsterinsights_","monsterinsights_check_authentication","monsterinsights_send_tracking_data"]', now()), ('analytify-ga-google-analytics', 1, '["^analytify_","analytify_version","analytify_options","analytify_profile","analytify_license"]', '["^analytify_"]', '[]', '["^analytify_","analytify_email_report"]', now()), ('seo-by-rank-math', 1, '["^rank_math_","^rank-math-","rank_math_version","rank_math_db_version","rank_math_settings","rank_math_modules"]', '["^rank_math_","^rank-math-"]', '["^wp_rank_math"]', '["^rank_math_","rank_math_send_analytics","rank_math_sitemap_ping"]', now()), ('wp-smush-pro', 1, '["^wp_smush_","wp_smushit_","smush_pro_","wp_smush_api_key"]', '["^wp_smush_","^smush_pro_"]', '[]', '["^wp_smush_","smush_send_smush_data"]', now()), ('beaver-builder-lite-version', 1, '["^fl_builder_","^fl_page_data","fl_builder_version","fl_builder_enabled","fl_builder_settings"]', '["^fl_builder_","^fl_page_data"]', '[]', '["^fl_builder_","fl_builder_schedule_import"]', now()), ('oxygen', 1, '["^oxygen_","ct_version","ct_db_version","ct_settings","oxygen_vsb_"]', '["^oxygen_"]', '[]', '["^oxygen_","ct_prune_log"]', now()), ('divi', 1, '["et_theme_version","et_bfb_updated","et_support_token","et_pb_","et_automatic_updates"]', '[]', '[]', '["et_core_update_components_schedule","et_daily_cleanup_cron"]', now()), ('avada', 1, '["^avada_","fusion_builder_","avada_version","fusion_options","avada_options"]', '["^avada_","^fusion_"]', '[]', '["^avada_","fusion_cron_save_form_entries"]', now()), ('astra', 1, '["^astra_","astra_version","astra_db_version","astra_theme_setup","astra_migration_","astra_sites_"]', '["^astra_"]', '[]', '["^astra_","astra_update_check"]', now()), ('generatepress', 1, '["^generate_","generate_version","generate_settings"]', '["^generate_"]', '[]', '[]', now()), ('envira-gallery-lite', 1, '["^envira_","envira_version","envira_db_version","envira_settings"]', '["^envira_"]', '["^wp_envira"]', '["^envira_","envira_auto_recovery"]', now()), ('soliloquy-lite', 1, '["^soliloquy_","soliloquy_version","soliloquy_settings"]', '["^soliloquy_"]', '[]', '[]', now()), ('wp-file-manager', 1, '["fm_version","fm_db_version","fm_folders_tree"]', '[]', '[]', '[]', now()), ('user-role-editor', 1, '["user_role_editor_version","user_role_editor","ure_capabilities_to_add","ure_capabilities_to_remove"]', '[]', '[]', '[]', now()), ('wp-members', 1, '["^wpmem_","wpmem_db_version","wpmembers_version"]', '["^wpmem_"]', '["^wp_wpmem_"]', '["^wpmem_"]', now()), ('s2member', 1, '["^s2member_","^ws_plugin_s2member_","s2member_version","s2member_configured"]', '["^s2member_","^ws_plugin_s2member_"]', '["^wp_s2member"]', '["^s2member_","s2member_garbage_collector"]', now()), ('memberpress', 1, '["^mepr_","^memberpress_","mepr_version","memberpress_version","mepr_activated"]', '["^mepr_","^memberpress_"]', '["^wp_mepr_"]', '["^mepr_","mepr_send_analytics","mepr_cron_cleanup"]', now()), ('paid-memberships-pro', 1, '["^pmpro_","pmpro_version","pmpro_db_version","pmpro_currency","pmpro_activation_date"]', '["^pmpro_"]', '["^wp_pmpro_"]', '["^pmpro_","pmpro_cron_expire_memberships","pmpro_cron_email_member_expiring"]', now()), ('translatepress-multilingual', 1, '["translatepress_version","trp_db_version","trp_settings"]', '[]', '["^wp_trp_"]', '[]', now()), ('polylang', 1, '["^polylang_","polylang_version","polylang"]', '["^polylang_"]', '[]', '[]', now()), ('wp-mail-smtp', 1, '["^wp_mail_smtp_","wp_mail_smtp","wpms_version","wp_mail_smtp_debug"]', '["^wp_mail_smtp_"]', '[]', '["^wp_mail_smtp_","wp_mail_smtp_send_failed_email_notification"]', now()), ('fluent-smtp', 1, '["^fluentmail_","^fluent_smtp_","fluentmail_settings","fluentmail_auth_token"]', '["^fluentmail_","^fluent_smtp_"]', '[]', '["^fluent_smtp_"]', now()), ('searchwp', 1, '["^searchwp_","searchwp_version","searchwp_installed_on","searchwp_license","searchwp_settings","searchwp_db_version"]', '["^searchwp_"]', '["^wp_searchwp"]', '["^searchwp_","searchwp_background_indexer","searchwp_purge_request_queue"]', now()), ('elasticpress', 1, '["^elasticpress_","ep_version","ep_db_version","ep_host","ep_credentials","ep_index_meta"]', '["^elasticpress_"]', '[]', '["^elasticpress_","ep_index","ep_sync_404","ep_daily_cleanup"]', now()), ('relevanssi', 1, '["^relevanssi_","relevanssi_version","relevanssi_db_version","relevanssi_my_stoplisted_words"]', '["^relevanssi_"]', '["^wp_relevanssi"]', '["^relevanssi_","relevanssi_update_index","relevanssi_daily_clear"]', now()), ('media-library-assistant', 1, '["^mlaCoreOptions","mla_version","mla_db_version"]', '[]', '["^wp_mla"]', '[]', now()), ('enable-media-replace', 1, '["emr_version","enable_media_replace_version"]', '[]', '[]', '[]', now()), ('filebird', 1, '["^filebird_","filebird_version","filebird_db_version"]', '["^filebird_"]', '["^wp_fbv"]', '[]', now()), ('real-media-library', 1, '["rml_version","rml_db_version"]', '[]', '["^wp_realmedialibrary"]', '["rml_clean"]', now()), ('wp-security-audit-log', 1, '["^wsal_","mwp_wsal_version","wpaudit_version","wsal_events_settings"]', '["^wsal_"]', '["^wp_wsal"]', '["^wsal_","wsal_search_archive","wsal_cleanup"]', now()), ('sucuri-scanner', 1, '["^sucuri_","sucuriscan_version","sucuriscan_settings","sucuri_cloudproxy_settings"]', '["^sucuri_","^sucuriscan_"]', '[]', '["^sucuri_","sucuriscan_core_integrity_check","sucuriscan_scan_checksums"]', now()), ('ithemes-security', 1, '["^itsec_","^itbr_","itsec_data","itsec_version","itsec_db_version","itsec_strong_passwords"]', '["^itsec_","^itbr_"]', '["^wp_itsec_"]', '["^itsec_","itsec_digest","itsec_malware_scheduled_scan","itsec_password_expiration_check"]', now()), ('loginizer', 1, '["^loginizer_","loginizer_version","loginizer_log_attempt"]', '["^loginizer_"]', '[]', '["^loginizer_","loginizer_cleanup_logs"]', now()), ('limit-login-attempts-reloaded', 1, '["^limit_login_","limit_login_attempt","limit_login_lockouts","limit_login_retries","limit_login_allowed_retries"]', '["^limit_login_"]', '[]', '["^limit_login_","limit_login_cleanup"]', now()), ('hide-my-wp', 1, '["^hmwp_","hide_my_wp_version","hide_my_wp_settings","hmwp_settings","hmwp_hash"]', '["^hmwp_","^hide_my_wp"]', '[]', '["^hmwp_","hmwp_cleanup","hmwp_update_check"]', now()), ('wps-hide-login', 1, '["whl_page","whl_version","wps_hide_login_page","wps_hide_login_version"]', '["^wps_hide_login_"]', '[]', '[]', now()), ('simple-history', 1, '["^simple_history_","simplehistory_version","simple-history_version","simple_history_db_version"]', '["^simple_history_"]', '["^wp_simple_history"]', '["^simple_history_","simple_history_cleanup_db"]', now()), ('stream', 1, '["^wp_stream_","stream_version","stream_db_version","stream_settings"]', '["^wp_stream_","^stream_"]', '["^wp_stream"]', '["^stream_","stream_cleanup"]', now()), ('woocommerce-bookings', 1, '["^wc_bookings_","woocommerce_bookings_version","wc_bookings_send_reminder_before"]', '["^wc_bookings_"]', '["^wp_wc_booking"]', '["^wc_bookings_","wc_bookings_reminder_cron","wc_bookings_cleanup_invalid_cron"]', now()), ('woocommerce-appointments', 1, '["^wc_appointments_","woocommerce_appointments_version"]', '["^wc_appointments_"]', '["^wp_wc_appointment"]', '["^wc_appointments_"]', now()), ('stripe', 1, '["^stripe_","stripe_version","stripe_settings","stripe_live_publishable_key","stripe_test_publishable_key"]', '["^stripe_"]', '[]', '["^stripe_","stripe_retry_webhooks"]', now()), ('woo-stripe-payment', 1, '["^wc_stripe_","woocommerce_stripe_version","stripe_gateway_account_data","wc_stripe_account_data"]', '["^wc_stripe_"]', '[]', '["^wc_stripe_","wc_stripe_webhook"]', now()), ('paypal-for-woocommerce', 1, '["^angelleye_","^paypal_","paypal_plus_version","paypal_ec_settings","angelleye_version"]', '["^angelleye_","^paypal_"]', '[]', '["^angelleye_","angelleye_ppcp_recurring_payment_cron"]', now()), ('metorik-helper', 1, '["^metorik_","metorik_version","metorik_token"]', '["^metorik_"]', '[]', '["^metorik_","metorik_send_data"]', now()), ('convertkit', 1, '["^convertkit_","convertkit_version","convertkit_settings","convertkit_api_key"]', '["^convertkit_"]', '[]', '["^convertkit_","convertkit_resource_type_forms","convertkit_send_purchase_data"]', now()), ('mailpoet', 1, '["^mailpoet_","mailPoet_version","mailpoet_db_version","mailpoet_premium_key","mailPoet_activated_at"]', '["^mailpoet_","^mailPoet_"]', '["^wp_mailpoet"]', '["^mailpoet_","mailpoet_send_scheduled_newsletters","mailpoet_bounce_sync_scheduled","mailpoet_delete_old_stats"]', now()), ('newsletter', 1, '["^newsletter_","newsletter_version","newsletter_db_version","newsletter_main"]', '["^newsletter_"]', '["^wp_newsletter"]', '["^newsletter_","newsletter_send","newsletter_stats","newsletter_maintenance"]', now()), ('wp-ses', 1, '["^wpses_","^wp_ses_","wp_ses_version","wpses_version","wpses_aws_key"]', '["^wpses_","^wp_ses_"]', '[]', '["^wpses_","wpses_send_log"]', now()), ('sendgrid-email-delivery-simplified', 1, '["^sendgrid_","sendgrid_version","sendgrid_apikey","sendgrid_settings"]', '["^sendgrid_"]', '[]', '[]', now()), ('give', 1, '["^give_","give_version","give_db_version","give_settings","give_license"]', '["^give_"]', '["^wp_give_"]', '["^give_","give_daily_scheduled_events","give_weekly_scheduled_events"]', now()), ('charitable', 1, '["^charitable_","charitable_version","charitable_db_version","charitable_settings"]', '["^charitable_"]', '["^wp_charitable"]', '["^charitable_","charitable_maybe_activate_license","charitable_maybe_dismiss_update_nag"]', now()), ('woocommerce-gateway-paypal-express-checkout', 1, '["^woocommerce_paypal_","ppec_paypal_","ppec_paypal_settings_","woocommerce_ppec_paypal_version"]', '["^ppec_paypal_","^woocommerce_paypal_"]', '[]', '["^ppec_paypal_","ppec_paypal_cart_flow_transient_cleanup"]', now()), ('wp-staging', 1, '["^wpstg_","wp_staging_version","wpstg_db_version","wpstg_settings","wpstg_optimizer"]', '["^wpstg_"]', '[]', '["^wpstg_"]', now()), ('duplicator', 1, '["^duplicator_","duplicator_version","duplicator_install_notice","duplicator_ui_css_version"]', '["^duplicator_"]', '[]', '["^duplicator_","duplicator_temp_cleanup"]', now()), ('duplicator-pro', 1, '["^duplicator_pro_","^duplicator_","duplicator_pro_version","duplicator_pro_license"]', '["^duplicator_pro_"]', '[]', '["^duplicator_pro_","duplicator_pro_cleanup_temp"]', now()), ('wp-clone-by-wp-academy', 1, '["^wp_clone_","wp_clone_version"]', '["^wp_clone_"]', '[]', '[]', now()), ('codepress-admin-columns', 1, '["^cpac_","cpac_version","cpac_db_version","cpac_storage_key"]', '["^cpac_","^ac_storage"]', '[]', '[]', now()), ('adminimize', 1, '["^adminimize_","_adminimize","adminimize_version"]', '["^adminimize_"]', '[]', '[]', now()), ('monarch', 1, '["^et_social_","monarch_version","monarch_settings"]', '["^et_social_"]', '[]', '[]', now()), ('vc-extensions-bundle', 1, '["vc_version","wpb_js_composer_version","js_composer_version"]', '[]', '[]', '["wpb_vc_update"]', now()), ('revslider', 1, '["^revslider_","revslider_version","rev_slider_version","revslider_update_check"]', '["^revslider_","^rev_slider_"]', '[]', '["^revslider_","revslider_update"]', now()), ('layerslider', 1, '["^layerslider_","layerslider_version","ls_db_version"]', '["^layerslider_"]', '[]', '["^layerslider_"]', now()), ('solvedby-wpml', 1, '["^sitepress_","^wpml_","sitepress_settings","icl_sitepress_version"]', '["^sitepress_"]', '["^wp_icl_"]', '["^sitepress_","sitepress_migration","wpml_cache"]', now()), ('wpdatatables', 1, '["^wpdatatables_","wpDataTables_version","wpdatatables_options","wpdatatables_db_version"]', '["^wpdatatables_","^wpDataTables_"]', '["^wp_wpdatatables"]', '["^wpdatatables_"]', now()), ('tablesmith', 1, '["^tablesmith_","tablesmith_version"]', '["^tablesmith_"]', '[]', '[]', now()), ('um-user-roles', 1, '["^ultimate_member_","um_version","um_db_version","um_is_installed","um_options"]', '["^ultimate_member_"]', '["^wp_um_"]', '["^ultimate_member_","um_notify_expire_memberships"]', now()), ('learnpress', 1, '["^learnpress_","lp_db_version","learnpress_version","lp_settings"]', '["^learnpress_"]', '["^wp_learnpress","^wp_lp_"]', '["^learnpress_","lp_send_usage_data","lp_cron_check"]', now()), ('wcfm', 1, '["^wcfm_","^wc_vendors_","wcfm_version","wcfmmp_version"]', '["^wcfm_","^wc_vendors_"]', '["^wp_wcfm"]', '["^wcfm_","wcfm_cron"]', now()), ('dokan-lite', 1, '["^dokan_","^wc_dokan_","dokan_version","dokan_db_version","dokan_settings"]', '["^dokan_","^wc_dokan_"]', '["^wp_dokan"]', '["^dokan_","dokan_withdraw_update","dokan_follow_store_cron"]', now()), ('permalink-manager', 1, '["^permalink_manager_","permalink_manager_db_version","permalink_manager_uris","permalink_manager_redirects"]', '["^permalink_manager_"]', '[]', '["^permalink_manager_","permalink_manager_update_uris"]', now()), ('broken-link-checker', 1, '["blc_version","blc_db_version","blc_options","blc_instances","blc_synch"]', '[]', '["^wp_blc_"]', '["blc_check_links","blc_delete_broken"]', now()), ('regenerate-thumbnails', 1, '["^regen_thumbs_","regenerate_thumbnails_version"]', '["^regen_thumbs_"]', '[]', '[]', now()), ('the-seo-framework', 1, '["^the_seo_framework_","tsf_version","the_seo_framework_db_version"]', '["^the_seo_framework_"]', '[]', '["^the_seo_framework_","tsf_sitemap_cron"]', now()), ('schema-and-structured-data-for-wp', 1, '["^saswp_","saswp_version","saswp_db_version","saswp_settings","saswp_post_"]', '["^saswp_"]', '[]', '["^saswp_","saswp_clear_cache"]', now()), ('woocommerce-product-addon', 1, '["^wc_product_addon_","pao_version","woocommerce_product_addons_version"]', '["^wc_product_addon_"]', '[]', '[]', now()), ('woocommerce-pdf-invoices-packing-slips', 1, '["^wpo_wcpdf_","woocommerce_pdf_invoices_packing_slips_version","wpo_wcpdf_settings_"]', '["^wpo_wcpdf_"]', '[]', '["^wpo_wcpdf_","wpo_wcpdf_daily_cleanup"]', now()), ('product-import-export-for-woo', 1, '["^xi_woocommerce_","xi_woocommerce_product_csv_import_export_version"]', '["^xi_woocommerce_"]', '[]', '[]', now()), ('cart-abandonment-recovery', 1, '["^cartflows_ca_","cartflows_ca_version","cartflows_ca_db_version","cartflows_ca_settings"]', '["^cartflows_ca_"]', '["^wp_cartflows_ca"]', '["^cartflows_ca_","cartflows_ca_delete_ghost_orders"]', now()), ('monsterinsights-lite', 1, '["^monsterinsights_","monsterinsights_version","monsterinsights_settings","monsterinsights_license","monsterinsights_installed_at"]', '["^monsterinsights_"]', '[]', '["^monsterinsights_","monsterinsights_reporting","monsterinsights_license_check"]', now()), ('optinmonster', 1, '["^optin_monster_","^optinmonster_","optin_monster_version","optinmonster_version","optin_monster_license"]', '["^optin_monster_","^optinmonster_"]', '[]', '["^optinmonster_","optin_monster_check_license"]', now()), ('pushengage', 1, '["^pushengage_","pushengage_version","pushengage_settings","pushengage_api_key"]', '["^pushengage_"]', '[]', '["^pushengage_"]', now()), ('abandoned-cart-lite-for-woocommerce', 1, '["^ac_wc_","^abandoned_cart_","abandoned_cart_version","ac_wc_enabled","tyche_softwares_debug_mode"]', '["^ac_wc_","^abandoned_cart_"]', '["^wp_ac_"]', '["^abandoned_cart_","^ac_wc_","abandoned_cart_send_email"]', now()), ('kadence-blocks', 1, '["^kadence_","kadence_blocks_version","kadence_version","kadence_db_version","kb_design_library_data"]', '["^kadence_"]', '[]', '["^kadence_","kb_clean_delete"]', now()), ('ultimate-addons-for-gutenberg', 1, '["^uagb_","^ultimate_addons_","uagb_version","uagb_old_install","uagb_disable_","uagb_stylesheet_"]', '["^uagb_","^ultimate_addons_"]', '[]', '["^uagb_","uagb_assets_regenerate"]', now()), ('essential-addons-for-elementor-lite', 1, '["^eael_","eael_version","eael_db_version","eael_settings","essential_addons_for_elementor"]', '["^eael_"]', '[]', '["^eael_"]', now()), ('header-footer-elementor', 1, '["hfe_version","hfe_db_version","hfe_builder_status"]', '[]', '[]', '[]', now()), ('wp-popups', 1, '["^wppopups_","^wd_popup_","wppopups_version","wd_popup_version"]', '["^wppopups_","^wd_popup_"]', '[]', '["^wppopups_"]', now()), ('popup-maker', 1, '["^popup_maker_","pum_version","popup_maker_version","pum_db_ver"]', '["^popup_maker_"]', '["^wp_pum"]', '["^popup_maker_","pum_notify_popup_subscriber"]', now()), ('coming-soon', 1, '["^seedprod_","^seed_prod_","seedprod_version","seedprod_settings","seed_csp3_"]', '["^seedprod_","^seed_csp3_"]', '[]', '["^seedprod_","seedprod_cron_cleanup"]', now()), ('under-construction-page', 1, '["ucp_version","ucp_settings","under_construction_active"]', '[]', '[]', '[]', now()), ('insert-headers-and-footers', 1, '["^wpcode_","^ihaf_","wpcode_version","ihaf_version","wpcode_settings","wpcode_db_version"]', '["^wpcode_","^ihaf_"]', '[]', '["^wpcode_","wpcode_auto_insert_snippet_runner"]', now()), ('code-snippets', 1, '["^code_snippets_","code_snippets_version","code_snippets_db_version","code_snippets_option"]', '["^code_snippets_"]', '["^wp_snippets"]', '["^code_snippets_","code_snippets_db_maintenance"]', now()), ('meta-box', 1, '["^rwmb_","rwmb_version","rwmb_db_version","meta-box-version","rwmb_global_styles"]', '["^rwmb_"]', '[]', '[]', now()), ('pods', 1, '["^pods_","pods_version","pods_db_version","pods_system_count"]', '["^pods_"]', '["^wp_pods"]', '["^pods_","pods_cleanup"]', now()), ('toolset-types', 1, '["^wpcf-","^types_","wpcf_version","types_version","wpcf_data"]', '["^wpcf-","^types_"]', '["^wp_toolset_"]', '["^types_","^wpcf_","wpcf_delete_old_relationships"]', now()), ('woocommerce-multilingual', 1, '["^wcml_","wcml_version","woocommerce_wpml_version"]', '["^wcml_"]', '["^wp_wcml"]', '["^wcml_","wcml_sync_external_products"]', now()) ON CONFLICT (slug) DO UPDATE SET corpus_version = EXCLUDED.corpus_version, option_patterns = EXCLUDED.option_patterns, transient_patterns = EXCLUDED.transient_patterns, table_patterns = EXCLUDED.table_patterns, cron_hook_patterns = EXCLUDED.cron_hook_patterns, updated_at = now(); -> REVOKE INSERT, UPDATE, DELETE ON plugin_signatures FROM wpmgr_app; -- ok (404.254167ms) -- migrating version 20260605030000 -> ALTER TABLE site_db_scan_results ADD COLUMN IF NOT EXISTS orphaned_options_json jsonb NOT NULL DEFAULT '[]', ADD COLUMN IF NOT EXISTS orphaned_cron_json jsonb NOT NULL DEFAULT '[]', ADD COLUMN IF NOT EXISTS installed_plugins_json jsonb NOT NULL DEFAULT '[]'; -- ok (336.465417ms) -- migrating version 20260606000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_db_size_history" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "db_size_bytes" bigint NOT NULL DEFAULT 0, "table_count" int NOT NULL DEFAULT 0, "scanned_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_db_size_history_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_db_size_history_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_db_size_history_site_scanned_uniq" UNIQUE ("site_id", "scanned_at") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_db_size_history' AND indexname = 'site_db_size_history_site_scanned_idx' ) THEN CREATE INDEX "site_db_size_history_site_scanned_idx" ON "public"."site_db_size_history" ("site_id", "scanned_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_db_size_history' AND indexname = 'site_db_size_history_created_idx' ) THEN CREATE INDEX "site_db_size_history_created_idx" ON "public"."site_db_size_history" ("created_at"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_db_size_history" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_db_size_history" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_db_size_history' AND policyname = 'site_db_size_history_tenant_isolation' ) THEN CREATE POLICY "site_db_size_history_tenant_isolation" ON "public"."site_db_size_history" USING ( "tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid ) WITH CHECK ( "tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_db_size_history' AND policyname = 'site_db_size_history_agent' ) THEN CREATE POLICY "site_db_size_history_agent" ON "public"."site_db_size_history" USING (current_setting('app.agent', true) = 'on'); -- No WITH CHECK: the GC path only deletes; inserts flow through -- the tenant_isolation policy via InTenantTx. END IF; END; $$; -- ok (718.360583ms) -- migrating version 20260606010000 -> ALTER TABLE site_perf_config ADD COLUMN IF NOT EXISTS active_orphan_delete_job_id text, ADD COLUMN IF NOT EXISTS active_orphan_delete_started timestamptz; -- ok (403.046917ms) -- migrating version 20260606020000 -> GRANT INSERT, UPDATE ON plugin_signatures TO wpmgr_app; -> INSERT INTO plugin_signatures (slug, corpus_version, option_patterns, transient_patterns, table_patterns, cron_hook_patterns, updated_at) VALUES -- ----------------------------------------------------------------------- -- BACKUP / MIGRATION -- ----------------------------------------------------------------------- ('blogvault-real-time-backup', 2, '["^blogvault_","blogvault_version","blogvault_api_key","blogvault_account_email","blogvault_site_id"]', '["^blogvault_"]', '[]', '["^blogvault_","blogvault_perform_cron"]', now()), ('xcloner-backup-and-restore', 2, '["^xcloner_","xcloner_version","xcloner_settings","xcloner_db_version"]', '["^xcloner_"]', '["^wp_xcloner"]', '["^xcloner_","xcloner_cleanup_old_archives"]', now()), ('wpvivid-backups', 2, '["^wpvivid_","wpvivid_version","wpvivid_settings","wpvivid_backup_schedule"]', '["^wpvivid_"]', '[]', '["^wpvivid_","wpvivid_cron_backup","wpvivid_cron_cleanup"]', now()), ('migrate-guru', 2, '["^mgmt_","mgmt_version","mgmt_api_key","mgmt_site_url","migrate_guru_version"]', '["^mgmt_"]', '[]', '["^mgmt_","mgmt_cron_push"]', now()), -- ----------------------------------------------------------------------- -- SEO -- ----------------------------------------------------------------------- ('squirrly-seo', 2, '["sq_options","sq_version","sq_db_version","squirrly_version"]', '[]', '[]', '["squirrly_hourly_cron","squirrly_daily_cron"]', now()), ('seopress', 2, '["^seopress_","seopress_version","seopress_db_version","seopress_settings","seopress_toggle"]', '["^seopress_"]', '[]', '["^seopress_","seopress_daily_cron"]', now()), ('slim-seo', 2, '["slim_seo_version","slim_seo_settings","slim_seo_schema"]', '[]', '[]', '[]', now()), ('wp-seopress', 2, '["^seopress_","seopress_titles","seopress_social","seopress_xml_sitemap"]', '["^seopress_"]', '[]', '["^seopress_"]', now()), ('broken-link-checker-lite', 2, '["blcx_version","blcx_db_version","blcx_options","blcx_settings"]', '[]', '["^wp_blcx_"]', '["blcx_check_links","blcx_cleanup_data"]', now()), -- ----------------------------------------------------------------------- -- CACHE -- ----------------------------------------------------------------------- ('swift-performance-lite', 2, '["^swift_performance_","swift_performance_version","swift_performance_settings","swift_performance_db_version"]', '["^swift_performance_"]', '[]', '["^swift_performance_","swift_performance_cron"]', now()), ('hummingbird-performance', 2, '["^wphb_","wphb_version","wphb_settings","wphb_caching_settings","wphb_minify_group"]', '["^wphb_"]', '[]', '["^wphb_","wphb_cron_run","wphb_admin_notices"]', now()), ('comet-cache', 2, '["^comet_cache_","comet_cache_version","comet_cache_options","quick_cache_version"]', '["^comet_cache_","^quick_cache_"]', '[]', '["^comet_cache_","comet_cache_auto_purge"]', now()), ('litespeed-cache', 2, '["^litespeed_","litespeed_version","litespeed_conf","litespeed_cache_conf","lscwp_db_version"]', '["^litespeed_"]', '[]', '["^litespeed_","litespeed_crawl_cron","litespeed_queue_cron"]', now()), ('sg-cachepress', 2, '["^siteground_optimizer_","sgc_version","sg_cachepress_version","siteground_optimizer_settings"]', '["^siteground_optimizer_"]', '[]', '["^siteground_optimizer_","siteground_optimizer_cron"]', now()), ('cache-enabler', 2, '["^cache_enabler_","cache_enabler_version","cache_enabler_settings"]', '["^cache_enabler_"]', '[]', '["^cache_enabler_","cache_enabler_check"]', now()), ('nitropack', 2, '["^nitropack_","nitropack_version","nitropack_cache_key","nitropack_config"]', '["^nitropack_"]', '[]', '["^nitropack_","nitropack_sync_cron","nitropack_purge_cron"]', now()), -- ----------------------------------------------------------------------- -- SECURITY -- ----------------------------------------------------------------------- ('wp-cerber', 2, '["^cerber_","wp_cerber_version","cerber_settings","cerber_acl","cerber_traffic"]', '["^cerber_"]', '["^wp_cerber"]', '["^cerber_","cerber_cron_integrity_check","cerber_cron_request_scan"]', now()), ('all-in-one-wp-security-and-firewall', 2, '["^aiowps_","^aio_wp_security_","aiowps_version","aiowps_settings","aio_wp_security_configs"]', '["^aiowps_","^aio_wp_security_"]', '["^wp_aiowps_"]', '["^aiowps_","aiowps_scan_and_blacklist_check","aiowps_email_alert_digest"]', now()), ('security-ninja', 2, '["^wf_sn_","^secnin_","security_ninja_version","secnin_settings","wf_sn_db_version"]', '["^wf_sn_","^secnin_"]', '["^wp_wf_sn"]', '["^secnin_","security_ninja_scheduled_scan"]', now()), ('anti-malware', 2, '["^gotmls_","gotmls_version","gotmls_login_offset","gotmls_settings"]', '["^gotmls_"]', '[]', '["^gotmls_","gotmls_db_cleanup","gotmls_scheduled_scan"]', now()), ('defender-security', 2, '["^wpdef_","^wd_disable_","defender_security_version","wpdef_settings","wpdef_db_version"]', '["^wpdef_","^wd_disable_"]', '["^wp_defender"]', '["^wpdef_","wpdef_hub_sync","wpdef_security_scan_cron"]', now()), ('malcare-security', 2, '["^malcare_","^mcare_","malcare_version","malcare_api_key","malcare_site_id","mcare_db_version"]', '["^malcare_","^mcare_"]', '[]', '["^malcare_","malcare_cron_push_data"]', now()), ('two-factor', 2, '["^two_factor_","two_factor_version","two_factor_backup_codes"]', '["^two_factor_"]', '[]', '[]', now()), ('wp-2fa', 2, '["^wp_2fa_","wp_2fa_version","wp_2fa_settings","wp_2fa_db_version"]', '["^wp_2fa_"]', '[]', '["^wp_2fa_","wp_2fa_clear_expired_graceful"]', now()), ('clef', 2, '["^clef_","clef_settings","clef_version"]', '["^clef_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- WOOCOMMERCE EXTENSIONS -- ----------------------------------------------------------------------- ('woocommerce-payments', 2, '["^wcpay_","^woocommerce_payments_","wcpay_version","woocommerce_woocommerce_payments_version","wcpay_db_version"]', '["^wcpay_","^woocommerce_payments_"]', '["^wp_wcpay"]', '["^wcpay_","wcpay_send_scheduled_action","wcpay_delete_expired_transient"]', now()), ('woo-paypal-gateway', 2, '["^woo_paypal_","woo_paypal_version","woo_paypal_settings"]', '["^woo_paypal_"]', '[]', '["^woo_paypal_"]', now()), ('woocommerce-coupon-campaigns', 2, '["^wc_coupon_campaigns_","woocommerce_coupon_campaigns_version"]', '["^wc_coupon_campaigns_"]', '[]', '[]', now()), ('woocommerce-extra-checkout-fields-for-brazil', 2, '["^wcbcf_","wcbcf_settings","wcbcf_version"]', '["^wcbcf_"]', '[]', '[]', now()), ('woocommerce-product-bundles', 2, '["^wc_pb_","woocommerce_product_bundles_version","wc_pb_version","wc_product_bundles_version"]', '["^wc_pb_"]', '[]', '["^wc_pb_","wc_pb_sync_bundled_stock"]', now()), ('woocommerce-mix-and-match-products', 2, '["^wc_mnm_","woocommerce_mix_and_match_products_version","wc_mnm_version"]', '["^wc_mnm_"]', '[]', '[]', now()), ('woocommerce-checkout-field-editor', 2, '["thwcfd_version","thwcfd_fields","thwcfd_settings"]', '[]', '[]', '[]', now()), ('woocommerce-bulk-discount', 2, '["^woobd_","woobd_version","woobd_settings"]', '["^woobd_"]', '[]', '[]', now()), ('woocommerce-wishlist-plugin', 2, '["^wlfmc_","^yith_wcwl_","yith_wcwl_version","wlfmc_version","wlfmc_settings"]', '["^wlfmc_","^yith_wcwl_"]', '["^wp_yith_wcwl","^wp_wlfmc"]', '["^wlfmc_","yith_wcwl_maybe_expire_wishlist"]', now()), ('yith-woocommerce-wishlist', 2, '["^yith_wcwl_","yith_wcwl_version","yith_wcwl_options","yith_wcwl_db_version"]', '["^yith_wcwl_"]', '["^wp_yith_wcwl"]', '["^yith_wcwl_","yith_wcwl_delete_expired_wishlists"]', now()), ('woo-variation-swatches', 2, '["wvs_version","wvs_settings","wvs_pro_version"]', '[]', '[]', '[]', now()), ('woo-smart-quick-view', 2, '["^woosqv_","woosqv_version","woosqv_settings"]', '["^woosqv_"]', '[]', '[]', now()), ('woocommerce-order-export', 2, '["woe_version","woe_settings","woe_db_version"]', '[]', '[]', '["woe_cron_schedule_export"]', now()), ('woocommerce-warranty', 2, '["^wc_warranty_","woocommerce_warranty_version"]', '["^wc_warranty_"]', '[]', '[]', now()), ('woocommerce-shipping-labels', 2, '["^wc_shipping_labels_","wc_shipping_labels_version","wc_shipping_labels_settings"]', '["^wc_shipping_labels_"]', '[]', '[]', now()), ('woocommerce-sequential-order-numbers', 2, '["woocommerce_seq_order_number","woocommerce_sequential_order_numbers_version"]', '[]', '[]', '[]', now()), ('cartflows', 2, '["^cartflows_","cartflows_version","cartflows_db_version","cartflows_settings","cartflows_license"]', '["^cartflows_"]', '["^wp_cartflows"]', '["^cartflows_","cartflows_cleanup_ghost_orders"]', now()), ('checkout-plugins-stripe-woo', 2, '["^cpsw_","cpsw_version","cpsw_settings","cpsw_publishable_key"]', '["^cpsw_"]', '[]', '["^cpsw_"]', now()), -- ----------------------------------------------------------------------- -- FORMS -- ----------------------------------------------------------------------- ('caldera-forms', 2, '["^caldera_forms_","caldera_forms_version","caldera_forms_db_version"]', '["^caldera_forms_"]', '["^wp_cf_"]', '["^caldera_forms_","caldera_forms_send_usage_data"]', now()), ('formidable', 2, '["frm_version","frm_db_version","frm_options","frm_license"]', '[]', '["^wp_frm_"]', '["frm_run_jobs","frm_cleanup_drafts"]', now()), ('happyforms', 2, '["^happyforms_","happyforms_version","happyforms_settings","happyforms_db_version"]', '["^happyforms_"]', '["^wp_happyforms"]', '["^happyforms_","happyforms_cleanup_entries"]', now()), ('weforms', 2, '["^weforms_","weforms_version","weforms_db_version","weforms_license_key"]', '["^weforms_"]', '["^wp_weforms"]', '["^weforms_","weforms_clear_logs"]', now()), ('everest-forms', 2, '["^everest_forms_","evf_version","evf_db_version","everest_forms_settings"]', '["^everest_forms_"]', '["^wp_evf_"]', '["^everest_forms_","evf_email_report"]', now()), ('piotnetforms', 2, '["pfb_version","piotnetforms_version","pfb_settings"]', '[]', '[]', '["pfb_cleanup_entries"]', now()), ('forminator', 2, '["^forminator_","forminator_version","forminator_db_version","forminator_license"]', '["^forminator_"]', '["^wp_frmt_"]', '["^forminator_","forminator_truncate_ip","forminator_delete_old_submissions"]', now()), ('fluent-forms', 2, '["^fluentform_","^fluent_form_","fluentform_version","fluentform_settings","fluentform_db_version"]', '["^fluentform_"]', '["^wp_fluentform"]', '["^fluentform_","fluentform_scheduled_cleanup","fluentform_report_email"]', now()), -- ----------------------------------------------------------------------- -- PAGE BUILDERS -- ----------------------------------------------------------------------- ('brizy', 2, '["^brizy_","brizy_version","brizy_db_version","brizy_settings","brizy_cloud_"]', '["^brizy_"]', '[]', '["^brizy_","brizy_db_cleanup"]', now()), ('bricks', 2, '["^bricks_","bricks_version","bricks_settings","bricks_db_version","bricks_license"]', '["^bricks_"]', '[]', '["^bricks_"]', now()), ('zion-builder', 2, '["^zionbuilder_","zionbuilder_version","zionbuilder_db_version"]', '["^zionbuilder_"]', '[]', '[]', now()), ('seedprod', 2, '["^seedprod_","seedprod_version","seedprod_settings","seedprod_db_version"]', '["^seedprod_"]', '[]', '["^seedprod_","seedprod_scheduled_cleanup"]', now()), ('thrive-visual-editor', 2, '["tve_version","tve_db_version","^tve_leads_","^tve_quiz_"]', '[]', '[]', '["tve_leads_cleanup"]', now()), ('otter-blocks', 2, '["^otter_","otter_blocks_version","otter_blocks_settings","otter_db_version"]', '["^otter_"]', '[]', '["^otter_","otter_feedback_notice"]', now()), ('blocksy-companion', 2, '["^blocksy_","blocksy_version","blocksy_settings"]', '["^blocksy_"]', '[]', '[]', now()), ('spectra', 2, '["^spectra_","spectra_version","spectra_settings","spectra_free_version"]', '["^spectra_"]', '[]', '["^spectra_"]', now()), -- ----------------------------------------------------------------------- -- MEMBERSHIP -- ----------------------------------------------------------------------- ('restrict-content-pro', 2, '["rcp_version","rcp_db_version","rcp_settings","rcp_license"]', '[]', '["^wp_rcp_"]', '["rcp_check_member_expiration","rcp_email_expiring_members"]', now()), ('wishlist-member', 2, '["^wishlistmember_","wishlistmember_version","wlm_db_version","wlm_settings"]', '["^wishlistmember_"]', '["^wp_wlm"]', '["^wishlistmember_","wlm_cron"]', now()), ('groups', 2, '["^groups_","groups_version","groups_db_version","groups_options"]', '["^groups_"]', '["^wp_groups"]', '["^groups_"]', now()), ('indeed-membership-pro', 2, '["^indeed_","indeed_version","ump_version","ump_db_version"]', '["^indeed_"]', '["^wp_ump_"]', '["ump_cron_expire"]', now()), -- ----------------------------------------------------------------------- -- EMAIL / NEWSLETTER / CRM -- ----------------------------------------------------------------------- ('sendinblue', 2, '["^sendinblue_","sib_version","sib_db_version","sendinblue_api_key","sendinblue_settings"]', '["^sendinblue_"]', '[]', '["^sendinblue_","sib_cron_workflow_runner"]', now()), ('brevo', 2, '["^brevo_","brevo_version","brevo_api_key","brevo_settings","brevo_db_version"]', '["^brevo_"]', '[]', '["^brevo_","brevo_cron_sync"]', now()), ('klaviyo', 2, '["^klaviyo_","klaviyo_version","klaviyo_settings","klaviyo_api_key"]', '["^klaviyo_"]', '[]', '["^klaviyo_","klaviyo_cron_sync"]', now()), ('hubspot', 2, '["^hubspot_","^leadin_","hubspot_version","leadin_version","leadin_api_key","hubspot_portal_id"]', '["^hubspot_","^leadin_"]', '[]', '["^hubspot_","leadin_analytics_cron"]', now()), ('activecampaign', 2, '["^activecampaign_","activecampaign_version","activecampaign_api_key","activecampaign_url"]', '["^activecampaign_"]', '[]', '["^activecampaign_"]', now()), ('constant-contact-forms', 2, '["^ctct_","ctct_version","ctct_plus_version","ctct_settings","ctct_api_key"]', '["^ctct_"]', '[]', '["^ctct_","ctct_cron_task"]', now()), ('fluentcrm', 2, '["^fluentcrm_","fluentcrm_version","fluentcrm_db_version","fluentcrm_settings"]', '["^fluentcrm_"]', '["^wp_fc_"]', '["^fluentcrm_","fluentcrm_scheduled_task","fluentcrm_run_sequences","fluentcrm_bulk_email_runner"]', now()), ('mailster', 2, '["^mailster_","mailster_version","mailster_db_version","mailster_settings","mailster_license"]', '["^mailster_"]', '["^wp_mailster"]', '["^mailster_","mailster_cron_check_lists","mailster_cron_queue"]', now()), ('emailoctopus', 2, '["^emailoctopus_","emailoctopus_version","emailoctopus_api_key"]', '["^emailoctopus_"]', '[]', '["^emailoctopus_"]', now()), ('aweber-web-form-widget', 2, '["^aweber_","aweber_version","aweber_settings","aweber_consumer_key"]', '["^aweber_"]', '[]', '["^aweber_"]', now()), ('drip', 2, '["^drip_","drip_version","drip_settings","drip_api_token","drip_account_id"]', '["^drip_"]', '[]', '["^drip_"]', now()), -- ----------------------------------------------------------------------- -- ANALYTICS / TRACKING -- ----------------------------------------------------------------------- ('independent-analytics', 2, '["^independent_analytics_","iawp_version","iawp_db_version","iawp_settings"]', '["^iawp_","^independent_analytics_"]', '["^wp_iawp"]', '["^iawp_","iawp_daily_maintenance","iawp_prune_salted_ids"]', now()), ('matomo', 2, '["^matomo_","^piwik_","matomo_version","matomo_db_version","matomo_settings","piwik_tracking_code"]', '["^matomo_","^piwik_"]', '["^wp_matomo","^wp_piwik"]', '["^matomo_","matomo_archive_cron","matomo_prune_old_data"]', now()), ('exactmetrics', 2, '["^exactmetrics_","exactmetrics_version","exactmetrics_settings","exactmetrics_license"]', '["^exactmetrics_"]', '[]', '["^exactmetrics_","exactmetrics_send_tracking_data"]', now()), ('pixel-cat', 2, '["^pixel_cat_","pixel_cat_version","pixel_cat_options","pxlct_version"]', '["^pixel_cat_","^pxlct_"]', '[]', '[]', now()), ('woocommerce-google-analytics', 2, '["^wc_google_analytics_","woocommerce_google_analytics_version","wgap_settings"]', '["^wc_google_analytics_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- MEDIA -- ----------------------------------------------------------------------- ('envato-elements', 2, '["^envato_elements_","envato_elements_version","envato_elements_token"]', '["^envato_elements_"]', '[]', '["^envato_elements_","envato_elements_sync"]', now()), ('fifu', 2, '["^fifu_","fifu_version","fifu_settings","fifu_db_version"]', '["^fifu_"]', '[]', '["^fifu_","fifu_cron_cleanup"]', now()), ('meow-gallery', 2, '["mwg_version","mwg_settings"]', '[]', '[]', '[]', now()), ('modula-best-grid-gallery', 2, '["^modula_","modula_version","modula_db_version","modula_settings"]', '["^modula_"]', '[]', '["^modula_","modula_cleanup"]', now()), ('robo-gallery', 2, '["^robo_gallery_","robo_gallery_version","robo_gallery_db_version"]', '["^robo_gallery_"]', '["^wp_robo_gallery"]', '[]', now()), ('photo-gallery', 2, '["bwg_version","bwg_db_version","bwg_settings","photo_gallery_version"]', '[]', '["^wp_bwg"]', '["bwg_cleanup"]', now()), ('video-gallery', 2, '["^videowhisper_","^vdgr_","videowhisper_version","vdgr_settings"]', '["^vdgr_","^videowhisper_"]', '[]', '[]', now()), ('wp-video-lightbox', 2, '["^wpvl_","wpvl_version","wpvl_settings"]', '["^wpvl_"]', '[]', '[]', now()), ('sirv', 2, '["^sirv_","sirv_version","sirv_settings","sirv_api_key"]', '["^sirv_"]', '[]', '["^sirv_","sirv_cron_sync"]', now()), ('optimus', 2, '["^optimus_","optimus_version","optimus_settings","optimus_apikey"]', '["^optimus_"]', '[]', '["^optimus_","optimus_cron_cleanup"]', now()), ('imageshop', 2, '["^imageshop_","imageshop_version","imageshop_settings"]', '["^imageshop_"]', '[]', '[]', now()), ('webp-express', 2, '["^webp_express_","webp_express_version","webp_express_settings"]', '["^webp_express_"]', '[]', '["^webp_express_"]', now()), -- ----------------------------------------------------------------------- -- MULTILINGUAL -- ----------------------------------------------------------------------- ('gtranslate', 2, '["^gtranslate_","gtranslate_version","gtranslate_settings","gt_widget_lang","gt_preferred_language"]', '["^gtranslate_","^gt_widget"]', '[]', '["^gtranslate_"]', now()), ('weglot', 2, '["^weglot_","weglot_version","weglot_settings","weglot_api_key","weglot_db_version"]', '["^weglot_"]', '[]', '["^weglot_","weglot_cron_cleanup"]', now()), ('transposh-translation-filter', 2, '["^transposh_","transposh_version","transposh_settings","transposh_db_version"]', '["^transposh_"]', '[]', '["^transposh_","transposh_cleanup"]', now()), ('loco-translate-pro', 2, '["^loco_","loco_translate_pro_version","loco_pro_options"]', '["^loco_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- ANTI-SPAM -- ----------------------------------------------------------------------- ('antispam-bee', 2, '["^antispam_bee_","antispam_bee_version","antispam_bee_settings","asb_version"]', '["^antispam_bee_"]', '[]', '["^antispam_bee_","antispam_bee_delete_old_spam"]', now()), ('wp-spamshield', 2, '["^wpsqt_","^spamshield_","wpsqt_version","spamshield_version","spamshield_settings"]', '["^wpsqt_","^spamshield_"]', '[]', '["^wpsqt_","spamshield_scheduled_cleanup"]', now()), ('zero-spam', 2, '["^zerospam_","zero_spam_version","zerospam_settings","zerospam_db_version"]', '["^zerospam_"]', '["^wp_zerospam"]', '["^zerospam_","zerospam_cleanup_logs"]', now()), ('cleantalk-spam-protect', 2, '["^cleantalk_","cleantalk_version","cleantalk_settings","cleantalk_api_key"]', '["^cleantalk_"]', '[]', '["^cleantalk_","cleantalk_cron_task"]', now()), -- ----------------------------------------------------------------------- -- REDIRECTS / URL MANAGEMENT -- ----------------------------------------------------------------------- ('safe-redirect-manager', 2, '["srm_version","srm_db_version","srm_max_redirects"]', '[]', '[]', '[]', now()), ('301-redirects', 2, '["^eps_redirects_","eps_redirects_version","301redirects_version"]', '["^eps_redirects_"]', '[]', '["^eps_redirects_","eps_redirect_check_hits"]', now()), ('rank-math-seo', 2, '["^rank_math_","rank_math_version","rank_math_db_version","rank_math_modules","rank_math_redirections_"]', '["^rank_math_"]', '["^wp_rank_math"]', '["^rank_math_","rank_math_redirection_cleanup","rank_math_sitemap_ping"]', now()), ('simple-301-redirects', 2, '["^sm_redirects_","sm301_version","301_redirects_version"]', '["^sm_redirects_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- PERFORMANCE / MISC UTILITIES -- ----------------------------------------------------------------------- ('query-monitor', 2, '["qm_version","qm_db_version","qm_settings"]', '[]', '[]', '[]', now()), ('debug-bar', 2, '["debug_bar_version","debug_bar_enabled"]', '[]', '[]', '[]', now()), ('redis-cache', 2, '["^redis_cache_","redis_cache_version","redis_cache_settings","redisobject_cache_version"]', '["^redis_cache_"]', '[]', '["^redis_cache_","redis_cache_health_check"]', now()), ('object-cache-pro', 2, '["ocp_version","object_cache_pro_version","object_cache_pro_license"]', '[]', '[]', '["ocp_health_check"]', now()), ('wp-crontrol', 2, '["crontrol_version","crontrol_settings","crontrol_disable"]', '[]', '[]', '[]', now()), ('health-check', 2, '["^health_check_","health_check_version","health_check_settings"]', '["^health_check_"]', '[]', '["^health_check_"]', now()), ('asset-cleanup', 2, '["^wpassetcleanup_","wpassetcleanup_version","wpassetcleanup_db_version","wpassetcleanup_settings"]', '["^wpassetcleanup_"]', '[]', '["^wpassetcleanup_","wpassetcleanup_cron_clear_cache"]', now()), ('perfmatters', 2, '["^perfmatters_","perfmatters_version","perfmatters_settings","perfmatters_license"]', '["^perfmatters_"]', '[]', '["^perfmatters_","perfmatters_cleanup"]', now()), ('flying-pages', 2, '["^flying_pages_","flying_pages_version","flying_pages_settings"]', '["^flying_pages_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- SOCIAL / SHARING / RATINGS -- ----------------------------------------------------------------------- ('sumome', 2, '["^sumome_","^sumo_","sumome_version","sumo_version","sumome_api_key"]', '["^sumome_","^sumo_"]', '[]', '["^sumome_","sumome_cron"]', now()), ('kk-star-ratings', 2, '["^kksr_","kksr_version","kksr_settings","kksr_db_version"]', '["^kksr_"]', '["^wp_kksr"]', '["^kksr_","kksr_cleanup"]', now()), ('wp-postratings', 2, '["^postratings_","wp_postratings_version","postratings_db_version","postratings_settings"]', '["^postratings_"]', '["^wp_ratings"]', '["^postratings_","postratings_garbage_collection"]', now()), ('easy-social-sharing', 2, '["ess_version","ess_settings","easy_social_sharing_version"]', '[]', '[]', '[]', now()), ('add-to-any', 2, '["^addtoany_","addtoany_version","addtoany_options"]', '["^addtoany_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- WOOCOMMERCE REVIEWS / LOYALTY -- ----------------------------------------------------------------------- ('judge-me-product-reviews-woocommerce', 2, '["^judgeme_","judgeme_version","judgeme_settings","judgeme_api_token"]', '["^judgeme_"]', '[]', '["^judgeme_","judgeme_sync_cron"]', now()), ('woocommerce-points-and-rewards', 2, '["^wc_points_rewards_","woocommerce_points_rewards_version","wc_points_rewards_points_label"]', '["^wc_points_rewards_"]', '[]', '["^wc_points_rewards_","wc_points_rewards_cron"]', now()), -- ----------------------------------------------------------------------- -- LIVE CHAT / SUPPORT -- ----------------------------------------------------------------------- ('tidio-live-chat', 2, '["^tidio_","tidio_version","tidio_api_key","tidio_track_user_email"]', '["^tidio_"]', '[]', '["^tidio_"]', now()), ('crisp', 2, '["^crisp_","crisp_version","crisp_website_id","crisp_settings"]', '["^crisp_"]', '[]', '[]', now()), ('tawk-to-live-chat', 2, '["^tawkto_","tawkto_version","tawkto_widget_id","tawkto_property_id"]', '["^tawkto_"]', '[]', '[]', now()), ('live-chat', 2, '["^livechat_","livechat_version","livechat_license","livechat_settings"]', '["^livechat_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- WP-CLI / DEVELOPER UTILITIES -- ----------------------------------------------------------------------- ('woocommerce-dev-helper', 2, '["^wc_dev_helper_","wc_dev_helper_version"]', '["^wc_dev_helper_"]', '[]', '[]', now()), ('wp-reset', 2, '["wpr_version","wp_reset_version","wpr_settings"]', '[]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- COOKIE / GDPR COMPLIANCE -- ----------------------------------------------------------------------- ('cookie-notice', 2, '["^cookie_notice_","cookie_notice_version","cookie_notice_options","cn_cookies_accepted"]', '["^cookie_notice_","^cn_cookies"]', '[]', '["^cookie_notice_","cookie_notice_check"]', now()), ('cookieyes', 2, '["^cookieyes_","cookieyes_version","cky_api_key","cky_db_version"]', '["^cookieyes_"]', '[]', '["^cookieyes_","cky_sync_cron"]', now()), ('gdpr-cookie-consent', 2, '["^gdpr_cookie_consent_","gdpr_cookie_consent_version","wt_cli_version","gdpr_cookie_settings"]', '["^gdpr_cookie_consent_","^wt_cli_"]', '[]', '["^gdpr_cookie_consent_","wt_cli_auto_clear_cookies"]', now()), ('complianz-gdpr', 2, '["^cmplz_","^complianz_","cmplz_version","complianz_version","complianz_settings","cmplz_db_version"]', '["^cmplz_","^complianz_"]', '["^wp_cmplz"]', '["^cmplz_","cmplz_daily_mailchimp_cleanup","cmplz_daily_stats_cleanup"]', now()), ('real-cookie-banner', 2, '["rcb_version","rcb_db_version","rcb_settings"]', '[]', '["^wp_rcb"]', '["rcb_cleanup_outdated"]', now()), -- ----------------------------------------------------------------------- -- MAP / LOCATION -- ----------------------------------------------------------------------- ('wp-google-maps', 2, '["^wpgmza_","wpgmza_version","wpgmza_db_version","wpgmza_settings","WPGMZA_VERSION"]', '["^wpgmza_"]', '["^wp_wpgmza"]', '["^wpgmza_","wpgmza_cleanup"]', now()), ('maps-marker-pro', 2, '["^leafletmapsmarker_","mmp_version","leafletmapsmarker_version","mmp_db_version"]', '["^leafletmapsmarker_"]', '["^wp_leafletmapsmarker","^wp_mmp"]', '["mmp_cron_cleanup"]', now()), -- ----------------------------------------------------------------------- -- BOOKING -- ----------------------------------------------------------------------- ('amelia', 2, '["^amelia_","amelia_version","amelia_db_version","amelia_settings","amelia_license"]', '["^amelia_"]', '["^wp_amelia"]', '["^amelia_","amelia_daily_cron","amelia_reminder_cron"]', now()), ('bookly-responsive-appointment-booking-tool', 2, '["^bookly_","bookly_version","bookly_db_version","bookly_settings","bookly_license"]', '["^bookly_"]', '["^wp_bookly"]', '["^bookly_","bookly_cron_send_notifications","bookly_cron_clean_cache"]', now()), ('simply-schedule-appointments', 2, '["ssa_version","ssa_db_version","ssa_settings"]', '[]', '["^wp_ssa"]', '["ssa_cleanup_cron"]', now()), -- ----------------------------------------------------------------------- -- INVOICING / PAYMENTS -- ----------------------------------------------------------------------- ('wpinvoices', 2, '["^wpinv_","wpinv_version","wpinv_db_version","wpinv_settings"]', '["^wpinv_"]', '["^wp_getpaid_"]', '["^wpinv_","wpinv_cleanup_scheduled_actions"]', now()), ('invoiceninja', 2, '["^ninja_forms_","^invoiceninja_","invoiceninja_version","invoiceninja_token"]', '["^invoiceninja_"]', '[]', '["^invoiceninja_"]', now()), -- ----------------------------------------------------------------------- -- TABLEPRESS EXTENSIONS / DATA TABLES -- ----------------------------------------------------------------------- ('datatables-manager', 2, '["^dtman_","dtman_version","dtman_settings"]', '["^dtman_"]', '[]', '[]', now()), -- ----------------------------------------------------------------------- -- MISCELLANEOUS HIGH-INSTALL PLUGINS -- ----------------------------------------------------------------------- ('wp-statistics', 2, '["^wp_statistics_","wpstatistics_version","wp_statistics_db_version","wp_statistics_visitor"]', '["^wp_statistics_"]', '["^wp_statistics"]', '["^wp_statistics_","wp_statistics_cleanup","wp_statistics_schedule_send_report"]', now()), ('surecart', 2, '["^surecart_","surecart_version","surecart_db_version","surecart_api_key","surecart_settings"]', '["^surecart_"]', '["^wp_surecart"]', '["^surecart_","surecart_sync_cron"]', now()), ('wcb2b', 2, '["^wcb2b_","wcb2b_version","wcb2b_settings","wcb2b_db_version"]', '["^wcb2b_"]', '[]', '["^wcb2b_"]', now()), ('happy-addons-for-elementor', 2, '["^happy_addons_","happy_addons_version","hap_version","happy_addons_settings"]', '["^happy_addons_"]', '[]', '["^happy_addons_"]', now()), ('premium-addons-for-elementor', 2, '["^premium_addons_","premium_addons_version","premium_addons_settings","premium_addons_license"]', '["^premium_addons_"]', '[]', '["^premium_addons_"]', now()), ('elementor-pro', 2, '["^elementor_pro_","elementor_pro_version","elementor_pro_license","elementor_pro_license_data"]', '["^elementor_pro_"]', '[]', '["^elementor_pro_","elementor_pro_update_kit"]', now()), ('wc-product-table', 2, '["^wc_product_table_","wc_product_table_version","wc_product_table_settings"]', '["^wc_product_table_"]', '[]', '["^wc_product_table_"]', now()), ('affiliate-wp', 2, '["^affwp_","affiliate_wp_version","affwp_db_version","affwp_settings","affwp_license"]', '["^affwp_"]', '["^wp_affiliate"]', '["^affwp_","affwp_cleanup_payouts","affwp_schedule_send_report"]', now()), ('presto-player', 2, '["^presto_player_","presto_player_version","presto_player_settings"]', '["^presto_player_"]', '["^wp_presto_player"]', '["^presto_player_"]', now()), ('restrict-content', 2, '["^rcno_","rcno_version","rcno_db_version","rcno_settings"]', '["^rcno_"]', '["^wp_rcno"]', '["^rcno_","rcno_scheduled_task"]', now()), ('better-click-to-tweet', 2, '["^bctt_","bctt_version","bctt_settings"]', '["^bctt_"]', '[]', '[]', now()), ('loginwp', 2, '["^loginwp_","loginwp_version","loginwp_settings","loginwp_db_version","peter_loginwp_"]', '["^loginwp_","^peter_loginwp_"]', '[]', '["^loginwp_"]', now()), ('woocommerce-germanized', 2, '["^woocommerce_gzd_","woocommerce_gzd_version","woocommerce_gzd_db_version"]', '["^woocommerce_gzd_"]', '["^wp_woocommerce_gzd"]', '["^woocommerce_gzd_","woocommerce_gzd_daily_maintenance"]', now()), ('user-registration', 2, '["^user_registration_","ur_version","ur_db_version","user_registration_version"]', '["^user_registration_"]', '["^wp_ur_"]', '["^user_registration_","ur_cleanup_sessions","user_registration_send_usage"]', now()) ON CONFLICT (slug) DO UPDATE SET corpus_version = EXCLUDED.corpus_version, option_patterns = EXCLUDED.option_patterns, transient_patterns = EXCLUDED.transient_patterns, table_patterns = EXCLUDED.table_patterns, cron_hook_patterns = EXCLUDED.cron_hook_patterns, updated_at = now(); -> UPDATE plugin_signatures SET corpus_version = 2, updated_at = now() WHERE corpus_version = 1; -> REVOKE INSERT, UPDATE, DELETE ON plugin_signatures FROM wpmgr_app; -- ok (607.274834ms) -- migrating version 20260607000000 -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN "is_incremental" boolean NOT NULL DEFAULT false, ADD COLUMN "parent_snapshot_id" uuid NULL CONSTRAINT "backup_snapshots_parent_snapshot_id_fkey" REFERENCES "public"."backup_snapshots" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, ADD COLUMN "base_snapshot_id" uuid NULL CONSTRAINT "backup_snapshots_base_snapshot_id_fkey" REFERENCES "public"."backup_snapshots" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, ADD COLUMN "chain_id" uuid NULL, ADD COLUMN "generation" integer NOT NULL DEFAULT 0, ADD COLUMN "cycle_files_scanned" bigint NOT NULL DEFAULT 0, ADD COLUMN "cycle_files_changed" bigint NOT NULL DEFAULT 0, ADD COLUMN "cycle_files_deleted" bigint NOT NULL DEFAULT 0, ADD COLUMN "cycle_bytes_uploaded" bigint NOT NULL DEFAULT 0; -> CREATE INDEX "backup_snapshots_chain_id_idx" ON "public"."backup_snapshots" ("chain_id") WHERE "chain_id" IS NOT NULL; -> CREATE INDEX "backup_snapshots_parent_id_idx" ON "public"."backup_snapshots" ("parent_snapshot_id") WHERE "parent_snapshot_id" IS NOT NULL; -> CREATE TABLE "public"."backup_file_index" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "snapshot_id" uuid NOT NULL, "file_path" text NOT NULL, "file_size" bigint NOT NULL DEFAULT 0, "file_mtime" bigint NOT NULL DEFAULT 0, "file_blake3" text NOT NULL DEFAULT '', "chunk_hashes" text[] NOT NULL DEFAULT '{}', "is_tombstone" boolean NOT NULL DEFAULT false, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "backup_file_index_snapshot_id_fkey" FOREIGN KEY ("snapshot_id") REFERENCES "public"."backup_snapshots" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "backup_file_index_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); -> CREATE UNIQUE INDEX "backup_file_index_snapshot_path_key" ON "public"."backup_file_index" ("snapshot_id", "file_path"); -> CREATE INDEX "backup_file_index_snapshot_path_idx" ON "public"."backup_file_index" ("snapshot_id", "file_path"); -> CREATE INDEX "backup_file_index_tenant_id_idx" ON "public"."backup_file_index" ("tenant_id"); -> ALTER TABLE "public"."backup_file_index" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."backup_file_index" FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_file_index_tenant_isolation" ON "public"."backup_file_index" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "backup_file_index_agent" ON "public"."backup_file_index" FOR SELECT USING (current_setting('app.agent', true) = 'on'); -- ok (1.152807542s) -- migrating version 20260608000000 -> CREATE INDEX IF NOT EXISTS backup_snapshots_chain_gen_idx ON backup_snapshots (chain_id, generation) WHERE chain_id IS NOT NULL; -- ok (329.395334ms) -- migrating version 20260609000000 -> UPDATE "public"."backup_snapshots" SET "chain_id" = "id" WHERE "chain_id" IS NULL AND "is_incremental" = false AND "generation" = 0 AND "parent_snapshot_id" IS NULL; -- ok (323.786625ms) -- migrating version 20260610000000 -> ALTER TABLE "public"."backup_chunks" ADD COLUMN "last_referenced_at" timestamptz NOT NULL DEFAULT now(); -> UPDATE "public"."backup_chunks" SET "last_referenced_at" = "created_at"; -- ok (392.7535ms) -- migrating version 20260611000000 -> ALTER TABLE backup_schedules ADD COLUMN incremental_enabled boolean NOT NULL DEFAULT false; -> ALTER TABLE backup_schedules ADD COLUMN base_window_days integer NULL CHECK (base_window_days IS NULL OR base_window_days BETWEEN 1 AND 365); -- ok (394.075167ms) -- migrating version 20260612000000 -> ALTER TABLE backup_snapshots ADD COLUMN locked boolean NOT NULL DEFAULT false; -> CREATE INDEX backup_snapshots_locked_idx ON backup_snapshots (tenant_id, locked) WHERE locked = true; -> ALTER TABLE backup_schedules ADD COLUMN notify_on_completion text NOT NULL DEFAULT 'never' CHECK (notify_on_completion IN ('always', 'on_failure', 'never')), ADD COLUMN notify_recipients jsonb NOT NULL DEFAULT '[]'::jsonb; -> ALTER TABLE backup_schedules ADD COLUMN backup_components jsonb NULL, ADD COLUMN exclude_paths jsonb NULL, ADD COLUMN exclude_extensions jsonb NULL, ADD COLUMN exclude_file_size_mb integer NULL CHECK (exclude_file_size_mb > 0), ADD COLUMN include_core boolean NOT NULL DEFAULT false; -- ok (531.451167ms) -- migrating version 20260613000000 -> CREATE TABLE site_backup_settings ( tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, site_id uuid PRIMARY KEY REFERENCES sites(id) ON DELETE CASCADE, backup_components jsonb NULL, include_core boolean NOT NULL DEFAULT false, exclude_paths jsonb NULL, exclude_extensions jsonb NULL, exclude_file_size_mb integer NULL CHECK (exclude_file_size_mb > 0), notify_on_completion text NOT NULL DEFAULT 'never' CHECK (notify_on_completion IN ('always','on_failure','never')), notify_recipients jsonb NOT NULL DEFAULT '[]'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -> CREATE INDEX backup_settings_tenant_id_idx ON site_backup_settings (tenant_id); -> INSERT INTO site_backup_settings ( tenant_id, site_id, backup_components, include_core, exclude_paths, exclude_extensions, exclude_file_size_mb, notify_on_completion, notify_recipients ) SELECT s.tenant_id, bs.site_id, bs.backup_components, bs.include_core, bs.exclude_paths, bs.exclude_extensions, bs.exclude_file_size_mb, bs.notify_on_completion, bs.notify_recipients FROM backup_schedules bs JOIN sites s ON s.id = bs.site_id WHERE bs.backup_components IS NOT NULL OR bs.include_core = true OR bs.exclude_paths IS NOT NULL OR bs.exclude_extensions IS NOT NULL OR bs.exclude_file_size_mb IS NOT NULL OR bs.notify_on_completion != 'never' OR bs.notify_recipients != '[]'::jsonb ON CONFLICT (site_id) DO NOTHING; -> ALTER TABLE backup_schedules DROP COLUMN notify_on_completion, DROP COLUMN notify_recipients, DROP COLUMN backup_components, DROP COLUMN exclude_paths, DROP COLUMN exclude_extensions, DROP COLUMN exclude_file_size_mb, DROP COLUMN include_core; -> ALTER TABLE site_backup_settings ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_backup_settings FORCE ROW LEVEL SECURITY; -> CREATE POLICY "backup_settings_tenant_isolation" ON "public"."site_backup_settings" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); -> CREATE POLICY "backup_settings_site_scope" ON "public"."site_backup_settings" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); -- ok (887.448791ms) -- migrating version 20260614000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'media_optimization_jobs' AND column_name = 'encode_river_job_id' ) THEN ALTER TABLE "public"."media_optimization_jobs" ADD COLUMN "encode_river_job_id" bigint NULL; COMMENT ON COLUMN "public"."media_optimization_jobs"."encode_river_job_id" IS 'River river_jobs.id for the media_encode job enqueued at encode-ready time. ' 'NULL for non-optimize jobs and for rows created before m51. ' 'Used by the cancel path to cancel the River job proactively.'; END IF; END; $$; -- ok (339.736583ms) -- migrating version 20260615000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_cache_hit_ratio_history" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "hit_count" bigint NOT NULL DEFAULT 0, "miss_count" bigint NOT NULL DEFAULT 0, "ratio_pct" numeric(5,2), "sampled_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_cache_hit_ratio_history_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_cache_hit_ratio_history_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_cache_hit_ratio_history_site_sampled_uniq" UNIQUE ("site_id", "sampled_at") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_cache_hit_ratio_history' AND indexname = 'site_cache_hit_ratio_history_site_sampled_idx' ) THEN CREATE INDEX "site_cache_hit_ratio_history_site_sampled_idx" ON "public"."site_cache_hit_ratio_history" ("site_id", "sampled_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_cache_hit_ratio_history' AND indexname = 'site_cache_hit_ratio_history_created_idx' ) THEN CREATE INDEX "site_cache_hit_ratio_history_created_idx" ON "public"."site_cache_hit_ratio_history" ("created_at"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_cache_hit_ratio_history" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_cache_hit_ratio_history" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_hit_ratio_history' AND policyname = 'site_cache_hit_ratio_history_tenant_isolation' ) THEN CREATE POLICY "site_cache_hit_ratio_history_tenant_isolation" ON "public"."site_cache_hit_ratio_history" USING ( "tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid ) WITH CHECK ( "tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_hit_ratio_history' AND policyname = 'site_cache_hit_ratio_history_agent' ) THEN CREATE POLICY "site_cache_hit_ratio_history_agent" ON "public"."site_cache_hit_ratio_history" USING (current_setting('app.agent', true) = 'on'); -- No WITH CHECK: the GC path only deletes; inserts flow through -- the tenant_isolation policy via InTenantTx. END IF; END; $$; -- ok (686.04325ms) -- migrating version 20260616000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'woo_cacheable_session' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "woo_cacheable_session" boolean NOT NULL DEFAULT false; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'woo_theme_fragments_supported' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "woo_theme_fragments_supported" boolean NOT NULL DEFAULT false; END IF; END; $$; -- ok (399.384375ms) -- migrating version 20260617000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'fonts_transcode_woff2' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "fonts_transcode_woff2" boolean NOT NULL DEFAULT false; END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."font_transcode_results" ( -- source_hash is the hex-encoded BLAKE3 hash of the raw source font bytes. -- It is the content-address key: the same font file always maps to the -- same hash regardless of tenant/site, so a hash collision across sites -- would land in the same row per tenant (tenant_id prevents cross-tenant -- sharing). The asset key derives from it: ".woff2". "source_hash" text NOT NULL, "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- river_job_id is the River job ID for the in-flight font_transcode job. -- NULL when the job has not yet been inserted or has already finished. "river_job_id" bigint, -- woff2_key is the object-storage key of the produced WOFF2 file. -- NULL until transcoding completes successfully. "woff2_key" text, -- negative is true when transcoding was attempted but permanently failed -- (unsupported font, malformed data, or repeated transient errors). -- When negative=true the agent must serve the original font forever; -- the CP will never retry this content hash. "negative" boolean NOT NULL DEFAULT false, -- error_detail is a short human-readable explanation for negative=true -- rows. NULL on success rows. "error_detail" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (source_hash, tenant_id) ); -> CREATE INDEX IF NOT EXISTS "font_transcode_results_site_id_idx" ON "public"."font_transcode_results" (tenant_id, site_id); -> ALTER TABLE "public"."font_transcode_results" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."font_transcode_results" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_transcode_results' AND policyname = 'tenant_isolation' ) THEN CREATE POLICY "tenant_isolation" ON "public"."font_transcode_results" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_transcode_results' AND policyname = 'agent_access' ) THEN CREATE POLICY "agent_access" ON "public"."font_transcode_results" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (801.302666ms) -- migrating version 20260618000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'fonts_subset' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "fonts_subset" boolean NOT NULL DEFAULT false; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'fonts_subset_mode' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "fonts_subset_mode" text NOT NULL DEFAULT 'range'; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'fonts_subset_range' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "fonts_subset_range" text NOT NULL DEFAULT 'latin-ext'; END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."font_results" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- source_hash is the hex-encoded BLAKE3 hash of the raw source font bytes. -- Joins to font_transcode_results for job-control details. "source_hash" text NOT NULL, -- family is the CSS font-family name reported by the agent at discovery. -- NULL until the agent reports it (pre-M55 or first push). "family" text, -- source_file is the basename of the original font URL (e.g. "inter.woff"). "source_file" text, -- original_ext is the source format: ttf | otf | woff. "original_ext" text, -- original_size is the byte length of the source font file. "original_size" integer, -- woff2_size is the byte length of the full WOFF2 output. -- NULL until the full transcode completes. "woff2_size" integer, -- subset_size is the byte length of the subset WOFF2 output. -- NULL unless a subset was produced. "subset_size" integer, -- unicode_range is the CSS unicode-range descriptor for the subset -- (e.g. "U+0000-00FF,U+0100-024F,U+1E00-1EFF"). NULL unless subset. "unicode_range" text, -- state is the agent-reported lifecycle: -- pending = job enqueued, output not yet available. -- ready = full WOFF2 produced (woff2_size set, subset_size NULL). -- subset = subset WOFF2 also produced (both sizes set). Superset of ready. -- negative = permanent failure; serve the original font forever. "state" text NOT NULL DEFAULT 'pending', -- error_detail is a short human-readable explanation for negative state. -- NULL for non-negative rows. "error_detail" text, -- savings_pct is CP-derived at upsert: 1 - (best_size / original_size). -- Uses min(woff2_size, subset_size) as best_size. NULL when sizes unknown. "savings_pct" numeric(5,2), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "font_results_pkey" PRIMARY KEY (id), CONSTRAINT "font_results_site_hash_uniq" UNIQUE (site_id, source_hash), CONSTRAINT "font_results_state_check" CHECK (state IN ('pending','ready','subset','negative')), CONSTRAINT "font_results_tenant_fk" FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, CONSTRAINT "font_results_site_fk" FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "idx_font_results_site" ON "public"."font_results" (site_id, updated_at DESC); -> CREATE INDEX IF NOT EXISTS "font_results_tenant_idx" ON "public"."font_results" (tenant_id); -> ALTER TABLE "public"."font_results" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."font_results" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_results' AND policyname = 'font_results_tenant_isolation' ) THEN CREATE POLICY "font_results_tenant_isolation" ON "public"."font_results" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_results' AND policyname = 'font_results_agent_access' ) THEN CREATE POLICY "font_results_agent_access" ON "public"."font_results" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (959.9895ms) -- migrating version 20260619000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'rum_enabled' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "rum_enabled" boolean NOT NULL DEFAULT false; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'rum_sample_rate' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "rum_sample_rate" real NOT NULL DEFAULT 1.0; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'max_distinct_countries' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "max_distinct_countries" integer NOT NULL DEFAULT 8; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'min_sample_count' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "min_sample_count" integer NOT NULL DEFAULT 100; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'beacon_key_hash' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "beacon_key_hash" bytea; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'beacon_key_hash_prev' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "beacon_key_hash_prev" bytea; END IF; END; $$; -> CREATE UNIQUE INDEX IF NOT EXISTS site_perf_config_beacon_key_hash_uniq ON "public"."site_perf_config" (beacon_key_hash) WHERE beacon_key_hash IS NOT NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_perf_config' AND policyname = 'site_perf_config_rum_lookup' ) THEN CREATE POLICY "site_perf_config_rum_lookup" ON "public"."site_perf_config" FOR SELECT USING (current_setting('app.rum_lookup', true) = 'on'); END IF; END; $$; -> CREATE OR REPLACE FUNCTION rum_add_int_arrays(a integer[], b integer[]) RETURNS integer[] LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$ SELECT array_agg(ai + bi ORDER BY idx) FROM unnest(a, b) WITH ORDINALITY AS t(ai, bi, idx) $$; -> CREATE TABLE IF NOT EXISTS "public"."rum_events_raw" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "url_pattern" text NOT NULL DEFAULT '', "metric" text NOT NULL CHECK (metric IN ('lcp','inp','cls','ttfb','fcp')), "value_milli" integer NOT NULL DEFAULT 0, "device" text NOT NULL DEFAULT 'desktop', "country" text NOT NULL DEFAULT '__other__', "conn" text NOT NULL DEFAULT 'unknown', "received_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "rum_events_raw_pkey" PRIMARY KEY (id), CONSTRAINT "rum_events_raw_tenant_fk" FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, CONSTRAINT "rum_events_raw_site_fk" FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS rum_events_raw_received_at_brin ON "public"."rum_events_raw" USING BRIN (received_at); -> CREATE INDEX IF NOT EXISTS rum_events_raw_site_received_idx ON "public"."rum_events_raw" (site_id, received_at); -> CREATE INDEX IF NOT EXISTS rum_events_raw_tenant_idx ON "public"."rum_events_raw" (tenant_id); -> ALTER TABLE "public"."rum_events_raw" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."rum_events_raw" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_events_raw' AND policyname = 'rum_events_raw_tenant_isolation' ) THEN CREATE POLICY "rum_events_raw_tenant_isolation" ON "public"."rum_events_raw" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_events_raw' AND policyname = 'rum_events_raw_rum_ingest' ) THEN CREATE POLICY "rum_events_raw_rum_ingest" ON "public"."rum_events_raw" FOR INSERT WITH CHECK ( current_setting('app.rum_ingest', true) = 'on' AND tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid AND site_id = nullif(current_setting('app.site_id', true), '')::uuid ); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."rum_rollup_hourly" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "url_pattern" text NOT NULL, "metric" text NOT NULL, "device" text NOT NULL, "country" text NOT NULL, "bucket_hour" timestamptz NOT NULL, "sample_count" bigint NOT NULL DEFAULT 0, "sample_rate" real NOT NULL DEFAULT 1.0, "bucket_counts" integer[] NOT NULL DEFAULT '{}', "sum_value" bigint NOT NULL DEFAULT 0, "min_value" integer NOT NULL DEFAULT 0, "max_value" integer NOT NULL DEFAULT 0, CONSTRAINT "rum_rollup_hourly_pkey" PRIMARY KEY (site_id, url_pattern, metric, device, country, bucket_hour), CONSTRAINT "rum_rollup_hourly_tenant_fk" FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, CONSTRAINT "rum_rollup_hourly_site_fk" FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS rum_rollup_hourly_tenant_idx ON "public"."rum_rollup_hourly" (tenant_id); -> ALTER TABLE "public"."rum_rollup_hourly" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."rum_rollup_hourly" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_hourly' AND policyname = 'rum_rollup_hourly_tenant_isolation' ) THEN CREATE POLICY "rum_rollup_hourly_tenant_isolation" ON "public"."rum_rollup_hourly" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_hourly' AND policyname = 'rum_rollup_hourly_rum_ingest' ) THEN CREATE POLICY "rum_rollup_hourly_rum_ingest" ON "public"."rum_rollup_hourly" FOR INSERT WITH CHECK ( current_setting('app.rum_ingest', true) = 'on' AND tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid AND site_id = nullif(current_setting('app.site_id', true), '')::uuid ); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."rum_rollup_daily" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "url_pattern" text NOT NULL, "metric" text NOT NULL, "device" text NOT NULL, "country" text NOT NULL, "bucket_day" date NOT NULL, "sample_count" bigint NOT NULL DEFAULT 0, "sample_rate" real NOT NULL DEFAULT 1.0, "bucket_counts" integer[] NOT NULL DEFAULT '{}', "sum_value" bigint NOT NULL DEFAULT 0, "min_value" integer NOT NULL DEFAULT 0, "max_value" integer NOT NULL DEFAULT 0, CONSTRAINT "rum_rollup_daily_pkey" PRIMARY KEY (site_id, url_pattern, metric, device, country, bucket_day), CONSTRAINT "rum_rollup_daily_tenant_fk" FOREIGN KEY (tenant_id) REFERENCES tenants (id) ON DELETE CASCADE, CONSTRAINT "rum_rollup_daily_site_fk" FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS rum_rollup_daily_tenant_idx ON "public"."rum_rollup_daily" (tenant_id); -> ALTER TABLE "public"."rum_rollup_daily" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."rum_rollup_daily" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_daily' AND policyname = 'rum_rollup_daily_tenant_isolation' ) THEN CREATE POLICY "rum_rollup_daily_tenant_isolation" ON "public"."rum_rollup_daily" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_daily' AND policyname = 'rum_rollup_daily_rum_ingest' ) THEN CREATE POLICY "rum_rollup_daily_rum_ingest" ON "public"."rum_rollup_daily" FOR INSERT WITH CHECK ( current_setting('app.rum_ingest', true) = 'on' AND tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid AND site_id = nullif(current_setting('app.site_id', true), '')::uuid ); END IF; END; $$; -- ok (2.403956208s) -- migrating version 20260620000000 -> ALTER TABLE "public"."site_perf_config" ALTER COLUMN "min_sample_count" SET DEFAULT 30; -- ok (314.023334ms) -- migrating version 20260621000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'sites' AND column_name = 'missed_heartbeats' ) THEN ALTER TABLE "public"."sites" ADD COLUMN "missed_heartbeats" integer NOT NULL DEFAULT 0; END IF; END $$; -- ok (339.435917ms) -- migrating version 20260622000000 -> CREATE TABLE IF NOT EXISTS "public"."site_email_config" ( -- Surrogate PK; surrogate is necessary because site_id may be NULL (org-wide -- default rows have no site_id). "id" uuid NOT NULL DEFAULT gen_random_uuid(), -- Tenancy + site scope. tenant_id is always present; site_id is NULL for the -- org-wide default row and non-NULL for per-site overrides. "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "site_id" uuid REFERENCES sites (id) ON DELETE CASCADE, -- Provider slug: smtp | ses | sendgrid | mailgun | postmark (v1). "provider" text NOT NULL DEFAULT 'smtp', -- Sender identity. "from_address" text NOT NULL DEFAULT '', "from_name" text NOT NULL DEFAULT '', -- When true the agent overrides the WP-generated From address/name with these. "force_from_email" boolean NOT NULL DEFAULT false, "force_from_name" boolean NOT NULL DEFAULT false, -- When true the provider sets the Return-Path / bounce address. "return_path" boolean NOT NULL DEFAULT false, -- Non-secret provider config (host/port/encryption/auth for SMTP; -- region/domain/message_stream for API providers). "config" jsonb NOT NULL DEFAULT '{}'::jsonb, -- age-encrypted provider secret (API key or SMTP password). -- Never returned to clients; only a secret_set: bool is surfaced. "provider_secret_encrypted" bytea, -- OAuth refresh/access tokens (Phase 3 — Gmail/Outlook; NULL in Phase 1). "oauth_refresh_encrypted" bytea, "oauth_access_encrypted" bytea, "oauth_expires_at" timestamptz, -- Routing: per-FROM-address routing map (JSON object: email→connection_key), -- and the name of the default + optional fallback connections. "mappings" jsonb NOT NULL DEFAULT '{}'::jsonb, "default_connection" text, "fallback_connection" text, -- Log policy (per-site opt-in overrides). "log_emails" boolean NOT NULL DEFAULT true, "store_body" boolean NOT NULL DEFAULT false, "retention_days" integer NOT NULL DEFAULT 14, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_email_config_pkey" PRIMARY KEY ("id") ); -> CREATE UNIQUE INDEX IF NOT EXISTS "site_email_config_per_site_idx" ON "public"."site_email_config" ("tenant_id", "site_id") WHERE "site_id" IS NOT NULL; -> CREATE UNIQUE INDEX IF NOT EXISTS "site_email_config_org_default_idx" ON "public"."site_email_config" ("tenant_id") WHERE "site_id" IS NULL; -> CREATE INDEX IF NOT EXISTS "site_email_config_tenant_idx" ON "public"."site_email_config" ("tenant_id"); -> ALTER TABLE "public"."site_email_config" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_email_config" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_tenant_isolation' ) THEN CREATE POLICY "site_email_config_tenant_isolation" ON "public"."site_email_config" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_agent' ) THEN CREATE POLICY "site_email_config_agent" ON "public"."site_email_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."site_email_log" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "site_id" uuid NOT NULL REFERENCES sites (id) ON DELETE CASCADE, -- agent_seq is the agent-local monotonic counter for keyset-cursor pagination. -- UNIQUE (tenant_id, site_id, agent_seq) enforces idempotent ingest. "agent_seq" bigint, "message_id" text, "to_addresses" text[] NOT NULL DEFAULT '{}', "from_address" text NOT NULL DEFAULT '', "subject" text NOT NULL DEFAULT '', "provider" text NOT NULL DEFAULT '', -- status: pending | sent | failed | bounced | complained "status" text NOT NULL DEFAULT 'pending', "response" jsonb NOT NULL DEFAULT '{}'::jsonb, "error" text NOT NULL DEFAULT '', "retries" integer NOT NULL DEFAULT 0, "resent_count" integer NOT NULL DEFAULT 0, -- body_stored=true means body column is populated; false by default (privacy). "body_stored" boolean NOT NULL DEFAULT false, "body" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_email_log_pkey" PRIMARY KEY ("id") ); -> CREATE INDEX IF NOT EXISTS "site_email_log_site_time_idx" ON "public"."site_email_log" ("tenant_id", "site_id", "created_at" DESC); -> CREATE INDEX IF NOT EXISTS "site_email_log_tenant_time_idx" ON "public"."site_email_log" ("tenant_id", "created_at" DESC); -> CREATE INDEX IF NOT EXISTS "site_email_log_failed_idx" ON "public"."site_email_log" ("tenant_id", "created_at" DESC) WHERE "status" = 'failed'; -> CREATE UNIQUE INDEX IF NOT EXISTS "site_email_log_seq_idx" ON "public"."site_email_log" ("tenant_id", "site_id", "agent_seq") WHERE "agent_seq" IS NOT NULL; -> ALTER TABLE "public"."site_email_log" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_email_log" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_tenant_isolation' ) THEN CREATE POLICY "site_email_log_tenant_isolation" ON "public"."site_email_log" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_agent' ) THEN CREATE POLICY "site_email_log_agent" ON "public"."site_email_log" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."email_suppression" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, -- site_id NULL = fleet-wide suppression across the whole org/tenant. "site_id" uuid REFERENCES sites (id) ON DELETE CASCADE, -- email_hash is the HMAC-SHA256 of the normalised email address (lowercase) -- keyed with the tenant's age-derived HMAC secret — so raw emails are never -- stored when store_body=false. email column may be populated for opt-in body -- storage or when the reason requires the original (hard_bounce/complaint). "email_hash" bytea NOT NULL, "email" text, -- reason: hard_bounce | complaint | unsubscribe | manual "reason" text NOT NULL DEFAULT 'manual', "provider" text NOT NULL DEFAULT '', "event_at" timestamptz, "source_message_id" text, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "email_suppression_pkey" PRIMARY KEY ("id") ); -> CREATE UNIQUE INDEX IF NOT EXISTS "email_suppression_site_hash_idx" ON "public"."email_suppression" ("tenant_id", "site_id", "email_hash") WHERE "site_id" IS NOT NULL; -> CREATE UNIQUE INDEX IF NOT EXISTS "email_suppression_fleet_hash_idx" ON "public"."email_suppression" ("tenant_id", "email_hash") WHERE "site_id" IS NULL; -> CREATE INDEX IF NOT EXISTS "email_suppression_tenant_idx" ON "public"."email_suppression" ("tenant_id"); -> ALTER TABLE "public"."email_suppression" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_suppression" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_tenant_isolation' ) THEN CREATE POLICY "email_suppression_tenant_isolation" ON "public"."email_suppression" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_agent' ) THEN CREATE POLICY "email_suppression_agent" ON "public"."email_suppression" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (2.082972125s) -- migrating version 20260623000000 -> CREATE TABLE IF NOT EXISTS "public"."email_webhook_events" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), -- Provider-assigned stable event identifier (used for dedup). -- For SNS: Message.MessageId -- For SendGrid: event.sg_event_id -- For Mailgun: event.id (or derived from token) -- For Postmark: RecordID (cast to text) "provider_event_id" text NOT NULL, "provider" text NOT NULL, -- Resolved fan-out target (NULL when metadata was absent / unparseable). "tenant_id" uuid, "site_id" uuid, -- Normalised email address that generated the event (lower-cased before storing). "email" text, "event_type" text NOT NULL DEFAULT '', -- hard_bounce | complaint | etc. -- Suppression row created for this event (NULL = no suppression row written, -- e.g. non-suppression event type like click/open). "suppression_id" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "email_webhook_events_pkey" PRIMARY KEY ("id") ); -> CREATE UNIQUE INDEX IF NOT EXISTS "email_webhook_events_dedup_idx" ON "public"."email_webhook_events" ("provider", "provider_event_id"); -> CREATE INDEX IF NOT EXISTS "email_webhook_events_created_idx" ON "public"."email_webhook_events" ("created_at"); -> CREATE INDEX IF NOT EXISTS "email_webhook_events_tenant_idx" ON "public"."email_webhook_events" ("tenant_id") WHERE "tenant_id" IS NOT NULL; -> ALTER TABLE "public"."email_webhook_events" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_webhook_events" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_webhook_events' AND policyname = 'email_webhook_events_agent' ) THEN CREATE POLICY "email_webhook_events_agent" ON "public"."email_webhook_events" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_webhook_events' AND policyname = 'email_webhook_events_tenant_isolation' ) THEN CREATE POLICY "email_webhook_events_tenant_isolation" ON "public"."email_webhook_events" USING ( tenant_id IS NULL OR tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid ) WITH CHECK ( tenant_id IS NULL OR tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid ); END IF; END; $$; -- ok (801.5315ms) -- migrating version 20260624000000 -> DO $$ BEGIN -- webhook_route_token_hash: SHA-256(random 32-byte token) for constant-time -- lookup. Unique across the table so each URL resolves exactly one config row. IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_email_config' AND column_name = 'webhook_route_token_hash' ) THEN ALTER TABLE site_email_config ADD COLUMN webhook_route_token_hash bytea; END IF; -- webhook_signing_key_enc: age-encrypted per-provider webhook signing key. -- For SendGrid: ECDSA public key PEM -- For Mailgun: HMAC webhook signing key (NOT the Private API Key) -- For Postmark: per-server secret (embedded in the URL path) -- For SES: unused (SES uses cert-pinned RSA; ses_topic_arns is the guard) IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_email_config' AND column_name = 'webhook_signing_key_enc' ) THEN ALTER TABLE site_email_config ADD COLUMN webhook_signing_key_enc bytea; END IF; -- ses_topic_arns: allowlist of SNS TopicArns this config row accepts. -- For SES users: at least one ARN must be present; events arriving on any -- other TopicArn are rejected even if the SNS signature is valid. -- NULL for non-SES providers. IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_email_config' AND column_name = 'ses_topic_arns' ) THEN ALTER TABLE site_email_config ADD COLUMN ses_topic_arns text[]; END IF; END; $$; -> CREATE UNIQUE INDEX IF NOT EXISTS site_email_config_route_token_hash_idx ON site_email_config (webhook_route_token_hash) WHERE webhook_route_token_hash IS NOT NULL; -> DO $$ BEGIN -- Add email_hash column if absent. IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'email_webhook_events' AND column_name = 'email_hash' ) THEN ALTER TABLE email_webhook_events ADD COLUMN email_hash bytea; END IF; -- Drop plaintext email column if still present. IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'email_webhook_events' AND column_name = 'email' ) THEN ALTER TABLE email_webhook_events DROP COLUMN email; END IF; END; $$; -- ok (460.916125ms) -- migrating version 20260625000000 -> CREATE TABLE IF NOT EXISTS "public"."site_email_connection" ( -- Surrogate PK. "id" uuid NOT NULL DEFAULT gen_random_uuid(), -- Tenancy + parent config row. "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "config_id" uuid NOT NULL REFERENCES site_email_config (id) ON DELETE CASCADE, -- Operator-chosen slug key: ^[a-z0-9][a-z0-9_-]{0,31}$ -- 'default' is RESERVED for the primary (the site_email_config row itself). "connection_key" text NOT NULL, -- Provider slug (smtp | ses | sendgrid | mailgun | postmark). "provider" text NOT NULL DEFAULT 'smtp', -- Optional per-connection sender identity overrides. "from_address" text NOT NULL DEFAULT '', "from_name" text NOT NULL DEFAULT '', -- Non-secret provider config (host/port/encryption/region/domain_name etc.). "config" jsonb NOT NULL DEFAULT '{}'::jsonb, -- age-encrypted per-connection secret (API key or SMTP password). -- Never returned to clients; only secret_set: bool is surfaced. "provider_secret_encrypted" bytea, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_email_connection_pkey" PRIMARY KEY ("id"), -- Slug format: lowercase alphanumeric + hyphens/underscores, 1-32 chars. -- 'default' reserved: the primary row always owns that key. CONSTRAINT "site_email_connection_key_check" CHECK (connection_key ~ '^[a-z0-9][a-z0-9_-]{0,31}$' AND connection_key <> 'default') ); -> CREATE UNIQUE INDEX IF NOT EXISTS "site_email_connection_cfg_key_idx" ON "public"."site_email_connection" ("config_id", "connection_key"); -> CREATE INDEX IF NOT EXISTS "site_email_connection_tenant_idx" ON "public"."site_email_connection" ("tenant_id"); -> ALTER TABLE "public"."site_email_connection" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_email_connection" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_tenant_isolation' ) THEN CREATE POLICY "site_email_connection_tenant_isolation" ON "public"."site_email_connection" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_agent' ) THEN CREATE POLICY "site_email_connection_agent" ON "public"."site_email_connection" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> ALTER TABLE "public"."site_email_log" ADD COLUMN IF NOT EXISTS "connection_key" text NOT NULL DEFAULT ''; -> ALTER TABLE "public"."site_email_log" ADD COLUMN IF NOT EXISTS "attachments" jsonb NOT NULL DEFAULT '[]'::jsonb; -> CREATE TABLE IF NOT EXISTS "public"."email_notify_settings" ( -- One row per tenant (tenant_id IS the PK — org-level, not per-site). "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, -- Master kill-switch. "enabled" boolean NOT NULL DEFAULT false, -- Recipients: JSONB array of email strings, max 20 per application logic. "recipients" jsonb NOT NULL DEFAULT '[]'::jsonb, -- Per-failure alert (agent-ingested status=failed only). "alert_on_failure" boolean NOT NULL DEFAULT true, -- Minimum minutes between consecutive failure alerts for the same site. -- Range [15, 1440] (15 min – 24 h); default 60 min. "alert_throttle_minutes" integer NOT NULL DEFAULT 60 CONSTRAINT "email_notify_settings_throttle_range" CHECK (alert_throttle_minutes BETWEEN 15 AND 1440), -- Hourly digest scheduler. "digest_enabled" boolean NOT NULL DEFAULT false, "digest_cadence" text NOT NULL DEFAULT 'weekly' CONSTRAINT "email_notify_settings_digest_cadence" CHECK (digest_cadence IN ('weekly', 'monthly')), -- For weekly: 0=Sunday … 6=Saturday. For monthly: 1-28. "digest_day" integer NOT NULL DEFAULT 1 CONSTRAINT "email_notify_settings_digest_day" CHECK (digest_day BETWEEN 0 AND 28), "digest_hour" integer NOT NULL DEFAULT 8 CONSTRAINT "email_notify_settings_digest_hour" CHECK (digest_hour BETWEEN 0 AND 23), "timezone" text NOT NULL DEFAULT 'UTC', -- Next scheduled digest timestamp (computed by the service on PUT). -- NULL = never scheduled (digest_enabled = false or no tz loaded). "next_digest_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "email_notify_settings_pkey" PRIMARY KEY ("tenant_id") ); -> CREATE INDEX IF NOT EXISTS "email_notify_settings_due_idx" ON "public"."email_notify_settings" ("next_digest_at") WHERE "digest_enabled" AND "enabled"; -> ALTER TABLE "public"."email_notify_settings" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_notify_settings" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_notify_settings' AND policyname = 'email_notify_settings_tenant_isolation' ) THEN CREATE POLICY "email_notify_settings_tenant_isolation" ON "public"."email_notify_settings" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_notify_settings' AND policyname = 'email_notify_settings_agent' ) THEN CREATE POLICY "email_notify_settings_agent" ON "public"."email_notify_settings" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."email_alert_state" ( -- Composite PK: one row per (tenant, site). "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "site_id" uuid NOT NULL REFERENCES sites (id) ON DELETE CASCADE, -- Timestamp of the last alert email sent for this site. -- NULL = no alert has been sent yet. "last_alert_at" timestamptz, -- Failures accumulated since the last alert was sent (reset on claim). "failures_since_alert" bigint NOT NULL DEFAULT 0, "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "email_alert_state_pkey" PRIMARY KEY ("tenant_id", "site_id") ); -> ALTER TABLE "public"."email_alert_state" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."email_alert_state" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_alert_state' AND policyname = 'email_alert_state_tenant_isolation' ) THEN CREATE POLICY "email_alert_state_tenant_isolation" ON "public"."email_alert_state" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_alert_state' AND policyname = 'email_alert_state_agent' ) THEN CREATE POLICY "email_alert_state_agent" ON "public"."email_alert_state" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.71902575s) -- migrating version 20260626000000 -> CREATE TABLE IF NOT EXISTS "public"."clients" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "name" text NOT NULL, "contact_email" citext, "company" text, "phone" text, "notes" text, "color" text, "logo_url" text, "archived_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "clients_pkey" PRIMARY KEY ("id"), -- Backs the composite FK on sites (prevents tenant drift, mirrors -- sites_id_tenant_key used by site_shares in m19). CONSTRAINT "clients_id_tenant_key" UNIQUE ("id", "tenant_id") ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'clients' AND indexname = 'clients_tenant_idx' ) THEN CREATE INDEX "clients_tenant_idx" ON "public"."clients" ("tenant_id"); END IF; END; $$; -> ALTER TABLE "public"."clients" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."clients" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'clients' AND policyname = 'clients_tenant_isolation' ) THEN CREATE POLICY "clients_tenant_isolation" ON "public"."clients" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'clients' AND policyname = 'clients_agent' ) THEN CREATE POLICY "clients_agent" ON "public"."clients" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "client_id" uuid NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema = 'public' AND table_name = 'sites' AND constraint_name = 'sites_client_tenant_fkey' ) THEN ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_client_tenant_fkey" FOREIGN KEY ("client_id", "tenant_id") REFERENCES "public"."clients" ("id", "tenant_id") ON DELETE SET NULL; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'sites' AND indexname = 'sites_client_idx' ) THEN CREATE INDEX "sites_client_idx" ON "public"."sites" ("client_id") WHERE "client_id" IS NOT NULL; END IF; END; $$; -- ok (914.551583ms) -- migrating version 20260627000000 -> ALTER TABLE "public"."clients" ADD COLUMN IF NOT EXISTS "timezone" text NOT NULL DEFAULT 'UTC'; -> CREATE TABLE IF NOT EXISTS "public"."report_schedules" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "client_id" uuid NOT NULL, "enabled" boolean NOT NULL DEFAULT false, "cadence" text NOT NULL DEFAULT 'monthly' CONSTRAINT "report_schedules_cadence" CHECK (cadence IN ('weekly','monthly')), -- weekly: 0=Sunday..6=Saturday; monthly: 1-28 (m62 semantics verbatim, -- migrations/20260625000000_m62_email_v1_completion.sql:146-152) "send_day" integer NOT NULL DEFAULT 1 CONSTRAINT "report_schedules_send_day" CHECK (send_day BETWEEN 0 AND 28), "send_hour" integer NOT NULL DEFAULT 8 CONSTRAINT "report_schedules_send_hour" CHECK (send_hour BETWEEN 0 AND 23), -- JSONB array of email strings; max 10 enforced in the service (cap directive). "recipients" jsonb NOT NULL DEFAULT '[]'::jsonb, -- Section on/off flags: {"overview":bool,"uptime":bool,"backups":bool, -- "updates":bool,"performance":bool,"email":bool}. Missing key = true. "sections" jsonb NOT NULL DEFAULT '{}'::jsonb, "intro_text" text NOT NULL DEFAULT '', "closing_text" text NOT NULL DEFAULT '', -- Decision 5: powered-by footer ON by default, FREE toggle to remove. "powered_by_removed" boolean NOT NULL DEFAULT false, "next_run_at" timestamptz, "last_run_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "report_schedules_pkey" PRIMARY KEY ("id"), -- One schedule per client. CONSTRAINT "report_schedules_client_key" UNIQUE ("client_id"), -- Composite FK: cross-tenant-proof (m63 pattern, 20260626000000_m63_clients.sql). -- ON DELETE CASCADE: a schedule is meaningless without its client; deleting a -- client silently stops (removes) its report schedule. This deliberately -- DIFFERS from sites.client_id ON DELETE SET NULL (decision 4) because sites -- outlive clients but a per-client schedule does not. CONSTRAINT "report_schedules_client_tenant_fkey" FOREIGN KEY ("client_id","tenant_id") REFERENCES "public"."clients" ("id","tenant_id") ON DELETE CASCADE ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'report_schedules' AND indexname = 'report_schedules_tenant_idx' ) THEN CREATE INDEX "report_schedules_tenant_idx" ON "public"."report_schedules" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'report_schedules' AND indexname = 'report_schedules_due_idx' ) THEN CREATE INDEX "report_schedules_due_idx" ON "public"."report_schedules" ("next_run_at") WHERE "enabled"; END IF; END; $$; -> ALTER TABLE "public"."report_schedules" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."report_schedules" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'report_schedules' AND policyname = 'report_schedules_tenant_isolation' ) THEN CREATE POLICY "report_schedules_tenant_isolation" ON "public"."report_schedules" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'report_schedules' AND policyname = 'report_schedules_agent' ) THEN CREATE POLICY "report_schedules_agent" ON "public"."report_schedules" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."generated_reports" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "client_id" uuid NOT NULL, -- NULL = on-demand. SET NULL (not CASCADE): toggling/recreating a schedule -- must never destroy report history. "schedule_id" uuid REFERENCES "public"."report_schedules" ("id") ON DELETE SET NULL, "period_start" timestamptz NOT NULL, "period_end" timestamptz NOT NULL, "status" text NOT NULL DEFAULT 'pending' CONSTRAINT "generated_reports_status" CHECK (status IN ('pending','generating','completed','failed')), "data_snapshot" jsonb NOT NULL DEFAULT '{}'::jsonb, "html_blob_key" text NOT NULL DEFAULT '', "pdf_blob_key" text NOT NULL DEFAULT '', "error" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "completed_at" timestamptz, CONSTRAINT "generated_reports_pkey" PRIMARY KEY ("id"), -- ON DELETE CASCADE: reports are only reachable through the client detail page; -- rows about a deleted client are dead UI weight. Cost accepted + documented: -- client-delete orphans the html/pdf blobs (presigned URLs expire <=7d; objects -- are KB-MB scale). The DELETE endpoint deletes blobs best-effort; a lifecycle -- rule can sweep stragglers later. CONSTRAINT "generated_reports_client_tenant_fkey" FOREIGN KEY ("client_id","tenant_id") REFERENCES "public"."clients" ("id","tenant_id") ON DELETE CASCADE ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'generated_reports' AND indexname = 'generated_reports_list_idx' ) THEN CREATE INDEX "generated_reports_list_idx" ON "public"."generated_reports" ("tenant_id", "client_id", "created_at" DESC, "id" DESC); END IF; END; $$; -> ALTER TABLE "public"."generated_reports" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."generated_reports" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'generated_reports' AND policyname = 'generated_reports_tenant_isolation' ) THEN CREATE POLICY "generated_reports_tenant_isolation" ON "public"."generated_reports" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'generated_reports' AND policyname = 'generated_reports_agent' ) THEN CREATE POLICY "generated_reports_agent" ON "public"."generated_reports" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.40801575s) -- migrating version 20260628000000 -> ALTER TABLE "public"."generated_reports" ADD COLUMN IF NOT EXISTS "updated_at" timestamptz NOT NULL DEFAULT now(); -- ok (321.861542ms) -- migrating version 20260629000000 -> ALTER TABLE "public"."sites" DROP CONSTRAINT IF EXISTS "sites_client_tenant_fkey"; -> ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_client_tenant_fkey" FOREIGN KEY ("client_id", "tenant_id") REFERENCES "public"."clients" ("id", "tenant_id") ON DELETE SET NULL ("client_id"); -> CREATE TABLE IF NOT EXISTS "public"."client_members" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, "client_id" uuid NOT NULL, "user_id" uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE, "invited_by" uuid NULL REFERENCES users (id) ON DELETE SET NULL, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "client_members_pkey" PRIMARY KEY ("id"), -- One roster row per (client, user); upserts target this pair. CONSTRAINT "client_members_client_user_key" UNIQUE ("client_id", "user_id"), -- Composite FK: cross-tenant-proof (mirrors sites_client_tenant_fkey in -- m63). ON DELETE CASCADE: deleting a client revokes portal access. CONSTRAINT "client_members_client_tenant_fkey" FOREIGN KEY ("client_id", "tenant_id") REFERENCES "public"."clients" ("id", "tenant_id") ON DELETE CASCADE ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'client_members' AND indexname = 'client_members_user_tenant_idx' ) THEN CREATE INDEX "client_members_user_tenant_idx" ON "public"."client_members" ("user_id", "tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'client_members' AND indexname = 'client_members_client_idx' ) THEN CREATE INDEX "client_members_client_idx" ON "public"."client_members" ("client_id"); END IF; END; $$; -> ALTER TABLE "public"."client_members" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."client_members" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'client_members' AND policyname = 'client_members_tenant_isolation' ) THEN CREATE POLICY "client_members_tenant_isolation" ON "public"."client_members" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'client_members' AND policyname = 'client_members_agent' ) THEN CREATE POLICY "client_members_agent" ON "public"."client_members" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'client_members' AND policyname = 'client_members_self_read' ) THEN CREATE POLICY "client_members_self_read" ON "public"."client_members" FOR SELECT USING (user_id = nullif(current_setting('app.user_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'clients' AND policyname = 'clients_member_read' ) THEN CREATE POLICY "clients_member_read" ON "public"."clients" FOR SELECT USING (EXISTS ( SELECT 1 FROM client_members cm WHERE cm.client_id = clients.id AND cm.tenant_id = clients.tenant_id AND cm.user_id = nullif(current_setting('app.user_id', true), '')::uuid )); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'sites' AND policyname = 'sites_client_read' ) THEN CREATE POLICY "sites_client_read" ON "public"."sites" FOR SELECT USING (EXISTS ( SELECT 1 FROM client_members cm JOIN clients cl ON cl.id = cm.client_id AND cl.tenant_id = cm.tenant_id WHERE cm.client_id = sites.client_id AND cm.tenant_id = sites.tenant_id AND cm.user_id = nullif(current_setting('app.user_id', true), '')::uuid AND cl.archived_at IS NULL )); END IF; END; $$; -> ALTER TABLE "public"."invitations" ADD COLUMN IF NOT EXISTS "client_id" uuid NULL; -> ALTER TABLE "public"."invitations" DROP CONSTRAINT IF EXISTS "invitations_scope_check"; -> ALTER TABLE "public"."invitations" ADD CONSTRAINT "invitations_scope_check" CHECK (scope IN ('org', 'site', 'client')); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema = 'public' AND table_name = 'invitations' AND constraint_name = 'invitations_client_tenant_fkey' ) THEN ALTER TABLE "public"."invitations" ADD CONSTRAINT "invitations_client_tenant_fkey" FOREIGN KEY ("client_id", "tenant_id") REFERENCES "public"."clients" ("id", "tenant_id") ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'invitations' AND indexname = 'invitations_client_id_idx' ) THEN CREATE INDEX "invitations_client_id_idx" ON "public"."invitations" ("client_id", "created_at" DESC) WHERE scope = 'client'; END IF; END; $$; -- ok (1.499013916s) -- migrating version 20260630000000 -> DO $$ BEGIN -- Drop NOT NULL constraint if it still exists. IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'woo_theme_fragments_supported' AND is_nullable = 'NO' ) THEN ALTER TABLE "public"."site_perf_config" ALTER COLUMN "woo_theme_fragments_supported" DROP NOT NULL; END IF; END; $$; -> DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'woo_theme_fragments_supported' AND column_default IS NOT NULL ) THEN ALTER TABLE "public"."site_perf_config" ALTER COLUMN "woo_theme_fragments_supported" DROP DEFAULT; END IF; END; $$; -> UPDATE "public"."site_perf_config" SET woo_theme_fragments_supported = NULL WHERE woo_theme_fragments_supported = false; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'woo_fragments_probed_at' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "woo_fragments_probed_at" timestamptz; END IF; END; $$; -- ok (537.307583ms) -- migrating version 20260701000000 -> CREATE TABLE IF NOT EXISTS "public"."site_object_cache_config" ( -- One row per site; site_id is the natural PK (mirrors site_perf_config). "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, -- Feature toggle. When false the agent ignores this config entirely. "enabled" boolean NOT NULL DEFAULT false, -- v1 topology: tcp | unix | tls. Schema also reserves sentinel/replicated/ -- cluster values for future topologies so no column migration is needed later. "scheme" text NOT NULL DEFAULT 'tcp', -- TCP / TLS connection fields. "host" text NOT NULL DEFAULT '', "port" integer NOT NULL DEFAULT 6379, -- Unix socket path (used when scheme='unix'; host/port ignored). "socket_path" text NOT NULL DEFAULT '', -- Redis database index (SELECT n). Default 0 = database 0. "database" integer NOT NULL DEFAULT 0, -- ACL username (empty = password-only AUTH, no ACL user). "username" text NOT NULL DEFAULT '', -- age-encrypted Redis password / ACL secret. NULL = no secret configured -- (Unix socket setups with no auth). nil-sentinel: on PUT, a missing or -- null value in the request preserves whatever ciphertext is already stored. -- Only decrypted inside the service when rendering a signed agent command. "password_encrypted" bytea, -- Key prefix applied to every Redis key written by this site. -- Defaults to a stable per-site value derived from site_id (set on first -- save by the service when the operator leaves it blank). Sanitized to -- 32 chars [a-z0-9_-] by the agent. "prefix" text NOT NULL DEFAULT '', -- TTL knobs. -- maxttl: ceiling applied to every SET with expire=0 or expire>maxttl. -- Default 604800 = 7 days (D6). -- queryttl: TTL for *-queries cache groups. Default 86400 = 24h. "maxttl_seconds" integer NOT NULL DEFAULT 604800, "queryttl_seconds" integer NOT NULL DEFAULT 86400, -- Connection resilience knobs (operator-tunable; agent enforces bounds). -- connect_timeout_ms: max time to establish a TCP connection. Default 1000ms. -- read_timeout_ms: max time to wait for a Redis response. Default 1000ms. -- retry_count: max connect attempts (decorrelated-jitter backoff). Default 3. -- retry_interval_ms: backoff base interval for connect retries. Default 25ms. "connect_timeout_ms" integer NOT NULL DEFAULT 1000, "read_timeout_ms" integer NOT NULL DEFAULT 1000, "retry_count" integer NOT NULL DEFAULT 3, "retry_interval_ms" integer NOT NULL DEFAULT 25, -- Serializer: php | igbinary. igbinary availability is capability-probed at -- connection TEST time; the agent falls back to php if igbinary is absent. "serializer" text NOT NULL DEFAULT 'php', -- Compression: none | lzf | lz4 | zstd. Similarly probed at TEST time. "compression" text NOT NULL DEFAULT 'none', -- async_flush: use UNLINK (async delete) instead of DEL for individual ops -- and FLUSHDB ASYNC instead of FLUSHDB for full flushes. Default false. "async_flush" boolean NOT NULL DEFAULT false, -- flush_strategy: auto | flushdb | scan. auto = let the TEST probe decide -- (flushdb on confirmed-dedicated DB, scan on shared). Default 'auto'. "flush_strategy" text NOT NULL DEFAULT 'auto', -- shared: operator-declared hint. When true the flush strategy is always -- scan; when false and TEST confirms FLUSHDB is permitted, full flush is used. -- Default true (safe for managed-Redis shared instances, D3). "shared" boolean NOT NULL DEFAULT true, -- flush_on_failback: when true the agent flushes the cache when Redis returns -- after a degraded/down window (ensures coherence, D5). Default true. "flush_on_failback" boolean NOT NULL DEFAULT true, -- analytics_enabled: when false the agent stops pushing the extended stats -- block (analytics disable switch, D4 interaction). Default true. "analytics_enabled" boolean NOT NULL DEFAULT true, -- Last passing test result hash-keyed to the current config. The enable -- handshake gate checks this: enable is rejected when NULL (no passing test). -- Cleared to NULL whenever the config changes (password/host/port/scheme/db). -- Stored as a short opaque token (sha256 hex of the config snapshot). "last_test_config_hash" text, -- Human-readable result from the most recent objectcache.test command. -- Stored for display in the dashboard; not used for gate logic. "last_test_result_json" jsonb NOT NULL DEFAULT '{}'::jsonb, -- Timestamp of the last passing test (for display only). "last_tested_at" timestamptz, -- Latest heartbeat-sourced status fields (stored so the CP can detect -- state transitions and publish SSE events without querying the agent). -- oc_state: connected | degraded | down | disabled (empty = disabled/unknown). "oc_state" text NOT NULL DEFAULT '', -- oc_latency_ms: rolling median command wait time reported in the heartbeat. "oc_latency_ms" integer NOT NULL DEFAULT 0, -- oc_last_error_class: last error class string from the agent error journal. "oc_last_error_class" text NOT NULL DEFAULT '', -- oc_used_memory_bytes: server INFO used_memory field. "oc_used_memory_bytes" bigint NOT NULL DEFAULT 0, -- oc_hit_ratio_pct: rolling hit ratio from the latest heartbeat window. "oc_hit_ratio_pct" numeric(5,2), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_object_cache_config_pkey" PRIMARY KEY ("site_id"), CONSTRAINT "site_object_cache_config_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON DELETE CASCADE, CONSTRAINT "site_object_cache_config_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "site_object_cache_config_tenant_idx" ON "public"."site_object_cache_config" ("tenant_id"); -> ALTER TABLE "public"."site_object_cache_config" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_object_cache_config" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_config' AND policyname = 'site_object_cache_config_tenant_isolation' ) THEN CREATE POLICY "site_object_cache_config_tenant_isolation" ON "public"."site_object_cache_config" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_config' AND policyname = 'site_object_cache_config_agent' ) THEN CREATE POLICY "site_object_cache_config_agent" ON "public"."site_object_cache_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."site_object_cache_stats_history" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, -- Window-delta hit/miss counts since the agent's last emission. "hit_count" bigint NOT NULL DEFAULT 0, "miss_count" bigint NOT NULL DEFAULT 0, -- Derived hit ratio percentage (CP-computed at ingest). -- NULL when both counts are zero. "ratio_pct" numeric(5,2), -- Server INFO snapshot fields sampled at report time. "used_memory_bytes" bigint NOT NULL DEFAULT 0, "avg_wait_ms" numeric(8,3) NOT NULL DEFAULT 0, "ops_per_sec" integer NOT NULL DEFAULT 0, "evicted_keys_delta" bigint NOT NULL DEFAULT 0, "connected_clients" integer NOT NULL DEFAULT 0, -- CP-assigned timestamp (canonical time axis for trend charts). "sampled_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_object_cache_stats_history_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON DELETE CASCADE, CONSTRAINT "site_object_cache_stats_history_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON DELETE CASCADE, CONSTRAINT "site_object_cache_stats_history_site_sampled_uniq" UNIQUE ("site_id", "sampled_at") ); -> CREATE INDEX IF NOT EXISTS "site_object_cache_stats_history_site_sampled_idx" ON "public"."site_object_cache_stats_history" ("site_id", "sampled_at" DESC); -> CREATE INDEX IF NOT EXISTS "site_object_cache_stats_history_created_idx" ON "public"."site_object_cache_stats_history" ("created_at"); -> ALTER TABLE "public"."site_object_cache_stats_history" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."site_object_cache_stats_history" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_stats_history' AND policyname = 'site_object_cache_stats_history_tenant_isolation' ) THEN CREATE POLICY "site_object_cache_stats_history_tenant_isolation" ON "public"."site_object_cache_stats_history" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_stats_history' AND policyname = 'site_object_cache_stats_history_agent' ) THEN CREATE POLICY "site_object_cache_stats_history_agent" ON "public"."site_object_cache_stats_history" USING (current_setting('app.agent', true) = 'on'); -- No WITH CHECK: the GC path only deletes; inserts flow through -- the tenant_isolation policy via InTenantTx. END IF; END; $$; -- ok (1.216504125s) -- migrating version 20260702000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_object_cache_config' AND column_name = 'oc_config_drift' ) THEN ALTER TABLE "public"."site_object_cache_config" ADD COLUMN "oc_config_drift" boolean NOT NULL DEFAULT false; END IF; END; $$; -- ok (322.864ms) -- migrating version 20260703000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_object_cache_config' AND column_name = 'debug_header_enabled' ) THEN ALTER TABLE "public"."site_object_cache_config" ADD COLUMN "debug_header_enabled" boolean NOT NULL DEFAULT false; END IF; END; $$; -- ok (311.857792ms) -- migrating version 20260704000000 -> CREATE TABLE IF NOT EXISTS site_db_clean_results ( site_id uuid NOT NULL, tenant_id uuid NOT NULL, job_id text NOT NULL, result_json jsonb NOT NULL DEFAULT '{}', rows_deleted bigint NOT NULL DEFAULT 0, bytes_freed bigint NOT NULL DEFAULT 0, cleaned_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT site_db_clean_results_pkey PRIMARY KEY (site_id) ); -> CREATE INDEX IF NOT EXISTS site_db_clean_results_tenant_idx ON site_db_clean_results (tenant_id); -> ALTER TABLE site_db_clean_results ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_db_clean_results FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'site_db_clean_results' AND policyname = 'site_db_clean_results_tenant_isolation' ) THEN CREATE POLICY site_db_clean_results_tenant_isolation ON site_db_clean_results USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'site_db_clean_results' AND policyname = 'site_db_clean_results_agent' ) THEN CREATE POLICY site_db_clean_results_agent ON site_db_clean_results USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (666.947666ms) -- migrating version 20260705000000 -> CREATE TABLE IF NOT EXISTS site_screenshots ( site_id uuid NOT NULL, tenant_id uuid NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, screenshot_key text NOT NULL DEFAULT '', screenshot_key_2x text NOT NULL DEFAULT '', width integer NOT NULL DEFAULT 0, height integer NOT NULL DEFAULT 0, status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','ready','failed')), failed_reason text, captured_at timestamptz, etag text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT site_screenshots_pkey PRIMARY KEY (site_id) ); -> CREATE INDEX IF NOT EXISTS site_screenshots_tenant_idx ON site_screenshots (tenant_id); -> ALTER TABLE site_screenshots ENABLE ROW LEVEL SECURITY; -> ALTER TABLE site_screenshots FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'site_screenshots' AND policyname = 'site_screenshots_tenant_isolation' ) THEN CREATE POLICY site_screenshots_tenant_isolation ON site_screenshots USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'site_screenshots' AND policyname = 'site_screenshots_agent' ) THEN CREATE POLICY site_screenshots_agent ON site_screenshots USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_screenshots' AND policyname = 'site_screenshots_site_scope' ) THEN CREATE POLICY "site_screenshots_site_scope" ON "public"."site_screenshots" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -- ok (724.596958ms) -- migrating version 20260706000000 -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "two_factor_enabled" bool NOT NULL DEFAULT false; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "totp_secret_encrypted" bytea; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "totp_confirmed_at" timestamptz; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "totp_last_step" bigint; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "totp_provisional_secret_encrypted" bytea; -> ALTER TABLE "public"."users" ADD COLUMN IF NOT EXISTS "totp_provisional_expires_at" timestamptz; -> CREATE TABLE IF NOT EXISTS "public"."user_recovery_codes" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "code_hash" text NOT NULL, "used_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "user_recovery_codes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "user_recovery_codes_user_idx" ON "public"."user_recovery_codes" ("user_id"); -> ALTER TABLE "public"."user_recovery_codes" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."user_recovery_codes" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'user_recovery_codes' AND policyname = 'user_recovery_codes_agent' ) THEN CREATE POLICY "user_recovery_codes_agent" ON "public"."user_recovery_codes" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END $$; -> CREATE TABLE IF NOT EXISTS "public"."webauthn_credentials" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "credential_id" bytea NOT NULL, "public_key" bytea NOT NULL, "attestation_type" text NOT NULL DEFAULT '', "aaguid" bytea NOT NULL DEFAULT ''::bytea, "sign_count" bigint NOT NULL DEFAULT 0, "transports" text[], "name" text NOT NULL DEFAULT '', "backup_eligible" bool NOT NULL DEFAULT false, "backup_state" bool NOT NULL DEFAULT false, "created_at" timestamptz NOT NULL DEFAULT now(), "last_used_at" timestamptz, PRIMARY KEY ("id"), UNIQUE ("credential_id"), CONSTRAINT "webauthn_credentials_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "webauthn_credentials_user_idx" ON "public"."webauthn_credentials" ("user_id"); -> ALTER TABLE "public"."webauthn_credentials" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."webauthn_credentials" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'webauthn_credentials' AND policyname = 'webauthn_credentials_agent' ) THEN CREATE POLICY "webauthn_credentials_agent" ON "public"."webauthn_credentials" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END $$; -> CREATE TABLE IF NOT EXISTS "public"."two_factor_challenges" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "challenge_nonce" text NOT NULL, "kind" text NOT NULL DEFAULT 'login', "webauthn_session" jsonb, "expires_at" timestamptz NOT NULL, "used_at" timestamptz, "attempts" integer NOT NULL DEFAULT 0, "requested_ip" inet, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "two_factor_challenges_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE UNIQUE INDEX IF NOT EXISTS "two_factor_challenges_nonce_key" ON "public"."two_factor_challenges" ("challenge_nonce") WHERE used_at IS NULL; -> CREATE INDEX IF NOT EXISTS "two_factor_challenges_user_active_idx" ON "public"."two_factor_challenges" ("user_id") WHERE used_at IS NULL; -> ALTER TABLE "public"."two_factor_challenges" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."two_factor_challenges" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'two_factor_challenges' AND policyname = 'two_factor_challenges_agent' ) THEN CREATE POLICY "two_factor_challenges_agent" ON "public"."two_factor_challenges" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END $$; -> CREATE TABLE IF NOT EXISTS "public"."webauthn_registration_sessions" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "session" jsonb NOT NULL, "expires_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "webauthn_registration_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "webauthn_registration_sessions_user_idx" ON "public"."webauthn_registration_sessions" ("user_id"); -> ALTER TABLE "public"."webauthn_registration_sessions" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."webauthn_registration_sessions" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'webauthn_registration_sessions' AND policyname = 'webauthn_registration_sessions_agent' ) THEN CREATE POLICY "webauthn_registration_sessions_agent" ON "public"."webauthn_registration_sessions" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END $$; -> CREATE TABLE IF NOT EXISTS "public"."trusted_devices" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NOT NULL, "token_hash" text NOT NULL, "label" text NOT NULL DEFAULT '', "user_agent" text NOT NULL DEFAULT '', "ip" inet, "created_at" timestamptz NOT NULL DEFAULT now(), "expires_at" timestamptz NOT NULL, "last_used_at" timestamptz, "revoked_at" timestamptz, PRIMARY KEY ("id"), UNIQUE ("token_hash"), CONSTRAINT "trusted_devices_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON DELETE CASCADE ); -> CREATE INDEX IF NOT EXISTS "trusted_devices_user_idx" ON "public"."trusted_devices" ("user_id"); -> ALTER TABLE "public"."trusted_devices" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."trusted_devices" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'trusted_devices' AND policyname = 'trusted_devices_agent' ) THEN CREATE POLICY "trusted_devices_agent" ON "public"."trusted_devices" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END $$; -- ok (2.43461875s) -- migrating version 20260707000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_error_config' AND column_name = 'enabled' ) THEN ALTER TABLE "public"."site_error_config" ADD COLUMN "enabled" boolean NOT NULL DEFAULT true; END IF; END; $$; -- ok (320.643459ms) -- migrating version 20260708000000 -> DO $$ BEGIN -- Mark older duplicates as failed so the unique index creation succeeds. -- The subquery picks the NEWEST in-flight snapshot per site; the UPDATE -- targets all other in-flight rows for the same site. UPDATE backup_snapshots SET status = 'failed', error = 'duplicate_in_flight_healed', finished_at = now(), updated_at = now() WHERE status IN ('pending', 'running') AND id NOT IN ( SELECT DISTINCT ON (site_id) id FROM backup_snapshots WHERE status IN ('pending', 'running') ORDER BY site_id, created_at DESC ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = 'backup_snapshots_one_inflight_per_site' AND c.relkind = 'i' ) THEN CREATE UNIQUE INDEX backup_snapshots_one_inflight_per_site ON backup_snapshots (site_id) WHERE status IN ('pending', 'running'); END IF; END; $$; -- ok (390.698208ms) -- migrating version 20260709000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_security_hardening_config" ( -- tenant / site keys "site_id" uuid PRIMARY KEY, "tenant_id" uuid NOT NULL, -- WordPress hardening toggles (all default OFF — opt-in, non-breaking) -- Adds DISALLOW_FILE_EDIT to wp-config so the built-in template/plugin -- editor is not available from the wp-admin dashboard. "disable_file_editor" boolean NOT NULL DEFAULT false, -- Three-state XML-RPC control: -- 'on' — XML-RPC is left as WordPress ships it (default). -- 'off' — all XML-RPC requests are rejected at the mu-plugin layer. -- 'limited' — XML-RPC is allowed but system.multicall is blocked. "xmlrpc_mode" text NOT NULL DEFAULT 'on' CONSTRAINT "site_security_hardening_config_xmlrpc_mode_chk" CHECK ("xmlrpc_mode" IN ('on', 'off', 'limited')), -- Two-state REST API gating: -- 'default' — REST API is left as WordPress ships it (default). -- 'restricted' — anonymous access to sensitive REST routes -- (users, comments listing) is blocked. "restrict_rest_api" text NOT NULL DEFAULT 'default' CONSTRAINT "site_security_hardening_config_restrict_rest_api_chk" CHECK ("restrict_rest_api" IN ('default', 'restricted')), -- Controls which credential type the WordPress login form accepts: -- 'username' — only usernames are accepted (WP default). -- 'email' — only email addresses are accepted. -- 'both' — either username or email is accepted. "restrict_login_identifier" text NOT NULL DEFAULT 'both' CONSTRAINT "site_security_hardening_config_login_id_chk" CHECK ("restrict_login_identifier" IN ('username', 'email', 'both')), -- Forces each user's display_name to differ from their user_login to -- prevent username enumeration via public author profiles. "force_unique_nickname" boolean NOT NULL DEFAULT false, -- Returns 404 for author archive URLs when the author has zero published -- posts, preventing username enumeration via /?author=N probing. "disable_author_archive_enum" boolean NOT NULL DEFAULT false, -- Adds FORCE_SSL_ADMIN to wp-config and ensures WordPress redirects -- the admin panel over HTTPS. "force_ssl" boolean NOT NULL DEFAULT false, -- Adds Options -Indexes to the .htaccess / nginx deny rule so the web -- server does not render directory listings. "disable_directory_browsing" boolean NOT NULL DEFAULT false, -- Adds per-directory rules (RewriteRule / deny from all) to block direct -- PHP execution from the uploads, plugins, and themes directories. "disable_php_in_uploads" boolean NOT NULL DEFAULT false, -- Adds deny rules for sensitive files: readme.html, readme.txt, -- wp-config.php, wp-admin/install.php, .git/, xmlrpc.php header deny. "protect_system_files" boolean NOT NULL DEFAULT false, -- Audit / actor fields "updated_at" timestamptz NOT NULL DEFAULT now(), "actor_type" text, "actor_id" text, CONSTRAINT "site_security_hardening_config_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_security_hardening_config_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_hardening_config' AND indexname = 'site_security_hardening_config_tenant_idx' ) THEN CREATE INDEX "site_security_hardening_config_tenant_idx" ON "public"."site_security_hardening_config" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_security_hardening_config" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_security_hardening_config" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_hardening_config' AND policyname = 'site_security_hardening_config_tenant_isolation' ) THEN CREATE POLICY "site_security_hardening_config_tenant_isolation" ON "public"."site_security_hardening_config" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_hardening_config' AND policyname = 'site_security_hardening_config_agent' ) THEN CREATE POLICY "site_security_hardening_config_agent" ON "public"."site_security_hardening_config" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_security_bans" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- type: the kind of ban entry. -- 'ip' — exact IPv4 or IPv6 address. -- 'range' — IPv4 or IPv6 CIDR block. -- 'user_agent' — user-agent string (exact match; agent may also support -- substring / glob matching in a later phase). "type" text NOT NULL CONSTRAINT "site_security_bans_type_chk" CHECK ("type" IN ('ip', 'range', 'user_agent')), -- value: the banned value. For type='ip' this is a dotted-decimal / -- RFC5952 address; for type='range' a valid CIDR; for type='user_agent' -- a plain string. Validated at write time in the service layer. "value" text NOT NULL, -- comment: optional operator note explaining why this ban was added. "comment" text NOT NULL DEFAULT '', -- actor tracking (operator or API key that created the ban). "actor_type" text NOT NULL DEFAULT '', "actor_id" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_security_bans_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_security_bans_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_bans' AND indexname = 'site_security_bans_unique_entry_idx' ) THEN CREATE UNIQUE INDEX "site_security_bans_unique_entry_idx" ON "public"."site_security_bans" ("site_id", "type", "value"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_bans' AND indexname = 'site_security_bans_tenant_site_idx' ) THEN CREATE INDEX "site_security_bans_tenant_site_idx" ON "public"."site_security_bans" ("tenant_id", "site_id", "created_at" DESC); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_security_bans" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_security_bans" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_bans' AND policyname = 'site_security_bans_tenant_isolation' ) THEN CREATE POLICY "site_security_bans_tenant_isolation" ON "public"."site_security_bans" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_bans' AND policyname = 'site_security_bans_agent' ) THEN CREATE POLICY "site_security_bans_agent" ON "public"."site_security_bans" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.030657625s) -- migrating version 20260710000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_file_baseline" ( -- Primary key: one row per (site, relative path). "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "path" text NOT NULL, -- site-relative, forward-slash -- Hash captured by the agent. "md5" text NOT NULL, "size" bigint NOT NULL DEFAULT 0, "mtime" bigint NOT NULL DEFAULT 0, "is_link" boolean NOT NULL DEFAULT false, -- Source classification persisted with the row so readers know which -- authority blessed this hash (informational; not used in diff logic). -- 'baseline' — promoted from a full/files scan run. -- 'wporg_core' — replaced by the known-good core checksum. -- 'wporg_plugin' — replaced by the known-good plugin/theme checksum. -- 'managed' — written by the self-managed-file registry. "source" text NOT NULL DEFAULT 'baseline' CONSTRAINT "site_file_baseline_source_chk" CHECK ("source" IN ('baseline', 'wporg_core', 'wporg_plugin', 'managed')), -- The scan run that last promoted / updated this row. "updated_run" uuid NOT NULL, "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_file_baseline_pkey" PRIMARY KEY ("site_id", "path"), CONSTRAINT "site_file_baseline_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_file_baseline_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_file_baseline' AND indexname = 'site_file_baseline_tenant_idx' ) THEN CREATE INDEX "site_file_baseline_tenant_idx" ON "public"."site_file_baseline" ("tenant_id", "site_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_file_baseline" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_file_baseline" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_file_baseline' AND policyname = 'site_file_baseline_tenant_isolation' ) THEN CREATE POLICY "site_file_baseline_tenant_isolation" ON "public"."site_file_baseline" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_file_baseline' AND policyname = 'site_file_baseline_agent' ) THEN CREATE POLICY "site_file_baseline_agent" ON "public"."site_file_baseline" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_managed_files" ( "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "path" text NOT NULL, "md5" text NOT NULL DEFAULT '', -- '' = suppress all findings -- managed_by identifies the WPMgr subsystem that owns this path: -- 'perf_cache' — page-cache rules files (.htaccess, advanced-cache.php) -- 'object_cache' — object-cache.php drop-in -- 'config_writer' — wp-config.php block additions -- 'hardening' — security-hardening file writes -- 'cp_command' — generic CP-pushed write via record_managed_files "managed_by" text NOT NULL DEFAULT 'cp_command', "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_managed_files_pkey" PRIMARY KEY ("site_id", "path"), CONSTRAINT "site_managed_files_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_managed_files_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_managed_files' AND indexname = 'site_managed_files_tenant_idx' ) THEN CREATE INDEX "site_managed_files_tenant_idx" ON "public"."site_managed_files" ("tenant_id", "site_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_managed_files" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_managed_files" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_managed_files' AND policyname = 'site_managed_files_tenant_isolation' ) THEN CREATE POLICY "site_managed_files_tenant_isolation" ON "public"."site_managed_files" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_managed_files' AND policyname = 'site_managed_files_agent' ) THEN CREATE POLICY "site_managed_files_agent" ON "public"."site_managed_files" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wporg_plugin_checksums" ( "kind" text NOT NULL CONSTRAINT "wporg_plugin_checksums_kind_chk" CHECK ("kind" IN ('plugin', 'theme')), "slug" text NOT NULL, "version" text NOT NULL, "path" text NOT NULL, -- plugin-relative path, e.g. "akismet.php" "md5" text NOT NULL, -- one accepted md5 variant (lowercase hex) "fetched_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "wporg_plugin_checksums_pkey" PRIMARY KEY ("kind", "slug", "version", "path", "md5") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'wporg_plugin_checksums' AND indexname = 'wporg_plugin_checksums_lookup_idx' ) THEN CREATE INDEX "wporg_plugin_checksums_lookup_idx" ON "public"."wporg_plugin_checksums" ("kind", "slug", "version", "path"); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wporg_plugin_checksums_meta" ( "kind" text NOT NULL CONSTRAINT "wporg_plugin_checksums_meta_kind_chk" CHECK ("kind" IN ('plugin', 'theme')), "slug" text NOT NULL, "version" text NOT NULL, "fetched_at" timestamptz NOT NULL DEFAULT now(), "ok" boolean NOT NULL DEFAULT true, CONSTRAINT "wporg_plugin_checksums_meta_pkey" PRIMARY KEY ("kind", "slug", "version") ); END; $$; -- ok (1.141891584s) -- migrating version 20260711000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_security_policy" ( -- tenant / site keys "site_id" uuid PRIMARY KEY, "tenant_id" uuid NOT NULL, -- --------------------------------------------------------------- -- Two-factor authentication policy knobs (all default OFF) -- --------------------------------------------------------------- -- Master switch for the site-user 2FA subsystem. When false, all 2FA -- enforcement is inert regardless of other knobs. "two_factor_enabled" boolean NOT NULL DEFAULT false, -- Allowed 2FA providers for this site. The agent enforces that a user -- may only enroll methods in this set. Default: all three methods allowed. "two_factor_methods" text[] NOT NULL DEFAULT '{totp,email,backup}', -- WP roles that must use 2FA. An empty array means 2FA is optional for -- all roles (the master switch still controls whether any prompting occurs). "two_factor_required_roles" text[] NOT NULL DEFAULT '{}', -- Number of allowed logins before a required-but-unenrolled user is -- forced into the enrollment onboarding interstitial. 0 = force immediately. "two_factor_grace_logins" int NOT NULL DEFAULT 3 CONSTRAINT "site_security_policy_grace_logins_chk" CHECK ("two_factor_grace_logins" >= 0 AND "two_factor_grace_logins" <= 100), -- Trusted-device TTL in days. 0 = remember-device feature is disabled. "two_factor_remember_device_days" int NOT NULL DEFAULT 30 CONSTRAINT "site_security_policy_remember_device_days_chk" CHECK ("two_factor_remember_device_days" >= 0 AND "two_factor_remember_device_days" <= 365), -- When true, reject password-based XML-RPC requests for any user who has -- 2FA configured. XML-RPC has no second-factor challenge channel. "block_xmlrpc_for_2fa_users" boolean NOT NULL DEFAULT true, -- --------------------------------------------------------------- -- Password policy knobs (all default OFF / 0) -- --------------------------------------------------------------- -- Minimum zxcvbn score required on password set / change / reset. -- 0 = disabled; 1-4 = score threshold (4 = very strong). "password_min_zxcvbn_score" int NOT NULL DEFAULT 0 CONSTRAINT "site_security_policy_zxcvbn_score_chk" CHECK ("password_min_zxcvbn_score" >= 0 AND "password_min_zxcvbn_score" <= 4), -- WP roles the strength rule applies to. Empty array = applies to all roles. "password_min_zxcvbn_roles" text[] NOT NULL DEFAULT '{}', -- When true, reject passwords whose SHA-1 5-char prefix appears in the -- HIBP Pwned Passwords corpus (checked via CP proxy, fail-open). "password_block_compromised" boolean NOT NULL DEFAULT false, -- Number of previous password hashes to retain for reuse detection. -- 0 = reuse blocking is disabled. "password_reuse_block_count" int NOT NULL DEFAULT 0 CONSTRAINT "site_security_policy_reuse_block_count_chk" CHECK ("password_reuse_block_count" >= 0 AND "password_reuse_block_count" <= 50), -- Force a password change after this many days since the last change. -- 0 = expiry is disabled. "password_max_age_days" int NOT NULL DEFAULT 0 CONSTRAINT "site_security_policy_max_age_days_chk" CHECK ("password_max_age_days" >= 0 AND "password_max_age_days" <= 3650), -- WP roles the expiry rule applies to. Empty array = applies to all roles. "password_expiry_roles" text[] NOT NULL DEFAULT '{}', -- --------------------------------------------------------------- -- Hide-backend (secret login slug) policy knobs -- --------------------------------------------------------------- -- Master switch for the secret login slug feature. "hide_backend_enabled" boolean NOT NULL DEFAULT false, -- Secret login slug (e.g. "my-login"). Validated ^[a-z0-9-]{4,64}$ in the -- service layer before storing. Empty string = no slug configured. "hide_backend_slug" text NOT NULL DEFAULT '', -- Where to redirect logged-out visitors who hit the canonical wp-login / -- wp-admin paths when hide_backend_enabled is true. Empty = 404. "hide_backend_redirect" text NOT NULL DEFAULT '', -- --------------------------------------------------------------- -- Audit fields -- --------------------------------------------------------------- "updated_at" timestamptz NOT NULL DEFAULT now(), "actor_type" text, "actor_id" text, CONSTRAINT "site_security_policy_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_security_policy_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_policy' AND indexname = 'site_security_policy_tenant_idx' ) THEN CREATE INDEX "site_security_policy_tenant_idx" ON "public"."site_security_policy" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_security_policy" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_security_policy" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy' AND policyname = 'site_security_policy_tenant_isolation' ) THEN CREATE POLICY "site_security_policy_tenant_isolation" ON "public"."site_security_policy" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy' AND policyname = 'site_security_policy_agent' ) THEN CREATE POLICY "site_security_policy_agent" ON "public"."site_security_policy" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_security_policy_groups" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- WP role slug this group override applies to (e.g. "administrator"). "role" text NOT NULL, -- Nullable override columns. NULL means "inherit from site-level policy". "require_2fa" boolean, "allowed_methods" text[], "min_zxcvbn_score" int CONSTRAINT "site_security_policy_groups_zxcvbn_score_chk" CHECK ("min_zxcvbn_score" IS NULL OR ("min_zxcvbn_score" >= 0 AND "min_zxcvbn_score" <= 4)), "block_compromised" boolean, "max_age_days" int CONSTRAINT "site_security_policy_groups_max_age_days_chk" CHECK ("max_age_days" IS NULL OR ("max_age_days" >= 0 AND "max_age_days" <= 3650)), "created_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_security_policy_groups_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_security_policy_groups_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_policy_groups' AND indexname = 'site_security_policy_groups_site_role_idx' ) THEN CREATE UNIQUE INDEX "site_security_policy_groups_site_role_idx" ON "public"."site_security_policy_groups" ("site_id", "role"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_security_policy_groups' AND indexname = 'site_security_policy_groups_tenant_site_idx' ) THEN CREATE INDEX "site_security_policy_groups_tenant_site_idx" ON "public"."site_security_policy_groups" ("tenant_id", "site_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_security_policy_groups" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_security_policy_groups" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy_groups' AND policyname = 'site_security_policy_groups_tenant_isolation' ) THEN CREATE POLICY "site_security_policy_groups_tenant_isolation" ON "public"."site_security_policy_groups" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy_groups' AND policyname = 'site_security_policy_groups_agent' ) THEN CREATE POLICY "site_security_policy_groups_agent" ON "public"."site_security_policy_groups" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."hibp_breach_cache" ( "prefix" char(5) PRIMARY KEY, "body" text NOT NULL, "fetched_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'hibp_breach_cache' AND indexname = 'hibp_breach_cache_fetched_at_idx' ) THEN CREATE INDEX "hibp_breach_cache_fetched_at_idx" ON "public"."hibp_breach_cache" ("fetched_at"); END IF; END; $$; -- ok (1.178066375s) -- migrating version 20260712000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wordfence_vuln_feed" ( "vuln_id" text PRIMARY KEY, -- Wordfence UUID (root key) "title" text NOT NULL DEFAULT '', "cve" text, -- nullable; Production-only "cve_link" text, "cvss_score" numeric(3,1), -- nullable; Production-only "cvss_rating" text, -- None/Low/Medium/High/Critical "cwe" jsonb, -- {id,name,description} nullable "informational" boolean NOT NULL DEFAULT false, "references" jsonb NOT NULL DEFAULT '[]', -- attribution link-back array "published" timestamptz, "updated" timestamptz, "raw" jsonb NOT NULL DEFAULT '{}', -- full record inc. copyrights "created_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wordfence_vuln_software" ( "vuln_id" text NOT NULL REFERENCES "public"."wordfence_vuln_feed" ("vuln_id") ON DELETE CASCADE, "kind" text NOT NULL, -- 'core' | 'plugin' | 'theme' "slug" text NOT NULL, "affected_versions" jsonb NOT NULL, -- array of range objects (range test input) "patched" boolean NOT NULL DEFAULT false, "patched_versions" jsonb NOT NULL DEFAULT '[]', CONSTRAINT "wordfence_vuln_software_pkey" PRIMARY KEY ("vuln_id", "kind", "slug") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'wordfence_vuln_software' AND indexname = 'idx_wf_vuln_software_lookup' ) THEN CREATE INDEX "idx_wf_vuln_software_lookup" ON "public"."wordfence_vuln_software" ("kind", "slug"); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."wordfence_vuln_feed_meta" ( "id" integer PRIMARY KEY DEFAULT 1 CONSTRAINT "wordfence_vuln_feed_meta_singleton_chk" CHECK ("id" = 1), "fetched_at" timestamptz, "ok" boolean NOT NULL DEFAULT false, "record_count" integer NOT NULL DEFAULT 0, "defiant_notice" text, -- copyrights.defiant.notice (display in UI footer) "defiant_license" text, -- copyrights.defiant.license (full text stored once) "mitre_notice" text, -- copyrights.mitre.notice (display on CVE rows) "last_error" text ); -- Ensure the sentinel row exists so the ingester can UPDATE rather than -- INSERT-or-UPDATE (simpler code, avoids a race on first run). INSERT INTO "public"."wordfence_vuln_feed_meta" ("id") VALUES (1) ON CONFLICT ("id") DO NOTHING; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_vulnerabilities" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, -- Feed reference (soft; no FK so feed prune does not cascade to findings). "vuln_id" text NOT NULL, "kind" text NOT NULL, -- 'core' | 'plugin' | 'theme' "slug" text NOT NULL, "name" text NOT NULL, -- human-readable component name -- Snapshot of installed state at detection time. "installed_version" text NOT NULL, -- Remediation target derived from patched_versions / range upper bound. -- Null when no patched version is known (unpatched 0-day). "fixed_version" text, -- Severity bucket: critical | high | medium | low. -- Derived from cvss_score or cvss_rating; stored for fast severity-sorted -- list queries and the fleet rollup count. "severity" text NOT NULL CONSTRAINT "site_vulnerabilities_severity_chk" CHECK ("severity" IN ('critical', 'high', 'medium', 'low')), "cvss_score" numeric(3,1), "cve" text, "title" text NOT NULL, -- Lifecycle status. "status" text NOT NULL DEFAULT 'open' CONSTRAINT "site_vulnerabilities_status_chk" CHECK ("status" IN ('open', 'dismissed', 'resolved')), "first_seen" timestamptz NOT NULL DEFAULT now(), "last_seen" timestamptz NOT NULL DEFAULT now(), "resolved_at" timestamptz, "dismissed_at" timestamptz, "dismissed_by" uuid, -- user ID who dismissed; null for system dismiss CONSTRAINT "site_vulnerabilities_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, -- Unique finding per (site, vuln, component) — a site can only have one -- active finding per (vuln_id, kind, slug) combination. CONSTRAINT "site_vulnerabilities_uq" UNIQUE ("site_id", "vuln_id", "kind", "slug") ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND indexname = 'idx_site_vuln_site_open' ) THEN CREATE INDEX "idx_site_vuln_site_open" ON "public"."site_vulnerabilities" ("site_id") WHERE "status" = 'open'; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND indexname = 'idx_site_vuln_tenant_sev' ) THEN CREATE INDEX "idx_site_vuln_tenant_sev" ON "public"."site_vulnerabilities" ("tenant_id", "severity") WHERE "status" = 'open'; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND indexname = 'site_vulnerabilities_tenant_idx' ) THEN CREATE INDEX "site_vulnerabilities_tenant_idx" ON "public"."site_vulnerabilities" ("tenant_id", "site_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_vulnerabilities" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_vulnerabilities" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND policyname = 'site_vulnerabilities_tenant_isolation' ) THEN CREATE POLICY "site_vulnerabilities_tenant_isolation" ON "public"."site_vulnerabilities" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND policyname = 'site_vulnerabilities_agent' ) THEN CREATE POLICY "site_vulnerabilities_agent" ON "public"."site_vulnerabilities" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.022742167s) -- migrating version 20260713000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."instance_settings" ( "key" text PRIMARY KEY, "value_enc" bytea, -- age-encrypted ciphertext; NULL = unset "updated_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> DO $$ BEGIN ALTER TABLE "public"."instance_settings" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."instance_settings" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'instance_settings' AND policyname = 'instance_settings_agent' ) THEN CREATE POLICY "instance_settings_agent" ON "public"."instance_settings" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (471.00525ms) -- migrating version 20260714000000 -> DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'wordfence_vuln_feed' AND column_name = 'references' ) THEN ALTER TABLE "public"."wordfence_vuln_feed" RENAME COLUMN "references" TO "reference_urls"; END IF; END; $$; -- ok (321.137208ms) -- migrating version 20260715000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_file_manager" ( -- tenant / site keys "site_id" uuid PRIMARY KEY, "tenant_id" uuid NOT NULL, -- Per-site opt-in flag. Default OFF — the feature must be explicitly -- enabled by an owner or admin before any file_* command is signed. "files_enabled" boolean NOT NULL DEFAULT false, -- Optional root-jail override. Empty string means the agent uses its -- own configured default (ABSPATH or the narrow configured subtree). -- When non-empty, the CP passes this to the agent on every signed -- command so the agent can narrow the accessible tree further. "root_jail" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "site_file_manager_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_file_manager_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_file_manager' AND indexname = 'site_file_manager_tenant_idx' ) THEN CREATE INDEX "site_file_manager_tenant_idx" ON "public"."site_file_manager" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_file_manager" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_file_manager" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_file_manager' AND policyname = 'site_file_manager_tenant_isolation' ) THEN CREATE POLICY "site_file_manager_tenant_isolation" ON "public"."site_file_manager" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_file_manager' AND policyname = 'site_file_manager_agent' ) THEN CREATE POLICY "site_file_manager_agent" ON "public"."site_file_manager" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."file_transfers" ( -- identity "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- direction: 'download' (P1) or 'upload' (P2, not yet implemented) "direction" text NOT NULL CONSTRAINT "file_transfers_direction_chk" CHECK ("direction" IN ('download', 'upload')), -- rel_path: the site-relative path that was staged for transfer. "rel_path" text NOT NULL, -- status lifecycle: staged → active → done | failed "status" text NOT NULL DEFAULT 'done' CONSTRAINT "file_transfers_status_chk" CHECK ("status" IN ('staged', 'active', 'done', 'failed')), -- object_key: tenant-namespaced S3 staging key prefix, -- e.g. file-transfers//. "object_key" text NOT NULL DEFAULT '', -- size / chunks: bytes staged and number of S3 parts. "size_bytes" bigint NOT NULL DEFAULT 0, "chunk_count" integer NOT NULL DEFAULT 0, -- created_by: the user who initiated the transfer (uuid.Nil for system). "created_by" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', "created_at" timestamptz NOT NULL DEFAULT now(), -- expires_at: after this time the staged object may be GC'd. -- The CP should clean up short-lived staging objects after TTL (≤5 min). "expires_at" timestamptz NOT NULL, CONSTRAINT "file_transfers_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "file_transfers_site_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'file_transfers' AND indexname = 'file_transfers_tenant_site_idx' ) THEN CREATE INDEX "file_transfers_tenant_site_idx" ON "public"."file_transfers" ("tenant_id", "site_id", "created_at" DESC, "id" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'file_transfers' AND indexname = 'file_transfers_expires_at_idx' ) THEN CREATE INDEX "file_transfers_expires_at_idx" ON "public"."file_transfers" ("expires_at") WHERE "status" IN ('staged', 'done'); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."file_transfers" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."file_transfers" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'file_transfers' AND policyname = 'file_transfers_tenant_isolation' ) THEN CREATE POLICY "file_transfers_tenant_isolation" ON "public"."file_transfers" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'file_transfers' AND policyname = 'file_transfers_agent' ) THEN CREATE POLICY "file_transfers_agent" ON "public"."file_transfers" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (1.00751575s) -- migrating version 20260716000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_file_manager' AND column_name = 'files_write_enabled' ) THEN ALTER TABLE "public"."site_file_manager" ADD COLUMN "files_write_enabled" boolean NOT NULL DEFAULT false; END IF; END; $$; -- ok (323.209666ms) -- migrating version 20260717000000 -> DO $$ BEGIN -- Drop the old SELECT-only policy if it exists. IF EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_schedules' AND policyname = 'backup_schedules_scheduler' ) THEN DROP POLICY backup_schedules_scheduler ON backup_schedules; END IF; -- Re-create as FOR ALL so that SELECT … FOR UPDATE (and the subsequent -- UPDATE) in ClaimAndAdvanceDueSchedules work under InAgentTx. CREATE POLICY backup_schedules_scheduler ON backup_schedules FOR ALL USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END; $$; -- ok (322.143583ms) -- migrating version 20260718000000 -> DO $$ BEGIN -- Drop the narrower index that is a subset of the new covering index. -- IF EXISTS guards against re-run on an already-migrated DB. DROP INDEX IF EXISTS site_uptime_probes_site_time_idx; END; $$; -> CREATE INDEX IF NOT EXISTS site_uptime_probes_agg_idx ON site_uptime_probes (site_id, tenant_id, probed_at DESC) INCLUDE (up, total_ms); -- ok (383.053416ms) -- migrating version 20260719000000 -> CREATE INDEX IF NOT EXISTS site_uptime_probes_probed_at_idx ON site_uptime_probes (probed_at); -- ok (318.516125ms) -- migrating version 20260720000000 -> ALTER TABLE "public"."email_notify_settings" DROP CONSTRAINT IF EXISTS "email_notify_settings_digest_cadence"; -> ALTER TABLE "public"."email_notify_settings" ADD CONSTRAINT "email_notify_settings_digest_cadence" CHECK (digest_cadence IN ('daily', 'weekly', 'monthly')); -- ok (380.183167ms) -- migrating version 20260721000000 -> DO $$ BEGIN UPDATE update_tasks u SET status = 'skipped', finished_at = now(), updated_at = now(), detail = 'skipped: superseded by a newer duplicate in-flight task for the same target (m88 dedup)' WHERE u.status IN ('pending', 'running') AND EXISTS ( SELECT 1 FROM update_tasks v WHERE v.tenant_id = u.tenant_id AND v.site_id = u.site_id AND v.target_type = u.target_type AND v.target_slug = u.target_slug AND v.status IN ('pending', 'running') AND (v.created_at, v.id) > (u.created_at, u.id) ); END $$; -> CREATE UNIQUE INDEX IF NOT EXISTS update_tasks_inflight_target_idx ON update_tasks (tenant_id, site_id, target_type, target_slug) WHERE status IN ('pending', 'running'); -- ok (381.272584ms) -- migrating version 20260722000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'update_tasks' AND policyname = 'update_tasks_agent' ) THEN CREATE POLICY update_tasks_agent ON update_tasks USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (320.920042ms) -- migrating version 20260723000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."audit_integrity_baseline" ( "tenant_id" uuid PRIMARY KEY, "baseline_created_at" timestamptz NOT NULL, "baseline_id" uuid NOT NULL, "baseline_hash" text NOT NULL, "set_by" uuid, "set_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "audit_integrity_baseline_tenant_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "audit_integrity_baseline_set_by_fkey" FOREIGN KEY ("set_by") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL ); END; $$; -> DO $$ BEGIN ALTER TABLE "public"."audit_integrity_baseline" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."audit_integrity_baseline" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'audit_integrity_baseline' AND policyname = 'audit_integrity_baseline_tenant_isolation' ) THEN CREATE POLICY "audit_integrity_baseline_tenant_isolation" ON "public"."audit_integrity_baseline" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -- ok (469.239583ms) -- migrating version 20260724000000 -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "plan" text NOT NULL DEFAULT 'free'; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "plan_status" text NOT NULL DEFAULT 'none'; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "plan_overrides" jsonb NOT NULL DEFAULT '{}'::jsonb; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "grace_until" timestamptz; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "billing_provider" text; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "provider_customer_id" text; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "provider_subscription_id" text; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "current_period_end" timestamptz; -> ALTER TABLE "public"."tenants" DROP CONSTRAINT IF EXISTS "tenants_plan_check"; -> ALTER TABLE "public"."tenants" ADD CONSTRAINT "tenants_plan_check" CHECK (plan IN ('free', 'starter', 'agency', 'scale')); -> ALTER TABLE "public"."tenants" DROP CONSTRAINT IF EXISTS "tenants_plan_status_check"; -> ALTER TABLE "public"."tenants" ADD CONSTRAINT "tenants_plan_status_check" CHECK (plan_status IN ('none', 'trialing', 'active', 'past_due', 'canceled', 'paused', 'comped')); -> CREATE TABLE IF NOT EXISTS "public"."billing_events" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "provider" text NOT NULL, "provider_event_id" text NOT NULL, "kind" text NOT NULL, "tenant_id" uuid REFERENCES "public"."tenants" ("id") ON DELETE CASCADE, "payload" jsonb NOT NULL DEFAULT '{}'::jsonb, "occurred_at" timestamptz NOT NULL, "processed_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now() ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'billing_events_provider_event_key' ) THEN CREATE UNIQUE INDEX "billing_events_provider_event_key" ON "public"."billing_events" ("provider", "provider_event_id"); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'billing_events_tenant_id_idx' ) THEN CREATE INDEX "billing_events_tenant_id_idx" ON "public"."billing_events" ("tenant_id"); END IF; END; $$; -> ALTER TABLE "public"."billing_events" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."billing_events" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'billing_events' AND policyname = 'billing_events_tenant_isolation' ) THEN CREATE POLICY "billing_events_tenant_isolation" ON "public"."billing_events" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'billing_events' AND policyname = 'billing_events_system' ) THEN CREATE POLICY "billing_events_system" ON "public"."billing_events" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> CREATE OR REPLACE FUNCTION billing_count_active_sites(p_tenant uuid) RETURNS bigint LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_count bigint; v_prev text := current_setting('app.agent', true); BEGIN PERFORM set_config('app.agent', 'on', true); SELECT count(*) INTO v_count FROM sites WHERE tenant_id = p_tenant AND connection_state <> 'archived'; PERFORM set_config('app.agent', coalesce(v_prev, ''), true); RETURN v_count; END; $$; -> REVOKE ALL ON FUNCTION billing_count_active_sites(uuid) FROM PUBLIC; -> GRANT EXECUTE ON FUNCTION billing_count_active_sites(uuid) TO wpmgr_app; -> CREATE OR REPLACE FUNCTION "public"."admin_delete_empty_tenant"(p_tenant_id uuid) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_count integer; v_result boolean := false; v_prev_agent text := current_setting('app.agent', true); BEGIN PERFORM set_config('app.agent', 'on', true); IF NOT ( EXISTS (SELECT 1 FROM memberships m WHERE m.tenant_id = p_tenant_id) OR EXISTS (SELECT 1 FROM sites s WHERE s.tenant_id = p_tenant_id) ) THEN PERFORM set_config('app.tenant_id', p_tenant_id::text, true); DELETE FROM audit_log WHERE tenant_id = p_tenant_id; PERFORM set_config('app.tenant_id', '', true); DELETE FROM tenants t WHERE t.id = p_tenant_id; GET DIAGNOSTICS v_count = ROW_COUNT; v_result := v_count > 0; END IF; PERFORM set_config('app.agent', coalesce(v_prev_agent, ''), true); RETURN v_result; END; $$; -> REVOKE ALL ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) FROM PUBLIC; -> GRANT EXECUTE ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) TO "wpmgr_app"; -> UPDATE "public"."tenants" t SET "plan_overrides" = jsonb_set(t.plan_overrides, '{max_sites}', to_jsonb(cnt.active_count), true) FROM ( SELECT tenant_id, count(*) AS active_count FROM sites WHERE connection_state <> 'archived' GROUP BY tenant_id ) cnt WHERE t.id = cnt.tenant_id AND cnt.active_count > 3 AND NOT (t.plan_overrides ? 'max_sites'); -- ok (1.948588542s) -- migrating version 20260725000000 -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "comp_reason" text; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "suspended_at" timestamptz; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "suspended_reason" text; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "cancel_at_period_end" boolean NOT NULL DEFAULT false; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'backup_chunks' AND policyname = 'backup_chunks_agent' ) THEN CREATE POLICY "backup_chunks_agent" ON "public"."backup_chunks" FOR SELECT USING (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'audit_log' AND policyname = 'audit_log_agent' ) THEN CREATE POLICY "audit_log_agent" ON "public"."audit_log" FOR SELECT USING (current_setting('app.agent', true) = 'on'); END IF; END; $$; -- ok (692.711875ms) -- migrating version 20260726000000 -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "deleted_at" timestamptz; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "purge_started_at" timestamptz; -> CREATE TABLE IF NOT EXISTS "public"."system_audit_log" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "occurred_at" timestamptz NOT NULL DEFAULT now(), "actor_type" text NOT NULL, "actor_id" uuid, "action" text NOT NULL, "tenant_id" uuid NOT NULL, "tenant_name" text NOT NULL, "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'system_audit_log_tenant_id_idx' ) THEN CREATE INDEX "system_audit_log_tenant_id_idx" ON "public"."system_audit_log" ("tenant_id", "occurred_at"); END IF; END; $$; -> CREATE OR REPLACE FUNCTION "public"."admin_purge_tenant"(p_tenant_id uuid) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_count integer; v_prev_tenant text := current_setting('app.tenant_id', true); BEGIN -- Set for the WHOLE function body (see the top-of-file rationale): every -- tenant-scoped table's tenant_isolation policy must see p_tenant_id for -- the cascade the final DELETE triggers. PERFORM set_config('app.tenant_id', p_tenant_id::text, true); -- audit_log is insert-only for wpmgr_app (m1 revokes UPDATE/DELETE/ -- TRUNCATE), so it is cleared explicitly here, as the function OWNER -- (which retains DELETE) — mirrors admin_delete_empty_tenant exactly. DELETE FROM audit_log WHERE tenant_id = p_tenant_id; -- Every other row the tenant owns cascades from this single statement. DELETE FROM tenants t WHERE t.id = p_tenant_id; GET DIAGNOSTICS v_count = ROW_COUNT; -- Single return path: restore the caller's prior app.tenant_id exactly -- once (M91 Finding A GUC-leak lesson). PERFORM set_config('app.tenant_id', coalesce(v_prev_tenant, ''), true); RETURN v_count > 0; END; $$; -> REVOKE ALL ON FUNCTION "public"."admin_purge_tenant"(uuid) FROM PUBLIC; -> GRANT EXECUTE ON FUNCTION "public"."admin_purge_tenant"(uuid) TO "wpmgr_app"; -- ok (755.705792ms) -- migrating version 20260727000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_incidents" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "started_at" timestamptz NOT NULL DEFAULT now(), "ended_at" timestamptz, "peak_status" text NOT NULL DEFAULT 'down', "last_http_status" integer NOT NULL DEFAULT 0, "probe_count" integer NOT NULL DEFAULT 0, "down_count" integer NOT NULL DEFAULT 0, "opened_by" text NOT NULL DEFAULT 'probe', "reason" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "site_incidents_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_incidents_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_incidents' AND indexname = 'site_incidents_site_started_idx' ) THEN CREATE INDEX "site_incidents_site_started_idx" ON "public"."site_incidents" ("site_id", "started_at" DESC); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_incidents' AND indexname = 'site_incidents_tenant_started_idx' ) THEN CREATE INDEX "site_incidents_tenant_started_idx" ON "public"."site_incidents" ("tenant_id", "started_at" DESC); END IF; END; $$; -> CREATE UNIQUE INDEX IF NOT EXISTS "site_incidents_one_open_per_site" ON "public"."site_incidents" ("site_id") WHERE "ended_at" IS NULL; -> DO $$ BEGIN ALTER TABLE "public"."site_incidents" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_incidents" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_incidents' AND policyname = 'site_incidents_tenant_isolation' ) THEN CREATE POLICY "site_incidents_tenant_isolation" ON "public"."site_incidents" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_incidents' AND policyname = 'site_incidents_agent' ) THEN CREATE POLICY "site_incidents_agent" ON "public"."site_incidents" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_incidents' AND policyname = 'site_incidents_site_scope' ) THEN CREATE POLICY "site_incidents_site_scope" ON "public"."site_incidents" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> INSERT INTO "public"."site_incidents" ("tenant_id", "site_id", "started_at", "ended_at", "peak_status", "reason", "opened_by") SELECT "tenant_id", "site_id", COALESCE("last_alert_at", now()), NULL, 'down', '', 'seed' FROM "public"."site_alert_state" WHERE "in_incident" = true ON CONFLICT ("site_id") WHERE "ended_at" IS NULL DO NOTHING; -- ok (886.715333ms) -- migrating version 20260728000000 -> UPDATE "public"."tenants" SET "plan_overrides" = jsonb_set(plan_overrides, '{managed_backup_storage}', 'true'::jsonb, true) WHERE NOT (plan_overrides ? 'managed_backup_storage'); -- ok (321.137375ms) -- migrating version 20260729000000 -> DO $$ BEGIN UPDATE backup_snapshots losers SET status = 'failed', error = 'superseded duplicate completed snapshot at the same chain generation (m96 dedup)', finished_at = COALESCE(finished_at, now()), updated_at = now() WHERE losers.status = 'completed' AND losers.chain_id IS NOT NULL AND EXISTS ( SELECT 1 FROM backup_snapshots winners WHERE winners.chain_id = losers.chain_id AND winners.generation = losers.generation AND winners.status = 'completed' AND winners.id < losers.id ); END $$; -> CREATE UNIQUE INDEX IF NOT EXISTS backup_snapshots_chain_gen_completed_uidx ON backup_snapshots (chain_id, generation) WHERE status = 'completed'; -> CREATE INDEX IF NOT EXISTS backup_manifest_entries_chunk_hashes_gin ON backup_manifest_entries USING gin (chunk_hashes); -- ok (500.208458ms) -- migrating version 20260730000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'beacon_key_acked_present' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "beacon_key_acked_present" boolean NOT NULL DEFAULT false; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'site_perf_config' AND column_name = 'beacon_key_acked_at' ) THEN ALTER TABLE "public"."site_perf_config" ADD COLUMN "beacon_key_acked_at" timestamptz; END IF; END; $$; -- ok (440.748042ms) -- migrating version 20260731000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'email_verification_tokens' AND column_name = 'desired_plan' ) THEN ALTER TABLE "public"."email_verification_tokens" ADD COLUMN "desired_plan" text; END IF; END; $$; -- ok (319.183041ms) -- migrating version 20260801000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_uptime_daily" ( "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, "day" date NOT NULL, "up_checks" integer NOT NULL DEFAULT 0, "total_checks" integer NOT NULL DEFAULT 0, "sum_latency_ms" double precision NOT NULL DEFAULT 0, "latency_samples" integer NOT NULL DEFAULT 0, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id", "day"), CONSTRAINT "site_uptime_daily_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_uptime_daily_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_uptime_daily' AND indexname = 'site_uptime_daily_tenant_idx' ) THEN CREATE INDEX "site_uptime_daily_tenant_idx" ON "public"."site_uptime_daily" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_uptime_daily" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_uptime_daily" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_daily' AND policyname = 'site_uptime_daily_tenant_isolation' ) THEN CREATE POLICY "site_uptime_daily_tenant_isolation" ON "public"."site_uptime_daily" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_daily' AND policyname = 'site_uptime_daily_agent' ) THEN CREATE POLICY "site_uptime_daily_agent" ON "public"."site_uptime_daily" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_daily' AND policyname = 'site_uptime_daily_site_scope' ) THEN CREATE POLICY "site_uptime_daily_site_scope" ON "public"."site_uptime_daily" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_uptime_status" ( "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "latest_up" boolean NOT NULL, "last_probed_at" timestamptz NOT NULL, "tls_expiry" timestamptz, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_uptime_status_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_uptime_status_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_uptime_status' AND indexname = 'site_uptime_status_tenant_idx' ) THEN CREATE INDEX "site_uptime_status_tenant_idx" ON "public"."site_uptime_status" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_uptime_status" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_uptime_status" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_status' AND policyname = 'site_uptime_status_tenant_isolation' ) THEN CREATE POLICY "site_uptime_status_tenant_isolation" ON "public"."site_uptime_status" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_status' AND policyname = 'site_uptime_status_agent' ) THEN CREATE POLICY "site_uptime_status_agent" ON "public"."site_uptime_status" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_uptime_status' AND policyname = 'site_uptime_status_site_scope' ) THEN CREATE POLICY "site_uptime_status_site_scope" ON "public"."site_uptime_status" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> INSERT INTO "public"."site_uptime_daily" ("tenant_id", "site_id", "day", "up_checks", "total_checks", "sum_latency_ms", "latency_samples") SELECT "tenant_id", "site_id", ("probed_at" AT TIME ZONE 'UTC')::date AS "day", count(*) FILTER (WHERE "up") AS "up_checks", count(*) AS "total_checks", coalesce(sum("total_ms") FILTER (WHERE "up" AND "total_ms" <> 0), 0) AS "sum_latency_ms", count(*) FILTER (WHERE "up" AND "total_ms" <> 0) AS "latency_samples" FROM "public"."site_uptime_probes" GROUP BY "tenant_id", "site_id", (("probed_at" AT TIME ZONE 'UTC')::date) ON CONFLICT ("site_id", "day") DO NOTHING; -> INSERT INTO "public"."site_uptime_status" ("site_id", "tenant_id", "latest_up", "last_probed_at", "tls_expiry") SELECT DISTINCT ON ("site_id") "site_id", "tenant_id", "up", "probed_at", "tls_expiry" FROM "public"."site_uptime_probes" ORDER BY "site_id", "probed_at" DESC ON CONFLICT ("site_id") DO NOTHING; -- ok (1.261642375s) -- migrating version 20260802000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_tags" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "name" text NOT NULL, -- '' = auto (client picks a deterministic color from the name); else a -- lowercase '#rrggbb' hex code (app-layer normalizes to lowercase; the -- CHECK below is case-insensitive so it never rejects a pre-normalized -- value written outside the app, e.g. during a future data fix). "color" text NOT NULL DEFAULT '', "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id"), CONSTRAINT "site_tags_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, -- Exact-case unique per tenant (site.normalizeTags/ANY(tags) semantics). CONSTRAINT "site_tags_tenant_name_key" UNIQUE ("tenant_id", "name"), -- Backs a future composite FK the same way clients_id_tenant_key does; -- not referenced by any FK today (sites.tags has no join table). CONSTRAINT "site_tags_id_tenant_key" UNIQUE ("id", "tenant_id"), CONSTRAINT "site_tags_name_nonempty" CHECK (btrim("name") != '' AND char_length("name") <= 64), CONSTRAINT "site_tags_color_format" CHECK ("color" = '' OR "color" ~* '^#[0-9a-f]{6}$') ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_tags' AND indexname = 'site_tags_tenant_idx' ) THEN CREATE INDEX "site_tags_tenant_idx" ON "public"."site_tags" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_tags" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_tags" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_tags' AND policyname = 'site_tags_tenant_isolation' ) THEN CREATE POLICY "site_tags_tenant_isolation" ON "public"."site_tags" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_tags' AND policyname = 'site_tags_agent' ) THEN CREATE POLICY "site_tags_agent" ON "public"."site_tags" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."sites" DISABLE ROW LEVEL SECURITY; ALTER TABLE "public"."pairing_codes" DISABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_tags" DISABLE ROW LEVEL SECURITY; -- Tags currently assigned to a site. char_length(t.tag) <= 64 is -- LOAD-BEARING, not cosmetic: two live write paths (site-first -- MintEnrollmentCode and the legacy POST /enroll) never enforced the -- 64-char cap before this release (see internal/site validation -- follow-up), so a real prod site can carry an over-length tag today. An -- unguarded INSERT here would violate site_tags_name_nonempty's -- char_length<=64 CHECK and abort this migration's single boot -- transaction (crash-loop on deploy). An over-length tag simply stays -- OFF the registry — it remains on sites.tags untouched (still renders -- as a chip via name-derived auto color, still filterable via -- ?tags=/?tags_match=) and is harmless to leave unregistered. INSERT INTO "public"."site_tags" ("tenant_id", "name") SELECT DISTINCT s."tenant_id", t."tag" FROM "public"."sites" s CROSS JOIN LATERAL unnest(s."tags") AS t("tag") WHERE btrim(t."tag") != '' AND char_length(t."tag") <= 64 ON CONFLICT ("tenant_id", "name") DO NOTHING; -- Tags sitting on an unexpired, unredeemed pairing code (about to become -- a site's tags once the code is consumed). Same over-length guard as -- above and for the same reason (CreatePairingCodeInput DID validate -- max=64 already, but a code minted via the site-first flow could still -- carry a long tag inherited from the unvalidated MintEnrollmentInput -- path — see the follow-up fix). INSERT INTO "public"."site_tags" ("tenant_id", "name") SELECT DISTINCT pc."tenant_id", t."tag" FROM "public"."pairing_codes" pc CROSS JOIN LATERAL unnest(pc."tags") AS t("tag") WHERE btrim(t."tag") != '' AND char_length(t."tag") <= 64 AND pc."consumed_at" IS NULL AND pc."expires_at" > now() ON CONFLICT ("tenant_id", "name") DO NOTHING; ALTER TABLE "public"."site_tags" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_tags" FORCE ROW LEVEL SECURITY; ALTER TABLE "public"."pairing_codes" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."pairing_codes" FORCE ROW LEVEL SECURITY; ALTER TABLE "public"."sites" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."sites" FORCE ROW LEVEL SECURITY; END; $$; -- ok (677.057375ms) -- migrating version 20260803000000 -> ALTER TABLE "public"."wordfence_vuln_feed_meta" ADD COLUMN IF NOT EXISTS "enrichment_ok" boolean NOT NULL DEFAULT false; -> ALTER TABLE "public"."wordfence_vuln_feed_meta" ADD COLUMN IF NOT EXISTS "last_enrichment_at" timestamptz; -> ALTER TABLE "public"."wordfence_vuln_feed_meta" ADD COLUMN IF NOT EXISTS "next_feed_kind" text NOT NULL DEFAULT 'scanner'; -> ALTER TABLE "public"."wordfence_vuln_feed_meta" DROP CONSTRAINT IF EXISTS "wordfence_vuln_feed_meta_next_feed_chk"; -> ALTER TABLE "public"."wordfence_vuln_feed_meta" ADD CONSTRAINT "wordfence_vuln_feed_meta_next_feed_chk" CHECK ("next_feed_kind" IN ('scanner', 'production')); -> ALTER TABLE "public"."site_vulnerabilities" DROP CONSTRAINT IF EXISTS "site_vulnerabilities_severity_chk"; -> ALTER TABLE "public"."site_vulnerabilities" ADD CONSTRAINT "site_vulnerabilities_severity_chk" CHECK ("severity" IN ('critical', 'high', 'medium', 'low', 'unknown')); -- ok (723.285917ms) -- migrating version 20260804000000 -> ALTER TABLE "public"."wordfence_vuln_feed_meta" ADD COLUMN IF NOT EXISTS "last_request_at" timestamptz; -- ok (311.443959ms) -- migrating version 20260805000000 -> ALTER TABLE "public"."alert_configs" ADD COLUMN IF NOT EXISTS "notify_vulns" boolean NOT NULL DEFAULT false; -> ALTER TABLE "public"."alert_configs" ADD COLUMN IF NOT EXISTS "vuln_min_severity" text NOT NULL DEFAULT 'high'; -> ALTER TABLE "public"."alert_configs" ADD COLUMN IF NOT EXISTS "vuln_include_in_digest" boolean NOT NULL DEFAULT true; -> ALTER TABLE "public"."alert_configs" DROP CONSTRAINT IF EXISTS "alert_configs_vuln_min_severity_chk"; -> ALTER TABLE "public"."alert_configs" ADD CONSTRAINT "alert_configs_vuln_min_severity_chk" CHECK ("vuln_min_severity" IN ('critical', 'high', 'medium', 'low')); -> ALTER TABLE "public"."site_vulnerabilities" ADD COLUMN IF NOT EXISTS "notified_at" timestamptz; -> UPDATE "public"."site_vulnerabilities" SET "notified_at" = now() WHERE "notified_at" IS NULL; -> CREATE INDEX IF NOT EXISTS "idx_site_vuln_tenant_unnotified" ON "public"."site_vulnerabilities" ("tenant_id") WHERE "status" = 'open' AND "notified_at" IS NULL; -- ok (794.070625ms) -- migrating version 20260806000000 -> ALTER TABLE "public"."backup_snapshots" ADD COLUMN IF NOT EXISTS "stalled_at" timestamptz NULL; -- ok (328.385375ms) -- migrating version 20260807000000 -> ALTER TABLE "public"."autologin_policies" ADD COLUMN IF NOT EXISTS "default_wp_user_login" text NOT NULL DEFAULT ''; -- ok (345.64975ms) -- migrating version 20260808000000 -> SET LOCAL lock_timeout = '5s'; -> ALTER TABLE "public"."wporg_plugin_checksums" ADD COLUMN IF NOT EXISTS "sha256" text; -- ok (394.683292ms) -- migrating version 20260809000000 -> ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "app_probe_path" text NULL; -> ALTER TABLE "public"."site_uptime_probes" ADD COLUMN IF NOT EXISTS "app_up" boolean NULL, ADD COLUMN IF NOT EXISTS "app_probe_reason" text NULL; -> ALTER TABLE "public"."site_uptime_daily" ADD COLUMN IF NOT EXISTS "app_up_checks" integer NULL, ADD COLUMN IF NOT EXISTS "app_total_checks" integer NULL; -> ALTER TABLE "public"."site_uptime_status" ADD COLUMN IF NOT EXISTS "latest_app_up" boolean NULL, ADD COLUMN IF NOT EXISTS "app_probe_reason" text NULL, ADD COLUMN IF NOT EXISTS "last_app_probed_at" timestamptz NULL; -- ok (528.759625ms) -- migrating version 20260810000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."app_alert_rollout" ( "singleton" boolean PRIMARY KEY DEFAULT true, "fresh_install" boolean NOT NULL, "decided_at" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "app_alert_rollout_singleton_chk" CHECK ("singleton") ); END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_app_alert_state" ( "site_id" uuid NOT NULL, "tenant_id" uuid NOT NULL, "last_status" text NOT NULL DEFAULT 'unknown', "consecutive_down" integer NOT NULL DEFAULT 0, "in_incident" boolean NOT NULL DEFAULT false, -- Sticky: set true the first time a CONCLUSIVE app_up=true verdict is -- observed, and never reset false again (a site cannot "un-prove" -- that it was once conclusively healthy). See EvaluateApp. "ever_app_up" boolean NOT NULL DEFAULT false, "last_alert_at" timestamptz, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("site_id"), CONSTRAINT "site_app_alert_state_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "site_app_alert_state_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> CREATE INDEX IF NOT EXISTS "site_app_alert_state_tenant_id_idx" ON "public"."site_app_alert_state" ("tenant_id"); -> DO $$ BEGIN ALTER TABLE "public"."site_app_alert_state" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_app_alert_state" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_app_alert_state' AND policyname = 'site_app_alert_state_tenant_isolation' ) THEN CREATE POLICY "site_app_alert_state_tenant_isolation" ON "public"."site_app_alert_state" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_app_alert_state' AND policyname = 'site_app_alert_state_agent' ) THEN CREATE POLICY "site_app_alert_state_agent" ON "public"."site_app_alert_state" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."tenant_app_alert_breaker" ( "tenant_id" uuid NOT NULL, "tripped" boolean NOT NULL DEFAULT false, "tripped_at" timestamptz, "last_alert_at" timestamptz, -- last_down_count (GH #291 Phase 3 Fix 3): the down count AT THE -- TIME OF THE LAST notification (trip, update, or recovery), so a -- later tick can detect "materially worse since we last said -- anything" - see EvaluateAppBreaker's FireUpdate. "last_down_count" integer NOT NULL DEFAULT 0, "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("tenant_id"), CONSTRAINT "tenant_app_alert_breaker_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE ); END; $$; -> ALTER TABLE "public"."tenant_app_alert_breaker" ADD COLUMN IF NOT EXISTS "last_down_count" integer NOT NULL DEFAULT 0; -> DO $$ BEGIN ALTER TABLE "public"."tenant_app_alert_breaker" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."tenant_app_alert_breaker" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'tenant_app_alert_breaker' AND policyname = 'tenant_app_alert_breaker_tenant_isolation' ) THEN CREATE POLICY "tenant_app_alert_breaker_tenant_isolation" ON "public"."tenant_app_alert_breaker" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'tenant_app_alert_breaker' AND policyname = 'tenant_app_alert_breaker_agent' ) THEN CREATE POLICY "tenant_app_alert_breaker_agent" ON "public"."tenant_app_alert_breaker" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> ALTER TABLE "public"."sites" ADD COLUMN IF NOT EXISTS "app_alerts_disabled" boolean NOT NULL DEFAULT false; -> DO $$ DECLARE fresh_install boolean; BEGIN SELECT (count(*) = 0) INTO fresh_install FROM "public"."sites"; EXECUTE format( 'ALTER TABLE "public"."alert_configs" ADD COLUMN IF NOT EXISTS "app_alerts_enabled" boolean NOT NULL DEFAULT %L', fresh_install ); INSERT INTO "public"."app_alert_rollout" ("singleton", "fresh_install") VALUES (true, fresh_install) ON CONFLICT ("singleton") DO NOTHING; END; $$; -- ok (1.140377792s) -- migrating version 20260811000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."agent_mirror_state" ( "id" integer PRIMARY KEY DEFAULT 1 CONSTRAINT "agent_mirror_state_singleton_chk" CHECK ("id" = 1), -- Spacing clock: the wall-clock time of the last ACTUAL GitHub -- request, whatever the status code (200, 304, 403 and 429 all -- spend the slot). Persisted, not merely in-memory, so the manual -- check endpoint can answer "not checked, and here is why" from a -- replica that is not the one that will work the job. "last_request_at" timestamptz, -- LAST ATTEMPT. Written for every run that actually executed. -- NEVER written while mirroring is disabled entirely (see -- internal/agentupstream.MirrorWorker.Work): stamping an attempt for -- a run that did nothing would be the same lie in miniature this -- feature exists to remove. "last_attempt_at" timestamptz, "last_attempt_outcome" text, "last_attempt_detail" text, -- curated, non-secret, <=200 chars (Go) "last_attempt_trigger" text, -- LAST SUCCESS (LAST CONFIRMATION). Advances only when this install -- established what upstream publishes: mirrored, current, or -- unchanged (a 304). This is the ONLY timestamp an operator-facing -- "checked N ago" age may ever be computed from, never -- last_attempt_at (see the module doc: LAST ATTEMPT vs LAST SUCCESS -- is the whole point of this table). "last_success_at" timestamptz, "last_success_outcome" text, "last_success_version" text, -- Last time a NEW release was actually published into this -- install's own storage, as distinct from merely confirming the -- existing one. "last_mirrored_at" timestamptz, "last_mirrored_version" text, "updated_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> ALTER TABLE "public"."agent_mirror_state" DROP CONSTRAINT IF EXISTS "agent_mirror_state_attempt_outcome_chk"; -> ALTER TABLE "public"."agent_mirror_state" ADD CONSTRAINT "agent_mirror_state_attempt_outcome_chk" CHECK ("last_attempt_outcome" IS NULL OR "last_attempt_outcome" IN ( 'mirrored', 'current', 'unchanged', 'rate_limited', 'refused', 'foreign_channel', 'upstream_unavailable', 'storage_error', 'not_configured' )); -> ALTER TABLE "public"."agent_mirror_state" DROP CONSTRAINT IF EXISTS "agent_mirror_state_success_outcome_chk"; -> ALTER TABLE "public"."agent_mirror_state" ADD CONSTRAINT "agent_mirror_state_success_outcome_chk" CHECK ("last_success_outcome" IS NULL OR "last_success_outcome" IN ( 'mirrored', 'current', 'unchanged' )); -> ALTER TABLE "public"."agent_mirror_state" DROP CONSTRAINT IF EXISTS "agent_mirror_state_trigger_chk"; -> ALTER TABLE "public"."agent_mirror_state" ADD CONSTRAINT "agent_mirror_state_trigger_chk" CHECK ("last_attempt_trigger" IS NULL OR "last_attempt_trigger" IN ( 'periodic', 'manual' )); -> DO $$ BEGIN INSERT INTO "public"."agent_mirror_state" ("id") VALUES (1) ON CONFLICT ("id") DO NOTHING; END; $$; -- ok (829.232209ms) -- migrating version 20260812000000 -> CREATE TABLE IF NOT EXISTS user_identities ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 'google' | 'github' | 'oidc' (the generic single-issuer SSO that predates -- this table). Free text rather than an enum so adding a provider is a code -- change and not a migration. provider text NOT NULL, subject text NOT NULL, -- Only meaningful for 'oidc', where a deployment picks its own issuer and -- two deployments can legitimately hand out the same subject. issuer text NOT NULL DEFAULT '', email text NOT NULL DEFAULT '', email_verified boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), last_login_at timestamptz ); -> CREATE UNIQUE INDEX IF NOT EXISTS user_identities_provider_subject_key ON user_identities (provider, subject, issuer); -> CREATE UNIQUE INDEX IF NOT EXISTS user_identities_user_provider_key ON user_identities (user_id, provider); -> CREATE INDEX IF NOT EXISTS user_identities_user_id_idx ON user_identities (user_id); -> INSERT INTO user_identities (user_id, provider, subject, issuer, email, email_verified, created_at) SELECT u.id, 'oidc', u.oidc_subject, COALESCE(u.oidc_issuer, ''), u.email, -- These identities were linked under the old rule, which already required -- the provider to assert a verified email before linking to an existing -- account. Recording true here preserves that meaning; it is not an -- assumption, it is what the old code enforced at link time. true, u.created_at FROM users u WHERE u.oidc_subject IS NOT NULL AND u.oidc_issuer IS NOT NULL ON CONFLICT DO NOTHING; -- ok (615.534667ms) -- migrating version 20260813000000 -> INSERT INTO user_identities (user_id, provider, subject, issuer, email, email_verified, created_at) SELECT u.id, 'oidc', u.oidc_subject, u.oidc_issuer, u.email, -- The old code required the provider to assert a verified email before it -- would link to an existing account, so true records what was enforced at -- link time rather than an assumption made now. true, u.created_at FROM users u WHERE u.oidc_subject IS NOT NULL AND u.oidc_issuer IS NOT NULL ON CONFLICT DO NOTHING; -- ok (329.107667ms) -- migrating version 20260814000000 -> DO $$ BEGIN ALTER TABLE "public"."site_email_config" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_email_config" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_site_scope_read' ) THEN CREATE POLICY "site_email_config_site_scope_read" ON "public"."site_email_config" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" IS NULL OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_site_scope_insert' ) THEN CREATE POLICY "site_email_config_site_scope_insert" ON "public"."site_email_config" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_site_scope_update' ) THEN CREATE POLICY "site_email_config_site_scope_update" ON "public"."site_email_config" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_config' AND policyname = 'site_email_config_site_scope_delete' ) THEN CREATE POLICY "site_email_config_site_scope_delete" ON "public"."site_email_config" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_email_connection" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_email_connection" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_site_scope_read' ) THEN CREATE POLICY "site_email_connection_site_scope_read" ON "public"."site_email_connection" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR EXISTS ( SELECT 1 FROM "public"."site_email_config" c WHERE c."id" = "site_email_connection"."config_id" AND ( c."site_id" IS NULL OR c."site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_site_scope_insert' ) THEN CREATE POLICY "site_email_connection_site_scope_insert" ON "public"."site_email_connection" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR EXISTS ( SELECT 1 FROM "public"."site_email_config" c WHERE c."id" = "site_email_connection"."config_id" AND c."site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_site_scope_update' ) THEN CREATE POLICY "site_email_connection_site_scope_update" ON "public"."site_email_connection" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR EXISTS ( SELECT 1 FROM "public"."site_email_config" c WHERE c."id" = "site_email_connection"."config_id" AND c."site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR EXISTS ( SELECT 1 FROM "public"."site_email_config" c WHERE c."id" = "site_email_connection"."config_id" AND c."site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_connection' AND policyname = 'site_email_connection_site_scope_delete' ) THEN CREATE POLICY "site_email_connection_site_scope_delete" ON "public"."site_email_connection" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR EXISTS ( SELECT 1 FROM "public"."site_email_config" c WHERE c."id" = "site_email_connection"."config_id" AND c."site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) ); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_email_log" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_email_log" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_site_scope_read' ) THEN CREATE POLICY "site_email_log_site_scope_read" ON "public"."site_email_log" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_site_scope_insert' ) THEN CREATE POLICY "site_email_log_site_scope_insert" ON "public"."site_email_log" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_site_scope_update' ) THEN CREATE POLICY "site_email_log_site_scope_update" ON "public"."site_email_log" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_email_log' AND policyname = 'site_email_log_site_scope_delete' ) THEN CREATE POLICY "site_email_log_site_scope_delete" ON "public"."site_email_log" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."email_suppression" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."email_suppression" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_site_scope_read' ) THEN CREATE POLICY "email_suppression_site_scope_read" ON "public"."email_suppression" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" IS NULL OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_site_scope_insert' ) THEN CREATE POLICY "email_suppression_site_scope_insert" ON "public"."email_suppression" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_site_scope_update' ) THEN CREATE POLICY "email_suppression_site_scope_update" ON "public"."email_suppression" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'email_suppression' AND policyname = 'email_suppression_site_scope_delete' ) THEN CREATE POLICY "email_suppression_site_scope_delete" ON "public"."email_suppression" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -- ok (1.761044959s) -- migrating version 20260815000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."site_object_reclaim" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- NEITHER of these is a foreign key. See the header: a cascade from -- either parent destroys the record in the operation it exists to -- survive, and for tenants that operation is admin_delete_empty_tenant. "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- Which site-scoped storage root to reclaim. 'backup_manifest' is the -- only kind today, and the set is CLOSED by the constraint below. -- -- The operator remedy in this header is a hand-written INSERT, so a typo -- in this column is a realistic event, and an expensive one: the worker -- cannot derive a prefix for a kind it does not know, and there is no -- other record anywhere of the objects that row was meant to reclaim. -- The constraint puts that failure in front of the person typing the -- statement, at the moment they can fix it. A database that predates the -- constraint gets it from m115 (NOT VALID, so it cannot fail a boot), and -- the worker treats an unknown kind as a retryable failure rather than a -- cancel so a row written before either still stays visible. -- -- backup.ReclaimKinds is the code-side copy of this set; tests/contract -- compares the two, so a kind cannot be added to one half alone. "kind" text NOT NULL DEFAULT 'backup_manifest' CONSTRAINT "site_object_reclaim_kind_check" CHECK ("kind" IN ('backup_manifest')), -- 'cp' | 'local' | 's3_compat' | NULL when the site had no destination -- row (the legacy control-plane-global bucket). Diagnostic only. "destination_kind" text, "attempts" int NOT NULL DEFAULT 0, "next_attempt_at" timestamptz NOT NULL DEFAULT now(), "last_error" text, "completed_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND indexname = 'site_object_reclaim_site_kind_key' ) THEN CREATE UNIQUE INDEX "site_object_reclaim_site_kind_key" ON "public"."site_object_reclaim" ("tenant_id", "site_id", "kind"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND indexname = 'site_object_reclaim_tenant_idx' ) THEN CREATE INDEX "site_object_reclaim_tenant_idx" ON "public"."site_object_reclaim" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND indexname = 'site_object_reclaim_due_idx' ) THEN CREATE INDEX "site_object_reclaim_due_idx" ON "public"."site_object_reclaim" ("next_attempt_at") WHERE "completed_at" IS NULL; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_object_reclaim" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_object_reclaim" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND policyname = 'site_object_reclaim_tenant_isolation' ) THEN CREATE POLICY "site_object_reclaim_tenant_isolation" ON "public"."site_object_reclaim" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND policyname = 'site_object_reclaim_agent' ) THEN CREATE POLICY "site_object_reclaim_agent" ON "public"."site_object_reclaim" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND policyname = 'site_object_reclaim_site_scope' ) THEN CREATE POLICY "site_object_reclaim_site_scope" ON "public"."site_object_reclaim" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -- ok (868.334833ms) -- migrating version 20260816000000 -> DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'site_object_reclaim' ) THEN ALTER TABLE "public"."site_object_reclaim" DROP CONSTRAINT IF EXISTS "site_object_reclaim_tenant_id_fkey"; END IF; END; $$; -> DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'site_object_reclaim' ) AND NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_reclaim' AND policyname = 'site_object_reclaim_site_scope' ) THEN CREATE POLICY "site_object_reclaim_site_scope" ON "public"."site_object_reclaim" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -- ok (424.909459ms) -- migrating version 20260817000000 -> DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'site_object_reclaim' ) AND NOT EXISTS ( SELECT 1 FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE n.nspname = 'public' AND t.relname = 'site_object_reclaim' AND c.conname = 'site_object_reclaim_kind_check' ) THEN ALTER TABLE "public"."site_object_reclaim" ADD CONSTRAINT "site_object_reclaim_kind_check" CHECK ("kind" IN ('backup_manifest')) NOT VALID; END IF; END; $$; -- ok (334.889458ms) -- migrating version 20260818000000 -> DO $$ BEGIN CREATE TABLE IF NOT EXISTS "public"."tenant_object_reclaim" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- NOT a foreign key, and that is the entire point. See the header. "tenant_id" uuid NOT NULL, -- Which tenant-scoped storage set to reclaim. 'tenant_storage' means all -- seven roots org.ObjectStoragePrefixes returns, which is every root -- Lane B sweeps, including chunks//. The set is CLOSED by the -- constraint below for the same reason m113/m115 closed its own: a task -- carrying a kind the worker cannot act on reclaims nothing while being -- the only record naming those objects. backup.TenantReclaimKinds is the -- code-side copy; tests/contract compares the two. "kind" text NOT NULL DEFAULT 'tenant_storage' CONSTRAINT "tenant_object_reclaim_kind_check" CHECK ("kind" IN ('tenant_storage')), "attempts" int NOT NULL DEFAULT 0, -- The 24 hour floor. Defence in depth, not the proof. See the header. "next_attempt_at" timestamptz NOT NULL DEFAULT now() + interval '24 hours', "last_error" text, "completed_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now() ); END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'tenant_object_reclaim' AND indexname = 'tenant_object_reclaim_tenant_kind_key' ) THEN CREATE UNIQUE INDEX "tenant_object_reclaim_tenant_kind_key" ON "public"."tenant_object_reclaim" ("tenant_id", "kind"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'tenant_object_reclaim' AND indexname = 'tenant_object_reclaim_due_idx' ) THEN CREATE INDEX "tenant_object_reclaim_due_idx" ON "public"."tenant_object_reclaim" ("next_attempt_at") WHERE "completed_at" IS NULL; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."tenant_object_reclaim" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."tenant_object_reclaim" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'tenant_object_reclaim' AND policyname = 'tenant_object_reclaim_tenant_isolation' ) THEN CREATE POLICY "tenant_object_reclaim_tenant_isolation" ON "public"."tenant_object_reclaim" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'tenant_object_reclaim' AND policyname = 'tenant_object_reclaim_agent' ) THEN CREATE POLICY "tenant_object_reclaim_agent" ON "public"."tenant_object_reclaim" USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> GRANT SELECT, INSERT, UPDATE ON "public"."tenant_object_reclaim" TO "wpmgr_app"; -> CREATE OR REPLACE FUNCTION "public"."admin_delete_empty_tenant"(p_tenant_id uuid) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_count integer; v_result boolean := false; v_prev_agent text := current_setting('app.agent', true); BEGIN PERFORM set_config('app.agent', 'on', true); IF NOT ( EXISTS (SELECT 1 FROM memberships m WHERE m.tenant_id = p_tenant_id) OR EXISTS (SELECT 1 FROM sites s WHERE s.tenant_id = p_tenant_id) ) THEN PERFORM set_config('app.tenant_id', p_tenant_id::text, true); DELETE FROM audit_log WHERE tenant_id = p_tenant_id; PERFORM set_config('app.tenant_id', '', true); DELETE FROM tenants t WHERE t.id = p_tenant_id; GET DIAGNOSTICS v_count = ROW_COUNT; v_result := v_count > 0; -- GH #408. The cascade above has just destroyed backup_chunks for this -- tenant, which was the only inventory naming chunks//, and -- this statement frees no object storage whatsoever. This row is now the -- only surviving name for that storage. It is written HERE, in the same -- transaction and gated on the delete having actually happened, so it -- exists if and only if the tenant row is really gone. A failure here -- aborts the delete on purpose: a committed delete with no record is the -- bug, and is unrecoverable; a failed request is not. IF v_result THEN INSERT INTO tenant_object_reclaim (tenant_id) VALUES (p_tenant_id) ON CONFLICT (tenant_id, kind) DO UPDATE SET completed_at = NULL, attempts = 0, next_attempt_at = now() + interval '24 hours', last_error = NULL, updated_at = now(); END IF; END IF; PERFORM set_config('app.agent', coalesce(v_prev_agent, ''), true); RETURN v_result; END; $$; -> REVOKE ALL ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) FROM PUBLIC; -> GRANT EXECUTE ON FUNCTION "public"."admin_delete_empty_tenant"(uuid) TO "wpmgr_app"; -- ok (963.488708ms) -- migrating version 20260819000000 -> ALTER TABLE "public"."sites" -- NULL means monitoring is ACTIVE. Non-NULL means paused, and the value is -- the instant it was paused: the flag and the since-when in one column, so -- the two can never disagree. Every phase-2 scheduler predicate is -- "monitoring_paused_at IS NULL". ADD COLUMN IF NOT EXISTS "monitoring_paused_at" timestamptz NULL, -- Who paused it, for the badge. Nullable by design and NOT only because of -- ON DELETE SET NULL: a pause set by an automated path has no user to name. ADD COLUMN IF NOT EXISTS "monitoring_paused_by" uuid NULL, -- Optional free text, shown on hover. NOT NULL DEFAULT '' rather than -- nullable text, matching every other optional text column on this table -- (wp_version, host_provider, age_recipient, ...), so readers never have to -- distinguish NULL from empty. ADD COLUMN IF NOT EXISTS "monitoring_paused_reason" text NOT NULL DEFAULT '', -- Optional auto-resume instant. NULL means "paused until someone resumes -- it", which is the default and the common case. ADD COLUMN IF NOT EXISTS "monitoring_resume_at" timestamptz NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.sites'::regclass AND conname = 'sites_monitoring_paused_by_fkey' ) THEN ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_monitoring_paused_by_fkey" FOREIGN KEY ("monitoring_paused_by") REFERENCES "public"."users" ("id") ON UPDATE NO ACTION ON DELETE SET NULL; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.sites'::regclass AND conname = 'sites_monitoring_resume_requires_pause_check' ) THEN ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_monitoring_resume_requires_pause_check" CHECK ("monitoring_resume_at" IS NULL OR "monitoring_paused_at" IS NOT NULL); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'sites' AND indexname = 'sites_monitoring_resume_due_idx' ) THEN CREATE INDEX "sites_monitoring_resume_due_idx" ON "public"."sites" ("monitoring_resume_at") WHERE "monitoring_resume_at" IS NOT NULL AND "monitoring_paused_at" IS NOT NULL; END IF; END; $$; -- ok (571.080208ms) -- migrating version 20260820000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'update_runs' AND policyname = 'update_runs_agent' ) THEN CREATE POLICY "update_runs_agent" ON "public"."update_runs" FOR ALL USING (current_setting('app.agent', true) = 'on') WITH CHECK (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'update_runs' AND indexname = 'update_runs_due_idx' ) THEN CREATE INDEX "update_runs_due_idx" ON "public"."update_runs" ("scheduled_at") WHERE "status" = 'scheduled'; END IF; END; $$; -> COMMENT ON COLUMN "public"."update_runs"."status" IS $c$Run lifecycle. No CHECK constraint exists; this comment is the contract. pending Created and its tasks enqueued for immediate execution. The m3 default, and still the only state an immediate run passes through. scheduled (#463) Created with a future scheduled_at and NOT yet handed to the worker. The dispatcher's due-scan selects exactly these, and update_runs_due_idx is partial on this value. dispatching (#463) Claimed by the dispatcher for this tick. The row has left update_runs_due_idx, so a concurrent tick, a second replica or a restart mid-dispatch cannot claim it again. Transient: the same transaction that sets it enqueues the work. running At least one task is running. completed Every task reached a terminal state. expired (#463) The run passed its dispatch window without being dispatched - the control plane was down across scheduled_at, or the run sat past the point where executing it is still what the operator asked for. Terminal, and NEVER retried: a deferred bulk update that fires days late is a surprise, not a service. Distinct from 'completed' with failures, which was attempted. Cross-tenant readers of this column run under InAgentTx and are admitted by update_runs_agent (m118), not by update_runs_tenant_isolation.$c$; -> COMMENT ON COLUMN "public"."update_tasks"."status" IS $c$Per-task lifecycle. No CHECK constraint exists; this comment is the contract. pending Created, awaiting execution. running In flight on the agent. succeeded Applied. failed Attempted and failed. rolled_back Attempted, failed, and reverted from the snapshot. skipped Not attempted, by decision at plan time. scheduled (#463) Belongs to a run that is 'scheduled' and is not yet eligible for execution. NOTE: 'scheduled' is NOT one of the statuses in update_tasks_inflight_target_idx, whose predicate is status IN ('pending','running'). That index is the authoritative cross-run dedup guard (m88), so a scheduled task does NOT reserve its (tenant, site, target) pair against a concurrent immediate run. See the note handed to backend-architect with this migration: Phase 1 must decide deliberately whether scheduled tasks reserve their target, and widening that unique index is a separate migration with a data-dedup step, not a comment. expired (#463) The parent run expired without dispatching, so this task was never attempted. Terminal. The RESTRICTIVE update_tasks_site_scope policy (m19) and the cross-tenant update_tasks_agent policy (m89) both apply to this table; update_runs carries the agent policy from m118 but no site-scope policy, because it has no site_id.$c$; -- ok (609.811792ms) -- migrating version 20260821000000 -> COMMENT ON COLUMN "public"."update_runs"."status" IS $c$Run lifecycle. No CHECK constraint exists; this comment is the contract. Reconciled against internal/update/model.go by m119 (#482). pending Created and its tasks enqueued for immediate execution. The m3 default, and still the only state an immediate run passes through. scheduled (#463) Created with a future scheduled_at and NOT yet handed to the worker. The dispatcher's due-scan selects exactly these, and update_runs_due_idx is partial on this value. dispatching (#463) Claimed by the dispatcher for this tick. The row has left update_runs_due_idx, so a concurrent tick, a second replica or a restart mid-dispatch cannot claim it again. Transient: the same transaction that sets it enqueues the work. running At least one task is running. completed Every task reached a terminal state. halted (m119/#482 - written since the agent self-update wave machine shipped, and declared by no migration until m119.) Terminal. The run was STOPPED rather than finished, and is deliberately not spelled 'completed', which would erase that fact. Reached two ways, by two subsystems: - a wave gate refused to advance an agent self-update rollout (update/agent_repo.go haltLocked). Tasks underneath are a MIXTURE of real outcomes: those already dispatched run to their own conclusion and are never overwritten, only the still-'pending' ones become 'cancelled'. - an operator cancelled a scheduled run before it fired (update/cancel_repo.go CancelScheduledRun, #463). Tasks underneath are UNIFORMLY 'cancelled' and nothing was ever sent to any site. The run vocabulary has no separate 'cancelled': cancel_repo.go reuses this value on purpose rather than minting a status no existing reader can render, and the task statuses underneath are what distinguish the two cases. expired (#463) The run passed its dispatch window without being dispatched - the control plane was down across scheduled_at, or the run sat past the point where executing it is still what the operator asked for. Terminal, and NEVER retried: a deferred bulk update that fires days late is a surprise, not a service. Distinct from 'completed' with failures, which was attempted, and from 'halted', which was stopped by a gate or a human. Cross-tenant readers of this column run under InAgentTx and are admitted by update_runs_agent (m118), not by update_runs_tenant_isolation.$c$; -> COMMENT ON COLUMN "public"."update_tasks"."status" IS $c$Per-task lifecycle. No CHECK constraint exists; this comment is the contract. Reconciled against internal/update/model.go by m119 (#482). pending Created, awaiting execution. running In flight on the agent. succeeded Applied. failed Attempted and failed. rolled_back Attempted, failed, and reverted from the snapshot. skipped Not attempted, by decision at plan time - the control plane declined this particular target. cancelled (m119/#482 - written since the wave machine shipped, and declared by no migration until m119.) Terminal. NOTHING WAS EVER SENT TO THIS SITE, and a human or a gate decided that. Written by update/agent_repo.go haltLocked (only over tasks still 'pending'; a 'running' task is left alone, because its command is already delivered and marking it cancelled would both record a falsehood and stop the confirm poll that is the control plane's only way to learn whether the site upgraded or bricked) and by update/cancel_repo.go CancelScheduledRun (#463, over the 'scheduled' tasks of a run an operator cancelled). Distinct from 'skipped', where the control plane declined the target rather than a human stopping the run; from 'failed', where the site WAS contacted; and from 'expired', below. scheduled (#463) Belongs to a run that is 'scheduled' and is not yet eligible for execution. NOTE: 'scheduled' is NOT one of the statuses in update_tasks_inflight_target_idx, whose predicate is status IN ('pending','running'). That index is the authoritative cross-run dedup guard (m88), so a scheduled task does NOT reserve its (tenant, site, target) pair against a concurrent immediate run. expired (#463) The parent run expired without dispatching, so this task was never attempted. Terminal. NOT a spelling of 'cancelled': 'cancelled' records a decision somebody made, 'expired' records that the window closed while the control plane was unavailable. The RESTRICTIVE update_tasks_site_scope policy (m19) and the cross-tenant update_tasks_agent policy (m89) both apply to this table; update_runs carries the agent policy from m118 but no site-scope policy, because it has no site_id.$c$; -- ok (415.698125ms) -- migrating version 20260822000000 -> ALTER TABLE "public"."api_keys" ADD COLUMN IF NOT EXISTS "kind" text NOT NULL DEFAULT 'integration'; -> ALTER TABLE "public"."api_keys" ADD COLUMN IF NOT EXISTS "auth_model" text NOT NULL DEFAULT 'role'; -> ALTER TABLE "public"."api_keys" ADD COLUMN IF NOT EXISTS "capabilities" text[]; -> ALTER TABLE "public"."api_keys" ADD COLUMN IF NOT EXISTS "site_scope" text NOT NULL DEFAULT 'org'; -> ALTER TABLE "public"."api_keys" ADD COLUMN IF NOT EXISTS "allowed_site_ids" uuid[] NOT NULL DEFAULT '{}'::uuid[]; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_kind_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_kind_check" CHECK ("kind" IN ('integration', 'agent')); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_auth_model_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_auth_model_check" CHECK ("auth_model" IN ('role', 'capability')); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_site_scope_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_site_scope_check" CHECK ("site_scope" IN ('org', 'site')); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_capabilities_shape_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_capabilities_shape_check" CHECK ( "capabilities" IS NULL OR ( coalesce(array_ndims("capabilities"), 1) = 1 AND cardinality("capabilities") <= 64 AND array_position("capabilities", NULL) IS NULL AND NOT ('' = ANY ("capabilities")) ) ); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_auth_model_capabilities_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_auth_model_capabilities_check" CHECK ( ("auth_model" = 'capability' AND "capabilities" IS NOT NULL) OR ("auth_model" = 'role' AND "capabilities" IS NULL) ); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_site_scope_allowlist_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_site_scope_allowlist_check" CHECK ( "site_scope" = 'site' OR cardinality("allowed_site_ids") = 0 ); END IF; END $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.api_keys'::regclass AND conname = 'api_keys_agent_capability_check' ) THEN ALTER TABLE "public"."api_keys" ADD CONSTRAINT "api_keys_agent_capability_check" CHECK ( "kind" <> 'agent' OR "auth_model" = 'capability' ); END IF; END $$; -- ok (1.115022542s) -- migrating version 20260823000000 -> ALTER TABLE "public"."sites" -- When the control plane last WROTE sites.components, whether or not the -- document changed. NULL means "we have never recorded this" and is the -- correct value for every row existing when m121 applies - no default and -- no backfill, because a manufactured observation is worse than an admitted -- absence (GH #509). NOT the instant the agent collected the inventory: the -- payload carries no such timestamp, so that fact is not knowable here. -- Written only by UpdateSiteMetadata (db/query/sites.sql), which is the only -- statement in the tree that writes components. Deliberately NOT written by -- the heartbeat, by any connection-state transition, or by re-enrollment - -- see DECISION 3. ADD COLUMN IF NOT EXISTS "components_updated_at" timestamptz NULL; -- ok (352.214542ms) -- migrating version 20260824000000 -> CREATE TABLE IF NOT EXISTS "public"."org_context_versions" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- The organisation. In this schema an organisation IS a tenant, so this -- column is both the subject key and the tenant-isolation key. "tenant_id" uuid NOT NULL, -- Monotonic from 1 per organisation. See DECISION 5: the unique index over -- this is what makes a concurrent double-write an error instead of a lost -- update, while ADR-064's open question 2 is still open. "version" bigint NOT NULL CONSTRAINT "org_context_versions_version_positive_check" CHECK ("version" >= 1), -- THE TWO KINDS OF FIELD (ADR-064 Decision 3). Two columns, never one: -- the never-widen check reads restrictions and must not be reachable by -- guidance. See DECISION 1. -- -- Structured, mechanically comparable. Decision 4's widen-check runs here. "restrictions" jsonb NOT NULL DEFAULT '{}'::jsonb CONSTRAINT "org_context_versions_restrictions_object_check" CHECK (jsonb_typeof("restrictions") = 'object'), -- Free text. ADR-064 is deliberate that "wider" and "narrower" are not -- defined relations over prose, and that no mechanical check applies here. "guidance" jsonb NOT NULL DEFAULT '{}'::jsonb CONSTRAINT "org_context_versions_guidance_object_check" CHECK (jsonb_typeof("guidance") = 'object'), -- WHO. No foreign key, deliberately - see the header. author_id is NULL -- exactly when the author has no principal id, which today means the -- transfer operation of ADR-064 Decision 12. "author_type" text NOT NULL CONSTRAINT "org_context_versions_author_type_check" CHECK ("author_type" IN ('user', 'api_key', 'system')), "author_id" uuid NULL, -- A 'system' author has no id and a credentialed one always does. This -- deliberately does NOT constrain which of users or api_keys the id names: -- that is what author_type is for, and no single column can reference two -- tables. CONSTRAINT "org_context_versions_author_id_matches_type_check" CHECK (("author_type" = 'system') = ("author_id" IS NULL)), -- HOW. Closed set; 'import' is deliberately absent. See DECISION 9. "provenance" text NOT NULL CONSTRAINT "org_context_versions_provenance_check" CHECK ("provenance" IN ('manual', 'restore', 'transfer')), -- Which version this one reproduces. NULL means "not a restore" - never a -- sentinel. No foreign key: the check that matters is a cross-row stamp -- comparison no single-column reference can express. "restored_from_version_id" uuid NULL, -- A restore names the version it restored, and nothing else does. This is -- the m115 lesson: close the constraint on the way in. CONSTRAINT "org_context_versions_restore_pointer_check" CHECK (("provenance" = 'restore') = ("restored_from_version_id" IS NOT NULL)), "created_at" timestamptz NOT NULL DEFAULT now() ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'org_context_versions' AND indexname = 'org_context_versions_version_key' ) THEN CREATE UNIQUE INDEX "org_context_versions_version_key" ON "public"."org_context_versions" ("tenant_id", "version"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.org_context_versions'::regclass AND conname = 'org_context_versions_tenant_id_fkey' ) THEN ALTER TABLE "public"."org_context_versions" ADD CONSTRAINT "org_context_versions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."org_context_versions" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."org_context_versions" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'org_context_versions' AND policyname = 'org_context_versions_tenant_isolation' ) THEN CREATE POLICY "org_context_versions_tenant_isolation" ON "public"."org_context_versions" FOR ALL USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'org_context_versions' AND policyname = 'org_context_versions_site_scope_insert' ) THEN CREATE POLICY "org_context_versions_site_scope_insert" ON "public"."org_context_versions" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."site_context_versions" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- THE ORGANISATION STAMP, per ADR-064 Decision 3: the organisation that -- owned this site AT THE TIME OF THIS WRITE. Set once, never rewritten by -- a later transfer. It is NOT a live view of the site's current owner, and -- the composite foreign key that would force it to be one is deliberately -- absent - see the header. "tenant_id" uuid NOT NULL, "site_id" uuid NOT NULL, -- Monotonic from 1 per SITE, not per (site, organisation): the sequence -- continues across a transfer so a restore reference stays unambiguous. "version" bigint NOT NULL CONSTRAINT "site_context_versions_version_positive_check" CHECK ("version" >= 1), "restrictions" jsonb NOT NULL DEFAULT '{}'::jsonb CONSTRAINT "site_context_versions_restrictions_object_check" CHECK (jsonb_typeof("restrictions") = 'object'), "guidance" jsonb NOT NULL DEFAULT '{}'::jsonb CONSTRAINT "site_context_versions_guidance_object_check" CHECK (jsonb_typeof("guidance") = 'object'), "author_type" text NOT NULL CONSTRAINT "site_context_versions_author_type_check" CHECK ("author_type" IN ('user', 'api_key', 'system')), "author_id" uuid NULL, CONSTRAINT "site_context_versions_author_id_matches_type_check" CHECK (("author_type" = 'system') = ("author_id" IS NULL)), "provenance" text NOT NULL CONSTRAINT "site_context_versions_provenance_check" CHECK ("provenance" IN ('manual', 'restore', 'transfer')), "restored_from_version_id" uuid NULL, CONSTRAINT "site_context_versions_restore_pointer_check" CHECK (("provenance" = 'restore') = ("restored_from_version_id" IS NOT NULL)), "created_at" timestamptz NOT NULL DEFAULT now() ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_context_versions' AND indexname = 'site_context_versions_version_key' ) THEN CREATE UNIQUE INDEX "site_context_versions_version_key" ON "public"."site_context_versions" ("site_id", "version"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'site_context_versions' AND indexname = 'site_context_versions_tenant_idx' ) THEN CREATE INDEX "site_context_versions_tenant_idx" ON "public"."site_context_versions" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.site_context_versions'::regclass AND conname = 'site_context_versions_tenant_id_fkey' ) THEN ALTER TABLE "public"."site_context_versions" ADD CONSTRAINT "site_context_versions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.site_context_versions'::regclass AND conname = 'site_context_versions_site_id_fkey' ) THEN ALTER TABLE "public"."site_context_versions" ADD CONSTRAINT "site_context_versions_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "public"."sites" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."site_context_versions" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."site_context_versions" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_context_versions' AND policyname = 'site_context_versions_tenant_isolation' ) THEN CREATE POLICY "site_context_versions_tenant_isolation" ON "public"."site_context_versions" FOR ALL USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_context_versions' AND policyname = 'site_context_versions_site_scope' ) THEN CREATE POLICY "site_context_versions_site_scope" ON "public"."site_context_versions" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array(nullif(current_setting('app.allowed_site_ids', true), ''), ',')::uuid[] ) ); END IF; END; $$; -> GRANT SELECT, INSERT ON "public"."org_context_versions" TO wpmgr_app; -> GRANT SELECT, INSERT ON "public"."site_context_versions" TO wpmgr_app; -> REVOKE UPDATE, DELETE, TRUNCATE ON "public"."org_context_versions" FROM wpmgr_app; -> REVOKE UPDATE, DELETE, TRUNCATE ON "public"."site_context_versions" FROM wpmgr_app; -- ok (1.56749075s) -- migrating version 20260825000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'org_context_versions' AND policyname = 'org_context_versions_site_scope_update' ) THEN CREATE POLICY "org_context_versions_site_scope_update" ON "public"."org_context_versions" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'org_context_versions' AND policyname = 'org_context_versions_site_scope_delete' ) THEN CREATE POLICY "org_context_versions_site_scope_delete" ON "public"."org_context_versions" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -- ok (437.555292ms) -- migrating version 20260826000000 -> CREATE TABLE IF NOT EXISTS "public"."mcp_grants" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, -- Human-set. What the operator called this connection in the UI. "name" text NOT NULL CONSTRAINT "mcp_grants_name_not_blank_check" CHECK (length(btrim("name")) > 0), -- LIVENESS. NOT NULL, closed set, NO DEFAULT: a DEFAULT 'active' would mean -- a caller that forgot to say produces a live credential. See DECISION 2. "status" text NOT NULL CONSTRAINT "mcp_grants_status_check" CHECK ("status" IN ('active', 'revoked')), -- WHICH SITES THIS GRANT MAY TOUCH. NOT NULL, closed set, NO DEFAULT. -- There is deliberately no DEFAULT 'all'. See DECISION 1. "site_scope_mode" text NOT NULL CONSTRAINT "mcp_grants_site_scope_mode_check" CHECK ("site_scope_mode" IN ('all', 'tags', 'list')), -- The payload for modes 'tags' and 'list'. Empty grants nothing, which is -- the restrictive direction; the CHECK below stops empty from co-existing -- with a mode that would make it meaningful. "scope_tag_ids" uuid[] NOT NULL DEFAULT '{}', "scope_site_ids" uuid[] NOT NULL DEFAULT '{}', -- "Requested a scope and named nothing" is unrepresentable. An empty -- allowlist is precisely the value a caller reads as "no filter, therefore -- everything", and it cannot be stored. CONSTRAINT "mcp_grants_site_scope_payload_check" CHECK ( ("site_scope_mode" = 'all' AND cardinality("scope_tag_ids") = 0 AND cardinality("scope_site_ids") = 0) OR ("site_scope_mode" = 'tags' AND cardinality("scope_tag_ids") > 0 AND cardinality("scope_site_ids") = 0) OR ("site_scope_mode" = 'list' AND cardinality("scope_site_ids") > 0 AND cardinality("scope_tag_ids") = 0) ), -- WHICH CLIENT. For the OAuth path this is the registered client_id; for -- the headless connection-token path there is no OAuth client and it is -- NULL. No foreign key - see DECISION 12. "client_id" text NULL, -- WHAT THE CLIENT REPORTED ABOUT ITSELF. All three are observations, so all -- three are nullable with no default. protocol_version IS NULL means the -- client sent no MCP-Protocol-Version header; recorded_at IS NULL means it -- has never connected at all. Two distinct absences. See DECISION 10. "client_name" text NULL, "client_version" text NULL, "protocol_version" text NULL, "client_identity_recorded_at" timestamptz NULL, -- Who created it. No foreign key: SET NULL would erase the authorship of a -- live credential and CASCADE would destroy an organisation's connections -- because a member left. Same call as m122's author_id. "created_by_user_id" uuid NULL, "created_at" timestamptz NOT NULL DEFAULT now(), -- NULL means never used. Not epoch, not created_at. See DECISION 10. "last_used_at" timestamptz NULL, "revoked_at" timestamptz NULL, -- A revoked grant records when, and a live one cannot claim to have been -- revoked. Closed on the way in - the m115 lesson. CONSTRAINT "mcp_grants_revoked_at_matches_status_check" CHECK (("status" = 'revoked') = ("revoked_at" IS NOT NULL)) ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND indexname = 'mcp_grants_live_idx' ) THEN CREATE INDEX "mcp_grants_live_idx" ON "public"."mcp_grants" ("tenant_id") WHERE "status" = 'active'; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND indexname = 'mcp_grants_tenant_idx' ) THEN CREATE INDEX "mcp_grants_tenant_idx" ON "public"."mcp_grants" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_tenant_id_fkey' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."mcp_grants" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."mcp_grants" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND policyname = 'mcp_grants_tenant_isolation' ) THEN CREATE POLICY "mcp_grants_tenant_isolation" ON "public"."mcp_grants" FOR ALL USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND policyname = 'mcp_grants_site_scope_select' ) THEN CREATE POLICY "mcp_grants_site_scope_select" ON "public"."mcp_grants" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND policyname = 'mcp_grants_site_scope_insert' ) THEN CREATE POLICY "mcp_grants_site_scope_insert" ON "public"."mcp_grants" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND policyname = 'mcp_grants_site_scope_update' ) THEN CREATE POLICY "mcp_grants_site_scope_update" ON "public"."mcp_grants" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_grants' AND policyname = 'mcp_grants_site_scope_delete' ) THEN CREATE POLICY "mcp_grants_site_scope_delete" ON "public"."mcp_grants" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."mcp_connection_tokens" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "tenant_id" uuid NOT NULL, "grant_id" uuid NOT NULL, -- The short PUBLIC handle shown in the UI so an operator can tell two -- tokens apart while rotating. Carries no authentication weight; the lookup -- is on the hash. Mirrors api_keys.prefix. "token_prefix" text NOT NULL CONSTRAINT "mcp_connection_tokens_prefix_not_blank_check" CHECK (length(btrim("token_prefix")) > 0), -- THE CREDENTIAL, HASHED. Lower-case hex SHA-256, the construction at -- internal/apikey/apikey.go:102 and internal/agent/signature.go:47. The -- plaintext is returned once at creation and never stored. See DECISION 4. "token_hash" text NOT NULL CONSTRAINT "mcp_connection_tokens_hash_format_check" CHECK ("token_hash" ~ '^[0-9a-f]{64}$'), -- LIVENESS. NOT NULL, closed set, NO DEFAULT. See DECISION 2. "status" text NOT NULL CONSTRAINT "mcp_connection_tokens_status_check" CHECK ("status" IN ('active', 'revoked')), "created_at" timestamptz NOT NULL DEFAULT now(), -- NOT THE LIVENESS GATE. NULL means this token does not expire on a clock, -- which is the documented headless path. status is what revocation flips. -- See DECISION 2. "expires_at" timestamptz NULL, "last_used_at" timestamptz NULL, "revoked_at" timestamptz NULL, CONSTRAINT "mcp_connection_tokens_revoked_at_matches_status_check" CHECK (("status" = 'revoked') = ("revoked_at" IS NOT NULL)) ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND indexname = 'mcp_connection_tokens_hash_key' ) THEN CREATE UNIQUE INDEX "mcp_connection_tokens_hash_key" ON "public"."mcp_connection_tokens" ("token_hash"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND indexname = 'mcp_connection_tokens_grant_idx' ) THEN CREATE INDEX "mcp_connection_tokens_grant_idx" ON "public"."mcp_connection_tokens" ("grant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND indexname = 'mcp_connection_tokens_tenant_idx' ) THEN CREATE INDEX "mcp_connection_tokens_tenant_idx" ON "public"."mcp_connection_tokens" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_connection_tokens'::regclass AND conname = 'mcp_connection_tokens_tenant_id_fkey' ) THEN ALTER TABLE "public"."mcp_connection_tokens" ADD CONSTRAINT "mcp_connection_tokens_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_connection_tokens'::regclass AND conname = 'mcp_connection_tokens_grant_id_fkey' ) THEN ALTER TABLE "public"."mcp_connection_tokens" ADD CONSTRAINT "mcp_connection_tokens_grant_id_fkey" FOREIGN KEY ("grant_id") REFERENCES "public"."mcp_grants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."mcp_connection_tokens" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."mcp_connection_tokens" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_tenant_isolation' ) THEN CREATE POLICY "mcp_connection_tokens_tenant_isolation" ON "public"."mcp_connection_tokens" FOR ALL USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_lookup' ) THEN CREATE POLICY "mcp_connection_tokens_lookup" ON "public"."mcp_connection_tokens" FOR SELECT USING (current_setting('app.mcp_token_lookup', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_site_scope_select' ) THEN CREATE POLICY "mcp_connection_tokens_site_scope_select" ON "public"."mcp_connection_tokens" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_site_scope_insert' ) THEN CREATE POLICY "mcp_connection_tokens_site_scope_insert" ON "public"."mcp_connection_tokens" AS RESTRICTIVE FOR INSERT WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_site_scope_update' ) THEN CREATE POLICY "mcp_connection_tokens_site_scope_update" ON "public"."mcp_connection_tokens" AS RESTRICTIVE FOR UPDATE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_connection_tokens' AND policyname = 'mcp_connection_tokens_site_scope_delete' ) THEN CREATE POLICY "mcp_connection_tokens_site_scope_delete" ON "public"."mcp_connection_tokens" AS RESTRICTIVE FOR DELETE USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."mcp_oauth_clients" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- Public per RFC 6749 section 2.2. Not a secret; possession authorizes -- nothing on its own. "client_id" text NOT NULL, -- THE SECRET, HASHED, and NULL exactly when there is none. Lower-case hex -- SHA-256, the same construction as the connection token. See DECISION 4. "client_secret_hash" text NULL CONSTRAINT "mcp_oauth_clients_secret_format_check" CHECK ("client_secret_hash" IS NULL OR "client_secret_hash" ~ '^[0-9a-f]{64}$'), -- NOT NULL, closed set, NO DEFAULT. "token_endpoint_auth_method" text NOT NULL CONSTRAINT "mcp_oauth_clients_auth_method_check" CHECK ("token_endpoint_auth_method" IN ('none', 'client_secret_basic', 'client_secret_post')), -- 'none' if and only if there is no secret. A confidential client with a -- NULL hash - the row where the secret comparison has nothing to compare -- against - is now unrepresentable. See DECISION 11. CONSTRAINT "mcp_oauth_clients_secret_matches_method_check" CHECK (("token_endpoint_auth_method" = 'none') = ("client_secret_hash" IS NULL)), -- At least one redirect URI, always. An empty array would leave the -- redirect check with nothing to match against, and "matches nothing" is -- one careless Go comparison away from "matches anything". "redirect_uris" text[] NOT NULL CONSTRAINT "mcp_oauth_clients_redirect_uris_present_check" CHECK (cardinality("redirect_uris") > 0), -- What the client said about itself at registration. Observations, so -- nullable with no default. "client_name" text NULL, "client_uri" text NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "last_used_at" timestamptz NULL ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_oauth_clients' AND indexname = 'mcp_oauth_clients_client_id_key' ) THEN CREATE UNIQUE INDEX "mcp_oauth_clients_client_id_key" ON "public"."mcp_oauth_clients" ("client_id"); END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."mcp_oauth_clients" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."mcp_oauth_clients" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_oauth_clients' AND policyname = 'mcp_oauth_clients_registration' ) THEN CREATE POLICY "mcp_oauth_clients_registration" ON "public"."mcp_oauth_clients" FOR INSERT WITH CHECK (current_setting('app.mcp_client_register', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_oauth_clients' AND policyname = 'mcp_oauth_clients_lookup' ) THEN CREATE POLICY "mcp_oauth_clients_lookup" ON "public"."mcp_oauth_clients" FOR SELECT USING (current_setting('app.mcp_client_lookup', true) = 'on'); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."mcp_authorization_codes" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- The user has consented by the time a code exists, so the organisation IS -- known and this table is tenant-isolated - unlike mcp_oauth_clients. "tenant_id" uuid NOT NULL, "grant_id" uuid NOT NULL, -- The client that will redeem it. No foreign key - see DECISION 12. "client_id" text NOT NULL, -- THE CODE, HASHED. Never the plaintext. See DECISION 4. "code_hash" text NOT NULL CONSTRAINT "mcp_authorization_codes_hash_format_check" CHECK ("code_hash" ~ '^[0-9a-f]{64}$'), -- PKCE (RFC 7636). The CHALLENGE is public by construction - it is already -- SHA-256 of the verifier and travels in the authorize request - so it is -- stored as sent and is not a secret. The verifier is the secret and this -- schema never sees it. "code_challenge" text NOT NULL CONSTRAINT "mcp_authorization_codes_challenge_not_blank_check" CHECK (length(btrim("code_challenge")) > 0), -- 'S256' ONLY, and NO DEFAULT. 'plain' is deliberately not in the set, and -- a missing method must not fall back to anything. See DECISION 5. "code_challenge_method" text NOT NULL CONSTRAINT "mcp_authorization_codes_challenge_method_check" CHECK ("code_challenge_method" IN ('S256')), -- Must match the redirect the code was issued for, per RFC 6749 4.1.3. "redirect_uri" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), -- Short-lived, and NOT NULL: a code with no expiry is a code that never -- stops being redeemable. "expires_at" timestamptz NOT NULL, -- SINGLE-USE. NULL means not yet consumed - a fact, so no default. The row -- is kept after consumption so a REPLAY is detectable rather than looking -- like an expired or forged code. See DECISION 6. "consumed_at" timestamptz NULL ); -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND indexname = 'mcp_authorization_codes_hash_key' ) THEN CREATE UNIQUE INDEX "mcp_authorization_codes_hash_key" ON "public"."mcp_authorization_codes" ("code_hash"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND indexname = 'mcp_authorization_codes_tenant_idx' ) THEN CREATE INDEX "mcp_authorization_codes_tenant_idx" ON "public"."mcp_authorization_codes" ("tenant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND indexname = 'mcp_authorization_codes_grant_idx' ) THEN CREATE INDEX "mcp_authorization_codes_grant_idx" ON "public"."mcp_authorization_codes" ("grant_id"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_authorization_codes'::regclass AND conname = 'mcp_authorization_codes_tenant_id_fkey' ) THEN ALTER TABLE "public"."mcp_authorization_codes" ADD CONSTRAINT "mcp_authorization_codes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_authorization_codes'::regclass AND conname = 'mcp_authorization_codes_grant_id_fkey' ) THEN ALTER TABLE "public"."mcp_authorization_codes" ADD CONSTRAINT "mcp_authorization_codes_grant_id_fkey" FOREIGN KEY ("grant_id") REFERENCES "public"."mcp_grants" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; END IF; END; $$; -> DO $$ BEGIN ALTER TABLE "public"."mcp_authorization_codes" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."mcp_authorization_codes" FORCE ROW LEVEL SECURITY; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND policyname = 'mcp_authorization_codes_tenant_isolation' ) THEN CREATE POLICY "mcp_authorization_codes_tenant_isolation" ON "public"."mcp_authorization_codes" FOR ALL USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND policyname = 'mcp_authorization_codes_lookup' ) THEN CREATE POLICY "mcp_authorization_codes_lookup" ON "public"."mcp_authorization_codes" FOR SELECT USING (current_setting('app.mcp_code_lookup', true) = 'on'); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'mcp_authorization_codes' AND policyname = 'mcp_authorization_codes_site_scope_select' ) THEN CREATE POLICY "mcp_authorization_codes_site_scope_select" ON "public"."mcp_authorization_codes" AS RESTRICTIVE FOR SELECT USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' ); END IF; END; $$; -> GRANT SELECT, INSERT, UPDATE, DELETE ON "public"."mcp_grants" TO wpmgr_app; -> GRANT SELECT, INSERT, UPDATE, DELETE ON "public"."mcp_connection_tokens" TO wpmgr_app; -> GRANT SELECT, INSERT, UPDATE, DELETE ON "public"."mcp_oauth_clients" TO wpmgr_app; -> GRANT SELECT, INSERT, UPDATE, DELETE ON "public"."mcp_authorization_codes" TO wpmgr_app; -- ok (3.415024042s) -- migrating version 20260827000000 -> DROP POLICY IF EXISTS "email_webhook_events_tenant_isolation" ON "public"."email_webhook_events"; -> CREATE POLICY "email_webhook_events_tenant_isolation" ON "public"."email_webhook_events" USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid); -- ok (397.921417ms) -- migrating version 20260828000000 -> ALTER TABLE "public"."mcp_oauth_clients" DROP COLUMN IF EXISTS "last_used_at"; -- ok (330.961417ms) -- migrating version 20260829000000 -> ALTER TABLE "public"."mcp_grants" ADD COLUMN IF NOT EXISTS "capabilities" text[]; -> ALTER TABLE "public"."mcp_grants" ADD COLUMN IF NOT EXISTS "expires_at" timestamptz; -> ALTER TABLE "public"."mcp_grants" ADD COLUMN IF NOT EXISTS "idle_expire_after_days" integer; -> UPDATE "public"."mcp_grants" SET "capabilities" = ARRAY['mcp.sites.read']::text[] WHERE "capabilities" IS NULL; -> UPDATE "public"."mcp_grants" SET "expires_at" = now() + interval '90 days' WHERE "expires_at" IS NULL; -> ALTER TABLE "public"."mcp_grants" ALTER COLUMN "capabilities" SET NOT NULL; -> ALTER TABLE "public"."mcp_grants" ALTER COLUMN "expires_at" SET NOT NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_capabilities_shape_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_capabilities_shape_check" CHECK ( coalesce(array_ndims("capabilities"), 1) = 1 AND cardinality("capabilities") <= 64 AND array_position("capabilities", NULL) IS NULL AND NOT ('' = ANY ("capabilities")) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_capabilities_vocabulary_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_capabilities_vocabulary_check" CHECK ("capabilities" <@ ARRAY['mcp.sites.read']::text[]); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_expires_at_after_created_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_expires_at_after_created_check" CHECK ("expires_at" > "created_at"); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_idle_expire_after_days_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_idle_expire_after_days_check" CHECK ( "idle_expire_after_days" IS NULL OR ("idle_expire_after_days" >= 1 AND "idle_expire_after_days" <= 3650) ); END IF; END; $$; -- ok (1.049282s) -- migrating version 20260830000000 -> ALTER TABLE "public"."mcp_grants" ADD COLUMN IF NOT EXISTS "setup_client" text NULL; -> COMMENT ON COLUMN "public"."mcp_grants"."setup_client" IS 'The AI client the operator chose at S29 step 2, as a client-table.ts slug. ' 'NULL means no operator choice was recorded -- NOT "generic", which is the ' 'distinct case of an operator actively choosing "Other MCP client". ' 'Distinct from client_name/client_version, which are self-reported by the ' 'client at initialize, and from client_id, which is an OAuth registration ' 'id. Never written by RecordConnect. See m128 DECISIONS 1, 2 and 4.'; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_setup_client_shape_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_setup_client_shape_check" CHECK ( "setup_client" IS NULL OR ("setup_client" ~ '^[a-z0-9]+(-[a-z0-9]+)*$' AND length("setup_client") <= 64) ); END IF; END; $$; -- ok (461.99225ms) -- migrating version 20260831000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_expires_at_max_one_year_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_expires_at_max_one_year_check" CHECK ( timezone('UTC', "expires_at") <= timezone('UTC', "created_at") + interval '1 year' ); END IF; END; $$; -- ok (335.867666ms) -- migrating version 20260901000000 -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "assistant_enabled_at" timestamptz; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "assistant_paused_at" timestamptz; -> ALTER TABLE "public"."tenants" ADD COLUMN IF NOT EXISTS "assistant_paused_reason" text; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.tenants'::regclass AND conname = 'tenants_assistant_paused_reason_check' ) THEN ALTER TABLE "public"."tenants" ADD CONSTRAINT "tenants_assistant_paused_reason_check" CHECK ( "assistant_paused_reason" IS NULL OR ("assistant_paused_at" IS NOT NULL AND length(btrim("assistant_paused_reason")) > 0 AND length("assistant_paused_reason") <= 500) ); END IF; END; $$; -- ok (567.792833ms) -- migrating version 20260902000000 -> ALTER TABLE "public"."mcp_grants" DROP CONSTRAINT IF EXISTS "mcp_grants_capabilities_vocabulary_check"; -> ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_capabilities_vocabulary_check" CHECK ("capabilities" <@ ARRAY[ 'mcp.activity.read', 'mcp.backups.read', 'mcp.content.read', 'mcp.diagnostics.read', 'mcp.performance.read', 'mcp.security.read', 'mcp.sites.read', 'mcp.uptime.read' ]::text[]); -- ok (412.043291ms) -- migrating version 20260903000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_perf_config' AND policyname = 'site_perf_config_site_scope' ) THEN CREATE POLICY "site_perf_config_site_scope" ON "public"."site_perf_config" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_stats' AND policyname = 'site_cache_stats_site_scope' ) THEN CREATE POLICY "site_cache_stats_site_scope" ON "public"."site_cache_stats" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_cache_hit_ratio_history' AND policyname = 'site_cache_hit_ratio_history_site_scope' ) THEN CREATE POLICY "site_cache_hit_ratio_history_site_scope" ON "public"."site_cache_hit_ratio_history" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_config' AND policyname = 'site_object_cache_config_site_scope' ) THEN CREATE POLICY "site_object_cache_config_site_scope" ON "public"."site_object_cache_config" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_object_cache_stats_history' AND policyname = 'site_object_cache_stats_history_site_scope' ) THEN CREATE POLICY "site_object_cache_stats_history_site_scope" ON "public"."site_object_cache_stats_history" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'cache_purge_audit' AND policyname = 'cache_purge_audit_site_scope' ) THEN CREATE POLICY "cache_purge_audit_site_scope" ON "public"."cache_purge_audit" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_jobs' AND policyname = 'rucss_jobs_site_scope' ) THEN CREATE POLICY "rucss_jobs_site_scope" ON "public"."rucss_jobs" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rucss_results' AND policyname = 'rucss_results_site_scope' ) THEN CREATE POLICY "rucss_results_site_scope" ON "public"."rucss_results" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_results' AND policyname = 'font_results_site_scope' ) THEN CREATE POLICY "font_results_site_scope" ON "public"."font_results" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'font_transcode_results' AND policyname = 'font_transcode_results_site_scope' ) THEN CREATE POLICY "font_transcode_results_site_scope" ON "public"."font_transcode_results" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_events_raw' AND policyname = 'rum_events_raw_site_scope' ) THEN CREATE POLICY "rum_events_raw_site_scope" ON "public"."rum_events_raw" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_hourly' AND policyname = 'rum_rollup_hourly_site_scope' ) THEN CREATE POLICY "rum_rollup_hourly_site_scope" ON "public"."rum_rollup_hourly" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'rum_rollup_daily' AND policyname = 'rum_rollup_daily_site_scope' ) THEN CREATE POLICY "rum_rollup_daily_site_scope" ON "public"."rum_rollup_daily" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_db_size_history' AND policyname = 'site_db_size_history_site_scope' ) THEN CREATE POLICY "site_db_size_history_site_scope" ON "public"."site_db_size_history" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_db_scan_results' AND policyname = 'site_db_scan_results_site_scope' ) THEN CREATE POLICY "site_db_scan_results_site_scope" ON "public"."site_db_scan_results" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_db_clean_results' AND policyname = 'site_db_clean_results_site_scope' ) THEN CREATE POLICY "site_db_clean_results_site_scope" ON "public"."site_db_clean_results" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_vulnerabilities' AND policyname = 'site_vulnerabilities_site_scope' ) THEN CREATE POLICY "site_vulnerabilities_site_scope" ON "public"."site_vulnerabilities" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy' AND policyname = 'site_security_policy_site_scope' ) THEN CREATE POLICY "site_security_policy_site_scope" ON "public"."site_security_policy" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_security_policy_groups' AND policyname = 'site_security_policy_groups_site_scope' ) THEN CREATE POLICY "site_security_policy_groups_site_scope" ON "public"."site_security_policy_groups" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_events' AND policyname = 'site_events_site_scope' ) THEN CREATE POLICY "site_events_site_scope" ON "public"."site_events" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_connection_history' AND policyname = 'site_connection_history_site_scope' ) THEN CREATE POLICY "site_connection_history_site_scope" ON "public"."site_connection_history" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'site_app_alert_state' AND policyname = 'site_app_alert_state_site_scope' ) THEN CREATE POLICY "site_app_alert_state_site_scope" ON "public"."site_app_alert_state" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -- ok (1.941990792s) -- migrating version 20260904000000 -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sites_tenant_id_id_key' AND conrelid = 'public.sites'::regclass ) THEN ALTER TABLE "public"."sites" ADD CONSTRAINT "sites_tenant_id_id_key" UNIQUE ("tenant_id", "id"); END IF; END; $$; -> CREATE TABLE IF NOT EXISTS "public"."assistant_update_proposals" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- Tenancy and site key side by side, per DECISION 1(a). -- -- THE PAIR IS BOUND, NOT JUST THE HALVES. See DECISION 10. Two independent -- foreign keys would each be satisfied by a real tenant and a real site -- while saying nothing about whether the site belongs to the tenant. The -- composite FK below is what makes "this tenant's proposal about that -- tenant's site" a row PostgreSQL will not store. "tenant_id" uuid NOT NULL REFERENCES "public"."tenants" ("id") ON DELETE CASCADE, "site_id" uuid NOT NULL, -- ON DELETE CASCADE: deleting the site deletes its proposals, and deleting -- a tenant reaches them the same way, because sites.tenant_id cascades from -- tenants. Asked as m113/m116 require -- what audit or reclaim record dies -- with this cascade? Nothing that outlives the site; a proposal reserves no -- object storage and names no external resource. -- -- ON UPDATE is deliberately left at NO ACTION. Nothing moves a site between -- tenants, and if anything ever tries while proposals exist, the right -- outcome is a loud refusal rather than a silent rewrite of tenant_id -- -- which section (5) makes immutable to every ordinary writer, and which a -- referential action would change anyway, since those run with the table -- owner's rights and do not consult column privileges. CONSTRAINT "assistant_update_proposals_site_within_tenant_fkey" FOREIGN KEY ("tenant_id", "site_id") REFERENCES "public"."sites" ("tenant_id", "id") ON DELETE CASCADE, -- WHO ASKED. No FK, per DECISION 7. "proposed_by_grant_id" uuid NOT NULL, -- WHAT IS PROPOSED. Scalars only, per DECISION 2. "component_type" text NOT NULL CONSTRAINT "assistant_update_proposals_component_type_check" CHECK ("component_type" IN ('plugin')), "component_slug" text NOT NULL CONSTRAINT "assistant_update_proposals_component_slug_not_blank_check" CHECK (length(btrim("component_slug")) > 0), "from_version" text NOT NULL CONSTRAINT "assistant_update_proposals_from_version_not_blank_check" CHECK (length(btrim("from_version")) > 0), "to_version" text NOT NULL CONSTRAINT "assistant_update_proposals_to_version_not_blank_check" CHECK (length(btrim("to_version")) > 0), CONSTRAINT "assistant_update_proposals_version_changes_check" CHECK ("from_version" <> "to_version"), -- THE FINGERPRINT. DECISION 6. SHA-256, lowercase hex, set at INSERT. "presented_digest" text NOT NULL CONSTRAINT "assistant_update_proposals_presented_digest_shape_check" CHECK ("presented_digest" ~ '^[0-9a-f]{64}$'), -- THE ONE QUARANTINED PROPOSER-CONTROLLED FREE TEXT, ADR-061 Decision 3. -- Every other column on this table is control-plane-derived or a closed -- enum. Excluded from the digest on purpose. Bounded so a model cannot -- push a wall of text onto a decision surface; the renderer still has to -- treat it as hostile, which is Session C's two text cleaners, not this -- file's job. "note" text NULL CONSTRAINT "assistant_update_proposals_note_length_check" CHECK ("note" IS NULL OR length("note") <= 2000), -- THE STATE MACHINE. DECISION 3. Closed set, NOT NULL, NO DEFAULT: a -- DEFAULT here would mean a caller that forgot to say produces a row in -- whichever state the default names. "state" text NOT NULL CONSTRAINT "assistant_update_proposals_state_check" CHECK ("state" IN ( 'pending', 'approved_undispatched', 'dispatched', 'rejected', 'expired' )), "created_at" timestamptz NOT NULL DEFAULT now(), -- THE WINDOW. NOT NULL, NO DEFAULT: a proposal with no expiry is one that -- waits forever, and a stale ask approved months later is the surprise the -- window exists to prevent. "expires_at" timestamptz NOT NULL, CONSTRAINT "assistant_update_proposals_window_is_positive_check" CHECK ("expires_at" > "created_at"), -- THE DECISION. "decided_at" timestamptz NULL, -- THE APPROVER. A RECORDED FACT, NOT A FOREIGN KEY. See DECISION 7. "decided_by_user_id" uuid NULL, -- A decided state has a decision time. Without this, 'rejected' with a NULL -- decided_at is representable and the audit trail cannot say when. CONSTRAINT "assistant_update_proposals_decided_states_have_time_check" CHECK ( ("state" IN ('approved_undispatched', 'dispatched', 'rejected')) = ("decided_at" IS NOT NULL) ), -- RUNNING OUT OF TIME IS NEVER CONSENT. DECISION 4. CONSTRAINT "assistant_update_proposals_consent_within_window_check" CHECK ( "state" NOT IN ('approved_undispatched', 'dispatched') OR ("decided_at" IS NOT NULL AND "decided_at" < "expires_at") ), -- Expiry never names a human. DECISION 4, second half. CONSTRAINT "assistant_update_proposals_expiry_is_not_a_decision_check" CHECK ("state" <> 'expired' OR "decided_by_user_id" IS NULL), -- AN APPROVAL NAMES A HUMAN, OR IT IS NOT AN APPROVAL. DECISION 8. -- This is the constraint that makes "a machine approved this" a state the -- database refuses rather than a rule a handler remembers. CONSTRAINT "assistant_update_proposals_approval_names_a_human_check" CHECK ( "state" NOT IN ('approved_undispatched', 'dispatched') OR "decided_by_user_id" IS NOT NULL ), -- A REJECTION NAMES ITS HUMAN TOO. DECISION 9. -- Every decided state that is not expiry names the person who decided it. -- Expiry is the one decided-looking state with no human, and it is not a -- decision -- the constraint above this one forbids it naming anyone. -- -- This is not symmetry for its own sake. decided_by_user_id is inside the -- column UPDATE grant section (5) hands back, because an approval has to be -- able to write it. Without this constraint, -- -- SET state = 'rejected', decided_by_user_id = NULL -- -- is a legal statement against an already-approved row, and it erases who -- approved while leaving a plausible decided row behind. A review executed -- exactly that. With this constraint that statement raises 23514: a -- rejection cannot be anonymous, so there is no decided state a writer can -- move an approved row into that drops the name. -- -- WHAT THIS DOES NOT CLOSE, stated so the next reader does not read more -- into it than it says: it guarantees a decided row NAMES someone. It does -- not guarantee the someone it names is the someone who decided -- the same -- limit DECISION 8 already states for approvals, for the same reason. A -- CHECK sees only the finished row. CONSTRAINT "assistant_update_proposals_rejection_names_a_human_check" CHECK ( "state" <> 'rejected' OR "decided_by_user_id" IS NOT NULL ), -- THE HANDOFF. Set by the dispatch worker in the same statement that moves -- the row to 'dispatched', so "dispatched" and "here is the run" are one -- fact and cannot disagree. No FK, same recorded-fact reasoning as -- DECISION 7: an update run may be pruned, and pruning it must not erase -- that this proposal was acted on. "dispatched_update_run_id" uuid NULL, CONSTRAINT "assistant_update_proposals_dispatch_pointer_check" CHECK (("state" = 'dispatched') = ("dispatched_update_run_id" IS NOT NULL)) ); -> CREATE INDEX IF NOT EXISTS "assistant_update_proposals_tenant_idx" ON "public"."assistant_update_proposals" ("tenant_id"); -> CREATE INDEX IF NOT EXISTS "assistant_update_proposals_site_idx" ON "public"."assistant_update_proposals" ("site_id"); -> CREATE INDEX IF NOT EXISTS "assistant_update_proposals_dispatch_idx" ON "public"."assistant_update_proposals" ("decided_at") WHERE "state" = 'approved_undispatched'; -> CREATE INDEX IF NOT EXISTS "assistant_update_proposals_expiry_sweep_idx" ON "public"."assistant_update_proposals" ("expires_at") WHERE "state" = 'pending'; -> CREATE UNIQUE INDEX IF NOT EXISTS "assistant_update_proposals_one_live_per_component_idx" ON "public"."assistant_update_proposals" ("tenant_id", "site_id", "component_type", "component_slug") WHERE "state" = 'pending'; -> ALTER TABLE "public"."assistant_update_proposals" ENABLE ROW LEVEL SECURITY; -> ALTER TABLE "public"."assistant_update_proposals" FORCE ROW LEVEL SECURITY; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'assistant_update_proposals' AND policyname = 'assistant_update_proposals_tenant_isolation' ) THEN CREATE POLICY "assistant_update_proposals_tenant_isolation" ON "public"."assistant_update_proposals" USING ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid) WITH CHECK ("tenant_id" = nullif(current_setting('app.tenant_id', true), '')::uuid); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'assistant_update_proposals' AND policyname = 'assistant_update_proposals_site_scope' ) THEN CREATE POLICY "assistant_update_proposals_site_scope" ON "public"."assistant_update_proposals" AS RESTRICTIVE FOR ALL USING ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ) WITH CHECK ( coalesce(current_setting('app.site_scope', true), '') <> 'on' OR "site_id" = ANY ( string_to_array( nullif(current_setting('app.allowed_site_ids', true), ''), ',' )::uuid[] ) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'assistant_update_proposals' AND policyname = 'assistant_update_proposals_agent' ) THEN CREATE POLICY "assistant_update_proposals_agent" ON "public"."assistant_update_proposals" FOR SELECT USING (current_setting('app.agent', true) = 'on'); END IF; END; $$; -> REVOKE DELETE, TRUNCATE ON "public"."assistant_update_proposals" FROM "wpmgr_app"; -> REVOKE UPDATE ON "public"."assistant_update_proposals" FROM "wpmgr_app"; -> GRANT UPDATE ("state", "decided_at", "decided_by_user_id", "dispatched_update_run_id", "note") ON "public"."assistant_update_proposals" TO "wpmgr_app"; -- ok (1.444544208s) -- migrating version 20260906000000 -> ALTER TABLE "public"."mcp_grants" DROP CONSTRAINT IF EXISTS "mcp_grants_capabilities_vocabulary_check"; -> ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_capabilities_vocabulary_check" CHECK ("capabilities" <@ ARRAY[ 'mcp.activity.read', 'mcp.backups.read', 'mcp.cache.purge', 'mcp.content.read', 'mcp.diagnostics.read', 'mcp.performance.read', 'mcp.security.read', 'mcp.sites.read', 'mcp.uptime.read' ]::text[]); -- ok (427.36025ms) -- migrating version 20260907000000 -> ALTER TABLE "public"."mcp_grants" ADD COLUMN IF NOT EXISTS "oauth_scopes" text[]; -> UPDATE "public"."mcp_grants" SET "oauth_scopes" = ARRAY['mcp:read']::text[] WHERE "oauth_scopes" IS NULL; -> ALTER TABLE "public"."mcp_grants" ALTER COLUMN "oauth_scopes" SET NOT NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_oauth_scopes_shape_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_oauth_scopes_shape_check" CHECK ( coalesce(array_ndims("oauth_scopes"), 1) = 1 AND cardinality("oauth_scopes") <= 16 AND array_position("oauth_scopes", NULL) IS NULL AND NOT ('' = ANY ("oauth_scopes")) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_oauth_scopes_not_empty_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_oauth_scopes_not_empty_check" CHECK (cardinality("oauth_scopes") >= 1); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_grants'::regclass AND conname = 'mcp_grants_oauth_scopes_vocabulary_check' ) THEN ALTER TABLE "public"."mcp_grants" ADD CONSTRAINT "mcp_grants_oauth_scopes_vocabulary_check" CHECK ("oauth_scopes" <@ ARRAY['mcp:read']::text[]); END IF; END; $$; -- ok (757.502542ms) -- migrating version 20260908000000 -> ALTER TABLE "public"."mcp_oauth_clients" ADD COLUMN IF NOT EXISTS "registered_scopes" text[]; -> UPDATE "public"."mcp_oauth_clients" SET "registered_scopes" = ARRAY['mcp:read']::text[] WHERE "registered_scopes" IS NULL; -> ALTER TABLE "public"."mcp_oauth_clients" ALTER COLUMN "registered_scopes" SET NOT NULL; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_oauth_clients'::regclass AND conname = 'mcp_oauth_clients_registered_scopes_shape_check' ) THEN ALTER TABLE "public"."mcp_oauth_clients" ADD CONSTRAINT "mcp_oauth_clients_registered_scopes_shape_check" CHECK ( coalesce(array_ndims("registered_scopes"), 1) = 1 AND cardinality("registered_scopes") <= 16 AND array_position("registered_scopes", NULL) IS NULL AND NOT ('' = ANY ("registered_scopes")) ); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_oauth_clients'::regclass AND conname = 'mcp_oauth_clients_registered_scopes_present_check' ) THEN ALTER TABLE "public"."mcp_oauth_clients" ADD CONSTRAINT "mcp_oauth_clients_registered_scopes_present_check" CHECK (cardinality("registered_scopes") >= 1); END IF; END; $$; -> DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.mcp_oauth_clients'::regclass AND conname = 'mcp_oauth_clients_registered_scopes_vocabulary_check' ) THEN ALTER TABLE "public"."mcp_oauth_clients" ADD CONSTRAINT "mcp_oauth_clients_registered_scopes_vocabulary_check" CHECK ("registered_scopes" <@ ARRAY['mcp:read']::text[]); END IF; END; $$; -- ok (496.304917ms) ------------------------- -- 1m55.048371542s -- 145 migrations -- 1078 sql statements