| name | frontend-dashboard-patterns |
| description | Patrones completos para dashboards frontend vanilla JS: cliente API robusto, orquestación de carga, error boundaries, tabs con navegación, persistencia, fechas, colores, sparklines, Web Workers y debugging. Todo lo que necesitas para construir dashboards resilientes sin bundler. |
| version | 1.4.0 |
| author | Hermes Agent |
| tags | ["frontend","dashboard","patterns","vanilla-js","resilience","geodatos","leaflet","choropleth"] |
Frontend Dashboard Patterns — Colección Completa
Patrones reutilizables para dashboards frontend vanilla JS sin bundler. Inspirados en ESIOS Dashboard y Ntizar Aurora.
Tabla de Contenidos
-
Cliente API Robusto — reintentos, circuit breaker, timeouts
-
Orquestación de Carga — Promise.allSettled, anti-doble-carga, cache fallback
-
Error Boundaries — overlay global, errores por sección, retry
-
Tabs con Navegación — hash navigation, lazy-loading, deep-linking
-
Persistencia de Estado — localStorage con validación, expiración, corrupción
-
Manejo de Fechas — UTC→local, zonas horarias, DST
-
Sistema de Colores — tokens CSS, modo oscuro, variables semánticas
-
Sparklines con Plotly — mini gráficos inline optimizados
-
Web Workers + Comlink — cálculos pesados sin bloquear UI
-
Debugging Patterns — scope, lifecycle, DOM integrity, paréntesis desbalanceados, checklist página en blanco
-
Aurora Design System — CSS puro, 11 packs opt-in, namespaced .nz, 5 skins
-
Geodatos Choropleth — Leaflet + Canvas, TopoJSON, lazy loading geodatos
-
GTFS Browser Parser — parsing GTFS en navegador sin servidor
-
Three.js 3D Scenes — escenas 3D interactivas con texturas procedurales, partículas con identidad, zoom a partícula individual, raycasting, labels flotantes, WebSocket sync, deep linking
-
Three.js Escenarios JSON — patrón para cargar escenarios desde JSON con validación de schema, normalización y conversión de parámetros a wave params del shader
-
ESLint v10 Flat Config — flat config, globals, ignores
-
Vite HTML Script Processing — IIFEs, post-build, rutas relativas
-
Full-Stack SPA Audit Pattern — auditar SPA con backend + frontend (Express + vanilla JS)
-
Toast Notifications — sistema de feedback UX con queue, animaciones y auto-dismiss
-
Conservative Vanilla JS Modernization — refactorizar ES5→ES6 en 6 fases sin frameworks, sin romper nada
-
Data Integrity & Graceful Degradation — nunca inventar datos, fallback chains, empty states honestos, limitaciones de contenedores
-
Hero Redesign Pattern — primera pantalla con datos reales + acciones rápidas, no hero vacío
-
NaN Deploy Cache Pattern — verificación de deploy, cache de Cloudflare, debugging "no cargan datos"
-
switchTab Pattern — onclick + event listener — función switchTab para onclick inline + event listener, hero placement, quick panel en tab correcta
-
Tab System con Sidebar — Sistema completo de tabs con sidebar de navegación, hero, lazy-loading, persistencia de estado y toast notifications
-
Vanilla JS SPA Module Extension — añadir nuevos módulos a un dashboard SPA existente
-
Single-File SPA con Aurora — patrón completo de dashboard en un solo HTML: sidebar + tabs + hero + lazy-loading + localStorage + toasts. Base para proyectos como ContrataPúblico.
-
Single-File SPA con datos grandes — patrón para embeber datasets grandes (JSON de cientos de KB) en SPAs de un solo archivo sin romper el deploy.
-
Debugging typeof LEY_DATA === 'undefined' — checklist de 4 causas: </script> collision, strings con newlines, Content-Type, CSP.
-
Migración Completa a Aurora — patrón de 5 fases para migrar un HTML existente con CSS custom propio al Aurora Design System (auditoría → packs → componentes → CSS mínimo → JS dinámico). Incluye mapeo de componentes, pitfalls de mesh/orbs/gradientes, y métricas objetivo.
-
Three.js Particle Systems — sistemas de partículas con Three.js: pool reciclado, shaders custom, espuma, spray, humo, fuego. Partículas con vida limitada, tamaño variable, blending additive.
1. Cliente API Robusto
Clase ApiClient con reintentos exponenciales (backoff + jitter), circuit breaker por endpoint, timeouts por operación, y clasificación de errores HTTP.
Reglas clave:
- No reintentar 4xx (excepto 429)
- Circuit breaker: N fallos → abrir circuito T segundos → half-open → cerrar/reabrir
- Timeout por request con AbortController
- Integrar con
Promise.allSettled para carga paralela tolerante a fallos
Ver: references/api-client-pattern.md para código completo.
2. Orquestación de Carga
Clase DataOrchestrator para cargar múltiples endpoints en paralelo con:
- Anti-doble-carga (mínimo 3s entre cargas)
- Estados de loading por sección
- Fallback a datos cacheados
Promise.allSettled para tolerancia parcial
Ver: references/orquestacion-carga-pattern.md para código completo.
3. Error Boundaries
Patrón de resiliencia:
- Overlay global cuando todo falla (network, 5xx)
- Errores por sección para fallos parciales
safeRender() wrapper para que un error de render no rompa el resto
- Timeout en servidor con
withTimeout() wrapper
- Clasificación de errores: 404, 502, 503, network, timeout → mensajes distintos
Ver: references/error-boundaries-pattern.md para código completo.
4. Tabs con Navegación
Clase TabController para:
- Navegación por hash (
#demanda) con deep-linking
- Lazy-loading de contenido
- Persistencia de última tab en localStorage
- Navegación por teclado (ArrowLeft/ArrowRight)
- Accesibilidad:
role="tab", aria-selected
Ver: references/tabs-navigation-pattern.md para código completo.
5. Persistencia de Estado
Clase PersistStore para localStorage con:
- Validación de integridad al leer
- Expiración automática (TTL configurable)
- Control de tamaño (truncar arrays si exceden límite)
- Manejo de
QuotaExceededError
- Recuperación ante datos corruptos
- Opcional:
HybridStore con sessionStorage + localStorage
Ver: references/persistence-pattern.md para código completo.
6. Manejo de Fechas
Patrones para fechas consistentes:
- NUNCA usar
new Date('YYYY-MM-DD') → se interpreta como UTC
- Usar
new Date(year, month, day) para fechas locales
toLocaleString('es-ES') para formateo con zona horaria del navegador
- Detección de DST con
Intl.DateTimeFormat().resolvedOptions().timeZone
- Normalización de inputs de fecha
- Hora por defecto en
<input type="time">: siempre rellenar con la hora actual de la zona horaria del usuario, no dejar vacío:
el.value = new Date().toLocaleTimeString('es-ES', { hour:'2-digit', minute:'2-digit', timeZone:'Europe/Madrid', hour12:false });
Ver: references/dates-timezone-pattern.md para código completo.
6.2 Retroactive Date Entry — Registro en días pasados
Patrón para formularios de registro que permitan al usuario elegir cualquier fecha, no solo hoy. Necesario en apps de seguimiento (dieta, fitness, hábitos) donde el usuario puede olvidar registrar un día y necesita retroceder la fecha.
Implementación resumida (ver reference para código completo):
- HTML — añadir
<input type="date"> a cada tarjeta de registro, pre-rellenado con today() (helper new Date().toLocaleDateString('sv-SE'))
- Orden UX — input date va primero en el layout (antes del valor), para que el usuario cambie primero el día
- JS — la función de registro lee
inputFecha.value || '' y lo pasa en el body del fetch
- Edición estimada — añadir también input date en la caja de edición (comida y ejercicio estimados)
- Backend — patrón
fecha || hoy(): aceptar fecha opcional, usarla si viene, fallback a hoy si no
- UPSERT — para tablas con lógica upsert por día (ej: pasos), usar la fecha recibida en el lookup, no
hoy()
const { fecha, ... } = req.body;
sql_run('INSERT INTO ... fecha VALUES ?', [req.userId, fecha || hoy(), ...]);
const f = fecha || hoy();
const existing = sql_get('SELECT id FROM pasos WHERE usuario_id = ? AND fecha = ?', [req.userId, f]);
Pitfalls específicos:
- El helper
today() debe estar definido ANTES de construir los HTML templates. Ponerlo al inicio del script.
sv-SE locale es el que entienden los inputs date nativos. toISOString().slice(0,10) también funciona pero puede tener offset UTC.
- La clave de todo el patrón es el backend con
fecha || hoy() — sin eso, clientes viejos que no envían fecha se rompen.
- Para pasos (UPSERT por día): si no se usa
f en el SELECT, los pasos de ayer se actualizarían sobre los de hoy.
Ver: references/retroactive-date-entry.md para código completo con todos los formularios, estimaciones editables y server-side.
7. Sistema de Colores
Patrón de diseño centralizado:
- Variables CSS semánticas (
--color-success, --color-danger)
- Variables de gráfico fijas (
--color-chart-1...) para consistencia
- Modo oscuro con
[data-theme="dark"]
- Toggle con localStorage +
prefers-color-scheme fallback
- Un solo archivo de configuración → cambios globales en un lugar
Ver: references/color-system-pattern.md para código completo.
8. Sparklines con Plotly
Mini gráficos inline optimizados:
- Reducir datos a 50 puntos máx. antes de renderizar
- Sin hover, sin ejes, sin leyenda → rendimiento máximo
Plotly.purge() al destruir para liberar memoria
- Color condicional (verde si sube, rojo si baja)
- Punto final destacado con marker más grande
Ver: references/sparklines-pattern.md para código completo.
9. Web Workers + Comlink
Cálculos pesados sin bloquear UI:
- Spatial hashing para ray-casting eficiente
- Precomputación + cache
Comlink.transfer() para arrays grandes (ArrayBuffer)
visitToken vs Set para evitar allocation por frame
- Batch calculations para terrazas grandes
Ver: references/web-workers-pattern.md para código completo.
🔥 REGEX-based removal de bloques de código es INFIABLE
2026-06-13 (MasterFit dieta): Intenté eliminar código de dark mode con re.sub y un patrón regex que buscaba desde un comentario hasta })();. El regex eliminó el botón HTML pero dejó fragmentos del IIFE abierto (bloque try { localStorage.getItem(DARK_MODE_KEY) ... } catch(e) {}), lo que rompió la ejecución JS. loadData() quedó definida pero nunca llamada porque el bloque IIFE abierto cortaba el script.
Síntomas:
- HTML carga, CSS aplica, pero
typeof loadData === 'undefined'
- No hay errores en consola (el parser JS no falla, solo las funciones después del bloque roto no se definen)
- Los datos existen en
database.json pero no se muestran
- Hero vacío, botones de navegación sin efecto
Causa: Los bloques IIFE con try { ... } catch(e) {} anidados son difíciles de delimitar con regex. re.sub(r'from_comment_to_closing', '', content) deja fragmentos huérfanos que el parser JS ignora pero que cortan la ejecución.
Fix seguro — 3 pasos:
- Identificar el bloque completo — buscar el comentario que inicia (
// === DARK MODE TOGGLE ===) y el cierre (})();)
- Eliminar con
content[:start] + content[end:] — NO usar re.sub
- Verificar que no queda ningún fragmento — grep por
DARK_MODE_KEY, darkModeToggle, toggleDarkMode, nz-btn--secondary, mf-dark, data-nz-theme
Regla: Cuando elimines bloques de código, SIEMPRE verifica que no quedan fragmentos huérfanos. Los fragmentos de JS sueltos son bugs silenciosos: el parser no falla, pero las funciones después del fragmento no se definen.
Pitfall adicional: Después de eliminar bloques grandes con content[:start] + content[end:], verificar que el archivo no se truncó. Un archivo HTML de 120KB que queda en 28KB está truncado — se perdió todo el JS y la mayor parte del HTML. Siempre verificar </html> y </body> están presentes.
Verificación post-fix:
with open('dashboard.html', 'r') as f:
content = f.read()
for term in ['darkModeToggle', 'DARK_MODE_KEY', 'toggleDarkMode', 'nz-btn--secondary', 'mf-dark', 'data-nz-theme']:
if term in content:
print(f"⚠️ {term} aún presente")
assert '</html>' in content, "Archivo truncado — falta </html>"
assert '</body>' in content, "Archivo truncado — falta </body>"
assert 'function loadData()' in content, "loadData() eliminada"
assert 'function renderDashboard' in content, "renderDashboard eliminada"
🔥 Hero vacío en primera pantalla — UX crítica
2026-06-13 (MasterFit dieta): El hero solo mostraba branding ("MasterFit", "Objetivo: 88 kg", avatar) sin ningún dato ni acción. El usuario dice "no se ve información relevante" — la primera pantalla DEBE mostrar:
- Datos reales — peso actual, kg perdidos, ritmo semanal (o "sin datos" si no hay)
- Acciones rápidas — botones para "Registrar" y "Hablar con IA" (las 2 acciones principales)
Patrón de hero correcto:
<section class="nz-hero nz-hero--centered">
<div class="nz-hero__inner">
<div class="nz-hero__eyebrow">Dashboard de seguimiento</div>
<h1 class="nz-hero__title nz-gradient-text">🏋️ MasterFit</h1>
<p class="nz-hero__sub">Objetivo: <strong>88 kg</strong></p>
<div id="heroQuickStatus">
<div class="nz-surface nz-surface--glass-soft">
<div>⚖️ Peso</div>
<div id="heroPeso">--</div>
</div>
<div class="nz-surface nz-surface--glass-soft">
<div>📉 Perdido</div>
<div id="heroPerdido">--</div>
</div>
<div class="nz-surface nz-surface--glass-soft">
<div>🔥 Ritmo</div>
<div id="heroRitmo">--</div>
</div>
</div>
<div class="nz-hero__cta">
<a class="nz-btn nz-btn--glass-liquid-brand" onclick="switchTab('registrar')">➕ Registrar</a>
<a class="nz-btn nz-btn--glass-liquid-accent" onclick="switchTab('ia')">🤖 Hablar con IA</a>
</div>
</div>
</section>