feat: LinkedIn editorial, accesos agencia estables y deshacer en historial

Integra estrategias LinkedIn en calendarios (backlog publicado vs pendiente), unifica roles/cookies de vista agencia y permite deshacer cambios de brandbook dejando constancia en el historial.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-16 04:15:51 +02:00
parent 3b57cdce53
commit 4a1abd3318
54 changed files with 6400 additions and 527 deletions

View File

@@ -0,0 +1,132 @@
/**
* Create client Drive roots for ECP + TSP under 03-Clientes if missing,
* link integrations + tenants.drive_folder_id, ensure Ads/RRSS creatives folders,
* re-enrich brandbooks.
*
* npx tsx scripts/ensure_ecp_tsp_drive.ts
*/
import { config } from 'dotenv';
import { resolve } from 'path';
config({ path: resolve(process.cwd(), '.env.local') });
import { enrichTenantBrandbook } from '../src/lib/brandbook-enrich';
import { ensureCreativesDriveFolders } from '../src/lib/creatives-drive';
import {
ensureChildFolder,
findClientesRootFolder,
listClientFoldersUnderClientes,
} from '../src/lib/google-drive';
const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const h = {
apikey: key,
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
};
const TARGETS: { slug: string; folderName: string }[] = [
{ slug: 'ecp', folderName: '50-ECP Escuela de Cuidadores' },
{ slug: 'tsp', folderName: '51-TSP Talento Scholar Program' },
];
async function main() {
const listed = await listClientFoldersUnderClientes();
const root = listed.clientesRoot || (await findClientesRootFolder());
if (!root?.id) throw new Error('No se encontró carpeta Clientes en Drive');
console.log('Clientes root', root.name, root.id);
const folders = listed.folders;
for (const t of TARGETS) {
const tr = await fetch(
`${url}/rest/v1/tenants?slug=eq.${t.slug}&select=id,slug,name`,
{ headers: h }
);
const tenant = (await tr.json())[0] as
| { id: string; slug: string; name: string }
| undefined;
if (!tenant) {
console.log(`[skip] ${t.slug} no existe`);
continue;
}
let folder = folders.find((f) =>
t.slug === 'ecp'
? /ecp|cuidadores/i.test(f.name) && !/navidad/i.test(f.name)
: /tsp|scholar/i.test(f.name)
);
if (!folder) {
const id = await ensureChildFolder(root.id, t.folderName);
folder = { id, name: t.folderName };
console.log(`[create] ${t.folderName} → ${id}`);
} else {
console.log(`[exist] ${folder.name} → ${folder.id}`);
}
await fetch(
`${url}/rest/v1/integrations?tenant_id=eq.${tenant.id}&provider=eq.google_drive`,
{
method: 'PATCH',
headers: h,
body: JSON.stringify({
status: 'connected',
enabled: true,
external_account_id: folder.id,
label: folder.name,
updated_at: new Date().toISOString(),
}),
}
);
// Upsert if no google_drive row
const check = await fetch(
`${url}/rest/v1/integrations?tenant_id=eq.${tenant.id}&provider=eq.google_drive&select=id`,
{ headers: h }
);
const rows = (await check.json()) as { id: string }[];
if (!rows.length) {
await fetch(`${url}/rest/v1/integrations`, {
method: 'POST',
headers: h,
body: JSON.stringify({
tenant_id: tenant.id,
provider: 'google_drive',
status: 'connected',
enabled: true,
external_account_id: folder.id,
label: folder.name,
}),
});
}
await fetch(`${url}/rest/v1/tenants?id=eq.${tenant.id}`, {
method: 'PATCH',
headers: h,
body: JSON.stringify({ drive_folder_id: folder.id }),
});
const creatives = await ensureCreativesDriveFolders(tenant.id, t.slug);
console.log(
`[creatives] ${t.slug}`,
creatives.ok ? 'Ads+RRSS ok' : creatives.warning
);
const enrich = await enrichTenantBrandbook({
tenantId: tenant.id,
forceSeed: true,
});
console.log(
`[enrich] ${t.slug} ok=${enrich.ok} score=${enrich.completenessAfter} warn=${enrich.warning || '-'}`
);
}
console.log('Done');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});