Files
4ventures-client-portal/scripts/seed_blue_parrot_pipeline.ts
Claude 6566b42037 feat: script de carga para las 33 creatividades de Blue Parrot (oct26-ene27)
scripts/seed_blue_parrot_pipeline.ts sube portada + slides de carrusel al
bucket tenant-assets y crea/actualiza las filas en creative_pipeline
(upsert por tenant_id+external_ref, así se puede re-ejecutar sin duplicar).

Metadata en scripts/blue-parrot-pipeline-seed.json: títulos reales
extraídos de las 33 piezas ya diseñadas (oct-01..oct-12, nov-01..nov-07,
dic-01..dic-07, ene27-01..ene27-07). Estado inicial por defecto:
revision_interna (parametrizable con --status).

Requiere ejecutarse en el propio VPS (con las credenciales reales de
Supabase ya configuradas ahí) contra la carpeta descomprimida del ZIP de
imágenes — no incluye las imágenes ni credenciales en el repo.

Añade dotenv y tsx como devDependencies (ya usados por otros scripts del
repo pero no declarados en package.json).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqGTGPznxTp8mwMg6Yv4Ew
2026-10-04 00:25:25 +02:00

165 lines
5.2 KiB
TypeScript

/**
* Carga las 33 piezas de Blue Parrot (oct 2026 - ene 2027) en creative_pipeline.
*
* Requiere:
* 1. Haber aplicado sql/creative_pipeline.sql en Supabase.
* 2. Haber descomprimido Blue-Parrot-creatividades-con-imagen.zip en una carpeta.
* 3. .env.local con NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY reales
* (los mismos que usa la app en producción — ejecutar esto en el propio VPS,
* por ejemplo desde "Open Terminal" en Dokploy, nunca pegando la service
* role key en un chat).
*
* Uso:
* npx tsx scripts/seed_blue_parrot_pipeline.ts --dir /ruta/a/Blue-Parrot-creatividades-con-imagen \
* [--status revision_interna] [--dry-run]
*/
import { config } from 'dotenv';
import { resolve, join } from 'path';
import { existsSync, readFileSync } from 'fs';
import { createClient } from '@supabase/supabase-js';
config({ path: resolve(process.cwd(), '.env.local') });
// Mismo tenant_id que scripts/provision_victor_bps.ts
const BLUE_PARROT_TENANT_ID = '8bec977a-6032-4759-aa7f-b5042081f450';
const BUCKET = 'tenant-assets';
type SeedItem = {
id: string;
format: 'post' | 'story' | 'reel' | 'carrusel';
month: string;
category: string;
title: string;
};
function arg(name: string, fallback?: string): string | undefined {
const i = process.argv.indexOf(`--${name}`);
if (i === -1) return fallback;
return process.argv[i + 1];
}
const DRY_RUN = process.argv.includes('--dry-run');
async function main() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim();
const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();
if (!url || !key) throw new Error('Faltan NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY en .env.local');
const dir = arg('dir');
if (!dir || !existsSync(dir)) {
throw new Error('Pasa --dir /ruta/a/la/carpeta/descomprimida (el ZIP de las 33 piezas)');
}
const status = arg('status', 'revision_interna')!;
const metaPath = resolve(__dirname, 'blue-parrot-pipeline-seed.json');
const items: SeedItem[] = JSON.parse(readFileSync(metaPath, 'utf-8'));
const admin = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
// Asegura el bucket (no falla si ya existe).
const { data: buckets } = await admin.storage.listBuckets();
if (!buckets?.some((b) => b.id === BUCKET)) {
await admin.storage.createBucket(BUCKET, { public: false });
}
let ok = 0;
let failed = 0;
for (const it of items) {
const isCarrusel = it.format === 'carrusel';
const coverLocal = isCarrusel
? join(dir, `${it.id}-carrusel`, `${it.id}-1.jpg`)
: join(dir, `${it.id}-${it.format}.jpg`);
if (!existsSync(coverLocal)) {
console.error(`[SKIP] ${it.id}: no existe ${coverLocal}`);
failed++;
continue;
}
console.log(`[${it.id}] ${it.title}`);
if (DRY_RUN) {
ok++;
continue;
}
// 1) upsert de la fila (sin imágenes todavía) para obtener el id.
const { data: row, error: upErr } = await admin
.from('creative_pipeline')
.upsert(
{
tenant_id: BLUE_PARROT_TENANT_ID,
external_ref: it.id,
title: it.title,
format: it.format,
month: it.month,
status,
notes: it.category,
},
{ onConflict: 'tenant_id,external_ref' }
)
.select('id')
.single();
if (upErr || !row) {
console.error(`[FAIL] ${it.id}: ${upErr?.message}`);
failed++;
continue;
}
const itemId = row.id as string;
// 2) sube portada
const coverPath = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/cover.jpg`;
const coverBuf = readFileSync(coverLocal);
const coverUp = await admin.storage.from(BUCKET).upload(coverPath, coverBuf, {
contentType: 'image/jpeg',
upsert: true,
});
if (coverUp.error) {
console.error(`[FAIL cover] ${it.id}: ${coverUp.error.message}`);
failed++;
continue;
}
// 3) sube slides (carrusel: 2,3,4,5 → 4 slides interiores + CTA)
const slidePaths: string[] = [];
if (isCarrusel) {
for (let n = 2; n <= 5; n++) {
const local = join(dir, `${it.id}-carrusel`, `${it.id}-${n}.jpg`);
if (!existsSync(local)) continue;
const path = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/slide-${n - 1}.jpg`;
const buf = readFileSync(local);
const up = await admin.storage.from(BUCKET).upload(path, buf, {
contentType: 'image/jpeg',
upsert: true,
});
if (up.error) {
console.error(`[FAIL slide ${n}] ${it.id}: ${up.error.message}`);
continue;
}
slidePaths.push(path);
}
}
// 4) cierra la fila con las rutas de storage
const { error: patchErr } = await admin
.from('creative_pipeline')
.update({ cover_path: coverPath, cover_mime: 'image/jpeg', slide_paths: slidePaths })
.eq('id', itemId);
if (patchErr) {
console.error(`[FAIL patch] ${it.id}: ${patchErr.message}`);
failed++;
continue;
}
ok++;
}
console.log(`\nHecho: ${ok} ok, ${failed} con error, de ${items.length} piezas.`);
if (failed > 0) process.exitCode = 1;
}
main().catch((e) => {
console.error(e);
process.exitCode = 1;
});