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,135 @@
/**
* Rename portfolio brand slugs to canonical codes:
* talento-y-deporte → tyd
* talento-scholar-program → tsp
* de-tu-a-tu-formacion → dtatf
* ecp → ecp (unchanged)
*
* npx tsx scripts/rename_brand_slugs.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 {
BRAND_DISPLAY_NAMES,
brandSiblingNote,
} from '../src/lib/tenant-brand-aliases';
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',
};
const RENAMES: { from: string; to: keyof typeof BRAND_DISPLAY_NAMES }[] = [
{ from: 'talento-y-deporte', to: 'tyd' },
{ from: 'talento-scholar-program', to: 'tsp' },
{ from: 'de-tu-a-tu-formacion', to: 'dtatf' },
];
async function rest<T>(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)}`);
return text ? (JSON.parse(text) as T) : ([] as T);
}
async function main() {
const today = new Date().toISOString().slice(0, 10);
const tenants = await rest<{ id: string; slug: string; name: string; agency_notes?: string }[]>(
'tenants?select=id,slug,name,agency_notes'
);
const bySlug = Object.fromEntries(tenants.map((t) => [t.slug, t]));
for (const { from, to } of RENAMES) {
const existingTo = bySlug[to];
const fromRow = bySlug[from];
if (existingTo && !fromRow) {
console.log(`[${to}] already canonical`);
continue;
}
if (!fromRow) {
console.log(`[skip] ${from} not found`);
continue;
}
if (existingTo && existingTo.id !== fromRow.id) {
throw new Error(`Conflict: both ${from} and ${to} exist`);
}
const sib = brandSiblingNote(to);
const notes = [
`[${today}] Slug canónico: ${to} · ${BRAND_DISPLAY_NAMES[to]}.`,
sib,
fromRow.agency_notes || '',
]
.filter(Boolean)
.join('\n')
.slice(0, 4000);
await rest(`tenants?id=eq.${fromRow.id}`, {
method: 'PATCH',
headers: { Prefer: 'return=minimal' },
body: JSON.stringify({
slug: to,
name: BRAND_DISPLAY_NAMES[to],
agency_followed: true,
agency_notes: notes,
}),
});
console.log(`[rename] ${from} → ${to}`);
bySlug[to] = { ...fromRow, slug: to, name: BRAND_DISPLAY_NAMES[to] };
delete bySlug[from];
}
// Ensure ECP display name
const ecp = bySlug['ecp'];
if (ecp) {
await rest(`tenants?id=eq.${ecp.id}`, {
method: 'PATCH',
headers: { Prefer: 'return=minimal' },
body: JSON.stringify({
name: BRAND_DISPLAY_NAMES.ecp,
agency_followed: true,
agency_notes: [
`[${today}] Marca canónica ECP (separada de DTATF).`,
brandSiblingNote('ecp'),
ecp.agency_notes || '',
]
.filter(Boolean)
.join('\n')
.slice(0, 4000),
}),
});
console.log('[ecp] display name refreshed');
}
for (const slug of ['tyd', 'tsp', 'ecp', 'dtatf'] as const) {
const t = bySlug[slug] || (await rest<{ id: string; slug: string }[]>(
`tenants?slug=eq.${slug}&select=id,slug&limit=1`
))[0];
if (!t) {
console.log(`[missing] ${slug}`);
continue;
}
const r = await enrichTenantBrandbook({ tenantId: t.id, forceSeed: true });
const drive = await ensureCreativesDriveFolders(t.id, slug);
console.log(
`[enrich] ${slug} ${r.completenessBefore}→${r.completenessAfter} drive=${drive.ok ? 'ok' : drive.warning}`
);
}
console.log('Done');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});