#!/usr/bin/env python3 """Ensure Dokploy schedule for daily Clientify CRM reconciliation.""" from __future__ import annotations import json 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" APP_ID = "NZS797t9Rnq9Pj4mHiii3" SCHEDULE_NAME = "clientify-sync" # 05:15 UTC daily — before typical morning review in Spain (07:15) CRON_EXPR = "15 5 * * *" CMD = ( "bash -lc " "'curl -sS -H \"Authorization: Bearer $CRON_SECRET\" " "\"https://app.4ventures.es/api/cron/clientify-sync?lookback=60\"'" ) def cred(key: str) -> str: import os v = os.environ.get(key, "").strip() if v: return v for p in (BPS / ".env", ROOT / ".env.local"): if not p.is_file(): continue 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 main() -> int: email = cred("DOKPLOY_EMAIL") or "marketing@4ventures.es" password = cred("DOKPLOY_PASSWORD") if not password: print("missing DOKPLOY_PASSWORD", flush=True) return 2 with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1500, "height": 1200}) 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) result = page.evaluate( """async ({ appId, name, cron, command }) => { const post = async (path, body) => { const r = await fetch(path, { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const text = await r.text(); let json = null; try { json = JSON.parse(text); } catch {} return { ok: r.ok, status: r.status, text: text.slice(0, 1200), json }; }; const get = async (path) => { const r = await fetch(path, { credentials: 'include' }); const text = await r.text(); let json = null; try { json = JSON.parse(text); } catch {} return { ok: r.ok, status: r.status, text: text.slice(0, 2000), json }; }; const listed = await get( '/api/trpc/schedule.list?batch=1&input=' + encodeURIComponent(JSON.stringify({ '0': { json: { id: appId, scheduleType: 'application' } } })) ); const rows = listed.json?.[0]?.result?.data?.json || listed.json?.[0]?.result?.data || []; const existing = Array.isArray(rows) ? rows.filter((r) => (r?.name || '') === name) : []; for (const row of existing) { const sid = row.scheduleId || row.id; if (!sid) continue; await post('/api/trpc/schedule.delete?batch=1', { '0': { json: { scheduleId: sid } }, }); } const created = await post('/api/trpc/schedule.create?batch=1', { '0': { json: { name, cronExpression: cron, shellType: 'bash', command, applicationId: appId, scheduleType: 'application', enabled: true, }, }, }); return { deleted: existing.length, created, listedCount: Array.isArray(rows) ? rows.length : 0 }; }""", { "appId": APP_ID, "name": SCHEDULE_NAME, "cron": CRON_EXPR, "command": CMD, }, ) print(json.dumps(result, indent=2)[:4000], flush=True) ok = bool(result.get("created", {}).get("ok")) browser.close() return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main())