// ==UserScript==
// @name OLT Monitor Maestro
// @namespace Violentmonkey Scripts
// @match *://190.153.58.82/monitoring/olt/*
// @version 16.6
// @inject-into content
// @run-at document-end
// @author Ing. Adrian Leon
// @updateURL https://raw.githubusercontent.com/TakRiuto/ACSScripts/release/Monitor.user.js
// @downloadURL https://raw.githubusercontent.com/TakRiuto/ACSScripts/release/Monitor.user.js
// @grant GM_fetch
// @connect raw.githubusercontent.com
// @icon https://avatars.githubusercontent.com/u/20828447?v=4
// ==/UserScript==
(async function() {
'use strict';
if (window.self !== window.top) return;
// =========================================================
// DB
// =========================================================
let DB_NODOS = {};
let dbCargada = true;
try {
DB_NODOS = await fetch(`https://raw.githubusercontent.com/TakRiuto/ACSScripts/refs/heads/release/nodos.json?t=${Date.now()}`)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
});
console.log('✅ DB_NODOS cargado:', Object.keys(DB_NODOS).length, 'nodos');
} catch (e) {
console.error('❌ Error cargando nodos.json:', e);
dbCargada = false;
}
// =========================================================
// ESTADO
// =========================================================
const resultadosRCA = new Map();
const CAUSAS_CONOCIDAS = {
'dying-gasp': { titulo: 'Pérdida de alimentación eléctrica', icono: '⚡' },
'LOS': { titulo: 'Loss of Signal — Pérdida de señal óptica', icono: '🔴' },
'LOSi': { titulo: 'Loss of Signal — Pérdida de señal óptica (intermitente)', icono: '🟡' },
'power-off': { titulo: 'Apagado de equipo', icono: '🔌' },
'link-down': { titulo: 'Enlace caído', icono: '🔗' },
'deactive': { titulo: 'Falso positivo (deuda/desconexión adm.)', icono: '💡' },
'LOFi': { titulo: 'Loss of Frame — Pérdida de trama', icono: '🔗' },
'SUSPENDED': { titulo: 'Suspendido (Admin)', icono: '🚫' },
'OTHER': { titulo: 'Falla Desconocida', icono: '❓' }
};
function _oltNormCausa(raw) {
if (!raw || raw === "-") return null;
const k = raw.trim();
if (k === "LOBi" || k === "LOSi/LOBi" || k === "LOBi/LOSi" || k.includes("LOSi")) return "LOSi";
if (k.toUpperCase() === "LOFI") return "LOFi";
return k;
}
function obtenerTokenAcs() {
const storages = [localStorage, sessionStorage];
for (let storage of storages) {
for (let i = 0; i < storage.length; i++) {
let key = storage.key(i);
let val = storage.getItem(key);
if (!val) continue;
// Si es un JWT puro en el storage
if (val.startsWith('eyJ') && val.split('.').length >= 3) return val;
// Si está dentro de un JSON (ngStorage, redux, etc.)
if (val.includes('eyJ')) {
try {
let parsed = JSON.parse(val);
// Exploración profunda para encontrar cualquier string JWT
const traverse = (obj) => {
for (let k in obj) {
if (typeof obj[k] === 'string' && obj[k].startsWith('eyJ') && obj[k].split('.').length >= 3) {
return obj[k];
} else if (typeof obj[k] === 'object' && obj[k] !== null) {
const found = traverse(obj[k]);
if (found) return found;
}
}
return null;
};
const found = traverse(parsed);
if (found) return found;
} catch(e) {}
}
}
}
// Buscar en cookies por si acaso
const cookies = document.cookie.split(';');
for (let c of cookies) {
let val = c.trim().split('=')[1];
if (val && val.startsWith('eyJ') && val.split('.').length >= 3) return val;
}
return '';
}
async function ejecutarRCA(idNodo, oltId, slotId, portId) {
if (!oltId) return;
const token = obtenerTokenAcs();
resultadosRCA.set(idNodo, { status: 'loading', text: '⏳ Analizando causa raíz...', ts: Date.now() });
if (!token) {
resultadosRCA.set(idNodo, { status: 'error', text: '🔑 Sin Token de Auth para RCA', ts: Date.now() });
return;
}
try {
const filterObj = {
"skip": 0, "limit": 100,
"where": {"and": [{"slotId": slotId.toString()}, {"portId": portId.toString()}, {"generalStatus": "OFFLINE"}]}
};
const filter = encodeURIComponent(JSON.stringify(filterObj));
const url = `https://${window.location.host}/api/fttx/olts/${oltId}/active-devices?filter=${filter}`;
const res = await fetch(url, {
headers: {
"accept": "application/json",
"x-access-token": token
}
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!Array.isArray(data) || data.length === 0) {
resultadosRCA.set(idNodo, { status: 'done', text: '🦕 Sin reporte de falla en OLT', ts: Date.now() });
return;
}
const causasCount = {};
let validCount = 0;
data.forEach(ont => {
let causa = (ont.lastDownCause && ont.lastDownCause.trim() !== '-') ? ont.lastDownCause.trim() : null;
causa = _oltNormCausa(causa);
if (causa) {
causasCount[causa] = (causasCount[causa] || 0) + 1;
validCount++;
}
});
if (validCount === 0) {
resultadosRCA.set(idNodo, { status: 'done', text: '🦕 Caída sin diagnóstico', ts: Date.now() });
return;
}
let dominante = null;
let max = 0;
for (const [c, cnt] of Object.entries(causasCount)) {
if (cnt > max) { max = cnt; dominante = c; }
}
const pct = Math.round((max / validCount) * 100);
const info = CAUSAS_CONOCIDAS[dominante] || { titulo: dominante, icono: '⚠️' };
let desglose = "";
if (Object.keys(causasCount).length > 1) {
const porcentajes = {};
const restos = [];
let suma = 0;
for (const [mot, cnt] of Object.entries(causasCount)) {
const pctExacto = (cnt / validCount) * 100;
const pctEntero = Math.floor(pctExacto);
porcentajes[mot] = pctEntero;
suma += pctEntero;
restos.push({ mot, resto: pctExacto - pctEntero });
}
restos.sort((a, b) => b.resto - a.resto);
let faltante = 100 - suma;
for (let i = 0; i < faltante; i++) {
porcentajes[restos[i].mot]++;
}
const partes = [];
for (const [mot, pctVal] of Object.entries(porcentajes)) {
if (pctVal > 0) partes.push(`${mot}: ${pctVal}%`);
}
desglose = partes.join(" · ");
}
resultadosRCA.set(idNodo, {
status: 'done',
dominante: dominante,
text: `${info.icono} RCA: ${info.titulo} (${pct}%)${desglose ? `
[ ${desglose} ]` : ''}`,
ts: Date.now()
});
} catch (e) {
console.error("Error en RCA para", idNodo, e);
resultadosRCA.set(idNodo, { status: 'error', text: '❌ Falla comunicación RCA', ts: Date.now() });
}
}
let oltActual = "";
let modoCargaInicial = true;
let umbralValor = parseFloat(localStorage.getItem('oltUmbralValor')) || 30;
let umbralTipo = localStorage.getItem('oltUmbralTipo') || 'porcentaje';
let filtroOp = localStorage.getItem('oltFiltroOp') || 'TODOS';
let logBusqueda = '';
let escala = parseFloat(localStorage.getItem('oltPanelEscala')) || 1;
let panelState = 'minimizado';
let modoPreferido = localStorage.getItem('oltModoPreferido') || 'abierto';
let ultimoHTMLRenderizado = '';
let ultimoEstadoPestana = { criticos: -1, hayNuevos: null };
const registroNodos = new Map();
const memoriaRedGlobal = new Map();
// =========================================================
// AUDIO
// =========================================================
const AUDIOS = {
dosimeter: new Audio('https://actions.google.com/sounds/v1/alarms/dosimeter_alarm.ogg'),
alarm: new Audio('https://www.myinstants.com/media/sounds/alarm.MP3'),
alarmabrazil: new Audio('https://www.myinstants.com/media/sounds/brazil-alarm.mp3'),
alarmhard: new Audio('https://www.myinstants.com/media/sounds/chicken-on-tree-screaming.mp3'),
chevette99: new Audio('https://soundbible.com/grab.php?id=2214&type=mp3')
};
Object.values(AUDIOS).forEach(a => { a.preload = 'auto'; a.loop = true; });
const alarmaGuardada = localStorage.getItem('oltAlarmaSeleccionada') || 'dosimeter';
let sonidoAlerta = AUDIOS[alarmaGuardada] || AUDIOS.alarm;
let silenciado = false;
let muteGlobal = localStorage.getItem('oltMuteGlobal') === 'true';
let enPrueba = false;
let audioAutorizado = false;
document.addEventListener('click', () => {
if (!audioAutorizado) {
audioAutorizado = true;
Object.values(AUDIOS).forEach(a => {
a.play().then(() => { a.pause(); a.currentTime = 0; }).catch(() => {});
});
}
}, { once: true });
function detenerSonido() {
sonidoAlerta.pause();
sonidoAlerta.currentTime = 0;
}
// =========================================================
// LOG
// =========================================================
let logEntradas = [];
function timestamp() {
const now = new Date();
const fecha = now.toLocaleDateString('es-VE', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = now.toLocaleTimeString('es-VE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
return `${fecha} ${hora}`;
}
function registrarLog(tipo, nodo, datos) {
logEntradas.unshift({ tipo, nodo, datos, ts: timestamp() });
if (logEntradas.length > 2000) logEntradas.splice(2000);
renderizarLog();
}
function exportarLog() {
const lineas = [`OLT: ${oltActual} - LOG DE EVENTOS`, `Exportado: ${timestamp()}`, '─'.repeat(80)];
logEntradas.forEach(e => {
const b = `[${e.ts}] ${e.nodo}`;
if (e.tipo === 'inicial') lineas.push(`${b} | INICIO | ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total} (${e.datos.zona} | ${e.datos.op})`);
if (e.tipo === 'inicial_alarma') lineas.push(`${b} | INI_ALARM | ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total} (${e.datos.zona} | ${e.datos.op})`);
if (e.tipo === 'nueva_alarma') lineas.push(`${b} | ALARMA | Antes:ON:${e.datos.onPrev||0} OFF:${e.datos.offPrev||0} (${e.datos.pDownPrev||0}%) -> Ahora:ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total}`);
if (e.tipo === 'empeora') lineas.push(`${b} | EMPEORA | Antes:ON:${e.datos.onAntes} OFF:${e.datos.offAntes} (${e.datos.pDownAntes||0}%) -> Ahora:ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total}`);
if (e.tipo === 'mejora') lineas.push(`${b} | MEJORA | Antes:ON:${e.datos.onAntes} OFF:${e.datos.offAntes} (${e.datos.pDownAntes||0}%) -> Ahora:ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total}`);
if (e.tipo === 'recuperado') lineas.push(`${b} | RECUPERA | Antes: ON:${e.datos.onAntes} OFF:${e.datos.offAntes} (${e.datos.pDownAntes}%) -> Ahora: ON:${e.datos.onActual} OFF:${e.datos.offActual} (${e.datos.pDownActual}%) | TOT: ${e.datos.total}`);
});
const blob = new Blob([lineas.join('\n')], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `log_${oltActual}_${new Date().toISOString().slice(0, 10)}.txt`;
a.click();
}
function exportarCSV() {
const ESTATUS = { inicial: 'INICIO', inicial_alarma: 'INICIO_ALARMADO', nueva_alarma: 'ALARMA', empeora: 'EMPEORA', mejora: 'MEJORA', recuperado: 'RECUPERADO' };
const esc = v => `"${String(v).replace(/"/g, '""')}"`;
const enc = ['Fecha', 'Hora', 'OLT', 'Nodo', 'Ubicacion', 'Operadora', 'Estatus', 'ON Ant', 'OFF Ant', '% Ant', 'ON Act', 'OFF Act', '% Act', 'TOT'];
const filas = logEntradas.map(e => {
const [fecha, hora] = e.ts.split(' ');
let oA = '', oF = '', pA = '', nA = '', nF = '', pN = '', total = '';
if (e.tipo === 'inicial' || e.tipo === 'inicial_alarma') {
nA = e.datos.on; nF = e.datos.off; pN = `${e.datos.pDown}%`; total = e.datos.total;
} else if (e.tipo === 'nueva_alarma') {
oA = e.datos.onPrev||0; oF = e.datos.offPrev||0; pA = `${e.datos.pDownPrev||0}%`;
nA = e.datos.on; nF = e.datos.off; pN = `${e.datos.pDown}%`; total = e.datos.total;
} else if (e.tipo === 'empeora' || e.tipo === 'mejora') {
oA = e.datos.onAntes; oF = e.datos.offAntes; pA = `${e.datos.pDownAntes||0}%`;
nA = e.datos.on; nF = e.datos.off; pN = `${e.datos.pDown}%`; total = e.datos.total;
} else if (e.tipo === 'recuperado') {
oA = e.datos.onAntes; oF = e.datos.offAntes; pA = `${e.datos.pDownAntes||0}%`;
nA = e.datos.onActual; nF = e.datos.offActual; pN = `${e.datos.pDownActual||0}%`; total = e.datos.onActual + e.datos.offActual;
}
return [esc(fecha), esc(hora), esc(oltActual), esc(e.nodo), esc(e.datos.zona||''), esc(e.datos.op||''), esc(ESTATUS[e.tipo]||e.tipo), oA, oF, esc(pA), nA, nF, esc(pN), esc(total)].join(',');
});
const blob = new Blob(['\uFEFF' + [enc.join(','), ...filas].join('\n')], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `log_${oltActual}_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
}
function renderizarLog() {
const contenedor = document.getElementById('log-list');
if (!contenedor) return;
if (logEntradas.length === 0) {
contenedor.innerHTML = '
Sin eventos registrados.
';
return;
}
const colores = { inicial: '#555', inicial_alarma: '#ed5565', nueva_alarma: '#ed5565', empeora: '#e67e22', mejora: '#f1c40f', recuperado: '#1ab394' };
const iconos = { inicial: '📋', inicial_alarma: '🔴', nueva_alarma: '🔴', empeora: '📉', mejora: '📈', recuperado: '✅' };
const labels = { inicial: 'INICIO', inicial_alarma: 'INICIO (ALARMADO)', nueva_alarma: 'ALARMA', empeora: 'EMPEORA', mejora: 'MEJORA', recuperado: 'RECUPERADO' };
const entradasFiltradas = logBusqueda
? logEntradas.filter(e => e.nodo.toLowerCase().includes(logBusqueda) || (e.datos.zona || '').toLowerCase().includes(logBusqueda))
: logEntradas;
if (!entradasFiltradas.length) {
contenedor.innerHTML = `Sin resultados para "${logBusqueda}"
`;
return;
}
contenedor.innerHTML = entradasFiltradas.map(e => {
let detalle = '';
if (e.tipo === 'inicial' || e.tipo === 'inicial_alarma') {
detalle = `ON: ${e.datos.on} | OFF: ${e.datos.off} (${e.datos.pDown}%) | TOT: ${e.datos.total}`;
} else if (e.tipo === 'nueva_alarma') {
detalle = `Antes: ON:${e.datos.onPrev||0} OFF:${e.datos.offPrev||0} (${e.datos.pDownPrev||0}%) → Ahora: ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%)`;
} else if (e.tipo === 'empeora' || e.tipo === 'mejora') {
detalle = `Antes: ON:${e.datos.onAntes} OFF:${e.datos.offAntes} (${e.datos.pDownAntes||0}%) → Ahora: ON:${e.datos.on} OFF:${e.datos.off} (${e.datos.pDown}%)`;
} else if (e.tipo === 'recuperado') {
detalle = `Antes: ON:${e.datos.onAntes} OFF:${e.datos.offAntes} (${e.datos.pDownAntes}%) → Ahora: ON:${e.datos.onActual} OFF:${e.datos.offActual} (${e.datos.pDownActual}%)`;
}
const zona = e.datos.zona || '';
const op = e.datos.op || '';
return `
${iconos[e.tipo]} ${labels[e.tipo]}
${e.ts}
${e.nodo}
${zona || op ? `
📍${zona} | 🏢${op}
` : ''}
${detalle}
`;
}).join('');
}
// =========================================================
// FAVICON
// =========================================================
let faviconEl = document.querySelector("link[rel~='icon']");
if (!faviconEl) {
faviconEl = document.createElement('link');
faviconEl.rel = 'icon';
document.head.appendChild(faviconEl);
}
const faviconCanvas = document.createElement('canvas');
faviconCanvas.width = 32;
faviconCanvas.height = 32;
const faviconCtx = faviconCanvas.getContext('2d');
function actualizarPestana(oltName, totalCriticos, hayNuevos) {
if (ultimoEstadoPestana.criticos === totalCriticos && ultimoEstadoPestana.hayNuevos === hayNuevos) return;
ultimoEstadoPestana.criticos = totalCriticos;
ultimoEstadoPestana.hayNuevos = hayNuevos;
document.title = totalCriticos > 0 ? `${hayNuevos ? '🆕' : ''} ${oltName}` : `✅ ${oltName}`;
const color = totalCriticos > 0 ? (hayNuevos ? '#e74c3c' : '#a93226') : '#1ab394';
faviconCtx.clearRect(0, 0, 32, 32);
faviconCtx.beginPath();
faviconCtx.arc(16, 16, 15, 0, 2 * Math.PI);
faviconCtx.fillStyle = color;
faviconCtx.fill();
faviconCtx.fillStyle = '#ffffff';
faviconCtx.textAlign = 'center';
faviconCtx.textBaseline = 'middle';
if (totalCriticos > 0) {
document.title = `(${totalCriticos}) ${oltName}`;
} else {
faviconCtx.font = 'bold 20px sans-serif';
faviconCtx.fillText('✓', 16, 17);
}
faviconEl.href = faviconCanvas.toDataURL('image/png');
}
// =========================================================
// CSS
// =========================================================
const style = document.createElement('style');
style.innerHTML = `
@keyframes pulseACS {
0%, 100% { box-shadow: inset 0 0 0px #fff; background-color: #a93226; }
50% { box-shadow: inset 0 0 20px #fff; background-color: #ed5565; border:1px solid #fff; }
}
@keyframes pulsePanel {
0%, 100% { background-color: rgba(237,85,101,0.1); border-left:5px solid #ed5565; }
50% { background-color: rgba(237,85,101,0.6); border-left:5px solid #fff; }
}
.celda-acs-blink { animation: pulseACS 0.8s infinite !important; }
.tarjeta-panel-blink { animation: pulsePanel 1s infinite ease-in-out !important; }
.badge-nuevo {
background:#fff; color:#ed5565; font-size:10px; font-weight:900;
padding:2px 6px; border-radius:4px; margin-left:8px; box-shadow:0 0 8px #fff;
}
.header-blink { animation:pulsePanel 0.4s infinite alternate !important; box-shadow:0 0 15px #ed5565 !important; }
.ctrl {
background:#222; color:#ed5565; border:1px solid #555;
border-radius:3px; padding:3px 5px; font-size:11px;
font-weight:bold; cursor:pointer; outline:none; box-sizing:border-box;
}
.ctrl:hover, .ctrl:focus { border-color:#ed5565; }
input[type=number]::-webkit-inner-spin-button { opacity:1; }
.tab-btn { flex:1; padding:4px 0; font-size:10px; font-weight:bold; border:none; border-radius:3px; cursor:pointer; }
.tab-active { background:#ed5565; color:white; }
.tab-inactive { background:#333; color:#aaa; }
.mute-permanente { background:#7d3c98 !important; border-color:#9b59b6 !important; color:white !important; }
.mute-temporal { background:#784212 !important; border-color:#e67e22 !important; color:white !important; }
.mute-off { background:#333 !important; border-color:#555 !important; color:#ccc !important; }
#mute-status {
width:100%; box-sizing:border-box; padding:2px 4px; white-space:normal;
word-wrap:break-word; line-height:1.3; font-size:10px; font-weight:bold; text-align:center;
}
.mute-status-text { background:transparent !important; color:#aaa; border:none; }
#alert-content { max-height:0; overflow:hidden; transition:max-height 0.25s ease; }
#alert-content.panel-abierto { max-height:85dvh; overflow-y:auto; }
#olt-alert-panel { transition:width 0.25s ease, padding 0.25s ease, font-size 0.25s ease; }
#olt-alert-panel.modo-flotante {
left:50% !important; bottom:auto !important; top:50% !important;
transform:translate(-50%, -50%); border-radius:8px !important;
border:1px solid #555 !important; resize:both; overflow:auto; min-width:320px; min-height:200px;
}
#olt-alert-panel.modo-flotante #panel-drag-handle { cursor:pointer; }
#btn-flotante { font-size:13px; cursor:pointer; opacity:0.6; user-select:none; }
#btn-flotante:hover { opacity:1; }
.zoom-buttons { display:flex; gap:4px; margin-top:5px; flex-wrap:wrap; }
.zoom-btn {
background:#333; color:#aaa; border:1px solid #555; border-radius:3px;
padding:3px 6px; font-size:10px; font-weight:bold; cursor:pointer;
font-family:'Google Sans'; flex:1; text-align:center; transition:background 0.2s;
}
.zoom-btn:hover { background:#444; }
.zoom-btn.active { background:#f1c40f; color:#111; border-color:#e67e22; }
#olt-alert-panel:not(.modo-flotante) .zoom-buttons { display:none; }
@media (max-width: 768px) { #btn-flotante { display:none; } }
#umbral-valor::-webkit-inner-spin-button,
#umbral-valor::-webkit-outer-spin-button { display: none; }
`;
(document.head || document.documentElement).appendChild(style);
// =========================================================
// ZOOM
// =========================================================
function aplicarZoom() {
const panel = document.getElementById('olt-alert-panel');
if (!panel) return;
panel.style.zoom = panel.classList.contains('modo-flotante') ? escala : '';
}
function resaltarBotonZoom() {
document.querySelectorAll('.zoom-btn').forEach(btn => {
parseFloat(btn.dataset.scale) === escala ? btn.classList.add('active') : btn.classList.remove('active');
});
}
// =========================================================
// PANEL
// =========================================================
function crearPanel() {
if (document.getElementById('olt-alert-panel')) return;
const panel = document.createElement('div');
panel.id = 'olt-alert-panel';
panel.innerHTML = `
${!dbCargada ? '⚠️ Fallo DB_NODOS
' : ''}
TIPO DE ALARMA:
OPERADORA:
`;
Object.assign(panel.style, {
position: 'fixed', bottom: '20px', left: '0px', width: '120px',
backgroundColor: 'rgba(5,5,5,0.98)', color: 'white', padding: '12px',
borderRadius: '0 8px 8px 0', boxShadow: '5px 0 20px rgba(0,0,0,1)',
zIndex: '10000', border: '1px solid #444', borderLeft: 'none'
});
document.body.appendChild(panel);
// --- Zoom ---
panel.querySelectorAll('.zoom-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const nuevaEscala = parseFloat(btn.dataset.scale);
if (isNaN(nuevaEscala)) return;
escala = nuevaEscala;
localStorage.setItem('oltPanelEscala', escala);
resaltarBotonZoom();
aplicarZoom();
});
});
resaltarBotonZoom();
// --- Máquina de estados del panel ---
function aplicarEstado(estado) {
const content = document.getElementById('alert-content');
const btnToggle = document.getElementById('toggle-btn');
const btnFlotante = document.getElementById('btn-flotante');
if (estado === 'flotante') {
panel.classList.add('modo-flotante');
panel.style.width = '1040px'; panel.style.padding = '18px'; panel.style.fontSize = '15px'; panel.style.zoom = escala;
content.classList.add('panel-abierto'); content.style.maxHeight = 'none';
btnToggle.style.display = 'none';
btnFlotante.style.opacity = '1'; btnFlotante.title = 'Salir de modo flotante';
} else {
panel.classList.remove('modo-flotante');
panel.style.width = estado === 'abierto' ? '360px' : '120px';
panel.style.padding = '12px'; panel.style.fontSize = ''; panel.style.zoom = '';
estado === 'abierto' ? content.classList.add('panel-abierto') : content.classList.remove('panel-abierto');
content.style.maxHeight = '';
btnToggle.style.display = 'inline'; btnToggle.innerText = estado === 'abierto' ? '−' : '+';
btnFlotante.style.opacity = '0.6'; btnFlotante.title = 'Modo flotante';
}
}
aplicarEstado('minimizado');
// --- Controles ---
document.getElementById('umbral-valor').value = umbralValor;
document.getElementById('umbral-tipo').value = umbralTipo;
const actualizarConfiguracion = () => {
umbralValor = parseFloat(document.getElementById('umbral-valor').value) || 0;
umbralTipo = document.getElementById('umbral-tipo').value;
localStorage.setItem('oltUmbralValor', umbralValor);
localStorage.setItem('oltUmbralTipo', umbralTipo);
modoCargaInicial = true;
registroNodos.clear();
memoriaRedGlobal.clear();
logEntradas = [];
document.getElementById('log-list').innerHTML = '';
actualizarPestana(oltActual, 0, false);
};
document.getElementById('umbral-valor').addEventListener('change', actualizarConfiguracion);
document.getElementById('umbral-tipo').addEventListener('change', actualizarConfiguracion);
const selectAlarma = document.getElementById('selector-alarma');
selectAlarma.value = alarmaGuardada in AUDIOS ? alarmaGuardada : 'dosimeter';
selectAlarma.addEventListener('change', function() {
detenerSonido();
sonidoAlerta = AUDIOS[this.value] || AUDIOS.alarm;
localStorage.setItem('oltAlarmaSeleccionada', this.value);
if ([...registroNodos.values()].some(d => !d.reconocido) && !silenciado && !muteGlobal) sonidoAlerta.play().catch(() => {});
});
document.getElementById('btn-test-play').onclick = () => {
detenerSonido();
enPrueba = true;
sonidoAlerta.play().catch(() => {
const statusEl = document.getElementById('mute-status');
if (statusEl) {
const orig = statusEl.innerHTML;
statusEl.style.display = 'block'; statusEl.style.color = '#ed5565';
statusEl.innerHTML = '❌ Error al reproducir audio';
setTimeout(() => {
if (statusEl.innerHTML === '❌ Error al reproducir audio') {
statusEl.style.display = 'none'; statusEl.innerHTML = orig;
}
}, 3000);
}
});
document.getElementById('btn-test-play').textContent = '🔊';
};
document.getElementById('btn-test-stop').onclick = () => {
enPrueba = false;
detenerSonido();
document.getElementById('btn-test-play').textContent = '▶';
};
document.getElementById('filtro-op').addEventListener('change', function() {
filtroOp = this.value; localStorage.setItem('oltFiltroOp', filtroOp);
});
document.getElementById('filtro-op').value = filtroOp;
document.getElementById('btn-marcar-todos').onclick = () => {
for (let data of registroNodos.values()) data.reconocido = true;
enPrueba = false; detenerSonido();
procesarNodos();
};
// --- Silenciador ---
let clicksRapidos = 0;
let timerClickReset = null;
let timerMuteTemporal = null;
let muteExpiraEn = null;
function aplicarMuteEstado() {
const statusEl = document.getElementById('mute-status');
const btnSil = document.getElementById('btn-silenciar');
if (!statusEl || !btnSil) return;
btnSil.classList.remove('mute-permanente', 'mute-temporal', 'mute-off');
statusEl.classList.remove('mute-permanente', 'mute-temporal');
statusEl.classList.add('mute-status-text');
if (muteGlobal) {
btnSil.classList.add('mute-permanente');
statusEl.style.display = 'block'; statusEl.textContent = '🔒 Silencio permanente activo';
} else if (silenciado && muteExpiraEn) {
btnSil.classList.add('mute-temporal');
statusEl.style.display = 'block';
statusEl.textContent = `⏱ Silenciado ${Math.ceil((muteExpiraEn - Date.now()) / 60000)}min restantes`;
} else {
btnSil.classList.add('mute-off');
statusEl.style.display = 'none'; statusEl.textContent = '';
}
}
setInterval(aplicarMuteEstado, 30000);
document.getElementById('btn-silenciar').addEventListener('click', () => {
clicksRapidos++;
clearTimeout(timerClickReset);
timerClickReset = setTimeout(() => { clicksRapidos = 0; }, 10000);
if (clicksRapidos >= 10) {
clicksRapidos = 0;
clearTimeout(timerMuteTemporal);
muteExpiraEn = null;
muteGlobal = !muteGlobal;
silenciado = muteGlobal;
localStorage.setItem('oltMuteGlobal', muteGlobal);
enPrueba = false;
detenerSonido();
aplicarMuteEstado();
return;
}
if (muteGlobal) {
muteGlobal = false; silenciado = false; muteExpiraEn = null;
localStorage.setItem('oltMuteGlobal', 'false'); aplicarMuteEstado(); return;
}
if (silenciado && muteExpiraEn) {
clearTimeout(timerMuteTemporal); timerMuteTemporal = null;
silenciado = false; muteExpiraEn = null; aplicarMuteEstado(); return;
}
const DURACION_MS = 60 * 60 * 1000;
muteExpiraEn = Date.now() + DURACION_MS; silenciado = true;
enPrueba = false; detenerSonido();
clearTimeout(timerMuteTemporal);
timerMuteTemporal = setTimeout(() => { silenciado = false; muteExpiraEn = null; aplicarMuteEstado(); }, DURACION_MS);
aplicarMuteEstado();
});
if (muteGlobal) aplicarMuteEstado();
document.getElementById('btn-exportar-log').onclick = exportarLog;
document.getElementById('btn-exportar-csv').onclick = exportarCSV;
// --- Tabs ---
document.getElementById('tab-alarmas').onclick = () => {
document.getElementById('vista-alarmas').style.display = 'block';
document.getElementById('vista-log').style.display = 'none';
document.getElementById('tab-alarmas').className = 'tab-btn tab-active';
document.getElementById('tab-log').className = 'tab-btn tab-inactive';
};
document.getElementById('tab-log').onclick = () => {
document.getElementById('vista-alarmas').style.display = 'none';
document.getElementById('vista-log').style.display = 'block';
document.getElementById('tab-alarmas').className = 'tab-btn tab-inactive';
document.getElementById('tab-log').className = 'tab-btn tab-active';
renderizarLog();
};
document.getElementById('log-busqueda').addEventListener('input', function() {
logBusqueda = this.value.trim().toLowerCase(); renderizarLog();
});
document.getElementById('panel-header').onclick = function(e) {
if (e.target.id === 'btn-flotante') return;
if (panelState === 'minimizado') {
aplicarEstado(modoPreferido);
panelState = modoPreferido;
} else {
aplicarEstado('minimizado');
panelState = 'minimizado';
}
};
document.getElementById('btn-flotante').addEventListener('click', (e) => {
e.stopPropagation();
if (window.innerWidth <= 768) return;
modoPreferido = panelState === 'flotante' ? 'abierto' : 'flotante';
localStorage.setItem('oltModoPreferido', modoPreferido);
aplicarEstado(modoPreferido);
panelState = modoPreferido;
});
}
// =========================================================
// LOOP PRINCIPAL
// =========================================================
function procesarNodos() {
if (!window.location.href.includes('/monitoring/olt/')) return;
crearPanel();
const oltName = document.querySelector('.olt-monitoring-details-olt-title')?.innerText.trim() || "";
if (oltName && oltName !== oltActual) {
oltActual = oltName; modoCargaInicial = true;
logEntradas = []; registroNodos.clear(); memoriaRedGlobal.clear();
}
const filas = document.querySelectorAll('tr');
let tablaCargada = false;
for (let fila of filas) {
const celdas = fila.querySelectorAll('td');
if (celdas.length >= 2) {
const slotStr = celdas[0].querySelector('strong')?.innerText.trim();
if (slotStr && /^\d+$/.test(slotStr)) { tablaCargada = true; break; }
}
}
if (!tablaCargada) {
const listContainer = document.getElementById('alert-list');
const msgCarga = '⏳ Esperando datos...
';
if (listContainer && ultimoHTMLRenderizado !== msgCarga) {
listContainer.innerHTML = msgCarga;
ultimoHTMLRenderizado = msgCarga;
document.title = `⏳ Cargando ${oltActual}...`;
}
return;
}
const criticosActuales = [];
let hayNovedadesParaAlarma = false;
filas.forEach(fila => {
const celdas = fila.querySelectorAll('td');
if (celdas.length < 2) return;
const slotStrong = celdas[0].querySelector('strong');
if (!slotStrong) return;
const slotStr = slotStrong.innerText.trim().padStart(2, '0');
if (!/^\d+$/.test(slotStr)) return;
for (let i = 1; i < celdas.length; i++) {
const celdaPort = celdas[i];
const tituloPuerto = celdaPort.querySelector('.table-cell-head strong')?.innerText.trim() || '';
const matchPuerto = tituloPuerto.match(/^Port\s+(\d+)$/i);
if (!matchPuerto) continue;
const portStr = matchPuerto[1].padStart(2, '0');
const on = parseInt(celdaPort.querySelector('.label-green')?.innerText) || 0;
const off = parseInt(celdaPort.querySelector('.label-danger')?.innerText) || 0;
const total = on + off;
if (total === 0) continue;
const pDown = ((off / total) * 100).toFixed(1);
const pUp = ((on / total) * 100).toFixed(1);
const idNodo = `${oltName}${slotStr}${portStr}A`;
const superaUmbral = umbralTipo === 'porcentaje' ? parseFloat(pDown) >= umbralValor : off >= umbralValor;
const info = DB_NODOS[idNodo] || { op: "---", zona: "S/I" };
const datosNodo = { total, on, off, pDown, pUp, zona: info.zona, op: info.op };
const memPrevia = memoriaRedGlobal.get(idNodo);
const comoInicial = modoCargaInicial || !memPrevia;
if (!comoInicial && memPrevia && (memPrevia.off !== off || memPrevia.on !== on)) {
resultadosRCA.delete(idNodo);
}
const etiquetas = celdaPort.querySelectorAll('.gpon-util .label');
let labelPrincipal = null;
if (etiquetas.length > 0) {
labelPrincipal = etiquetas[0];
labelPrincipal.innerHTML = `${pDown}% DN
${pUp}% UP
`;
for (let j = 1; j < etiquetas.length; j++) {
if (!etiquetas[j].classList.contains('label-success')) etiquetas[j].style.display = 'none';
}
}
if (superaUmbral) {
const rcaState = resultadosRCA.get(idNodo);
if (!rcaState || (rcaState.status === 'error' && (Date.now() - rcaState.ts) > 30000)) {
const matchOlt = window.location.pathname.match(/\/olt\/(\d+)/);
if (matchOlt) {
// Ejecutar RCA en background sin bloquear iteración
ejecutarRCA(idNodo, matchOlt[1], parseInt(slotStr, 10), parseInt(portStr, 10));
}
}
const RCA_VALIDAS = ['LOS', 'LOSi', 'LOFi', 'link-down'];
const rcaPendiente = !rcaState || rcaState.status === 'loading';
const rcaValido = rcaState && rcaState.status === 'done' && RCA_VALIDAS.includes(rcaState.dominante);
const rcaIgnorado = rcaState && rcaState.status === 'done' && !rcaValido;
if (!registroNodos.has(idNodo)) {
const offPrev = memPrevia ? memPrevia.off : 0;
const onPrev = memPrevia ? memPrevia.on : total;
const pDownPrev = memPrevia ? memPrevia.pDown : '0';
registroNodos.set(idNodo, {
origen: comoInicial ? 'inicial' : 'nuevo',
reconocido: comoInicial || rcaIgnorado, // Ignorados no titilan
notificado: false, // ¿Ya se alertó al log y con sonido?
timestamp: Date.now(),
offAnterior: off,
onAnterior: on,
pDownAnterior: pDown,
totalAnterior: total
});
if (comoInicial) {
registrarLog('inicial', idNodo, datosNodo);
registroNodos.get(idNodo).notificado = true;
}
} else {
const data = registroNodos.get(idNodo);
if (off > data.offAnterior) {
registrarLog('empeora', idNodo, { ...datosNodo, offAntes: data.offAnterior, onAntes: data.onAnterior, pDownAntes: data.pDownAnterior });
data.offAnterior = off; data.onAnterior = on; data.pDownAnterior = pDown;
} else if (off < data.offAnterior) {
registrarLog('mejora', idNodo, { ...datosNodo, offAntes: data.offAnterior, onAntes: data.onAnterior, pDownAntes: data.pDownAnterior });
data.offAnterior = off; data.onAnterior = on; data.pDownAnterior = pDown;
}
}
const data = registroNodos.get(idNodo);
// Disparar alarma solo cuando el RCA confirma que es válido (y no ha sido notificado)
if (rcaValido && !data.notificado && !comoInicial && data.origen === 'nuevo') {
data.notificado = true;
hayNovedadesParaAlarma = true;
const prev = memoriaRedGlobal.get(idNodo) || {};
registrarLog('nueva_alarma', idNodo, { ...datosNodo, offPrev: prev.off, onPrev: prev.on, pDownPrev: prev.pDown });
}
if (rcaIgnorado) {
data.reconocido = true; // Silenciar visualmente
}
const esNuevoParaPanel = (data.origen === 'nuevo' && !data.reconocido && rcaValido);
if (labelPrincipal) {
if (rcaIgnorado) {
labelPrincipal.className = "label";
labelPrincipal.style.cssText = `display:inline-block!important;width:68px!important;background-color:#7f8c8d!important;color:white!important;border-radius:4px;text-align:center;border:1px solid rgba(255,255,255,0.05);`;
} else if (rcaPendiente) {
labelPrincipal.className = "label";
labelPrincipal.style.cssText = `display:inline-block!important;width:68px!important;background-color:#e67e22!important;color:white!important;border-radius:4px;text-align:center;border:1px solid rgba(255,255,255,0.05);`;
} else {
labelPrincipal.className = esNuevoParaPanel ? "label celda-acs-blink" : "label";
labelPrincipal.style.cssText = esNuevoParaPanel
? `display:inline-block!important;width:68px!important;color:white!important;border-radius:4px;text-align:center;`
: `display:inline-block!important;width:68px!important;background-color:#a93226!important;color:white!important;border-radius:4px;text-align:center;border:1px solid rgba(255,255,255,0.05);`;
}
}
const borderColor = rcaIgnorado ? '#7f8c8d' : (rcaPendiente ? '#e67e22' : '#a93226');
criticosActuales.push({ id: idNodo, down: pDown, off, total, ...info, esNuevoParaPanel, borderColor });
} else {
resultadosRCA.delete(idNodo);
if (registroNodos.has(idNodo)) {
const data = registroNodos.get(idNodo);
if (!modoCargaInicial) {
registrarLog('recuperado', idNodo, {
offAntes: data.offAnterior, onAntes: data.onAnterior, pDownAntes: data.pDownAnterior,
offActual: off, onActual: on, pDownActual: pDown,
total, zona: info.zona, op: info.op
});
}
registroNodos.delete(idNodo);
}
if (labelPrincipal) {
labelPrincipal.className = "label";
labelPrincipal.style.cssText = `display:inline-block!important;width:68px!important;background-color:#1ab394!important;color:white!important;border-radius:4px;text-align:center;`;
}
}
memoriaRedGlobal.set(idNodo, { off, on, total, pDown });
}
});
if (hayNovedadesParaAlarma && !silenciado && !muteGlobal) sonidoAlerta.play().catch(() => {});
if (!enPrueba && criticosActuales.length === 0) detenerSonido();
modoCargaInicial = false;
const selectOp = document.getElementById('filtro-op');
if (selectOp) {
const opsEnOlt = [...new Set(criticosActuales.map(c => c.op).filter(Boolean))].sort();
if (filtroOp !== 'TODOS' && !opsEnOlt.includes(filtroOp)) opsEnOlt.push(filtroOp);
opsEnOlt.sort();
const opcionesActuales = [...selectOp.options].slice(1).map(o => o.value);
if (JSON.stringify(opsEnOlt) !== JSON.stringify(opcionesActuales)) {
const selAnterior = selectOp.value;
while (selectOp.options.length > 1) selectOp.remove(1);
opsEnOlt.forEach(op => {
const opt = document.createElement('option');
opt.value = op; opt.textContent = op; selectOp.appendChild(opt);
});
selectOp.value = opsEnOlt.includes(selAnterior) ? selAnterior : 'TODOS';
filtroOp = selectOp.value; localStorage.setItem('oltFiltroOp', filtroOp);
}
}
const criticosFiltrados = filtroOp === 'TODOS' ? criticosActuales : criticosActuales.filter(c => c.op === filtroOp);
const badgeContador = document.getElementById('alert-count');
const btnMarcar = document.getElementById('btn-marcar-todos');
const hayAlgoSinLeer = criticosActuales.some(c => c.esNuevoParaPanel);
badgeContador.innerText = criticosActuales.length;
hayAlgoSinLeer ? badgeContador.classList.add('header-blink') : badgeContador.classList.remove('header-blink');
btnMarcar.style.display = hayAlgoSinLeer ? 'inline-block' : 'none';
const listContainer = document.getElementById('alert-list');
if (listContainer) {
const nuevoHTML = criticosFiltrados.length > 0
? criticosFiltrados
.sort((a, b) => {
if (a.esNuevoParaPanel !== b.esNuevoParaPanel) return a.esNuevoParaPanel ? -1 : 1;
function getPrio(id) {
const dom = resultadosRCA.get(id)?.dominante;
if (!dom) return 2; // Pendiente/Inconcluso/OTHER
if (['LOS', 'LOSi', 'LOFi', 'link-down'].includes(dom)) return 1;
if (['dying-gasp', 'power-off'].includes(dom)) return 3;
if (['deactive', 'SUSPENDED'].includes(dom)) return 4;
return 2;
}
const prioA = getPrio(a.id);
const prioB = getPrio(b.id);
if (prioA !== prioB) return prioA - prioB;
return b.down - a.down;
})
.map(c => `
${c.id}
${c.esNuevoParaPanel ? 'NUEVO' : ''}
📍 ${c.zona} | 🏢 ${c.op}
⚠️ ${c.down}% caída | 🔴 OFF:${c.off} | 👥 ${c.total}
${resultadosRCA.has(c.id) ? `
${resultadosRCA.get(c.id).text}
` : ''}
`).join('')
: criticosActuales.length > 0
? `Sin alarmas para ${filtroOp}
`
: 'SISTEMA OK ✅
';
if (ultimoHTMLRenderizado !== nuevoHTML) {
listContainer.innerHTML = nuevoHTML;
ultimoHTMLRenderizado = nuevoHTML;
}
}
actualizarPestana(oltActual, criticosActuales.length, hayAlgoSinLeer);
}
// =========================================================
// WORKER
// =========================================================
const workerBlobUrl = URL.createObjectURL(
new Blob([`setInterval(()=>postMessage('tick'),2500)`], { type: 'application/javascript' })
);
const worker = new Worker(workerBlobUrl);
URL.revokeObjectURL(workerBlobUrl);
worker.onmessage = procesarNodos;
})();