qualora / static /js /alerts-controller.js
Pratham Amritkar
final
d833ce9
Raw
History Blame Contribute Delete
7.44 kB
/**
* 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 = '<p class="empty-message" style="padding:1.5rem;text-align:center;color:var(--md-on-surface-muted);font-size:0.85rem;">No new alerts</p>';
return;
}
list.innerHTML = alerts.map(alert => `
<div class="notification-item ${alert.acknowledged ? 'acknowledged' : ''}" data-alert-id="${escapeHTML(alert._id)}">
<span class="notification-icon">${getSeverityIcon(alert.severity)}</span>
<div class="notification-content">
<p class="notification-title">${escapeHTML(alert.message)}</p>
<p class="notification-time">${escapeHTML(formatTimeAgo(alert.created_at))}</p>
</div>
${!alert.acknowledged ? `
<button class="notification-ack-btn" data-alert-id="${escapeHTML(alert._id)}" title="Mark as read">
<span class="material-symbols-rounded">check</span>
</button>
` : ''}
</div>
`).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 `<span class="material-symbols-rounded" style="color:${color}">${icon}</span>`;
}
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();
}
})();