feature: Scan progress and timing features

This commit is contained in:
Keith Solomon
2026-03-08 15:40:11 -05:00
parent 12b23b43d8
commit 5a35a7158b
5 changed files with 207 additions and 15 deletions

View File

@@ -4,6 +4,11 @@ const portsListEl = document.getElementById('portsList');
const statusText = document.getElementById('statusText');
const scanBtn = document.getElementById('scanBtn');
const subnetInput = document.getElementById('subnetInput');
const scanProgressCard = document.getElementById('scanProgressCard');
const scanProgressTitle = document.getElementById('scanProgressTitle');
const scanProgressPercent = document.getElementById('scanProgressPercent');
const scanProgressFill = document.getElementById('scanProgressFill');
const scanProgressDetail = document.getElementById('scanProgressDetail');
let devices = [];
let selectedDeviceId = null;
@@ -12,6 +17,45 @@ function setStatus(msg) {
statusText.textContent = msg;
}
function formatDuration(totalSeconds) {
if (totalSeconds == null || Number.isNaN(Number(totalSeconds))) return '-';
const seconds = Math.max(0, Math.floor(Number(totalSeconds)));
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) return `${h}h ${m}m ${s}s`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}
function renderScanProgress(progress) {
if (!progress || !progress.running) {
scanProgressCard.classList.add('idle');
scanProgressTitle.textContent = 'No Active Scan';
scanProgressPercent.textContent = '0%';
scanProgressFill.style.width = '0%';
scanProgressDetail.textContent = 'Start a scan to see live progress.';
scanBtn.disabled = false;
return;
}
const total = Number(progress.total_hosts || 0);
const processed = Number(progress.processed_hosts || 0);
const saved = Number(progress.saved_hosts || 0);
const percent = Number(progress.percent || 0);
const current = progress.current_host || 'waiting for next host';
const totalLabel = total > 0 ? total : '?';
const etaLabel = progress.eta_seconds == null ? 'calculating...' : formatDuration(progress.eta_seconds);
const elapsedLabel = formatDuration(progress.elapsed_seconds || 0);
scanProgressCard.classList.remove('idle');
scanProgressTitle.textContent = `Scan #${progress.scan_id} Running`;
scanProgressPercent.textContent = `${percent}%`;
scanProgressFill.style.width = `${percent}%`;
scanProgressDetail.textContent = `Processed ${processed}/${totalLabel} hosts | Saved ${saved} | ETA ${etaLabel} | Elapsed ${elapsedLabel} | Current ${current}`;
scanBtn.disabled = true;
}
async function api(path, options = {}) {
const res = await fetch(path, options);
if (!res.ok) {
@@ -131,47 +175,73 @@ async function runScan() {
const subnet = subnetInput.value.trim();
if (!subnet) return;
scanBtn.disabled = true;
setStatus(`Starting scan on ${subnet}...`);
try {
const result = await api(`/api/scans/run?subnet=${encodeURIComponent(subnet)}`, { method: 'POST' });
renderScanProgress({
running: true,
scan_id: result.scan_id,
total_hosts: 0,
processed_hosts: 0,
saved_hosts: 0,
current_host: null,
percent: 0,
});
setStatus(`Scan #${result.scan_id} running on ${result.subnet}. Refreshing automatically...`);
await pollUntilComplete(result.scan_id);
} catch (err) {
setStatus(`Scan failed to start: ${err.message}`);
} finally {
scanBtn.disabled = false;
await refreshScanProgress();
}
}
async function pollUntilComplete(scanId) {
for (let i = 0; i < 240; i += 1) {
await new Promise((r) => setTimeout(r, 3000));
const scans = await api('/api/scans?limit=1');
await new Promise((r) => setTimeout(r, 2000));
const [health, scans] = await Promise.all([api('/api/health'), api('/api/scans?limit=1')]);
renderScanProgress(health.scan_progress);
const latest = scans[0];
if (!latest || latest.id !== scanId) continue;
if (latest.status === 'running') {
setStatus(`Scan #${scanId} is running...`);
const progress = health.scan_progress || {};
const total = progress.total_hosts || 0;
const processed = progress.processed_hosts || 0;
const label = total > 0 ? `${processed}/${total}` : `${processed}/?`;
const eta = progress.eta_seconds == null ? 'calculating ETA' : `ETA ${formatDuration(progress.eta_seconds)}`;
setStatus(`Scan #${scanId} is running (${label} hosts processed, ${eta})...`);
continue;
}
setStatus(`Scan #${scanId} ${latest.status} (${latest.host_count} hosts).`);
renderScanProgress(null);
await loadDevices();
return;
}
setStatus(`Scan #${scanId} is taking longer than expected.`);
}
async function refreshScanProgress() {
try {
const health = await api('/api/health');
renderScanProgress(health.scan_progress);
} catch (_) {
renderScanProgress(null);
}
}
scanBtn.addEventListener('click', runScan);
(async function init() {
setStatus('Loading inventory...');
try {
await loadDevices();
await Promise.all([loadDevices(), refreshScanProgress()]);
setStatus('Ready.');
} catch (err) {
setStatus(`Failed loading data: ${err.message}`);
}
})();
setInterval(refreshScanProgress, 3000);

View File

@@ -12,9 +12,21 @@
<h1>NetTrak</h1>
<p>Network inventory and port intelligence</p>
</div>
<div class="controls">
<input id="subnetInput" type="text" value="192.168.2.0/24" aria-label="Subnet" />
<button id="scanBtn">Run Scan</button>
<div class="toolbar">
<div class="controls">
<input id="subnetInput" type="text" value="192.168.2.0/24" aria-label="Subnet" />
<button id="scanBtn">Run Scan</button>
</div>
<div id="scanProgressCard" class="scan-progress-card idle" aria-live="polite">
<div class="scan-progress-head">
<strong id="scanProgressTitle">No Active Scan</strong>
<span id="scanProgressPercent">0%</span>
</div>
<div class="scan-progress-track">
<div id="scanProgressFill" class="scan-progress-fill"></div>
</div>
<div id="scanProgressDetail" class="scan-progress-detail">Start a scan to see live progress.</div>
</div>
</div>
</header>

View File

@@ -55,6 +55,12 @@ body {
gap: 10px;
}
.toolbar {
display: grid;
gap: 10px;
min-width: 420px;
}
input, button {
border: 1px solid var(--border);
border-radius: 8px;
@@ -75,6 +81,47 @@ button {
font-weight: 600;
}
.scan-progress-card {
border: 1px solid var(--border);
border-radius: 10px;
padding: 8px 10px;
background: rgba(255,255,255,0.03);
}
.scan-progress-card.idle {
opacity: 0.8;
}
.scan-progress-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.84rem;
}
.scan-progress-track {
margin-top: 6px;
width: 100%;
height: 8px;
border-radius: 999px;
background: #0d1822;
border: 1px solid var(--border);
overflow: hidden;
}
.scan-progress-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #57b6ff, #72e8c8);
transition: width 0.25s ease;
}
.scan-progress-detail {
margin-top: 6px;
color: var(--muted);
font-size: 0.78rem;
}
.layout {
height: calc(100vh - 128px);
display: grid;
@@ -209,6 +256,7 @@ summary {
min-height: 500px;
}
.toolbar { min-width: 0; }
.controls { flex-direction: column; align-items: stretch; }
input { min-width: 0; }
}