Compare commits
No commits in common. "edge-case-metronode" and "main" have entirely different histories.
edge-case-
...
main
@ -22,10 +22,5 @@ SNMP_COMMUNITY=public
|
||||
# "targeted" = walk only subtrees used by the viewer (faster)
|
||||
SNMP_WALK_MODE=targeted
|
||||
|
||||
# ── Policy data ──
|
||||
# ACD-POLICY-MIB is ~73% of all OIDs. Set to "false" to skip it for faster walks.
|
||||
# The Traffic Policies card will be empty when disabled.
|
||||
SNMP_WALK_POLICIES=true
|
||||
|
||||
# ── Server ──
|
||||
SERVER_PORT=5525
|
||||
|
||||
@ -60,8 +60,6 @@ def build_html(data: dict) -> str:
|
||||
<title>NID Viewer — {page_title}</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">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
:root {{
|
||||
--bg-dark: #0f1117;
|
||||
@ -163,7 +161,7 @@ body {{
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
gap: 1.2rem;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}}
|
||||
@ -212,8 +210,8 @@ body {{
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
min-height: 1.8em;
|
||||
}}
|
||||
.mgmt-port {{
|
||||
width: 40px;
|
||||
@ -351,20 +349,6 @@ body {{
|
||||
font-size: 0.85rem;
|
||||
}}
|
||||
.walk-select:focus {{ outline: none; border-color: var(--accent); }}
|
||||
.walk-toggle {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}}
|
||||
.walk-toggle input[type="checkbox"] {{
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}}
|
||||
.walk-btn {{
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
@ -475,21 +459,14 @@ body {{
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}}
|
||||
/* ── LLDP vertical columns ── */
|
||||
.lldp-columns {{
|
||||
.lldp-row {{
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}}
|
||||
.lldp-col {{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
}}
|
||||
.lldp-col.idle {{ opacity: 0.4; }}
|
||||
.lldp-col-header {{
|
||||
.lldp-row:last-child {{ margin-bottom: 0; }}
|
||||
.lldp-local-slot {{
|
||||
width: 56px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
@ -502,178 +479,147 @@ body {{
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}}
|
||||
.lldp-col-header.present-link {{
|
||||
.lldp-local-slot.present-link {{
|
||||
background: rgba(34,197,94,0.15);
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
}}
|
||||
.lldp-col-header.present-nolink {{
|
||||
.lldp-local-slot.present-nolink {{
|
||||
background: rgba(245,158,11,0.15);
|
||||
border-color: var(--amber);
|
||||
color: var(--amber);
|
||||
}}
|
||||
.lldp-col-header.empty {{
|
||||
.lldp-local-slot.empty {{
|
||||
background: #1a1d24;
|
||||
border-color: #2d3340;
|
||||
color: #555;
|
||||
}}
|
||||
.lldp-col-header .slot-label {{
|
||||
.lldp-local-slot .slot-label {{
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
}}
|
||||
.lldp-col-port-label {{
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin: 0.3rem 0 0.15rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
.lldp-connector {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
max-width: 200px;
|
||||
flex: 1;
|
||||
padding: 0 0.4rem;
|
||||
}}
|
||||
.lldp-col-line {{
|
||||
width: 3px;
|
||||
height: 36px;
|
||||
.lldp-connector .link-line {{
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
margin: 0.15rem 0;
|
||||
position: relative;
|
||||
}}
|
||||
.lldp-col-line.up {{
|
||||
.lldp-connector .link-line.up {{
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 8px rgba(34,197,94,0.3);
|
||||
}}
|
||||
.lldp-col-line.down {{
|
||||
.lldp-connector .link-line.down {{
|
||||
background: var(--amber);
|
||||
box-shadow: 0 0 8px rgba(245,158,11,0.3);
|
||||
}}
|
||||
.lldp-col-line.idle {{
|
||||
background: #2d3340;
|
||||
height: 24px;
|
||||
opacity: 0.5;
|
||||
}}
|
||||
.lldp-col-line::before,
|
||||
.lldp-col-line::after {{
|
||||
.lldp-connector .link-line::before,
|
||||
.lldp-connector .link-line::after {{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
transform: translateY(-50%);
|
||||
}}
|
||||
.lldp-col-line.up::before,
|
||||
.lldp-col-line.up::after {{ background: var(--green); }}
|
||||
.lldp-col-line.down::before,
|
||||
.lldp-col-line.down::after {{ background: var(--amber); }}
|
||||
.lldp-col-line::before {{ top: -4px; }}
|
||||
.lldp-col-line::after {{ bottom: -4px; }}
|
||||
.lldp-col-line.idle::before,
|
||||
.lldp-col-line.idle::after {{ display: none; }}
|
||||
.lldp-col-remote {{
|
||||
.lldp-connector .link-line.up::before,
|
||||
.lldp-connector .link-line.up::after {{ background: var(--green); }}
|
||||
.lldp-connector .link-line.down::before,
|
||||
.lldp-connector .link-line.down::after {{ background: var(--amber); }}
|
||||
.lldp-connector .link-line::before {{ left: -4px; }}
|
||||
.lldp-connector .link-line::after {{ right: -4px; }}
|
||||
.lldp-connector .link-port-label {{
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin: 0.2rem 0;
|
||||
}}
|
||||
.lldp-remote {{
|
||||
background: #1e2128;
|
||||
border: 1px solid #3a3f4b;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
width: 100%;
|
||||
margin-top: 0.15rem;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 320px;
|
||||
flex-shrink: 0;
|
||||
}}
|
||||
.lldp-col-remote .remote-hostname {{
|
||||
.lldp-remote .remote-hostname {{
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
word-break: break-all;
|
||||
}}
|
||||
.lldp-col-remote .remote-model {{
|
||||
.lldp-remote .remote-model {{
|
||||
font-size: 0.72rem;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 0.3rem;
|
||||
}}
|
||||
.lldp-col-remote .remote-detail {{
|
||||
.lldp-remote .remote-detail {{
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0.1rem 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}}
|
||||
.lldp-col-remote .remote-detail .rlabel {{
|
||||
.lldp-remote .remote-detail .rlabel {{
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
}}
|
||||
.lldp-col-remote .remote-mgmt {{
|
||||
.lldp-remote .remote-mgmt {{
|
||||
font-size: 0.75rem;
|
||||
color: var(--green);
|
||||
font-weight: 600;
|
||||
margin-top: 0.3rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}}
|
||||
.lldp-col-idle-label {{
|
||||
.lldp-row.idle {{
|
||||
opacity: 0.4;
|
||||
}}
|
||||
.lldp-row.idle .lldp-connector {{
|
||||
min-width: 40px;
|
||||
}}
|
||||
.lldp-idle-label {{
|
||||
font-size: 0.7rem;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
margin-top: 0.3rem;
|
||||
}}
|
||||
.lldp-col-divider {{
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
border-left: 1px dashed #3a3f4b;
|
||||
margin: 0 0.25rem;
|
||||
}}
|
||||
/* ── Location Map ── */
|
||||
#sec-map .card-dark {{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
#sec-map .card-body {{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
#nid-map {{
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-dark);
|
||||
}}
|
||||
.leaflet-container {{
|
||||
background: var(--bg-dark) !important;
|
||||
}}
|
||||
.map-coords {{
|
||||
.topo-stats-table {{
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.75rem;
|
||||
}}
|
||||
.topo-stats-table th {{
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.4rem;
|
||||
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;
|
||||
}}
|
||||
#sec-header .card-dark {{
|
||||
height: 100%;
|
||||
}}
|
||||
#sec-sfp > .card-dark,
|
||||
#sec-alarms > .card-dark,
|
||||
#sec-filters > .card-dark,
|
||||
#sec-regulators > .card-dark {{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
#sec-alarms > .card-dark > .card-body,
|
||||
#sec-regulators > .card-dark > .card-body {{
|
||||
flex: 1;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid py-3" style="max-width:1800px">
|
||||
<div class="container-fluid py-3" style="max-width:1400px">
|
||||
|
||||
<!-- ═══════════════ 0. WALK CONTROL ═══════════════ -->
|
||||
<div id="sec-walk"></div>
|
||||
|
||||
<!-- ═══════════════ 1. DEVICE HEADER + MAP ═══════════════ -->
|
||||
<div style="display:flex;gap:1rem;align-items:stretch">
|
||||
<div id="sec-header" style="flex:1;min-width:0"></div>
|
||||
<div id="sec-map" style="flex:1;min-width:0"></div>
|
||||
</div>
|
||||
<!-- ═══════════════ 1. DEVICE HEADER ═══════════════ -->
|
||||
<div id="sec-header"></div>
|
||||
|
||||
<!-- ═══════════════ 2. FRONT PANEL ═══════════════ -->
|
||||
<div id="sec-panel"></div>
|
||||
@ -684,20 +630,20 @@ body {{
|
||||
<!-- ═══════════════ 4. INTERFACES TABLE ═══════════════ -->
|
||||
<div id="sec-interfaces"></div>
|
||||
|
||||
<!-- ═══════════════ 5–6. SFP + ALARMS (side by side) ═══════════════ -->
|
||||
<div style="display:flex;gap:1rem;align-items:stretch">
|
||||
<div id="sec-sfp" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||
<div id="sec-alarms" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||
</div>
|
||||
<!-- ═══════════════ 5. SFP CARDS ═══════════════ -->
|
||||
<div id="sec-sfp"></div>
|
||||
|
||||
<!-- ═══════════════ 6. ALARMS ═══════════════ -->
|
||||
<div id="sec-alarms"></div>
|
||||
|
||||
<!-- ═══════════════ 7. TRAFFIC POLICIES ═══════════════ -->
|
||||
<div id="sec-policies"></div>
|
||||
|
||||
<!-- ═══════════════ 8–9. L2 FILTERS + REGULATORS (side by side) ═══════════════ -->
|
||||
<div style="display:flex;gap:1rem;align-items:stretch">
|
||||
<div id="sec-filters" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||
<div id="sec-regulators" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||
</div>
|
||||
<!-- ═══════════════ 8. L2 FILTERS ═══════════════ -->
|
||||
<div id="sec-filters"></div>
|
||||
|
||||
<!-- ═══════════════ 9. REGULATORS ═══════════════ -->
|
||||
<div id="sec-regulators"></div>
|
||||
|
||||
<!-- ═══════════════ 10. COVERAGE MATRIX ═══════════════ -->
|
||||
<div id="sec-coverage"></div>
|
||||
@ -734,26 +680,6 @@ function formatUptime(sec) {{
|
||||
p.push(s+'s');
|
||||
return p.join(' ');
|
||||
}}
|
||||
// SNMP TruthValue: some agents return '1'/'2', others 'true'/'false'
|
||||
function isTrue(v) {{ return v === '1' || v === 'true' || v === true; }}
|
||||
|
||||
// Derive data ports and management port from connectors dynamically
|
||||
function getPortLists() {{
|
||||
const connectors = DATA.connectors || {{}};
|
||||
const keys = Object.keys(connectors).sort((a,b) => parseInt(a) - parseInt(b));
|
||||
const dataPorts = [];
|
||||
let mgmtPort = null;
|
||||
for (const k of keys) {{
|
||||
const c = connectors[k];
|
||||
if (c.name && c.name.toLowerCase() === 'management') {{
|
||||
mgmtPort = k;
|
||||
}} else {{
|
||||
dataPorts.push(k);
|
||||
}}
|
||||
}}
|
||||
return {{ dataPorts, mgmtPort }};
|
||||
}}
|
||||
|
||||
function sevLabel(s) {{
|
||||
return {{'0':'INFO','1':'MINOR','2':'MAJOR','3':'CRITICAL'}}[s] || s;
|
||||
}}
|
||||
@ -849,10 +775,6 @@ function renderWalkControl() {{
|
||||
<option value="targeted">Targeted</option>
|
||||
<option value="full">Full</option>
|
||||
</select>
|
||||
<label class="walk-toggle" title="ACD-POLICY-MIB is ~73% of all OIDs — disable for faster walks">
|
||||
<input type="checkbox" id="walk-policies" checked>
|
||||
Policies
|
||||
</label>
|
||||
<button class="walk-btn" id="walk-btn" onclick="startWalk()">
|
||||
<i class="bi bi-play-fill"></i> Walk
|
||||
</button>
|
||||
@ -873,7 +795,6 @@ let walkEventSource = null;
|
||||
function startWalk() {{
|
||||
const target = document.getElementById('walk-target').value.trim();
|
||||
const mode = document.getElementById('walk-mode').value;
|
||||
const policies = document.getElementById('walk-policies').checked;
|
||||
const btn = document.getElementById('walk-btn');
|
||||
|
||||
if (!target) {{
|
||||
@ -882,78 +803,55 @@ function startWalk() {{
|
||||
}}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Pinging...';
|
||||
updateWalkStatus('running', 'Checking reachability...');
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Walking...';
|
||||
updateWalkStatus('running', 'Starting walk...');
|
||||
|
||||
// Step 1: Ping check
|
||||
fetch('/api/ping', {{
|
||||
// Close any previous SSE connection
|
||||
if (walkEventSource) walkEventSource.close();
|
||||
|
||||
fetch('/api/walk', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{ target }})
|
||||
body: JSON.stringify({{ target, mode }})
|
||||
}})
|
||||
.then(r => r.json())
|
||||
.then(ping => {{
|
||||
if (!ping.reachable) {{
|
||||
updateWalkStatus('error', 'NID is DOWN. Verify Local Power and Router Interface Status.');
|
||||
.then(resp => {{
|
||||
if (resp.error) {{
|
||||
updateWalkStatus('error', resp.error);
|
||||
resetWalkBtn();
|
||||
return;
|
||||
}}
|
||||
updateWalkStatus('complete', 'NID Management is UP');
|
||||
// Open SSE for status updates
|
||||
walkEventSource = new EventSource('/api/status');
|
||||
walkEventSource.onmessage = (e) => {{
|
||||
const s = JSON.parse(e.data);
|
||||
const pct = s.progress || 0;
|
||||
document.getElementById('walk-progress-fill').style.width = pct + '%';
|
||||
|
||||
// Step 2: Proceed with walk after brief pause to show UP status
|
||||
setTimeout(() => {{
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Walking...';
|
||||
updateWalkStatus('running', 'Starting walk...');
|
||||
|
||||
// Close any previous SSE connection
|
||||
if (walkEventSource) walkEventSource.close();
|
||||
|
||||
fetch('/api/walk', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{ target, mode, policies }})
|
||||
}})
|
||||
.then(r => r.json())
|
||||
.then(resp => {{
|
||||
if (resp.error) {{
|
||||
updateWalkStatus('error', resp.error);
|
||||
resetWalkBtn();
|
||||
return;
|
||||
}}
|
||||
// Open SSE for status updates
|
||||
walkEventSource = new EventSource('/api/status');
|
||||
walkEventSource.onmessage = (e) => {{
|
||||
const s = JSON.parse(e.data);
|
||||
const pct = s.progress || 0;
|
||||
document.getElementById('walk-progress-fill').style.width = pct + '%';
|
||||
|
||||
if (s.state === 'complete') {{
|
||||
updateWalkStatus('complete', s.message);
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
setTimeout(() => window.location.reload(), 800);
|
||||
}} else if (s.state === 'error') {{
|
||||
updateWalkStatus('error', s.message);
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
resetWalkBtn();
|
||||
}} else {{
|
||||
updateWalkStatus('running', s.message);
|
||||
}}
|
||||
}};
|
||||
walkEventSource.onerror = () => {{
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
}};
|
||||
}})
|
||||
.catch(err => {{
|
||||
updateWalkStatus('error', 'Failed to connect: ' + err.message);
|
||||
if (s.state === 'complete') {{
|
||||
updateWalkStatus('complete', s.message);
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
// Reload to pick up fresh data
|
||||
setTimeout(() => window.location.reload(), 800);
|
||||
}} else if (s.state === 'error') {{
|
||||
updateWalkStatus('error', s.message);
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
resetWalkBtn();
|
||||
}});
|
||||
}}, 600);
|
||||
}} else {{
|
||||
updateWalkStatus('running', s.message);
|
||||
}}
|
||||
}};
|
||||
walkEventSource.onerror = () => {{
|
||||
walkEventSource.close();
|
||||
walkEventSource = null;
|
||||
// SSE closed — could be server-side close after complete/error
|
||||
// Check if we already handled it; if not, it was an unexpected close
|
||||
}};
|
||||
}})
|
||||
.catch(err => {{
|
||||
updateWalkStatus('error', 'Ping check failed: ' + err.message);
|
||||
updateWalkStatus('error', 'Failed to connect: ' + err.message);
|
||||
resetWalkBtn();
|
||||
}});
|
||||
}}
|
||||
@ -993,7 +891,7 @@ function renderHeader() {{
|
||||
const cfgByNum = {{}};
|
||||
for (const [k,v] of Object.entries(alarmCfg)) cfgByNum[v.number] = v;
|
||||
for (const [k,a] of Object.entries(alarmStatus)) {{
|
||||
if (isTrue(a.active)) {{
|
||||
if (a.active === '1') {{
|
||||
activeCount++;
|
||||
const cfg = cfgByNum[a.number];
|
||||
if (cfg) sevCounts[cfg.severity] = (sevCounts[cfg.severity]||0) + 1;
|
||||
@ -1085,40 +983,6 @@ function renderHeader() {{
|
||||
</div>`;
|
||||
}}
|
||||
|
||||
// ── 1b. Location Map ────────────────────────────────
|
||||
function renderMap() {{
|
||||
const d = DATA.device || {{}};
|
||||
const loc = (d.sysLocation || '').trim();
|
||||
const m = loc.match(/^(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)$/);
|
||||
if (!m) return; // no valid coordinates — skip map
|
||||
|
||||
const lat = parseFloat(m[1]);
|
||||
const lon = parseFloat(m[2]);
|
||||
const hostname = d.sysName || d.identifier || 'NID';
|
||||
|
||||
document.getElementById('sec-map').innerHTML = `
|
||||
<div class="card-dark">
|
||||
<div class="card-header collapsible"><i class="bi bi-geo-alt"></i> Location Map<i class="bi bi-chevron-down collapse-chevron"></i></div>
|
||||
<div class="card-body">
|
||||
<div id="nid-map"></div>
|
||||
<div class="map-coords"><i class="bi bi-crosshair"></i> ${{lat.toFixed(6)}}, ${{lon.toFixed(6)}}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const map = L.map('nid-map').setView([lat, lon], 15);
|
||||
L.tileLayer('https://{{s}}.basemaps.cartocdn.com/dark_all/{{z}}/{{x}}/{{y}}{{r}}.png', {{
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 19
|
||||
}}).addTo(map);
|
||||
L.marker([lat, lon]).addTo(map)
|
||||
.bindPopup(`<b>${{esc(hostname)}}</b><br>${{lat.toFixed(6)}}, ${{lon.toFixed(6)}}`)
|
||||
.openPopup();
|
||||
|
||||
// Leaflet needs a resize nudge when rendered in a hidden/collapsed container
|
||||
setTimeout(() => map.invalidateSize(), 200);
|
||||
}}
|
||||
|
||||
// ── 2. Front Panel ───────────────────────────────────
|
||||
function renderPanel() {{
|
||||
const connectors = DATA.connectors || {{}};
|
||||
@ -1140,7 +1004,7 @@ function renderPanel() {{
|
||||
const iface = ifaces[connIdx];
|
||||
if (connType(connIdx) === 'sfp') {{
|
||||
const sfp = sfpInfo[connIdx];
|
||||
const present = sfp && isTrue(sfp.present);
|
||||
const present = sfp && sfp.present === '1';
|
||||
if (!present) return 'empty';
|
||||
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
||||
return 'present-nolink';
|
||||
@ -1155,7 +1019,7 @@ function renderPanel() {{
|
||||
function sfpLabel(connIdx) {{
|
||||
const sfp = sfpInfo[connIdx];
|
||||
if (!sfp) return '';
|
||||
if (!isTrue(sfp.present)) return 'EMPTY';
|
||||
if (sfp.present !== '1') return 'EMPTY';
|
||||
const pn = sfp.vendorPn || '';
|
||||
if (pn.length > 8) return pn.substring(0,8);
|
||||
return pn || sfp.vendor || '';
|
||||
@ -1170,15 +1034,15 @@ function renderPanel() {{
|
||||
return {{ label, alias }};
|
||||
}}
|
||||
|
||||
// Build port slots dynamically from connectors
|
||||
const {{ dataPorts, mgmtPort }} = getPortLists();
|
||||
// Build port slots dynamically from connectors 1-4
|
||||
let slots = '';
|
||||
for (const idx of dataPorts) {{
|
||||
for (let i = 1; i <= 4; i++) {{
|
||||
const idx = String(i);
|
||||
const conn = connectors[idx] || {{}};
|
||||
const isSfp = connType(idx) === 'sfp';
|
||||
const state = slotState(idx);
|
||||
const pi = portInfo(idx);
|
||||
const slotName = conn.name || (isSfp ? `SFP-${{idx}}` : `RJ45-${{idx}}`);
|
||||
const slotName = conn.name || (isSfp ? `SFP-${{i}}` : `RJ45-${{i}}`);
|
||||
|
||||
let icon, detail;
|
||||
if (isSfp) {{
|
||||
@ -1198,7 +1062,7 @@ function renderPanel() {{
|
||||
? `<div class="port-label" title="${{esc(pi.label + (pi.alias ? ' / ' + pi.alias : ''))}}">${{labelParts.join('<br>')}}</div>`
|
||||
: '';
|
||||
slots += `<div class="sfp-slot-group">
|
||||
<div class="sfp-slot ${{state}}" data-sfp="${{idx}}" onclick="selectSfp(${{idx}})">
|
||||
<div class="sfp-slot ${{state}}" data-sfp="${{i}}" onclick="selectSfp(${{i}})">
|
||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
||||
<span style="font-size:0.5rem;max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${{esc(detail)}}</span>
|
||||
</div>
|
||||
@ -1207,7 +1071,7 @@ function renderPanel() {{
|
||||
}}
|
||||
|
||||
// Management port
|
||||
const mgmtIf = mgmtPort ? ifaces[mgmtPort] : null;
|
||||
const mgmtIf = ifaces['5'];
|
||||
const mgmtUp = mgmtIf && isUp(mgmtIf.ifOperStatus);
|
||||
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>
|
||||
@ -1217,7 +1081,7 @@ function renderPanel() {{
|
||||
// Power feeds
|
||||
let pwrHtml = '<div class="pwr-block">';
|
||||
for (const [k,p] of Object.entries(pwr)) {{
|
||||
const ok = isTrue(p.present);
|
||||
const ok = p.present === '1';
|
||||
pwrHtml += `<div><span class="pwr-led ${{ok?'ok':'fail'}}"></span>${{esc(p.name)}} ${{ok?'OK':'ABSENT'}}</div>`;
|
||||
}}
|
||||
pwrHtml += '</div>';
|
||||
@ -1331,27 +1195,27 @@ function renderSfp() {{
|
||||
const sfpDiag = DATA.sfp_diagnostics || {{}};
|
||||
const sfpThresh = DATA.sfp_thresholds || {{}};
|
||||
|
||||
const {{ dataPorts }} = getPortLists();
|
||||
let cards = '';
|
||||
for (const si of dataPorts) {{
|
||||
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 || !isTrue(info.present)) {{
|
||||
cards += `<div class="sfp-detail" id="sfp-detail-${{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>${{esc(conn.name || 'Port ' + si)}} — Not Present</div>
|
||||
<div>SFP-${{i}} — Not Present</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
continue;
|
||||
}}
|
||||
|
||||
const ddm = isTrue(info.diagCapable);
|
||||
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>';
|
||||
@ -1385,10 +1249,10 @@ function renderSfp() {{
|
||||
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 ${{si===dataPorts[0]?'active':''}}" id="sfp-detail-${{si}}">
|
||||
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> ${{esc(conn.name || 'Port ' + si)}}: ${{esc(info.vendor)}} ${{esc(info.vendorPn)}}
|
||||
<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">
|
||||
@ -1408,8 +1272,8 @@ function renderSfp() {{
|
||||
<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>${{isTrue(info.internalCal) ? 'Yes' : 'No'}}</strong></div>
|
||||
<div>Alarm Capable: <strong>${{isTrue(info.alarmCapable) ? '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>
|
||||
@ -1429,7 +1293,7 @@ function renderSfp() {{
|
||||
<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:
|
||||
${{dataPorts.map(k => `<button class="btn btn-sm btn-outline-secondary ms-1" onclick="selectSfp(${{k}})">${{esc((connectors[k]||{{}}).name || 'Port '+k)}}</button>`).join('')}}
|
||||
${{[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>
|
||||
@ -1450,7 +1314,7 @@ function renderAlarms() {{
|
||||
// Collect active alarms (when status table available)
|
||||
const active = [];
|
||||
for (const [k,a] of Object.entries(alarmStatus)) {{
|
||||
if (isTrue(a.active)) {{
|
||||
if (a.active === '1') {{
|
||||
const cfg = cfgByNum[a.number] || {{}};
|
||||
active.push({{ ...a, ...cfg, _statusId: k }});
|
||||
}}
|
||||
@ -1492,7 +1356,7 @@ function renderAlarms() {{
|
||||
<td>${{esc(c.description)}}</td>`;
|
||||
if (hasSeverity) rows += `
|
||||
<td><span class="badge ${{sevClass(c.severity)}}">${{sevLabel(c.severity)}}</span></td>
|
||||
<td>${{isTrue(c.enabled) ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--text-muted)">No</span>'}}</td>`;
|
||||
<td>${{c.enabled === '1' ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--text-muted)">No</span>'}}</td>`;
|
||||
if (hasCondition) rows += `
|
||||
<td class="mono">${{esc(c.conditionType)}}</td>
|
||||
<td class="mono">${{esc(c.amoType)}}</td>`;
|
||||
@ -1639,12 +1503,12 @@ function renderFilters() {{
|
||||
let rows = '';
|
||||
for (const [id, f] of Object.entries(filters)) {{
|
||||
const conditions = [];
|
||||
if (isTrue(f.macDstEn)) conditions.push('MAC Dst: ' + esc(f.macDst));
|
||||
if (isTrue(f.macSrcEn)) conditions.push('MAC Src: ' + esc(f.macSrc));
|
||||
if (isTrue(f.etypeEn)) conditions.push('EType: ' + esc(f.etype));
|
||||
if (isTrue(f.vlan1IdEn)) conditions.push('VLAN1: ' + esc(f.vlan1Id));
|
||||
if (isTrue(f.vlan2IdEn)) conditions.push('VLAN2: ' + esc(f.vlan2Id));
|
||||
if (isTrue(f.vlan1PriorEn)) conditions.push('PCP1: ' + esc(f.vlan1Prior));
|
||||
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>
|
||||
@ -1788,6 +1652,7 @@ function parseRemotePlatform(sysDesc, sysName) {{
|
||||
|
||||
function renderLldp() {{
|
||||
const neighbors = DATA.lldp_neighbors || {{}};
|
||||
const stats = DATA.lldp_stats || {{}};
|
||||
const ifaces = DATA.interfaces || {{}};
|
||||
const sfpInfo = DATA.sfp_info || {{}};
|
||||
const device = DATA.device || {{}};
|
||||
@ -1799,13 +1664,14 @@ function renderLldp() {{
|
||||
const localName = device.identifier || device.sysName || 'NID';
|
||||
const localModel = device.commercialName || device.sysDescr || 'NID';
|
||||
|
||||
// Determine slot state — connector-type aware (same logic as renderPanel)
|
||||
function slotState(portIdx) {{
|
||||
const conn = connectors[portIdx];
|
||||
const isSfp = conn && conn.type === '14';
|
||||
const iface = ifaces[portIdx];
|
||||
if (isSfp) {{
|
||||
const sfp = sfpInfo[portIdx];
|
||||
const present = sfp && isTrue(sfp.present);
|
||||
const present = sfp && sfp.present === '1';
|
||||
if (!present) return 'empty';
|
||||
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
||||
return 'present-nolink';
|
||||
@ -1816,99 +1682,137 @@ function renderLldp() {{
|
||||
}}
|
||||
}}
|
||||
|
||||
function buildNeighborCard(nbr, cssClass) {{
|
||||
const platform = parseRemotePlatform(nbr.remSysDesc, nbr.remSysName);
|
||||
const shortName = (nbr.remSysName || '').split('.')[0] || 'Unknown';
|
||||
const modelLine = platform.vendor ? `${{platform.vendor}} ${{platform.model}}` : platform.model;
|
||||
return `<div class="${{cssClass}}">
|
||||
<div class="remote-hostname">${{esc(shortName)}}</div>
|
||||
<div class="remote-model">${{esc(modelLine)}}</div>
|
||||
${{platform.firmware ? `<div class="remote-detail"><span class="rlabel">FW</span> ${{esc(platform.firmware)}}</div>` : ''}}
|
||||
<div class="remote-detail"><span class="rlabel">Port</span> ${{esc(nbr.remPortId || '?')}}</div>
|
||||
${{nbr.remPortDesc ? `<div class="remote-detail"><span class="rlabel">Desc</span> ${{esc(nbr.remPortDesc)}}</div>` : ''}}
|
||||
<div class="remote-detail"><span class="rlabel">MAC</span> ${{esc(nbr.chassisId || '?')}}</div>
|
||||
${{nbr.mgmtIPv4 ? `<div class="remote-mgmt"><i class="bi bi-globe2"></i> ${{esc(nbr.mgmtIPv4)}}</div>` : ''}}
|
||||
${{nbr.mgmtIPv6 ? `<div class="remote-detail"><span class="rlabel">IPv6</span> <span style="font-size:0.65rem">${{esc(nbr.mgmtIPv6)}}</span></div>` : ''}}
|
||||
<div class="remote-detail" style="margin-top:0.2rem">
|
||||
<span class="rlabel">Caps</span>
|
||||
${{nbr.capsEnabled === '2' ? '<span style="color:var(--amber)">Bridge</span>' :
|
||||
nbr.capsEnabled === '4' ? '<span style="color:var(--cyan)">Router</span>' :
|
||||
'Cap=' + (nbr.capsEnabled||'?')}}
|
||||
</div>
|
||||
</div>`;
|
||||
}}
|
||||
|
||||
// Build vertical columns for data ports
|
||||
const {{ dataPorts, mgmtPort }} = getPortLists();
|
||||
let colsHtml = '';
|
||||
for (const portKey of dataPorts) {{
|
||||
// Build a row for each network port (1-4)
|
||||
let rowsHtml = '';
|
||||
for (let i = 1; i <= 4; i++) {{
|
||||
const portKey = String(i);
|
||||
const conn = connectors[portKey] || {{}};
|
||||
const isSfp = conn.type === '14';
|
||||
const nbr = neighborByPort[portKey];
|
||||
const state = slotState(portKey);
|
||||
const iface = ifaces[portKey] || {{}};
|
||||
const localUp = isUp(iface.ifOperStatus);
|
||||
const slotName = conn.name || (isSfp ? `SFP-${{portKey}}` : `RJ45-${{portKey}}`);
|
||||
const slotName = conn.name || (isSfp ? `SFP-${{i}}` : `RJ45-${{i}}`);
|
||||
const icon = isSfp
|
||||
? (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>')
|
||||
: '<i class="bi bi-ethernet"></i>';
|
||||
const portLabel = iface.ifName || 'Port ' + portKey;
|
||||
|
||||
if (nbr) {{
|
||||
const platform = parseRemotePlatform(nbr.remSysDesc, nbr.remSysName);
|
||||
const shortName = (nbr.remSysName || '').split('.')[0] || 'Unknown';
|
||||
const modelLine = platform.vendor ? `${{platform.vendor}} ${{platform.model}}` : platform.model;
|
||||
const linkClass = localUp ? 'up' : 'down';
|
||||
colsHtml += `
|
||||
<div class="lldp-col">
|
||||
<div class="lldp-col-header ${{state}}">
|
||||
|
||||
rowsHtml += `
|
||||
<div class="lldp-row">
|
||||
<div class="lldp-local-slot ${{state}}">
|
||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
||||
</div>
|
||||
<div class="lldp-col-port-label">${{esc(portLabel)}}</div>
|
||||
<div class="lldp-col-line ${{linkClass}}"></div>
|
||||
<div class="lldp-col-port-label">${{esc(nbr.remPortId || '?')}}</div>
|
||||
${{buildNeighborCard(nbr, 'lldp-col-remote')}}
|
||||
<div class="lldp-connector">
|
||||
<div class="link-port-label">${{esc(iface.ifName || nbr.localPortName || 'Port ' + i)}}</div>
|
||||
<div class="link-line ${{linkClass}}"></div>
|
||||
<div class="link-port-label">${{esc(nbr.remPortId || '?')}}</div>
|
||||
</div>
|
||||
<div class="lldp-remote">
|
||||
<div class="remote-hostname">${{esc(shortName)}}</div>
|
||||
<div class="remote-model">${{esc(modelLine)}}</div>
|
||||
${{platform.firmware ? `<div class="remote-detail"><span class="rlabel">FW</span> ${{esc(platform.firmware)}}</div>` : ''}}
|
||||
<div class="remote-detail"><span class="rlabel">Port</span> ${{esc(nbr.remPortId || '?')}}</div>
|
||||
${{nbr.remPortDesc ? `<div class="remote-detail"><span class="rlabel">Desc</span> ${{esc(nbr.remPortDesc)}}</div>` : ''}}
|
||||
<div class="remote-detail"><span class="rlabel">MAC</span> ${{esc(nbr.chassisId || '?')}}</div>
|
||||
${{nbr.mgmtIPv4 ? `<div class="remote-mgmt"><i class="bi bi-globe2"></i> ${{esc(nbr.mgmtIPv4)}}</div>` : ''}}
|
||||
${{nbr.mgmtIPv6 ? `<div class="remote-detail"><span class="rlabel">IPv6</span> <span style="font-size:0.65rem">${{esc(nbr.mgmtIPv6)}}</span></div>` : ''}}
|
||||
<div class="remote-detail" style="margin-top:0.2rem">
|
||||
<span class="rlabel">Caps</span>
|
||||
${{nbr.capsEnabled === '2' ? '<span style="color:var(--amber)">Bridge</span>' :
|
||||
nbr.capsEnabled === '4' ? '<span style="color:var(--cyan)">Router</span>' :
|
||||
'Cap=' + (nbr.capsEnabled||'?')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}} else {{
|
||||
colsHtml += `
|
||||
<div class="lldp-col idle">
|
||||
<div class="lldp-col-header ${{state}}">
|
||||
rowsHtml += `
|
||||
<div class="lldp-row idle">
|
||||
<div class="lldp-local-slot ${{state}}">
|
||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
||||
</div>
|
||||
<div class="lldp-col-port-label">${{esc(portLabel)}}</div>
|
||||
<div class="lldp-col-line idle"></div>
|
||||
<div class="lldp-col-idle-label">No LLDP neighbor</div>
|
||||
<div class="lldp-connector">
|
||||
<div class="link-line" style="background:#2d3340;height:2px;opacity:0.5"></div>
|
||||
</div>
|
||||
<div class="lldp-idle-label">No LLDP neighbor</div>
|
||||
</div>`;
|
||||
}}
|
||||
}}
|
||||
|
||||
// MGMT column
|
||||
const mgmtNbr = mgmtPort ? neighborByPort[mgmtPort] : null;
|
||||
// Include MGMT port if it has a neighbor
|
||||
const mgmtNbr = neighborByPort['5'];
|
||||
if (mgmtNbr) {{
|
||||
const mgmtIface = ifaces[mgmtPort] || {{}};
|
||||
const mgmtIface = ifaces['5'] || {{}};
|
||||
const mgmtUp = isUp(mgmtIface.ifOperStatus);
|
||||
colsHtml += `<div class="lldp-col-divider"></div>`;
|
||||
colsHtml += `
|
||||
<div class="lldp-col">
|
||||
<div class="lldp-col-header ${{mgmtUp ? 'present-link' : 'present-nolink'}}">
|
||||
const platform = parseRemotePlatform(mgmtNbr.remSysDesc, mgmtNbr.remSysName);
|
||||
const shortName = (mgmtNbr.remSysName || '').split('.')[0] || 'Unknown';
|
||||
const modelLine = platform.vendor ? `${{platform.vendor}} ${{platform.model}}` : platform.model;
|
||||
rowsHtml += `
|
||||
<div class="lldp-row" style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px dashed #3a3f4b">
|
||||
<div class="lldp-local-slot ${{mgmtUp ? 'present-link' : 'present-nolink'}}" style="width:40px;font-size:0.55rem">
|
||||
<i class="bi bi-ethernet"></i><span class="slot-label">MGMT</span>
|
||||
</div>
|
||||
<div class="lldp-col-port-label">Management</div>
|
||||
<div class="lldp-col-line ${{mgmtUp ? 'up' : 'down'}}"></div>
|
||||
<div class="lldp-col-port-label">${{esc(mgmtNbr.remPortId || '?')}}</div>
|
||||
${{buildNeighborCard(mgmtNbr, 'lldp-col-remote')}}
|
||||
<div class="lldp-connector">
|
||||
<div class="link-port-label">Management</div>
|
||||
<div class="link-line ${{mgmtUp ? 'up' : 'down'}}"></div>
|
||||
<div class="link-port-label">${{esc(mgmtNbr.remPortId || '?')}}</div>
|
||||
</div>
|
||||
<div class="lldp-remote">
|
||||
<div class="remote-hostname">${{esc(shortName)}}</div>
|
||||
<div class="remote-model">${{esc(modelLine)}}</div>
|
||||
<div class="remote-detail"><span class="rlabel">MAC</span> ${{esc(mgmtNbr.chassisId || '?')}}</div>
|
||||
${{mgmtNbr.mgmtIPv4 ? `<div class="remote-mgmt"><i class="bi bi-globe2"></i> ${{esc(mgmtNbr.mgmtIPv4)}}</div>` : ''}}
|
||||
</div>
|
||||
</div>`;
|
||||
}}
|
||||
|
||||
// Build per-port LLDP stats table
|
||||
let statsRows = '';
|
||||
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 || `Port ${{port}}`;
|
||||
const hasActive = !!neighborByPort[port];
|
||||
const activeMarker = hasActive ? '<span style="color:var(--green)"> (active)</span>' : '';
|
||||
statsRows += `<tr>
|
||||
<td>${{parseInt(port) <= 4 ? 'SFP-' + port : 'MGMT'}}</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 collapsible"><i class="bi bi-diagram-3"></i> LLDP Topology<i class="bi bi-chevron-down collapse-chevron"></i></div>
|
||||
<div class="card-body">
|
||||
<div class="lldp-panel">
|
||||
<span class="panel-label">${{esc(localName)}} — ${{esc(localModel)}}</span>
|
||||
<div class="lldp-columns">
|
||||
${{colsHtml}}
|
||||
</div>
|
||||
${{rowsHtml}}
|
||||
</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>`;
|
||||
}}
|
||||
@ -2008,7 +1912,7 @@ function renderCoverage() {{
|
||||
// Check SFP DDM
|
||||
const sfpInfo = DATA.sfp_info || {{}};
|
||||
for (const [k,s] of Object.entries(sfpInfo)) {{
|
||||
if (isTrue(s.present) && !isTrue(s.diagCapable)) {{
|
||||
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.`
|
||||
@ -2127,7 +2031,6 @@ function renderPortCmp() {{
|
||||
// ── Render all sections ──────────────────────────────
|
||||
renderWalkControl();
|
||||
renderHeader();
|
||||
renderMap();
|
||||
renderPanel();
|
||||
renderLldp();
|
||||
renderInterfaces();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB |
@ -41,6 +41,7 @@ TARGETED_OIDS = [
|
||||
(".1.3.6.1.4.1.22420.1.1", "ACD-DESC-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.1", "ACD-ALARM-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.2", "ACD-FILTER-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.3", "ACD-POLICY-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.4", "ACD-SFP-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.6", "ACD-REGULATOR-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.8", "ACD-SMAP-MIB"),
|
||||
@ -78,10 +79,6 @@ SNMP_V3_PRIV_PROTO = ENV.get("SNMP_V3_PRIV_PROTO", "AES")
|
||||
SNMP_V3_PRIV_PASS = ENV.get("SNMP_V3_PRIV_PASS", "")
|
||||
SNMP_V3_SEC_LEVEL = ENV.get("SNMP_V3_SEC_LEVEL", "authPriv")
|
||||
|
||||
# Conditionally include heavy policy MIB (~73% of all OIDs)
|
||||
if ENV.get("SNMP_WALK_POLICIES", "true").lower() == "true":
|
||||
TARGETED_OIDS.append((".1.3.6.1.4.1.22420.2.3", "ACD-POLICY-MIB"))
|
||||
|
||||
# ── Walk state (shared across threads) ───────────────────────────────
|
||||
|
||||
walk_lock = threading.Lock()
|
||||
@ -128,7 +125,7 @@ def build_snmp_auth() -> list:
|
||||
return ["-v", SNMP_VERSION, "-c", SNMP_COMMUNITY]
|
||||
|
||||
|
||||
def run_walk(target: str, mode: str, policies: bool = True):
|
||||
def run_walk(target: str, mode: str):
|
||||
"""Execute the full walk pipeline in a background thread."""
|
||||
global latest_json
|
||||
|
||||
@ -145,11 +142,6 @@ def run_walk(target: str, mode: str, policies: bool = True):
|
||||
auth = build_snmp_auth()
|
||||
t_start = time.time()
|
||||
|
||||
# Build OID list for this walk — optionally exclude heavy policy MIB
|
||||
walk_oids = list(TARGETED_OIDS)
|
||||
if not policies:
|
||||
walk_oids = [(oid, lbl) for oid, lbl in walk_oids if lbl != "ACD-POLICY-MIB"]
|
||||
|
||||
try:
|
||||
# ── Step 1: snmpwalk ──────────────────────────────────────
|
||||
# Use snmpbulkwalk (GETBULK PDUs) when available — much faster
|
||||
@ -165,7 +157,7 @@ def run_walk(target: str, mode: str, policies: bool = True):
|
||||
walk_file.write_text(result.stdout)
|
||||
else:
|
||||
# Walk subtrees in parallel for speed
|
||||
total = len(walk_oids)
|
||||
total = len(TARGETED_OIDS)
|
||||
completed = [0] # mutable counter for progress
|
||||
results_map = {}
|
||||
|
||||
@ -183,7 +175,7 @@ def run_walk(target: str, mode: str, policies: bool = True):
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
futures = [
|
||||
pool.submit(walk_subtree, i, oid, label)
|
||||
for i, (oid, label) in enumerate(walk_oids)
|
||||
for i, (oid, label) in enumerate(TARGETED_OIDS)
|
||||
]
|
||||
for fut in as_completed(futures):
|
||||
idx, output = fut.result()
|
||||
@ -317,39 +309,11 @@ class NIDHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == "/api/walk":
|
||||
self._handle_walk()
|
||||
elif self.path == "/api/ping":
|
||||
self._handle_ping()
|
||||
elif self.path == "/api/clear":
|
||||
self._handle_clear()
|
||||
else:
|
||||
self._send(404, "text/plain", b"Not found")
|
||||
|
||||
def _handle_ping(self):
|
||||
"""Ping a target IP to check reachability."""
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length)) if length else {}
|
||||
target = body.get("target", "").strip()
|
||||
|
||||
if not target:
|
||||
self._send_json(400, {"error": "target IP required"})
|
||||
return
|
||||
|
||||
ip_re = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
|
||||
if not ip_re.match(target):
|
||||
self._send_json(400, {"error": f"Invalid IP address: {target}"})
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ping", "-c", "1", "-W", "2", target],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
reachable = result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
reachable = False
|
||||
|
||||
self._send_json(200, {"reachable": reachable, "target": target})
|
||||
|
||||
def _handle_walk(self):
|
||||
"""Start a walk in a background thread."""
|
||||
current, _ = get_status()
|
||||
@ -361,7 +325,6 @@ class NIDHandler(BaseHTTPRequestHandler):
|
||||
body = json.loads(self.rfile.read(length)) if length else {}
|
||||
target = body.get("target", "").strip()
|
||||
mode = body.get("mode", "targeted").strip()
|
||||
policies = body.get("policies", True)
|
||||
|
||||
if not target:
|
||||
self._send_json(400, {"error": "target IP required"})
|
||||
@ -371,7 +334,7 @@ class NIDHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
set_status("walking", message="Starting walk...", progress=1)
|
||||
thread = threading.Thread(target=run_walk, args=(target, mode, policies), daemon=True)
|
||||
thread = threading.Thread(target=run_walk, args=(target, mode), daemon=True)
|
||||
thread.start()
|
||||
self._send_json(200, {"status": "started", "target": target, "mode": mode})
|
||||
|
||||
|
||||
@ -75,8 +75,6 @@ fi
|
||||
|
||||
# ── Define OID subtrees ──────────────────────────────────────────────
|
||||
# Targeted: only what the viewer/parser needs
|
||||
WALK_POLICIES="${SNMP_WALK_POLICIES:-true}"
|
||||
|
||||
TARGETED_OIDS=(
|
||||
.1.3.6.1.2.1.1 # System (sysDescr, sysName, sysUpTime, …)
|
||||
.1.3.6.1.2.1.2 # IF-MIB (interface table)
|
||||
@ -87,16 +85,13 @@ TARGETED_OIDS=(
|
||||
.1.3.6.1.4.1.22420.1.1 # ACD-DESC-MIB (device identity, connectors, sensors)
|
||||
.1.3.6.1.4.1.22420.2.1 # ACD-ALARM-MIB
|
||||
.1.3.6.1.4.1.22420.2.2 # ACD-FILTER-MIB
|
||||
.1.3.6.1.4.1.22420.2.3 # ACD-POLICY-MIB (L2 policies, stats)
|
||||
.1.3.6.1.4.1.22420.2.4 # ACD-SFP-MIB (transceiver info/diag)
|
||||
.1.3.6.1.4.1.22420.2.6 # ACD-REGULATOR-MIB
|
||||
.1.3.6.1.4.1.22420.2.8 # ACD-SMAP-MIB (CoS profiles)
|
||||
.1.3.6.1.4.1.22420.2.9 # ACD-PORT-MIB (port config/status)
|
||||
)
|
||||
|
||||
if [[ "$WALK_POLICIES" == "true" ]]; then
|
||||
TARGETED_OIDS+=(.1.3.6.1.4.1.22420.2.3) # ACD-POLICY-MIB (~73% of all OIDs)
|
||||
fi
|
||||
|
||||
# ── Prepare output paths ─────────────────────────────────────────────
|
||||
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||
SAFE_IP="${TARGET//./-}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user