feat(mac-locations): VLAN-aware port MAC counts, access/trunk thresholds, likely_attachment
Made-with: Cursor
This commit is contained in:
+51
-14
@@ -56,13 +56,19 @@
|
||||
|
||||
<div id="macSearchPanel" style="margin:10px 0;padding:12px 14px;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;box-shadow:0 1px 3px rgba(99,102,241,0.2);">
|
||||
<div style="font-weight:700;font-size:15px;color:#1e1b4b;margin-bottom:6px;">Где этот MAC подключён? (таблица FDB по выбранному scan)</div>
|
||||
<p class="hint" style="margin:0 0 10px;">Сначала укажите <strong>scan_id</strong> в поле выше (или «Последний scan» / «Загрузить»). Введите MAC — увидите коммутатор (IP и sysName), порт, VLAN, IP за портом из ARP.</p>
|
||||
<p class="hint" style="margin:0 0 10px;">Сначала укажите <strong>scan_id</strong> в поле выше (или «Последний scan» / «Загрузить»). Введите MAC. Строки сортируются: сначала порты с малым числом уникальных MAC на том же <strong>VLAN</strong> и порту (вероятный доступ), затем «между порогами», затем транк/uplink.</p>
|
||||
<div class="row" style="align-items:center;flex-wrap:wrap;gap:8px;">
|
||||
<label for="macSearchInput" style="font-weight:600;color:#312e81;">MAC</label>
|
||||
<input id="macSearchInput" placeholder="aa:bb:cc:dd:ee:ff" style="min-width:260px;font-family:monospace;" autocomplete="off" />
|
||||
<button type="button" id="macSearchBtn" style="background:#4f46e5;color:#fff;border:none;padding:8px 14px;border-radius:8px;cursor:pointer;font-weight:600;">Показать на каком коммутаторе</button>
|
||||
<span id="macSearchSummary" class="hint" style="margin:0;"></span>
|
||||
</div>
|
||||
<div class="row" style="flex-wrap:wrap;gap:10px;align-items:center;font-size:13px;margin-top:4px;">
|
||||
<span style="color:#4338ca;font-weight:600;">Пороги</span>
|
||||
<label title="Если на порту (и VLAN) не больше столько уникальных MAC — считаем порт доступом">доступ ≤ <input type="number" id="macAccessMax" min="1" max="4096" value="8" style="width:56px;padding:4px 6px;" /></label>
|
||||
<label title="Если не меньше столько уникальных MAC — сильный признак транка/uplink">транк ≥ <input type="number" id="macTrunkMin" min="2" max="65536" value="32" style="width:56px;padding:4px 6px;" /></label>
|
||||
<span class="hint" style="margin:0;">(должно быть: доступ < транк)</span>
|
||||
</div>
|
||||
<div style="overflow:auto; max-height:260px; margin-top:12px; border:1px solid #c7d2fe; border-radius:8px;background:#fff;">
|
||||
<table id="macSearchTable" style="width:100%; border-collapse:collapse; font-size:13px;">
|
||||
<thead>
|
||||
@@ -75,10 +81,12 @@
|
||||
<th style="text-align:left;padding:8px 10px;">MAC</th>
|
||||
<th style="text-align:left;padding:8px 10px;">IP за портом</th>
|
||||
<th style="text-align:left;padding:8px 10px;">Имя клиента</th>
|
||||
<th style="text-align:left;padding:8px 10px;" title="Уникальных MAC на этом коммутаторе, ifIndex, bridge, VLAN">MAC на порту</th>
|
||||
<th style="text-align:left;padding:8px 10px;">Оценка порта</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="macSearchTableBody">
|
||||
<tr><td colspan="8" style="padding:10px;color:#64748b;">Введите MAC и нажмите кнопку (нужен scan_id в поле выше).</td></tr>
|
||||
<tr><td colspan="10" style="padding:10px;color:#64748b;">Введите MAC и нажмите кнопку (нужен scan_id в поле выше).</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1256,6 +1264,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function macPortKindRu(kind) {
|
||||
if (kind === "access") return "доступ (мало MAC)";
|
||||
if (kind === "strong_transit") return "транк / uplink";
|
||||
if (kind === "ambiguous") return "между порогами";
|
||||
return kind ? String(kind) : "—";
|
||||
}
|
||||
|
||||
async function runMacSearch() {
|
||||
const scanId = scanIdInput.value.trim();
|
||||
const mac = (macSearchInput && macSearchInput.value.trim()) || "";
|
||||
@@ -1267,20 +1282,35 @@
|
||||
setStatus("Введите MAC-адрес.");
|
||||
return;
|
||||
}
|
||||
const amEl = document.getElementById("macAccessMax");
|
||||
const tmEl = document.getElementById("macTrunkMin");
|
||||
const accessMax = amEl && amEl.value !== "" ? parseInt(amEl.value, 10) : 8;
|
||||
const trunkMin = tmEl && tmEl.value !== "" ? parseInt(tmEl.value, 10) : 32;
|
||||
if (!Number.isFinite(accessMax) || !Number.isFinite(trunkMin) || accessMax < 1 || trunkMin < 2) {
|
||||
setStatus("Пороги: введите числа (доступ ≥1, транк ≥2).");
|
||||
return;
|
||||
}
|
||||
if (accessMax >= trunkMin) {
|
||||
setStatus("Пороги: «доступ» должно быть строго меньше «транк» (например 8 и 32).");
|
||||
return;
|
||||
}
|
||||
if (macSearchSummary) macSearchSummary.textContent = "";
|
||||
if (macSearchTableBody) {
|
||||
macSearchTableBody.innerHTML =
|
||||
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Загрузка…</td></tr>';
|
||||
'<tr><td colspan="10" 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`
|
||||
);
|
||||
let url = `/api/scans/${encodeURIComponent(scanId)}/mac-locations?mac=${encodeURIComponent(
|
||||
mac
|
||||
)}&limit=2000&access_max_macs=${encodeURIComponent(String(accessMax))}&trunk_min_macs=${encodeURIComponent(
|
||||
String(trunkMin)
|
||||
)}`;
|
||||
const res = await fetch(url);
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
if (macSearchTableBody) {
|
||||
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||
macSearchTableBody.innerHTML = `<tr><td colspan="10" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||
text || res.statusText
|
||||
)}</td></tr>`;
|
||||
}
|
||||
@@ -1289,16 +1319,21 @@
|
||||
}
|
||||
const data = JSON.parse(text);
|
||||
const items = data.items || [];
|
||||
const likely = data.likely_attachment || [];
|
||||
const canon = data.mac || mac;
|
||||
const am = data.access_max_macs ?? accessMax;
|
||||
const tm = data.trunk_min_macs ?? trunkMin;
|
||||
if (macSearchSummary) {
|
||||
macSearchSummary.textContent = items.length
|
||||
? `Нормализовано: ${canon} · записей: ${items.length}`
|
||||
: `Нормализовано: ${canon} · в FDB этого скана нет`;
|
||||
if (!items.length) {
|
||||
macSearchSummary.textContent = `Нормализовано: ${canon} · в FDB этого скана нет`;
|
||||
} else {
|
||||
macSearchSummary.textContent = `Пороги: доступ ≤${am}, транк ≥${tm}. Вероятный доступ: ${likely.length} из ${items.length} строк (см. колонки).`;
|
||||
}
|
||||
}
|
||||
if (!macSearchTableBody) return;
|
||||
if (items.length === 0) {
|
||||
macSearchTableBody.innerHTML =
|
||||
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Нет совпадений в scan_port_devices.</td></tr>';
|
||||
'<tr><td colspan="10" style="padding:8px;color:#64748b;">Нет совпадений в scan_port_devices.</td></tr>';
|
||||
setStatus(`Поиск MAC: совпадений нет (${canon}).`);
|
||||
return;
|
||||
}
|
||||
@@ -1311,13 +1346,15 @@
|
||||
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>`
|
||||
)}</td><td>${escapeHtml(row.client_ip || "—")}</td><td>${escapeHtml(row.client_name || "—")}</td><td>${escapeHtml(
|
||||
String(row.macs_on_port ?? "")
|
||||
)}</td><td>${escapeHtml(macPortKindRu(row.port_kind))}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
setStatus(`Поиск MAC: найдено ${items.length} строк (${canon}).`);
|
||||
setStatus(`Поиск MAC: ${items.length} строк (${canon}), вероятный доступ: ${likely.length}.`);
|
||||
} catch (e) {
|
||||
if (macSearchTableBody) {
|
||||
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||
macSearchTableBody.innerHTML = `<tr><td colspan="10" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||
e.message
|
||||
)}</td></tr>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user