feat: MAC lookup in scan FDB (API mac-locations, UI table, index)

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 12:41:48 +10:00
parent 886300b6c0
commit d85ec9ff61
9 changed files with 332 additions and 41 deletions
+108
View File
@@ -71,6 +71,37 @@
</details>
<div id="status">Ожидание действия. Строка статуса показывает текущую операцию и прогресс скана.</div>
<div class="panel" id="macSearchPanel" style="background:#fafafa;border:1px solid #e5e7eb;border-radius:8px;padding:10px 12px;">
<div class="row" style="margin-bottom:8px;align-items:center;">
<strong>Поиск MAC в сохранённом скане</strong>
<span class="hint" style="margin:0;">По FDB в БД: коммутатор (IP коммутатора из скана), порт (ifIndex / bridge), VLAN; IP и имя клиента — из ARP на коммутаторе и SNMP этого scan.</span>
</div>
<div class="row">
<input id="macSearchInput" placeholder="MAC, напр. aa:99:0f:91:76:76" style="min-width:280px;" autocomplete="off" />
<button type="button" id="macSearchBtn">Найти</button>
<span id="macSearchSummary" class="hint" style="margin:0;"></span>
</div>
<div style="overflow:auto; max-height:240px; margin-top:8px; border:1px solid #e5e7eb; border-radius:8px;">
<table id="macSearchTable" style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="background:#f1f5f9;">
<th style="text-align:left;padding:6px 8px;">Коммутатор (IP)</th>
<th style="text-align:left;padding:6px 8px;">Имя (sysName)</th>
<th style="text-align:left;padding:6px 8px;">ifIndex</th>
<th style="text-align:left;padding:6px 8px;">bridge</th>
<th style="text-align:left;padding:6px 8px;">VLAN</th>
<th style="text-align:left;padding:6px 8px;">MAC</th>
<th style="text-align:left;padding:6px 8px;">IP за портом</th>
<th style="text-align:left;padding:6px 8px;">Имя клиента</th>
</tr>
</thead>
<tbody id="macSearchTableBody">
<tr><td colspan="8" style="padding:8px;color:#64748b;">Укажите scan_id выше и MAC, нажмите «Найти».</td></tr>
</tbody>
</table>
</div>
</div>
<div class="panel">
<div class="row" style="margin-bottom: 6px;">
<strong>Найденные устройства</strong>
@@ -188,6 +219,10 @@
const deviceTableBody = document.getElementById("deviceTableBody");
const deviceTableHead = document.getElementById("deviceTableHead");
const refreshDevicesBtn = document.getElementById("refreshDevicesBtn");
const macSearchInput = document.getElementById("macSearchInput");
const macSearchBtn = document.getElementById("macSearchBtn");
const macSearchTableBody = document.getElementById("macSearchTableBody");
const macSearchSummary = document.getElementById("macSearchSummary");
const edgeListPanel = document.getElementById("edgeListPanel");
const edgeListPre = document.getElementById("edgeListPre");
const NS = "http://www.w3.org/2000/svg";
@@ -1221,11 +1256,84 @@
}
}
async function runMacSearch() {
const scanId = scanIdInput.value.trim();
const mac = (macSearchInput && macSearchInput.value.trim()) || "";
if (!scanId) {
setStatus("Укажите scan_id для поиска MAC.");
return;
}
if (!mac) {
setStatus("Введите MAC-адрес.");
return;
}
if (macSearchSummary) macSearchSummary.textContent = "";
if (macSearchTableBody) {
macSearchTableBody.innerHTML =
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Загрузка…</td></tr>';
}
setStatus(`Поиск MAC в scan ${scanId}`);
try {
const res = await fetch(
`/api/scans/${encodeURIComponent(scanId)}/mac-locations?mac=${encodeURIComponent(mac)}&limit=2000`
);
const text = await res.text();
if (!res.ok) {
if (macSearchTableBody) {
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
text || res.statusText
)}</td></tr>`;
}
setStatus(`Ошибка поиска MAC: ${res.status}`);
return;
}
const data = JSON.parse(text);
const items = data.items || [];
const canon = data.mac || mac;
if (macSearchSummary) {
macSearchSummary.textContent = items.length
? `Нормализовано: ${canon} · записей: ${items.length}`
: `Нормализовано: ${canon} · в FDB этого скана нет`;
}
if (!macSearchTableBody) return;
if (items.length === 0) {
macSearchTableBody.innerHTML =
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Нет совпадений в scan_port_devices.</td></tr>';
setStatus(`Поиск MAC: совпадений нет (${canon}).`);
return;
}
macSearchTableBody.innerHTML = items
.map(
(row) =>
`<tr><td>${escapeHtml(row.switch_ip || "")}</td><td>${escapeHtml(
row.switch_sys_name || "—"
)}</td><td>${escapeHtml(String(row.if_index ?? ""))}</td><td>${escapeHtml(
String(row.bridge_port ?? "")
)}</td><td>${escapeHtml(String(row.vlan ?? ""))}</td><td>${escapeHtml(
row.mac || "—"
)}</td><td>${escapeHtml(row.client_ip || "—")}</td><td>${escapeHtml(row.client_name || "—")}</td></tr>`
)
.join("");
setStatus(`Поиск MAC: найдено ${items.length} строк (${canon}).`);
} catch (e) {
if (macSearchTableBody) {
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
e.message
)}</td></tr>`;
}
setStatus(`Ошибка поиска MAC: ${e.message}`);
}
}
loadBtn.addEventListener("click", async () => {
await loadTopology();
await loadDeviceTable(scanIdInput.value.trim());
});
refreshDevicesBtn.addEventListener("click", () => loadDeviceTable(scanIdInput.value.trim()));
macSearchBtn?.addEventListener("click", runMacSearch);
macSearchInput?.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") runMacSearch();
});
createScanBtn.addEventListener("click", createScan);
document.getElementById("purgeDbBtn")?.addEventListener("click", async () => {
const inp = document.getElementById("purgeSecretInput");