Compare commits

...

5 Commits

Author SHA1 Message Date
sam
bcb179e7e4 Fix TruthValue handling for different firmware versions
Some Accedian firmware reports SNMP TruthValue fields as '1'/'2'
(INTEGER) while others use 'true'/'false' (textual). Add isTrue()
helper that accepts both formats and replace all 15+ boolean checks
across the viewer (present, diagCapable, active, enabled, filter
enable flags, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:30:36 -07:00
Sam Lewis
4daf26b778 Added walks folder 2026-03-06 08:22:22 -07:00
Sam Lewis
09a2db7373 added images 2026-03-06 08:02:33 -07:00
sam
df8c74627b Dynamic port rendering for multi-port NID devices
Replace hardcoded 4-port loops in renderPanel(), renderSfp(), and
renderLldp() with dynamic iteration over connectors data. Devices
like the AMO-10000-LT-S with more than 4 ports now render all
ports automatically. Management port detected by connector name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:45:34 -07:00
sam
d6bf394297 Add optional policy MIB toggle and pre-walk ping check
- Add SNMP_WALK_POLICIES env var and UI checkbox to skip ACD-POLICY-MIB
  (~73% of all OIDs), cutting walk time from ~25s to ~11s
- Add /api/ping endpoint with reachability check before walk starts
- Show "NID Management is UP" (green) or "NID is DOWN" (red) status
- Block walk if target is unreachable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:02:52 -07:00
34 changed files with 285086 additions and 264 deletions

View File

@ -22,5 +22,10 @@ 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

View File

@ -60,6 +60,8 @@ def build_html(data: dict) -> str:
<title>NID Viewer &mdash; {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;
@ -161,7 +163,7 @@ body {{
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 1.2rem;
gap: 1.5rem;
flex-wrap: wrap;
position: relative;
}}
@ -210,8 +212,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;
@ -349,6 +351,20 @@ 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;
@ -459,14 +475,21 @@ body {{
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.lldp-row {{
/* LLDP vertical columns */
.lldp-columns {{
display: flex;
align-items: center;
gap: 0;
margin-bottom: 0.75rem;
gap: 1rem;
align-items: stretch;
}}
.lldp-row:last-child {{ margin-bottom: 0; }}
.lldp-local-slot {{
.lldp-col {{
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
min-width: 0;
}}
.lldp-col.idle {{ opacity: 0.4; }}
.lldp-col-header {{
width: 56px;
height: 44px;
border-radius: 4px;
@ -479,147 +502,178 @@ body {{
font-weight: 600;
flex-shrink: 0;
}}
.lldp-local-slot.present-link {{
.lldp-col-header.present-link {{
background: rgba(34,197,94,0.15);
border-color: var(--green);
color: var(--green);
}}
.lldp-local-slot.present-nolink {{
.lldp-col-header.present-nolink {{
background: rgba(245,158,11,0.15);
border-color: var(--amber);
color: var(--amber);
}}
.lldp-local-slot.empty {{
.lldp-col-header.empty {{
background: #1a1d24;
border-color: #2d3340;
color: #555;
}}
.lldp-local-slot .slot-label {{
.lldp-col-header .slot-label {{
font-size: 0.6rem;
color: var(--text-muted);
}}
.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-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 {{
.lldp-col-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;
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;
border: 1px solid #3a3f4b;
border-radius: 4px;
padding: 0.5rem 0.75rem;
min-width: 200px;
max-width: 320px;
flex-shrink: 0;
width: 100%;
margin-top: 0.15rem;
flex: 1;
}}
.lldp-remote .remote-hostname {{
.lldp-col-remote .remote-hostname {{
font-size: 0.85rem;
font-weight: 700;
color: var(--text-main);
word-break: break-all;
}}
.lldp-remote .remote-model {{
.lldp-col-remote .remote-model {{
font-size: 0.72rem;
color: var(--cyan);
margin-bottom: 0.3rem;
}}
.lldp-remote .remote-detail {{
.lldp-col-remote .remote-detail {{
font-size: 0.7rem;
color: var(--text-muted);
margin: 0.1rem 0;
font-family: 'JetBrains Mono', monospace;
}}
.lldp-remote .remote-detail .rlabel {{
.lldp-col-remote .remote-detail .rlabel {{
color: #555;
display: inline-block;
min-width: 32px;
}}
.lldp-remote .remote-mgmt {{
.lldp-col-remote .remote-mgmt {{
font-size: 0.75rem;
color: var(--green);
font-weight: 600;
margin-top: 0.3rem;
font-family: 'JetBrains Mono', monospace;
}}
.lldp-row.idle {{
opacity: 0.4;
}}
.lldp-row.idle .lldp-connector {{
min-width: 40px;
}}
.lldp-idle-label {{
.lldp-col-idle-label {{
font-size: 0.7rem;
color: #555;
font-style: italic;
margin-top: 0.3rem;
}}
.topo-stats-table {{
width: 100%;
margin-top: 1rem;
.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 {{
font-size: 0.75rem;
}}
.topo-stats-table th {{
color: var(--text-muted);
font-weight: 500;
padding: 0.3rem 0.5rem;
border-bottom: 1px solid var(--border-color);
}}
.topo-stats-table td {{
padding: 0.3rem 0.5rem;
margin-top: 0.4rem;
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:1400px">
<div class="container-fluid py-3" style="max-width:1800px">
<!-- 0. WALK CONTROL -->
<div id="sec-walk"></div>
<!-- 1. DEVICE HEADER -->
<div id="sec-header"></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>
<!-- 2. FRONT PANEL -->
<div id="sec-panel"></div>
@ -630,20 +684,20 @@ body {{
<!-- 4. INTERFACES TABLE -->
<div id="sec-interfaces"></div>
<!-- 5. SFP CARDS -->
<div id="sec-sfp"></div>
<!-- 6. ALARMS -->
<div id="sec-alarms"></div>
<!-- 56. 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>
<!-- 7. TRAFFIC POLICIES -->
<div id="sec-policies"></div>
<!-- 8. L2 FILTERS -->
<div id="sec-filters"></div>
<!-- 9. REGULATORS -->
<div id="sec-regulators"></div>
<!-- 89. 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>
<!-- 10. COVERAGE MATRIX -->
<div id="sec-coverage"></div>
@ -680,6 +734,26 @@ 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;
}}
@ -775,6 +849,10 @@ 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>
@ -795,6 +873,7 @@ 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) {{
@ -803,55 +882,78 @@ function startWalk() {{
}}
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Walking...';
updateWalkStatus('running', 'Starting walk...');
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Pinging...';
updateWalkStatus('running', 'Checking reachability...');
// Close any previous SSE connection
if (walkEventSource) walkEventSource.close();
fetch('/api/walk', {{
// Step 1: Ping check
fetch('/api/ping', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ target, mode }})
body: JSON.stringify({{ target }})
}})
.then(r => r.json())
.then(resp => {{
if (resp.error) {{
updateWalkStatus('error', resp.error);
.then(ping => {{
if (!ping.reachable) {{
updateWalkStatus('error', 'NID is DOWN. Verify Local Power and Router Interface Status.');
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 + '%';
updateWalkStatus('complete', 'NID Management is UP');
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;
// 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);
resetWalkBtn();
}} 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
}};
}});
}}, 600);
}})
.catch(err => {{
updateWalkStatus('error', 'Failed to connect: ' + err.message);
updateWalkStatus('error', 'Ping check failed: ' + err.message);
resetWalkBtn();
}});
}}
@ -891,7 +993,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 (a.active === '1') {{
if (isTrue(a.active)) {{
activeCount++;
const cfg = cfgByNum[a.number];
if (cfg) sevCounts[cfg.severity] = (sevCounts[cfg.severity]||0) + 1;
@ -983,6 +1085,40 @@ 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <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 || {{}};
@ -1004,7 +1140,7 @@ function renderPanel() {{
const iface = ifaces[connIdx];
if (connType(connIdx) === 'sfp') {{
const sfp = sfpInfo[connIdx];
const present = sfp && sfp.present === '1';
const present = sfp && isTrue(sfp.present);
if (!present) return 'empty';
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
return 'present-nolink';
@ -1019,7 +1155,7 @@ function renderPanel() {{
function sfpLabel(connIdx) {{
const sfp = sfpInfo[connIdx];
if (!sfp) return '';
if (sfp.present !== '1') return 'EMPTY';
if (!isTrue(sfp.present)) return 'EMPTY';
const pn = sfp.vendorPn || '';
if (pn.length > 8) return pn.substring(0,8);
return pn || sfp.vendor || '';
@ -1034,15 +1170,15 @@ function renderPanel() {{
return {{ label, alias }};
}}
// Build port slots dynamically from connectors 1-4
// Build port slots dynamically from connectors
const {{ dataPorts, mgmtPort }} = getPortLists();
let slots = '';
for (let i = 1; i <= 4; i++) {{
const idx = String(i);
for (const idx of dataPorts) {{
const conn = connectors[idx] || {{}};
const isSfp = connType(idx) === 'sfp';
const state = slotState(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;
if (isSfp) {{
@ -1062,7 +1198,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="${{i}}" onclick="selectSfp(${{i}})">
<div class="sfp-slot ${{state}}" data-sfp="${{idx}}" onclick="selectSfp(${{idx}})">
${{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>
@ -1071,7 +1207,7 @@ function renderPanel() {{
}}
// Management port
const mgmtIf = ifaces['5'];
const mgmtIf = mgmtPort ? ifaces[mgmtPort] : null;
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>
@ -1081,7 +1217,7 @@ function renderPanel() {{
// Power feeds
let pwrHtml = '<div class="pwr-block">';
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>';
@ -1195,27 +1331,27 @@ function renderSfp() {{
const sfpDiag = DATA.sfp_diagnostics || {{}};
const sfpThresh = DATA.sfp_thresholds || {{}};
const {{ dataPorts }} = getPortLists();
let cards = '';
for (let i = 1; i <= 4; i++) {{
const si = String(i);
for (const si of dataPorts) {{
const info = sfpInfo[si];
const diag = sfpDiag[si] || {{}};
const thresh = sfpThresh[si] || {{}};
const conn = connectors[si] || {{}};
if (!info || info.present !== '1') {{
cards += `<div class="sfp-detail" id="sfp-detail-${{i}}">
if (!info || !isTrue(info.present)) {{
cards += `<div class="sfp-detail" id="sfp-detail-${{si}}">
<div class="card-dark" style="border-left:3px solid #555">
<div class="card-body" style="text-align:center;color:#555;padding:2rem">
<i class="bi bi-slash-circle" style="font-size:2rem"></i>
<div>SFP-${{i}} &mdash; Not Present</div>
<div>${{esc(conn.name || 'Port ' + si)}} &mdash; Not Present</div>
</div>
</div>
</div>`;
continue;
}}
const ddm = info.diagCapable === '1';
const ddm = isTrue(info.diagCapable);
const ddmBadge = ddm
? '<span class="badge bg-success">DDM 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 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-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>
</div>
<div class="card-body">
@ -1272,8 +1408,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>${{info.internalCal==='1' ? 'Yes' : 'No'}}</strong></div>
<div>Alarm Capable: <strong>${{info.alarmCapable==='1' ? '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>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>
@ -1293,7 +1429,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:
${{[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>
${{cards}}
</div>
@ -1314,7 +1450,7 @@ function renderAlarms() {{
// Collect active alarms (when status table available)
const active = [];
for (const [k,a] of Object.entries(alarmStatus)) {{
if (a.active === '1') {{
if (isTrue(a.active)) {{
const cfg = cfgByNum[a.number] || {{}};
active.push({{ ...a, ...cfg, _statusId: k }});
}}
@ -1356,7 +1492,7 @@ function renderAlarms() {{
<td>${{esc(c.description)}}</td>`;
if (hasSeverity) rows += `
<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 += `
<td class="mono">${{esc(c.conditionType)}}</td>
<td class="mono">${{esc(c.amoType)}}</td>`;
@ -1503,12 +1639,12 @@ function renderFilters() {{
let rows = '';
for (const [id, f] of Object.entries(filters)) {{
const conditions = [];
if (f.macDstEn === '1') conditions.push('MAC Dst: ' + esc(f.macDst));
if (f.macSrcEn === '1') conditions.push('MAC Src: ' + esc(f.macSrc));
if (f.etypeEn === '1') conditions.push('EType: ' + esc(f.etype));
if (f.vlan1IdEn === '1') conditions.push('VLAN1: ' + esc(f.vlan1Id));
if (f.vlan2IdEn === '1') conditions.push('VLAN2: ' + esc(f.vlan2Id));
if (f.vlan1PriorEn === '1') conditions.push('PCP1: ' + esc(f.vlan1Prior));
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));
const condStr = conditions.length ? conditions.join(', ') : '<span class="status-na">any (catchall)</span>';
rows += `<tr>
@ -1652,7 +1788,6 @@ 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 || {{}};
@ -1664,14 +1799,13 @@ 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 && sfp.present === '1';
const present = sfp && isTrue(sfp.present);
if (!present) return 'empty';
if (iface && isUp(iface.ifOperStatus)) return 'present-link';
return 'present-nolink';
@ -1682,137 +1816,99 @@ function renderLldp() {{
}}
}}
// Build a row for each network port (1-4)
let rowsHtml = '';
for (let i = 1; i <= 4; i++) {{
const portKey = String(i);
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) {{
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-${{i}}` : `RJ45-${{i}}`);
const slotName = conn.name || (isSfp ? `SFP-${{portKey}}` : `RJ45-${{portKey}}`);
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';
rowsHtml += `
<div class="lldp-row">
<div class="lldp-local-slot ${{state}}">
colsHtml += `
<div class="lldp-col">
<div class="lldp-col-header ${{state}}">
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
</div>
<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 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>`;
}} else {{
rowsHtml += `
<div class="lldp-row idle">
<div class="lldp-local-slot ${{state}}">
colsHtml += `
<div class="lldp-col idle">
<div class="lldp-col-header ${{state}}">
${{icon}}<span class="slot-label">${{esc(slotName)}}</span>
</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 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>`;
}}
}}
// Include MGMT port if it has a neighbor
const mgmtNbr = neighborByPort['5'];
// MGMT column
const mgmtNbr = mgmtPort ? neighborByPort[mgmtPort] : null;
if (mgmtNbr) {{
const mgmtIface = ifaces['5'] || {{}};
const mgmtIface = ifaces[mgmtPort] || {{}};
const mgmtUp = isUp(mgmtIface.ifOperStatus);
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">
colsHtml += `<div class="lldp-col-divider"></div>`;
colsHtml += `
<div class="lldp-col">
<div class="lldp-col-header ${{mgmtUp ? 'present-link' : 'present-nolink'}}">
<i class="bi bi-ethernet"></i><span class="slot-label">MGMT</span>
</div>
<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 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>`;
}}
// 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)}} &mdash; ${{esc(localModel)}}</span>
${{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 class="lldp-columns">
${{colsHtml}}
</div>
` : ''}}
</div>
</div>
</div>`;
}}
@ -1912,7 +2008,7 @@ function renderCoverage() {{
// Check SFP DDM
const sfpInfo = DATA.sfp_info || {{}};
for (const [k,s] of Object.entries(sfpInfo)) {{
if (s.present === '1' && s.diagCapable !== '1') {{
if (isTrue(s.present) && !isTrue(s.diagCapable)) {{
gaps.push({{
type: 'red',
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
renderWalkControl();
renderHeader();
renderMap();
renderPanel();
renderLldp();
renderInterfaces();

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -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.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"),
@ -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_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()
@ -125,7 +128,7 @@ def build_snmp_auth() -> list:
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."""
global latest_json
@ -142,6 +145,11 @@ def run_walk(target: str, mode: str):
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
@ -157,7 +165,7 @@ def run_walk(target: str, mode: str):
walk_file.write_text(result.stdout)
else:
# Walk subtrees in parallel for speed
total = len(TARGETED_OIDS)
total = len(walk_oids)
completed = [0] # mutable counter for progress
results_map = {}
@ -175,7 +183,7 @@ def run_walk(target: str, mode: str):
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [
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):
idx, output = fut.result()
@ -309,11 +317,39 @@ 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()
@ -325,6 +361,7 @@ 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"})
@ -334,7 +371,7 @@ class NIDHandler(BaseHTTPRequestHandler):
return
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()
self._send_json(200, {"status": "started", "target": target, "mode": mode})

View File

@ -75,6 +75,8 @@ 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)
@ -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.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