diff --git a/web/admin.html b/web/admin.html index 044cd3e..1547a73 100644 --- a/web/admin.html +++ b/web/admin.html @@ -927,6 +927,11 @@ let isNewPPR = false; let wsConnection = null; let pendingStatusUpdate = null; // Track pending status update for timestamp modal + let wsHeartbeatInterval = null; + let wsReconnectTimeout = null; + let lastHeartbeatResponse = null; + let sessionExpiryWarningShown = false; + let sessionExpiryCheckInterval = null; // WebSocket connection for real-time updates function connectWebSocket() { @@ -934,17 +939,34 @@ return; // Already connected } + // Clear any existing reconnect timeout + if (wsReconnectTimeout) { + clearTimeout(wsReconnectTimeout); + wsReconnectTimeout = null; + } + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/tower-updates`; + console.log('Connecting to WebSocket:', wsUrl); wsConnection = new WebSocket(wsUrl); wsConnection.onopen = function(event) { - console.log('WebSocket connected for real-time updates'); + console.log('✅ WebSocket connected for real-time updates'); + lastHeartbeatResponse = Date.now(); + startHeartbeat(); + showNotification('Real-time updates connected'); }; wsConnection.onmessage = function(event) { try { + // Check if it's a heartbeat response + if (event.data.startsWith('Heartbeat:')) { + lastHeartbeatResponse = Date.now(); + console.log('💓 WebSocket heartbeat received'); + return; + } + const data = JSON.parse(event.data); console.log('WebSocket message received:', data); @@ -952,6 +974,7 @@ if (data.type && (data.type.includes('ppr_') || data.type === 'status_update')) { console.log('PPR update detected, refreshing...'); loadPPRs(); + showNotification('Data updated'); } } catch (error) { console.error('Error parsing WebSocket message:', error); @@ -959,22 +982,59 @@ }; wsConnection.onclose = function(event) { - console.log('WebSocket disconnected'); - // Attempt to reconnect after 5 seconds - setTimeout(() => { - if (accessToken) { // Only reconnect if still logged in + console.log('⚠️ WebSocket disconnected', event.code, event.reason); + stopHeartbeat(); + + // Attempt to reconnect after 5 seconds if still logged in + if (accessToken) { + showNotification('Real-time updates disconnected, reconnecting...', true); + wsReconnectTimeout = setTimeout(() => { console.log('Attempting to reconnect WebSocket...'); connectWebSocket(); - } - }, 5000); + }, 5000); + } }; wsConnection.onerror = function(error) { - console.error('WebSocket error:', error); + console.error('❌ WebSocket error:', error); }; } + function startHeartbeat() { + // Clear any existing heartbeat + stopHeartbeat(); + + // Send ping every 30 seconds + wsHeartbeatInterval = setInterval(() => { + if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { + wsConnection.send('ping'); + console.log('💓 Sending WebSocket heartbeat'); + + // Check if last heartbeat was more than 60 seconds ago + if (lastHeartbeatResponse && (Date.now() - lastHeartbeatResponse > 60000)) { + console.warn('⚠️ No heartbeat response for 60 seconds, reconnecting...'); + wsConnection.close(); + } + } else { + console.warn('⚠️ WebSocket not open, stopping heartbeat'); + stopHeartbeat(); + } + }, 30000); + } + + function stopHeartbeat() { + if (wsHeartbeatInterval) { + clearInterval(wsHeartbeatInterval); + wsHeartbeatInterval = null; + } + } + function disconnectWebSocket() { + stopHeartbeat(); + if (wsReconnectTimeout) { + clearTimeout(wsReconnectTimeout); + wsReconnectTimeout = null; + } if (wsConnection) { wsConnection.close(); wsConnection = null; @@ -1020,6 +1080,52 @@ }); } + // Session expiry monitoring + function startSessionExpiryCheck() { + // Clear any existing interval + if (sessionExpiryCheckInterval) { + clearInterval(sessionExpiryCheckInterval); + } + + sessionExpiryWarningShown = false; + + // Check every 60 seconds + sessionExpiryCheckInterval = setInterval(() => { + const tokenExpiry = localStorage.getItem('ppr_token_expiry'); + if (!tokenExpiry || !accessToken) { + stopSessionExpiryCheck(); + return; + } + + const now = Date.now(); + const expiryTime = parseInt(tokenExpiry); + const timeUntilExpiry = expiryTime - now; + + // Warn if less than 5 minutes remaining + if (timeUntilExpiry < 5 * 60 * 1000 && !sessionExpiryWarningShown) { + sessionExpiryWarningShown = true; + const minutesLeft = Math.ceil(timeUntilExpiry / 60000); + showNotification(`⚠️ Session expires in ${minutesLeft} minute${minutesLeft !== 1 ? 's' : ''}. Please save your work.`, true); + console.warn(`Session expires in ${minutesLeft} minutes`); + } + + // Force logout if expired + if (timeUntilExpiry <= 0) { + console.error('Session has expired'); + showNotification('Session expired. Please log in again.', true); + logout(); + } + }, 60000); // Check every minute + } + + function stopSessionExpiryCheck() { + if (sessionExpiryCheckInterval) { + clearInterval(sessionExpiryCheckInterval); + sessionExpiryCheckInterval = null; + } + sessionExpiryWarningShown = false; + } + // Authentication management async function initializeAuth() { // Try to get cached token @@ -1035,6 +1141,7 @@ currentUser = cachedUser; document.getElementById('current-user').textContent = cachedUser; await updateUserRole(); // Update role-based UI + startSessionExpiryCheck(); // Start monitoring session expiry connectWebSocket(); // Connect WebSocket for real-time updates loadPPRs(); return; @@ -1128,6 +1235,7 @@ hideLogin(); await updateUserRole(); // Update role-based UI + startSessionExpiryCheck(); // Start monitoring session expiry connectWebSocket(); // Connect WebSocket for real-time updates loadPPRs(); } else { @@ -1153,6 +1261,7 @@ accessToken = null; currentUser = null; + stopSessionExpiryCheck(); // Stop monitoring session disconnectWebSocket(); // Disconnect WebSocket // Close any open modals @@ -1298,6 +1407,13 @@ document.getElementById('arrivals-no-data').style.display = 'block'; return; } + + // Sort arrivals by ETA (ascending) + arrivals.sort((a, b) => { + if (!a.eta) return 1; + if (!b.eta) return -1; + return new Date(a.eta) - new Date(b.eta); + }); tbody.innerHTML = ''; document.getElementById('arrivals-table-content').style.display = 'block'; for (const ppr of arrivals) { @@ -1346,6 +1462,13 @@ document.getElementById('departures-no-data').style.display = 'block'; return; } + + // Sort departures by ETD (ascending), nulls last + departures.sort((a, b) => { + if (!a.etd) return 1; + if (!b.etd) return -1; + return new Date(a.etd) - new Date(b.etd); + }); tbody.innerHTML = ''; document.getElementById('departures-table-content').style.display = 'block'; for (const ppr of departures) { diff --git a/web/index.html b/web/index.html index 63178f9..21109d6 100644 --- a/web/index.html +++ b/web/index.html @@ -237,30 +237,29 @@ }; } - // Convert UTC time to local time (Europe/London) - function convertToLocalTime(utcTimeString) { - if (!utcTimeString) return ''; + // Convert UTC datetime to local time display + function convertToLocalTime(utcDateTimeString) { + if (!utcDateTimeString) return ''; - // Parse the time string (format: HH:MM or HH:MM:SS) - const timeParts = utcTimeString.split(':'); - if (timeParts.length < 2) return utcTimeString; - - // Create a date object with today's date and the UTC time - const now = new Date(); - const utcDate = new Date(Date.UTC( - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate(), - parseInt(timeParts[0]), - parseInt(timeParts[1]), - timeParts.length > 2 ? parseInt(timeParts[2]) : 0 - )); - - // Convert to local time - const localHours = utcDate.getHours().toString().padStart(2, '0'); - const localMinutes = utcDate.getMinutes().toString().padStart(2, '0'); - - return `${localHours}:${localMinutes}`; + try { + // Parse the ISO datetime string + const date = new Date(utcDateTimeString); + + // Check if valid date + if (isNaN(date.getTime())) { + console.error('Invalid date:', utcDateTimeString); + return utcDateTimeString; + } + + // Format as HH:MM in local time + const localHours = date.getHours().toString().padStart(2, '0'); + const localMinutes = date.getMinutes().toString().padStart(2, '0'); + + return `${localHours}:${localMinutes}`; + } catch (error) { + console.error('Error converting time:', error, utcDateTimeString); + return utcDateTimeString; + } } // Fetch and display arrivals