#!/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())