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