diff --git a/nmapui/handlers/connections.py b/nmapui/handlers/connections.py index 9b9f4fe..eaf0f73 100644 --- a/nmapui/handlers/connections.py +++ b/nmapui/handlers/connections.py @@ -129,6 +129,13 @@ def on_connect(auth=None): if auto_scan_config is not None: emit_to_client(new_sid, "auto_scan_status", build_auto_scan_status_payload(auto_scan_config)) + # #237: ignore zombie jobs whose owner tab has disconnected. + if owner_sid and owner_sid not in getattr(broadcaster, "_connected_sids", set()): + _complete = getattr(job_registry, "complete", None) + if callable(_complete): + _complete(owner_sid, active_job_type or "scan", status="completed") + broadcaster.end_job(owner_sid, job_type=active_job_type or "scan") + job = None job = job_registry.get(owner_sid, active_job_type) if owner_sid else None is_scanning = bool(job and job.get("status") in ("running", "cancelling")) last_scan_target = source_state.get("last_scan_target") or "" @@ -197,6 +204,14 @@ def on_get_initial_data(): ) job = job_registry.get(owner_sid, active_job_type) if owner_sid else None + # #237 follow-up: a job whose owner tab is gone is a zombie - it would + # otherwise disable scan buttons for every future page load. + if owner_sid and owner_sid not in getattr(broadcaster, "_connected_sids", set()): + job = None + _complete = getattr(job_registry, "complete", None) + if callable(_complete): + _complete(owner_sid, active_job_type or "scan", status="completed") + broadcaster.end_job(owner_sid, job_type=active_job_type or "scan") is_scanning = bool(job and job.get("status") in ("running", "cancelling")) last_scan_target = source_state.get("last_scan_target") or "" network_key = source_state.get("network_key") or {} diff --git a/static/js/app_bootstrap.js b/static/js/app_bootstrap.js index 2b785bb..8c5b56e 100644 --- a/static/js/app_bootstrap.js +++ b/static/js/app_bootstrap.js @@ -57,6 +57,14 @@ async function bootstrapApp() { } // Request the legacy sync snapshot now that all listeners are wired (#230 bridge). socket.emit('get_initial_data'); + // Transport-level reconnect = new server session; closures hold the dead socket. + // Reload to rebuild everything against the live session (#237 follow-up). + if (socket.io) { + socket.io.on('reconnect', () => { location.reload(); }); + } + if (typeof initializeScanButtonWiring === 'function') { + initializeScanButtonWiring(socket); + } if (typeof initializeDiscoveryUI === 'function') { initializeDiscoveryUI(socket); } diff --git a/static/js/scan_runtime.js b/static/js/scan_runtime.js index 3aeedcb..ec96075 100644 --- a/static/js/scan_runtime.js +++ b/static/js/scan_runtime.js @@ -697,7 +697,7 @@ function updateHostRow(data) { updateCVESummaries(); } -function initializeDiscoveryUI(socket) { +function initializeScanButtonWiring(socket) { const startScanBtn = document.getElementById('start-scan-btn'); const completeScanBtn = document.getElementById('generate-report-btn'); const dragnetScanBtn = document.getElementById('dragnet-scan-btn'); diff --git a/templates/index.html b/templates/index.html index 5c0f716..5076045 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1650,6 +1650,13 @@