// ========================================== // GLOBAL NOTIFICATION SYSTEM - Right Sidebar // ========================================== // Right-side slide-out toolbar for notifications and quick actions // Supports browser system notifications (cross-platform) // Notification settings let notifSettings = { systemNotifEnabled: false, soundEnabled: false }; /** * Initialize global notification sidebar */ function initGlobalNotifications() { // Load settings from localStorage loadNotifSettings(); // Request notification permission if enabled if (notifSettings.systemNotifEnabled) { requestNotificationPermission(); } // Create sidebar if not exists if (!document.getElementById('notifSidebar')) { const sidebarHtml = `
`; const container = document.createElement('div'); container.id = 'notifSidebarContainer'; container.innerHTML = sidebarHtml; document.body.appendChild(container); // Initialize toggle state const toggle = document.getElementById('systemNotifToggle'); if (toggle) { toggle.checked = notifSettings.systemNotifEnabled; } } renderGlobalNotifications(); updateGlobalNotifBadge(); } /** * Load notification settings from localStorage */ function loadNotifSettings() { try { const saved = localStorage.getItem('ccw_notif_settings'); if (saved) { notifSettings = { ...notifSettings, ...JSON.parse(saved) }; } } catch (e) { console.error('[Notif] Failed to load settings:', e); } } /** * Save notification settings to localStorage */ function saveNotifSettings() { try { localStorage.setItem('ccw_notif_settings', JSON.stringify(notifSettings)); } catch (e) { console.error('[Notif] Failed to save settings:', e); } } /** * Toggle system notifications */ function toggleSystemNotifications(enabled) { notifSettings.systemNotifEnabled = enabled; saveNotifSettings(); if (enabled) { requestNotificationPermission(); } } /** * Request browser notification permission */ async function requestNotificationPermission() { if (!('Notification' in window)) { console.warn('[Notif] Browser does not support notifications'); return false; } if (Notification.permission === 'granted') { return true; } if (Notification.permission !== 'denied') { const permission = await Notification.requestPermission(); return permission === 'granted'; } return false; } /** * Show system notification (browser notification) */ function showSystemNotification(notification) { if (!notifSettings.systemNotifEnabled) return; if (!('Notification' in window)) return; if (Notification.permission !== 'granted') return; const typeIcon = { 'info': 'âšī¸', 'success': 'â ', 'warning': 'â ī¸', 'error': 'â' }[notification.type] || 'đ'; const title = `${typeIcon} ${notification.message}`; let body = ''; if (notification.source) { body = `[${notification.source}]`; } // Extract plain text from details if HTML formatted if (notification.details) { const detailText = notification.details.replace(/<[^>]*>/g, '').trim(); if (detailText) { body += body ? '\n' + detailText : detailText; } } try { const sysNotif = new Notification(title, { body: body.substring(0, 200), icon: '/favicon.ico', tag: `ccw-notif-${notification.id}`, requireInteraction: notification.type === 'error' }); // Click to open sidebar sysNotif.onclick = () => { window.focus(); if (!isNotificationPanelVisible) { toggleNotifSidebar(); } sysNotif.close(); }; // Auto close after 5s (except errors) if (notification.type !== 'error') { setTimeout(() => sysNotif.close(), 5000); } } catch (e) { console.error('[Notif] Failed to show system notification:', e); } } /** * Toggle notification sidebar visibility */ function toggleNotifSidebar() { isNotificationPanelVisible = !isNotificationPanelVisible; const sidebar = document.getElementById('notifSidebar'); const overlay = document.getElementById('notifSidebarOverlay'); const toggle = document.getElementById('notifSidebarToggle'); if (sidebar && overlay && toggle) { if (isNotificationPanelVisible) { sidebar.classList.add('open'); overlay.classList.add('show'); toggle.classList.add('hidden'); } else { sidebar.classList.remove('open'); overlay.classList.remove('show'); toggle.classList.remove('hidden'); } } } // Backward compatibility alias function toggleGlobalNotifications() { toggleNotifSidebar(); } /** * Add a global notification * @param {string} type - 'info', 'success', 'warning', 'error' * @param {string} message - Main notification message * @param {string|object} details - Optional details (string or object) * @param {string} source - Optional source identifier */ function addGlobalNotification(type, message, details = null, source = null) { // Format details if it's an object let formattedDetails = details; let rawDetails = details; // Keep raw for system notification if (details && typeof details === 'object') { formattedDetails = formatNotificationJson(details); rawDetails = JSON.stringify(details, null, 2); } else if (typeof details === 'string') { // Try to parse and format if it looks like JSON const trimmed = details.trim(); if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) { try { const parsed = JSON.parse(trimmed); formattedDetails = formatNotificationJson(parsed); rawDetails = JSON.stringify(parsed, null, 2); } catch (e) { // Not valid JSON, use as-is formattedDetails = details; } } } const notification = { id: Date.now(), type, message, details: formattedDetails, rawDetails: rawDetails, source, timestamp: new Date().toISOString(), read: false, expanded: false }; globalNotificationQueue.unshift(notification); // Keep only last 100 notifications if (globalNotificationQueue.length > 100) { globalNotificationQueue = globalNotificationQueue.slice(0, 100); } // Persist to localStorage if (typeof saveNotificationsToStorage === 'function') { saveNotificationsToStorage(); } renderGlobalNotifications(); updateGlobalNotifBadge(); // Show system notification instead of toast showSystemNotification(notification); } /** * Format JSON object for notification display * @param {Object} obj - Object to format * @returns {string} HTML formatted string */ function formatNotificationJson(obj) { if (obj === null || obj === undefined) return ''; if (typeof obj !== 'object') return String(obj); // Handle arrays if (Array.isArray(obj)) { if (obj.length === 0) return '(empty array)'; const items = obj.slice(0, 5).map((item, i) => { const itemStr = typeof item === 'object' ? JSON.stringify(item) : String(item); const truncated = itemStr.length > 60 ? itemStr.substring(0, 57) + '...' : itemStr; return `