Enhance UI with latest-scan and inline diff tools.
Add automatic latest scan selection and two-scan diff viewer in web UI, then reflect completion in technical specification checklist. Made-with: Cursor
This commit is contained in:
@@ -56,6 +56,12 @@ UI открывается по адресу:
|
|||||||
http://localhost:8080/
|
http://localhost:8080/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Что умеет UI сейчас:
|
||||||
|
|
||||||
|
- загрузить topology по `scan_id`;
|
||||||
|
- автоматически подставить последний scan (`Последний scan`);
|
||||||
|
- сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`.
|
||||||
|
|
||||||
По умолчанию используется `STORE_BACKEND=memory`.
|
По умолчанию используется `STORE_BACKEND=memory`.
|
||||||
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
|
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
- [x] добавлены автоматические миграции таблиц при старте;
|
- [x] добавлены автоматические миграции таблиц при старте;
|
||||||
- [x] добавлены endpoints результатов: hosts, ports, snmp, lldp, links, topology;
|
- [x] добавлены endpoints результатов: hosts, ports, snmp, lldp, links, topology;
|
||||||
- [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows.
|
- [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows.
|
||||||
|
- [x] UI позволяет выбрать последний scan и выполнить diff двух сканов.
|
||||||
|
|
||||||
## Минимальные API
|
## Минимальные API
|
||||||
|
|
||||||
|
|||||||
+66
-1
@@ -15,24 +15,37 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>NetTopo: просмотр топологии</h2>
|
<h2>NetTopo: просмотр топологии</h2>
|
||||||
<div class="hint">Введите `scan_id`, нажмите "Загрузить", и страница покажет граф nodes/edges.</div>
|
<div class="hint">Можно выбрать последний scan автоматически или сравнить два скана через встроенный diff.</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input id="scanId" placeholder="scan_id" style="min-width: 380px;" />
|
<input id="scanId" placeholder="scan_id" style="min-width: 380px;" />
|
||||||
<button id="loadBtn">Загрузить</button>
|
<button id="loadBtn">Загрузить</button>
|
||||||
|
<button id="latestBtn">Последний scan</button>
|
||||||
<button id="fitBtn">По центру</button>
|
<button id="fitBtn">По центру</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="fromScanId" placeholder="from scan_id" style="min-width: 280px;" />
|
||||||
|
<input id="toScanId" placeholder="to scan_id" style="min-width: 280px;" />
|
||||||
|
<button id="diffBtn">Сравнить</button>
|
||||||
|
</div>
|
||||||
<div id="status">Ожидание данных...</div>
|
<div id="status">Ожидание данных...</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>
|
<svg id="canvas" viewBox="0 0 1200 620"></svg>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const scanIdInput = document.getElementById("scanId");
|
const scanIdInput = document.getElementById("scanId");
|
||||||
|
const fromScanIdInput = document.getElementById("fromScanId");
|
||||||
|
const toScanIdInput = document.getElementById("toScanId");
|
||||||
const loadBtn = document.getElementById("loadBtn");
|
const loadBtn = document.getElementById("loadBtn");
|
||||||
|
const latestBtn = document.getElementById("latestBtn");
|
||||||
|
const diffBtn = document.getElementById("diffBtn");
|
||||||
const fitBtn = document.getElementById("fitBtn");
|
const fitBtn = document.getElementById("fitBtn");
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
|
const diffBox = document.getElementById("diffBox");
|
||||||
const svg = document.getElementById("canvas");
|
const svg = document.getElementById("canvas");
|
||||||
const NS = "http://www.w3.org/2000/svg";
|
const NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
function setStatus(text) { statusEl.textContent = text; }
|
function setStatus(text) { statusEl.textContent = text; }
|
||||||
|
function setDiff(text) { diffBox.textContent = text; }
|
||||||
|
|
||||||
function clearSVG() {
|
function clearSVG() {
|
||||||
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
||||||
@@ -116,10 +129,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadLatestScan() {
|
||||||
|
setStatus("Ищу последний scan...");
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/scans?limit=1");
|
||||||
|
if (!res.ok) {
|
||||||
|
setStatus(`Ошибка ${res.status} при получении scan list`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
const latest = data.items && data.items[0];
|
||||||
|
if (!latest || !latest.id) {
|
||||||
|
setStatus("Сканы не найдены");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scanIdInput.value = latest.id;
|
||||||
|
toScanIdInput.value = latest.id;
|
||||||
|
setStatus(`Найден последний scan: ${latest.id}`);
|
||||||
|
await loadTopology();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Ошибка загрузки последнего scan: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDiff() {
|
||||||
|
const fromId = fromScanIdInput.value.trim();
|
||||||
|
const toId = toScanIdInput.value.trim();
|
||||||
|
if (!fromId || !toId) {
|
||||||
|
setStatus("Введите from и to scan_id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Сравниваю сканы...");
|
||||||
|
try {
|
||||||
|
const url = `/api/scans/diff?from=${encodeURIComponent(fromId)}&to=${encodeURIComponent(toId)}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
const t = await res.text();
|
||||||
|
setStatus(`Ошибка ${res.status}: ${t}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = await res.json();
|
||||||
|
setDiff(JSON.stringify(d, null, 2));
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Ошибка diff: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadBtn.addEventListener("click", loadTopology);
|
loadBtn.addEventListener("click", loadTopology);
|
||||||
|
latestBtn.addEventListener("click", loadLatestScan);
|
||||||
|
diffBtn.addEventListener("click", loadDiff);
|
||||||
fitBtn.addEventListener("click", () => {
|
fitBtn.addEventListener("click", () => {
|
||||||
svg.setAttribute("viewBox", "0 0 1200 620");
|
svg.setAttribute("viewBox", "0 0 1200 620");
|
||||||
});
|
});
|
||||||
|
setDiff("Diff будет показан здесь...");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user