Biz Admin Leads Dashboard — estilo CRM /admin para Supabase
Genera la página /admin estilo 1CRM en tu proyecto Next.js (App Router). 3 pestañas — Resumen (KPI + gráficos), Clientes (tabla + filtros), Email Marketing (redacción + envío masivo + historial).
Filosofía: Más simple que 1CRM (1 contraseña compartida, sin multi-usuario), pero con suficientes gráficos, deltas y email marketing para dueños de negocio de 1-2 personas. Todo el estado se guarda en Supabase, el scaffolding de archivos es independiente del proyecto existente.
/admin → ventana "Ingrese contraseña de administración" → las pestañas llaman a /api/admin/{leads|dashboard|campaigns}
→ cada solicitud envía el header x-admin-pass
→ el servidor compara de forma segura (timing-safe) con la variable de entorno ADMIN_PASSWORD
La contraseña se guarda en el estado de React — al recargar la página, la ventana emergente aparece de nuevo (intencional, simple, no se necesita cookie de sesión).
Solo Supabase — se eliminó la variante KV. Motivo: campaigns + campaign_sends necesitan JOINs SQL y omisión de RLS con service_role; KV no lo soporta de forma nativa.
Cuándo usar esta skill
- El proyecto ya tiene pago Sepay y almacenamiento de leads a través de
lib/leads-supabase.ts(debiz-setup-sepay-paymenteligiendo Supabase) → se necesita una capa de visualización, panel y email marketing. - El usuario quiere una página estilo CRM: tarjetas KPI con delta, gráfico de líneas por día, dona de conversión, envío de correos promocionales a leads registrados.
NO usar cuando:
- El proyecto no tiene
lib/leads-supabase.ts→ pedir al usuario que ejecute/biz-setup-sepay-payment(eligiendo Supabase) primero. - El proyecto usa Vercel KV en lugar de Supabase → esta skill YA NO soporta KV. El usuario solo tiene 2 opciones: migrar a Supabase o portar manualmente.
- Se necesita multi-administrador con roles/permisos → la skill usa 1 contraseña compartida, no es adecuada.
- Se necesita seguimiento detallado de correos (apertura/clic por destinatario) → la skill solo rastrea enviado/fallido. El usuario necesita Resend/SendGrid/Brevo masivo con seguimiento.
Flujo de trabajo (6 fases)
Fase 0: DETECTAR Next.js App Router + verificar lib/leads-supabase.ts + verificar variables SMTP
Fase 1: PREGUNTAR contraseña al usuario (por defecto 123456) — BLOQUEANTE
Fase 2: INSTALAR dependencia npm `recharts`
Fase 3: EJECUTAR supabase-migration-admin.sql en el Editor SQL de Supabase — BLOQUEANTE
Fase 4: ESCRIBIR ADMIN_PASSWORD en .env.local
Fase 5: SCAFFOLDING de 9 archivos (5 lib + 4 rutas + 1 página)
Fase 6: PRUEBA en 3 niveles (curl del dashboard, prueba visual en UI, envío de campaña de prueba)
Fase 0 — Detectar y verificar dependencias
# Verificar Next.js + App Router
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "App Router: ✓" || { echo "✗ Se necesita App Router. Esta skill no soporta Pages Router."; exit 1; }
# Verificar almacenamiento de leads en Supabase
test -f lib/leads-supabase.ts && echo "Supabase store: ✓" || { echo "✗ Falta lib/leads-supabase.ts — ejecuta /biz-setup-sepay-payment eligiendo Supabase primero"; exit 1; }
test -f lib/leads-store.ts && echo "Wrapper agnóstico al proveedor: ✓"
test -f lib/supabase-admin.ts && echo "Cliente con service-role: ✓"
# Verificar variables SMTP (de biz-email-setup)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST: ✓" || { echo "✗ Faltan variables SMTP — ejecuta /biz-email-setup primero"; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# Verificar paquetes
grep '"nodemailer"' package.json && echo "nodemailer: ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase: ✓"
Si falta lib/leads-supabase.ts:
⚠ Veo que el proyecto aún no tiene almacenamiento de leads en Supabase. Ejecuta
/biz-setup-sepay-paymentprimero (eligiendo backend Supabase) para conectar pagos + tablaleads+ RLS. Luego vuelve para que pueda generar el panel de administración.
Si faltan variables SMTP:
⚠ Veo que SMTP no está configurado. La pestaña Email Marketing necesita SMTP para enviar. Ejecuta
/biz-email-setupprimero para elegir proveedor (Gmail/Resend SMTP/Brevo/SendGrid/Zoho...) y luego vuelve para que genere el panel. Puedo continuar omitiendo la pestaña de email — ¿confirmas?
Resumen para el usuario:
Detecté: Next.js App Router ✓, Supabase store ✓, SMTP configurado ✓. Voy a:
- Preguntarte la contraseña para /admin
- Instalar
recharts(para gráficos de línea y dona)- Ejecutar migración SQL para crear tablas
campaigns+campaign_sendsen Supabase- Escribir
ADMIN_PASSWORDen .env.local- Generar 9 archivos (5 lib + 4 rutas + 1 página)
- Guiarte en las pruebas
¿Listo para pasar a la Fase 1?
Fase 1 — Preguntar contraseña (BLOQUEANTE)
Preguntar exactamente:
¿Qué contraseña quieres usar para la página /admin?
- Por defecto te sugiero:
123456- O pega una contraseña más segura para producción (≥12 caracteres, con letras + números + caracteres especiales)
Pega la contraseña (o escribe "123456" para usar la predeterminada):
Esperar respuesta del usuario. Tomar la contraseña que pegue. Si está vacío → usar 123456.
NO pasar a la Fase 2 hasta que el usuario confirme la contraseña.
Fase 2 — Instalar recharts
# Detectar gestor de paquetes
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # o: pnpm add recharts / yarn add recharts
nodemailer ya fue instalado por /biz-email-setup. No reinstalar.
Fase 3 — Ejecutar migración SQL (BLOQUEANTE)
Instruir al usuario:
Abre el Dashboard de Supabase → SQL Editor → New query → pega todo el contenido del archivo de abajo → haz clic en Run:
[pegar templates/supabase-migration-admin.sql aquí]Este SQL crea 2 tablas nuevas:
campaigns(cabecera de campaña) +campaign_sends(registro por destinatario). NO toca la tablaleadsexistente.Cuando termines, verifica al final del SQL — debe mostrar
campaigns: 0ycampaign_sends: 0.
Esperar a que el usuario confirme "Ejecuté el SQL" y luego pasar a la Fase 4.
Fase 4 — Escribir .env.local
Agregar a .env.local:
# Contraseña de la página /admin
ADMIN_PASSWORD=<contraseña-del-usuario-desde-fase-1>
Si ADMIN_PASSWORD ya existe → preguntar al usuario si quiere sobrescribir (por defecto NO).
Asegurarse de que .env.local esté en .gitignore:
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
Fase 5 — Scaffolding de 9 archivos
lib/admin-auth.ts ← templates/lib-admin-auth.ts
lib/admin-stats.ts ← templates/lib-admin-stats.ts
lib/campaigns.ts ← templates/lib-campaigns.ts
lib/email-render.ts ← templates/lib-email-render.ts
lib/mailer.ts ← templates/lib-mailer.ts
app/admin/page.tsx ← templates/app-admin-page.tsx
app/api/admin/leads/route.ts ← templates/app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← templates/app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← templates/app-api-admin-campaigns-route.ts
Módulo de renderizado de email: lib/email-render.ts es un módulo PURO (no importa nodemailer) — contiene textToHtml / wrapEmailHtml / personalize / renderCampaignEmail. lib/mailer.ts re-exporta estas funciones y agrega transporte SMTP. Separarlos así permite que app/admin/page.tsx (componente del cliente) importe los helpers de renderizado para el botón Vista previa sin arrastrar nodemailer al bundle del cliente.
Nota si el proyecto ya tiene lib/mailer.ts de biz-email-setup: NO sobrescribir — convertir el mailer de esta skill en lib/admin-mailer.ts y cambiar la importación en app/api/admin/campaigns/route.ts a @/lib/admin-mailer. lib/email-render.ts siempre es seguro (nombre único, no interfiere).
Nota sobre alias de importación: todas las plantillas usan @/lib/.... Verificar que el proyecto tenga en tsconfig.json paths "@/*": ["./*"] (por defecto en Next.js). Si no, cambiar a importaciones relativas.
Nota sobre lib/leads-store.ts: La ruta API de leads importa listLeads desde @/lib/leads-store (wrapper agnóstico al proveedor de biz-setup-sepay-payment). Si el usuario configuró KV antes y luego renombró → hay que verificar que el wrapper apunte a Supabase.
Diagrama de flujo de datos
Navegador (admin/page.tsx)
│ fetch (header x-admin-pass)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → tabla leads → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → tabla leads → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → tabla campaigns → { campaigns: [...] }
└──► POST /api/admin/campaigns (body con bodyFormat: 'text' | 'html')
└─► lib/campaigns.queryAudience(leads where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (renderizar 1 vez)
└─► lib/campaigns.createCampaign (fila en campaigns)
└─► bucle (throttle 200ms):
lib/mailer.personalize(html/text por destinatario)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (fila en campaign_sends)
└─► lib/campaigns.setCampaignStatus (conteos finales + sentAt)
return { successCount, failCount }
Vista previa del cliente (NO llama al servidor): admin/page.tsx importa lib/email-render directamente,
el botón "Vista previa" renderiza { html } + personalize(plantilla) → lo muestra en <iframe srcDoc sandbox>.
Fase 6 — Prueba en 3 niveles
Prueba 1 — Curl al endpoint del dashboard
PASS="<contraseña-desde-fase-1>"
# Contraseña incorrecta → 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# Esperado: 401, {"error":"invalid_password"}
# Contraseña correcta → 200 con KPI + series temporales
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# Esperado: period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
Prueba 2 — Smoke test de UI en el navegador
1. pnpm dev (o yarn/npm)
2. Abrir http://localhost:3000/admin
3. Ver ventana emergente "Página de administración" → ingresar contraseña incorrecta → "Código incorrecto, inténtalo de nuevo"
4. Ingresar correcta → entrar al dashboard
5. Pestaña Resumen: ver 4 tarjetas KPI, gráfico de líneas, dona, selector de período 7d/30d/90d funcionando
6. Pestaña Clientes: ver tabla + filtros + exportación CSV
7. Pestaña Email Marketing: ver formulario de redacción + historial (vacío al inicio)
8. Pestaña Email Marketing: escribir contenido → hacer clic en "👁 Vista previa" → modal muestra el correo
renderizado (alternar entre Texto plano ↔ HTML para confirmar ambos modos)
Prueba 3 — Enviar campaña de prueba al email del propietario
IMPORTANTE: Elegir audiencia
last_days= 7 con la contraseña configurada, cuerpo con{{name}}. Enviar prueba a un único lead en la BD (lead de prueba del propietario) para confirmar el flujo antes de enviar a todos.
1. Ir a la pestaña Email Marketing
2. Nombre de campaña: "Prueba de envío"
3. Asunto: "Email de prueba desde /admin — ¿lo recibiste?"
4. Audiencia: "Según días recientes" → 7 días
5. Contenido:
Hola {{name}},
Este es un email de prueba desde la página de administración para confirmar que SMTP funciona.
Si lo recibes, comenzaré a enviar campañas reales.
6. Hacer clic en "👁 Vista previa" → confirmar que el correo se muestra correctamente → cerrar modal
7. Hacer clic en "Enviar ahora" → confirmar
8. Esperar 10-30s (200ms × cantidad de destinatarios)
9. Verificar:
- Banner verde "Se enviaron X/X correos (0 errores)"
- Tabla de historial con nueva fila estado "Enviado"
- El propietario revisa su bandeja de entrada → ve el correo con el nombre reemplazando {{name}}
Salida final
✓ Se generó la página /admin estilo CRM con Supabase + recharts + email marketing
📁 Archivos nuevos (9):
lib/
admin-auth.ts (verificación de contraseña segura ante ataques de tiempo)
admin-stats.ts (KPI + series temporales + deltas)
campaigns.ts (CRUD + consulta de audiencia)
email-render.ts (puro: textToHtml + wrapEmailHtml + renderCampaignEmail — usado tanto en servidor como en vista previa del cliente)
mailer.ts (transportador nodemailer + re-exportación de email-render + pie de página de exclusión)
app/admin/page.tsx (UI de 3 pestañas estilo 1CRM — Resumen / Clientes / Email Marketing — cuerpo text/HTML + Vista previa)
app/api/admin/
leads/route.ts (GET list + filtro)
dashboard/route.ts (GET KPI + series temporales)
campaigns/route.ts (GET historial + POST envío masivo)
🗄 Migración de Supabase:
- Tablas campaigns + campaign_sends (RLS denegar todo, omisión con service_role)
- Ya ejecutaste templates/supabase-migration-admin.sql ✓
📦 Dependencia npm:
- recharts (recién instalado)
- nodemailer (ya existente de biz-email-setup)
🔐 Variables de entorno:
- ADMIN_PASSWORD ya escrita en .env.local
- .gitignore ya incluye .env.local
- SMTP_* + MAIL_FROM reutilizados de biz-email-setup
🧪 Pruebas:
1. ✓ Curl al endpoint del dashboard (Prueba 1)
2. ✓ Smoke test de UI en navegador (Prueba 2)
3. ✓ Envío de campaña de prueba al email del propietario (Prueba 3)
🔧 Tareas pendientes:
1. (Si despliegas a producción) Agregar ADMIN_PASSWORD a las variables de entorno de Vercel:
vercel env add ADMIN_PASSWORD production
2. Redesplegar: vercel --prod
3. Abrir https://<dominio>/admin → ingresar contraseña → confirmar que las 3 pestañas funcionan
4. Primera campaña real: elegir audiencia pequeña (last_days 7) para verificar
el límite de tasa SMTP del proveedor antes de escalar a 'all'
Plantillas
| Archivo | Descripción |
|---|---|
templates/supabase-migration-admin.sql | Migración que crea campaigns + campaign_sends |
templates/lib-admin-auth.ts | checkAdminPass() comparación segura ante ataques de tiempo |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + series temporales + deltas |
templates/lib-campaigns.ts | CRUD de campañas + queryAudience() |
templates/lib-email-render.ts | Módulo puro — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (texto/HTML). No importa nodemailer → el componente del cliente puede usarlo para vista previa |
templates/lib-mailer.ts | Transportador nodemailer + sendOne() + sleep() + re-exportación de helpers de renderizado desde email-render |
templates/app-admin-page.tsx | Página estilo 1CRM con barra lateral + 3 pestañas + recharts. Pestaña de email tiene conmutador de formato texto/HTML + botón Vista previa (modal iframe) |
templates/app-api-admin-leads-route.ts | API de tabla de leads (llama a listLeads()) |
templates/app-api-admin-dashboard-route.ts | API de analytics del dashboard |
templates/app-api-admin-campaigns-route.ts | GET lista + POST crear y enviar |
Anti-patrones (no hacer)
- ❌ Comparar contraseña con
===→ ataque de tiempo. La plantilla usacrypto.timingSafeEquala través delib/admin-auth.ts. - ❌ Hardcodear contraseña en el código fuente → olvidar cambiarla → producción desplegada con contraseña de prueba. Siempre usar
process.env.ADMIN_PASSWORD. - ❌ Enviar contraseña en la cadena de consulta de la URL (
?pass=123456) → se filtra en el historial del navegador + registros de acceso. Usar el headerx-admin-pass. - ❌ Crear página de inicio de sesión separada + cookie de sesión + JWT cuando el usuario solo necesita un panel simple. La ventana emergente inline es suficiente.
- ❌ Guardar contraseña en localStorage → JS puede leerla, fuga por XSS. La plantilla la guarda en el estado de React (se pierde al recargar, intencional).
- ❌ Omitir verificación de existencia de archivos antes de sobrescribir → siempre preguntar al usuario si ya existe
lib/mailer.tsu otros. - ❌ Enviar campaña de prueba a todos los leads (audiencia
all) en la primera prueba → usarlast_days7 primero. - ❌ Procesar variables de entorno como
SMTP_HOSTdirectamente en componentes del cliente → hacerlo solo en rutas API olib/server. - ❌ Usar
fetchsin manejar errores en componentes del cliente → mostrar mensajes de error amigables si la API falla (contraseña incorrecta, servidor caído).