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:
135
scripts/rename_brand_slugs.ts
Normal file
135
scripts/rename_brand_slugs.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user