@@ -735,7 +739,7 @@ var Xr=function(e){if(!(this instanceof Xr))return new Xr(e);this.id="Thenable/1
@@ -1120,7 +1124,13 @@ async function saveCredentials() {
msg.className = 'modal-msg ok';
}
-async function openTestConnModal() {
+// The switch picker modal serves both Test Connection and Scan Switch.
+let switchPickerMode = 'test';
+
+async function openTestConnModal(mode = 'test') {
+ switchPickerMode = mode;
+ document.getElementById('testConnTitle').textContent = mode === 'scan' ? 'Scan One Switch' : 'Test Connection';
+ document.getElementById('testConnRunBtn').textContent = mode === 'scan' ? 'Scan' : 'Test';
document.getElementById('modalOverlay').classList.add('visible');
document.getElementById('testConnModal').style.display = 'block';
const result = document.getElementById('testConnResult');
@@ -1178,6 +1188,31 @@ async function runTestConnection() {
}
}
+async function runScanOne() {
+ const sel = document.getElementById('testConnSelect');
+ const opt = sel.options[sel.selectedIndex];
+ const result = document.getElementById('testConnResult');
+ if (!opt || !opt.value) {
+ result.className = 'modal-msg fail';
+ result.textContent = 'No switch selected';
+ return;
+ }
+ const res = await fetch('/api/scan/switch', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ip: opt.value, ...scanSettings() }),
+ });
+ if (res.status === 409) { alert('Scan already running'); return; }
+ if (!res.ok) {
+ const d = await res.json().catch(() => ({}));
+ result.className = 'modal-msg fail';
+ result.textContent = `✗ ${d.error || 'Scan failed to start'}`;
+ return;
+ }
+ closeModals();
+ startPolling();
+}
+
function startPolling() {
if (pollInterval) return;
pollInterval = setInterval(pollStatus, 1500);
@@ -1201,6 +1236,7 @@ async function pollStatus() {
document.getElementById('scanGwBtn').disabled = true;
document.getElementById('scanSecBtn').disabled = true;
document.getElementById('scanAmiBtn').disabled = true;
+ document.getElementById('scanOneBtn').disabled = true;
document.getElementById('clearScanBtn').disabled = true;
document.getElementById('abortScanBtn').style.display = '';
progressWrap.classList.add('visible');
@@ -1266,6 +1302,7 @@ async function pollStatus() {
document.getElementById('scanGwBtn').disabled = false;
document.getElementById('scanSecBtn').disabled = false;
document.getElementById('scanAmiBtn').disabled = false;
+ document.getElementById('scanOneBtn').disabled = false;
document.getElementById('clearScanBtn').disabled = false;
document.getElementById('abortScanBtn').style.display = 'none';
progressBar.style.width = '100%';
diff --git a/scanner.py b/scanner.py
index 476df99..43068f4 100644
--- a/scanner.py
+++ b/scanner.py
@@ -39,10 +39,12 @@ def request_abort():
return False
-def run_scan(dept: str = None, workers: int = 5, login_delay: int = 3):
+def run_scan(dept: str = None, workers: int = 5, login_delay: int = 3, only_ip: str = None):
"""
Full scan: fetch switch IPs from NocoDB → SSH all → parse → store → export.
dept: None = all active switches, "ELEC" or "GW" = dept-filtered.
+ only_ip: scan just that one switch from the list; like a dept scan, it
+ leaves every other switch's links in place.
"""
global scan_state
@@ -54,6 +56,8 @@ def run_scan(dept: str = None, workers: int = 5, login_delay: int = 3):
# Fetch switch list from NocoDB (or fallback)
switches = get_switches(dept=dept)
+ if only_ip:
+ switches = [s for s in switches if s["ip"] == only_ip]
if not switches:
logger.error("NocoDB returned no switches — aborting scan.")
@@ -72,13 +76,13 @@ def run_scan(dept: str = None, workers: int = 5, login_delay: int = 3):
"fail": 0,
"errors": [],
"log_lines": [],
- "dept_filter": dept,
+ "dept_filter": dept or only_ip,
"aborted": False,
})
scan_id = log_scan_start()
- if dept is None:
- clear_links() # Full scan only — dept scans upsert without wiping other depts
+ if dept is None and only_ip is None:
+ clear_links() # Full scan only — dept/single-switch scans upsert without wiping the rest
def on_progress(done, total, ip, result):
from datetime import datetime