Harden scheduled Meta publish and auto-link Drive for pipeline creatives.

Schedule save auto-approves and publishes when due; cron recovers stale jobs every 5m. Drive sync auto-matches client folders under Clientes when not linked.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-07 13:33:41 +02:00
parent 0d65614518
commit 26dc411647
9 changed files with 531 additions and 415 deletions

View File

@@ -1,14 +1,10 @@
#!/usr/bin/env python3
"""
Ensure Dokploy schedule for pipeline Meta publish (every 10 min).
Uses application.createSchedule / findAll if API available; falls back to UI.
"""
"""Ensure / refresh Dokploy schedule for pipeline Meta publish (every 5 min)."""
from __future__ import annotations
import json
import re
import sys
import time
from pathlib import Path
from playwright.sync_api import sync_playwright
@@ -17,18 +13,13 @@ 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"
f"/environment/bGja70l_a7HQ1dxyXqSuf/services/application/{APP_ID}"
)
SCHEDULE_NAME = "pipeline-publish"
CRON_EXPR = "*/10 * * * *"
# Runs inside the app container where CRON_SECRET is already in env.
CRON_EXPR = "*/5 * * * *"
# Public URL + env expansion via bash -lc so CRON_SECRET from app env is used.
CMD = (
'curl -sS -H "Authorization: Bearer $CRON_SECRET" '
'"http://127.0.0.1:3000/api/cron/pipeline-publish" || '
'curl -sS -H "Authorization: Bearer $CRON_SECRET" '
'"https://app.4ventures.es/api/cron/pipeline-publish"'
"bash -lc "
"'curl -sS -H \"Authorization: Bearer $CRON_SECRET\" "
"\"https://app.4ventures.es/api/cron/pipeline-publish?limit=20\"'"
)
@@ -66,10 +57,9 @@ def main() -> int:
page.wait_for_url(re.compile(r"/dashboard"), timeout=90000)
print("login ok", flush=True)
# Probe Dokploy schedule APIs
probe = page.evaluate(
result = page.evaluate(
"""async ({ appId, name, cron, command }) => {
const tryFetch = async (path, body) => {
const post = async (path, body) => {
const r = await fetch(path, {
method: 'POST',
credentials: 'include',
@@ -79,43 +69,41 @@ def main() -> int:
const text = await r.text();
let json = null;
try { json = JSON.parse(text); } catch {}
return { ok: r.ok, status: r.status, path, text: text.slice(0, 500), json };
return { ok: r.ok, status: r.status, text: text.slice(0, 1200), json };
};
const listPaths = [
'/api/trpc/schedule.all',
'/api/application.one',
];
const outs = {};
// application.one to inspect shape
outs.one = await tryFetch('/api/application.one', { applicationId: appId });
// Dokploy v0. something: schedule.create
const createBodies = [
{ applicationId: appId, name, cron, command, shellType: 'bash' },
{ applicationId: appId, scheduleName: name, cronExpression: cron, command, enabled: true },
{ applicationId: appId, name, cronExpression: cron, command },
];
const createPaths = [
'/api/schedule.create',
'/api/application.createSchedule',
'/api/trpc/schedule.create',
];
outs.creates = [];
for (const path of createPaths) {
for (const body of createBodies) {
const r = await tryFetch(path, body);
outs.creates.push(r);
if (r.ok) return { ok: true, via: path, r };
}
}
// list schedules
for (const path of [
// Delete existing schedules with same name
for (const listPath of [
'/api/schedule.all',
'/api/application.schedules',
'/api/schedule.byApplicationId',
]) {
outs['list_'+path] = await tryFetch(path, { applicationId: appId });
const listed = await post(listPath, { applicationId: appId });
const rows = Array.isArray(listed.json)
? listed.json
: listed.json?.data || listed.json?.schedules || [];
if (Array.isArray(rows)) {
for (const row of rows) {
const n = row?.name || row?.scheduleName;
const id = row?.scheduleId || row?.id;
if (n === name && id) {
await post('/api/schedule.delete', { scheduleId: id });
await post('/api/schedule.remove', { scheduleId: id });
}
}
}
}
return { ok: false, outs };
const created = await post('/api/schedule.create', {
applicationId: appId,
name,
cron,
cronExpression: cron,
command,
shellType: 'bash',
enabled: true,
scheduleType: 'application',
});
return { created };
}""",
{
"appId": APP_ID,
@@ -124,83 +112,10 @@ def main() -> int:
"command": CMD,
},
)
print("API_PROBE", json.dumps(probe, indent=2)[:4000], flush=True)
if probe.get("ok"):
print("SCHEDULE_OK_API", flush=True)
browser.close()
return 0
# UI fallback: Schedules tab
page.goto(APP, wait_until="domcontentloaded", timeout=90000)
page.wait_for_timeout(1000)
tab = page.get_by_role("tab", name=re.compile(r"Schedule", re.I))
if not tab.count():
# try Advanced / Domains area
for name in ("Schedules", "Cron", "Advanced"):
t = page.get_by_text(re.compile(rf"^{name}$", re.I))
if t.count():
t.first.click()
page.wait_for_timeout(600)
break
else:
tab.first.click()
page.wait_for_timeout(800)
body = page.inner_text("body")
print("UI_BODY_SNIP", body[:1500].replace("\n", " | "), flush=True)
if SCHEDULE_NAME in body or "pipeline-publish" in body:
print("SCHEDULE_ALREADY_PRESENT", flush=True)
browser.close()
return 0
add = page.get_by_role("button", name=re.compile(r"Add|Create|New|Schedule", re.I))
if add.count():
add.first.click()
page.wait_for_timeout(700)
# fill fields heuristically
inputs = page.locator("input, textarea")
n = inputs.count()
filled = 0
for i in range(min(n, 12)):
el = inputs.nth(i)
try:
typ = (el.get_attribute("type") or "").lower()
name = (el.get_attribute("name") or el.get_attribute("placeholder") or "").lower()
if typ == "hidden":
continue
if "name" in name or filled == 0:
el.fill(SCHEDULE_NAME)
filled += 1
elif "cron" in name or filled == 1:
el.fill(CRON_EXPR)
filled += 1
elif "command" in name or "cmd" in name or filled == 2:
el.fill(CMD)
filled += 1
except Exception:
pass
save = page.get_by_role("button", name=re.compile(r"Save|Create|Confirm|Add", re.I))
if save.count():
save.first.click()
page.wait_for_timeout(1200)
print("SCHEDULE_UI_SAVED", flush=True)
browser.close()
return 0
print("SCHEDULE_NEEDS_MANUAL", flush=True)
# Persist ops note
note = ROOT / ".ops" / "pipeline-publish-cron.txt"
note.write_text(
f"Schedule needed on Dokploy app {APP_ID}:\n"
f" name: {SCHEDULE_NAME}\n"
f" cron: {CRON_EXPR}\n"
f" command:\n{CMD}\n",
encoding="utf-8",
)
print(json.dumps(result, indent=2)[:4000], flush=True)
ok = bool(result.get("created", {}).get("ok"))
browser.close()
return 0 # non-fatal: docs + note written
return 0 if ok else 1
if __name__ == "__main__":