{
“sources”: [
{“type”:“commit”,“ref”:“579c3d06ee3e8bdefdb74a98578a8fe0bfd6f631”,“last_seen”:“2026-08-25”},
{“type”:“commit”,“ref”:“8ed4057e5281060f8d656c6b1855e070d23ab20b”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“static/js/pages/observatory.js”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“static/js/pages/observatory/ObservatoryWebSocket.js”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“static/js/pages/observatory/ObservatoryChartDefaults.js”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“templates/monitoring/observatory.html”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“frontend/vitest.static.config.ts”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“frontend/src/tests/static/observatory_ws_contract.test.js”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“frontend/src/tests/static/observatory_chart_defaults.test.js”,“last_seen”:“2026-08-27”},
{“type”:“code”,“ref”:“terminal/api/sentinel_ingest.py”,“last_seen”:“2026-08-25”},
{“type”:“code”,“ref”:“tests/monitoring/test_static_module_versions.py”,“last_seen”:“2026-08-25”},
{“type”:“doc”,“ref”:“CHANGELOG.md”,“last_seen”:“2026-08-27”},
{“type”:“doc”,“ref”:“RELEASE_NOTES.md”,“last_seen”:“2026-08-27”}
],
“content”: ”## User value\n\nCuarto ciclo de la auditoría profunda del monitoraje (task #261), esta vez centrado en cablear bien la pantalla del Observatory. La pestaña llevaba abierta todo el día en la sala de control y varios hallazgos se acumulaban sin que nadie lo notara: la conexión en vivo por WebSocket dejaba de reconectar PARA SIEMPRE tras un rato de red mala o, simplemente, tras un despliegue (Daphne — el servidor ASGI — se reinicia en cada merge a main, que es deploy directo a PROD); el estado arriba/abajo de un equipo tardaba hasta un minuto en reflejarse cuando quien sondeaba era el Agente local en vez del navegador; un Agente secundario que se apagaba pintaba toda la Observatory en rojo aunque el primario siguiera sano; y la consola de ping no abría nunca para un target creado a mano. Por debajo, un nombre de dispositivo o de target — que rellena cualquier usuario con permiso observatory.edit, o el propio sysName SNMP del equipo — podía inyectar HTML en la página de cualquiera que abriera esa pestaña (XSS almacenado intra-organización).\n\nCierre de la auditoría (27-08-2026, A36): dos días después, un último commit remató el mismo cableado y cerró formalmente la auditoría #261 — con él, los seis ciclos de arreglo de la auditoría del 24-08 quedan en producción (v1.83.0 → v1.85.8, Agente 2.21.0 → 2.23.1).\n\n## Qué vas a notar\n\n- La conexión en vivo ya no se rinde nunca: tras una caída larga, reconecta sola al volver a la pestaña o al recuperar la red.\n- El estado arriba/abajo de cada dispositivo se actualiza al momento también en modo Sentinel (antes había que pasar por Overview para verlo fresco).\n- Con dos Agentes, que el secundario se desconecte ya no pinta todo en rojo.\n- La consola ping funciona en los targets creados manualmente.\n- El estilo del panel Resources (CPU/Memoria) por fin se aplica.\n- Una pestaña recién habilitada se refresca sola, sin cambiar de pestaña y volver.\n- La interfaz en español ya no pierde el selector de rango ni el control de suavizado.\n- (A36) Las filas Anim y AL del panel Chart Defaults ya no dependen del texto de su etiqueta: con la interfaz en otro idioma seguían reflejando la preferencia guardada.\n- (A36) Un tipo de mensaje nuevo del WebSocket ya no se pierde en silencio: error se ve en la consola del navegador y el resto de tipos documentados se ignoran a propósito.\n\n## Implementación\n\n1. A35 · XSS almacenado (seguridad): observatory.js interpolaba device.name / target.name en el título de la pestaña y device_name del port-connection directamente en innerHTML, sin escapar (3 puntos). Ahora pasan por escHtml/escAttr (static/js/utils/html.js).\n2. A31 · backoff del WebSocket sin tope (ObservatoryWebSocket.js): antes, 5 fallos seguidos (≈31 s) o 20 en toda la sesión agotaban el contador _totalReconnectAttempts — que nunca se reseteaba — y la pestaña se quedaba sin metric_update/agent_status hasta recargar a mano. Ahora el backoff exponencial tiene techo de 30 s sin límite de intentos, el contador se resetea al abrir la conexión (onopen), y se instalan listeners de visibilitychange/online que fuerzan una reconexión inmediata. El JSON.parse del onmessage va envuelto en try/catch para no morir con un frame corrupto.\n3. A32 · estado vivo también en modo Sentinel (terminal/api/sentinel_ingest.py::_broadcast_live_status, llamada desde receive_bulk_metrics): por cada lote de métricas del Agente emite metric_update (status/latencia/pérdida) al broadcast_metric_update del consumer de tiempo real, best-effort (nunca rompe el ingest si falla). Antes solo lo emitía el ping lanzado desde el SaaS.\n4. A33 · solo el Agente primary gobierna “Agent offline” (ObservatoryWebSocket.js::handleAgentStatusChange): el evento trae role; si es secondary solo refresca el widget de flota, no dispara el aviso de caída (sin role = Agente antiguo, comportamiento previo intacto).\n5. A30 · un módulo, un ?v=: ObservatoryCNS.js se importaba con ?v=7 desde observatory.js y ?v=5 desde ObservatoryWebSocket.js — dos URLs, dos instancias del módulo. La instancia del WebSocket tenía su estado interno (_tab) sin inicializar, así que loadCNSData() al recibir un insight_update era un no-op mudo (footgun s281). Mismo patrón corregido en ObservatoryCharts.js y ObservatoryAlerts.js. Nuevo test tests/monitoring/test_static_module_versions.py que recorre todo static/js y falla si un módulo vuelve a importarse con versiones distintas.\n6. Otros cables sueltos del mismo ciclo: A07 (la consola ping para un target manual buscaba la pestaña por deviceId, siempre null en un target manual — ahora resuelve por id como el resto), A28 (el dropdown de estilo apuntaba a resources-N, que no existe — ahora a cpu-N/memory-N), A29 (una pestaña recién habilitada arranca su auto-refresco sin cambiar de pestaña), A34 (comparaciones por texto traducido 'Range:'/'Smooth' sustituidas por data-range-selector/data-defaults-row), A37 (receive_deep_discovery_result solo reescribe selected_interfaces y notifica al Agente si el valor cambió de verdad — antes reescribía y re-empujaba TODOS los targets en cada resultado, hasta 124 veces en un sweep con una pestaña abierta).\n7. A36 · cierre con tests de contrato (vitest+jsdom, 27-08-2026): dos días después, A34 solo había arreglado la fila smoothing por data-defaults-row — las filas Anim y AL seguían buscándose por el texto de su etiqueta ('Anim'/'AL'), así que con la interfaz en otro idioma dejaban de reflejar la preferencia guardada. Ahora las tres filas llevan data-defaults-row en la plantilla (templates/monitoring/observatory.html) y el JS selecciona por ese atributo (ObservatoryChartDefaults.js?v=4). A la vez, ObservatoryWebSocket.js?v=13 gana rama explícita para los 4 tipos de mensaje que MonitoringConsumer podía mandar y el navegador descartaba sin decir nada: unsubscribed y pong se ignoran a propósito, job_progress lo consumen otras páginas por su propio socket, y error se registra en consola. Para que esto no vuelva a pasar, frontend/vitest.static.config.ts (nueva config, reutiliza vitest+jsdom ya instalado en frontend/) monta 4 suites de contrato sobre los módulos reales de static/js con su ?v=: todo type que el consumer manda tiene rama en handleWebSocketMessage, getRefreshInterval respeta el suelo de 15 s, PingConsole encuentra la pestaña target-N/device-N, y el panel Chart Defaults (montado desde la plantilla real, con etiquetas traducidas) refleja las preferencias. El pre-push (scripts/ci/pre_push_tests.sh) los lanza cuando el push toca static/js (avisa y deja pasar si falta node_modules, el CI los corre siempre). De paso, frontend/biome.json pasa a \"root\": false — sin eso, el hook pre-commit que lintea desde la raíz rechazaba cualquier commit que tocara frontend/ (“Found a nested root configuration”, Biome 2).\n\n## Tests\n\n- tests/monitoring/test_static_module_versions.py — recorre static/js/**/*.js, agrupa los imports from '...X.js?v=N' por módulo resuelto y falla si el mismo módulo aparece con más de una versión entre sus importadores.\n- frontend/src/__tests__/static/observatory_ws_contract.test.js — todo type que MonitoringConsumer manda al navegador tiene rama en handleWebSocketMessage y no hay ramas huérfanas.\n- frontend/src/__tests__/static/observatory_autorefresh.test.js — getRefreshInterval sigue interval_seconds con suelo de 15 s.\n- frontend/src/__tests__/static/observatory_ping_console.test.js — la consola de ping encuentra la pestaña target-N de un target manual (A07) y device-N de un dispositivo.\n- frontend/src/__tests__/static/observatory_chart_defaults.test.js — el panel Chart Defaults, montado desde la plantilla real con las etiquetas traducidas, refleja las preferencias guardadas (Anim/AL/Smooth por data-defaults-row).\n\n## Commits relacionados\n\n- 579c3d06 (2026-08-25) — fix(monitoring): ciclo 4 de la auditoría #261 - cableado de la interfaz del Observatory - v1.84.1 (#436).\n- a15e327f (2026-08-24) — ciclo 1 de la misma auditoría (métricas SNMP), ver [[feature—monitoring—auditoria-261-ciclo-1-metricas-snmp]].\n- 8ed4057e (2026-08-27) — feat(tests): A36 #261 - tests de contrato del JS del Observatory con vitest+jsdom + 4 tipos WS sin rama y filas Anim/AL por data-defaults-row - v1.85.8 (#445). Cierra la auditoría #261.\n\n## Límite honesto\n\n- A10 queda para el Agente 2.22.0: cada resultado de deep discovery de un solo AP sigue incrementando el contador de job de TODA la flota (un AP suelto adelanta el “Complete” del sweep) — exige que el Agente devuelva job_key; va en el PR del Agente (task #265).\n- El fix de XSS es parcial: observatory.js queda limpio, pero la Auditoría Suprema Frontend FE5a ya había documentado el mismo patrón de innerHTML sin escapar en ObservatoryTabs.js (cabecera de pestaña de device), ObservatoryAlerts.js (modal de severidad) y ObservatorySettings.js (opciones de interfaz SNMP) — los 3 siguen sin tocar tras este commit.\n- Los tests de A36 son de contrato, no de comportamiento visual: un tema lavado o una gráfica mal pintada siguen necesitando un click-test en el navegador.\n- Lo que queda de la auditoría #261 tiene task propia y no está en este ciclo: verificación con la flota real del CCIB en septiembre (#271), la próxima compilación del Agente (#270) y la fase 2 del contrato único de métricas (#272).\n\n## Véase también\n\n- [[feature—monitoring—auditoria-261-ciclo-1-metricas-snmp]]\n- [[decision—20260604—broken-access-control-vistas-observatory]]\n- [[feature—monitoring—auditoria-suprema-sa6-observatory-realtime]]\n- [[feature—monitoring—alertas-via-viva-agente]]\n- [[entity—monitoring—consumer—realtime-websocket]]\n- [[crearack—monitoring—fleet-manager]]\n- [[concept—terminal—local-agent]]\n”
}
Referenciado desde
- Ancho de banda por interfaz SNMP · Agente 2.22.0 cierra A10 y A47 de la auditoría #261
- Deuda #274 Tanda A — alertas en tiempo real y datos honestos en Wireless (v1.85.9)
- Deuda #274 Tanda B — puerto sin recarga, deep discovery deduplicado y un solo cargador de series en Wireless (v1.86.17)
- Las gráficas del Observatory salían vacías a ratos: el suavizado se leía fuera del hilo del ORM (task #297)