// ========================================== // 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 = `
🔔 Notifications 0
🔔
No notifications
System events and task updates will appear here
🔔
`; 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 `
[${i}] ${escapeHtml(truncated)}
`; }); if (obj.length > 5) { items.push(`
... +${obj.length - 5} more items
`); } return items.join(''); } // Handle objects const entries = Object.entries(obj); if (entries.length === 0) return '(empty object)'; const lines = entries.slice(0, 8).map(([key, val]) => { let valStr; let valClass = 'json-value'; if (val === null) { valStr = 'null'; valClass = 'json-null'; } else if (val === undefined) { valStr = 'undefined'; valClass = 'json-null'; } else if (typeof val === 'boolean') { valStr = val ? 'true' : 'false'; valClass = 'json-bool'; } else if (typeof val === 'number') { valStr = String(val); valClass = 'json-number'; } else if (typeof val === 'object') { valStr = JSON.stringify(val); if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...'; valClass = 'json-object'; } else { valStr = String(val); if (valStr.length > 60) valStr = valStr.substring(0, 57) + '...'; valClass = 'json-string'; } return `
${escapeHtml(key)}: ${escapeHtml(valStr)}
`; }); if (entries.length > 8) { lines.push(`
... +${entries.length - 8} more fields
`); } return lines.join(''); } /** * Toggle notification item expansion */ function toggleNotifExpand(notifId) { const notif = globalNotificationQueue.find(n => n.id === notifId); if (notif) { notif.expanded = !notif.expanded; renderGlobalNotifications(); } } /** * Render notification list in sidebar */ function renderGlobalNotifications() { const contentEl = document.getElementById('notifSidebarContent'); if (!contentEl) return; if (globalNotificationQueue.length === 0) { contentEl.innerHTML = `
🔔
No notifications
System events and task updates will appear here
`; return; } contentEl.innerHTML = globalNotificationQueue.map(notif => { const typeIcon = { 'info': 'â„šī¸', 'success': '✅', 'warning': 'âš ī¸', 'error': '❌' }[notif.type] || 'â„šī¸'; const time = formatNotifTime(notif.timestamp); const sourceLabel = notif.source ? `${escapeHtml(notif.source)}` : ''; const hasDetails = notif.details && notif.details.length > 0; const expandIcon = hasDetails ? (notif.expanded ? 'â–ŧ' : 'â–ļ') : ''; // Details section - collapsed by default, show preview let detailsHtml = ''; if (hasDetails) { if (notif.expanded) { // Expanded view - show full details if (typeof notif.details === 'string' && notif.details.includes('class="json-')) { detailsHtml = `
${notif.details}
`; } else { detailsHtml = `
${escapeHtml(String(notif.details))}
`; } } else { // Collapsed view - show hint detailsHtml = `
Click to view details
`; } } return `
${typeIcon}
${escapeHtml(notif.message)} ${sourceLabel}
${hasDetails ? `${expandIcon}` : ''}
${detailsHtml}
${time}
`; }).join(''); } /** * Format notification time */ function formatNotifTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return 'Just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleDateString(); } /** * Update notification badge counts */ function updateGlobalNotifBadge() { const unreadCount = globalNotificationQueue.filter(n => !n.read).length; const countBadge = document.getElementById('notifCountBadge'); const toggleBadge = document.getElementById('notifToggleBadge'); if (countBadge) { countBadge.textContent = globalNotificationQueue.length; countBadge.style.display = globalNotificationQueue.length > 0 ? 'inline-flex' : 'none'; } if (toggleBadge) { toggleBadge.textContent = unreadCount; toggleBadge.style.display = unreadCount > 0 ? 'flex' : 'none'; } } /** * Clear all notifications */ function clearGlobalNotifications() { globalNotificationQueue = []; if (typeof saveNotificationsToStorage === 'function') { saveNotificationsToStorage(); } renderGlobalNotifications(); updateGlobalNotifBadge(); } /** * Mark all as read */ function markAllNotificationsRead() { globalNotificationQueue.forEach(n => n.read = true); if (typeof saveNotificationsToStorage === 'function') { saveNotificationsToStorage(); } renderGlobalNotifications(); updateGlobalNotifBadge(); }