From 6566b420379cb1492478d3eb73ece84cb9028084 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 00:25:25 +0200 Subject: [PATCH] feat: script de carga para las 33 creatividades de Blue Parrot (oct26-ene27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01WqGTGPznxTp8mwMg6Yv4Ew --- package.json | 2 + scripts/blue-parrot-pipeline-seed.json | 38 ++++++ scripts/seed_blue_parrot_pipeline.ts | 164 +++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 scripts/blue-parrot-pipeline-seed.json create mode 100644 scripts/seed_blue_parrot_pipeline.ts diff --git a/package.json b/package.json index 040e2c6..ff14cf4 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "autoprefixer": "^10.4.20", + "dotenv": "^18.0.5", "postcss": "^8.5.2", "tailwindcss": "^3.4.17", + "tsx": "^4.23.15", "typescript": "^5.7.3" } } diff --git a/scripts/blue-parrot-pipeline-seed.json b/scripts/blue-parrot-pipeline-seed.json new file mode 100644 index 0000000..e7db616 --- /dev/null +++ b/scripts/blue-parrot-pipeline-seed.json @@ -0,0 +1,38 @@ +[ + { "id": "oct-01", "format": "carrusel", "month": "2026-10", "category": "aprende inglés", "title": "3 false friends que te delatan" }, + { "id": "oct-02", "format": "story", "month": "2026-10", "category": "aprende inglés", "title": "Palabra del día: deadline" }, + { "id": "oct-03", "format": "reel", "month": "2026-10", "category": "speaking", "title": "Preséntate en inglés en 30 segundos" }, + { "id": "oct-04", "format": "post", "month": "2026-10", "category": "cursos y certificaciones", "title": "Cursos intensivos: certifica tu nivel" }, + { "id": "oct-05", "format": "story", "month": "2026-10", "category": "test de nivel", "title": "¿No sabes por dónde empezar?" }, + { "id": "oct-06", "format": "carrusel", "month": "2026-10", "category": "cursos y certificaciones", "title": "Aptis, Cambridge o IELTS: ¿cuál te conviene?" }, + { "id": "oct-07", "format": "reel", "month": "2026-10", "category": "aprende inglés", "title": "Make o do: el truco para no fallar" }, + { "id": "oct-08", "format": "story", "month": "2026-10", "category": "comunidad", "title": "¿Qué idioma quieres aprender?" }, + { "id": "oct-09", "format": "carrusel", "month": "2026-10", "category": "inglés para el trabajo", "title": "3 frases para sonar más natural en una reunión" }, + { "id": "oct-10", "format": "reel", "month": "2026-10", "category": "comunidad", "title": "Un día en la academia" }, + { "id": "oct-11", "format": "post", "month": "2026-10", "category": "cursos y certificaciones", "title": "Grupos de máximo 8 alumnos" }, + { "id": "oct-12", "format": "carrusel", "month": "2026-10", "category": "aprende inglés", "title": "Phrasal verbs con «get» que usarás cada día" }, + + { "id": "nov-01", "format": "carrusel", "month": "2026-11", "category": "inglés con series", "title": "Friendsgiving: el inglés de Acción de Gracias con Friends" }, + { "id": "nov-02", "format": "reel", "month": "2026-11", "category": "inglés para el día a día", "title": "Black Friday: 5 palabras que verás en todas las tiendas" }, + { "id": "nov-03", "format": "story", "month": "2026-11", "category": "comunidad", "title": "¿Deal o bargain? Tú decides" }, + { "id": "nov-04", "format": "carrusel", "month": "2026-11", "category": "inglés con series", "title": "El inglés de Stranger Things" }, + { "id": "nov-05", "format": "post", "month": "2026-11", "category": "cursos y certificaciones", "title": "Certifica tu nivel antes de que acabe el año" }, + { "id": "nov-06", "format": "reel", "month": "2026-11", "category": "speaking", "title": "Thanks, cheers, much appreciated: cómo dar las gracias en inglés" }, + { "id": "nov-07", "format": "story", "month": "2026-11", "category": "aprende inglés", "title": "Palabra del día: leftovers" }, + + { "id": "dic-01", "format": "carrusel", "month": "2026-12", "category": "inglés con películas", "title": "El inglés de Solo en casa" }, + { "id": "dic-02", "format": "story", "month": "2026-12", "category": "aprende inglés", "title": "Palabra del día: cosy" }, + { "id": "dic-03", "format": "reel", "month": "2026-12", "category": "speaking", "title": "Felicita la Navidad en inglés sin decir solo Merry Christmas" }, + { "id": "dic-04", "format": "carrusel", "month": "2026-12", "category": "inglés con películas", "title": "Love Actually: frases para practicar tu inglés" }, + { "id": "dic-05", "format": "post", "month": "2026-12", "category": "cursos y certificaciones", "title": "Estas Navidades, regala un idioma" }, + { "id": "dic-06", "format": "reel", "month": "2026-12", "category": "aprende inglés", "title": "Christmas vocabulary: mistletoe, stocking, Boxing Day" }, + { "id": "dic-07", "format": "story", "month": "2026-12", "category": "comunidad", "title": "¿Tu propósito de 2027 es aprender un idioma?" }, + + { "id": "ene27-01", "format": "carrusel", "month": "2027-01", "category": "aprende inglés", "title": "New Year's resolutions: di tus propósitos en inglés" }, + { "id": "ene27-02", "format": "story", "month": "2027-01", "category": "test de nivel", "title": "Empieza el año sabiendo tu nivel" }, + { "id": "ene27-03", "format": "reel", "month": "2027-01", "category": "aprende inglés", "title": "Take up, give up, keep up: 3 phrasal verbs para tus propósitos" }, + { "id": "ene27-04", "format": "post", "month": "2027-01", "category": "cursos y certificaciones", "title": "Nuevo año, nuevo idioma" }, + { "id": "ene27-05", "format": "carrusel", "month": "2027-01", "category": "aprende inglés", "title": "Blue Monday: vocabulario para los días grises" }, + { "id": "ene27-06", "format": "reel", "month": "2027-01", "category": "aprende inglés", "title": "Winter vocabulary: freezing, chilly, to bundle up" }, + { "id": "ene27-07", "format": "story", "month": "2027-01", "category": "aprende inglés", "title": "Palabra del día: chilly" } +] diff --git a/scripts/seed_blue_parrot_pipeline.ts b/scripts/seed_blue_parrot_pipeline.ts new file mode 100644 index 0000000..b8bc31d --- /dev/null +++ b/scripts/seed_blue_parrot_pipeline.ts @@ -0,0 +1,164 @@ +/** + * 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; +});