feat(snmp,ui): устройства за портом (MAC/IP) в таблице 2
Добавил сбор FDB/ARP по SNMP (BRIDGE-MIB + ipNetToMedia), хранение и API /port-devices. В UI Таблица 2 теперь показывает MAC/IP за выбранным портом после клика по строке Таблицы 1. Made-with: Cursor
This commit is contained in:
+55
-34
@@ -91,7 +91,7 @@
|
||||
|
||||
<div class="panel" id="deviceDetailPanel">
|
||||
<strong>Связи выбранного устройства: <span id="selectedDeviceLabel">—</span></strong>
|
||||
<p class="hint" style="margin:6px 0 10px;">Кликните по строке в таблице «Найденные устройства». Таблица 1 — все интерфейсы из IF-MIB (SNMP) и LLDP-соседи по ifIndex; сортировка по «Локальный порт». Таблица 2 — сырые строки LLDP.</p>
|
||||
<p class="hint" style="margin:6px 0 10px;">Кликните по строке в таблице «Найденные устройства». Таблица 1 — все интерфейсы из IF-MIB (SNMP) и LLDP-соседи по ifIndex; сортировка по «Локальный порт». Клик по строке Таблицы 1 загружает Таблицу 2 (MAC/IP за выбранным портом).</p>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<div style="font-weight:600; margin-bottom:4px; color:#334155;">Таблица 1. Связи (IF-MIB + LLDP по портам)</div>
|
||||
@@ -116,17 +116,18 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-weight:600; margin-bottom:4px; color:#334155;">Таблица 2. LLDP текстом (по одной строке на запись MIB)</div>
|
||||
<div style="font-weight:600; margin-bottom:4px; color:#334155;">Таблица 2. Устройства за портом (MAC/IP по BRIDGE-MIB + ARP)</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; width:2.5rem;">№</th>
|
||||
<th style="text-align:left;">Текст LLDP</th>
|
||||
<th style="text-align:left;">ifIndex</th>
|
||||
<th style="text-align:left;">MAC</th>
|
||||
<th style="text-align:left;">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="deviceLldpTextTableBody">
|
||||
<tr><td colspan="2" style="padding:10px;color:#64748b;">Выберите устройство выше.</td></tr>
|
||||
<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -191,6 +192,7 @@
|
||||
/** @type {{sortKey:number,localPort:string,snmpInfo:string,targetName:string,targetIP:string,targetPort:string,chassis:string,resolved:string}[]} */
|
||||
let deviceLinksRowsCache = [];
|
||||
let deviceLinksSort = { key: "port", dir: 1 };
|
||||
let selectedIfIndex = null;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
@@ -320,12 +322,13 @@
|
||||
|
||||
function clearDeviceDetailPanel() {
|
||||
selectedDeviceIP = null;
|
||||
selectedIfIndex = null;
|
||||
selectedDeviceLabel.textContent = "—";
|
||||
deviceLinksRowsCache = [];
|
||||
deviceLinksTableBody.innerHTML =
|
||||
'<tr><td colspan="7" style="padding:10px;color:#64748b;">Выберите устройство в таблице выше.</td></tr>';
|
||||
deviceLldpTextTableBody.innerHTML =
|
||||
'<tr><td colspan="2" style="padding:10px;color:#64748b;">Выберите устройство в таблице выше.</td></tr>';
|
||||
'<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите устройство и порт в Таблице 1.</td></tr>';
|
||||
updateDeviceLinksSortIndicators();
|
||||
}
|
||||
|
||||
@@ -464,19 +467,37 @@
|
||||
deviceLinksTableBody.innerHTML = arr
|
||||
.map(
|
||||
(row) =>
|
||||
`<tr><td>${escapeHtml(row.localPort)}</td><td style="font-size:12px;color:#475569;">${escapeHtml(row.snmpInfo)}</td><td>${escapeHtml(row.targetName)}</td><td>${escapeHtml(row.targetIP)}</td><td>${escapeHtml(row.targetPort)}</td><td>${escapeHtml(row.chassis)}</td><td>${escapeHtml(row.resolved)}</td></tr>`
|
||||
`<tr class="port-row" data-if-index="${escapeHtml(row.sortKey)}"><td>${escapeHtml(row.localPort)}</td><td style="font-size:12px;color:#475569;">${escapeHtml(row.snmpInfo)}</td><td>${escapeHtml(row.targetName)}</td><td>${escapeHtml(row.targetIP)}</td><td>${escapeHtml(row.targetPort)}</td><td>${escapeHtml(row.chassis)}</td><td>${escapeHtml(row.resolved)}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function formatLldpTextLine(item) {
|
||||
const chunks = [];
|
||||
chunks.push(`Источник ${item.ip}`);
|
||||
if (item.local_port_num) chunks.push(`локальный порт: ${item.local_port_num}`);
|
||||
if (item.remote_sys_name) chunks.push(`удалённый sysName: ${item.remote_sys_name}`);
|
||||
if (item.remote_port_id) chunks.push(`удалённый порт ID: ${item.remote_port_id}`);
|
||||
if (item.remote_chassis_id) chunks.push(`удалённый chassis ID: ${item.remote_chassis_id}`);
|
||||
return chunks.join(". ") + ".";
|
||||
async function loadPortDevicesForSelectedPort() {
|
||||
const scanId = scanIdInput.value.trim();
|
||||
if (!scanId || !selectedDeviceIP || !selectedIfIndex) return;
|
||||
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="3" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/scans/${encodeURIComponent(scanId)}/port-devices?ip=${encodeURIComponent(selectedDeviceIP)}&if_index=${encodeURIComponent(selectedIfIndex)}&limit=20000`
|
||||
);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const items = ((await res.json()) || {}).items || [];
|
||||
if (items.length === 0) {
|
||||
deviceLldpTextTableBody.innerHTML =
|
||||
'<tr><td colspan="3" style="padding:10px;color:#64748b;">За этим портом MAC/IP не найдены (или устройство не отдаёт BRIDGE-MIB/ARP).</td></tr>';
|
||||
return;
|
||||
}
|
||||
deviceLldpTextTableBody.innerHTML = items
|
||||
.map(
|
||||
(row) =>
|
||||
`<tr><td>${escapeHtml(row.if_index)}</td><td>${escapeHtml(row.mac || "—")}</td><td>${escapeHtml(row.learned_ip || "—")}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
} catch (e) {
|
||||
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="3" style="padding:10px;color:#b91c1c;">${escapeHtml(
|
||||
e.message
|
||||
)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectDeviceAndLoadLinks(ip) {
|
||||
@@ -490,26 +511,22 @@
|
||||
renderDeviceTableBody();
|
||||
|
||||
deviceLinksTableBody.innerHTML = '<tr><td colspan="7" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
|
||||
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="2" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
|
||||
deviceLldpTextTableBody.innerHTML = '<tr><td colspan="3" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
|
||||
|
||||
try {
|
||||
const [linksRes, lldpRes, ifRes] = await Promise.all([
|
||||
const [linksRes, ifRes] = await Promise.all([
|
||||
fetch(`/api/scans/${encodeURIComponent(scanId)}/links?limit=20000`),
|
||||
fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=20000`),
|
||||
fetch(
|
||||
`/api/scans/${encodeURIComponent(scanId)}/interfaces?ip=${encodeURIComponent(ip)}&limit=8192`
|
||||
),
|
||||
]);
|
||||
if (!linksRes.ok) throw new Error("links: " + (await linksRes.text()));
|
||||
if (!lldpRes.ok) throw new Error("lldp: " + (await lldpRes.text()));
|
||||
const linkItems = (await linksRes.json()).items || [];
|
||||
const lldpItems = (await lldpRes.json()).items || [];
|
||||
let ifItems = [];
|
||||
if (ifRes.ok) {
|
||||
ifItems = ((await ifRes.json()) || {}).items || [];
|
||||
}
|
||||
const myLinks = linkItems.filter((x) => x.source_ip === ip);
|
||||
const myLldp = lldpItems.filter((x) => x.ip === ip);
|
||||
|
||||
deviceLinksRowsCache = buildMergedPortRows(ifItems, myLinks);
|
||||
updateDeviceLinksSortIndicators();
|
||||
@@ -520,23 +537,15 @@
|
||||
renderDeviceLinksTableBody();
|
||||
}
|
||||
|
||||
if (myLldp.length === 0) {
|
||||
deviceLldpTextTableBody.innerHTML =
|
||||
'<tr><td colspan="2" style="padding:10px;color:#64748b;">Нет сырых записей LLDP для этого IP.</td></tr>';
|
||||
} else {
|
||||
deviceLldpTextTableBody.innerHTML = myLldp
|
||||
.map(
|
||||
(row, i) =>
|
||||
`<tr><td style="vertical-align:top;color:#64748b;">${i + 1}</td><td style="white-space:pre-wrap;">${escapeHtml(formatLldpTextLine(row))}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
selectedIfIndex = null;
|
||||
deviceLldpTextTableBody.innerHTML =
|
||||
'<tr><td colspan="3" style="padding:10px;color:#64748b;">Выберите строку в Таблице 1, чтобы увидеть MAC/IP за портом.</td></tr>';
|
||||
setStatus(
|
||||
`Связи для ${ip}: строк табл.1 ${deviceLinksRowsCache.length} (IF-MIB ${ifItems.length}, /links ${myLinks.length}), LLDP записей ${myLldp.length}.`
|
||||
`Связи для ${ip}: строк табл.1 ${deviceLinksRowsCache.length} (IF-MIB ${ifItems.length}, /links ${myLinks.length}).`
|
||||
);
|
||||
} catch (e) {
|
||||
deviceLinksTableBody.innerHTML = `<tr><td colspan="7" style="padding:10px;color:#b91c1c;">${escapeHtml(e.message)}</td></tr>`;
|
||||
deviceLldpTextTableBody.innerHTML = `<tr><td colspan="2" 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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +565,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
deviceLinksTableBody.addEventListener("click", (ev) => {
|
||||
const tr = ev.target.closest("tr.port-row");
|
||||
if (!tr) return;
|
||||
const idx = parseInt(tr.getAttribute("data-if-index") || "", 10);
|
||||
if (!Number.isFinite(idx) || idx <= 0 || idx >= 1e9) {
|
||||
setStatus("Для этой строки нет корректного ifIndex — выберите интерфейсную строку.");
|
||||
return;
|
||||
}
|
||||
selectedIfIndex = idx;
|
||||
loadPortDevicesForSelectedPort();
|
||||
});
|
||||
|
||||
deviceTableBody.addEventListener("click", (ev) => {
|
||||
const tr = ev.target.closest("tr.device-row");
|
||||
if (!tr) return;
|
||||
|
||||
Reference in New Issue
Block a user