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

@@ -9,110 +9,11 @@ import {
} from '@/lib/superhuman-docs';
import { syncAllClientifyTenants } from '@/lib/clientify-sync';
import { syncAllGhlTenants } from '@/lib/gohighlevel-sync';
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import {
mergePermalinkNotes,
publishPipelineItem,
type PipelinePublishRow,
} from '@/lib/meta-publish';
import { processDuePipelinePublishes } from '@/lib/pipeline-publish-worker';
export const dynamic = 'force-dynamic';
export const maxDuration = 300;
const PIPELINE_SELECT =
'id,tenant_id,title,format,status,scheduled_at,channels,publish_status,publish_attempts,cover_path,cover_mime,slide_paths,caption,notes,meta_ig_media_id,meta_fb_post_id';
async function runDuePipelinePublish(limit = 5): Promise<Record<string, unknown>> {
const headers = serviceHeaders();
const base = supabaseUrl();
if (!headers || !base) {
return { ok: false, error: 'Service role required' };
}
const now = new Date().toISOString();
const url =
`${base}/rest/v1/creative_pipeline?status=eq.aprobado` +
`&scheduled_at=lte.${encodeURIComponent(now)}` +
`&publish_status=in.(idle,queued,failed)` +
`&select=${PIPELINE_SELECT}&order=scheduled_at.asc&limit=${limit}`;
const list = await fetch(url, { headers, cache: 'no-store' });
if (!list.ok) {
return { ok: false, error: (await list.text()).slice(0, 200) };
}
const due = (await list.json()) as (PipelinePublishRow & {
title: string;
notes?: string | null;
})[];
const results: { id: string; ok: boolean; detail: string }[] = [];
for (const item of due) {
if (!item.cover_path) {
results.push({ id: item.id, ok: false, detail: 'missing cover' });
continue;
}
await fetch(`${base}/rest/v1/creative_pipeline?id=eq.${item.id}`, {
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({
publish_status: 'publishing',
publish_attempts: (item.publish_attempts || 0) + 1,
publish_error: null,
updated_at: new Date().toISOString(),
}),
});
const result = await publishPipelineItem(item);
if (!result.ok) {
await fetch(`${base}/rest/v1/creative_pipeline?id=eq.${item.id}`, {
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({
publish_status: 'failed',
publish_error: result.error || 'Error Meta',
meta_ig_media_id: result.igMediaId || item.meta_ig_media_id || null,
meta_fb_post_id: result.fbPostId || item.meta_fb_post_id || null,
updated_at: new Date().toISOString(),
}),
});
results.push({
id: item.id,
ok: false,
detail: result.error || 'failed',
});
continue;
}
await fetch(`${base}/rest/v1/creative_pipeline?id=eq.${item.id}`, {
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify({
publish_status: 'published',
publish_error: null,
published_at: new Date().toISOString(),
status: 'publicado',
meta_ig_media_id: result.igMediaId || null,
meta_fb_post_id: result.fbPostId || null,
notes: mergePermalinkNotes(
item.notes,
result.igPermalink,
result.fbPermalink
),
updated_at: new Date().toISOString(),
}),
});
results.push({ id: item.id, ok: true, detail: 'published' });
}
return { ok: true, due: due.length, results };
}
/**
* Master daily job: organic + ads + superhuman.
* Dokploy (daily ~06:00 UTC):
@@ -213,7 +114,7 @@ export async function GET(req: NextRequest) {
// 5) Pipeline Meta publish (safety net; prefer dedicated */10 cron)
try {
out.pipelinePublish = await runDuePipelinePublish(5);
out.pipelinePublish = await processDuePipelinePublishes({ limit: 5 });
} catch (e) {
out.pipelinePublish = {
ok: false,