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>
This commit is contained in:
sam 2026-03-05 19:45:34 -07:00
parent d6bf394297
commit df8c74627b

View File

@ -734,6 +734,23 @@ function formatUptime(sec) {{
p.push(s+'s'); p.push(s+'s');
return p.join(' '); return p.join(' ');
}} }}
// 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;
}} }}
@ -1150,15 +1167,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) {{
@ -1178,7 +1195,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>
@ -1187,7 +1204,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>
@ -1311,20 +1328,20 @@ 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 || info.present !== '1') {{
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}} &mdash; Not Present</div> <div>${{esc(conn.name || 'Port ' + si)}} &mdash; Not Present</div>
</div> </div>
</div> </div>
</div>`; </div>`;
@ -1365,10 +1382,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">
@ -1409,7 +1426,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>
@ -1818,23 +1835,23 @@ function renderLldp() {{
</div>`; </div>`;
}} }}
// Build vertical columns for ports 1-4 // Build vertical columns for data ports
const {{ dataPorts, mgmtPort }} = getPortLists();
let colsHtml = ''; let colsHtml = '';
for (let i = 1; i <= 4; i++) {{ for (const portKey of dataPorts) {{
const portKey = String(i);
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 ' + i; const portLabel = iface.ifName || 'Port ' + portKey;
if (nbr) {{ if (nbr) {{
const linkClass = localUp ? 'up' : 'down'; const linkClass = localUp ? 'up' : 'down';
@ -1861,10 +1878,10 @@ function renderLldp() {{
}} }}
}} }}
// MGMT column (port 5) // 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);
colsHtml += `<div class="lldp-col-divider"></div>`; colsHtml += `<div class="lldp-col-divider"></div>`;
colsHtml += ` colsHtml += `