feat: add WebSocket real-time updates with toast notifications (#5)

This commit is contained in:
2026-03-04 18:14:34 -08:00
parent cd53a4e612
commit 6fe05ba756
4 changed files with 129 additions and 2 deletions

View File

@@ -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();
}

View File

@@ -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;
}
}