Redesign topology diagram with dual-homed bus layout and improve inspector chassis
- Replace flat topology with tiered bus-bar layout: Internet → UDM-Pro → SVG fork → USW-Agg + Pro 24 PoE → dual-homed servers - Show 10G VLAN90 (Ceph) bus from USW-Agg and 1G DHCP management bus from Pro 24 PoE per host - Add per-host drop wires (solid 10G + dashed 1G) with correct rack positions - Mark large1 as off-rack (dashed border), ZimaBoards as off-rack mon-01/mon-02 - Add topology legend, inter-switch 10G ISL indicator - Add recently resolved events section (last 24h) to dashboard - Add last_seen column and relative timestamps to events table - Add stale data banner when monitoring data >15 min old - Improve inspector chassis with port speed labels, LLDP neighbor info, mounting ears, chassis legend - Add duplex/speed mismatch warnings and carrier changes to path debug panel - Bump updateTopology() to handle both topo-v2-status-* and topo-status-* classes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,96 +29,149 @@
|
||||
</div>
|
||||
|
||||
<div class="topology" id="topology-diagram">
|
||||
<div class="topo-v2">
|
||||
|
||||
<!-- ── Tier 1: Internet ───────────────────────── -->
|
||||
<div class="topo-row">
|
||||
<div class="topo-node topo-internet">
|
||||
<span class="topo-icon">◈</span>
|
||||
<span class="topo-label">Internet</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topo-connectors single">
|
||||
<div class="topo-line topo-line-labeled" data-link-label="WAN 10G SFP+"></div>
|
||||
</div>
|
||||
{%- set topo_h = snapshot.hosts if snapshot.hosts else {} -%}
|
||||
|
||||
<!-- ── Tier 2: Router ─────────────────────────── -->
|
||||
<div class="topo-row">
|
||||
<div class="topo-node topo-unifi">
|
||||
<span class="topo-icon">⬡</span>
|
||||
<span class="topo-label">UDM-Pro</span>
|
||||
<span class="topo-node-sub">Dream Machine Pro · RU24</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topo-connectors single">
|
||||
<div class="topo-line topo-line-labeled" data-link-label="10G DAC"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Tier 3: Switches (Agg + PoE side by side) ─ -->
|
||||
<div class="topo-row">
|
||||
<div class="topo-switch-tier">
|
||||
<div class="topo-node topo-switch" id="topo-switch-agg">
|
||||
<span class="topo-icon">⬡</span>
|
||||
<span class="topo-label">USW-Agg</span>
|
||||
<span class="topo-node-sub">8×10G SFP+ · RU22</span>
|
||||
<span class="topo-node-sub topo-vlan-tag">VLAN90 · 10.10.90.x</span>
|
||||
</div>
|
||||
<div class="topo-h-link">
|
||||
<div class="topo-h-link-line"></div>
|
||||
<span class="topo-h-link-label">10G SFP+</span>
|
||||
</div>
|
||||
<div class="topo-node topo-switch" id="topo-switch-poe">
|
||||
<span class="topo-icon">⬡</span>
|
||||
<span class="topo-label">Pro 24 PoE</span>
|
||||
<span class="topo-node-sub">24×1G PoE · RU23</span>
|
||||
<span class="topo-node-sub topo-vlan-tag">DHCP mgmt</span>
|
||||
</div>
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
TIER 1: Internet (WAN edge)
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="topo-tier">
|
||||
<div class="topo-v2-node topo-v2-internet">
|
||||
<span class="topo-v2-icon">◈</span>
|
||||
<span class="topo-v2-label">INTERNET</span>
|
||||
<span class="topo-v2-sub">WAN uplink</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Tier 4: Hosts (all dual-homed 10G + 1G) ── -->
|
||||
<div class="topo-host-tier">
|
||||
<div class="topo-host-group">
|
||||
<!-- WAN wire: cyan → green gradient, labeled -->
|
||||
<div class="topo-vc">
|
||||
<div class="topo-vc-wire" style="background:linear-gradient(to bottom,var(--cyan),var(--cyan)); opacity:.55;"></div>
|
||||
<span class="topo-vc-label">WAN · 10G SFP+</span>
|
||||
</div>
|
||||
|
||||
<!-- 10G static VLAN90 lines from Agg (primary / Ceph) -->
|
||||
<div class="topo-connectors" style="gap:20px; justify-content:center">
|
||||
<div class="topo-line topo-line-labeled" data-link-label="10G SFP+"></div>
|
||||
<div class="topo-line"></div>
|
||||
<div class="topo-line"></div>
|
||||
<div class="topo-line"></div>
|
||||
<div class="topo-line"></div>
|
||||
<div class="topo-line topo-line-dashed"></div>
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
TIER 2: Router – UDM-Pro
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="topo-tier">
|
||||
<div class="topo-v2-node topo-v2-router">
|
||||
<span class="topo-v2-icon">⬡</span>
|
||||
<span class="topo-v2-label">UDM-Pro</span>
|
||||
<span class="topo-v2-sub">Dream Machine Pro</span>
|
||||
<span class="topo-v2-sub">RU24</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fork: UDM-Pro → two switches via SVG tree -->
|
||||
<div class="topo-vc" style="height:56px; overflow:visible;">
|
||||
<svg viewBox="0 0 400 56" preserveAspectRatio="none" style="width:100%;height:56px;overflow:visible;display:block;">
|
||||
<!-- stem down from router -->
|
||||
<line x1="200" y1="0" x2="200" y2="24" stroke="var(--green)" stroke-width="2" opacity=".6"/>
|
||||
<!-- horizontal branch -->
|
||||
<line x1="108" y1="24" x2="292" y2="24" stroke="var(--amber)" stroke-width="2" opacity=".5"/>
|
||||
<!-- drop to Agg switch (left) -->
|
||||
<line x1="108" y1="24" x2="108" y2="56" stroke="var(--amber)" stroke-width="2" opacity=".55"/>
|
||||
<!-- drop to PoE switch (right) -->
|
||||
<line x1="292" y1="24" x2="292" y2="56" stroke="var(--amber)" stroke-width="2" opacity=".55"/>
|
||||
<!-- DAC label -->
|
||||
<text x="204" y="21" fill="var(--amber)" font-size="9" font-family="monospace" opacity=".8">10G DAC</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
TIER 3: Switches (Agg + PoE)
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="topo-tier">
|
||||
<div class="topo-switch-pair">
|
||||
|
||||
<!-- USW-Aggregation -->
|
||||
<div class="topo-v2-node topo-v2-switch" id="topo-switch-agg" style="min-width:130px;">
|
||||
<span class="topo-v2-icon">⬡</span>
|
||||
<span class="topo-v2-label">USW-Agg</span>
|
||||
<span class="topo-v2-sub">Aggregation · RU22</span>
|
||||
<span class="topo-v2-sub">8 × 10G SFP+</span>
|
||||
<span class="topo-v2-vlan">VLAN90 · 10.10.90.x/24</span>
|
||||
</div>
|
||||
|
||||
<!-- 1G management lines from PoE (dashed amber) -->
|
||||
<!-- 1G DHCP management band from PoE switch -->
|
||||
<div class="topo-mgmt-band">
|
||||
<span class="topo-mgmt-label">← 1G DHCP mgmt (Pro 24 PoE) →</span>
|
||||
<div class="topo-mgmt-line"></div>
|
||||
<!-- Inter-switch link -->
|
||||
<div class="topo-isl">
|
||||
<div class="topo-isl-wire"></div>
|
||||
<span class="topo-isl-label">10G SFP+</span>
|
||||
</div>
|
||||
|
||||
<div class="topo-row topo-hosts-row">
|
||||
{%- set topo_h = snapshot.hosts if snapshot.hosts else {} -%}
|
||||
{%- set all_defs = [
|
||||
('compute-storage-gpu-01', 'csg-01', 'RU4–12 · VLAN90', False),
|
||||
('compute-storage-01', 'cs-01', 'RU14–17 · VLAN90', False),
|
||||
('storage-01', 'storage-01','rack · VLAN90', False),
|
||||
('monitor-01', 'monitor-01','ZimaBoard · VLAN90', False),
|
||||
('monitor-02', 'monitor-02','ZimaBoard · VLAN90', False),
|
||||
('large1', 'large1', 'table · VLAN90', True),
|
||||
] -%}
|
||||
{%- for hname, hlabel, hsub, off_rack in all_defs -%}
|
||||
{%- set st = topo_h[hname].status if hname in topo_h else 'unknown' -%}
|
||||
<div class="topo-node topo-host{{ ' topo-host-table' if off_rack else '' }} topo-status-{{ st }}" data-host="{{ hname }}">
|
||||
<span class="topo-icon">▣</span>
|
||||
<span class="topo-label">{{ hlabel }}</span>
|
||||
<span class="topo-node-sub">{{ hsub }}</span>
|
||||
<!-- Pro 24 PoE -->
|
||||
<div class="topo-v2-node topo-v2-switch" id="topo-switch-poe" style="min-width:130px;">
|
||||
<span class="topo-v2-icon">⬡</span>
|
||||
<span class="topo-v2-label">Pro 24 PoE</span>
|
||||
<span class="topo-v2-sub">24-Port · RU23</span>
|
||||
<span class="topo-v2-sub">24 × 1G PoE</span>
|
||||
<span class="topo-v2-vlan">DHCP · mgmt</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
TIER 4 connecting bus – two rails (10G green + 1G amber dashed)
|
||||
showing dual-homing for all 6 servers
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div class="topo-bus-section" style="max-width:860px;">
|
||||
|
||||
<!-- 10G storage bus (Agg → VLAN90) -->
|
||||
<div class="topo-bus-10g">
|
||||
<span class="topo-bus-10g-label">← USW-Agg · 10G SFP+ · VLAN90 →</span>
|
||||
<div class="topo-bus-10g-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- 1G management bus (PoE → DHCP) -->
|
||||
<div class="topo-bus-1g">
|
||||
<span class="topo-bus-1g-label">← Pro 24 PoE · 1G · DHCP mgmt →</span>
|
||||
<div class="topo-bus-1g-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Host nodes with drop wires ── -->
|
||||
<div class="topo-v2-hosts">
|
||||
{%- set all_defs = [
|
||||
('compute-storage-gpu-01', 'csg-01', 'RU4–12', 'Ceph · VLAN90', False),
|
||||
('compute-storage-01', 'cs-01', 'RU14–17', 'Ceph · VLAN90', False),
|
||||
('storage-01', 'sto-01', 'rack', 'Ceph · VLAN90', False),
|
||||
('monitor-01', 'mon-01', 'ZimaBoard', 'mgmt', False),
|
||||
('monitor-02', 'mon-02', 'ZimaBoard', 'mgmt', False),
|
||||
('large1', 'large1', 'off-rack', 'table', True),
|
||||
] -%}
|
||||
{%- for hname, hlabel, hsub, hvlan, off_rack in all_defs -%}
|
||||
{%- set st = topo_h[hname].status if hname in topo_h else 'unknown' -%}
|
||||
<div class="topo-v2-host-wrap">
|
||||
<!-- dual-homing wires: 10G solid green + 1G dashed amber -->
|
||||
<div class="topo-v2-host-wires">
|
||||
<div class="topo-v2-wire-10g" title="10G SFP+ → USW-Agg"></div>
|
||||
<div class="topo-v2-wire-1g" title="1G → Pro 24 PoE"></div>
|
||||
</div>
|
||||
<!-- host box -->
|
||||
<div class="topo-v2-node topo-v2-host topo-host topo-v2-status-{{ st }}{{ ' topo-v2-offrack' if off_rack else '' }}"
|
||||
data-host="{{ hname }}" style="min-width:80px; max-width:96px;">
|
||||
<span class="topo-v2-icon">▣</span>
|
||||
<span class="topo-v2-label">{{ hlabel }}</span>
|
||||
<span class="topo-v2-sub">{{ hsub }}</span>
|
||||
<span class="topo-v2-vlan">{{ hvlan }}</span>
|
||||
<span class="topo-badge topo-badge-{{ st }}">{{ st if st != 'unknown' else '–' }}</span>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div><!-- /topo-host-tier -->
|
||||
|
||||
</div><!-- /topo-bus-section -->
|
||||
|
||||
<!-- ── Legend ── -->
|
||||
<div class="topo-legend">
|
||||
<div class="topo-legend-item"><span class="topo-legend-line-wan"></span> WAN / uplink</div>
|
||||
<div class="topo-legend-item"><span class="topo-legend-line-isl"></span> ISL / inter-switch</div>
|
||||
<div class="topo-legend-item"><span class="topo-legend-line-10g"></span> 10G SFP+ (Ceph / VLAN90)</div>
|
||||
<div class="topo-legend-item"><span class="topo-legend-line-1g"></span> 1G DHCP (mgmt)</div>
|
||||
<div class="topo-legend-item" style="border:1px dashed var(--border); padding:1px 5px; font-size:.56em; color:var(--text-muted);">dashed border = off-rack</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /topo-v2 -->
|
||||
</div>
|
||||
|
||||
<!-- Host cards -->
|
||||
|
||||
@@ -84,16 +84,46 @@ function portBlockState(d) {
|
||||
return 'up';
|
||||
}
|
||||
|
||||
// ── Speed label helper ───────────────────────────────────────────────────
|
||||
function portSpeedLabel(port) {
|
||||
if (!port || !port.up) return '';
|
||||
const spd = port.speed; // speed in Mbps from UniFi API
|
||||
if (!spd) return '';
|
||||
if (spd >= 10000) return '10G';
|
||||
if (spd >= 1000) return '1G';
|
||||
if (spd >= 100) return '100M';
|
||||
return spd + 'M';
|
||||
}
|
||||
|
||||
// ── Render a single port block element ──────────────────────────────────
|
||||
function portBlockHtml(idx, port, swName, sfpBlock) {
|
||||
const state = portBlockState(port);
|
||||
const label = sfpBlock ? 'SFP' : idx;
|
||||
const title = port ? escHtml(port.name) : `Port ${idx}`;
|
||||
const sfpCls = sfpBlock ? ' sfp-block' : '';
|
||||
const state = portBlockState(port);
|
||||
const numLabel = sfpBlock ? 'SFP' : idx;
|
||||
const title = port ? escHtml(port.name) : `Port ${idx}`;
|
||||
const sfpCls = sfpBlock ? ' sfp-block' : '';
|
||||
const speedTxt = portSpeedLabel(port);
|
||||
// LLDP neighbor: first 6 chars of hostname
|
||||
const lldpName = (port && port.lldp_table && port.lldp_table.length)
|
||||
? escHtml((port.lldp_table[0].chassis_id_subtype === 'local'
|
||||
? port.lldp_table[0].chassis_id
|
||||
: port.lldp_table[0].system_name || port.lldp_table[0].chassis_id || '').slice(0, 6))
|
||||
: '';
|
||||
const lldpHtml = lldpName ? `<span class="port-lldp">${lldpName}</span>` : '';
|
||||
const speedHtml = speedTxt ? `<span class="port-speed">${speedTxt}</span>` : '';
|
||||
return `<div class="switch-port-block ${state}${sfpCls}"
|
||||
data-switch="${escHtml(swName)}" data-port-idx="${idx}"
|
||||
title="${title}"
|
||||
onclick="selectPort(this)">${label}</div>`;
|
||||
onclick="selectPort(this)"><span class="port-num">${numLabel}</span>${speedHtml}${lldpHtml}</div>`;
|
||||
}
|
||||
|
||||
// ── Chassis legend HTML ──────────────────────────────────────────────────
|
||||
function chassisLegendHtml() {
|
||||
return `<div class="chassis-legend">
|
||||
<div class="chassis-legend-item"><span class="chassis-legend-swatch cls-down"></span>down</div>
|
||||
<div class="chassis-legend-item"><span class="chassis-legend-swatch cls-up"></span>up</div>
|
||||
<div class="chassis-legend-item"><span class="chassis-legend-swatch cls-poe"></span>poe active</div>
|
||||
<div class="chassis-legend-item"><span class="chassis-legend-swatch cls-uplink"></span>uplink</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Render one switch chassis ────────────────────────────────────────────
|
||||
@@ -107,26 +137,38 @@ function renderChassis(swName, sw) {
|
||||
const downCount = totCount - upCount;
|
||||
const meta = [model, `${upCount}/${totCount} up`, downCount ? `${downCount} down` : ''].filter(Boolean).join(' · ');
|
||||
|
||||
// Is this a US24PRO? Used to add group-separator class
|
||||
const isUs24Pro = (model === 'US24PRO');
|
||||
|
||||
let chassisHtml = '';
|
||||
|
||||
if (layout) {
|
||||
const sfpPortSet = new Set(layout.sfp_ports || []);
|
||||
const sfpPortSet = new Set(layout.sfp_ports || []);
|
||||
const sfpSectionSet = new Set(layout.sfp_section || []);
|
||||
|
||||
// Main port rows
|
||||
chassisHtml += '<div class="chassis-rows">';
|
||||
for (const row of layout.rows) {
|
||||
chassisHtml += '<div class="chassis-row">';
|
||||
const rowCls = isUs24Pro ? ' us24pro-row' : '';
|
||||
chassisHtml += `<div class="chassis-row${rowCls}">`;
|
||||
for (const idx of row) {
|
||||
const port = portMap[idx];
|
||||
const isSfp = sfpPortSet.has(idx);
|
||||
const sfpCls = isSfp ? ' sfp-port' : '';
|
||||
const state = portBlockState(port);
|
||||
const title = port ? escHtml(port.name) : `Port ${idx}`;
|
||||
const speedTxt = portSpeedLabel(port);
|
||||
const lldpName = (port && port.lldp_table && port.lldp_table.length)
|
||||
? escHtml((port.lldp_table[0].chassis_id_subtype === 'local'
|
||||
? port.lldp_table[0].chassis_id
|
||||
: port.lldp_table[0].system_name || port.lldp_table[0].chassis_id || '').slice(0, 6))
|
||||
: '';
|
||||
const speedHtml = speedTxt ? `<span class="port-speed">${speedTxt}</span>` : '';
|
||||
const lldpHtml = lldpName ? `<span class="port-lldp">${lldpName}</span>` : '';
|
||||
chassisHtml += `<div class="switch-port-block ${state}${sfpCls}"
|
||||
data-switch="${escHtml(swName)}" data-port-idx="${idx}"
|
||||
title="${title}"
|
||||
onclick="selectPort(this)">${idx}</div>`;
|
||||
onclick="selectPort(this)"><span class="port-num">${idx}</span>${speedHtml}${lldpHtml}</div>`;
|
||||
}
|
||||
chassisHtml += '</div>';
|
||||
}
|
||||
@@ -158,7 +200,12 @@ function renderChassis(swName, sw) {
|
||||
${sw.ip ? `<span class="chassis-ip">${escHtml(sw.ip)}</span>` : ''}
|
||||
<span class="chassis-meta">${escHtml(meta)}</span>
|
||||
</div>
|
||||
<div class="chassis-body">${chassisHtml}</div>
|
||||
<div class="chassis-body">
|
||||
<div class="chassis-ear-l"></div>
|
||||
<div class="chassis-ear-r"></div>
|
||||
${chassisHtml}
|
||||
</div>
|
||||
${chassisLegendHtml()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user