Add scan creation and completion watch in UI.
Allow creating scans by CIDR from web UI and polling scan status until terminal state before refreshing topology. Made-with: Cursor
This commit is contained in:
@@ -65,6 +65,7 @@ http://localhost:8080/
|
|||||||
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
|
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
|
||||||
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
|
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
|
||||||
- экспортировать topology и diff в JSON-файлы кнопками из UI.
|
- экспортировать topology и diff в JSON-файлы кнопками из UI.
|
||||||
|
- создать scan по CIDR из UI и дождаться завершения кнопкой "Ждать завершения".
|
||||||
|
|
||||||
По умолчанию используется `STORE_BACKEND=memory`.
|
По умолчанию используется `STORE_BACKEND=memory`.
|
||||||
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
|
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
- [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе.
|
- [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе.
|
||||||
- [x] UI содержит легенду и фильтр отображения узлов по diff.
|
- [x] UI содержит легенду и фильтр отображения узлов по diff.
|
||||||
- [x] UI поддерживает экспорт topology/diff в JSON.
|
- [x] UI поддерживает экспорт topology/diff в JSON.
|
||||||
|
- [x] UI позволяет создать scan по CIDR и дождаться его завершения.
|
||||||
|
|
||||||
## Минимальные API
|
## Минимальные API
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<h2>NetTopo: просмотр топологии</h2>
|
<h2>NetTopo: просмотр топологии</h2>
|
||||||
<div class="hint">Можно выбрать последний scan автоматически или сравнить два скана через встроенный diff.</div>
|
<div class="hint">Можно выбрать последний scan автоматически или сравнить два скана через встроенный diff.</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="cidrInput" placeholder="CIDR, например 192.168.1.0/24" style="min-width: 320px;" />
|
||||||
|
<button id="createScanBtn">Создать scan</button>
|
||||||
|
<button id="watchScanBtn">Ждать завершения</button>
|
||||||
|
</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;" />
|
||||||
<select id="scanSelect" style="min-width: 360px;"></select>
|
<select id="scanSelect" style="min-width: 360px;"></select>
|
||||||
@@ -50,12 +55,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const scanIdInput = document.getElementById("scanId");
|
const scanIdInput = document.getElementById("scanId");
|
||||||
|
const cidrInput = document.getElementById("cidrInput");
|
||||||
const scanSelect = document.getElementById("scanSelect");
|
const scanSelect = document.getElementById("scanSelect");
|
||||||
const fromScanIdInput = document.getElementById("fromScanId");
|
const fromScanIdInput = document.getElementById("fromScanId");
|
||||||
const toScanIdInput = document.getElementById("toScanId");
|
const toScanIdInput = document.getElementById("toScanId");
|
||||||
const fromScanSelect = document.getElementById("fromScanSelect");
|
const fromScanSelect = document.getElementById("fromScanSelect");
|
||||||
const toScanSelect = document.getElementById("toScanSelect");
|
const toScanSelect = document.getElementById("toScanSelect");
|
||||||
const loadBtn = document.getElementById("loadBtn");
|
const loadBtn = document.getElementById("loadBtn");
|
||||||
|
const createScanBtn = document.getElementById("createScanBtn");
|
||||||
|
const watchScanBtn = document.getElementById("watchScanBtn");
|
||||||
const latestBtn = document.getElementById("latestBtn");
|
const latestBtn = document.getElementById("latestBtn");
|
||||||
const diffBtn = document.getElementById("diffBtn");
|
const diffBtn = document.getElementById("diffBtn");
|
||||||
const exportTopologyBtn = document.getElementById("exportTopologyBtn");
|
const exportTopologyBtn = document.getElementById("exportTopologyBtn");
|
||||||
@@ -216,6 +224,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createScan() {
|
||||||
|
const cidr = cidrInput.value.trim();
|
||||||
|
if (!cidr) {
|
||||||
|
setStatus("Введите CIDR для создания scan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Создаю scan...");
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: `manual-${new Date().toISOString()}`,
|
||||||
|
cidrs: [cidr],
|
||||||
|
exclude_ips: [],
|
||||||
|
options: {
|
||||||
|
ping_timeout_ms: 700,
|
||||||
|
ping_retries: 1,
|
||||||
|
max_parallel_hosts: 128,
|
||||||
|
port_scan_enabled: true,
|
||||||
|
ports: [22, 80, 443, 161]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = await fetch("/api/scans", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const t = await res.text();
|
||||||
|
setStatus(`Ошибка создания scan: ${res.status} ${t}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = await res.json();
|
||||||
|
scanIdInput.value = d.scan_id;
|
||||||
|
toScanIdInput.value = d.scan_id;
|
||||||
|
setStatus(`Scan создан: ${d.scan_id}`);
|
||||||
|
await loadScanOptions();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Ошибка создания scan: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchScan() {
|
||||||
|
const id = scanIdInput.value.trim();
|
||||||
|
if (!id) {
|
||||||
|
setStatus("Введите scan_id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus(`Ожидание завершения scan ${id}...`);
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const maxWaitMs = 20 * 60 * 1000;
|
||||||
|
while (Date.now() - startedAt < maxWaitMs) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/scans/${encodeURIComponent(id)}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
setStatus(`Ошибка чтения scan: ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const s = await res.json();
|
||||||
|
setStatus(`Scan ${id}: ${s.status}, progress ${s.progress || 0}%`);
|
||||||
|
if (s.status === "done" || s.status === "failed" || s.status === "canceled") {
|
||||||
|
await loadTopology();
|
||||||
|
await loadScanOptions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Ошибка ожидания scan: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
setStatus("Таймаут ожидания scan");
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLatestScan() {
|
async function loadLatestScan() {
|
||||||
setStatus("Ищу последний scan...");
|
setStatus("Ищу последний scan...");
|
||||||
try {
|
try {
|
||||||
@@ -278,6 +358,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBtn.addEventListener("click", loadTopology);
|
loadBtn.addEventListener("click", loadTopology);
|
||||||
|
createScanBtn.addEventListener("click", createScan);
|
||||||
|
watchScanBtn.addEventListener("click", watchScan);
|
||||||
scanSelect.addEventListener("change", () => {
|
scanSelect.addEventListener("change", () => {
|
||||||
if (scanSelect.value) scanIdInput.value = scanSelect.value;
|
if (scanSelect.value) scanIdInput.value = scanSelect.value;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user