feat: VLAN in FDB, persist vlan, enrich port-devices with hostname, UI columns

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 12:38:05 +10:00
parent 927110167c
commit 886300b6c0
8 changed files with 91 additions and 26 deletions
+27 -17
View File
@@ -124,18 +124,20 @@
</div>
<div>
<div style="font-weight:600; margin-bottom:4px; color:#334155;">Таблица 2. Устройства за портом (MAC/IP по BRIDGE-MIB + ARP)</div>
<div style="font-weight:600; margin-bottom:4px; color:#334155;">Таблица 2. Устройства за портом (VLAN, MAC, IP, имя — как MAC table; IP/имя при наличии ARP и строки SNMP в этом scan)</div>
<div class="subtable-wrap">
<table class="device-detail-table" id="deviceLldpTextTable" style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:#f1f5f9;">
<th style="text-align:left;">ifIndex</th>
<th style="text-align:left;">VLAN</th>
<th style="text-align:left;">MAC</th>
<th style="text-align:left;">IP</th>
<th style="text-align:left;">Имя (sysName)</th>
</tr>
</thead>
<tbody id="deviceLldpTextTableBody">
<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>
<tr><td colspan="5" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>
</tbody>
</table>
</div>
@@ -341,7 +343,7 @@
deviceLinksTableBody.innerHTML =
'<tr><td colspan="9" style="padding:10px;color:#64748b;">Выберите устройство в таблице выше.</td></tr>';
deviceLldpTextTableBody.innerHTML =
'<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>';
'<tr><td colspan="5" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>';
updateDeviceLinksSortIndicators();
}
@@ -384,23 +386,26 @@
const ifi = Number(p.if_index) || 0;
if (ifi === sortKey || br === sortKey) hits.push(p);
}
const byMac = new Map();
const byVlanMac = new Map();
for (const p of hits) {
const mk = normalizeMacLocal(p.mac);
if (!mk) continue;
const vlan = Number(p.vlan) || 0;
const k = `${vlan}|${mk}`;
const ip = String(p.learned_ip || "").trim();
const cur = byMac.get(mk);
const cur = byVlanMac.get(k);
const macDisp = String(p.mac || "").trim() || mk;
if (!cur || (ip && !cur.ip)) byMac.set(mk, { mac: macDisp, ip });
if (!cur || (ip && !cur.ip)) byVlanMac.set(k, { mac: macDisp, vlan, ip });
}
const rows = [...byMac.values()];
const rows = [...byVlanMac.values()];
const n = rows.length;
if (n === 0) return { macText: "—", ipText: "—" };
if (n === 1) {
const r = rows[0];
return { macText: r.mac, ipText: r.ip || "—" };
const macShow = r.vlan ? `${r.mac} (VLAN ${r.vlan})` : r.mac;
return { macText: macShow, ipText: r.ip || "—" };
}
return { macText: `несколько MAC (${n})`, ipText: "—" };
return { macText: `несколько MAC/VLAN (${n})`, ipText: "—" };
}
function attachFdbSummaries(rows, portItems) {
@@ -541,10 +546,13 @@
selectedFdbKey = null;
if (!items || items.length === 0) {
deviceLldpTextTableBody.innerHTML =
'<tr><td colspan="3" style="padding:10px;color:#64748b;">За этим портом MAC/IP не найдены (или устройство не отдаёт BRIDGE-MIB/ARP).</td></tr>';
'<tr><td colspan="5" style="padding:10px;color:#64748b;">За этим портом MAC/IP не найдены (или устройство не отдаёт BRIDGE-MIB/ARP).</td></tr>';
return;
}
const sorted = [...items].sort((a, b) => {
const va = Number(a.vlan) || 0;
const vb = Number(b.vlan) || 0;
if (va !== vb) return va - vb;
const ma = String(a.mac || "").localeCompare(String(b.mac || ""));
if (ma !== 0) return ma;
return String(a.learned_ip || "").localeCompare(String(b.learned_ip || ""));
@@ -553,22 +561,24 @@
.map((row) => {
const mk = normalizeMacLocal(row.mac);
const ip = String(row.learned_ip || "").trim();
const key = `${mk}|${ip}`;
return `<tr class="fdb-row" data-fdb-key="${escapeHtml(key)}"><td>${escapeHtml(row.if_index)}</td><td>${escapeHtml(row.mac || "—")}</td><td>${escapeHtml(row.learned_ip || "—")}</td></tr>`;
const vlan = Number(row.vlan) || 0;
const key = `${vlan}|${mk}|${ip}`;
const host = String(row.hostname || "").trim();
return `<tr class="fdb-row" data-fdb-key="${escapeHtml(key)}"><td>${escapeHtml(String(row.if_index ?? ""))}</td><td>${escapeHtml(String(vlan))}</td><td>${escapeHtml(row.mac || "—")}</td><td>${escapeHtml(row.learned_ip || "—")}</td><td>${escapeHtml(host || "—")}</td></tr>`;
})
.join("");
}
function loadPortDevicesForSelectedPort() {
if (!selectedDeviceIP || selectedIfIndex == null) return;
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="3" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
try {
const items = lastPortDevItems.filter(
(p) => Number(p.if_index) === selectedIfIndex || Number(p.bridge_port) === selectedIfIndex
);
renderFdbTable2Body(items);
} catch (e) {
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="3" style="padding:10px;color:#b91c1c;">${escapeHtml(
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="5" style="padding:10px;color:#b91c1c;">${escapeHtml(
e.message
)}</td></tr>`;
}
@@ -585,7 +595,7 @@
renderDeviceTableBody();
deviceLinksTableBody.innerHTML = '<tr><td colspan="9" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="3" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
try {
const [linksRes, ifRes, pdRes] = await Promise.all([
@@ -619,13 +629,13 @@
selectedIfIndex = null;
selectedFdbKey = null;
deviceLldpTextTableBody.innerHTML =
'<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите строку в Таблице 1, чтобы увидеть MAC/IP за портом.</td></tr>';
'<tr><td colspan="5" style="padding:10px;color:#64748b;">Выберите строку в Таблице 1, чтобы увидеть VLAN/MAC/IP за портом.</td></tr>';
setStatus(
`Связи для ${ip}: строк табл.1 ${deviceLinksRowsCache.length} (IF-MIB ${ifItems.length}, /links ${myLinks.length}, FDB строк ${lastPortDevItems.length}).`
);
} catch (e) {
deviceLinksTableBody.innerHTML = `<tr><td colspan="9" style="padding:10px;color:#b91c1c;">${escapeHtml(e.message)}</td></tr>`;
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="3" style="padding:10px;color:#b91c1c;">${escapeHtml(e.message)}</td></tr>`;
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="5" style="padding:10px;color:#b91c1c;">${escapeHtml(e.message)}</td></tr>`;
}
}