Browse Source

feat(dashboard): stabilize idle mode and add system controls

- Fix: Implement Heartbeat Tick in dashboard backend to prevent CARLA window from freezing during GPU Idle Mode.
- Feat: Add "Force Kill Simulator" functionality to terminate unresponsive engine instances.
- Feat: Add "Nuclear EXIT ALL" button to sidebar for coordinated shutdown of CARLA, Flask server, and Browser.
- UI/UX: Design and center a premium "System Offline" overlay for graceful exit visibility.
- Automation: Implement auto-minimization logic in dashboard.bat to keep the workspace clean.
1843_integration
RUSHIL AMBARISH KADU 1 month ago
parent
commit
0a0f72d447
  1. 3
      dashboard.bat
  2. 48
      dashboard/app.py
  3. 59
      dashboard/static/app.js
  4. 20
      dashboard/static/style.css
  5. 18
      dashboard/templates/index.html

3
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

48
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:

59
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 = `
<div style="height:100vh; width: 100vw; position: fixed; top: 0; left: 0; z-index: 9999; display:flex; flex-direction:column; align-items:center; justify-content:center; background:radial-gradient(circle at center, #1a202c, #0d1117); color:white; font-family:'Inter', sans-serif; text-align:center;">
<div style="width: 80px; height: 80px; background: #f85149; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 2rem; box-shadow: 0 0 40px rgba(248, 81, 73, 0.6); animation: pulse 2s infinite;">
<svg viewBox="0 0 24 24" width="40" height="40" stroke="white" stroke-width="2" fill="none"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
</div>
<h1 style="font-size: 2.5rem; margin-bottom: 0.5rem; letter-spacing: -1.5px; font-weight: 800; background: linear-gradient(to bottom, #fff, #8b949e); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">SYSTEM OFFLINE</h1>
<p style="color: #8b949e; font-size: 1.2rem; max-width: 400px;">All simulation processes and the dashboard server have been terminated.</p>
<p style="margin-top: 3rem; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: #484f58; border: 1px solid #30363d; padding: 0.5rem 1rem; border-radius: 6px;">You can now safely close this tab.</p>
<style>
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(248, 81, 73, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(248, 81, 73, 0); }
}
</style>
</div>
`;
setTimeout(() => window.close(), 2000);
}, 500);
}
});
}
// Fetch Initial Config
fetch('/api/config')
.then(response => response.json())

20
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;

18
dashboard/templates/index.html

@ -26,9 +26,14 @@
<div class="nav-section" style="margin-bottom: 2rem;">
<div class="section-title">SIMULATOR STATUS</div>
<div class="status-card" id="sim-status-card">
<div class="status-indicator">
<span class="dot red" id="sim-status-dot"></span>
<span id="sim-status-text" style="font-weight: 500; font-size: 0.9rem;">Offline</span>
<div class="status-indicator" style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="dot red" id="sim-status-dot"></span>
<span id="sim-status-text" style="font-weight: 500; font-size: 0.9rem;">Offline</span>
</div>
<button id="kill-sim-btn" class="icon-btn danger-icon" title="Force Kill CarlaUE4" style="display: none; padding: 4px; color: var(--danger); border-radius: 4px;">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</button>
</div>
<button id="launch-sim-btn" class="btn btn-secondary btn-small" style="width: 100%; margin-top: 10px; padding: 0.6rem;">Initialize CarlaUE4</button>
<!-- Idle Toggle -->
@ -48,6 +53,13 @@
<!-- Populated dynamically via JS -->
</div>
</div>
<div class="nav-section" style="margin-top: auto; padding-top: 2rem;">
<button id="exit-all-btn" class="btn btn-danger btn-outline" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; border-style: dashed; opacity: 0.8; padding: 0.8rem;">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
<span style="letter-spacing: 1px; font-weight: 700;">EXIT ALL</span>
</button>
</div>
</aside>
<!-- Main Workspace -->

Loading…
Cancel
Save