| name | esios-dashboard |
| description | Proyecto completo de dashboard energético español con datos ESIOS/REE — server, frontend, PDF, Telegram cron, deploy NaN. |
| version | 2.3.0 |
| author | Ntizar |
ESIOS Dashboard
Proyecto completo de dashboard energético español con datos ESIOS/REE.
Ubicación
- Repo:
/root/workspace/esios-dashboard (clon de Ntizar/esios-dashboard)
- URL pública:
https://esios-dashboard-ntizar-ntizar.apps.nan.builders
- Puerto NaN: 4000
- Mastermind sync:
/hermes-home/skills/mastermind/esios-dashboard
Arquitectura del Dashboard Principal (v3.0 — 2026-05-28)
Pestaña Precio — Pool vs PVPC (v2.3 — mayo 2026)
La pestaña Precio se ha enriquecido con una comparativa completa Pool OMIE vs PVPC:
Indicadores:
- Pool OMIE: ID 600 — Precio mercado SPOT Diario (€/MWh, DIRECTO)
- PVPC 2.0TD: ID 1001 — Precio voluntario pequeño consumidor (€/MWh, DIRECTO)
Qué se muestra (en orden, de arriba a abajo en la pestaña):
-
Panel explicativo 📚 con:
- Columna Pool OMIE vs PVPC con descripciones
- Desglose de componentes: Pool + Restricciones + Intradiarios + Secundaria + Desvíos + Capacidad + Peajes = PVPC
- Cada componente como badge con tooltip explicativo
-
Gráfico comparativo "Pool vs PVPC" (chartPoolVsPvpc):
- Doble línea: Pool en azul sólido (#2563eb) + PVPC en naranja punteado (#f97316)
- Tooltip muestra ambas series + diferencia calculada en footer
- Interacción: mode='index' para comparar hora a hora
-
Gráfico de diferencia "PVPC − Pool" (chartDiferenciaPool):
- Barras coloreadas: azul si < media, naranja si > media
- Tooltip muestra Pool → PVPC (€/MWh) en detalle
- El usuario ve de un vistazo los componentes adicionales del PVPC
-
Gráfico PVPC original (se mantiene con área sombreada)
-
Histograma de distribución de precios (se mantiene)
Métricas de resumen:
- Subtítulo de precio muestra:
Pico: X · Valle: Y €/MWh · Diferencia: Z €
- Backend devuelve
pool_medio, pool_pico, pool_valle en el resumen
Tabla horaria:
- Nueva columna
Pool €/MWh a la izquierda del PVPC
Previsión ocultada:
- La pestaña de Previsión está comentada en HTML (
<!-- Previsión ocultada -->) — lista para reactivar cuando esté disponible, sin borrar el código
Para implementar esta extensión en otro proyecto:
- En
summary.service.js:
- Añadir
pool: 600 a TELE_IDS
- Añadir
fetchIndicator(TELE_IDS.pool, fecha, token) después de precioData
- Añadir
poolData al destructuring
- Añadir
const pool = buildHourlySeries(TELE_IDS.pool, poolData, fecha)
- Añadir
mergeSeries(hourlyMap, pool, 'pool')
- Añadir
pool_medio/pool_pico/pool_valle al resumen
- Repetir en
buildSummary5min (mismo patrón)
- En
render.js:
- Antes del chartPrecio existente, crear chartPoolVsPvpc (Chart.js type:'line', 2 datasets)
- Crear chartDiferenciaPool (Chart.js type:'bar' con colores dinámicos)
- En
renderMetrics, añadir cálculo de diferencia en el subtítulo de precio
- En
render-final.js:
- Añadir columna
pool al array tableColumns (antes de precio)
- En
index.html:
- Añadir card con panel explicativo (
.pool-pvpc-explanation, .components-breakdown)
- Añadir
chartPoolVsPvpc y chartDiferenciaPool en sus chart-cards
- Comentar la pestaña previsión:
<!-- <button ...>🔮 Previsión</button> -->
- Comentar la sección previsión:
<!-- <div id="section-prevision"> ... </div> -->
- CSS necesario:
.pool-pvpc-explanation, .pool-def, .pvpc-def, .components-breakdown, .component-tag, .component-tag.pool-tag, .component-tag.plus, .component-eq
Estructura de HTML
- 5 tabs visibles: Resumen, Precio, Demanda, Mix, Interconexiones
- Previsión ocultada (comentada en HTML) — reactivar quitando comentarios
- Selector de fechas: botones ◀ 📅 ▶ + día seleccionado visible
- NO usar
fechaInput DOM element — usar AppState.fecha directamente
- Todas las tecnologías visibles: Nuclear, CC, Carbón, Cogeneración, Hidráulica, Solar FV, Solar Térmica, Eólica
Patrón AppState.fecha (CRÍTICO)
- NUNCA referenciar
document.getElementById('fechaInput') en el código
- Todas las funciones usan
AppState.fecha como fuente de verdad
setupDateNavigation() usa AppState.fecha directamente con botones ◀ 📅 ▶
cargarDatos() lee AppState.fecha || getMadridDateStr()
setupKeyboardShortcuts() usa AppState.fecha directamente
state.js NO referencia fechaInput — solo lee/escribe AppState
Scripts de frontend (orden de carga)
config.js — constantes, INDICATORS_CONFIG, techMap
state.js — AppState + persistencia
utils.js — helpers (formatNum, fmtHora, etc.)
api.js — cliente fetch
ui.js — tabs, keyboard shortcuts (NO range buttons)
data.js — cargarDatos, renderAll, setupDateNavigation
render.js — renderMetrics, renderTechCards, renderGeneracionMix, etc.
render-charts.js — gráficos adicionales
render-final.js — CO2 tracker, tabla horaria (sin init duplicado)
⚠️ Pitfall: múltiples DOMContentLoaded handlers
Tanto data.js como render-final.js tienen DOMContentLoaded. El de render-final.js llama a cargarDatos() que debe estar definido. Como data.js carga ANTES, cargarDatos está en scope global. Si hay error en data.js (ej: CDN falla), cargarDatos no se define → ReferenceError.
⚠️ Pitfall: render-final.js NO duplica init
El DOMContentLoaded en render-final.js debe ser mínimo: solo el botón de carga y auto-refresh. Todo el init (loadState, setupTabs, setupDateNavigation, setupKeyboardShortcuts) va en data.js.
Arquitectura v2.0 (modular por dominios)
esios-work/
├── server.js ← Bootstrap Express + routing (647 líneas)
├── src/
│ ├── config/
│ │ └── env.js ← Validación estricta de variables de entorno
│ ├── shared/
│ │ ├── time/madrid.js ← Centraliza Europe/Madrid
│ │ └── io/csv.repository.js ← FS async para CSV
│ ├── infra/
│ │ ├── clients/esios.client.js ← HTTP client con retry/backoff/jitter + disk cache
│ │ ├── clients/nan.client.js ← HTTP client para NaN/LLM API
│ │ ├── cache/memory-cache.js ← Cache memoria con métricas
│ │ └── cache/disk-cache.js ← Cache disco persistente (TTL 5 min)
│ ├── domains/
│ │ ├── energy/energy.service.js ← Lógica energía principal
│ │ ├── energy/summary.service.js ← Builder de resúmenes diarios
│ │ ├── forecast/montecarlo.service.js ← Simulación Monte Carlo
│ │ └── reports/report.service.js ← Informes IA + PDF
│ └── jobs/
│ ├── refresh.job.js
│ ├── telegram.job.js
│ └── daily-report.job.js
├── public/
│ ├── index.html ← HTML mínimo (223 líneas)
│ ├── css/styles.css ← Estilos extraídos
- `public/js/cache-bust.js` — Script de cache-busting dinámico (inyecta ?v=timestamp en otros scripts)
│ ├── api.js ← Cliente fetch con AbortController
│ ├── config.js ← Indicadores, techMap, constantes
│ ├── state.js ← AppState + persistencia localStorage/URL
│ ├── utils.js ← Helpers (formatNum, fmtHora, etc.)
│ ├── ui.js ← Range buttons, tabs, keyboard
│ ├── data.js ← cargarDatos + renderAll orquestador
│ ├── render.js ← Metrics, cards, gráficos principales
│ ├── render-charts.js ← Monte Carlo, gen real, interconexiones
│ └── render-final.js ← CO2 tracker, tabla horaria, init
├── tests/ ← Jest: 24 tests pasando
│ ├── api.test.js ← 8 tests integración endpoints
│ ├── env.test.js ← 2 tests variables entorno
│ ├── time.test.js ← 3 tests timezone Madrid
│ └── utils.test.js ← 11 tests utilidades datos
├── scripts/
│ └── fetch-all-indicators.js ← Usa ESIOS_API_TOKEN env (NO hardcode)
└── data/ ← CSV cache + PDF cache
Notas de arquitectura
- CommonJS estricto: Todo usa
require()/module.exports. NO usar ESM.
- Sin autollamadas HTTP internas: Los endpoints llaman servicios de dominio directamente.
- Todo I/O async:
fs/promises en toda la pila.
- Helmet + CORS whitelist: No CORS abierto.
- Método de arranque:
node server.js (no npm start en prod).
Endpoints API
| Endpoint | Descripción |
|---|
GET /api/esios/summary?fecha=YYYY-MM-DD | Todo combinado (precios, demanda, generación, CO2, interconexiones, previsión) |
GET /api/esios/precios | PVPC por hora |
GET /api/esios/demanda | Demanda real + prevista |
GET /api/esios/generacion | Generación medida por tecnología |
GET /api/esios/generacion-real | Gen renovable vs no renovable |
GET /api/esios/co2 | CO2 específico (t/MWh) |
GET /api/esios/interconexiones | Francia, Portugal, Marruecos |
GET /api/esios/prevision | Previsión eólica y solar D+1 |
GET /api/esios/prediccion | Monte Carlo precios |
GET /api/esios/informe | Informe Qwen/LLM |
GET /api/esios/pdf?fecha=YYYY-MM-DD | PDF diario |
POST /api/esios/refresh | Fuerza fetch y guarda CSV |
GET /api/esios/test-token | Diagnóstico: verifica si token ESIOS funciona |
GET /api/esios/cache-stats | Stats cache disco + memoria |
POST /api/esios/cache-clear | Limpia todo el cache (disco + memoria) |
GET /healthz | Health check |
GET /readyz | Readiness (verifica tokens) |
Indicadores ESIOS clave
Para la guía COMPLETA y actualizada de IDs y unidades, consultar la skill esios-api.
| ID | Nombre | Unidad |
|---|
| 1001 | PVPC 2.0TD | €/MWh |
| 600 | Precio mercado SPOT Diario (Pool OMIE) | €/MWh |
| 1293 | Demanda real | MW |
| 10035 | Gen medida Hidráulica | MWh (→ MW /1000) |
| 10037 | Gen medida Eólica | MWh (→ MW /1000) |
| 10041 | Gen medida Otras renovables | MWh (→ MW /1000) |
| 10205 | Gen medida solar | MW (NO kWh) |
| 10206 | Gen T.Real Solar | MW (NO kWh) |
| 4 | Nuclear programada | MWh/periodo |
| 9 | CC programada | MWh/periodo (1 valor/día) |
| 10351 | Gen T.Real renovable | MW (NO kWh) |
| 10352 | Gen T.Real no renovable | MW (NO kWh) |
| 10006 | Gen libre CO2 | MW (NO porcentaje) |
| 10355 | CO2 asociado gen real | tCO₂/h (NO tCO₂/MWh) |
| 10207 | Intercon Francia (telemedida) | MWh (→ MW /1000) |
| 10208 | Intercon Portugal (telemedida) | MWh (→ MW /1000) |
| 2052 | Demanda prevista nacional | MW |
| 1777 | Previsión D+1 eólica | MW |
| 1779 | Previsión D+1 fotovoltaica | MW |
| 10358 | Previsión D+1 renovable total | MW |
IDs que ya NO existen: 460 (previsionDemanda), 541 (previsionEolica), 10350 (no es previsión).
IDs de interconexión incorrectos: 10014 (P48), 10015 (P48) → usar 10207, 10208 (telemedida).
Referencia de indicadores completa
El archivo data/esios-indicator-index.json contiene los 2018 indicadores con unidades, tipos y descripciones. Siempre validar IDs contra este archivo antes de hardcodear. El archivo está también en references/esios-indicator-reference.md con una tabla resumida de los IDs críticos.
⚠️ Reglas críticas
- Unidades ESIOS: Los indicadores de telemedida (10035, 10037, 10041, 10043, 10207, 10208) devuelven MWh, NO kWh ni MW. Siempre dividir entre 1000.
- ⚠️
genOtrasRenMedida: ID 10041 (medida), NO 10042 (P48 programada). Usar 10042 causa datos null/incorrectos.
- CO2 Libre (10006): NO es porcentaje ni kWh — es
MW según esios-indicator-index.json.
- CO2 Real (10355): Es
tCO2/h (tasa horaria), NO tCO₂/MWh.
- Ciclo Combinado (9): Solo 1 valor diario (programación PBF), no por hora.
- CO2 total estimado =
gen_total_MWh * factor_específico_CO2.
- Previsión D+1: Usar IDs 2052, 1777, 1779, 10358. NUNCA 460, 541, 10350.
Deploy en NaN
- Push a
main → auto-deploy con Kaniko
- Variables de entorno:
ESIOS_API, OPENAI_API_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
- Puerto: 4000 (configurado en Dockerfile y server.js)
⚠️ Pitfall: Siempre hacer git push + verificar deploy tras cambios locales
El dashboard se despliega en GitHub → NaN.builders mediante auto-deploy con Kaniko. Los cambios locales en /root/workspace/esios-dashboard/ no se reflejan en producción hasta que se hace push a GitHub y NaN reconstruye el contenedor.
Flujo correcto tras cualquier cambio local:
cd /root/workspace/esios-dashboard
git add -A
git commit -m "feat: descripción del cambio"
git push origin main
Si no se despliega tras push (el contenedor antiguo sigue corriendo):
git commit --allow-empty -m "chore: trigger redeploy NaN"
git push origin main
Verificar que el nuevo código se sirve: comprobar que el HTML incluye los cambios esperados:
curl -s https://esios-dashboard-ntizar-ntizar.apps.nan.builders/ | grep -c 'característica_nueva'
📚 Referencias
references/dashboard-architecture-v3.md — Arquitectura del dashboard principal v3.0 (tabs, AppState.fecha, orden de scripts)
references/tdz-and-promise-all-fix.md — Guía de TDZ errors y anti-patrones Promise.all con ejemplos de código
Simuladores del Sistema Eléctrico
SistemaElectricoFuturo (Ntizar, repo propio) ⭐1
URL: https://ntizar.github.io/SistemaElectricoFuturo/ | v3.5 | MIT
Simulador interactivo del sistema eléctrico español con horizonte 2026-2035. Combina simulación anual de 8.760 horas con trayectoria multianual.
18 escenarios: cierre ENRESA, VE masivo, autoconsumo 30 GW, crisis gas, ley climática, sequías, ola de calor, datos REE reales...
Stack: Vue 3 + Plotly + Ntizar Aurora v5 + Liquid Glass
Características: demanda sectorial, calendario nuclear ENRESA, almacenamiento avanzado (degradación baterías, bombeo, V2G), política energética (tope ibérico, CfDs, peajes dinámicos, PVPC), datos REE en tiempo real + normativa vigente + informes CNMC
Patrón: Añadir sección "Datos en Tiempo Real + Normativa" a un simulador estático
Cuando quieras enriquecer un dashboard/ simulador con datos oficiales actuales, normativa y regulatory framework:
-
Crear archivo independiente (js/ree-data.js) con datos estructurados:
- Demanda real y prevista (MW)
- Estructura de generación por tecnología (capacidad, generación TWh, % participación, tendencia)
- Normativa vigente (leyes, RD, planes, mecanismos) con estado e impacto
- Informes oficiales (CNMC, REE, comisiones)
- Indicadores de mercado (precios, CO₂, interconexión)
- Objetivos PNIEC/planificación
-
Crear pestaña en el sidebar del dashboard:
- Añadir tab al array SIDE_TABS en setup de Vue
- Renderizar secciones HTML con los datos del módulo
- Usar computed properties para formatear (reeGeneracion, reeNormativa, reeInformes…)
-
Cargar en onMounted():
const reeData = ref(null);
const reeGeneracion = ref([]);