// ==UserScript== // @name InCa ACS - NOC Slot Outage Monitor // @namespace Violentmonkey Scripts // @version 4.3 // @description Monitoreo de caídas a cero (online=0) bajo demanda con reintento automático, progreso y copia al portapapeles. // @author Ing. Adrian Leon // @updateURL https://raw.githubusercontent.com/TakRiuto/ACSScripts/release/BoardDetector.user.js // @downloadURL https://raw.githubusercontent.com/TakRiuto/ACSScripts/release/BoardDetector.user.js // @icon https://avatars.githubusercontent.com/u/20828447?v=4 // @match https://190.153.58.82/monitoring/olt/* // @grant none // ==/UserScript== (function () { 'use strict'; let currentOltId = null; let lastTelemetryTimestamp = null; const slotCache = new Map(); let isFetching = false; let currentResults = []; // Estilos UI (Gris Grafito + Google Sans) const styles = ` @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap'); :root { --noc-font: 'Google Sans', 'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif; --noc-bg-canvas: #161a1f; --noc-bg-panel: #1f242c; --noc-bg-card: #272d36; --noc-bg-card-hover: #2e3540; --noc-border-subtle: #333b47; --noc-border-strong: #444e5e; --noc-text-main: #e6ebf2; --noc-text-muted: #9aa5b5; --noc-text-dim: #6c7684; --noc-red-bg: #3a1d22; --noc-red-border: #732a33; --noc-red-text: #ff6b7a; --noc-green-bg: #193324; --noc-green-border: #255237; --noc-green-text: #4cd984; --noc-blue-accent: #3d8bfd; --noc-blue-hover: #2b77e5; } /* Botón de la barra de la OLT */ .btn-audit-trigger { background-color: var(--noc-bg-card); color: var(--noc-text-main); border: 1px solid var(--noc-border-subtle); font-family: var(--noc-font); font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; margin-left: 12px; vertical-align: middle; transition: all 0.15s ease; } .btn-audit-trigger:hover { background-color: var(--noc-bg-card-hover); border-color: var(--noc-blue-accent); color: #fff; } /* Badges de la tabla */ .noc-slot-badge { font-family: var(--noc-font); font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 3px; display: inline-block; margin-left: 6px; vertical-align: middle; user-select: none; cursor: pointer; border: 1px solid transparent; } .noc-badge-ok { background: var(--noc-green-bg); border-color: var(--noc-green-border); color: var(--noc-green-text); } .noc-badge-alert { background: var(--noc-red-bg); border-color: var(--noc-red-border); color: var(--noc-red-text); } /* Menú lateral flotante (Drawer) */ .noc-drawer-overlay { position: fixed; inset: 0; background: rgba(10, 13, 17, 0.65); backdrop-filter: blur(2px); z-index: 999999; display: flex; justify-content: flex-end; animation: fadeIn 0.15s ease-out; } .noc-drawer { width: 530px; max-width: 92vw; height: 100vh; background: var(--noc-bg-panel); border-left: 1px solid var(--noc-border-subtle); box-shadow: -8px 0 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; font-family: var(--noc-font); color: var(--noc-text-main); animation: slideIn 0.2s ease-out; } @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .noc-drawer-header { padding: 16px 20px; background: var(--noc-bg-canvas); border-bottom: 1px solid var(--noc-border-subtle); display: flex; justify-content: space-between; align-items: center; } .noc-drawer-title { font-size: 14px; font-weight: 700; color: var(--noc-text-main); display: flex; align-items: center; gap: 8px; } .noc-drawer-close { background: none; border: none; color: var(--noc-text-muted); font-size: 20px; cursor: pointer; padding: 0 4px; line-height: 1; } .noc-drawer-close:hover { color: #fff; } /* Toolbar del Drawer */ .noc-drawer-toolbar { padding: 12px 20px; background: var(--noc-bg-canvas); border-bottom: 1px solid var(--noc-border-subtle); display: flex; gap: 10px; align-items: center; } .btn-modal-action { background-color: var(--noc-blue-accent); color: #fff; border: none; padding: 8px 14px; font-family: var(--noc-font); font-size: 12px; font-weight: 600; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: background-color 0.15s ease; } .btn-modal-action:hover { background-color: var(--noc-blue-hover); } .btn-modal-action:disabled { opacity: 0.5; cursor: not-allowed; } .btn-modal-copy { background-color: var(--noc-bg-card); color: var(--noc-text-main); border: 1px solid var(--noc-border-subtle); padding: 8px 14px; font-family: var(--noc-font); font-size: 12px; font-weight: 600; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; } .btn-modal-copy:hover:not(:disabled) { background-color: var(--noc-bg-card-hover); border-color: var(--noc-green-text); color: var(--noc-green-text); } .btn-modal-copy:disabled { opacity: 0.4; cursor: not-allowed; } .noc-drawer-body { flex: 1; padding: 16px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; } /* Barra de Progreso */ .noc-progress-container { width: 100%; display: flex; flex-direction: column; gap: 6px; padding: 10px 14px; background: var(--noc-bg-card); border: 1px solid var(--noc-border-subtle); border-radius: 6px; } .noc-progress-header { display: flex; justify-content: space-between; font-size: 11px; color: var(--noc-text-muted); } .noc-progress-track { width: 100%; height: 6px; background: var(--noc-bg-canvas); border-radius: 3px; overflow: hidden; } .noc-progress-fill { height: 100%; background: var(--noc-blue-accent); width: 0%; transition: width 0.2s ease; } /* Tarjeta de Slot */ .noc-slot-card { background: var(--noc-bg-card); border: 1px solid var(--noc-border-subtle); border-radius: 6px; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; } .noc-card-header { display: flex; justify-content: space-between; align-items: center; } .noc-card-title { font-size: 13px; font-weight: 700; color: var(--noc-text-main); } .noc-events-list { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; } .noc-event-row { background: var(--noc-bg-panel); border: 1px solid var(--noc-border-subtle); border-radius: 4px; padding: 7px 10px; display: flex; justify-content: space-between; align-items: center; font-size: 11px; } .noc-event-dates { display: flex; flex-direction: column; gap: 2px; } .noc-event-badge-dur { background: var(--noc-bg-canvas); color: var(--noc-text-muted); padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; border: 1px solid var(--noc-border-subtle); } .noc-summary-bar { padding: 10px 14px; background: var(--noc-bg-canvas); border: 1px solid var(--noc-border-subtle); border-radius: 6px; font-size: 12px; display: flex; justify-content: space-around; color: var(--noc-text-muted); } .noc-summary-bar strong { color: var(--noc-text-main); } .noc-empty-placeholder { text-align: center; color: var(--noc-text-muted); padding: 50px 20px; font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 12px; } .noc-empty-placeholder i { font-size: 28px; color: var(--noc-text-dim); } `; const styleEl = document.createElement('style'); styleEl.innerHTML = styles; document.head.appendChild(styleEl); // Helpers function getToken() { try { const rawUser = localStorage.getItem('currentUser'); if (!rawUser) return null; return JSON.parse(rawUser)?.token?.id || null; } catch { return null; } } function getOltIdFromUrl() { const match = window.location.pathname.match(/\/monitoring\/olt\/(\d+)/); return match ? match[1] : null; } function getOltName() { const titleEl = document.querySelector('.olt-monitoring-details-olt-title'); return titleEl?.textContent?.trim() || `OLT-${getOltIdFromUrl() || 'Unknown'}`; } function isNaValue(val) { if (val === null || val === undefined) return true; if (typeof val === 'string') { const clean = val.trim().toUpperCase(); if (clean === 'N/A' || clean === 'NA' || clean === '') return true; } return false; } // Regla de análisis de caídas (N/A no es 0) function parseOutageEvents(data) { data.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)); const events = []; let inOutage = false; let outageStart = null; for (let i = 0; i < data.length; i++) { const item = data[i]; if (isNaValue(item.online)) continue; const online = Number(item.online); const offline = Number(item.offline) || 0; const suspended = Number(item.suspended) || 0; const totalOnus = online + offline + suspended; // Descartar si el slot está desocupado o es inválido if (isNaN(online) || totalOnus === 0) continue; if (online === 0) { if (!inOutage) { inOutage = true; outageStart = item.createdAt; } } else { if (inOutage) { inOutage = false; const diffMin = Math.round((new Date(item.createdAt) - new Date(outageStart)) / 60000); events.push({ start: outageStart, end: item.createdAt, duration: `${diffMin} min` }); outageStart = null; } } } if (inOutage && outageStart) { events.push({ start: outageStart, end: null, duration: 'En curso' }); } return events; } // Consulta de datos de un Slot con REINTENTO AUTOMÁTICO (hasta 3 intentos) async function fetchSlotWithRetry(oltId, slotId, token, maxRetries = 3) { if (slotCache.has(slotId)) return slotCache.get(slotId); const url = `https://190.153.58.82/api/fttx/monitoring/olt-usage/${oltId}/statuses/slots/${slotId}?filter=%7B%22order%22:%22createdAt%20ASC%22,%22include%22:%7B%7D%7D`; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const resp = await fetch(url, { headers: { 'accept': 'application/json, text/plain, */*', 'x-access-token': token } }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); if (Array.isArray(data)) { const events = parseOutageEvents(data); slotCache.set(slotId, events); return events; } throw new Error('Respuesta no válida'); } catch (e) { console.warn(`[NOC Monitor] Slot ${slotId} intento ${attempt}/${maxRetries} falló:`, e.message); if (attempt === maxRetries) { console.error(`[NOC Monitor] Slot ${slotId} descartado tras ${maxRetries} intentos.`); return []; } await new Promise(res => setTimeout(res, attempt * 500)); } } return []; } // Render de tarjetas dentro del Drawer function renderCardsInsideDrawer(results) { const bodyEl = document.getElementById('noc-drawer-body-container'); if (!bodyEl) return; let totalEventsCount = 0; let slotsWithOutages = 0; const cardsHtml = results.map(item => { const count = item.events.length; totalEventsCount += count; if (count > 0) slotsWithOutages++; const statusBadge = count > 0 ? `${count} caída(s)` : `Estable`; const eventsList = count > 0 ? `