| name | dashboard-from-sheet |
| description | Skill conversacional que conecta un dashboard HTML (generado con /dashboard-builder) a un Google Sheet VIVO vía el endpoint gviz CSV — fetch + PapaParse + auto-refresh cada 60 min, sin Make, sin backend, sin copiar datos. Pregunta Sheet, pestañas, mapeo de columnas y targets ANTES de tocar el HTML. USA esta skill siempre que el alumno quiera que su dashboard "lea datos reales", "se actualice solo", "se conecte al Sheet", muestre los leads que llegan de la automation de S3/S4, o cuando el dashboard tenga datos hardcodeados y deban pasar a vivos. En S5 es la skill que convierte el dashboard estático en el dashboard consolidado del curso. |
/dashboard-from-sheet — Conversacional
Qué hace y por qué existe
/dashboard-builder genera un dashboard con datos hardcodeados — perfecto para diseñar, inútil para operar. Esta skill lo convierte en un dashboard vivo: lee del MISMO Sheet que llenan tus automations de S3 (leads del webhook) y S4 (segmentos RFM), directamente desde el navegador.
Decisión de arquitectura (y por qué): el HTML hace fetch al endpoint gviz CSV de Google Sheets. NO se mete Make en medio, NO hay backend, NO se copian datos.
- 1 servicio menos que mantener · 0 ops/mes de Make consumidas
- El refresh es un F5 del CEO (o el auto-refresh de 60 min)
- El dashboard solo LEE — jamás escribe en el Sheet
Sheet vivo (S3+S4 lo llenan) ──gviz CSV──▶ fetch() en el navegador ──▶ PapaParse ──▶ Chart.js
Pattern (cumple feedback_esic-skills-conversational)
- Acoge — confirma que existe el HTML base (de
/dashboard-builder) + el Sheet con datos
- Diagnose — 4 preguntas
- Confirma — espejo del mapeo completo antes de tocar código
- Produce — edita el
<script> del HTML: conexión + parseo + render + freshness
- Itera — prueba en vivo (lead de test → F5) · ¿publicamos con
/publish-pages?
Flujo
Q1 · El Sheet y sus pestañas
"Pásame la URL de tu Sheet. ¿Qué pestañas alimentan el dashboard? (en el curso: leads_enriched de S3 · leads_with_rfm de S4 · experimentos de S5)"
→ De la URL saco el SHEET_ID. Cada pestaña = un endpoint distinto.
→ Requisito: el Sheet debe estar en "Cualquiera con el enlace puede VER" (no hace falta publicarlo en la web). Si es privado, el fetch devuelve HTML de login en vez de CSV — lo detectas porque PapaParse saca basura.
Q2 · Mapeo de columnas
"Dime qué columna del Sheet alimenta cada elemento del dashboard (hero, cada chart, cada filtro). Si no lo sabes, pégame la fila de cabeceras y lo propongo yo."
→ Sin mapeo explícito, el dashboard rompe silenciosamente cuando alguien renombra una columna. Se documenta en un comentario del <script>.
Q3 · Targets del plan (bloque plan-vs-real)
"¿Cuál es el target de tu north-star y de sus inputs? (los definisteis con /north-star-tree · si tenéis targets por semana/mes, mejor)"
→ Los targets van hardcodeados en el HTML (son EL PLAN — no cambian solos) y se pintan como línea sobre la evolución real. Real vs plan en el mismo chart o no hay juicio posible.
Q4 · Frecuencia de refresh
"¿Cada cuánto se refresca solo? (default: 60 min · el CEO siempre puede forzar con el botón ↻)"
→ Menos de 15 min no aporta (los datos entran gota a gota) y castiga el móvil.
Confirma (espejo)
"Recapitulo: Sheet {ID} · pestañas {X, Y} · hero = columna {A} vs target {N} · chart 1 = {B} por {C} · filtros {D, E} · refresh 60 min. ¿Correcto?"
Produce
Edita SOLO el <script> del HTML existente (no regeneres el dashboard — el diseño ya está aprobado):
const SHEET_ID = '{SHEET_ID}';
const url = (sheet) =>
`https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(sheet)}`;
const TARGETS = { north_star: {…}, inputs: {…} };
async function loadData() {
document.body.classList.add('loading');
try {
const [leads, rfm] = await Promise.all([
fetch(url('leads_enriched')).then(r => r.text()),
fetch(url('leads_with_rfm')).then(r => r.text()),
]);
const L = Papa.parse(leads, { header: true, dynamicTyping: true, skipEmptyLines: true }).data;
const R = Papa.parse(rfm, { header: true, dynamicTyping: true, skipEmptyLines: true }).data;
drawHero(L, TARGETS);
drawEvolution(L, TARGETS);
drawFunnel(L);
drawRFM(R);
document.getElementById('updated').textContent = new Date().toLocaleString('es-ES');
} finally {
document.body.classList.remove('loading');
}
}
document.getElementById('refresh').addEventListener('click', loadData);
loadData();
setInterval(loadData, 60 * 60 * 1000);
Además añade (si el HTML no lo trae):
- Freshness:
Última actualización: HH:MM + botón ↻ — un dashboard vivo sin fecha de actualización no es creíble
- Defensive coding:
(row.email ?? row.Email ?? '') — las cabeceras de Sheets cambian de mayúsculas cuando menos lo esperas
- Filtro de filas vacías: el gviz devuelve filas fantasma al final
En S5 · el dashboard consolidado (contexto del curso)
En S5 esta skill conecta los 3 bloques del dashboard consolidado (spec del profesor: demos/s5-dashboard-stack/dashboard/dashboard-spec.md):
| Bloque | Fuente | Nota |
|---|
| ① Plan vs real | Dataset XLY (histórico) + TARGETS hardcodeados | La evolución temporal lleva SIEMPRE la línea de target superpuesta |
| ② Motor (funnel + retención) | leads_enriched (S3) + leads_with_rfm (S4) · vivos | Anclas #adquisicion y #retencion |
| ③ Experimentos | Pestaña experimentos · vivos | Sembrada con la priorización ICE de S2 · pide a Iria crearla si no existe (columnas: hipotesis · metrica · ice · estado · nota) |
El bloque ③ es la memoria del curso: lo que priorizasteis en S2 + lo que diseñáis en S5. Estados descartada NO se borran — son la capa B (decisión-log) de vuestra defensa.
Reglas
- Solo lectura · el dashboard JAMÁS escribe en el Sheet (escribe la automation, no el report)
- Sin Make en medio · gviz directo · si te piden meterlo, pregunta qué aporta (respuesta: nada, y quema ops)
- Targets hardcodeados, datos vivos · el plan es una constante; el real, un fetch
- Máx 3 pestañas · más fetches = dashboard lento en móvil
- Prueba end-to-end obligatoria antes de dar por bueno: lead de prueba en la landing → F5 → el número sube
Troubleshooting
| Síntoma | Causa | Fix |
|---|
CSV llega pero es HTML con <title>Sign in | Sheet privado | Compartir → "Cualquiera con el enlace · Lector" |
| Charts vacíos con datos en el Sheet | Cabeceras con espacios o renombradas | Revisar fila 1 · defensive coding ?? |
| Número del hero = NaN | dynamicTyping no pudo con formato ("1.234,56 €") | Columna limpia en el Sheet (número puro) o parseo manual |
| Funciona en local, falla publicado | URL relativa o mixed content | Endpoint siempre https:// absoluto |
| Filas duplicadas en el funnel | Filas fantasma del gviz | skipEmptyLines: true + filtrar por columna clave |
Handoff típico
← Vienes de /dashboard-builder (HTML con datos hardcodeados) — esta skill lo conecta.
→ Conectado y probado, pasa a /publish-pages (Netlify por tu agente · URL pública).
→ Publicado, pasa a /dashboard-judge para auditarlo antes de compartirlo con el "CEO".
Estado de la skill
Versión 1 · 2026-07-04: creada para S5 v4.4 (dashboard consolidado). Cubre conexión gviz CSV multi-pestaña, plan-vs-real con targets, freshness y siembra del tracker de experimentos desde la priorización de S2.