From 982a9e295459199a50eb8ade4ad26650dc107129 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Tue, 22 Sep 2026 21:41:58 +0200 Subject: [PATCH] chore: Dokploy helper to merge Superhuman Docs API key Keeps existing portal env vars and triggers deploy after merge. Co-authored-by: Cursor --- scripts/dokploy_set_superhuman_env.py | 177 ++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 scripts/dokploy_set_superhuman_env.py diff --git a/scripts/dokploy_set_superhuman_env.py b/scripts/dokploy_set_superhuman_env.py new file mode 100644 index 0000000..a8b1044 --- /dev/null +++ b/scripts/dokploy_set_superhuman_env.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Merge SUPERHUMAN_DOCS_API_KEY into client-portal Dokploy env, then Deploy.""" +from __future__ import annotations + +import json +import re +import sys +import time +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" +APP_ID = "NZS797t9Rnq9Pj4mHiii3" +APP = f"{BASE}/dashboard/project/Z_4M-uDOH1_--itGSTh4l/environment/bGja70l_a7HQ1dxyXqSuf/services/application/{APP_ID}" +HEALTH = "https://app.4ventures.es/api/health" + + +def cred(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(encoding="utf-8").splitlines(): + if line.startswith(f"{key}="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return "" + + +def local_key() -> str: + for line in (ROOT / ".env.local").read_text(encoding="utf-8").splitlines(): + if line.startswith("SUPERHUMAN_DOCS_API_KEY="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + p = ROOT / ".ops" / "superhuman-docs-api-key.txt" + if p.is_file(): + return p.read_text(encoding="utf-8").strip() + return "" + + +def parse_env(text: str) -> dict[str, str]: + out: dict[str, str] = {} + for line in (text or "").splitlines(): + if not line.strip() or line.strip().startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def dump_env(vals: dict[str, str]) -> str: + return "\n".join(f"{k}={v}" for k, v in vals.items()) + "\n" + + +def main() -> int: + sh_key = local_key() + if not sh_key: + print("missing SUPERHUMAN_DOCS_API_KEY locally", flush=True) + return 2 + email = cred("DOKPLOY_EMAIL") or "marketing@4ventures.es" + password = cred("DOKPLOY_PASSWORD") + + 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(600) + 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(APP, wait_until="networkidle", timeout=120000) + page.wait_for_timeout(800) + + result = page.evaluate( + """async ({ appId, shKey }) => { + const oneUrl = '/api/trpc/application.one?batch=1&input=' + encodeURIComponent(JSON.stringify({ '0': { json: { applicationId: appId } } })); + const one = await fetch(oneUrl, { credentials: 'include' }); + const oneText = await one.text(); + if (!one.ok) return { ok: false, step: 'one', status: one.status, body: oneText.slice(0, 400) }; + let envText = ''; + try { + const j = JSON.parse(oneText); + const data = j[0]?.result?.data?.json || j[0]?.result?.data || j; + envText = data?.env || data?.environment || ''; + if (typeof envText !== 'string') envText = ''; + } catch (e) { + return { ok: false, step: 'parse', error: String(e), body: oneText.slice(0, 400) }; + } + const lines = envText.split('\\n').filter(Boolean); + const map = {}; + for (const line of lines) { + const i = line.indexOf('='); + if (i < 0) continue; + map[line.slice(0, i)] = line.slice(i + 1); + } + map['SUPERHUMAN_DOCS_API_KEY'] = shKey; + const merged = Object.keys(map).map(k => k + '=' + map[k]).join('\\n') + '\\n'; + const keys = Object.keys(map); + + const tries = []; + const payloads = [ + { path: '/api/application.update', body: { applicationId: appId, env: merged } }, + { path: '/api/trpc/application.update?batch=1', body: { '0': { json: { applicationId: appId, env: merged } } } }, + { path: '/api/trpc/application.saveEnvironment?batch=1', body: { '0': { json: { applicationId: appId, env: merged } } } }, + ]; + for (const t of payloads) { + const r = await fetch(t.path, { + method: 'POST', + credentials: 'include', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(t.body), + }); + const text = await r.text(); + tries.push({ path: t.path, status: r.status, body: text.slice(0, 180) }); + if (r.ok) return { ok: true, keys, tries, hadKey: keys.includes('SUPERHUMAN_DOCS_API_KEY') }; + } + return { ok: false, keys, tries, envLen: envText.length }; + }""", + {"appId": APP_ID, "shKey": sh_key}, + ) + print( + json.dumps( + { + "ok": result.get("ok"), + "hadKey": result.get("hadKey"), + "keyCount": len(result.get("keys") or []), + "tries": result.get("tries"), + "step": result.get("step"), + "status": result.get("status"), + }, + indent=2, + )[:2000], + flush=True, + ) + if not result.get("ok"): + browser.close() + return 6 + + # Deploy + page.goto(APP, wait_until="domcontentloaded", timeout=90000) + page.wait_for_timeout(700) + page.get_by_role("tab", name=re.compile(r"^General$", re.I)).first.click() + page.wait_for_timeout(500) + page.get_by_role("button", name=re.compile(r"^Deploy$", re.I)).first.click() + page.wait_for_timeout(600) + conf = page.get_by_role("button", name=re.compile(r"^Confirm$", re.I)) + if conf.count(): + conf.first.click() + print("DEPLOY_CLICKED", flush=True) + + for i in range(24): + time.sleep(10) + try: + import urllib.request + + code = urllib.request.urlopen(HEALTH, timeout=12).getcode() + except Exception as e: + code = str(e) + print(f"health_try{i}", code, flush=True) + if code == 200: + break + + browser.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())