@ -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 : 1 rem 1.5 rem ;
display : flex ;
align - items : center ;
gap : 1. 2 rem ;
gap : 1. 5 rem ;
flex - wrap : wrap ;
position : relative ;
} }
@ -210,8 +212,8 @@ body {{
max - width : 80 px ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
line - height : 1.2 ;
min - height : 1.8 em ;
} }
. mgmt - port { {
width : 40 px ;
@ -349,6 +351,20 @@ body {{
font - size : 0.85 rem ;
} }
. walk - select : focus { { outline : none ; border - color : var ( - - accent ) ; } }
. walk - toggle { {
display : flex ;
align - items : center ;
gap : 0.35 rem ;
font - size : 0.8 rem ;
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.05 em ;
} }
. lldp - row { {
/ * ─ ─ LLDP vertical columns ─ ─ * /
. lldp - columns { {
display : flex ;
align - items : center ;
gap : 0 ;
margin - bottom : 0.75 rem ;
gap : 1 rem ;
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 : 56 px ;
height : 44 px ;
border - radius : 4 px ;
@ -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.6 rem ;
color : var ( - - text - muted ) ;
} }
. lldp - connector { {
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
min - width : 80 px ;
max - width : 200 px ;
flex : 1 ;
padding : 0 0.4 rem ;
} }
. lldp - connector . link - line { {
width : 100 % ;
height : 3 px ;
border - radius : 2 px ;
position : relative ;
} }
. lldp - connector . link - line . up { {
background : var ( - - green ) ;
box - shadow : 0 0 8 px rgba ( 34 , 197 , 94 , 0.3 ) ;
} }
. lldp - connector . link - line . down { {
background : var ( - - amber ) ;
box - shadow : 0 0 8 px rgba ( 245 , 158 , 11 , 0.3 ) ;
} }
. lldp - connector . link - line : : before ,
. lldp - connector . link - line : : after { {
content : ' ' ;
position : absolute ;
top : 50 % ;
width : 8 px ;
height : 8 px ;
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 : - 4 px ; } }
. lldp - connector . link - line : : after { { right : - 4 px ; } }
. lldp - connector . link - port - label { {
. lldp - col - port - label { {
font - size : 0.6 rem ;
color : var ( - - text - muted ) ;
text - align : center ;
white - space : nowrap ;
font - family : ' JetBrains Mono ' , monospace ;
margin : 0.2 rem 0 ;
margin : 0.3 rem 0 0.15 rem ;
white - space : nowrap ;
overflow : hidden ;
text - overflow : ellipsis ;
max - width : 100 % ;
} }
. lldp - remote { {
. lldp - col - line { {
width : 3 px ;
height : 36 px ;
border - radius : 2 px ;
margin : 0.15 rem 0 ;
position : relative ;
} }
. lldp - col - line . up { {
background : var ( - - green ) ;
box - shadow : 0 0 8 px rgba ( 34 , 197 , 94 , 0.3 ) ;
} }
. lldp - col - line . down { {
background : var ( - - amber ) ;
box - shadow : 0 0 8 px rgba ( 245 , 158 , 11 , 0.3 ) ;
} }
. lldp - col - line . idle { {
background : #2d3340;
height : 24 px ;
opacity : 0.5 ;
} }
. lldp - col - line : : before ,
. lldp - col - line : : after { {
content : ' ' ;
position : absolute ;
left : 50 % ;
width : 8 px ;
height : 8 px ;
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 : - 4 px ; } }
. lldp - col - line : : after { { bottom : - 4 px ; } }
. lldp - col - line . idle : : before ,
. lldp - col - line . idle : : after { { display : none ; } }
. lldp - col - remote { {
background : #1e2128;
border : 1 px solid #3a3f4b;
border - radius : 4 px ;
padding : 0.5 rem 0.75 rem ;
min - width : 200 px ;
max - width : 320 px ;
flex - shrink : 0 ;
width : 100 % ;
margin - top : 0.15 rem ;
flex : 1 ;
} }
. lldp - remote . remote - hostname { {
. lldp - col- remote . remote - hostname { {
font - size : 0.85 rem ;
font - weight : 700 ;
color : var ( - - text - main ) ;
word - break : break - all ;
} }
. lldp - remote . remote - model { {
. lldp - col- remote . remote - model { {
font - size : 0.72 rem ;
color : var ( - - cyan ) ;
margin - bottom : 0.3 rem ;
} }
. lldp - remote . remote - detail { {
. lldp - col- remote . remote - detail { {
font - size : 0.7 rem ;
color : var ( - - text - muted ) ;
margin : 0.1 rem 0 ;
font - family : ' JetBrains Mono ' , monospace ;
} }
. lldp - remote . remote - detail . rlabel { {
. lldp - col- remote . remote - detail . rlabel { {
color : #555;
display : inline - block ;
min - width : 32 px ;
} }
. lldp - remote . remote - mgmt { {
. lldp - col- remote . remote - mgmt { {
font - size : 0.75 rem ;
color : var ( - - green ) ;
font - weight : 600 ;
margin - top : 0.3 rem ;
font - family : ' JetBrains Mono ' , monospace ;
} }
. lldp - row . idle { {
opacity : 0.4 ;
} }
. lldp - row . idle . lldp - connector { {
min - width : 40 px ;
} }
. lldp - idle - label { {
. lldp - col - idle - label { {
font - size : 0.7 rem ;
color : #555;
font - style : italic ;
margin - top : 0.3 rem ;
} }
. topo - stats - table { {
width : 100 % ;
margin - top : 1 rem ;
. lldp - col - divider { {
width : 1 px ;
align - self : stretch ;
border - left : 1 px dashed #3a3f4b;
margin : 0 0.25 rem ;
} }
/ * ─ ─ 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 : 400 px ;
border - radius : 4 px ;
background : var ( - - bg - dark ) ;
} }
. leaflet - container { {
background : var ( - - bg - dark ) ! important ;
} }
. map - coords { {
font - size : 0.75 rem ;
} }
. topo - stats - table th { {
color : var ( - - text - muted ) ;
font - weight : 500 ;
padding : 0.3 rem 0.5 rem ;
border - bottom : 1 px solid var ( - - border - color ) ;
} }
. topo - stats - table td { {
padding : 0.3 rem 0.5 rem ;
margin - top : 0.4 rem ;
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:1 8 00px" >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 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 >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 5 – 6. SFP + ALARMS ( side by side ) ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div style = " display:flex;gap:1rem;align-items:stretch " >
< div id = " sec-sfp " style = " flex:1;min-width:0;display:flex;flex-direction:column " > < / div >
< div id = " sec-alarms " style = " flex:1;min-width:0;display:flex;flex-direction:column " > < / div >
< / div >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 7. TRAFFIC POLICIES ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div id = " sec-policies " > < / div >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 8. L2 FILTERS ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div id = " sec-filters " > < / div >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 9. REGULATORS ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div id = " sec-regulators " > < / div >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 8 – 9. L2 FILTERS + REGULATORS ( side by side ) ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div style = " display:flex;gap:1rem;align-items:stretch " >
< div id = " sec-filters " style = " flex:1;min-width:0;display:flex;flex-direction:column " > < / div >
< div id = " sec-regulators " style = " flex:1;min-width:0;display:flex;flex-direction:column " > < / div >
< / div >
< ! - - ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ 10. COVERAGE MATRIX ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ - - >
< div id = " sec-coverage " > < / div >
@ -775,6 +829,10 @@ function renderWalkControl() {{
< option value = " targeted " > Targeted < / option >
< option value = " full " > Full < / option >
< / select >
< label class = " walk-toggle " title = " ACD-POLICY-MIB is ~73 % o f 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 +853,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,6 +862,26 @@ function startWalk() {{
} }
btn . disabled = true ;
btn . innerHTML = ' <i class= " bi bi-hourglass-split " ></i> Pinging... ' ;
updateWalkStatus ( ' running ' , ' Checking reachability... ' ) ;
/ / Step 1 : Ping check
fetch ( ' /api/ping ' , { {
method : ' POST ' ,
headers : { { ' Content-Type ' : ' application/json ' } } ,
body : JSON . stringify ( { { target } } )
} } )
. then ( r = > r . json ( ) )
. then ( ping = > { {
if ( ! ping . reachable ) { {
updateWalkStatus ( ' error ' , ' NID is DOWN. Verify Local Power and Router Interface Status. ' ) ;
resetWalkBtn ( ) ;
return ;
} }
updateWalkStatus ( ' complete ' , ' NID Management is UP ' ) ;
/ / 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... ' ) ;
@ -812,7 +891,7 @@ function startWalk() {{
fetch ( ' /api/walk ' , { {
method : ' POST ' ,
headers : { { ' Content-Type ' : ' application/json ' } } ,
body : JSON . stringify ( { { target , mode } } )
body : JSON . stringify ( { { target , mode , policies } } )
} } )
. then ( r = > r . json ( ) )
. then ( resp = > { {
@ -832,7 +911,6 @@ function startWalk() {{
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 ) ;
@ -846,14 +924,18 @@ function startWalk() {{
walkEventSource . onerror = ( ) = > { {
walkEventSource . close ( ) ;
walkEventSource = null ;
/ / SSE closed — could be server - side close after complete / error
/ / Check if we already handled it ; if not , it was an unexpected close
} } ;
} } )
. catch ( err = > { {
updateWalkStatus ( ' error ' , ' Failed to connect: ' + err . message ) ;
resetWalkBtn ( ) ;
} } ) ;
} } , 600 ) ;
} } )
. catch ( err = > { {
updateWalkStatus ( ' error ' , ' Ping check failed: ' + err . message ) ;
resetWalkBtn ( ) ;
} } ) ;
} }
function updateWalkStatus ( state , message ) { {
@ -983,6 +1065,40 @@ function renderHeader() {{
< / div > ` ;
} }
/ / ─ ─ 1 b . Location Map ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
function renderMap ( ) { {
const d = DATA . device | | { { } } ;
const loc = ( d . sysLocation | | ' ' ) . trim ( ) ;
const m = loc . match ( / ^ ( - ? \d + \. ? \d * ) \s * , \s * ( - ? \d + \. ? \d * ) $ / ) ;
if ( ! m ) return ; / / no valid coordinates — skip map
const lat = parseFloat ( m [ 1 ] ) ;
const lon = parseFloat ( m [ 2 ] ) ;
const hostname = d . sysName | | d . identifier | | ' NID ' ;
document . getElementById ( ' sec-map ' ) . innerHTML = `
< div class = " card-dark " >
< div class = " card-header collapsible " > < i class = " bi bi-geo-alt " > < / i > Location Map < i class = " bi bi-chevron-down collapse-chevron " > < / i > < / div >
< div class = " card-body " >
< div id = " nid-map " > < / div >
< div class = " map-coords " > < i class = " bi bi-crosshair " > < / i > $ { { lat . toFixed ( 6 ) } } , $ { { lon . toFixed ( 6 ) } } < / div >
< / div >
< / div > ` ;
const map = L . map ( ' nid-map ' ) . setView ( [ lat , lon ] , 15 ) ;
L . tileLayer ( ' https:// {{ s}}.basemaps.cartocdn.com/dark_all/ {{ z}}/ {{ x}}/ {{ y}} {{ r}}.png ' , { {
attribution : ' © <a href= " https://www.openstreetmap.org/copyright " >OSM</a> © <a href= " https://carto.com/ " >CARTO</a> ' ,
subdomains : ' abcd ' ,
maxZoom : 19
} } ) . addTo ( map ) ;
L . marker ( [ lat , lon ] ) . addTo ( map )
. bindPopup ( ` < b > $ { { esc ( hostname ) } } < / b > < br > $ { { lat . toFixed ( 6 ) } } , $ { { lon . toFixed ( 6 ) } } ` )
. openPopup ( ) ;
/ / Leaflet needs a resize nudge when rendered in a hidden / collapsed container
setTimeout ( ( ) = > map . invalidateSize ( ) , 200 ) ;
} }
/ / ─ ─ 2. Front Panel ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
function renderPanel ( ) { {
const connectors = DATA . connectors | | { { } } ;
@ -1652,7 +1768,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,7 +1779,6 @@ 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 ' ;
@ -1682,40 +1796,11 @@ function renderLldp() {{
} }
} }
/ / Build a row for each network port ( 1 - 4 )
let rowsHtml = ' ' ;
for ( let i = 1 ; i < = 4 ; i + + ) { {
const portKey = String ( i ) ;
const conn = connectors [ portKey ] | | { { } } ;
const isSfp = conn . type == = ' 14 ' ;
const nbr = neighborByPort [ portKey ] ;
const state = slotState ( portKey ) ;
const iface = ifaces [ portKey ] | | { { } } ;
const localUp = isUp ( iface . ifOperStatus ) ;
const slotName = conn . name | | ( isSfp ? ` SFP - $ { { i } } ` : ` RJ45 - $ { { i } } ` ) ;
const icon = isSfp
? ( state == = ' empty ' ? ' <i class= " bi bi-dash " ></i> ' :
state == = ' present-link ' ? ' <i class= " bi bi-arrow-left-right " ></i> ' :
' <i class= " bi bi-plug " ></i> ' )
: ' <i class= " bi bi-ethernet " ></i> ' ;
if ( nbr ) { {
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 ;
const linkClass = localUp ? ' up ' : ' down ' ;
rowsHtml + = `
< div class = " lldp-row " >
< div class = " lldp-local-slot $ {{ 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 " >
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 > ` : ' ' } }
@ -1730,89 +1815,80 @@ function renderLldp() {{
nbr . capsEnabled == = ' 4 ' ? ' <span style= " color:var(--cyan) " >Router</span> ' :
' Cap= ' + ( nbr . capsEnabled | | ' ? ' ) } }
< / div >
< / div >
< / div > ` ;
} } else { {
rowsHtml + = `
< div class = " lldp-row idle " >
< div class = " lldp-local-slot $ {{ state}} " >
} }
/ / Build vertical columns for ports 1 - 4
let colsHtml = ' ' ;
for ( let i = 1 ; i < = 4 ; i + + ) { {
const portKey = String ( i ) ;
const conn = connectors [ portKey ] | | { { } } ;
const isSfp = conn . type == = ' 14 ' ;
const nbr = neighborByPort [ portKey ] ;
const state = slotState ( portKey ) ;
const iface = ifaces [ portKey ] | | { { } } ;
const localUp = isUp ( iface . ifOperStatus ) ;
const slotName = conn . name | | ( isSfp ? ` SFP - $ { { i } } ` : ` RJ45 - $ { { i } } ` ) ;
const icon = isSfp
? ( state == = ' empty ' ? ' <i class= " bi bi-dash " ></i> ' :
state == = ' present-link ' ? ' <i class= " bi bi-arrow-left-right " ></i> ' :
' <i class= " bi bi-plug " ></i> ' )
: ' <i class= " bi bi-ethernet " ></i> ' ;
const portLabel = iface . ifName | | ' Port ' + i ;
if ( nbr ) { {
const linkClass = localUp ? ' up ' : ' down ' ;
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-line " style = " background:#2d3340;height:2px;opacity:0.5 " > < / 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 { {
colsHtml + = `
< div class = " lldp-col idle " >
< div class = " lldp-col-header $ {{ state}} " >
$ { { icon } } < span class = " slot-label " > $ { { esc ( slotName ) } } < / span >
< / 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
/ / MGMT column ( port 5 )
const mgmtNbr = neighborByPort [ ' 5 ' ] ;
if ( mgmtNbr ) { {
const mgmtIface = ifaces [ ' 5 ' ] | | { { } } ;
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 class = " lldp-columns " >
$ { { colsHtml } }
< / div >
$ { { Object . keys ( stats ) . length ? `
< div style = " margin-top:1rem " >
< h6 style = " font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem " >
< i class = " bi bi-bar-chart " > < / i > Per - Port LLDP Statistics
< / h6 >
< table class = " topo-stats-table " >
< thead > < tr >
< th > Port < / th > < th > Interface < / th > < th style = " text-align:right " > TX Frames < / th >
< th style = " text-align:right " > RX Frames < / th > < th style = " text-align:center " > Neighbors < / th >
< / tr > < / thead >
< tbody > $ { { statsRows } } < / tbody >
< / table >
< / div >
` : ' ' } }
< / div >
< / div > ` ;
} }
@ -2031,6 +2107,7 @@ function renderPortCmp() {{
/ / ─ ─ Render all sections ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
renderWalkControl ( ) ;
renderHeader ( ) ;
renderMap ( ) ;
renderPanel ( ) ;
renderLldp ( ) ;
renderInterfaces ( ) ;