@@ -18,9 +18,9 @@ const SocketClient = (function() {
1818 let disconnectTimer = null ;
1919 let disconnectTime = null ;
2020
21- // Idle state tracking
22- let activityRecorded = false ;
23- let serverIsIdle = false ;
21+ // When true, the WebSocket was closed intentionally (idle transition).
22+ // Suppresses disconnect status updates and prevents auto-reconnect.
23+ let intentionalDisconnect = false ;
2424
2525 // Callbacks
2626 const callbacks = {
@@ -58,11 +58,8 @@ const SocketClient = (function() {
5858
5959 /**
6060 * Updates the connection status UI.
61- * While the server is idle, all status transitions are suppressed — only
62- * dashboard.js (via setServerIdle) may change the indicator in that state.
6361 */
6462 function updateConnectionStatus ( status ) {
65- if ( serverIsIdle ) return ;
6663 const statusEl = document . getElementById ( 'connection-status' ) ;
6764 if ( statusEl ) {
6865 // Remove all status classes
@@ -94,12 +91,6 @@ const SocketClient = (function() {
9491 */
9592 function connect ( ) {
9693 updateConnectionStatus ( 'connecting' ) ;
97-
98- // Record activity only on initial page load, not on reconnections
99- // Reconnections should NOT reset the idle timeout - only explicit user activity should
100- if ( firstConnection ) {
101- recordActivity ( ) ;
102- }
10394
10495 // Create SockJS connection
10596 const socket = new SockJS ( '/ws' ) ;
@@ -121,6 +112,7 @@ const SocketClient = (function() {
121112 console . log ( '[SocketClient] Connected to WebSocket server' ) ;
122113 connected = true ;
123114 reconnectAttempts = 0 ;
115+ intentionalDisconnect = false ;
124116 updateConnectionStatus ( 'connected' ) ;
125117
126118 // Clear any pending disconnect timer
@@ -160,6 +152,13 @@ const SocketClient = (function() {
160152 stompClient . onWebSocketClose = function ( event ) {
161153 console . log ( '[SocketClient] WebSocket closed' ) ;
162154 connected = false ;
155+
156+ // Intentional close (idle transition) — do not update the status
157+ // indicator or schedule a reconnect. Just let the connection sit closed.
158+ if ( intentionalDisconnect ) {
159+ return ;
160+ }
161+
163162 updateConnectionStatus ( 'disconnected' ) ;
164163 trigger ( 'onDisconnect' , event ) ;
165164
@@ -270,18 +269,37 @@ const SocketClient = (function() {
270269 return connected ;
271270 }
272271
272+ /**
273+ * Intentionally closes the WebSocket during idle transition.
274+ * Sets intentionalDisconnect so the onclose handler does not update the
275+ * status indicator or schedule a reconnect.
276+ */
277+ function closeForIdle ( ) {
278+ intentionalDisconnect = true ;
279+ if ( stompClient && stompClient . active ) {
280+ stompClient . deactivate ( ) ;
281+ connected = false ;
282+ }
283+ }
284+
285+ /**
286+ * Ensures the WebSocket is connected. If the STOMP client is not active
287+ * (e.g., after an intentional idle disconnect), reconnects immediately.
288+ * Called at the top of every simulation trigger so clicking a button while
289+ * idle automatically re-establishes the connection before the API call.
290+ */
291+ function ensureWebSocket ( ) {
292+ if ( ! stompClient || ! stompClient . active ) {
293+ connect ( ) ;
294+ }
295+ }
296+
273297 /**
274298 * Records activity with the server to prevent idle timeout.
275- * Should only be called on:
276- * - Initial page load (not reconnections)
277- * - Explicit user activity events
278- * Wakes the app from idle state if necessary.
299+ * Called once on page load (before WS init) to wake the server so the
300+ * first WebSocket broadcast arrives with is_idle: false.
279301 */
280302 async function recordActivity ( ) {
281- if ( activityRecorded ) {
282- return ; // Only record once per page load to avoid spam
283- }
284-
285303 try {
286304 const response = await fetch ( '/api/health/activity' , {
287305 method : 'POST' ,
@@ -290,7 +308,6 @@ const SocketClient = (function() {
290308
291309 if ( response . ok ) {
292310 const result = await response . json ( ) ;
293- activityRecorded = true ;
294311
295312 if ( result . wokeFromIdle ) {
296313 console . log ( '[SocketClient] App woken from idle state' ) ;
@@ -307,15 +324,6 @@ const SocketClient = (function() {
307324 }
308325 }
309326
310- /**
311- * Marks the server as idle or active, controlling whether WebSocket
312- * reconnect/disconnect events may update the status indicator.
313- * Call with true on GOING_IDLE, false on WAKING_UP.
314- */
315- function setServerIdle ( idle ) {
316- serverIsIdle = idle ;
317- }
318-
319327 // Public API
320328 return {
321329 connect,
@@ -324,6 +332,7 @@ const SocketClient = (function() {
324332 on,
325333 isConnected,
326334 recordActivity,
327- setServerIdle
335+ closeForIdle,
336+ ensureWebSocket
328337 } ;
329338} ) ( ) ;
0 commit comments