nid-snmp/walks/nid-viewer.html

1507 lines
249 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NID Viewer &mdash; NID12-DC-MTSO-Pri</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
:root {
--bg-dark: #0f1117;
--bg-card: #1a1d24;
--bg-card2: #22262f;
--text-main: #e2e8f0;
--text-muted: #a0b4c8;
--border-color: #2d3340;
--accent: #0d6efd;
--green: #22c55e;
--amber: #f59e0b;
--red: #dc3545;
--crit: #dc3545;
--major: #fd7e14;
--minor: #ffc107;
--info-sev: #17a2b8;
}
body {
background: var(--bg-dark);
color: var(--text-main);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
font-size: 14px;
}
.card-dark {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 1rem;
}
.card-dark .card-header {
background: var(--bg-card2);
border-bottom: 1px solid var(--border-color);
padding: 0.6rem 1rem;
font-weight: 600;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 8px 8px 0 0;
}
.card-dark .card-body { padding: 1rem; }
.table-dark-custom {
--bs-table-bg: transparent;
--bs-table-color: var(--text-main);
--bs-table-border-color: var(--border-color);
font-size: 0.85rem;
margin-bottom: 0;
}
.table-dark-custom th {
background: var(--bg-card2);
font-weight: 600;
white-space: nowrap;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-muted);
}
.table-dark-custom td { vertical-align: middle; }
.mono { font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.82rem; }
.badge-sev-3 { background: var(--crit); }
.badge-sev-2 { background: var(--major); }
.badge-sev-1 { background: var(--minor); color: #000; }
.badge-sev-0 { background: var(--info-sev); }
.status-up { color: var(--green); font-weight: 600; }
.status-down { color: var(--red); font-weight: 600; }
.status-na { color: #555; }
.kv-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.2rem 1rem;
font-size: 0.85rem;
}
.kv-grid dt { color: var(--text-muted); white-space: nowrap; font-weight: 500; }
.kv-grid dd { margin: 0; }
/* Front panel */
.front-panel {
background: #16181f;
border: 2px solid #3a3f4b;
border-radius: 6px;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 1.2rem;
flex-wrap: wrap;
position: relative;
}
.panel-label {
position: absolute;
top: -10px;
left: 12px;
background: var(--bg-card);
padding: 0 6px;
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sfp-slot {
width: 56px;
height: 44px;
border-radius: 4px;
border: 2px solid #3a3f4b;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
position: relative;
}
.sfp-slot:hover { border-color: var(--accent); transform: translateY(-1px); }
.sfp-slot.present-link { background: rgba(34,197,94,0.15); border-color: var(--green); color: var(--green); }
.sfp-slot.present-nolink { background: rgba(245,158,11,0.15); border-color: var(--amber); color: var(--amber); }
.sfp-slot.empty { background: #1a1d24; border-color: #2d3340; color: #555; }
.sfp-slot.selected { box-shadow: 0 0 0 2px var(--accent); }
.sfp-slot .slot-label { font-size: 0.6rem; color: var(--text-muted); }
.mgmt-port {
width: 40px;
height: 44px;
border-radius: 4px;
border: 2px solid #3a3f4b;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 0.55rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.mgmt-port.link-up { background: rgba(34,197,94,0.15); border-color: var(--green); color: var(--green); }
.mgmt-port.link-down { background: #1a1d24; border-color: #2d3340; color: #555; }
.divider { width: 1px; height: 44px; background: var(--border-color); margin: 0 0.3rem; }
.pwr-led {
width: 12px; height: 12px; border-radius: 50%;
display: inline-block;
margin-right: 4px;
}
.pwr-led.ok { background: var(--green); box-shadow: 0 0 6px rgba(34,197,94,0.5); }
.pwr-led.fail { background: var(--red); box-shadow: 0 0 6px rgba(220,53,69,0.5); }
.pwr-block { display: flex; flex-direction: column; gap: 4px; font-size: 0.7rem; }
.temp-block {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 0.75rem;
min-width: 80px;
}
.temp-bar-track {
height: 6px;
background: #2d3340;
border-radius: 3px;
overflow: hidden;
position: relative;
}
.temp-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s;
}
/* CPU gauge */
.cpu-bar {
display: inline-block;
width: 40px;
height: 8px;
background: #2d3340;
border-radius: 4px;
overflow: hidden;
vertical-align: middle;
margin-left: 4px;
}
.cpu-bar-fill {
height: 100%;
border-radius: 4px;
background: var(--accent);
}
/* Coverage */
.cov-bar-track {
height: 10px;
background: #2d3340;
border-radius: 5px;
overflow: hidden;
min-width: 120px;
}
.cov-bar-fill {
height: 100%;
border-radius: 5px;
background: var(--accent);
}
.oid-bar-track {
height: 14px;
background: #2d3340;
border-radius: 3px;
overflow: hidden;
}
.oid-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 3px;
}
.gap-callout {
background: rgba(220,53,69,0.08);
border-left: 3px solid var(--red);
padding: 0.5rem 0.75rem;
border-radius: 0 4px 4px 0;
font-size: 0.82rem;
margin-bottom: 0.5rem;
}
.gap-callout.warn {
background: rgba(245,158,11,0.08);
border-left-color: var(--amber);
}
/* SFP detail card */
.sfp-detail { display: none; }
.sfp-detail.active { display: block; }
.unavailable {
opacity: 0.4;
position: relative;
}
.unavailable::after {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 4px,
rgba(100,100,100,0.08) 4px,
rgba(100,100,100,0.08) 8px
);
border-radius: 4px;
pointer-events: none;
}
/* Mismatch highlight */
.mismatch { background: rgba(253,126,20,0.12) !important; }
/* Scrollable table wrapper */
.tbl-scroll {
max-height: 400px;
overflow-y: auto;
}
.tbl-scroll::-webkit-scrollbar { width: 6px; }
.tbl-scroll::-webkit-scrollbar-track { background: var(--bg-card); }
.tbl-scroll::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; }
/* LLDP topology diagram */
.topo-container {
display: flex;
align-items: stretch;
gap: 0;
overflow-x: auto;
padding: 1rem 0;
}
.topo-device {
background: var(--bg-card);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 1rem 1.2rem;
min-width: 220px;
max-width: 300px;
flex-shrink: 0;
}
.topo-device.local {
border-color: var(--accent);
}
.topo-device.remote {
border-color: var(--cyan);
}
.topo-device .topo-hostname {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.3rem;
word-break: break-all;
}
.topo-device .topo-model {
font-size: 0.8rem;
color: var(--cyan);
margin-bottom: 0.5rem;
}
.topo-device .topo-detail {
font-size: 0.75rem;
color: var(--text-muted);
margin: 0.15rem 0;
}
.topo-device .topo-detail .label {
color: #666;
min-width: 40px;
display: inline-block;
}
.topo-device .topo-mgmt {
font-size: 0.8rem;
color: var(--green);
font-weight: 600;
margin-top: 0.4rem;
font-family: 'JetBrains Mono', monospace;
}
.topo-link {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 120px;
max-width: 200px;
flex-shrink: 0;
position: relative;
padding: 0 0.5rem;
}
.topo-link .link-line {
width: 100%;
height: 3px;
border-radius: 2px;
position: relative;
}
.topo-link .link-line.up { background: var(--green); box-shadow: 0 0 8px rgba(34,197,94,0.3); }
.topo-link .link-line.down { background: var(--amber); box-shadow: 0 0 8px rgba(245,158,11,0.3); }
.topo-link .link-line::before,
.topo-link .link-line::after {
content: '';
position: absolute;
top: 50%;
width: 8px;
height: 8px;
border-radius: 50%;
transform: translateY(-50%);
}
.topo-link .link-line.up::before,
.topo-link .link-line.up::after { background: var(--green); }
.topo-link .link-line.down::before,
.topo-link .link-line.down::after { background: var(--amber); }
.topo-link .link-line::before { left: -4px; }
.topo-link .link-line::after { right: -4px; }
.topo-link .link-label {
font-size: 0.65rem;
color: var(--text-muted);
text-align: center;
white-space: nowrap;
margin: 0.3rem 0;
font-family: 'JetBrains Mono', monospace;
}
.topo-port-list {
list-style: none;
padding: 0;
margin: 0.5rem 0 0 0;
font-size: 0.75rem;
}
.topo-port-list li {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.1rem 0;
}
.topo-port-list .dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.topo-port-list .dot.up { background: var(--green); }
.topo-port-list .dot.down { background: #555; }
.topo-port-list .dot.linked { background: var(--green); box-shadow: 0 0 4px rgba(34,197,94,0.5); }
.topo-stats-table {
width: 100%;
margin-top: 1rem;
font-size: 0.75rem;
}
.topo-stats-table th {
color: var(--text-muted);
font-weight: 500;
padding: 0.3rem 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.topo-stats-table td {
padding: 0.3rem 0.5rem;
font-family: 'JetBrains Mono', monospace;
}
</style>
</head>
<body>
<div class="container-fluid py-3" style="max-width:1400px">
<!-- ═══════════════ 1. DEVICE HEADER ═══════════════ -->
<div id="sec-header"></div>
<!-- ═══════════════ 2. FRONT PANEL ═══════════════ -->
<div id="sec-panel"></div>
<!-- ═══════════════ 3. INTERFACES TABLE ═══════════════ -->
<div id="sec-interfaces"></div>
<!-- ═══════════════ 4. SFP CARDS ═══════════════ -->
<div id="sec-sfp"></div>
<!-- ═══════════════ 5. ALARMS ═══════════════ -->
<div id="sec-alarms"></div>
<!-- ═══════════════ 6. TRAFFIC POLICIES ═══════════════ -->
<div id="sec-policies"></div>
<!-- ═══════════════ 7. L2 FILTERS ═══════════════ -->
<div id="sec-filters"></div>
<!-- ═══════════════ 8. REGULATORS ═══════════════ -->
<div id="sec-regulators"></div>
<!-- ═══════════════ 9. LLDP ═══════════════ -->
<div id="sec-lldp"></div>
<!-- ═══════════════ 10. COVERAGE MATRIX ═══════════════ -->
<div id="sec-coverage"></div>
<!-- ═══════════════ 11. PORT CONFIG vs STATUS ═══════════════ -->
<div id="sec-portcmp"></div>
</div><!-- container -->
<script>
const DATA = {"device": {"sysDescr": "AMN-1000-GT-S", "sysUpTime": "0:0:41:41.00", "sysContact": "root", "sysName": "LABNID-NID12-DC-MTSO-Pri.alluvion.net", "sysLocation": "Unknown", "commercialName": "AMN-1000-GT-S", "macBaseAddr": "00:15:AD:47:3E:60", "identifier": "NID12-DC-MTSO-Pri", "firmwareVersion": "AMT_7.9.1_23673", "hardwareVersion": "619-2179:20:23:14", "serialNumber": "G419-2495", "hardwareOptions": "Dry-contact Input", "cpuUsageCurrent": "12", "cpuUsageAvg15s": "14", "cpuUsageAvg30s": "15", "cpuUsageAvg60s": "15", "cpuUsageAvg900s": "5", "uptimeSeconds": "90350370"}, "interfaces": {"1": {"ifDescr": "EVEN_VLAN_UPLINK", "ifType": "ethernetCsmacd", "ifMtu": "4096", "ifSpeed": "1000000000", "ifPhysAddress": "0:15:ad:47:3e:61", "ifAdminStatus": "up", "ifOperStatus": "up", "ifInOctets": "4223389593", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "2365165170", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "EVEN_VLAN_UPLINK", "ifHCInOctets": "12813324665", "ifHCOutOctets": "15250068986", "ifHighSpeed": "1000"}, "2": {"ifDescr": "ODD_VLAN_UPLINK", "ifType": "ethernetCsmacd", "ifMtu": "4096", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:62", "ifAdminStatus": "up", "ifOperStatus": "down", "ifInOctets": "135053161", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "110445216", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "ODD_VLAN_UPLINK", "ifHCInOctets": "135053161", "ifHCOutOctets": "110445216", "ifHighSpeed": "0"}, "3": {"ifDescr": "EVEN_VLAN_DOWNLIN", "ifType": "ethernetCsmacd", "ifMtu": "2000", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:63", "ifAdminStatus": "up", "ifOperStatus": "down", "ifInOctets": "2194071425", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "3499102118", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "EVEN_VLAN_DOWNLIN", "ifHCInOctets": "15078973313", "ifHCOutOctets": "12089036710", "ifHighSpeed": "0"}, "4": {"ifDescr": "ODD_VLAN_DOWNLINK", "ifType": "ethernetCsmacd", "ifMtu": "4096", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:64", "ifAdminStatus": "up", "ifOperStatus": "down", "ifInOctets": "314336220", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "354824130", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "ODD_VLAN_DOWNLINK", "ifHCInOctets": "314336220", "ifHCOutOctets": "354824130", "ifHighSpeed": "0"}, "5": {"ifDescr": "Management", "ifType": "ethernetCsmacd", "ifMtu": "2000", "ifSpeed": "100000000", "ifPhysAddress": "0:15:ad:47:3e:65", "ifAdminStatus": "up", "ifOperStatus": "up", "ifInOctets": "3877078151", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "1386639353", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "Management", "ifHCInOctets": "132727052162", "ifHCOutOctets": "1387157336", "ifHighSpeed": "100"}, "6": {"ifDescr": "LAG-1", "ifType": "ethernetCsmacd", "ifMtu": "10240", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:61", "ifAdminStatus": "up", "ifOperStatus": "down", "ifInOctets": "0", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "0", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "LAG-1", "ifHCInOctets": "0", "ifHCOutOctets": "0", "ifHighSpeed": "0"}, "7": {"ifDescr": "LAG-2", "ifType": "ethernetCsmacd", "ifMtu": "10240", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:63", "ifAdminStatus": "up", "ifOperStatus": "down", "ifInOctets": "0", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "0", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "LAG-2", "ifHCInOctets": "0", "ifHCOutOctets": "0", "ifHighSpeed": "0"}, "1003": {"ifDescr": "VLAN_MGMT", "ifType": "ethernetCsmacd", "ifMtu": "2000", "ifSpeed": "0", "ifPhysAddress": "0:15:ad:47:3e:65", "ifAdminStatus": "up", "ifOperStatus": "up", "ifInOctets": "0", "ifInDiscards": "0", "ifInErrors": "0", "ifOutOctets": "0", "ifOutDiscards": "0", "ifOutErrors": "0", "ifName": "VLAN_MGMT", "ifHCInOctets": "0", "ifHCOutOctets": "0", "ifHighSpeed": "0"}}, "connectors": {"1": {"id": "1", "name": "SFP-1", "type": "14", "poeSupport": "2"}, "2": {"id": "2", "name": "SFP-2", "type": "14", "poeSupport": "2"}, "3": {"id": "3", "name": "SFP-3",
// ── Helpers ──────────────────────────────────────────
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function formatBytes(n) {
n = parseInt(n);
if (isNaN(n) || n === 0) return '0 B';
const u = ['B','KB','MB','GB','TB'];
const i = Math.floor(Math.log(n)/Math.log(1024));
return (n/Math.pow(1024,i)).toFixed(i?1:0) + ' ' + u[i];
}
function formatUptime(sec) {
sec = parseInt(sec);
if (isNaN(sec)) return '?';
const d = Math.floor(sec/86400), h = Math.floor((sec%86400)/3600),
m = Math.floor((sec%3600)/60), s = sec%60;
let p = [];
if (d) p.push(d+'d');
if (h) p.push(h+'h');
if (m) p.push(m+'m');
p.push(s+'s');
return p.join(' ');
}
function sevLabel(s) {
return {'0':'INFO','1':'MINOR','2':'MAJOR','3':'CRITICAL'}[s] || s;
}
function sevClass(s) {
return 'badge-sev-' + (s||'0');
}
function cpuBar(pct) {
pct = parseInt(pct)||0;
const c = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--amber)' : 'var(--accent)';
return `<span class="cpu-bar"><span class="cpu-bar-fill" style="width:${pct}%;background:${c}"></span></span> ${pct}%`;
}
function tempColor(cur, high, crit) {
cur = parseInt(cur)||0; high = parseInt(high)||85; crit = parseInt(crit)||90;
if (cur >= crit) return 'var(--red)';
if (cur >= high) return 'var(--amber)';
return 'var(--green)';
}
function parseDateAndTime(hex) {
// SNMP DateAndTime: 11 bytes hex-encoded like "07 B5 01 18 12 07 19 00 2D 07 00"
if (!hex || hex.startsWith('00 00 01 01 00 00')) return '';
const p = hex.split(' ').map(h => parseInt(h,16));
if (p.length < 8) return hex;
const yr = (p[0]<<8)|p[1], mo=p[2], dy=p[3], hr=p[4], mn=p[5], sc=p[6];
return `${yr}-${String(mo).padStart(2,'0')}-${String(dy).padStart(2,'0')} ${String(hr).padStart(2,'0')}:${String(mn).padStart(2,'0')}:${String(sc).padStart(2,'0')}`;
}
function isPopulated(v) {
if (v == null || v === '') return false;
if (v === '0' || v === '-inf dBm' || v === '0.0') return false;
if (/^0+$/.test(v.replace(/\s/g,''))) return false;
if (/^(00\s)+00?$/.test(v.trim())) return false;
return true;
}
// ── 1. Device Header ─────────────────────────────────
function renderHeader() {
const d = DATA.device || {};
const alarmStatus = DATA.alarm_status || {};
let activeCount = 0, sevCounts = {0:0,1:0,2:0,3:0};
const alarmCfg = DATA.alarm_config || {};
// Build number→config lookup
const cfgByNum = {};
for (const [k,v] of Object.entries(alarmCfg)) cfgByNum[v.number] = v;
for (const [k,a] of Object.entries(alarmStatus)) {
if (a.active === '1') {
activeCount++;
const cfg = cfgByNum[a.number];
if (cfg) sevCounts[cfg.severity] = (sevCounts[cfg.severity]||0) + 1;
}
}
let alarmBadge = '';
if (activeCount > 0) {
const parts = [];
if (sevCounts[3]) parts.push(`<span class="badge badge-sev-3">${sevCounts[3]} CRIT</span>`);
if (sevCounts[2]) parts.push(`<span class="badge badge-sev-2">${sevCounts[2]} MAJ</span>`);
if (sevCounts[1]) parts.push(`<span class="badge badge-sev-1">${sevCounts[1]} MIN</span>`);
if (sevCounts[0]) parts.push(`<span class="badge badge-sev-0">${sevCounts[0]} INFO</span>`);
alarmBadge = `<span class="badge bg-danger">${activeCount} Active Alarms</span> ${parts.join(' ')}`;
} else {
alarmBadge = '<span class="badge bg-success">No Active Alarms</span>';
}
document.getElementById('sec-header').innerHTML = `
<div class="card-dark">
<div class="card-header">
<i class="bi bi-router"></i>
${esc(d.commercialName || d.sysDescr || 'Accedian NID')}
<span class="ms-auto" style="font-weight:400;font-size:0.82rem;color:var(--text-muted)">
SNMP Walk Visualization
</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<dl class="kv-grid">
<dt>Hostname</dt><dd class="mono">${esc(d.sysName)}</dd>
<dt>Identifier</dt><dd class="mono">${esc(d.identifier)}</dd>
<dt>Serial</dt><dd class="mono">${esc(d.serialNumber)}</dd>
<dt>Firmware</dt><dd class="mono">${esc(d.firmwareVersion)}</dd>
<dt>Hardware</dt><dd class="mono">${esc(d.hardwareVersion)}</dd>
<dt>MAC</dt><dd class="mono">${esc(d.macBaseAddr)}</dd>
<dt>Options</dt><dd>${esc(d.hardwareOptions)}</dd>
<dt>Location</dt><dd>${esc(d.sysLocation)}</dd>
<dt>Contact</dt><dd>${esc(d.sysContact)}</dd>
<dt>Uptime</dt><dd class="mono">${formatUptime(d.uptimeSeconds)}</dd>
</dl>
</div>
<div class="col-md-3">
<h6 style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">CPU UTILIZATION</h6>
<div style="font-size:0.82rem">
<div>Current: ${cpuBar(d.cpuUsageCurrent)}</div>
<div>15s avg: ${cpuBar(d.cpuUsageAvg15s)}</div>
<div>30s avg: ${cpuBar(d.cpuUsageAvg30s)}</div>
<div>60s avg: ${cpuBar(d.cpuUsageAvg60s)}</div>
<div>15m avg: ${cpuBar(d.cpuUsageAvg900s)}</div>
</div>
</div>
<div class="col-md-3 text-end">
<div style="margin-bottom:0.5rem">${alarmBadge}</div>
<div style="font-size:0.78rem;color:var(--text-muted)">
${Object.keys(alarmStatus).length} alarm definitions<br>
${Object.keys(DATA.interfaces||{}).length} interfaces<br>
${Object.keys(DATA.connectors||{}).length} connectors
</div>
</div>
</div>
</div>
</div>`;
}
// ── 2. Front Panel ───────────────────────────────────
function renderPanel() {
const connectors = DATA.connectors || {};
const sfpInfo = DATA.sfp_info || {};
const portStatus = DATA.port_status || {};
const ifaces = DATA.interfaces || {};
const pwr = DATA.power_supplies || {};
const temps = DATA.temperature_sensors || {};
// Determine SFP slot states: need to map connector to interface link status
function sfpState(connIdx) {
const sfp = sfpInfo[connIdx];
const present = sfp && sfp.present === '1';
if (!present) return 'empty';
// Check interface link — interfaces 1-4 map to connectors 1-4 roughly
const iface = ifaces[connIdx];
if (iface && iface.ifOperStatus === 'up') return 'present-link';
return 'present-nolink';
}
function sfpLabel(connIdx) {
const sfp = sfpInfo[connIdx];
if (!sfp) return '';
if (sfp.present !== '1') return 'EMPTY';
const pn = sfp.vendorPn || '';
if (pn.length > 8) return pn.substring(0,8);
return pn || sfp.vendor || '';
}
let slots = '';
for (let i = 1; i <= 4; i++) {
const state = sfpState(String(i));
const label = sfpLabel(String(i));
const icon = state === 'empty' ? '<i class="bi bi-dash"></i>' :
state === 'present-link' ? '<i class="bi bi-arrow-left-right"></i>' :
'<i class="bi bi-plug"></i>';
slots += `<div class="sfp-slot ${state}" data-sfp="${i}" onclick="selectSfp(${i})">
${icon}<span class="slot-label">SFP-${i}</span>
<span style="font-size:0.5rem;max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(label)}</span>
</div>`;
}
// Management port
const mgmtIf = ifaces['5'];
const mgmtUp = mgmtIf && mgmtIf.ifOperStatus === 'up';
const mgmtSlot = `<div class="mgmt-port ${mgmtUp ? 'link-up' : 'link-down'}">
<i class="bi bi-ethernet"></i><span style="font-size:0.55rem">MGMT</span>
<span style="font-size:0.5rem">${mgmtUp ? 'UP' : 'DOWN'}</span>
</div>`;
// Power feeds
let pwrHtml = '<div class="pwr-block">';
for (const [k,p] of Object.entries(pwr)) {
const ok = p.present === '1';
pwrHtml += `<div><span class="pwr-led ${ok?'ok':'fail'}"></span>${esc(p.name)} ${ok?'OK':'ABSENT'}</div>`;
}
pwrHtml += '</div>';
// Temperature
let tempHtml = '';
for (const [k,t] of Object.entries(temps)) {
const cur = parseInt(t.currentTemp)||0;
const crit = parseInt(t.criticalThreshold)||90;
const pct = Math.min(100, Math.round(cur/crit*100));
const col = tempColor(t.currentTemp, t.highThreshold, t.criticalThreshold);
tempHtml += `<div class="temp-block">
<span style="color:${col};font-weight:600">${cur}&deg;C</span>
<span style="font-size:0.6rem;color:var(--text-muted)">${esc(t.label)} (warn:${t.highThreshold} crit:${t.criticalThreshold})</span>
<div class="temp-bar-track"><div class="temp-bar-fill" style="width:${pct}%;background:${col}"></div></div>
</div>`;
}
document.getElementById('sec-panel').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-cpu"></i> Front Panel</div>
<div class="card-body">
<div class="front-panel">
<span class="panel-label">AMN-1000-GT-S</span>
${slots}
<div class="divider"></div>
${mgmtSlot}
<div class="divider"></div>
${pwrHtml}
<div class="divider"></div>
${tempHtml}
</div>
<div style="font-size:0.7rem;color:var(--text-muted);margin-top:0.5rem">
<span style="display:inline-block;width:10px;height:10px;background:rgba(34,197,94,0.3);border:1px solid var(--green);border-radius:2px;margin-right:2px"></span> Present + Link &nbsp;
<span style="display:inline-block;width:10px;height:10px;background:rgba(245,158,11,0.3);border:1px solid var(--amber);border-radius:2px;margin-right:2px"></span> Present, No Link &nbsp;
<span style="display:inline-block;width:10px;height:10px;background:#1a1d24;border:1px solid #2d3340;border-radius:2px;margin-right:2px"></span> Empty
</div>
</div>
</div>`;
}
// ── 3. Interfaces Table ──────────────────────────────
function renderInterfaces() {
const ifaces = DATA.interfaces || {};
const portCfg = DATA.port_config || {};
let rows = '';
const sortedKeys = Object.keys(ifaces).sort((a,b) => parseInt(a)-parseInt(b));
for (const idx of sortedKeys) {
const iface = ifaces[idx];
const cfg = portCfg[idx] || {};
const up = iface.ifOperStatus === 'up';
const statusCls = up ? 'status-up' : 'status-down';
const rowCls = up ? '' : 'style="opacity:0.7"';
const speed = parseInt(iface.ifHighSpeed);
let speedStr = '';
if (speed >= 1000) speedStr = (speed/1000) + ' Gbps';
else if (speed > 0) speedStr = speed + ' Mbps';
else speedStr = '<span class="status-na">--</span>';
rows += `<tr ${rowCls}>
<td class="mono">${idx}</td>
<td><strong>${esc(iface.ifDescr)}</strong></td>
<td class="${statusCls}">${esc(iface.ifAdminStatus)}</td>
<td class="${statusCls}">${esc(iface.ifOperStatus)}</td>
<td>${speedStr}</td>
<td class="mono">${esc(iface.ifMtu)}</td>
<td class="mono" style="font-size:0.75rem">${esc(iface.ifPhysAddress)}</td>
<td class="mono">${formatBytes(iface.ifHCInOctets)}</td>
<td class="mono">${formatBytes(iface.ifHCOutOctets)}</td>
<td>${parseInt(iface.ifInErrors)||0}</td>
<td>${parseInt(iface.ifInDiscards)||0}</td>
<td>${parseInt(iface.ifOutErrors)||0}</td>
</tr>`;
}
document.getElementById('sec-interfaces').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-ethernet"></i> Interfaces & Traffic</div>
<div class="card-body" style="padding:0">
<div class="tbl-scroll">
<table class="table table-dark-custom table-sm table-hover">
<thead><tr>
<th>#</th><th>Name</th><th>Admin</th><th>Oper</th><th>Speed</th>
<th>MTU</th><th>MAC</th><th>RX</th><th>TX</th>
<th>In Err</th><th>In Disc</th><th>Out Err</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 4. SFP Cards ─────────────────────────────────────
let selectedSfp = null;
function selectSfp(idx) {
selectedSfp = idx;
document.querySelectorAll('.sfp-slot').forEach(s => s.classList.remove('selected'));
const el = document.querySelector(`.sfp-slot[data-sfp="${idx}"]`);
if (el) el.classList.add('selected');
document.querySelectorAll('.sfp-detail').forEach(d => d.classList.remove('active'));
const detail = document.getElementById('sfp-detail-'+idx);
if (detail) detail.classList.add('active');
}
function renderSfp() {
const connectors = DATA.connectors || {};
const sfpInfo = DATA.sfp_info || {};
const sfpDiag = DATA.sfp_diagnostics || {};
const sfpThresh = DATA.sfp_thresholds || {};
let cards = '';
for (let i = 1; i <= 4; i++) {
const si = String(i);
const info = sfpInfo[si];
const diag = sfpDiag[si] || {};
const thresh = sfpThresh[si] || {};
const conn = connectors[si] || {};
if (!info || info.present !== '1') {
cards += `<div class="sfp-detail" id="sfp-detail-${i}">
<div class="card-dark" style="border-left:3px solid #555">
<div class="card-body" style="text-align:center;color:#555;padding:2rem">
<i class="bi bi-slash-circle" style="font-size:2rem"></i>
<div>SFP-${i} &mdash; Not Present</div>
</div>
</div>
</div>`;
continue;
}
const ddm = info.diagCapable === '1';
const ddmBadge = ddm
? '<span class="badge bg-success">DDM Supported</span>'
: '<span class="badge bg-secondary">DDM Not Supported</span>';
let diagHtml;
if (ddm) {
diagHtml = `<div class="row g-2 mt-2">
<div class="col-6"><dt>TX Power</dt><dd class="mono">${esc(diag.txPower_dBm)}</dd></div>
<div class="col-6"><dt>RX Power</dt><dd class="mono">${esc(diag.rxPower_dBm)}</dd></div>
<div class="col-4"><dt>Temp</dt><dd class="mono">${esc(diag.temperature)}&deg;C</dd></div>
<div class="col-4"><dt>Vcc</dt><dd class="mono">${esc(diag.supplyVoltage)}</dd></div>
<div class="col-4"><dt>LBC</dt><dd class="mono">${esc(diag.laserBiasCurrent)} uA</dd></div>
</div>`;
} else {
diagHtml = `<div class="unavailable" style="padding:0.75rem;border-radius:4px;margin-top:0.5rem">
<div style="text-align:center;color:#666;font-size:0.82rem">
<i class="bi bi-eye-slash"></i> SFP DDM diagnostics not available via SNMP<br>
<span style="font-size:0.75rem">diagCapable=false &mdash; SNMP agent reports zeros.<br>
NID web UI reads SFP I2C bus directly (bypasses SNMP).</span>
</div>
<div class="row g-2 mt-1" style="opacity:0.3">
<div class="col-6"><dt>TX Power</dt><dd class="mono">${esc(diag.txPower_dBm || '--')}</dd></div>
<div class="col-6"><dt>RX Power</dt><dd class="mono">${esc(diag.rxPower_dBm || '--')}</dd></div>
<div class="col-4"><dt>Temp</dt><dd class="mono">--</dd></div>
<div class="col-4"><dt>Vcc</dt><dd class="mono">--</dd></div>
<div class="col-4"><dt>LBC</dt><dd class="mono">--</dd></div>
</div>
</div>`;
}
const wl = info.wavelength && info.wavelength !== '0' ? info.wavelength + ' nm' : 'N/A (copper)';
const mfgDate = [info.mfgYear, String(info.mfgMonth||'').padStart(2,'0'), String(info.mfgDay||'').padStart(2,'0')].join('-');
cards += `<div class="sfp-detail ${i===1?'active':''}" id="sfp-detail-${i}">
<div class="card-dark" style="border-left:3px solid var(--accent)">
<div class="card-header">
<i class="bi bi-lightning-charge"></i> SFP-${i}: ${esc(info.vendor)} ${esc(info.vendorPn)}
<span class="ms-auto">${ddmBadge}</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<dl class="kv-grid">
<dt>Vendor</dt><dd class="mono">${esc(info.vendor)}</dd>
<dt>Part Number</dt><dd class="mono">${esc(info.vendorPn)}</dd>
<dt>Revision</dt><dd class="mono">${esc(info.vendorRev)}</dd>
<dt>Serial</dt><dd class="mono">${esc(info.serialNum)}</dd>
<dt>Wavelength</dt><dd class="mono">${wl}</dd>
<dt>Manufactured</dt><dd class="mono">${mfgDate}</dd>
<dt>Connector Type</dt><dd class="mono">${esc(info.connectorType)}</dd>
</dl>
</div>
<div class="col-md-6">
<h6 style="font-size:0.8rem;color:var(--text-muted)">CAPABILITIES</h6>
<div style="font-size:0.82rem">
<div>DDM Capable: <strong>${ddm ? 'Yes' : 'No'}</strong></div>
<div>Internal Cal: <strong>${info.internalCal==='1' ? 'Yes' : 'No'}</strong></div>
<div>Alarm Capable: <strong>${info.alarmCapable==='1' ? 'Yes' : 'No'}</strong></div>
<div>SFF-8472 Rev: <strong>${esc(info.rev8472)}</strong></div>
<div>ID Type: <strong>${esc(info.idType)}</strong></div>
<div>Ext ID: <strong>${esc(info.extIdType)}</strong></div>
</div>
<h6 style="font-size:0.8rem;color:var(--text-muted);margin-top:0.75rem">DIAGNOSTICS</h6>
${diagHtml}
</div>
</div>
</div>
</div>
</div>`;
}
document.getElementById('sec-sfp').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-lightning-charge"></i> SFP Transceivers</div>
<div class="card-body">
<div style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.5rem">
Click an SFP slot in the front panel above, or select below:
${[1,2,3,4].map(i => `<button class="btn btn-sm btn-outline-secondary ms-1" onclick="selectSfp(${i})">SFP-${i}</button>`).join('')}
</div>
${cards}
</div>
</div>`;
}
// ── 5. Alarms ────────────────────────────────────────
function renderAlarms() {
const alarmCfg = DATA.alarm_config || {};
const alarmStatus = DATA.alarm_status || {};
const alarmGen = DATA.alarm_general || {};
// Build number→config lookup
const cfgByNum = {};
for (const [k,v] of Object.entries(alarmCfg)) cfgByNum[v.number] = v;
// Collect active alarms
const active = [];
for (const [k,a] of Object.entries(alarmStatus)) {
if (a.active === '1') {
const cfg = cfgByNum[a.number] || {};
active.push({ ...a, ...cfg, _statusId: k });
}
}
active.sort((a,b) => parseInt(b.severity||0) - parseInt(a.severity||0));
let rows = '';
for (const a of active) {
const dt = parseDateAndTime(a.lastChange);
rows += `<tr>
<td><span class="badge ${sevClass(a.severity)}">${sevLabel(a.severity)}</span></td>
<td class="mono">${esc(a.conditionType)}</td>
<td class="mono">${esc(a.amoType)}</td>
<td>${esc(a.description)}</td>
<td>${esc(a.message)}</td>
<td class="mono" style="font-size:0.75rem">${dt}</td>
</tr>`;
}
const total = Object.keys(alarmStatus).length;
// Severity breakdown
const sevCounts = {0:0,1:0,2:0,3:0};
active.forEach(a => sevCounts[a.severity] = (sevCounts[a.severity]||0)+1);
document.getElementById('sec-alarms').innerHTML = `
<div class="card-dark">
<div class="card-header">
<i class="bi bi-exclamation-triangle"></i> Alarms
<span class="badge bg-danger ms-2">${active.length} Active</span>
<span class="ms-auto" style="font-weight:400;font-size:0.78rem;color:var(--text-muted)">${total} defined</span>
</div>
<div class="card-body" style="padding:0">
<div style="padding:0.5rem 1rem;font-size:0.8rem;display:flex;gap:1rem;flex-wrap:wrap">
<span><span class="badge badge-sev-3">&nbsp;</span> Critical: ${sevCounts[3]}</span>
<span><span class="badge badge-sev-2">&nbsp;</span> Major: ${sevCounts[2]}</span>
<span><span class="badge badge-sev-1">&nbsp;</span> Minor: ${sevCounts[1]}</span>
<span><span class="badge badge-sev-0">&nbsp;</span> Info: ${sevCounts[0]}</span>
<span class="ms-auto" style="color:var(--text-muted)">
Thresh On: ${alarmGen.threshOnMs||'?'}ms | Off: ${alarmGen.threshOffMs||'?'}ms |
LED: ${alarmGen.ledEnabled==='1'?'On':'Off'} |
Syslog: ${alarmGen.syslogEnabled==='1'?'On':'Off'} |
SNMP Trap: ${alarmGen.snmpEnabled==='1'?'On':'Off'}
</span>
</div>
<div class="tbl-scroll" style="max-height:350px">
<table class="table table-dark-custom table-sm table-hover">
<thead><tr>
<th>Severity</th><th>Condition</th><th>Object</th>
<th>Description</th><th>Message</th><th>Last Change</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 6. Traffic Policies ──────────────────────────────
function renderPolicies() {
const lists = DATA.policy_lists || {};
const bindings = DATA.policy_port_bindings || {};
const entries = DATA.policy_entries || {};
const stats = DATA.policy_stats || {};
const portCfg = DATA.port_config || {};
const filters = DATA.l2_filters || {};
// Build filter name lookup
const filterNames = {};
for (const [k,v] of Object.entries(filters)) filterNames[k] = v.name || ('Filter-'+k);
const filterTypeMap = {'0':'L2','1':'IPv4','2':'IPv6','3':'VList'};
const actionMap = {'1':'Drop','2':'Permit','3':'Mgmt/OAM','4':'EVC','5':'Deny'};
// Policy lists + port bindings
let listRows = '';
for (const [id, pl] of Object.entries(lists)) {
// Find ports bound to this list
const ports = [];
for (const [portIdx, b] of Object.entries(bindings)) {
if (b.policyListId === id) {
const name = (portCfg[portIdx] || {}).name || ('Port-'+portIdx);
ports.push(name);
}
}
listRows += `<tr>
<td class="mono">${id}</td>
<td><strong>${esc(pl.name)}</strong></td>
<td class="mono">${pl.nbrEntries}</td>
<td>${ports.length ? ports.map(p => `<span class="badge bg-secondary me-1">${esc(p)}</span>`).join('') : '<span class="status-na">none</span>'}</td>
</tr>`;
}
// Enabled policy entries grouped by list
let entryRows = '';
for (const [id, e] of Object.entries(entries)) {
const listName = (lists[e.listId] || {}).name || e.listId;
const fType = filterTypeMap[e.filterType] || e.filterType;
const fName = filterNames[e.filterIndex] || ('idx:'+e.filterIndex);
const action = actionMap[e.action] || e.action;
const actionCls = e.action === '2' ? 'status-up' : e.action === '1' ? 'status-down' : '';
// Find matching stats
const st = stats[id] || {};
const pkts = st.inHCPkts ? parseInt(st.inHCPkts).toLocaleString() : '0';
const octets = st.inHCOctets ? formatBytes(st.inHCOctets) : '0 B';
entryRows += `<tr>
<td class="mono">${id}</td>
<td>${esc(listName)}</td>
<td class="mono">${fType}</td>
<td>${esc(fName)}</td>
<td class="${actionCls}">${action}</td>
<td class="mono">${pkts}</td>
<td class="mono">${octets}</td>
</tr>`;
}
document.getElementById('sec-policies').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-shield-check"></i> Traffic Policies</div>
<div class="card-body">
<h6 style="font-size:0.85rem;margin-bottom:0.5rem">Policy Lists &amp; Port Bindings</h6>
<table class="table table-dark-custom table-sm">
<thead><tr><th>ID</th><th>List Name</th><th>Max Entries</th><th>Bound Ports</th></tr></thead>
<tbody>${listRows}</tbody>
</table>
<h6 style="font-size:0.85rem;margin-top:1rem;margin-bottom:0.5rem">Enabled Policy Rules (${Object.keys(entries).length} active of 400 slots)</h6>
<div class="tbl-scroll" style="max-height:300px">
<table class="table table-dark-custom table-sm table-hover">
<thead><tr>
<th>#</th><th>List</th><th>Filter Type</th><th>Filter Name</th>
<th>Action</th><th>Matched Pkts</th><th>Matched Bytes</th>
</tr></thead>
<tbody>${entryRows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 7. L2 Filters ────────────────────────────────────
function renderFilters() {
const filters = DATA.l2_filters || {};
let rows = '';
for (const [id, f] of Object.entries(filters)) {
const conditions = [];
if (f.macDstEn === '1') conditions.push('MAC Dst: ' + esc(f.macDst));
if (f.macSrcEn === '1') conditions.push('MAC Src: ' + esc(f.macSrc));
if (f.etypeEn === '1') conditions.push('EType: ' + esc(f.etype));
if (f.vlan1IdEn === '1') conditions.push('VLAN1: ' + esc(f.vlan1Id));
if (f.vlan2IdEn === '1') conditions.push('VLAN2: ' + esc(f.vlan2Id));
if (f.vlan1PriorEn === '1') conditions.push('PCP1: ' + esc(f.vlan1Prior));
const condStr = conditions.length ? conditions.join(', ') : '<span class="status-na">any (catchall)</span>';
rows += `<tr>
<td class="mono">${id}</td>
<td><strong>${esc(f.name)}</strong></td>
<td style="font-size:0.8rem">${condStr}</td>
</tr>`;
}
document.getElementById('sec-filters').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-funnel"></i> L2 Filters (${Object.keys(filters).length} defined)</div>
<div class="card-body" style="padding:0">
<div class="tbl-scroll" style="max-height:350px">
<table class="table table-dark-custom table-sm table-hover">
<thead><tr><th>ID</th><th>Name</th><th>Match Conditions</th></tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 8. Regulators ────────────────────────────────────
function renderRegulators() {
const regs = DATA.regulators || {};
const regStats = DATA.regulator_stats || {};
const cos = DATA.cos_profiles || {};
let regRows = '';
for (const [id, r] of Object.entries(regs)) {
const st = regStats[id] || {};
const cir = parseInt(r.cirKbps)||0;
const eir = parseInt(r.eirKbps)||0;
const cirStr = cir >= 1000 ? (cir/1000).toFixed(0)+' Mbps' : cir+' Kbps';
const eirStr = eir >= 1000 ? (eir/1000).toFixed(0)+' Mbps' : eir+' Kbps';
const cirMax = parseInt(r.cirMaxKbps)||0;
const cirMaxStr = cirMax >= 1000 ? (cirMax/1000).toFixed(0)+' Mbps' : cirMax+' Kbps';
const greenPkts = parseInt(st.greenHCPkts)||0;
const yellowPkts = parseInt(st.yellowHCPkts)||0;
const redPkts = parseInt(st.redHCPkts)||0;
const totalPkts = greenPkts + yellowPkts + redPkts;
const greenPct = totalPkts ? Math.round(greenPkts/totalPkts*100) : 0;
const yellowPct = totalPkts ? Math.round(yellowPkts/totalPkts*100) : 0;
const redPct = totalPkts ? Math.round(redPkts/totalPkts*100) : 0;
regRows += `<tr>
<td class="mono">${id}</td>
<td><strong>${esc(r.name)}</strong></td>
<td class="mono">${cirStr}</td>
<td class="mono">${r.cbsKiB} KiB</td>
<td class="mono">${eirStr}</td>
<td class="mono">${r.ebsKiB} KiB</td>
<td class="mono">${cirMaxStr}</td>
<td>${r.isBlind==='1'?'Yes':'No'}</td>
<td>${r.workingRate==='1'?'L1':'L2'}</td>
</tr>
<tr>
<td colspan="9" style="padding:0.3rem 1rem;border-top:0">
<div style="display:flex;gap:1.5rem;font-size:0.8rem">
<span style="color:var(--green)">Green: ${greenPkts.toLocaleString()} pkts (${greenPct}%)</span>
<span style="color:var(--amber)">Yellow: ${yellowPkts.toLocaleString()} pkts (${yellowPct}%)</span>
<span style="color:var(--red)">Red/Drop: ${redPkts.toLocaleString()} pkts (${redPct}%)</span>
<span style="color:var(--text-muted)">Accept: ${formatBytes(st.acceptHCOctets||'0')} | Drop: ${formatBytes(st.dropHCOctets||'0')}</span>
</div>
<div style="display:flex;height:6px;border-radius:3px;overflow:hidden;margin-top:4px;background:#2d3340">
${greenPct ? `<div style="width:${greenPct}%;background:var(--green)"></div>` : ''}
${yellowPct ? `<div style="width:${yellowPct}%;background:var(--amber)"></div>` : ''}
${redPct ? `<div style="width:${redPct}%;background:var(--red)"></div>` : ''}
</div>
</td>
</tr>`;
}
// CoS profiles
const cosTypeMap = {'1':'PCP','2':'DSCP','3':'Precedence'};
let cosRows = '';
for (const [id, c] of Object.entries(cos)) {
cosRows += `<tr>
<td class="mono">${id}</td>
<td><strong>${esc(c.name)}</strong></td>
<td>${cosTypeMap[c.type] || c.type}</td>
<td>${c.decodeDropBit==='1'?'Yes':'No'}</td>
<td>${c.encodeDropBit==='1'?'Yes':'No'}</td>
</tr>`;
}
document.getElementById('sec-regulators').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-speedometer2"></i> Bandwidth Regulators &amp; QoS</div>
<div class="card-body">
<h6 style="font-size:0.85rem;margin-bottom:0.5rem">Regulators (${Object.keys(regs).length})</h6>
${Object.keys(regs).length ? `
<table class="table table-dark-custom table-sm">
<thead><tr>
<th>ID</th><th>Name</th><th>CIR</th><th>CBS</th><th>EIR</th><th>EBS</th>
<th>CIR Max</th><th>Color-Blind</th><th>Rate Mode</th>
</tr></thead>
<tbody>${regRows}</tbody>
</table>
` : '<div style="color:#555;text-align:center;padding:1rem">No regulators configured</div>'}
<h6 style="font-size:0.85rem;margin-top:1rem;margin-bottom:0.5rem">CoS Profiles (${Object.keys(cos).length})</h6>
<div class="tbl-scroll" style="max-height:250px">
<table class="table table-dark-custom table-sm">
<thead><tr><th>ID</th><th>Name</th><th>Type</th><th>Decode Drop</th><th>Encode Drop</th></tr></thead>
<tbody>${cosRows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 9. LLDP Topology ─────────────────────────────────
function parseRemotePlatform(sysDesc, sysName) {
// Parse sysDescr to extract vendor/model/firmware
// Cisco IOS-XR: " 7.5.2, NCS-5500" or "Cisco IOS XR Software, Version 7.5.2"
// Cisco IOS: "Cisco IOS Software, ..."
// Accedian: "AMN-1000-GT-S"
sysDesc = (sysDesc || '').trim();
sysName = (sysName || '').trim();
// Cisco NCS-5500 / IOS-XR pattern: "7.5.2, NCS-5500"
let m = sysDesc.match(/(\d+\.\d+\.\d+),?\s+(NCS-\S+|ASR-\S+|XRv\S*)/i);
if (m) return { vendor: 'Cisco', model: m[2], firmware: 'IOS-XR ' + m[1] };
// Cisco IOS pattern: "Cisco IOS Software, ... Version X.Y"
m = sysDesc.match(/Cisco IOS.*Version\s+(\S+)/i);
if (m) return { vendor: 'Cisco', model: sysName.split('.')[0], firmware: 'IOS ' + m[1] };
// Accedian pattern
m = sysDesc.match(/(AMN-\S+|MetroNID)/i);
if (m) return { vendor: 'Accedian', model: m[1], firmware: sysDesc };
// Generic: just show what we have
if (sysDesc) return { vendor: '', model: sysDesc, firmware: '' };
return { vendor: '', model: 'Unknown', firmware: '' };
}
function renderLldp() {
const neighbors = DATA.lldp_neighbors || {};
const stats = DATA.lldp_stats || {};
const ifaces = DATA.interfaces || {};
const connectors = DATA.connectors || {};
const device = DATA.device || {};
const neighborList = Object.values(neighbors);
const hasNeighbors = neighborList.length > 0;
// Build local device info
const localName = device.identifier || device.sysName || 'NID';
const localModel = device.commercialName || 'AMN-1000-GT-S';
// Build port list with link status and LLDP neighbor annotation
const neighborByPort = {};
neighborList.forEach(n => { neighborByPort[n.localPort] = n; });
let portListHtml = '';
// SFP ports 1-4
for (let i = 1; i <= 4; i++) {
const iface = ifaces[String(i)] || {};
const operUp = iface.ifOperStatus === 'up';
const hasNbr = !!neighborByPort[String(i)];
const dotClass = hasNbr ? 'linked' : (operUp ? 'up' : 'down');
const nbrLabel = hasNbr ? ' <i class="bi bi-arrow-right" style="font-size:0.6rem"></i>' : '';
portListHtml += `<li><span class="dot ${dotClass}"></span>SFP-${i} ${iface.ifName||''}${nbrLabel}</li>`;
}
// Mgmt port
const mgmtIface = ifaces['5'] || {};
const mgmtUp = mgmtIface.ifOperStatus === 'up';
portListHtml += `<li><span class="dot ${mgmtUp?'up':'down'}"></span>MGMT ${mgmtUp?'UP':'DOWN'}</li>`;
// Build neighbor cards
let neighborHtml = '';
if (hasNeighbors) {
neighborList.forEach(n => {
const platform = parseRemotePlatform(n.remSysDesc, n.remSysName);
const shortName = (n.remSysName || '').split('.')[0] || 'Unknown';
const localIface = ifaces[n.localPort] || {};
const localUp = localIface.ifOperStatus === 'up';
const linkClass = localUp ? 'up' : 'down';
// Model line: "Cisco NCS-5500" or just the model
const modelLine = platform.vendor ? `${platform.vendor} ${platform.model}` : platform.model;
neighborHtml += `
<div class="topo-link">
<div class="link-label">SFP-${n.localPort} / ${esc(n.localPortName || 'Port ' + n.localPort)}</div>
<div class="link-line ${linkClass}"></div>
<div class="link-label">${esc(n.remPortId || '?')}</div>
</div>
<div class="topo-device remote">
<div class="topo-hostname">${esc(shortName)}</div>
<div class="topo-model">${esc(modelLine)}</div>
${platform.firmware ? `<div class="topo-detail"><span class="label">FW</span> ${esc(platform.firmware)}</div>` : ''}
<div class="topo-detail"><span class="label">Port</span> <span class="mono">${esc(n.remPortId || '?')}</span></div>
${n.remPortDesc ? `<div class="topo-detail"><span class="label">Desc</span> ${esc(n.remPortDesc)}</div>` : ''}
<div class="topo-detail"><span class="label">MAC</span> <span class="mono">${esc(n.chassisId || '?')}</span></div>
${n.mgmtIPv4 ? `<div class="topo-mgmt"><i class="bi bi-globe2"></i> ${esc(n.mgmtIPv4)}</div>` : ''}
${n.mgmtIPv6 ? `<div class="topo-detail"><span class="label">IPv6</span> <span class="mono" style="font-size:0.65rem">${esc(n.mgmtIPv6)}</span></div>` : ''}
<div class="topo-detail" style="margin-top:0.3rem">
<span class="label">Caps</span>
${n.capsEnabled === '2' ? '<span style="color:var(--amber)">Bridge</span>' :
n.capsEnabled === '4' ? '<span style="color:var(--cyan)">Router</span>' :
'Cap=' + (n.capsEnabled||'?')}
</div>
</div>`;
});
}
// Build per-port LLDP stats table
let statsRows = '';
const portNames = { '1': 'EVEN_VLAN_UPLINK', '2': 'ODD_VLAN_UPLINK',
'3': 'EVEN_VLAN_DOWNLIN', '4': 'ODD_VLAN_DOWNLINK', '5': 'Management' };
for (const [port, s] of Object.entries(stats).sort((a,b) => parseInt(a[0]) - parseInt(b[0]))) {
const tx = parseInt(s.txFrames || '0').toLocaleString();
const rx = parseInt(s.rxFrames || '0').toLocaleString();
const nb = s.neighborsLearned || '0';
const name = (ifaces[port] || {}).ifName || portNames[port] || `Port ${port}`;
const hasActive = !!neighborByPort[port];
const activeMarker = hasActive ? '<span style="color:var(--green)"> (active)</span>' : '';
statsRows += `<tr>
<td>SFP-${port}</td>
<td>${esc(name)}</td>
<td style="text-align:right">${tx}</td>
<td style="text-align:right">${rx}</td>
<td style="text-align:center">${nb}${activeMarker}</td>
</tr>`;
}
document.getElementById('sec-lldp').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-diagram-3"></i> LLDP Topology</div>
<div class="card-body">
${hasNeighbors ? `
<div class="topo-container">
<div class="topo-device local">
<div class="topo-hostname">${esc(localName)}</div>
<div class="topo-model">${esc(localModel)}</div>
<ul class="topo-port-list">${portListHtml}</ul>
</div>
${neighborHtml}
</div>
` : `
<div style="text-align:center;color:#555;padding:1.5rem">
<i class="bi bi-slash-circle" style="font-size:1.5rem"></i>
<div style="margin-top:0.5rem">No active LLDP neighbors detected</div>
</div>
`}
${Object.keys(stats).length ? `
<div style="margin-top:1rem">
<h6 style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem">
<i class="bi bi-bar-chart"></i> Per-Port LLDP Statistics
</h6>
<table class="topo-stats-table">
<thead><tr>
<th>Port</th><th>Interface</th><th style="text-align:right">TX Frames</th>
<th style="text-align:right">RX Frames</th><th style="text-align:center">Neighbors</th>
</tr></thead>
<tbody>${statsRows}</tbody>
</table>
</div>
` : ''}
</div>
</div>`;
}
// ── 7. Coverage Matrix ───────────────────────────────
function renderCoverage() {
// Analyze each section for populated vs empty fields
const sections = [
{ name: 'Device Identity', key: 'device', type: 'scalar' },
{ name: 'Interfaces (IF-MIB)', key: 'interfaces', type: 'table' },
{ name: 'Connectors', key: 'connectors', type: 'table' },
{ name: 'Power Supplies', key: 'power_supplies', type: 'table' },
{ name: 'Temperature Sensors', key: 'temperature_sensors', type: 'table' },
{ name: 'SFP Info', key: 'sfp_info', type: 'table' },
{ name: 'SFP Diagnostics', key: 'sfp_diagnostics', type: 'table' },
{ name: 'SFP Thresholds', key: 'sfp_thresholds', type: 'table' },
{ name: 'Alarm Config', key: 'alarm_config', type: 'table' },
{ name: 'Alarm Status', key: 'alarm_status', type: 'table' },
{ name: 'Alarm General', key: 'alarm_general', type: 'scalar' },
{ name: 'Port Config', key: 'port_config', type: 'table' },
{ name: 'Port Status', key: 'port_status', type: 'table' },
{ name: 'L2 Filters', key: 'l2_filters', type: 'table' },
{ name: 'Policy Lists', key: 'policy_lists', type: 'table' },
{ name: 'Policy Port Bindings', key: 'policy_port_bindings', type: 'table' },
{ name: 'Policy Entries (enabled)', key: 'policy_entries', type: 'table' },
{ name: 'Policy Stats', key: 'policy_stats', type: 'table' },
{ name: 'Regulators', key: 'regulators', type: 'table' },
{ name: 'Regulator Stats', key: 'regulator_stats', type: 'table' },
{ name: 'CoS Profiles', key: 'cos_profiles', type: 'table' },
{ name: 'LLDP Neighbors', key: 'lldp_neighbors', type: 'table' },
{ name: 'LLDP Stats', key: 'lldp_stats', type: 'table' },
];
function analyzeSection(sec) {
const d = DATA[sec.key];
if (!d) return { total: 0, populated: 0, empty: 0 };
let total = 0, populated = 0;
if (sec.type === 'scalar') {
for (const [k,v] of Object.entries(d)) {
total++;
if (isPopulated(v)) populated++;
}
} else {
for (const [idx, row] of Object.entries(d)) {
for (const [k,v] of Object.entries(row)) {
total++;
if (isPopulated(v)) populated++;
}
}
}
return { total, populated, empty: total - populated };
}
let tableRows = '';
for (const sec of sections) {
const a = analyzeSection(sec);
const pct = a.total ? Math.round(a.populated / a.total * 100) : 0;
const barColor = pct > 70 ? 'var(--green)' : pct > 30 ? 'var(--amber)' : 'var(--red)';
tableRows += `<tr>
<td>${esc(sec.name)}</td>
<td class="mono">${a.total}</td>
<td class="mono">${a.populated}</td>
<td class="mono">${a.empty}</td>
<td>
<div class="d-flex align-items-center gap-2">
<div class="cov-bar-track flex-grow-1">
<div class="cov-bar-fill" style="width:${pct}%;background:${barColor}"></div>
</div>
<span class="mono" style="min-width:35px">${pct}%</span>
</div>
</td>
</tr>`;
}
// OID module distribution
const modCounts = DATA._module_oid_counts || {};
const totalOids = Object.values(modCounts).reduce((a,b) => a+b, 0);
let modRows = '';
for (const [mod, count] of Object.entries(modCounts)) {
const pct = totalOids ? Math.round(count/totalOids*100) : 0;
modRows += `<tr>
<td class="mono">${esc(mod)}</td>
<td class="mono">${count.toLocaleString()}</td>
<td>
<div class="d-flex align-items-center gap-2">
<div class="oid-bar-track flex-grow-1">
<div class="oid-bar-fill" style="width:${pct}%"></div>
</div>
<span class="mono" style="min-width:35px">${pct}%</span>
</div>
</td>
</tr>`;
}
// Known gaps
const gaps = [];
// Check SFP DDM
const sfpInfo = DATA.sfp_info || {};
for (const [k,s] of Object.entries(sfpInfo)) {
if (s.present === '1' && s.diagCapable !== '1') {
gaps.push({
type: 'red',
text: `SFP-${k} DDM: Not available via SNMP (diagCapable=false). NID web UI reads SFP I2C bus directly.`
});
}
}
// Check Feed B
const pwr = DATA.power_supplies || {};
for (const [k,p] of Object.entries(pwr)) {
if (p.present === '2') gaps.push({ type: 'warn', text: `${p.name}: Not present` });
}
// Check LLDP completeness
const lldpNbrs = Object.keys(DATA.lldp_neighbors || {});
if (lldpNbrs.length === 0) {
gaps.push({ type: 'warn', text: 'LLDP: No neighbor data available' });
}
let gapHtml = gaps.map(g =>
`<div class="gap-callout ${g.type==='warn'?'warn':''}">${esc(g.text)}</div>`
).join('');
document.getElementById('sec-coverage').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-bar-chart"></i> SNMP Data Coverage</div>
<div class="card-body">
<h6 style="font-size:0.85rem;margin-bottom:0.5rem">Field Population by Section</h6>
<p style="font-size:0.75rem;color:var(--text-muted)">Shows how many fields contain real (non-zero, non-empty) data vs. blank/zero values. This tells you what can be meaningfully polled.</p>
<div class="tbl-scroll" style="max-height:none">
<table class="table table-dark-custom table-sm">
<thead><tr>
<th>Section</th><th>Total Fields</th><th>Populated</th><th>Empty/Zero</th><th>Coverage</th>
</tr></thead>
<tbody>${tableRows}</tbody>
</table>
</div>
${gapHtml ? `<h6 style="font-size:0.85rem;margin-top:1rem;margin-bottom:0.5rem">Known Data Gaps</h6>${gapHtml}` : ''}
<h6 style="font-size:0.85rem;margin-top:1rem;margin-bottom:0.5rem">OID Distribution by MIB Module</h6>
<p style="font-size:0.75rem;color:var(--text-muted)">${totalOids.toLocaleString()} total OIDs across ${Object.keys(modCounts).length} modules</p>
<div class="tbl-scroll" style="max-height:none">
<table class="table table-dark-custom table-sm">
<thead><tr><th>Module</th><th>OIDs</th><th>Distribution</th></tr></thead>
<tbody>${modRows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── 8. Port Config vs Status ─────────────────────────
function renderPortCmp() {
const portCfg = DATA.port_config || {};
const portStatus = DATA.port_status || {};
const speedMap = {'0': 'Auto/--', '10': '10 Mbps', '100': '100 Mbps', '1000': '1 Gbps', '10000': '10 Gbps'};
const duplexMap = {'0': '--', '1': 'Half', '2': 'Full'};
const boolMap = {'1': 'Enabled', '2': 'Disabled'};
const linkMap = {'0': 'Down', '1': 'Up'};
const statusSpeedMap = {'0': '--', '1': '10M', '2': '1G', '3': '10G'};
let rows = '';
const keys = Object.keys(portCfg).sort((a,b) => parseInt(a)-parseInt(b));
for (const idx of keys) {
const cfg = portCfg[idx];
const st = portStatus[idx] || {};
const cfgSpeed = speedMap[cfg.speed] || cfg.speed;
const stSpeed = statusSpeedMap[st.speed] || st.speed || '--';
const cfgDuplex = duplexMap[cfg.duplex] || cfg.duplex;
const stDuplex = duplexMap[st.duplex] || st.duplex || '--';
const linkSt = linkMap[st.linkStatus] || st.linkStatus || '--';
const linkCls = st.linkStatus === '1' ? 'status-up' : 'status-down';
// Mismatch detection: if configured != operational (simplified)
const speedMismatch = cfg.speed && st.speed && cfg.speed !== '0' && st.speed !== '0' && cfgSpeed !== stSpeed ? 'mismatch' : '';
rows += `<tr>
<td class="mono">${idx}</td>
<td><strong>${esc(cfg.name)}</strong></td>
<td class="${linkCls}">${linkSt}</td>
<td>${boolMap[cfg.autoNego] || cfg.autoNego || '--'}</td>
<td class="mono ${speedMismatch}">${cfgSpeed}</td>
<td class="mono ${speedMismatch}">${stSpeed}</td>
<td class="mono">${cfgDuplex}</td>
<td class="mono">${stDuplex}</td>
<td class="mono">${cfg.mtu}</td>
<td class="mono">${boolMap[cfg.state] || cfg.state || '--'}</td>
<td class="mono">${boolMap[cfg.pauseMode] || cfg.pauseMode || '--'}</td>
<td class="mono">${boolMap[cfg.forceTxOn] || cfg.forceTxOn || '--'}</td>
</tr>`;
}
document.getElementById('sec-portcmp').innerHTML = `
<div class="card-dark">
<div class="card-header"><i class="bi bi-sliders"></i> Port Configuration vs. Operational Status</div>
<div class="card-body" style="padding:0">
<div style="padding:0.4rem 1rem;font-size:0.75rem;color:var(--text-muted)">
Configured values from ACD-PORT-MIB alongside operational status. <span style="background:rgba(253,126,20,0.12);padding:1px 4px;border-radius:2px">Orange highlight</span> = config/status mismatch.
</div>
<div class="tbl-scroll">
<table class="table table-dark-custom table-sm table-hover">
<thead><tr>
<th>#</th><th>Name</th><th>Link</th><th>AutoNeg</th>
<th>Cfg Speed</th><th>Oper Speed</th>
<th>Cfg Duplex</th><th>Oper Duplex</th>
<th>MTU</th><th>State</th><th>Pause</th><th>Force TX</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
</div>`;
}
// ── Render all sections ──────────────────────────────
renderHeader();
renderPanel();
renderInterfaces();
renderSfp();
renderAlarms();
renderPolicies();
renderFilters();
renderRegulators();
renderLldp();
renderCoverage();
renderPortCmp();
// Auto-select SFP-1
selectSfp(1);
</script>
</body>
</html>