/** * Enhanced Notification System for LibrePortal * Provides consistent, modular notifications with app icons and proper layout */ class NotificationSystem { constructor() { this.container = null; this.init(); } init() { // Create notification container this.container = document.createElement('div'); this.container.className = 'notification-container'; document.body.appendChild(this.container); // Restore any pending notifications from localStorage this.restoreNotifications(); } /** * Show a notification with consistent layout * Layout: [Type Icon] [App Icon] [Message] [Action Button] [Close] * * `customIcon` is an optional override for the leftmost icon slot — when * supplied, the notification renders that string/HTML there instead of * the level-derived SVG (success tick / error cross / warning triangle). * Used by task notifications so the icon reflects the task *type* * (install ✅, backup 💾, restore 📦, …) rather than just success/fail. */ show(message, type = 'info', appName = null, appUrl = null, appIcon = null, customIcon = null) { const notification = this.createNotificationElement(type, message, appName, appUrl, appIcon, customIcon); this.addNotificationToContainer(notification); this.saveNotificationToStorage({ message, type, appName, appUrl, appIcon, customIcon }); this.setupAutoRemove(notification); return notification; } /** * Create notification element with consistent structure */ createNotificationElement(type, message, appName, appUrl, appIcon, customIcon = null) { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; // Add data attributes for dynamic sizing if (appIcon) { notification.setAttribute('data-has-app', 'true'); } if (appName && appUrl) { notification.setAttribute('data-has-action', 'true'); } const typeIcon = customIcon != null && customIcon !== '' ? customIcon : this.getIcon(type); const content = this.buildNotificationContent(typeIcon, message, appName, appUrl, appIcon, type); notification.innerHTML = content; // Attach event listeners dynamically for action button if (appName && appUrl) { const actionBtn = notification.querySelector('.notification-action-btn'); if (actionBtn) { actionBtn.addEventListener('click', (e) => { console.log('🔗 Notification action button clicked for URL:', appUrl); e.preventDefault(); e.stopPropagation(); if (window.handleNotificationNavigation) { window.handleNotificationNavigation(appUrl); } else { console.error('❌ handleNotificationNavigation not available'); } }); // Remove any other click handlers that might interfere actionBtn.style.cursor = 'pointer'; } else { console.warn('⚠️ Action button not found in notification'); } } // Attach event listener for close button const closeBtn = notification.querySelector('.notification-close'); if (closeBtn) { closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); notification.remove(); }); } return notification; } /** * Build notification HTML content */ buildNotificationContent(typeIcon, message, appName, appUrl, appIcon, type) { let content = '
'; // Type icon (always first) content += `
${typeIcon}
`; // App icon (if provided) if (appIcon) { content += `
${appName}
`; } // Message section content += `
${message}
`; // Action button (context-aware) if (appName && appUrl) { let buttonText = 'Manage'; if (appUrl.includes('task=')) { buttonText = 'View Task'; } else if (type === 'success' && message.includes('install')) { buttonText = 'Configure'; } else if (type === 'info' || type === 'warning') { buttonText = 'View Task'; } content += ` `; } // Close button (always last) content += `
`; return content; } /** * Get icon based on notification type */ getIcon(type) { const icons = { success: '', error: '', warning: '', info: '', uninstall: '' }; return icons[type] || icons.info; } /** * Add notification to container with animation */ addNotificationToContainer(notification) { this.container.appendChild(notification); // Trigger animation setTimeout(() => { notification.classList.add('notification-show'); }, 10); } /** * Setup auto-remove after 10 seconds */ setupAutoRemove(notification) { setTimeout(() => { if (notification.parentElement) { notification.classList.add('notification-hide'); setTimeout(() => { if (notification.parentElement) { notification.remove(); } }, 300); } }, 10000); } /** * Save notification to localStorage for cross-page persistence */ saveNotificationToStorage(notificationData) { try { const notifications = JSON.parse(localStorage.getItem('libreportal_notifications') || '[]'); notificationData.id = Date.now().toString(); notificationData.timestamp = Date.now(); notifications.push(notificationData); // Keep only last 5 notifications to avoid clutter if (notifications.length > 5) { notifications.shift(); } localStorage.setItem('libreportal_notifications', JSON.stringify(notifications)); } catch (error) { console.error('Error saving notification to localStorage:', error); } } /** * Restore notifications from localStorage on page load */ restoreNotifications() { try { const notifications = JSON.parse(localStorage.getItem('libreportal_notifications') || '[]'); const now = Date.now(); notifications.forEach(notificationData => { const age = now - notificationData.timestamp; const remainingTime = 10000 - age; // 10 seconds if (remainingTime > 0) { const notification = this.createNotificationElement( notificationData.type, notificationData.message, notificationData.appName, notificationData.appUrl, notificationData.appIcon, notificationData.customIcon ); this.addNotificationToContainer(notification); this.setupAutoRemove(notification, remainingTime); } }); } catch (error) { console.error('Error restoring notifications:', error); } } /** * Remove notification from localStorage */ removeNotification(notificationId) { try { const notifications = JSON.parse(localStorage.getItem('libreportal_notifications') || '[]'); const filteredNotifications = notifications.filter(n => n.id !== notificationId); localStorage.setItem('libreportal_notifications', JSON.stringify(filteredNotifications)); } catch (error) { console.error('Error removing notification:', error); } } /** * Convenience methods */ success(message, appName = null, appUrl = null, appIcon = null) { return this.show(message, 'success', appName, appUrl, appIcon); } error(message, appName = null, appUrl = null, appIcon = null) { return this.show(message, 'error', appName, appUrl, appIcon); } warning(message, appName = null, appUrl = null, appIcon = null) { return this.show(message, 'warning', appName, appUrl, appIcon); } info(message, appName = null, appUrl = null, appIcon = null) { return this.show(message, 'info', appName, appUrl, appIcon); } /** * Show install notification with short app name */ showInstallNotification(appName, appUrl, appIcon = null) { const shortName = appName.split(' - ')[0]; const message = `${shortName} has been installed.`; return this.show(message, 'success', appName, appUrl, appIcon); } /** * Show uninstall notification with short app name */ showUninstallNotification(appName, appIcon = null) { const shortName = appName.split(' - ')[0]; const message = `${shortName} has been uninstalled.`; return this.show(message, 'uninstall', appName, null, appIcon); } } // Notification system initialization is now handled by SystemLoader // NotificationSystem instance will be created centrally // Resolve a slug (e.g. "ipinfo") to its proper display name from window.apps // (e.g. "IPInfo"). Falls back to a capitalized slug if window.apps isn't loaded // or no match is found. The slug is the trailing token of an app's `command`. window.getAppDisplayName = function (slug) { if (!slug) return ''; const apps = window.apps || []; const match = apps.find(a => { const command = a.command || ''; return command.endsWith(` ${slug}`) || a.name?.toLowerCase() === slug.toLowerCase(); }); if (match && match.name) { return match.name.split(' - ')[0].trim(); } return slug.charAt(0).toUpperCase() + slug.slice(1); }; // Expose helper functions globally window.ensureNotificationSystem = () => { if (!window.notificationSystem) { console.warn('Notification system not initialized, creating new instance'); window.notificationSystem = new NotificationSystem(); } return window.notificationSystem; }; window.removeNotification = (notificationId) => { if (window.notificationSystem) { window.notificationSystem.removeNotification(notificationId); } }; window.handleNotificationNavigation = (url) => { try { console.log('🔗 handleNotificationNavigation called with URL:', url); // Parse the URL to extract task ID and app name const urlObj = new URL(url, window.location.origin); const urlParams = new URLSearchParams(urlObj.search); // Extract app name from ?= parameter (the key is empty string, = is the separator) const appName = urlParams.get('') || urlParams.get('='); const taskId = urlParams.get('task'); const tab = urlParams.get('tab') || 'tasks'; console.log('🔗 Parsed URL:', { url, appName, taskId, tab, currentPath: window.location.pathname }); // Check if we're on an app page or tasks page const currentPath = window.location.pathname; if (currentPath.includes('/app') && appName) { console.log('🔗 On app page, appTabbedManager available:', !!window.appTabbedManager); console.log('🔗 Current app:', window.appTabbedManager?.currentApp, 'Target app:', appName); // We're on an app page - navigate to the specified app and tab if (window.appTabbedManager) { // Update the URL to the target app/tab/task const newUrl = `/app?=${appName}&tab=${tab}&task=${taskId}`; console.log('🔗 Pushing state to URL:', newUrl); window.history.pushState({}, '', newUrl); // If already on this app, just switch tab and highlight task if (window.appTabbedManager.currentApp === appName) { console.log('🔗 Same app, switching to tab:', tab); window.appTabbedManager.switchTab(tab); if (tab === 'tasks' && taskId) { // Wait for tasks to load and render, then open the task details setTimeout(() => { console.log('🔗 Opening task details for:', taskId); if (typeof window.toggleAppTaskDetails === 'function') { window.toggleAppTaskDetails(taskId); // Scroll to task after opening details if (window.appTabbedManager && window.appTabbedManager.scrollToTask) { window.appTabbedManager.scrollToTask(taskId); } } }, 800); } console.log('🔗 Navigation completed successfully'); } else { // Different app - need to load the full app detail console.log('🔗 Different app, loading app detail for:', appName); window.appTabbedManager.showAppDetail(appName); // Schedule the tab switch and task highlight after app loads setTimeout(() => { if (window.appTabbedManager) { console.log('🔗 Switching to tab:', tab); window.appTabbedManager.switchTab(tab); if (tab === 'tasks' && taskId) { // Wait for tasks to load and render, then open the task details setTimeout(() => { console.log('🔗 Opening task details for:', taskId); if (typeof window.toggleAppTaskDetails === 'function') { window.toggleAppTaskDetails(taskId); // Scroll to task after opening details if (window.appTabbedManager && window.appTabbedManager.scrollToTask) { window.appTabbedManager.scrollToTask(taskId); } } }, 800); } } }, 500); console.log('🔗 Navigation to different app started'); } return true; } else { console.warn('⚠️ appTabbedManager not available'); } } else if (currentPath.includes('/tasks')) { // We're on the tasks page, navigate to the specified task if (taskId) { console.log('🔗 On tasks page, opening task:', taskId); window.history.pushState({}, '', `/tasks?=all&task=${taskId}`); setTimeout(() => { if (typeof window.toggleTaskDetails === 'function') { console.log('🔗 Opening task details for:', taskId); window.toggleTaskDetails(taskId); } }, 300); return true; } } else { // Not on app or tasks page - navigate to the app's tasks tab if (appName && tab) { window.history.pushState({}, '', `/app?=${appName}&tab=${tab}&task=${taskId}`); // Let the SPA handle the navigation if (window.appTabbedManager) { window.appTabbedManager.showAppDetail(appName); setTimeout(() => { if (window.appTabbedManager) { window.appTabbedManager.switchTab(tab); if (window.tasksManager && taskId) { window.tasksManager.highlightedTaskId = taskId; if (window.tasksManager.loadTaskLogs) { window.tasksManager.loadTaskLogs(taskId); } } } }, 500); return true; } } } // If we get here and no managers were available, fallback console.warn('⚠️ Falling back to page reload for URL:', url); window.location.href = url; return false; } catch (error) { console.error('❌ Error handling notification navigation:', error); // Fallback to direct navigation if parsing fails console.warn('⚠️ Falling back to page reload due to error for URL:', url); window.location.href = url; return false; } };