Compare commits
5 Commits
main
...
edge-case-
| Author | SHA1 | Date | |
|---|---|---|---|
| bcb179e7e4 | |||
|
|
4daf26b778 | ||
|
|
09a2db7373 | ||
| df8c74627b | |||
| d6bf394297 |
@ -22,5 +22,10 @@ SNMP_COMMUNITY=public
|
|||||||
# "targeted" = walk only subtrees used by the viewer (faster)
|
# "targeted" = walk only subtrees used by the viewer (faster)
|
||||||
SNMP_WALK_MODE=targeted
|
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 ──
|
||||||
SERVER_PORT=5525
|
SERVER_PORT=5525
|
||||||
|
|||||||
@ -60,6 +60,8 @@ def build_html(data: dict) -> str:
|
|||||||
<title>NID Viewer — {page_title}</title>
|
<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@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 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>
|
<style>
|
||||||
:root {{
|
:root {{
|
||||||
--bg-dark: #0f1117;
|
--bg-dark: #0f1117;
|
||||||
@ -161,7 +163,7 @@ body {{
|
|||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.2rem;
|
gap: 1.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
}}
|
}}
|
||||||
@ -210,8 +212,8 @@ body {{
|
|||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
min-height: 1.8em;
|
||||||
}}
|
}}
|
||||||
.mgmt-port {{
|
.mgmt-port {{
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@ -349,6 +351,20 @@ body {{
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}}
|
}}
|
||||||
.walk-select:focus {{ outline: none; border-color: var(--accent); }}
|
.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 {{
|
.walk-btn {{
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border: none;
|
border: none;
|
||||||
@ -459,14 +475,21 @@ body {{
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}}
|
}}
|
||||||
.lldp-row {{
|
/* ── LLDP vertical columns ── */
|
||||||
|
.lldp-columns {{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 1rem;
|
||||||
gap: 0;
|
align-items: stretch;
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}}
|
}}
|
||||||
.lldp-row:last-child {{ margin-bottom: 0; }}
|
.lldp-col {{
|
||||||
.lldp-local-slot {{
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}}
|
||||||
|
.lldp-col.idle {{ opacity: 0.4; }}
|
||||||
|
.lldp-col-header {{
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -479,147 +502,178 @@ body {{
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}}
|
}}
|
||||||
.lldp-local-slot.present-link {{
|
.lldp-col-header.present-link {{
|
||||||
background: rgba(34,197,94,0.15);
|
background: rgba(34,197,94,0.15);
|
||||||
border-color: var(--green);
|
border-color: var(--green);
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}}
|
}}
|
||||||
.lldp-local-slot.present-nolink {{
|
.lldp-col-header.present-nolink {{
|
||||||
background: rgba(245,158,11,0.15);
|
background: rgba(245,158,11,0.15);
|
||||||
border-color: var(--amber);
|
border-color: var(--amber);
|
||||||
color: var(--amber);
|
color: var(--amber);
|
||||||
}}
|
}}
|
||||||
.lldp-local-slot.empty {{
|
.lldp-col-header.empty {{
|
||||||
background: #1a1d24;
|
background: #1a1d24;
|
||||||
border-color: #2d3340;
|
border-color: #2d3340;
|
||||||
color: #555;
|
color: #555;
|
||||||
}}
|
}}
|
||||||
.lldp-local-slot .slot-label {{
|
.lldp-col-header .slot-label {{
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}}
|
}}
|
||||||
.lldp-connector {{
|
.lldp-col-port-label {{
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 80px;
|
|
||||||
max-width: 200px;
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 0.4rem;
|
|
||||||
}}
|
|
||||||
.lldp-connector .link-line {{
|
|
||||||
width: 100%;
|
|
||||||
height: 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
position: relative;
|
|
||||||
}}
|
|
||||||
.lldp-connector .link-line.up {{
|
|
||||||
background: var(--green);
|
|
||||||
box-shadow: 0 0 8px rgba(34,197,94,0.3);
|
|
||||||
}}
|
|
||||||
.lldp-connector .link-line.down {{
|
|
||||||
background: var(--amber);
|
|
||||||
box-shadow: 0 0 8px rgba(245,158,11,0.3);
|
|
||||||
}}
|
|
||||||
.lldp-connector .link-line::before,
|
|
||||||
.lldp-connector .link-line::after {{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}}
|
|
||||||
.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;
|
font-size: 0.6rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
margin: 0.2rem 0;
|
margin: 0.3rem 0 0.15rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}}
|
}}
|
||||||
.lldp-remote {{
|
.lldp-col-line {{
|
||||||
|
width: 3px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0.15rem 0;
|
||||||
|
position: relative;
|
||||||
|
}}
|
||||||
|
.lldp-col-line.up {{
|
||||||
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 8px rgba(34,197,94,0.3);
|
||||||
|
}}
|
||||||
|
.lldp-col-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 {{
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translateX(-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 {{
|
||||||
background: #1e2128;
|
background: #1e2128;
|
||||||
border: 1px solid #3a3f4b;
|
border: 1px solid #3a3f4b;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
min-width: 200px;
|
width: 100%;
|
||||||
max-width: 320px;
|
margin-top: 0.15rem;
|
||||||
flex-shrink: 0;
|
flex: 1;
|
||||||
}}
|
}}
|
||||||
.lldp-remote .remote-hostname {{
|
.lldp-col-remote .remote-hostname {{
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}}
|
}}
|
||||||
.lldp-remote .remote-model {{
|
.lldp-col-remote .remote-model {{
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
}}
|
}}
|
||||||
.lldp-remote .remote-detail {{
|
.lldp-col-remote .remote-detail {{
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0.1rem 0;
|
margin: 0.1rem 0;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}}
|
}}
|
||||||
.lldp-remote .remote-detail .rlabel {{
|
.lldp-col-remote .remote-detail .rlabel {{
|
||||||
color: #555;
|
color: #555;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}}
|
}}
|
||||||
.lldp-remote .remote-mgmt {{
|
.lldp-col-remote .remote-mgmt {{
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}}
|
}}
|
||||||
.lldp-row.idle {{
|
.lldp-col-idle-label {{
|
||||||
opacity: 0.4;
|
|
||||||
}}
|
|
||||||
.lldp-row.idle .lldp-connector {{
|
|
||||||
min-width: 40px;
|
|
||||||
}}
|
|
||||||
.lldp-idle-label {{
|
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
margin-top: 0.3rem;
|
||||||
}}
|
}}
|
||||||
.topo-stats-table {{
|
.lldp-col-divider {{
|
||||||
width: 100%;
|
width: 1px;
|
||||||
margin-top: 1rem;
|
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 {{
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}}
|
|
||||||
.topo-stats-table th {{
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 500;
|
margin-top: 0.4rem;
|
||||||
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;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid py-3" style="max-width:1400px">
|
<div class="container-fluid py-3" style="max-width:1800px">
|
||||||
|
|
||||||
<!-- ═══════════════ 0. WALK CONTROL ═══════════════ -->
|
<!-- ═══════════════ 0. WALK CONTROL ═══════════════ -->
|
||||||
<div id="sec-walk"></div>
|
<div id="sec-walk"></div>
|
||||||
|
|
||||||
<!-- ═══════════════ 1. DEVICE HEADER ═══════════════ -->
|
<!-- ═══════════════ 1. DEVICE HEADER + MAP ═══════════════ -->
|
||||||
<div id="sec-header"></div>
|
<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>
|
||||||
|
|
||||||
<!-- ═══════════════ 2. FRONT PANEL ═══════════════ -->
|
<!-- ═══════════════ 2. FRONT PANEL ═══════════════ -->
|
||||||
<div id="sec-panel"></div>
|
<div id="sec-panel"></div>
|
||||||
@ -630,20 +684,20 @@ body {{
|
|||||||
<!-- ═══════════════ 4. INTERFACES TABLE ═══════════════ -->
|
<!-- ═══════════════ 4. INTERFACES TABLE ═══════════════ -->
|
||||||
<div id="sec-interfaces"></div>
|
<div id="sec-interfaces"></div>
|
||||||
|
|
||||||
<!-- ═══════════════ 5. SFP CARDS ═══════════════ -->
|
<!-- ═══════════════ 5–6. SFP + ALARMS (side by side) ═══════════════ -->
|
||||||
<div id="sec-sfp"></div>
|
<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>
|
||||||
<!-- ═══════════════ 6. ALARMS ═══════════════ -->
|
<div id="sec-alarms" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||||
<div id="sec-alarms"></div>
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════ 7. TRAFFIC POLICIES ═══════════════ -->
|
<!-- ═══════════════ 7. TRAFFIC POLICIES ═══════════════ -->
|
||||||
<div id="sec-policies"></div>
|
<div id="sec-policies"></div>
|
||||||
|
|
||||||
<!-- ═══════════════ 8. L2 FILTERS ═══════════════ -->
|
<!-- ═══════════════ 8–9. L2 FILTERS + REGULATORS (side by side) ═══════════════ -->
|
||||||
<div id="sec-filters"></div>
|
<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>
|
||||||
<!-- ═══════════════ 9. REGULATORS ═══════════════ -->
|
<div id="sec-regulators" style="flex:1;min-width:0;display:flex;flex-direction:column"></div>
|
||||||
<div id="sec-regulators"></div>
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════ 10. COVERAGE MATRIX ═══════════════ -->
|
<!-- ═══════════════ 10. COVERAGE MATRIX ═══════════════ -->
|
||||||
<div id="sec-coverage"></div>
|
<div id="sec-coverage"></div>
|
||||||
@ -680,6 +734,26 @@ function formatUptime(sec) {{
|
|||||||
p.push(s+'s');
|
p.push(s+'s');
|
||||||
return p.join(' ');
|
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) {{
|
function sevLabel(s) {{
|
||||||
return {{'0':'INFO','1':'MINOR','2':'MAJOR','3':'CRITICAL'}}[s] || s;
|
return {{'0':'INFO','1':'MINOR','2':'MAJOR','3':'CRITICAL'}}[s] || s;
|
||||||
}}
|
}}
|
||||||
@ -775,6 +849,10 @@ function renderWalkControl() {{
|
|||||||
<option value="targeted">Targeted</option>
|
<option value="targeted">Targeted</option>
|
||||||
<option value="full">Full</option>
|
<option value="full">Full</option>
|
||||||
</select>
|
</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()">
|
<button class="walk-btn" id="walk-btn" onclick="startWalk()">
|
||||||
<i class="bi bi-play-fill"></i> Walk
|
<i class="bi bi-play-fill"></i> Walk
|
||||||
</button>
|
</button>
|
||||||
@ -795,6 +873,7 @@ let walkEventSource = null;
|
|||||||
function startWalk() {{
|
function startWalk() {{
|
||||||
const target = document.getElementById('walk-target').value.trim();
|
const target = document.getElementById('walk-target').value.trim();
|
||||||
const mode = document.getElementById('walk-mode').value;
|
const mode = document.getElementById('walk-mode').value;
|
||||||
|
const policies = document.getElementById('walk-policies').checked;
|
||||||
const btn = document.getElementById('walk-btn');
|
const btn = document.getElementById('walk-btn');
|
||||||
|
|
||||||
if (!target) {{
|
if (!target) {{
|
||||||
@ -803,55 +882,78 @@ function startWalk() {{
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Walking...';
|
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Pinging...';
|
||||||
updateWalkStatus('running', 'Starting walk...');
|
updateWalkStatus('running', 'Checking reachability...');
|
||||||
|
|
||||||
// Close any previous SSE connection
|
// Step 1: Ping check
|
||||||
if (walkEventSource) walkEventSource.close();
|
fetch('/api/ping', {{
|
||||||
|
|
||||||
fetch('/api/walk', {{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{ 'Content-Type': 'application/json' }},
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
body: JSON.stringify({{ target, mode }})
|
body: JSON.stringify({{ target }})
|
||||||
}})
|
}})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(resp => {{
|
.then(ping => {{
|
||||||
if (resp.error) {{
|
if (!ping.reachable) {{
|
||||||
updateWalkStatus('error', resp.error);
|
updateWalkStatus('error', 'NID is DOWN. Verify Local Power and Router Interface Status.');
|
||||||
resetWalkBtn();
|
resetWalkBtn();
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
// Open SSE for status updates
|
updateWalkStatus('complete', 'NID Management is UP');
|
||||||
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') {{
|
// Step 2: Proceed with walk after brief pause to show UP status
|
||||||
updateWalkStatus('complete', s.message);
|
setTimeout(() => {{
|
||||||
walkEventSource.close();
|
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Walking...';
|
||||||
walkEventSource = null;
|
updateWalkStatus('running', 'Starting walk...');
|
||||||
// Reload to pick up fresh data
|
|
||||||
setTimeout(() => window.location.reload(), 800);
|
// Close any previous SSE connection
|
||||||
}} else if (s.state === 'error') {{
|
if (walkEventSource) walkEventSource.close();
|
||||||
updateWalkStatus('error', s.message);
|
|
||||||
walkEventSource.close();
|
fetch('/api/walk', {{
|
||||||
walkEventSource = null;
|
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);
|
||||||
resetWalkBtn();
|
resetWalkBtn();
|
||||||
}} else {{
|
}});
|
||||||
updateWalkStatus('running', s.message);
|
}}, 600);
|
||||||
}}
|
|
||||||
}};
|
|
||||||
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 => {{
|
.catch(err => {{
|
||||||
updateWalkStatus('error', 'Failed to connect: ' + err.message);
|
updateWalkStatus('error', 'Ping check failed: ' + err.message);
|
||||||
resetWalkBtn();
|
resetWalkBtn();
|
||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
@ -891,7 +993,7 @@ function renderHeader() {{
|
|||||||
const cfgByNum = {{}};
|
const cfgByNum = {{}};
|
||||||
for (const [k,v] of Object.entries(alarmCfg)) cfgByNum[v.number] = v;
|
for (const [k,v] of Object.entries(alarmCfg)) cfgByNum[v.number] = v;
|
||||||
for (const [k,a] of Object.entries(alarmStatus)) {{
|
for (const [k,a] of Object.entries(alarmStatus)) {{
|
||||||
if (a.active === '1') {{
|
if (isTrue(a.active)) {{
|
||||||
activeCount++;
|
activeCount++;
|
||||||
const cfg = cfgByNum[a.number];
|
const cfg = cfgByNum[a.number];
|
||||||
if (cfg) sevCounts[cfg.severity] = (sevCounts[cfg.severity]||0) + 1;
|
if (cfg) sevCounts[cfg.severity] = (sevCounts[cfg.severity]||0) + 1;
|
||||||
@ -983,6 +1085,40 @@ function renderHeader() {{
|
|||||||
</div>`;
|
</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 ───────────────────────────────────
|
// ── 2. Front Panel ───────────────────────────────────
|
||||||
function renderPanel() {{
|
function renderPanel() {{
|
||||||
const connectors = DATA.connectors || {{}};
|
const connectors = DATA.connectors || {{}};
|
||||||
@ -1004,7 +1140,7 @@ function renderPanel() {{
|
|||||||
const iface = ifaces[connIdx];
|
const iface = ifaces[connIdx];
|
||||||
if (connType(connIdx) === 'sfp') {{
|
if (connType(connIdx) === 'sfp') {{
|
||||||
const sfp = sfpInfo[connIdx];
|
const sfp = sfpInfo[connIdx];
|
||||||
const present = sfp && sfp.present === '1';
|
const present = sfp && isTrue(sfp.present);
|
||||||
if (!present) return 'empty';
|
if (!present) return 'empty';
|
||||||
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
||||||
return 'present-nolink';
|
return 'present-nolink';
|
||||||
@ -1019,7 +1155,7 @@ function renderPanel() {{
|
|||||||
function sfpLabel(connIdx) {{
|
function sfpLabel(connIdx) {{
|
||||||
const sfp = sfpInfo[connIdx];
|
const sfp = sfpInfo[connIdx];
|
||||||
if (!sfp) return '';
|
if (!sfp) return '';
|
||||||
if (sfp.present !== '1') return 'EMPTY';
|
if (!isTrue(sfp.present)) return 'EMPTY';
|
||||||
const pn = sfp.vendorPn || '';
|
const pn = sfp.vendorPn || '';
|
||||||
if (pn.length > 8) return pn.substring(0,8);
|
if (pn.length > 8) return pn.substring(0,8);
|
||||||
return pn || sfp.vendor || '';
|
return pn || sfp.vendor || '';
|
||||||
@ -1034,15 +1170,15 @@ function renderPanel() {{
|
|||||||
return {{ label, alias }};
|
return {{ label, alias }};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Build port slots dynamically from connectors 1-4
|
// Build port slots dynamically from connectors
|
||||||
|
const {{ dataPorts, mgmtPort }} = getPortLists();
|
||||||
let slots = '';
|
let slots = '';
|
||||||
for (let i = 1; i <= 4; i++) {{
|
for (const idx of dataPorts) {{
|
||||||
const idx = String(i);
|
|
||||||
const conn = connectors[idx] || {{}};
|
const conn = connectors[idx] || {{}};
|
||||||
const isSfp = connType(idx) === 'sfp';
|
const isSfp = connType(idx) === 'sfp';
|
||||||
const state = slotState(idx);
|
const state = slotState(idx);
|
||||||
const pi = portInfo(idx);
|
const pi = portInfo(idx);
|
||||||
const slotName = conn.name || (isSfp ? `SFP-${{i}}` : `RJ45-${{i}}`);
|
const slotName = conn.name || (isSfp ? `SFP-${{idx}}` : `RJ45-${{idx}}`);
|
||||||
|
|
||||||
let icon, detail;
|
let icon, detail;
|
||||||
if (isSfp) {{
|
if (isSfp) {{
|
||||||
@ -1062,7 +1198,7 @@ function renderPanel() {{
|
|||||||
? `<div class="port-label" title="${{esc(pi.label + (pi.alias ? ' / ' + pi.alias : ''))}}">${{labelParts.join('<br>')}}</div>`
|
? `<div class="port-label" title="${{esc(pi.label + (pi.alias ? ' / ' + pi.alias : ''))}}">${{labelParts.join('<br>')}}</div>`
|
||||||
: '';
|
: '';
|
||||||
slots += `<div class="sfp-slot-group">
|
slots += `<div class="sfp-slot-group">
|
||||||
<div class="sfp-slot ${{state}}" data-sfp="${{i}}" onclick="selectSfp(${{i}})">
|
<div class="sfp-slot ${{state}}" data-sfp="${{idx}}" onclick="selectSfp(${{idx}})">
|
||||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
${{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>
|
<span style="font-size:0.5rem;max-width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${{esc(detail)}}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1071,7 +1207,7 @@ function renderPanel() {{
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
// Management port
|
// Management port
|
||||||
const mgmtIf = ifaces['5'];
|
const mgmtIf = mgmtPort ? ifaces[mgmtPort] : null;
|
||||||
const mgmtUp = mgmtIf && isUp(mgmtIf.ifOperStatus);
|
const mgmtUp = mgmtIf && isUp(mgmtIf.ifOperStatus);
|
||||||
const mgmtSlot = `<div class="mgmt-port ${{mgmtUp ? 'link-up' : 'link-down'}}">
|
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>
|
<i class="bi bi-ethernet"></i><span style="font-size:0.55rem">MGMT</span>
|
||||||
@ -1081,7 +1217,7 @@ function renderPanel() {{
|
|||||||
// Power feeds
|
// Power feeds
|
||||||
let pwrHtml = '<div class="pwr-block">';
|
let pwrHtml = '<div class="pwr-block">';
|
||||||
for (const [k,p] of Object.entries(pwr)) {{
|
for (const [k,p] of Object.entries(pwr)) {{
|
||||||
const ok = p.present === '1';
|
const ok = isTrue(p.present);
|
||||||
pwrHtml += `<div><span class="pwr-led ${{ok?'ok':'fail'}}"></span>${{esc(p.name)}} ${{ok?'OK':'ABSENT'}}</div>`;
|
pwrHtml += `<div><span class="pwr-led ${{ok?'ok':'fail'}}"></span>${{esc(p.name)}} ${{ok?'OK':'ABSENT'}}</div>`;
|
||||||
}}
|
}}
|
||||||
pwrHtml += '</div>';
|
pwrHtml += '</div>';
|
||||||
@ -1195,27 +1331,27 @@ function renderSfp() {{
|
|||||||
const sfpDiag = DATA.sfp_diagnostics || {{}};
|
const sfpDiag = DATA.sfp_diagnostics || {{}};
|
||||||
const sfpThresh = DATA.sfp_thresholds || {{}};
|
const sfpThresh = DATA.sfp_thresholds || {{}};
|
||||||
|
|
||||||
|
const {{ dataPorts }} = getPortLists();
|
||||||
let cards = '';
|
let cards = '';
|
||||||
for (let i = 1; i <= 4; i++) {{
|
for (const si of dataPorts) {{
|
||||||
const si = String(i);
|
|
||||||
const info = sfpInfo[si];
|
const info = sfpInfo[si];
|
||||||
const diag = sfpDiag[si] || {{}};
|
const diag = sfpDiag[si] || {{}};
|
||||||
const thresh = sfpThresh[si] || {{}};
|
const thresh = sfpThresh[si] || {{}};
|
||||||
const conn = connectors[si] || {{}};
|
const conn = connectors[si] || {{}};
|
||||||
|
|
||||||
if (!info || info.present !== '1') {{
|
if (!info || !isTrue(info.present)) {{
|
||||||
cards += `<div class="sfp-detail" id="sfp-detail-${{i}}">
|
cards += `<div class="sfp-detail" id="sfp-detail-${{si}}">
|
||||||
<div class="card-dark" style="border-left:3px solid #555">
|
<div class="card-dark" style="border-left:3px solid #555">
|
||||||
<div class="card-body" style="text-align:center;color:#555;padding:2rem">
|
<div class="card-body" style="text-align:center;color:#555;padding:2rem">
|
||||||
<i class="bi bi-slash-circle" style="font-size:2rem"></i>
|
<i class="bi bi-slash-circle" style="font-size:2rem"></i>
|
||||||
<div>SFP-${{i}} — Not Present</div>
|
<div>${{esc(conn.name || 'Port ' + si)}} — Not Present</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
continue;
|
continue;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
const ddm = info.diagCapable === '1';
|
const ddm = isTrue(info.diagCapable);
|
||||||
const ddmBadge = ddm
|
const ddmBadge = ddm
|
||||||
? '<span class="badge bg-success">DDM Supported</span>'
|
? '<span class="badge bg-success">DDM Supported</span>'
|
||||||
: '<span class="badge bg-secondary">DDM Not Supported</span>';
|
: '<span class="badge bg-secondary">DDM Not Supported</span>';
|
||||||
@ -1249,10 +1385,10 @@ function renderSfp() {{
|
|||||||
const wl = info.wavelength && info.wavelength !== '0' ? info.wavelength + ' nm' : 'N/A (copper)';
|
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('-');
|
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}}">
|
cards += `<div class="sfp-detail ${{si===dataPorts[0]?'active':''}}" id="sfp-detail-${{si}}">
|
||||||
<div class="card-dark" style="border-left:3px solid var(--accent)">
|
<div class="card-dark" style="border-left:3px solid var(--accent)">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="bi bi-lightning-charge"></i> SFP-${{i}}: ${{esc(info.vendor)}} ${{esc(info.vendorPn)}}
|
<i class="bi bi-lightning-charge"></i> ${{esc(conn.name || 'Port ' + si)}}: ${{esc(info.vendor)}} ${{esc(info.vendorPn)}}
|
||||||
<span class="ms-auto">${{ddmBadge}}</span>
|
<span class="ms-auto">${{ddmBadge}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -1272,8 +1408,8 @@ function renderSfp() {{
|
|||||||
<h6 style="font-size:0.8rem;color:var(--text-muted)">CAPABILITIES</h6>
|
<h6 style="font-size:0.8rem;color:var(--text-muted)">CAPABILITIES</h6>
|
||||||
<div style="font-size:0.82rem">
|
<div style="font-size:0.82rem">
|
||||||
<div>DDM Capable: <strong>${{ddm ? 'Yes' : 'No'}}</strong></div>
|
<div>DDM Capable: <strong>${{ddm ? 'Yes' : 'No'}}</strong></div>
|
||||||
<div>Internal Cal: <strong>${{info.internalCal==='1' ? 'Yes' : 'No'}}</strong></div>
|
<div>Internal Cal: <strong>${{isTrue(info.internalCal) ? 'Yes' : 'No'}}</strong></div>
|
||||||
<div>Alarm Capable: <strong>${{info.alarmCapable==='1' ? 'Yes' : 'No'}}</strong></div>
|
<div>Alarm Capable: <strong>${{isTrue(info.alarmCapable) ? 'Yes' : 'No'}}</strong></div>
|
||||||
<div>SFF-8472 Rev: <strong>${{esc(info.rev8472)}}</strong></div>
|
<div>SFF-8472 Rev: <strong>${{esc(info.rev8472)}}</strong></div>
|
||||||
<div>ID Type: <strong>${{esc(info.idType)}}</strong></div>
|
<div>ID Type: <strong>${{esc(info.idType)}}</strong></div>
|
||||||
<div>Ext ID: <strong>${{esc(info.extIdType)}}</strong></div>
|
<div>Ext ID: <strong>${{esc(info.extIdType)}}</strong></div>
|
||||||
@ -1293,7 +1429,7 @@ function renderSfp() {{
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-bottom:0.5rem">
|
<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:
|
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('')}}
|
${{dataPorts.map(k => `<button class="btn btn-sm btn-outline-secondary ms-1" onclick="selectSfp(${{k}})">${{esc((connectors[k]||{{}}).name || 'Port '+k)}}</button>`).join('')}}
|
||||||
</div>
|
</div>
|
||||||
${{cards}}
|
${{cards}}
|
||||||
</div>
|
</div>
|
||||||
@ -1314,7 +1450,7 @@ function renderAlarms() {{
|
|||||||
// Collect active alarms (when status table available)
|
// Collect active alarms (when status table available)
|
||||||
const active = [];
|
const active = [];
|
||||||
for (const [k,a] of Object.entries(alarmStatus)) {{
|
for (const [k,a] of Object.entries(alarmStatus)) {{
|
||||||
if (a.active === '1') {{
|
if (isTrue(a.active)) {{
|
||||||
const cfg = cfgByNum[a.number] || {{}};
|
const cfg = cfgByNum[a.number] || {{}};
|
||||||
active.push({{ ...a, ...cfg, _statusId: k }});
|
active.push({{ ...a, ...cfg, _statusId: k }});
|
||||||
}}
|
}}
|
||||||
@ -1356,7 +1492,7 @@ function renderAlarms() {{
|
|||||||
<td>${{esc(c.description)}}</td>`;
|
<td>${{esc(c.description)}}</td>`;
|
||||||
if (hasSeverity) rows += `
|
if (hasSeverity) rows += `
|
||||||
<td><span class="badge ${{sevClass(c.severity)}}">${{sevLabel(c.severity)}}</span></td>
|
<td><span class="badge ${{sevClass(c.severity)}}">${{sevLabel(c.severity)}}</span></td>
|
||||||
<td>${{c.enabled === '1' ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--text-muted)">No</span>'}}</td>`;
|
<td>${{isTrue(c.enabled) ? '<span style="color:var(--green)">Yes</span>' : '<span style="color:var(--text-muted)">No</span>'}}</td>`;
|
||||||
if (hasCondition) rows += `
|
if (hasCondition) rows += `
|
||||||
<td class="mono">${{esc(c.conditionType)}}</td>
|
<td class="mono">${{esc(c.conditionType)}}</td>
|
||||||
<td class="mono">${{esc(c.amoType)}}</td>`;
|
<td class="mono">${{esc(c.amoType)}}</td>`;
|
||||||
@ -1503,12 +1639,12 @@ function renderFilters() {{
|
|||||||
let rows = '';
|
let rows = '';
|
||||||
for (const [id, f] of Object.entries(filters)) {{
|
for (const [id, f] of Object.entries(filters)) {{
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
if (f.macDstEn === '1') conditions.push('MAC Dst: ' + esc(f.macDst));
|
if (isTrue(f.macDstEn)) conditions.push('MAC Dst: ' + esc(f.macDst));
|
||||||
if (f.macSrcEn === '1') conditions.push('MAC Src: ' + esc(f.macSrc));
|
if (isTrue(f.macSrcEn)) conditions.push('MAC Src: ' + esc(f.macSrc));
|
||||||
if (f.etypeEn === '1') conditions.push('EType: ' + esc(f.etype));
|
if (isTrue(f.etypeEn)) conditions.push('EType: ' + esc(f.etype));
|
||||||
if (f.vlan1IdEn === '1') conditions.push('VLAN1: ' + esc(f.vlan1Id));
|
if (isTrue(f.vlan1IdEn)) conditions.push('VLAN1: ' + esc(f.vlan1Id));
|
||||||
if (f.vlan2IdEn === '1') conditions.push('VLAN2: ' + esc(f.vlan2Id));
|
if (isTrue(f.vlan2IdEn)) conditions.push('VLAN2: ' + esc(f.vlan2Id));
|
||||||
if (f.vlan1PriorEn === '1') conditions.push('PCP1: ' + esc(f.vlan1Prior));
|
if (isTrue(f.vlan1PriorEn)) conditions.push('PCP1: ' + esc(f.vlan1Prior));
|
||||||
const condStr = conditions.length ? conditions.join(', ') : '<span class="status-na">any (catchall)</span>';
|
const condStr = conditions.length ? conditions.join(', ') : '<span class="status-na">any (catchall)</span>';
|
||||||
|
|
||||||
rows += `<tr>
|
rows += `<tr>
|
||||||
@ -1652,7 +1788,6 @@ function parseRemotePlatform(sysDesc, sysName) {{
|
|||||||
|
|
||||||
function renderLldp() {{
|
function renderLldp() {{
|
||||||
const neighbors = DATA.lldp_neighbors || {{}};
|
const neighbors = DATA.lldp_neighbors || {{}};
|
||||||
const stats = DATA.lldp_stats || {{}};
|
|
||||||
const ifaces = DATA.interfaces || {{}};
|
const ifaces = DATA.interfaces || {{}};
|
||||||
const sfpInfo = DATA.sfp_info || {{}};
|
const sfpInfo = DATA.sfp_info || {{}};
|
||||||
const device = DATA.device || {{}};
|
const device = DATA.device || {{}};
|
||||||
@ -1664,14 +1799,13 @@ function renderLldp() {{
|
|||||||
const localName = device.identifier || device.sysName || 'NID';
|
const localName = device.identifier || device.sysName || 'NID';
|
||||||
const localModel = device.commercialName || device.sysDescr || 'NID';
|
const localModel = device.commercialName || device.sysDescr || 'NID';
|
||||||
|
|
||||||
// Determine slot state — connector-type aware (same logic as renderPanel)
|
|
||||||
function slotState(portIdx) {{
|
function slotState(portIdx) {{
|
||||||
const conn = connectors[portIdx];
|
const conn = connectors[portIdx];
|
||||||
const isSfp = conn && conn.type === '14';
|
const isSfp = conn && conn.type === '14';
|
||||||
const iface = ifaces[portIdx];
|
const iface = ifaces[portIdx];
|
||||||
if (isSfp) {{
|
if (isSfp) {{
|
||||||
const sfp = sfpInfo[portIdx];
|
const sfp = sfpInfo[portIdx];
|
||||||
const present = sfp && sfp.present === '1';
|
const present = sfp && isTrue(sfp.present);
|
||||||
if (!present) return 'empty';
|
if (!present) return 'empty';
|
||||||
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
|
||||||
return 'present-nolink';
|
return 'present-nolink';
|
||||||
@ -1682,137 +1816,99 @@ function renderLldp() {{
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Build a row for each network port (1-4)
|
function buildNeighborCard(nbr, cssClass) {{
|
||||||
let rowsHtml = '';
|
const platform = parseRemotePlatform(nbr.remSysDesc, nbr.remSysName);
|
||||||
for (let i = 1; i <= 4; i++) {{
|
const shortName = (nbr.remSysName || '').split('.')[0] || 'Unknown';
|
||||||
const portKey = String(i);
|
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) {{
|
||||||
const conn = connectors[portKey] || {{}};
|
const conn = connectors[portKey] || {{}};
|
||||||
const isSfp = conn.type === '14';
|
const isSfp = conn.type === '14';
|
||||||
const nbr = neighborByPort[portKey];
|
const nbr = neighborByPort[portKey];
|
||||||
const state = slotState(portKey);
|
const state = slotState(portKey);
|
||||||
const iface = ifaces[portKey] || {{}};
|
const iface = ifaces[portKey] || {{}};
|
||||||
const localUp = isUp(iface.ifOperStatus);
|
const localUp = isUp(iface.ifOperStatus);
|
||||||
const slotName = conn.name || (isSfp ? `SFP-${{i}}` : `RJ45-${{i}}`);
|
const slotName = conn.name || (isSfp ? `SFP-${{portKey}}` : `RJ45-${{portKey}}`);
|
||||||
const icon = isSfp
|
const icon = isSfp
|
||||||
? (state === 'empty' ? '<i class="bi bi-dash"></i>' :
|
? (state === 'empty' ? '<i class="bi bi-dash"></i>' :
|
||||||
state === 'present-link' ? '<i class="bi bi-arrow-left-right"></i>' :
|
state === 'present-link' ? '<i class="bi bi-arrow-left-right"></i>' :
|
||||||
'<i class="bi bi-plug"></i>')
|
'<i class="bi bi-plug"></i>')
|
||||||
: '<i class="bi bi-ethernet"></i>';
|
: '<i class="bi bi-ethernet"></i>';
|
||||||
|
const portLabel = iface.ifName || 'Port ' + portKey;
|
||||||
|
|
||||||
if (nbr) {{
|
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';
|
const linkClass = localUp ? 'up' : 'down';
|
||||||
|
colsHtml += `
|
||||||
rowsHtml += `
|
<div class="lldp-col">
|
||||||
<div class="lldp-row">
|
<div class="lldp-col-header ${{state}}">
|
||||||
<div class="lldp-local-slot ${{state}}">
|
|
||||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lldp-connector">
|
<div class="lldp-col-port-label">${{esc(portLabel)}}</div>
|
||||||
<div class="link-port-label">${{esc(iface.ifName || nbr.localPortName || 'Port ' + i)}}</div>
|
<div class="lldp-col-line ${{linkClass}}"></div>
|
||||||
<div class="link-line ${{linkClass}}"></div>
|
<div class="lldp-col-port-label">${{esc(nbr.remPortId || '?')}}</div>
|
||||||
<div class="link-port-label">${{esc(nbr.remPortId || '?')}}</div>
|
${{buildNeighborCard(nbr, 'lldp-col-remote')}}
|
||||||
</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>`;
|
</div>`;
|
||||||
}} else {{
|
}} else {{
|
||||||
rowsHtml += `
|
colsHtml += `
|
||||||
<div class="lldp-row idle">
|
<div class="lldp-col idle">
|
||||||
<div class="lldp-local-slot ${{state}}">
|
<div class="lldp-col-header ${{state}}">
|
||||||
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lldp-connector">
|
<div class="lldp-col-port-label">${{esc(portLabel)}}</div>
|
||||||
<div class="link-line" style="background:#2d3340;height:2px;opacity:0.5"></div>
|
<div class="lldp-col-line idle"></div>
|
||||||
</div>
|
<div class="lldp-col-idle-label">No LLDP neighbor</div>
|
||||||
<div class="lldp-idle-label">No LLDP neighbor</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Include MGMT port if it has a neighbor
|
// MGMT column
|
||||||
const mgmtNbr = neighborByPort['5'];
|
const mgmtNbr = mgmtPort ? neighborByPort[mgmtPort] : null;
|
||||||
if (mgmtNbr) {{
|
if (mgmtNbr) {{
|
||||||
const mgmtIface = ifaces['5'] || {{}};
|
const mgmtIface = ifaces[mgmtPort] || {{}};
|
||||||
const mgmtUp = isUp(mgmtIface.ifOperStatus);
|
const mgmtUp = isUp(mgmtIface.ifOperStatus);
|
||||||
const platform = parseRemotePlatform(mgmtNbr.remSysDesc, mgmtNbr.remSysName);
|
colsHtml += `<div class="lldp-col-divider"></div>`;
|
||||||
const shortName = (mgmtNbr.remSysName || '').split('.')[0] || 'Unknown';
|
colsHtml += `
|
||||||
const modelLine = platform.vendor ? `${{platform.vendor}} ${{platform.model}}` : platform.model;
|
<div class="lldp-col">
|
||||||
rowsHtml += `
|
<div class="lldp-col-header ${{mgmtUp ? 'present-link' : 'present-nolink'}}">
|
||||||
<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>
|
<i class="bi bi-ethernet"></i><span class="slot-label">MGMT</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lldp-connector">
|
<div class="lldp-col-port-label">Management</div>
|
||||||
<div class="link-port-label">Management</div>
|
<div class="lldp-col-line ${{mgmtUp ? 'up' : 'down'}}"></div>
|
||||||
<div class="link-line ${{mgmtUp ? 'up' : 'down'}}"></div>
|
<div class="lldp-col-port-label">${{esc(mgmtNbr.remPortId || '?')}}</div>
|
||||||
<div class="link-port-label">${{esc(mgmtNbr.remPortId || '?')}}</div>
|
${{buildNeighborCard(mgmtNbr, 'lldp-col-remote')}}
|
||||||
</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>`;
|
</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 = `
|
document.getElementById('sec-lldp').innerHTML = `
|
||||||
<div class="card-dark">
|
<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-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="card-body">
|
||||||
<div class="lldp-panel">
|
<div class="lldp-panel">
|
||||||
<span class="panel-label">${{esc(localName)}} — ${{esc(localModel)}}</span>
|
<span class="panel-label">${{esc(localName)}} — ${{esc(localModel)}}</span>
|
||||||
${{rowsHtml}}
|
<div class="lldp-columns">
|
||||||
</div>
|
${{colsHtml}}
|
||||||
${{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>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}}
|
}}
|
||||||
@ -1912,7 +2008,7 @@ function renderCoverage() {{
|
|||||||
// Check SFP DDM
|
// Check SFP DDM
|
||||||
const sfpInfo = DATA.sfp_info || {{}};
|
const sfpInfo = DATA.sfp_info || {{}};
|
||||||
for (const [k,s] of Object.entries(sfpInfo)) {{
|
for (const [k,s] of Object.entries(sfpInfo)) {{
|
||||||
if (s.present === '1' && s.diagCapable !== '1') {{
|
if (isTrue(s.present) && !isTrue(s.diagCapable)) {{
|
||||||
gaps.push({{
|
gaps.push({{
|
||||||
type: 'red',
|
type: 'red',
|
||||||
text: `SFP-${{k}} DDM: Not available via SNMP (diagCapable=false). NID web UI reads SFP I2C bus directly.`
|
text: `SFP-${{k}} DDM: Not available via SNMP (diagCapable=false). NID web UI reads SFP I2C bus directly.`
|
||||||
@ -2031,6 +2127,7 @@ function renderPortCmp() {{
|
|||||||
// ── Render all sections ──────────────────────────────
|
// ── Render all sections ──────────────────────────────
|
||||||
renderWalkControl();
|
renderWalkControl();
|
||||||
renderHeader();
|
renderHeader();
|
||||||
|
renderMap();
|
||||||
renderPanel();
|
renderPanel();
|
||||||
renderLldp();
|
renderLldp();
|
||||||
renderInterfaces();
|
renderInterfaces();
|
||||||
|
|||||||
BIN
images/Screenshot 2026-03-06 075511.png
Normal file
BIN
images/Screenshot 2026-03-06 075511.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@ -41,7 +41,6 @@ TARGETED_OIDS = [
|
|||||||
(".1.3.6.1.4.1.22420.1.1", "ACD-DESC-MIB"),
|
(".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.1", "ACD-ALARM-MIB"),
|
||||||
(".1.3.6.1.4.1.22420.2.2", "ACD-FILTER-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.4", "ACD-SFP-MIB"),
|
||||||
(".1.3.6.1.4.1.22420.2.6", "ACD-REGULATOR-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"),
|
(".1.3.6.1.4.1.22420.2.8", "ACD-SMAP-MIB"),
|
||||||
@ -79,6 +78,10 @@ SNMP_V3_PRIV_PROTO = ENV.get("SNMP_V3_PRIV_PROTO", "AES")
|
|||||||
SNMP_V3_PRIV_PASS = ENV.get("SNMP_V3_PRIV_PASS", "")
|
SNMP_V3_PRIV_PASS = ENV.get("SNMP_V3_PRIV_PASS", "")
|
||||||
SNMP_V3_SEC_LEVEL = ENV.get("SNMP_V3_SEC_LEVEL", "authPriv")
|
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 state (shared across threads) ───────────────────────────────
|
||||||
|
|
||||||
walk_lock = threading.Lock()
|
walk_lock = threading.Lock()
|
||||||
@ -125,7 +128,7 @@ def build_snmp_auth() -> list:
|
|||||||
return ["-v", SNMP_VERSION, "-c", SNMP_COMMUNITY]
|
return ["-v", SNMP_VERSION, "-c", SNMP_COMMUNITY]
|
||||||
|
|
||||||
|
|
||||||
def run_walk(target: str, mode: str):
|
def run_walk(target: str, mode: str, policies: bool = True):
|
||||||
"""Execute the full walk pipeline in a background thread."""
|
"""Execute the full walk pipeline in a background thread."""
|
||||||
global latest_json
|
global latest_json
|
||||||
|
|
||||||
@ -142,6 +145,11 @@ def run_walk(target: str, mode: str):
|
|||||||
auth = build_snmp_auth()
|
auth = build_snmp_auth()
|
||||||
t_start = time.time()
|
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:
|
try:
|
||||||
# ── Step 1: snmpwalk ──────────────────────────────────────
|
# ── Step 1: snmpwalk ──────────────────────────────────────
|
||||||
# Use snmpbulkwalk (GETBULK PDUs) when available — much faster
|
# Use snmpbulkwalk (GETBULK PDUs) when available — much faster
|
||||||
@ -157,7 +165,7 @@ def run_walk(target: str, mode: str):
|
|||||||
walk_file.write_text(result.stdout)
|
walk_file.write_text(result.stdout)
|
||||||
else:
|
else:
|
||||||
# Walk subtrees in parallel for speed
|
# Walk subtrees in parallel for speed
|
||||||
total = len(TARGETED_OIDS)
|
total = len(walk_oids)
|
||||||
completed = [0] # mutable counter for progress
|
completed = [0] # mutable counter for progress
|
||||||
results_map = {}
|
results_map = {}
|
||||||
|
|
||||||
@ -175,7 +183,7 @@ def run_walk(target: str, mode: str):
|
|||||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||||
futures = [
|
futures = [
|
||||||
pool.submit(walk_subtree, i, oid, label)
|
pool.submit(walk_subtree, i, oid, label)
|
||||||
for i, (oid, label) in enumerate(TARGETED_OIDS)
|
for i, (oid, label) in enumerate(walk_oids)
|
||||||
]
|
]
|
||||||
for fut in as_completed(futures):
|
for fut in as_completed(futures):
|
||||||
idx, output = fut.result()
|
idx, output = fut.result()
|
||||||
@ -309,11 +317,39 @@ class NIDHandler(BaseHTTPRequestHandler):
|
|||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path == "/api/walk":
|
if self.path == "/api/walk":
|
||||||
self._handle_walk()
|
self._handle_walk()
|
||||||
|
elif self.path == "/api/ping":
|
||||||
|
self._handle_ping()
|
||||||
elif self.path == "/api/clear":
|
elif self.path == "/api/clear":
|
||||||
self._handle_clear()
|
self._handle_clear()
|
||||||
else:
|
else:
|
||||||
self._send(404, "text/plain", b"Not found")
|
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):
|
def _handle_walk(self):
|
||||||
"""Start a walk in a background thread."""
|
"""Start a walk in a background thread."""
|
||||||
current, _ = get_status()
|
current, _ = get_status()
|
||||||
@ -325,6 +361,7 @@ class NIDHandler(BaseHTTPRequestHandler):
|
|||||||
body = json.loads(self.rfile.read(length)) if length else {}
|
body = json.loads(self.rfile.read(length)) if length else {}
|
||||||
target = body.get("target", "").strip()
|
target = body.get("target", "").strip()
|
||||||
mode = body.get("mode", "targeted").strip()
|
mode = body.get("mode", "targeted").strip()
|
||||||
|
policies = body.get("policies", True)
|
||||||
|
|
||||||
if not target:
|
if not target:
|
||||||
self._send_json(400, {"error": "target IP required"})
|
self._send_json(400, {"error": "target IP required"})
|
||||||
@ -334,7 +371,7 @@ class NIDHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
set_status("walking", message="Starting walk...", progress=1)
|
set_status("walking", message="Starting walk...", progress=1)
|
||||||
thread = threading.Thread(target=run_walk, args=(target, mode), daemon=True)
|
thread = threading.Thread(target=run_walk, args=(target, mode, policies), daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
self._send_json(200, {"status": "started", "target": target, "mode": mode})
|
self._send_json(200, {"status": "started", "target": target, "mode": mode})
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,8 @@ fi
|
|||||||
|
|
||||||
# ── Define OID subtrees ──────────────────────────────────────────────
|
# ── Define OID subtrees ──────────────────────────────────────────────
|
||||||
# Targeted: only what the viewer/parser needs
|
# Targeted: only what the viewer/parser needs
|
||||||
|
WALK_POLICIES="${SNMP_WALK_POLICIES:-true}"
|
||||||
|
|
||||||
TARGETED_OIDS=(
|
TARGETED_OIDS=(
|
||||||
.1.3.6.1.2.1.1 # System (sysDescr, sysName, sysUpTime, …)
|
.1.3.6.1.2.1.1 # System (sysDescr, sysName, sysUpTime, …)
|
||||||
.1.3.6.1.2.1.2 # IF-MIB (interface table)
|
.1.3.6.1.2.1.2 # IF-MIB (interface table)
|
||||||
@ -85,13 +87,16 @@ 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.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.1 # ACD-ALARM-MIB
|
||||||
.1.3.6.1.4.1.22420.2.2 # ACD-FILTER-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.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.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.8 # ACD-SMAP-MIB (CoS profiles)
|
||||||
.1.3.6.1.4.1.22420.2.9 # ACD-PORT-MIB (port config/status)
|
.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 ─────────────────────────────────────────────
|
# ── Prepare output paths ─────────────────────────────────────────────
|
||||||
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
|
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||||
SAFE_IP="${TARGET//./-}"
|
SAFE_IP="${TARGET//./-}"
|
||||||
|
|||||||
13960
walks/10-10-14-65_2026-03-06_08-20-09_walk.txt
Normal file
13960
walks/10-10-14-65_2026-03-06_08-20-09_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
10815
walks/10-10-14-65_2026-03-06_08-20-09_walk_monitoring.json
Normal file
10815
walks/10-10-14-65_2026-03-06_08-20-09_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
13733
walks/10-10-14-65_2026-03-06_08-20-09_walk_resolved.json
Normal file
13733
walks/10-10-14-65_2026-03-06_08-20-09_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
8345
walks/10-10-14-65_2026-03-06_08-20-09_walk_tables.csv
Normal file
8345
walks/10-10-14-65_2026-03-06_08-20-09_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
8384
walks/10-13-60-102_2026-03-06_08-15-34_walk.txt
Normal file
8384
walks/10-13-60-102_2026-03-06_08-15-34_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
5705
walks/10-13-60-102_2026-03-06_08-15-34_walk_monitoring.json
Normal file
5705
walks/10-13-60-102_2026-03-06_08-15-34_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
8417
walks/10-13-60-102_2026-03-06_08-15-34_walk_resolved.json
Normal file
8417
walks/10-13-60-102_2026-03-06_08-15-34_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
3796
walks/10-13-60-102_2026-03-06_08-15-34_walk_tables.csv
Normal file
3796
walks/10-13-60-102_2026-03-06_08-15-34_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
13960
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk.txt
Normal file
13960
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
10815
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_monitoring.json
Normal file
10815
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
13733
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_resolved.json
Normal file
13733
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
8345
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_tables.csv
Normal file
8345
walks/archive/10-10-14-65_2026-03-05_15-50-29_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
13960
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk.txt
Normal file
13960
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
10815
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_monitoring.json
Normal file
10815
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
13733
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_resolved.json
Normal file
13733
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
8345
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_tables.csv
Normal file
8345
walks/archive/10-10-14-65_2026-03-05_19-57-21_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
25212
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk.txt
Normal file
25212
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
10949
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_monitoring.json
Normal file
10949
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
25256
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_resolved.json
Normal file
25256
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
3796
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_tables.csv
Normal file
3796
walks/archive/10-13-60-102_2026-03-04_10-10-39_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
8384
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk.txt
Normal file
8384
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
5705
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_monitoring.json
Normal file
5705
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
8417
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_resolved.json
Normal file
8417
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
3796
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_tables.csv
Normal file
3796
walks/archive/10-13-60-102_2026-03-05_20-00-54_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
8384
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk.txt
Normal file
8384
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk.txt
Normal file
File diff suppressed because it is too large
Load Diff
5705
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_monitoring.json
Normal file
5705
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_monitoring.json
Normal file
File diff suppressed because it is too large
Load Diff
8417
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_resolved.json
Normal file
8417
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_resolved.json
Normal file
File diff suppressed because it is too large
Load Diff
3796
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_tables.csv
Normal file
3796
walks/archive/10-13-60-102_2026-03-05_20-53-05_walk_tables.csv
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user