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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user