feat: MAC lookup in scan FDB (API mac-locations, UI table, index)
Made-with: Cursor
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user