diff --git a/dashboard.bat b/dashboard.bat index 5b69465..32334df 100644 --- a/dashboard.bat +++ b/dashboard.bat @@ -29,6 +29,9 @@ if %errorlevel% neq 0 ( timeout /t 1 /nobreak >nul start "" "http://127.0.0.1:5000" +:: Auto-minimize this terminal to keep the workspace clean +powershell -Command "$t = '[DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport(\"kernel32.dll\")] public static extern IntPtr GetConsoleWindow();'; $type = Add-Type -MemberDefinition $t -Name 'Win32Utils' -Namespace 'Win32' -PassThru; [void]$type::ShowWindow($type::GetConsoleWindow(), 2)" + :: Launch the Flask app — always from PROJECT_ROOT so run.bat is reachable cd /d "%PROJECT_ROOT%" python dashboard\app.py diff --git a/dashboard/app.py b/dashboard/app.py index 810141c..7711ea3 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -48,6 +48,13 @@ app = Flask( active_simulation_process = None +def is_simulation_running(): + """Returns True if a scenario process is currently alive.""" + global active_simulation_process + if active_simulation_process is None: + return False + return active_simulation_process.poll() is None + # ── Routes ──────────────────────────────────────────────────────── @app.route("/") @@ -108,12 +115,51 @@ def simulator_status(): client.set_timeout(10.0) client.get_server_version() # Will raise if not ready status = "ready" - is_paused = client.get_world().get_settings().synchronous_mode + world = client.get_world() + is_paused = world.get_settings().synchronous_mode + + # HEARTBEAT: If idle (sync mode) and no scenario is running, tick once + # to keep the UE4 window responsive. + if is_paused and not is_simulation_running(): + try: + world.tick() + except Exception: + pass except Exception: pass return jsonify({"status": status, "is_paused": is_paused}) + +@app.route("/api/simulator/kill", methods=["POST"]) +def simulator_kill(): + """Forces all CarlaUE4 processes to terminate.""" + try: + killed_any = False + for proc in psutil.process_iter(['name']): + if proc.info['name'] and 'CarlaUE4' in proc.info['name']: + try: + proc.kill() + killed_any = True + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + return jsonify({"success": True, "killed": killed_any}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}) + + +@app.route("/api/shutdown", methods=["POST"]) +def shutdown(): + """Stops the Flask server itself.""" + import threading + def delay_exit(): + time.sleep(1.0) + print("[INFO] Shutdown triggered. Exiting...") + os._exit(0) + + threading.Thread(target=delay_exit).start() + return jsonify({"success": True, "message": "Server shutting down..."}) + @app.route("/api/simulator/idle", methods=["POST"]) def simulator_idle(): try: diff --git a/dashboard/static/app.js b/dashboard/static/app.js index 440b971..5f9dc72 100644 --- a/dashboard/static/app.js +++ b/dashboard/static/app.js @@ -14,6 +14,8 @@ document.addEventListener('DOMContentLoaded', () => { const simStatusDot = document.getElementById('sim-status-dot'); const simStatusText = document.getElementById('sim-status-text'); const launchSimBtn = document.getElementById('launch-sim-btn'); + const killSimBtn = document.getElementById('kill-sim-btn'); + const exitAllBtn = document.getElementById('exit-all-btn'); // Idle Toggle Elements const idleToggleContainer = document.getElementById('idle-toggle-container'); @@ -91,12 +93,14 @@ document.addEventListener('DOMContentLoaded', () => { simStatusText.textContent = 'Ready'; } launchSimBtn.style.display = 'none'; + if (killSimBtn) killSimBtn.style.display = 'block'; } else if (status === 'loading') { isSimulatorRunning = false; if (idleToggleContainer) idleToggleContainer.style.display = 'none'; simStatusDot.classList.add('yellow'); simStatusText.textContent = 'Loading...'; launchSimBtn.style.display = 'none'; + if (killSimBtn) killSimBtn.style.display = 'block'; } else { isSimulatorRunning = false; if (idleToggleContainer) idleToggleContainer.style.display = 'none'; @@ -105,6 +109,7 @@ document.addEventListener('DOMContentLoaded', () => { simStatusDot.classList.add('red'); simStatusText.textContent = 'Offline'; launchSimBtn.style.display = 'block'; + if (killSimBtn) killSimBtn.style.display = 'none'; } }) .catch(() => { @@ -123,6 +128,60 @@ document.addEventListener('DOMContentLoaded', () => { fetch('/api/simulator/launch', { method: 'POST' }); }); + // Kill CarlaUE4 action + if (killSimBtn) { + killSimBtn.addEventListener('click', () => { + if (confirm("Are you sure you want to FORCE KILL CarlaUE4?\n\nThis will terminate the engine immediately. Any unsaved simulation data or unsynced frames will be lost.")) { + fetch('/api/simulator/kill', { method: 'POST' }) + .then(res => res.json()) + .then(data => { + if (data.success) { + appendLog('> CarlaUE4 process terminated by user.', 'warn'); + updateSimulatorStatus(); + } + }) + .catch(err => appendLog(`> Failed to kill simulator: ${err.message}`, 'error')); + } + }); + } + + // EXIT ALL action + if (exitAllBtn) { + exitAllBtn.addEventListener('click', () => { + if (confirm("FULL SYSTEM EXIT\n\nThis will:\n1. Terminate CarlaUE4\n2. Shutdown the Dashboard Server\n3. Close this browser window\n\nProceed ? (use with caution)")) { + appendLog('\n> [CRITICAL] Initiating full system shutdown...', 'error'); + + // 1. Kill CARLA + fetch('/api/simulator/kill', { method: 'POST' }); + + // 2. Shutdown Server + fetch('/api/shutdown', { method: 'POST' }); + + // 3. UI Feedback and self-destruct window + setTimeout(() => { + document.body.innerHTML = ` +
+
+ +
+

SYSTEM OFFLINE

+

All simulation processes and the dashboard server have been terminated.

+

You can now safely close this tab.

+ +
+ `; + setTimeout(() => window.close(), 2000); + }, 500); + } + }); + } + // Fetch Initial Config fetch('/api/config') .then(response => response.json()) diff --git a/dashboard/static/style.css b/dashboard/static/style.css index da50548..a12cc6e 100644 --- a/dashboard/static/style.css +++ b/dashboard/static/style.css @@ -600,6 +600,26 @@ input:checked + .slider:before { font-size: 0.85rem; } +.danger-icon:hover { + background: rgba(248, 81, 73, 0.18) !important; + color: #ff7b72 !important; +} + +.btn-outline { + background: transparent !important; + border: 1.5px solid var(--danger) !important; + color: var(--danger) !important; + box-shadow: none !important; +} + +.btn-outline:hover { + background: var(--danger) !important; + color: white !important; + opacity: 1 !important; + border-style: solid !important; + box-shadow: 0 0 15px rgba(248, 81, 73, 0.4) !important; +} + /* Idle Toggle CSS */ .idle-toggle-container { display: flex; diff --git a/dashboard/templates/index.html b/dashboard/templates/index.html index 87fcbaa..537e164 100644 --- a/dashboard/templates/index.html +++ b/dashboard/templates/index.html @@ -26,9 +26,14 @@