Add topology legend and node visibility filters.

Show color legend and allow filtering graph nodes by all/new/missing states after scan diff analysis.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 22:13:36 +10:00
parent 6f6b822f16
commit 22c07c7fb1
3 changed files with 42 additions and 4 deletions
+1
View File
@@ -63,6 +63,7 @@ http://localhost:8080/
- сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`.
- выбрать scan из выпадающего списка последних 20;
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
По умолчанию используется `STORE_BACKEND=memory`.
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
+1
View File
@@ -53,6 +53,7 @@
- [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows.
- [x] UI позволяет выбрать последний scan и выполнить diff двух сканов.
- [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе.
- [x] UI содержит легенду и фильтр отображения узлов по diff.
## Минимальные API
+40 -4
View File
@@ -31,6 +31,18 @@
<button id="diffBtn">Сравнить</button>
</div>
<div id="status">Ожидание данных...</div>
<div style="margin: 6px 0 10px 0; font-size: 13px;">
Легенда:
<span style="display:inline-block;margin-left:8px;color:#2563eb;">● синий - обычный узел</span>
<span style="display:inline-block;margin-left:8px;color:#16a34a;">● зеленый - новый</span>
<span style="display:inline-block;margin-left:8px;color:#dc2626;">● красный - пропавший</span>
<span style="display:inline-block;margin-left:12px;">Фильтр:</span>
<select id="nodeFilter">
<option value="all">все</option>
<option value="new">только новые</option>
<option value="missing">только пропавшие</option>
</select>
</div>
<pre id="diffBox" style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px;white-space:pre-wrap;"></pre>
<svg id="canvas" viewBox="0 0 1200 620"></svg>
@@ -45,10 +57,13 @@
const latestBtn = document.getElementById("latestBtn");
const diffBtn = document.getElementById("diffBtn");
const fitBtn = document.getElementById("fitBtn");
const nodeFilter = document.getElementById("nodeFilter");
const statusEl = document.getElementById("status");
const diffBox = document.getElementById("diffBox");
const svg = document.getElementById("canvas");
const NS = "http://www.w3.org/2000/svg";
let lastTopology = null;
let lastHighlight = null;
function setStatus(text) { statusEl.textContent = text; }
function setDiff(text) { diffBox.textContent = text; }
@@ -59,8 +74,17 @@
function drawTopology(data, highlight = null) {
clearSVG();
const nodes = data.nodes || [];
let nodes = data.nodes || [];
const edges = data.edges || [];
const filter = nodeFilter.value || "all";
if (highlight && (filter === "new" || filter === "missing")) {
nodes = nodes.filter(n => {
const isNew = highlight.newHosts && highlight.newHosts.has(n.ip);
const isMissing = highlight.missingHosts && highlight.missingHosts.has(n.ip);
return filter === "new" ? isNew : isMissing;
});
}
const visibleIPs = new Set(nodes.map(n => n.ip));
const byIP = new Map();
nodes.forEach(n => byIP.set(n.ip, n));
@@ -79,6 +103,8 @@
edges.forEach(e => {
const s = pos.get(e.source_ip);
if (!s) return;
if (!visibleIPs.has(e.source_ip)) return;
if (e.target_ip && !visibleIPs.has(e.target_ip)) return;
const t = pos.get(e.target_ip) || { x: s.x + 30, y: s.y + 30 };
const line = document.createElementNS(NS, "line");
@@ -165,7 +191,9 @@
return;
}
const data = await res.json();
drawTopology(data);
lastTopology = data;
lastHighlight = null;
drawTopology(data, null);
setStatus(`Готово. Узлов: ${data.nodes?.length || 0}, связей: ${data.edges?.length || 0}`);
} catch (e) {
setStatus(`Ошибка загрузки: ${e.message}`);
@@ -216,10 +244,13 @@
const topoRes = await fetch(`/api/scans/${encodeURIComponent(toId)}/topology?limit=5000`);
if (topoRes.ok) {
const topo = await topoRes.json();
drawTopology(topo, {
const highlight = {
newHosts: new Set(d.new_hosts || []),
missingHosts: new Set(d.missing_hosts || []),
});
};
lastTopology = topo;
lastHighlight = highlight;
drawTopology(topo, highlight);
}
setStatus(
`Diff готов: +hosts ${d.new_hosts?.length || 0}, -hosts ${d.missing_hosts?.length || 0}, +links ${d.new_links?.length || 0}, -links ${d.missing_links?.length || 0}`
@@ -244,6 +275,11 @@
fitBtn.addEventListener("click", () => {
svg.setAttribute("viewBox", "0 0 1200 620");
});
nodeFilter.addEventListener("change", () => {
if (lastTopology) {
drawTopology(lastTopology, lastHighlight);
}
});
setDiff("Diff будет показан здесь...");
loadScanOptions();
</script>