diff --git a/package-lock.json b/package-lock.json index d6c6f6f..3eb31ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "express": "^4.19.2", "sqlite3": "^5.1.7", - "ws": "^8.18.0" + "ws": "^8.19.0" } }, "node_modules/@gar/promisify": { diff --git a/package.json b/package.json index 61a069a..73f0a29 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ "dependencies": { "express": "^4.19.2", "sqlite3": "^5.1.7", - "ws": "^8.18.0" + "ws": "^8.19.0" } } diff --git a/public/app.js b/public/app.js index 5659b26..2ee8fed 100644 --- a/public/app.js +++ b/public/app.js @@ -1444,3 +1444,92 @@ function makeTaskCardDraggable(cardEl, task) { cardEl.classList.remove('dragging'); }); } + +// ============ WEBSOCKET REAL-TIME UPDATES ============ +let ws = null; +let reconnectAttempts = 0; +const MAX_RECONNECT = 5; + +function initWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}`; + + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log('WebSocket connected'); + reconnectAttempts = 0; + }; + + ws.onmessage = (event) => { + try { + const { type, payload } = JSON.parse(event.data); + handleWebSocketMessage(type, payload); + } catch (err) { + console.error('WebSocket message error:', err); + } + }; + + ws.onclose = () => { + console.log('WebSocket disconnected'); + if (reconnectAttempts < MAX_RECONNECT) { + reconnectAttempts++; + setTimeout(initWebSocket, 2000 * reconnectAttempts); + } + }; + + ws.onerror = (err) => { + console.error('WebSocket error:', err); + }; +} + +function handleWebSocketMessage(type, payload) { + switch (type) { + case 'task_created': + if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { + loadTasks(); + showNotification(`New task: ${payload.title}`); + } + break; + + case 'task_updated': + if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { + loadTasks(); + } + break; + + case 'task_deleted': + if (typeof loadTasks === 'function' && CURRENT_PAGE === 'tasks') { + loadTasks(); + showNotification('Task deleted'); + } + break; + + case 'usage_updated': + if (typeof loadUsage === 'function' && CURRENT_PAGE === 'usage') { + loadUsage(); + } + break; + + default: + console.log('Unknown WebSocket message:', type); + } +} + +function showNotification(message) { + // Create a simple toast notification + const toast = document.createElement('div'); + toast.className = 'toast-notification'; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + toast.classList.add('fade-out'); + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +// Initialize WebSocket on page load +if (typeof CURRENT_PAGE !== 'undefined') { + initWebSocket(); +} diff --git a/public/styles.css b/public/styles.css index 525222d..565a850 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1025,3 +1025,41 @@ label { background: var(--primary); color: white; } + +/* Toast Notifications */ +.toast-notification { + position: fixed; + bottom: 20px; + right: 20px; + background: var(--primary); + color: white; + padding: 1rem 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + animation: slideIn 0.3s ease; +} + +.toast-notification.fade-out { + animation: fadeOut 0.3s ease forwards; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +}