Files
4ventures-client-portal/scripts/dokploy_ensure_pipeline_publish_cron.py
4Ventures 83de3fb3aa Fix calendar drag snap-back, IG filter, and default to 2026.
Prefer the latest year in the archive, normalize Instagram casing so stories show under IG/FB, sync pipeline dates on move, and stop stale plan re-renders from undoing a successful drag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 15:29:49 +02:00

135 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Ensure / refresh Dokploy schedule for pipeline Meta publish (every 5 min)."""
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 = "pipeline-publish"
CRON_EXPR = "*/5 * * * *"
# Runs inside the app container so $CRON_SECRET from Dokploy env is available.
CMD = (
"bash -lc "
"'curl -sS -H \"Authorization: Bearer $CRON_SECRET\" "
"\"https://app.4ventures.es/api/cron/pipeline-publish?limit=20\"'"
)
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 };
};
// List application schedules (Dokploy v0.30)
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 };
}""",
{
"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())