UI: LLDP column in device list with No LLDP data marker.

Made-with: Cursor
This commit is contained in:
Луценко Андрей Анатольевич
2026-04-10 13:17:14 +10:00
parent 7e79072619
commit 6d75de1e2e
+26 -10
View File
@@ -60,7 +60,7 @@
<div class="panel">
<div class="row" style="margin-bottom: 6px;">
<strong>Найденные устройства</strong>
<span class="hint" style="margin:0;">отвечают на ping; клик по строке — связи этого устройства в таблицах ниже. Граф — «Загрузить».</span>
<span class="hint" style="margin:0;">отвечают на ping; колонка LLDP: при отсутствии соседей в MIB — пометка «No LLDP data» (часто для коммутаторов без LLDP или без успешного SNMP). Клик по строке — связи ниже.</span>
<button type="button" id="refreshDevicesBtn">Обновить список</button>
</div>
<div style="overflow:auto; max-height: 240px; border: 1px solid #e5e7eb; border-radius: 8px;">
@@ -71,10 +71,11 @@
<th class="sortable-th" data-sort="ping" style="text-align:left;">Ping <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="name" style="text-align:left;">Имя (SNMP) <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="snmp" style="text-align:left;">SNMP <span class="sort-ind"></span></th>
<th class="sortable-th" data-sort="lldp" style="text-align:left;">LLDP <span class="sort-ind"></span></th>
</tr>
</thead>
<tbody id="deviceTableBody">
<tr><td colspan="4" style="padding:10px;color:#64748b;">Укажите scan_id или создайте scan.</td></tr>
<tr><td colspan="5" style="padding:10px;color:#64748b;">Укажите scan_id или создайте scan.</td></tr>
</tbody>
</table>
</div>
@@ -173,7 +174,7 @@
let lastTopology = null;
let lastHighlight = null;
let lastDiff = null;
/** @type {{ip:string,ping:string,name:string,snmp:string}[]} */
/** @type {{ip:string,ping:string,name:string,snmp:string,lldp:string,lldpCount:number}[]} */
let deviceRowsCache = [];
let deviceSort = { key: "ip", dir: 1 };
let selectedDeviceIP = null;
@@ -264,6 +265,8 @@
cmp = String(a.name).localeCompare(String(b.name), "ru", { sensitivity: "base" });
} else if (key === "snmp") {
cmp = String(a.snmp).localeCompare(String(b.snmp), "ru", { sensitivity: "base" });
} else if (key === "lldp") {
cmp = (a.lldpCount || 0) - (b.lldpCount || 0);
}
return cmp;
}
@@ -281,14 +284,16 @@
function renderDeviceTableBody() {
updateDeviceSortIndicators();
if (deviceRowsCache.length === 0) {
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Нет хостов с ping «вверх» для этого scan (или скан ещё не дошёл до них).</td></tr>';
deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Нет хостов с ping «вверх» для этого scan (или скан ещё не дошёл до них).</td></tr>';
return;
}
const sorted = [...deviceRowsCache].sort((a, b) => deviceSort.dir * compareRows(a, b, deviceSort.key));
deviceTableBody.innerHTML = sorted
.map((r) => {
const dip = escapeHtml(r.ip);
return `<tr class="device-row" data-ip="${dip}" title="Показать связи этого устройства"><td>${dip}</td><td>${escapeHtml(r.ping)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.snmp)}</td></tr>`;
const noLldp = (r.lldpCount || 0) === 0;
const lldpStyle = noLldp ? ' style="color:#9a3412;font-weight:500;"' : "";
return `<tr class="device-row" data-ip="${dip}" title="Показать связи этого устройства"><td>${dip}</td><td>${escapeHtml(r.ping)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.snmp)}</td><td${lldpStyle}>${escapeHtml(r.lldp)}</td></tr>`;
})
.join("");
deviceTableBody.querySelectorAll("tr.device-row").forEach((tr) => {
@@ -405,38 +410,49 @@
if (!scanId) {
deviceRowsCache = [];
clearDeviceDetailPanel();
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Нет scan_id.</td></tr>';
deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Нет scan_id.</td></tr>';
updateDeviceSortIndicators();
return;
}
clearDeviceDetailPanel();
deviceTableBody.innerHTML = '<tr><td colspan="4" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
try {
const [hRes, sRes] = await Promise.all([
const [hRes, sRes, lRes] = await Promise.all([
fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=5000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=5000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=10000`),
]);
if (!hRes.ok) throw new Error(await hRes.text());
if (!sRes.ok) throw new Error(await sRes.text());
if (!lRes.ok) throw new Error((await lRes.text()) || "LLDP");
const hData = await hRes.json();
const sData = await sRes.json();
const lData = await lRes.json();
const snmpByIp = new Map();
for (const r of sData.items || []) {
snmpByIp.set(r.ip, r);
}
const lldpCountByIp = new Map();
for (const row of lData.items || []) {
const ipk = row.ip;
if (!ipk) continue;
lldpCountByIp.set(ipk, (lldpCountByIp.get(ipk) || 0) + 1);
}
deviceRowsCache = [];
for (const h of hData.items || []) {
if (!h.is_up) continue;
const s = snmpByIp.get(h.ip);
const name = s && s.success ? s.sys_name || "(пусто)" : "—";
const snmpCell = s ? (s.success ? "OK" : s.error || "нет ответа") : "не опрашивался";
deviceRowsCache.push({ ip: h.ip, ping: "да", name, snmp: snmpCell });
const lldpCount = lldpCountByIp.get(h.ip) || 0;
const lldpCell = lldpCount === 0 ? "No LLDP data" : `${lldpCount} записей`;
deviceRowsCache.push({ ip: h.ip, ping: "да", name, snmp: snmpCell, lldp: lldpCell, lldpCount });
}
renderDeviceTableBody();
} catch (e) {
deviceRowsCache = [];
clearDeviceDetailPanel();
deviceTableBody.innerHTML = `<tr><td colspan="4" style="padding:10px;color:#b91c1c;">Ошибка: ${escapeHtml(e.message)}</td></tr>`;
deviceTableBody.innerHTML = `<tr><td colspan="5" style="padding:10px;color:#b91c1c;">Ошибка: ${escapeHtml(e.message)}</td></tr>`;
updateDeviceSortIndicators();
}
}