/** * alerts-controller.js - Notification Management with Acknowledgement * =================================================================== * Handles polling, rendering, and acknowledging system alerts. * * Hardened for Enterprise MVC: * - Uses central `apiFetch` to automatically inherit HTTP-Only Cookies and CSRF headers. * - Relies on `fetch-wrapper.js` for 401 session expiration handling. * - Uses `SecurityUtils` for XSS-safe DOM generation. */ (function() { 'use strict'; // Safe HTML Escaping (Fallback to native if SecurityUtils isn't loaded yet) const escapeHTML = (text) => { if (window.SecurityUtils) return window.SecurityUtils.escapeHTML(text); const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; // ── Data Fetching ─────────────────────────────────────────────────────── async function loadAlerts() { // Only fetch if user is authenticated (UI hint check) const user = JSON.parse(localStorage.getItem('user') || 'null'); if (!user || (!user.email && !user.temp)) return; try { // Enterprise Architecture: Centralized apiFetch handles CSRF, Cookies, and 401s automatically const data = await window.apiFetch('/alerts?limit=20'); if (data.success) { renderAlerts(data.alerts || []); // Ensure we only count unacknowledged alerts for the badge const unread = data.alerts ? data.alerts.filter(a => !a.acknowledged).length : 0; updateBadgeCount(unread); } } catch (error) { // Silently fail for background polling to prevent spamming the user, // but log for debugging purposes. console.error('[Alerts] Background sync failed:', error); } } // ── Rendering ─────────────────────────────────────────────────────────── function renderAlerts(alerts) { const list = document.querySelector('.notifications-list'); if (!list) return; if (alerts.length === 0) { list.innerHTML = '

No new alerts

'; return; } list.innerHTML = alerts.map(alert => `
${getSeverityIcon(alert.severity)}

${escapeHTML(alert.message)}

${escapeHTML(formatTimeAgo(alert.created_at))}

${!alert.acknowledged ? ` ` : ''}
`).join(''); list.querySelectorAll('.notification-ack-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); acknowledgeAlert(btn.dataset.alertId); }); }); } async function acknowledgeAlert(alertId) { try { // Use apiFetch so CSRF headers are automatically injected const data = await window.apiFetch(`/alerts/${alertId}`, { method: 'PUT' }); if (data.success) { const el = document.querySelector(`[data-alert-id="${alertId}"]`); if (el) { el.style.opacity = '0'; el.style.transform = 'translateX(20px)'; el.style.transition = '0.3s ease'; setTimeout(() => { el.remove(); loadAlerts(); // Refresh to update count and get fresh data }, 300); } } } catch (error) { console.error('[Alerts] Ack failed:', error); if (window.ErrorHandler) { window.ErrorHandler.showError(error); } else if (window.SecurityUtils) { window.SecurityUtils.showToast('Failed to acknowledge alert', 'error'); } } } // ── Utilities ─────────────────────────────────────────────────────────── function updateBadgeCount(count) { const badge = document.getElementById('notification-count'); if (!badge) return; badge.textContent = count > 99 ? '99+' : count; badge.hidden = (count <= 0); } function getSeverityIcon(sev) { const icons = { 'critical': ['warning', 'var(--md-error)'], 'high': ['error', 'var(--md-error)'], 'warning': ['info', 'var(--md-warning)'], 'medium': ['info', 'var(--md-warning)'] }; const [icon, color] = icons[sev.toLowerCase()] || ['notifications', 'var(--md-on-surface-muted)']; return `${icon}`; } function formatTimeAgo(dateStr) { if (!dateStr) return 'Unknown'; const date = new Date(dateStr); if (isNaN(date.getTime())) return 'Unknown'; const seconds = Math.floor((Date.now() - date.getTime()) / 1000); if (seconds < 60) return 'Just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; return `${Math.floor(seconds / 86400)}d ago`; } // ── Initialization & Lifecycle ────────────────────────────────────────── function init() { // Prevent Multiple Polling Intervals (Memory Leak Guard for SPAs) if (window.AlertsController && window.AlertsController._poll) { clearInterval(window.AlertsController._poll); } // Only begin polling if user is authenticated const user = JSON.parse(localStorage.getItem('user') || 'null'); if (!user || (!user.email && !user.temp)) { console.info('[Alerts] User not authenticated. Alerts polling disabled.'); return; } loadAlerts(); // 30s polling interval const _poll = setInterval(loadAlerts, 30000); // Expose globally to allow manual refresh and clean teardown window.AlertsController = { reload: loadAlerts, _poll: _poll, stop: () => clearInterval(_poll) }; console.log('✅ Qualora Alerts Controller Initialized'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();