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,272 @@
/**
* Portfolio fix:
* - Menttum: confirm archived (no longer a client)
* - De Tú a Tú Formación ↔ ECP: separate as two active clients (move ECP social/ads)
* - Create Talento Scholar Program as sibling of Talento y Deporte
* - Sync tenants.drive_folder_id from google_drive integrations
* - Seed DNA + enrich brandbooks for affected + active portfolio
*
* npx tsx scripts/split_tenants_and_fix.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';
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=representation',
};
async function rest<T = unknown>(
path: string,
init?: RequestInit
): Promise<T> {
const r = await fetch(`${url}/rest/v1/${path}`, {
...init,
headers: { ...h, ...(init?.headers || {}) },
});
const text = await r.text();
if (!r.ok) throw new Error(`${path} ${r.status}: ${text.slice(0, 400)}`);
if (!text) return [] as T;
return JSON.parse(text) as T;
}
async function patchTenant(
id: string,
patch: Record<string, unknown>
): Promise<void> {
await rest(`tenants?id=eq.${id}`, {
method: 'PATCH',
headers: { Prefer: 'return=minimal' },
body: JSON.stringify(patch),
});
}
async function patchIntegration(
tenantId: string,
provider: string,
patch: Record<string, unknown>
): Promise<void> {
await rest(
`integrations?tenant_id=eq.${tenantId}&provider=eq.${provider}`,
{
method: 'PATCH',
headers: { Prefer: 'return=minimal' },
body: JSON.stringify({ ...patch, updated_at: new Date().toISOString() }),
}
);
}
async function main() {
const today = new Date().toISOString().slice(0, 10);
const tenants = await rest<
{
id: string;
slug: string;
name: string;
agency_followed?: boolean;
agency_notes?: string | null;
drive_folder_id?: string | null;
}[]
>('tenants?select=id,slug,name,agency_followed,agency_notes,drive_folder_id&order=name.asc');
const bySlug = Object.fromEntries(tenants.map((t) => [t.slug, t]));
// ——— 1. Menttum: archived, not a client ———
const menttum = bySlug['menttum'];
if (menttum) {
await patchTenant(menttum.id, {
agency_followed: false,
agency_notes: `[archivado ${today}] Ya no es cliente 4Ventures. No enriquecer / no sincronizar creatividades.\n${menttum.agency_notes || ''}`.slice(
0,
4000
),
creatives_enabled: false,
});
for (const p of [
'meta_ads',
'facebook',
'instagram',
'google_ads',
'google_drive',
'ga4',
'search_console',
]) {
await patchIntegration(menttum.id, p, {
status: 'disconnected',
enabled: false,
}).catch(() => undefined);
}
console.log('[menttum] archived + integrations disconnected');
}
// ——— 2. Separate De Tú a Tú vs ECP ———
const detu = bySlug['de-tu-a-tu-formacion'];
const ecp = bySlug['ecp'];
if (!detu || !ecp) throw new Error('missing de-tu-a-tu-formacion or ecp');
// Activate ECP as its own client
await patchTenant(ecp.id, {
agency_followed: true,
name: 'ECP · Escuela de Cuidadores',
creatives_enabled: false,
default_landing_url: 'https://escueladecuidadoresprofesionales.com',
meta_ad_account_id: 'act_1320801442642004',
meta_page_id: '111175967913754',
meta_ig_user_id: '17841453624785447',
agency_notes: `[${today}] Cliente separado de De Tú a Tú Formación. Marca: Escuela de Cuidadores Profesionales (ECP). Meta act_1320801442642004 · IG @escuelaecp · web escueladecuidadoresprofesionales.com. Drive: buscar carpeta cliente ECP/Cuidadores (evitar «Navidad ecp» si no es raíz).`,
});
// Move ECP social/ads ownership onto ECP tenant (connect ECP rows)
await patchIntegration(ecp.id, 'meta_ads', {
status: 'connected',
enabled: true,
external_account_id: 'act_1320801442642004',
label: 'ECP',
});
await patchIntegration(ecp.id, 'facebook', {
status: 'connected',
enabled: true,
external_account_id: '111175967913754',
label: 'Escuela de cuidadores profesionales',
});
await patchIntegration(ecp.id, 'instagram', {
status: 'connected',
enabled: true,
external_account_id: '17841453624785447',
label: '@escuelaecp',
});
await patchIntegration(ecp.id, 'ga4', {
status: 'connected',
enabled: true,
external_account_id: 'G-82HW868X9L',
label: 'Web escueladecuidadoresprofesionales.com - GA4',
});
// Detach ECP assets from De Tú a Tú
await patchIntegration(detu.id, 'meta_ads', {
status: 'disconnected',
enabled: false,
external_account_id: 'act_413393941247349',
label: 'Detuatuformacion (pendiente permiso API system user)',
});
await patchIntegration(detu.id, 'facebook', {
status: 'disconnected',
enabled: false,
external_account_id: null,
label: 'Facebook Page (pendiente página propia De Tú a Tú)',
});
await patchIntegration(detu.id, 'instagram', {
status: 'disconnected',
enabled: false,
external_account_id: null,
label: 'Instagram (pendiente cuenta propia De Tú a Tú)',
});
await patchTenant(detu.id, {
agency_followed: true,
name: 'De Tú a Tú Formación',
creatives_enabled: false,
meta_ad_account_id: 'act_413393941247349',
meta_page_id: null,
meta_ig_user_id: null,
default_landing_url: null,
drive_folder_id: '1PWUL2sn18n9bIURwfuc8YIKfhbl1BQfg',
agency_notes: `[${today}] Separado de ECP. Marca De Tú a Tú Formación. Google Ads 3183140753 · Drive 08-Detuatu · Meta act_413393941247349 (otorgar ads_read al system user API 4V2030). RRSS/Meta page propias pendientes; ECP ya no se gestiona aquí.`,
});
console.log('[detuatu / ecp] separated');
// ——— 3. Create Talento Scholar Program ———
let scholar = bySlug['talento-scholar-program'];
if (!scholar) {
const created = await rest<
{ id: string; slug: string; name: string }[]
>('tenants', {
method: 'POST',
body: JSON.stringify({
slug: 'talento-scholar-program',
name: 'Talento Scholar Program',
agency_followed: true,
creatives_enabled: false,
agency_notes: `[${today}] Separado de Talento y Deporte. Línea académica / Scholar (becas de estudio USA). Emparejar Meta Ads, IG y Drive propios cuando existan.`,
default_landing_url: 'https://talentoydeporte.com',
}),
});
scholar = created[0];
console.log('[talento-scholar-program] created', scholar?.id);
} else {
await patchTenant(scholar.id, {
agency_followed: true,
name: 'Talento Scholar Program',
agency_notes: `[${today}] Cliente separado de Talento y Deporte · línea Scholar / becas académicas.\n${scholar.agency_notes || ''}`.slice(
0,
4000
),
});
console.log('[talento-scholar-program] already existed — activated');
}
// Seed empty integrations row set is usually done by app on first open;
// ensure notes on Talento y Deporte clarify split
const tyd = bySlug['talento-y-deporte'];
if (tyd) {
await patchTenant(tyd.id, {
agency_followed: true,
name: 'Talento y Deporte',
drive_folder_id: '1IwXdvcVFKpHta4jf3nxT7k7qltzSufuL',
default_landing_url: 'https://talentoydeporte.com',
meta_ad_account_id: 'act_1188310622211934',
meta_page_id: '189855164776887',
meta_ig_user_id: '17841405373441110',
agency_notes: `[${today}] Línea deportiva / becas deportivas USA. Sibling: Talento Scholar Program (académico). Drive 06-Talento y Deporte · IG @talentoydeporte.`,
});
console.log('[talento-y-deporte] notes + ids synced');
}
// ——— 4. Sync drive_folder_id from google_drive for all followed tenants ———
const driveRows = await rest<
{ tenant_id: string; external_account_id: string; label?: string }[]
>(
'integrations?provider=eq.google_drive&status=eq.connected&select=tenant_id,external_account_id,label'
);
for (const row of driveRows) {
// Skip bad ECP christmas folder as root — keep label but don't force as client root if name is Navidad
if (/navidad/i.test(row.label || '')) {
console.log(
`[drive] skip suspicious folder for tenant ${row.tenant_id}: ${row.label}`
);
continue;
}
await patchTenant(row.tenant_id, {
drive_folder_id: row.external_account_id,
});
}
console.log(`[drive] synced folder ids for ${driveRows.length} tenants`);
// ——— 5. Enrich brandbooks for active portfolio (skip archived) ———
const active = await rest<{ id: string; slug: string; name: string }[]>(
'tenants?agency_followed=neq.false&select=id,slug,name&order=name.asc'
);
for (const t of active) {
const r = await enrichTenantBrandbook({ tenantId: t.id, forceSeed: true });
const drive = await ensureCreativesDriveFolders(t.id, t.slug);
console.log(
`[enrich] ${t.slug} ${r.completenessBefore}→${r.completenessAfter} drive=${drive.ok ? 'ok' : drive.warning}`
);
}
console.log('Done · active tenants', active.length);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});