From 396d139482208bffa12b2f7c28963ee06a5070ba Mon Sep 17 00:00:00 2001 From: Andrey Lutsenko Date: Thu, 9 Apr 2026 22:15:44 +1000 Subject: [PATCH] Add JSON export actions for topology and diff. Provide UI buttons to export latest topology and diff payloads into downloadable JSON files. Made-with: Cursor --- README.md | 1 + TECH_SPEC.md | 1 + web/index.html | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/README.md b/README.md index f67c6ae..cad132f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ http://localhost:8080/ - выбрать scan из выпадающего списка последних 20; - подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff. - использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие. +- экспортировать topology и diff в JSON-файлы кнопками из UI. По умолчанию используется `STORE_BACKEND=memory`. SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`). diff --git a/TECH_SPEC.md b/TECH_SPEC.md index 6245c0c..b1df9ba 100644 --- a/TECH_SPEC.md +++ b/TECH_SPEC.md @@ -54,6 +54,7 @@ - [x] UI позволяет выбрать последний scan и выполнить diff двух сканов. - [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе. - [x] UI содержит легенду и фильтр отображения узлов по diff. +- [x] UI поддерживает экспорт topology/diff в JSON. ## Минимальные API diff --git a/web/index.html b/web/index.html index 6bab3d7..aa31d1d 100644 --- a/web/index.html +++ b/web/index.html @@ -21,6 +21,7 @@ +
@@ -29,6 +30,7 @@ +
Ожидание данных...
@@ -56,6 +58,8 @@ const loadBtn = document.getElementById("loadBtn"); const latestBtn = document.getElementById("latestBtn"); const diffBtn = document.getElementById("diffBtn"); + const exportTopologyBtn = document.getElementById("exportTopologyBtn"); + const exportDiffBtn = document.getElementById("exportDiffBtn"); const fitBtn = document.getElementById("fitBtn"); const nodeFilter = document.getElementById("nodeFilter"); const statusEl = document.getElementById("status"); @@ -64,9 +68,21 @@ const NS = "http://www.w3.org/2000/svg"; let lastTopology = null; let lastHighlight = null; + let lastDiff = null; function setStatus(text) { statusEl.textContent = text; } function setDiff(text) { diffBox.textContent = text; } + function downloadJSON(filename, data) { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } function clearSVG() { while (svg.firstChild) svg.removeChild(svg.firstChild); @@ -240,6 +256,7 @@ return; } const d = await res.json(); + lastDiff = d; setDiff(JSON.stringify(d, null, 2)); const topoRes = await fetch(`/api/scans/${encodeURIComponent(toId)}/topology?limit=5000`); if (topoRes.ok) { @@ -272,6 +289,25 @@ }); latestBtn.addEventListener("click", loadLatestScan); diffBtn.addEventListener("click", loadDiff); + exportTopologyBtn.addEventListener("click", () => { + const scanId = scanIdInput.value.trim() || "unknown"; + if (!lastTopology) { + setStatus("Сначала загрузите topology"); + return; + } + downloadJSON(`topology_${scanId}.json`, lastTopology); + setStatus("Topology JSON экспортирован"); + }); + exportDiffBtn.addEventListener("click", () => { + const fromId = fromScanIdInput.value.trim() || "from"; + const toId = toScanIdInput.value.trim() || "to"; + if (!lastDiff) { + setStatus("Сначала выполните diff"); + return; + } + downloadJSON(`diff_${fromId}_to_${toId}.json`, lastDiff); + setStatus("Diff JSON экспортирован"); + }); fitBtn.addEventListener("click", () => { svg.setAttribute("viewBox", "0 0 1200 620"); });