Patrón Monitoring* — bases comunes + descriptor por dominio (serie F1, auditoría s187)
Patrón Monitoring* — bases comunes + descriptor por dominio
Cierre documental de la serie R5-F1 de la auditoría s187 (PRs #242, #243, #249, #253, #265, #266 · v1.40.3 → v1.43.3). Verificado contra código el 03-07-2026.
0 · En dos palabras (para todo el equipo)
CreaRack tiene tres monitores gemelos — UPS, Wireless y Digital Signage — que durante meses evolucionaron como tres copias del mismo frontend (~72% de líneas idénticas medidas en la auditoría). La serie F1 los puso a los tres sobre una única implementación por pieza (dashboard, report, grids de charts, detalle de grupo y fontanería del detalle), donde cada monitor aporta solo un “carnet de identidad” (su descriptor: endpoints, métricas, textos, colores). Resultado: ~−3,3k LOC y, sobre todo, que un fix o mejora futura se hace UNA vez y llega a los tres a la vez — como pasó con el dial de cadencia (task #175), que se cableó en las bases y funcionó en los tres monitores el mismo día.
1 · El patrón
Cada pieza de la familia es una base común (clase o factoría) en static/js/pages/monitoring/ que concentra la MECÁNICA, y un descriptor por dominio (~20-100 LOC) en pages/ups|wireless|signage/ que aporta lo específico:
static/js/pages/monitoring/ ← las bases (mecánica)
├── MonitoringDashboard.js (632) PR1/PR2 · overview GridStack
├── MonitoringReport.js (439) PR3 · report histórico + CSV + print
├── MonitoringChartGrid.js (197) PR4 · grid de paneles + pills + layouts LS
├── MonitoringChartPanel.js (291) PR4 · panel de chart individual
├── MonitoringGroupDetail.js (228) PR5 · detalle de grupo
└── MonitoringDetailPlumbing.js (235) PR6 · deep discovery + WS realtime (mixins)
static/js/pages/ups/UpsDashboard.js (~135) ← descriptores (identidad)
UpsChartGrid.js (~21) «class X extends Base { constructor(o){ super(o, DESCRIPTOR) } }»
UpsGroupDetail.js(~57) «export const X = createBase({ ...descriptor })»
... (y equivalentes wireless/signage)
Dos variantes de composición conviven, elegidas por lo que ya existía en cada sitio:
- Clase + descriptor (
extendsconsuper(opts, DESCRIPTOR)): Dashboard, Report, ChartGrid, ChartPanel. - Factoría / mixin +
Object.assign(createMonitoringGroupDetail(d),createDeepDiscoveryMixin(d),createRealtimeWsMixin(d)): GroupDetail y la fontanería del Detail — los dominios eran object literals con otros mixins ya fusionados.
Los strings traducibles del descriptor van como thunks () => t('...') (se evalúan en render; el literal queda en el archivo del descriptor para el extractor del .po).
2 · Qué centraliza cada base
| Base | Mecánica que concentra | Lo que aporta el descriptor |
|---|---|---|
MonitoringDashboard | GridStack (drag/resize/save/reset layout), heatmap 24h, widgets, timers del overview | charts del registry, stat tiles, layoutKeys EXACTAS, textos, render custom |
MonitoringReport | Rango From/To, stats, charts, deep-tables, Print, Export CSV con sanitización (heredada por los 3 — antes solo UPS la tenía) | métricas, columnas, títulos |
MonitoringChartGrid | Toolbar con pills 1h/4h/12h/24h, add/remove paneles, persistencia del layout y del rango en localStorage, echarts.connect, realtime WS, destroy | registry, defaultLayout, maxPanels, lsPrefix, pillClass, PanelClass (¡inyectada!), extras de toolbar |
MonitoringChartPanel | Panel individual: fetch, estilos, dropdown de métrica, remove | descriptor de series/ejes (UPS y Signage; WirelessChartPanel se INYECTA intacto — multi-serie, no se fusionó) |
MonitoringGroupDetail | Layout del tab de grupo, summary con stat cards, tabla de miembros ordenable, binding Port Assign, timers (charts 120s, summary 60s), cleanup | stats, columnas/filas de miembros, fuente de devices, endpoint group-summary, extras (SSIDs de Wireless, badge-click) vía hooks |
MonitoringDetailPlumbing | Deep discovery (despacho al Agente + fallback REST a 127.0.0.1:5050, poll 30×3s, disparo silencioso al abrir, timer 5 min, refresh manual del botón) y WS realtime (/ws/monitoring/, batching de metric_update a 100 ms, indicador .ws-realtime-active, backoff 1→32s, repace() del dial #175) | tabPrefix, renderDeep, botón/toast del refresh, onExtraMessage (Wireless: job_progress) |
3 · Invariantes SAGRADOS (romperlos = romper a usuarios)
lsPrefixy las claves de localStorage son EXACTAS pre-refactor por dominio (ups_chart_layout_v1_…): cambiarlas borra los layouts guardados de todo el mundo. El rango horario va en clave separada (hours_) para no invalidar el JSON del layout.- Los IDs del DOM se conservan (
ups-chart-grid-<id>,wireless-group-summary-<id>…): hay código que los busca por id. - El contrato público de cada módulo no cambia:
show*Detail/showGroupDetail/cleanupTab/repacey el Map_chartGrids(los page controllers lo leen pararesizeAll()al cambiar de pestaña). Único método con caller externo por nombre:WirelessDetail.refreshApData(bridgeWireless._detail_refresh). layoutKeysdel Dashboard exactas — mismos motivos que el punto 1.- Timers: charts de detalle acompasados a la cadencia del target (
max(polling_interval, 15)s, 120s sin dato — task #175); grupos a 120s fijo; deep-data 5 min; summary de grupo 60s.
4 · Cómo añadir un dominio nuevo (receta)
- Crea su
ChartRegistry(métricas → series/ejes/formatters) y, si su panel es estándar, un descriptor deMonitoringChartPanel; si es especial, inyecta tu clase de panel (patrónWirelessChartPanel). class NuevoChartGrid extends MonitoringChartGridcon su descriptor (lsPrefixNUEVO, no recicles el de otro dominio).- Dashboard, Report y GroupDetail: un descriptor por pieza calcando cualquiera de los existentes (UPS es el más simple).
- Detail: monta tu render por dominio y fúndele
createDeepDiscoveryMixin(+createRealtimeWsMixinsi va con WS propio). - Todos los strings visibles con
t()/tc()(los del descriptor como thunks) y escape SIEMPRE por imports estáticos deutils/html.js(regla R5a — jamáswindow.escapeHtml ||a nivel top de módulo).
5 · Qué NO se fusiona (decidido en la auditoría, no lo “arregles”)
*DetailCharts/ render por dominio (tablas de radios/SSIDs vs batería vs playlists): ~13% común — fusionarlos costaría más de lo que ahorra.WirelessChartPanel: multi-serie, se inyecta tal cual en el grid base.- El WS de Signage (
SignageDetailCharts.js): vive con su grid en funciones de módulo; migrarlo implicaría reescribir ese archivo entero. - El refresh manual de Wireless (
refreshApData): espera la finalización por push WS (job_progress) con polling de seguridad — se apoya en_dispatchDeepDiscovercomún pero no se genericiza. ChartRegistries: ya SON descriptores (datos, no mecánica).- El CMS de Signage: otro dominio funcional, fuera de la familia.
6 · Historia de la serie (PRs)
| PR | Versión | Pieza | LOC |
|---|---|---|---|
| #242 (PR1) | v1.40.3 | MonitoringDashboard + UPS 789→135 | base nueva |
| #243 (PR2) | v1.40.4 | Signage 772→140 · Wireless 794→185 | −1.529/+336 |
| #249 (PR3) | v1.40.5 | MonitoringReport ×3 (CSV sanitizado para los 3) | −754 |
| #253 (PR4) | v1.40.6 | MonitoringChartGrid/Panel ×3 | −515 |
| #265 (PR5) | v1.43.2 | MonitoringGroupDetail ×3 | −269 |
| #266 (PR6) | v1.43.3 | MonitoringDetailPlumbing (deep discovery ×3 + WS ×2) | −171 |
Todos con click-test de Edu en PROD. Serie CERRADA el 03-07-2026.
Véase también
- [[crearack-tech—frontend—centralized-services]]
- [[crearack-tech—frontend—performance-guidelines]]
- [[feature—monitoring—dial-cadencia-sondeo]]
- [[crearack-tech—architecture—sentinel-mode]]