From 22c07c7fb1f41231fdbfa64493571833863dd21c Mon Sep 17 00:00:00 2001 From: Andrey Lutsenko Date: Thu, 9 Apr 2026 22:13:36 +1000 Subject: [PATCH] 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 --- README.md | 1 + TECH_SPEC.md | 1 + web/index.html | 44 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 311e90d..f67c6ae 100644 --- a/README.md +++ b/README.md @@ -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`). diff --git a/TECH_SPEC.md b/TECH_SPEC.md index 37910d6..6245c0c 100644 --- a/TECH_SPEC.md +++ b/TECH_SPEC.md @@ -53,6 +53,7 @@ - [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows. - [x] UI позволяет выбрать последний scan и выполнить diff двух сканов. - [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе. +- [x] UI содержит легенду и фильтр отображения узлов по diff. ## Минимальные API diff --git a/web/index.html b/web/index.html index eb1956e..6bab3d7 100644 --- a/web/index.html +++ b/web/index.html @@ -31,6 +31,18 @@
Ожидание данных...
+
+ Легенда: + ● синий - обычный узел + ● зеленый - новый + ● красный - пропавший + Фильтр: + +

     
 
@@ -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();