- 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.5.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
1. [Cliente API Robusto](#1-cliente-api-robusto) — reintentos, circuit breaker, timeouts
2. [Orquestación de Carga](#2-orquestación-de-carga) — Promise.allSettled, anti-doble-carga, cache fallback
3. [Error Boundaries](#3-error-boundaries) — overlay global, errores por sección, retry
4. [Tabs con Navegación](#4-tabs-con-navegación) — hash navigation, lazy-loading, deep-linking
5. [Persistencia de Estado](#5-persistencia-de-estado) — localStorage con validación, expiración, corrupción
6. [Manejo de Fechas](#6-manejo-de-fechas) — UTC→local, zonas horarias, DST
7. [Sistema de Colores](#7-sistema-de-colores) — tokens CSS, modo oscuro, variables semánticas
8. [Sparklines con Plotly](#8-sparklines-con-plotly) — mini gráficos inline optimizados
9. [Web Workers + Comlink](#9-web-workers--comlink) — cálculos pesados sin bloquear UI
10. [Debugging Patterns](#10-debugging-patterns) — scope, lifecycle, DOM integrity, paréntesis desbalanceados, checklist página en blanco
11. [Aurora Design System](#11-aurora-design-system) — CSS puro, 11 packs opt-in, namespaced .nz, 5 skins
12. [Geodatos Choropleth](#12-geodatos-choropleth) — Leaflet + Canvas, TopoJSON, lazy loading geodatos
13. [GTFS Browser Parser](#13-gtfs-browser-parser) — parsing GTFS en navegador sin servidor
14. [Three.js 3D Scenes](#14-threejs-3d-scenes) — escenas 3D interactivas con texturas procedurales, partículas con identidad, zoom a partícula individual, raycasting, labels flotantes, WebSocket sync, deep linking
15. [Three.js Escenarios JSON](references/threejs-scenario-loading.md) — 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
16. [ESLint v10 Flat Config](#16-eslint-v10-flat-config) — flat config, globals, ignores
17. [Vite HTML Script Processing](#17-vite-html-script-processing) — IIFEs, post-build, rutas relativas
18. [Full-Stack SPA Audit Pattern](#18-full-stack-spa-audit-pattern) — auditar SPA con backend + frontend (Express + vanilla JS)
19. [Toast Notifications](#19-toast-notifications) — sistema de feedback UX con queue, animaciones y auto-dismiss
20. [Conservative Vanilla JS Modernization](#20-conservative-vanilla-js-modernization) — refactorizar ES5→ES6 en 6 fases sin frameworks, sin romper nada
21. [Data Integrity & Graceful Degradation](#21-data-integrity--graceful-degradation) — nunca inventar datos, fallback chains, empty states honestos, limitaciones de contenedores
22. [Hero Redesign Pattern](references/hero-redesign-pattern.md) — primera pantalla con datos reales + acciones rápidas, no hero vacío
23. [NaN Deploy Cache Pattern](references/nan-deploy-cache-pattern.md) — verificación de deploy, cache de Cloudflare, debugging "no cargan datos"
24. [switchTab Pattern — onclick + event listener](references/switchtab-pattern.md) — función switchTab para onclick inline + event listener, hero placement, quick panel en tab correcta
25. [Tab System con Sidebar](references/tab-system-sidebar-pattern.md) — Sistema completo de tabs con sidebar de navegación, hero, lazy-loading, persistencia de estado y toast notifications
25. [Vanilla JS SPA Module Extension](#25-vanilla-js-spa-module-extension) — añadir nuevos módulos a un dashboard SPA existente
26. [Single-File SPA con Aurora](#26-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.
27. [Single-File SPA con datos grandes](#27-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.
28. [Debugging `typeof LEY_DATA === 'undefined'`](references/single-file-spa-large-data.md) — checklist de 4 causas: `</script>` collision, strings con newlines, Content-Type, CSP.
29. [Migración Completa a Aurora](references/aurora-migration-pattern.md) — 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.
30. [Three.js Particle Systems](#30-threejs-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:
```javascript
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):**
1. **HTML** — añadir `<input type="date">` a cada tarjeta de registro, pre-rellenado con `today()` (helper `new Date().toLocaleDateString('sv-SE')`)
2. **Orden UX** — input date va **primero** en el layout (antes del valor), para que el usuario cambie primero el día
3. **JS** — la función de registro lee `inputFecha.value || ''` y lo pasa en el body del fetch
4. **Edición estimada** — añadir también input date en la caja de edición (comida y ejercicio estimados)
5. **Backend** — patrón `fecha || hoy()`: aceptar `fecha` opcional, usarla si viene, fallback a hoy si no
6. **UPSERT** — para tablas con lógica upsert por día (ej: pasos), usar la fecha recibida en el lookup, no `hoy()`
```javascript
// Backend — patrón clave
const { fecha, ... } = req.body;
sql_run('INSERT INTO ... fecha VALUES ?', [req.userId, fecha || hoy(), ...]);
// UPSERT — lookup con la fecha recibida
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:**
1. **Identificar el bloque completo** — buscar el comentario que inicia (`// === DARK MODE TOGGLE ===`) y el cierre (`})();`)
2. **Eliminar con `content[:start] + content[end:]`** — NO usar `re.sub`
3. **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:**
```python
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")
# Verificar integridad del archivo
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:
1. **Datos reales** — peso actual, kg perdidos, ritmo semanal (o "sin datos" si no hay)
2. **Acciones rápidas** — botones para "Registrar" y "Hablar con IA" (las 2 acciones principales)
**Patrón de hero correcto:**
```html
<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>
<!-- Quick Status con datos reales -->
<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>
<!-- Acciones rápidas -->
<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>
```
Ver en GitHub