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
165 lines
5.2 KiB
TypeScript
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;
|
|
});
|