From c8608f36f5dc566325e67acd3c5b7160939c76d7 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Tue, 13 Oct 2026 16:49:18 +0200 Subject: [PATCH] Add Dokploy scripts to apply and verify unified approval_status SQL. Co-authored-by: Cursor --- .../dokploy_apply_approval_status_unify.py | 133 +++++++++++++++++ scripts/dokploy_verify_approval_status.py | 134 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 scripts/dokploy_apply_approval_status_unify.py create mode 100644 scripts/dokploy_verify_approval_status.py diff --git a/scripts/dokploy_apply_approval_status_unify.py b/scripts/dokploy_apply_approval_status_unify.py new file mode 100644 index 0000000..3e4a869 --- /dev/null +++ b/scripts/dokploy_apply_approval_status_unify.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Apply approval_status_unify.sql + generation_requests_copy_v2.sql via Dokploy supabase-db.""" +from __future__ import annotations + +import base64 +import re +import sys +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path("/Users/4ventures/4ventures-client-portal") +BPS = Path("/Users/4ventures/Projects/bps-facturacion") +BASE = "https://dokploy.4ventures.es" +SQLS = [ + ROOT / "sql" / "approval_status_unify.sql", + ROOT / "sql" / "generation_requests_copy_v2.sql", +] + + +def env(key: str) -> str: + import os + + v = os.environ.get(key, "").strip() + if v: + return v + p = BPS / ".env" + if not p.is_file(): + return "" + for line in p.read_text().splitlines(): + if line.startswith(f"{key}="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return "" + + +def main() -> int: + email = env("DOKPLOY_EMAIL") or "marketing@4ventures.es" + password = env("DOKPLOY_PASSWORD") + if not password: + print("missing DOKPLOY_PASSWORD", flush=True) + return 1 + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={"width": 1400, "height": 1000}) + page.goto(BASE + "/", wait_until="domcontentloaded", timeout=90000) + page.wait_for_timeout(800) + if "/dashboard" not in page.url: + page.locator('input[type="email"], input[name="email"]').first.fill(email) + page.locator('input[type="password"]').first.fill(password) + page.locator('button[type="submit"]').first.click() + page.wait_for_url(re.compile(r"/dashboard"), timeout=90000) + print("login ok", flush=True) + + page.goto( + f"{BASE}/dashboard/docker", wait_until="domcontentloaded", timeout=120000 + ) + page.wait_for_timeout(800) + page.locator( + "input[placeholder*='ilter' i], input[placeholder*='earch' i]" + ).first.fill("supabase-db") + page.wait_for_timeout(1000) + page.keyboard.press("Escape") + page.wait_for_timeout(200) + page.get_by_role("button", name=re.compile(r"Open menu", re.I)).first.click() + page.wait_for_timeout(400) + page.get_by_text("Terminal", exact=True).click() + page.wait_for_timeout(1000) + if page.locator("button", has_text="Bash").count(): + page.locator("button", has_text="Bash").first.click() + page.wait_for_timeout(2800) + if page.locator("textarea.xterm-helper-textarea").count(): + page.locator("textarea.xterm-helper-textarea").first.click() + + def run(cmd: str, wait: int = 800) -> None: + page.keyboard.type(cmd, delay=1) + page.keyboard.press("Enter") + page.wait_for_timeout(wait) + + all_ok = True + for idx, sql_path in enumerate(SQLS): + tag = f"m{idx}" + print(f"APPLY {sql_path.name}", flush=True) + b64 = base64.b64encode(sql_path.read_text(encoding="utf-8").encode()).decode() + chunks = [b64[i : i + 180] for i in range(0, len(b64), 180)] + run(f"rm -f /tmp/{tag}.b64 /tmp/{tag}.sql; echo READY_{tag}", 1200) + for i, ch in enumerate(chunks): + op = ">" if i == 0 else ">>" + run(f"printf '%s' '{ch}' {op} /tmp/{tag}.b64", 350) + run( + f"base64 -d /tmp/{tag}.b64 > /tmp/{tag}.sql && " + f"psql -U postgres -d postgres -v ON_ERROR_STOP=1 -f /tmp/{tag}.sql; " + f"echo EXIT_{tag}:$?", + 18000, + ) + + # Verify constraints / sample statuses + run( + "psql -U postgres -d postgres -c \"" + "select conname, pg_get_constraintdef(oid) " + "from pg_constraint " + "where conname in (" + "'creative_pipeline_status_check'," + "'creatives_status_check'," + "'generation_requests_reason_check'" + ");\" ; echo VERIFY_DONE", + 8000, + ) + run( + "psql -U postgres -d postgres -c \"" + "select status, count(*) from public.creative_pipeline group by 1 order by 1;\" " + "; echo PIPE_STATUSES", + 6000, + ) + + text = page.evaluate( + "() => (document.querySelector('.xterm-rows')||{}).innerText||''" + ) + browser.close() + + for idx, sql_path in enumerate(SQLS): + tag = f"m{idx}" + ok = f"EXIT_{tag}:0" in text + print(f"SQL_OK {sql_path.name}", ok, flush=True) + all_ok = all_ok and ok + + print("--- terminal tail ---", flush=True) + print(text[-2500:], flush=True) + return 0 if all_ok else 6 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dokploy_verify_approval_status.py b/scripts/dokploy_verify_approval_status.py new file mode 100644 index 0000000..f4c2698 --- /dev/null +++ b/scripts/dokploy_verify_approval_status.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Verify + re-apply approval_status_unify.sql if needed via Dokploy supabase-db.""" +from __future__ import annotations + +import base64 +import re +import sys +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path("/Users/4ventures/4ventures-client-portal") +BPS = Path("/Users/4ventures/Projects/bps-facturacion") +BASE = "https://dokploy.4ventures.es" +SQL = ROOT / "sql" / "approval_status_unify.sql" + + +def env(key: str) -> str: + import os + + v = os.environ.get(key, "").strip() + if v: + return v + p = BPS / ".env" + if not p.is_file(): + return "" + for line in p.read_text().splitlines(): + if line.startswith(f"{key}="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return "" + + +def main() -> int: + email = env("DOKPLOY_EMAIL") or "marketing@4ventures.es" + password = env("DOKPLOY_PASSWORD") + if not password: + print("missing DOKPLOY_PASSWORD", flush=True) + return 1 + + b64 = base64.b64encode(SQL.read_text(encoding="utf-8").encode()).decode() + chunks = [b64[i : i + 180] for i in range(0, len(b64), 180)] + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page(viewport={"width": 1400, "height": 1000}) + page.goto(BASE + "/", wait_until="domcontentloaded", timeout=90000) + page.wait_for_timeout(800) + if "/dashboard" not in page.url: + page.locator('input[type="email"], input[name="email"]').first.fill(email) + page.locator('input[type="password"]').first.fill(password) + page.locator('button[type="submit"]').first.click() + page.wait_for_url(re.compile(r"/dashboard"), timeout=90000) + print("login ok", flush=True) + + page.goto( + f"{BASE}/dashboard/docker", wait_until="domcontentloaded", timeout=120000 + ) + page.wait_for_timeout(800) + page.locator( + "input[placeholder*='ilter' i], input[placeholder*='earch' i]" + ).first.fill("supabase-db") + page.wait_for_timeout(1000) + page.keyboard.press("Escape") + page.wait_for_timeout(200) + page.get_by_role("button", name=re.compile(r"Open menu", re.I)).first.click() + page.wait_for_timeout(400) + page.get_by_text("Terminal", exact=True).click() + page.wait_for_timeout(1000) + if page.locator("button", has_text="Bash").count(): + page.locator("button", has_text="Bash").first.click() + page.wait_for_timeout(2800) + if page.locator("textarea.xterm-helper-textarea").count(): + page.locator("textarea.xterm-helper-textarea").first.click() + + def run(cmd: str, wait: int = 800) -> None: + page.keyboard.type(cmd, delay=1) + page.keyboard.press("Enter") + page.wait_for_timeout(wait) + + # Disable pager + run("export PAGER=cat; export LESS=-FX; echo PAGER_OK", 1000) + + # Re-apply idempotent migration + run("rm -f /tmp/asu.b64 /tmp/asu.sql; echo READY", 1000) + for i, ch in enumerate(chunks): + op = ">" if i == 0 else ">>" + run(f"printf '%s' '{ch}' {op} /tmp/asu.b64", 300) + run( + "base64 -d /tmp/asu.b64 > /tmp/asu.sql && " + "psql -U postgres -d postgres -v ON_ERROR_STOP=1 -P pager=off -f /tmp/asu.sql; " + "echo EXIT_ASU:$?", + 20000, + ) + + # Compact verifies + run( + "psql -U postgres -d postgres -P pager=off -tAc " + "\"select string_agg(conname, ',' order by conname) from pg_constraint " + "where conname in (" + "'creative_pipeline_status_check'," + "'creatives_status_check'," + "'generation_requests_reason_check'" + ");\" ; echo CONS:$?", + 8000, + ) + run( + "psql -U postgres -d postgres -P pager=off -tAc " + "\"select string_agg(status||':'||c, ',' order by status) from (" + "select status, count(*)::text c from public.creative_pipeline group by 1) s;\" " + "; echo PIPE:$?", + 6000, + ) + run( + "psql -U postgres -d postgres -P pager=off -tAc " + "\"select string_agg(status||':'||c, ',' order by status) from (" + "select status, count(*)::text c from public.creatives group by 1) s;\" " + "; echo ADS:$?", + 6000, + ) + + text = page.evaluate( + "() => (document.querySelector('.xterm-rows')||{}).innerText||''" + ) + browser.close() + + ok = "EXIT_ASU:0" in text + print("SQL_OK", ok, flush=True) + print("--- tail ---", flush=True) + print(text[-2000:], flush=True) + return 0 if ok else 6 + + +if __name__ == "__main__": + raise SystemExit(main())