document.addEventListener('DOMContentLoaded', () => { // Determine WS URL based on current host const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host || 'localhost:8000'; const wsUrl = `${protocol}//${host}/ws`; let ws; // Make closure function globally accessible for inline onclick window.approveClosure = function(nodeId, event) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'close_gate', node_id: nodeId })); if (event && event.target) { event.target.innerHTML = 'CLOSING...'; event.target.style.opacity = '0.7'; event.target.disabled = true; } } }; window.openGate = function(nodeId, event) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'open_gate', node_id: nodeId })); if (event && event.target) { event.target.innerHTML = 'OPENING...'; event.target.style.opacity = '0.7'; event.target.disabled = true; } } }; // Control Panel logic document.getElementById('control-scenario').addEventListener('change', (e) => { fetch(`/api/scenario?scenario=${e.target.value}&weather=${document.getElementById('control-weather').value}`, {method: 'POST'}); }); document.getElementById('control-weather').addEventListener('change', (e) => { fetch(`/api/scenario?scenario=${document.getElementById('control-scenario').value}&weather=${e.target.value}`, {method: 'POST'}); }); const tickSlider = document.getElementById('control-tick'); tickSlider.addEventListener('input', (e) => { document.getElementById('tick-val-display').textContent = e.target.value + 's'; }); tickSlider.addEventListener('change', (e) => { fetch(`/api/settings/tick?interval=${e.target.value}`, {method: 'POST'}); }); // Control Panel Toggle Logic const controlsHeader = document.getElementById('controls-header'); const controlsBody = document.getElementById('controls-body'); const controlsToggleIcon = document.getElementById('controls-toggle-icon'); if (controlsHeader) { controlsHeader.addEventListener('click', () => { if (controlsBody.style.maxHeight === '0px') { controlsBody.style.maxHeight = '500px'; controlsBody.style.opacity = '1'; controlsBody.style.marginTop = '16px'; controlsToggleIcon.style.transform = 'rotate(0deg)'; } else { controlsBody.style.maxHeight = '0px'; controlsBody.style.opacity = '0'; controlsBody.style.marginTop = '0px'; controlsToggleIcon.style.transform = 'rotate(-90deg)'; } }); } // Vis.js Network Setup const container = document.getElementById('network-graph'); const visNodes = new vis.DataSet(); const visEdges = new vis.DataSet(); const data = { nodes: visNodes, edges: visEdges }; const options = { nodes: { shape: 'dot', font: { color: '#e2e8f0', size: 14, face: 'Outfit' }, borderWidth: 2, shadow: true }, edges: { smooth: { type: 'continuous' }, arrows: { to: { enabled: true, scaleFactor: 0.5 } } }, physics: { enabled: true, solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -120, centralGravity: 0.005, springLength: 160, springConstant: 0.06, damping: 0.45, avoidOverlap: 1.0 }, stabilization: { iterations: 300 } }, interaction: { hover: true, tooltipDelay: 80, navigationButtons: true, keyboard: true, dragNodes: false } }; const network = new vis.Network(container, data, options); // Freeze the graph layout permanently once the initial physics settle network.once("stabilizationIterationsDone", function() { network.setOptions({ physics: false }); }); // Fullscreen Logic const fullscreenBtn = document.getElementById('fullscreen-btn'); const graphPanel = document.getElementById('graph-panel'); fullscreenBtn.addEventListener('click', () => { if (!document.fullscreenElement) { graphPanel.requestFullscreen().catch(err => { console.log(`Error attempting to enable fullscreen: ${err.message}`); }); } else { document.exitFullscreen(); } }); document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement) { fullscreenBtn.textContent = '✖ Close'; graphPanel.style.background = 'var(--bg-color)'; } else { fullscreenBtn.textContent = '⛶ Expand'; graphPanel.style.background = 'var(--panel-bg)'; } }); function connect() { ws = new WebSocket(wsUrl); ws.onopen = () => { document.getElementById('val-connection').textContent = 'Connected'; document.getElementById('val-connection').style.color = 'var(--low-color)'; document.getElementById('val-connection').style.textShadow = '0 0 8px rgba(0,230,168,0.4)'; }; ws.onmessage = (event) => { const payload = JSON.parse(event.data); if (payload.type === 'state_update') { updateDashboard(payload); } }; ws.onclose = () => { document.getElementById('val-connection').textContent = 'Disconnected'; document.getElementById('val-connection').style.color = 'var(--critical-color)'; document.getElementById('val-connection').style.textShadow = '0 0 8px rgba(255,51,102,0.4)'; // Reconnect after 3 seconds setTimeout(connect, 3000); }; ws.onerror = (err) => { console.error('WebSocket error:', err); ws.close(); }; } function updateDashboard(data) { // Update header info document.getElementById('val-scenario').textContent = data.scenario.toUpperCase(); document.getElementById('val-weather').textContent = data.weather.toUpperCase(); document.getElementById('val-tick').textContent = data.tick; // Sync control panel with active backend state if (document.activeElement.id !== 'control-scenario') { document.getElementById('control-scenario').value = data.scenario; } if (document.activeElement.id !== 'control-weather') { document.getElementById('control-weather').value = data.weather; } if (document.activeElement.id !== 'control-tick') { const ti = data.tick_interval || 30; document.getElementById('control-tick').value = ti; document.getElementById('tick-val-display').textContent = ti + 's'; } // --- 1. Update Nodes Table --- const tbody = document.getElementById('nodes-tbody'); // Sort nodes by load_pct descending const nodeList = Object.values(data.nodes); nodeList.sort((a, b) => b.load_pct - a.load_pct); const currentNodeIds = new Set(nodeList.map(n => 'node-row-' + n.node_id)); // Remove stale rows Array.from(tbody.children).forEach(row => { if (row.id && row.id.startsWith('node-row-') && !currentNodeIds.has(row.id)) { row.remove(); } if (!row.id && nodeList.length > 0) { row.remove(); } }); nodeList.forEach((node, index) => { let tr = document.getElementById('node-row-' + node.node_id); if (!tr) { tr = document.createElement('tr'); tr.id = 'node-row-' + node.node_id; tr.innerHTML = `
⚠️ ACTION REQUIRED: Close Gates & Evacuate?
No active alerts.
All nodes operating safely.