// System Loader - Centralized loading and health check system for LibrePortal class SystemLoader { constructor() { this.systems = new Map(); this.checks = new Map(); this.components = new Map(); this.progress = 0; this.totalChecks = 0; this.completedChecks = 0; this.errors = []; this.isFirstTime = false; this.callbacks = { onProgress: [], onSystemCheck: [], onComplete: [], onError: [] }; this.initializeSystems(); this.initializeHealthChecks(); // console.log('🚨 BREAKPOINT: About to call initializeComponents()'); this.initializeComponentRegistry(); // console.log('BREAKPOINT: initializeComponents() completed'); } // Initialize component registry initializeComponentRegistry() { // Config Manager - Essential for config pages this.components.set('config-manager', { system: 'managers', initializer: () => { if (typeof ConfigManager !== 'undefined') { window.configManager = new ConfigManager(); return window.configManager; } return null; }, global: 'configManager', dependencies: ['data'], scripts: [ '/js/components/config/config-options.js', '/js/components/config/config-shared.js', '/js/components/config/config-validator.js', '/js/components/config/toggle-manager.js', '/js/components/config/config-core.js', '/js/components/config/domain-manager.js', '/js/components/config/ip-whitelist-manager.js', '/js/components/config/config-renderer.js', '/js/components/config/config-sidebar.js', '/js/components/config/config-form.js', '/js/components/config/config-utils.js', '/js/components/config/config-manager.js' ] }); // Mobile Menu this.components.set('mobile-menu', { system: 'components', initializer: () => { if (typeof setupMobileMenu === 'function') { setupMobileMenu(); // console.log('✅ Mobile menu initialized'); return true; } console.warn('⚠️ setupMobileMenu not available'); return null; }, global: null, dependencies: [], scripts: [ '/js/components/topbar.js', '/js/components/mobile-menu.js' ] }); // Confirmation Dialog this.components.set('confirmation-dialog', { system: 'components', initializer: () => { if (typeof initConfirmationDialog === 'function') { initConfirmationDialog(); // console.log('✅ Confirmation dialog initialized'); return true; } console.warn('⚠️ initConfirmationDialog not available'); return null; }, global: null, dependencies: [], script: '/js/components/confirmation-dialog.js' }); // Notifications this.components.set('notifications', { system: 'components', initializer: () => { if (typeof NotificationSystem !== 'undefined') { window.notificationSystem = new NotificationSystem(); // console.log('✅ Notification system initialized'); return window.notificationSystem; } console.warn('⚠️ NotificationSystem not available'); return null; }, global: 'notificationSystem', dependencies: [], script: '/js/components/notifications.js' }); // Dashboard this.components.set('dashboard', { system: 'core', initializer: () => { // Initialize data loading system if (typeof initializeData === 'function') { initializeData(); // console.log('✅ Data loading system initialized'); } // Initialize dashboard-specific functions if (typeof loadSystemInfo === 'function') { loadSystemInfo(); // console.log('✅ System info loaded'); } if (typeof setupEventListeners === 'function') { setupEventListeners(); // console.log('✅ Dashboard event listeners set up'); } return true; }, global: null, dependencies: [] // data-loader.js is now loaded in core HTML }); // Task System - Load all task-related scripts together this.components.set('task-system', { system: 'task', initializer: () => { // console.log('🔧 Initializing Task System...'); // Open the SSE feed before anything else so we don't miss the upserts // that fire as the page boots. if (window.taskEventBus && typeof window.taskEventBus.start === 'function') { window.taskEventBus.start(); } // Initialize task global functions if (typeof setupTaskGlobalFunctions === 'function') { setupTaskGlobalFunctions(); } // Create TasksManager instance if (typeof TasksManager !== 'undefined') { window.tasksManager = new TasksManager(); // console.log('✅ TasksManager initialized and available globally'); return window.tasksManager; } console.warn('⚠️ TasksManager not available'); return null; }, global: 'tasksManager', dependencies: [], scripts: [ '/js/components/task/task-event-bus.js', '/js/components/task/task-commands.js', '/js/components/task/task-actions.js', '/js/components/task/task-router.js', '/js/components/task/task-global-functions.js', '/js/components/task/task-manager.js', '/js/task-parameter-preserve.js', '/js/components/tasks/tasks-manager.js' ] }); // Apps Manager - Essential for app management this.components.set('apps-manager', { system: 'managers', initializer: () => { // console.log('🔧 DEBUG: AppsManager initializer called'); // console.log('🔧 DEBUG: AppsManager class available:', typeof AppsManager !== 'undefined'); // console.log('🔧 DEBUG: Available globals:', Object.keys(window).filter(key => key.includes('Manager'))); if (typeof AppsManager !== 'undefined') { // console.log('🔧 DEBUG: Creating AppsManager instance...'); try { window.appsManager = new AppsManager(); // console.log('✅ AppsManager instance created successfully'); return window.appsManager; } catch (error) { console.error('❌ Failed to create AppsManager instance:', error); return null; } } else { console.error('❌ AppsManager class not available after script loading'); return null; } }, global: 'appsManager', dependencies: ['data'], scripts: [ '/js/components/app/port-manager.js', '/js/components/task/task-manager.js', // Add TaskManager for backup functionality '/js/components/backup/backup-app-card.js', '/js/components/app/services-manager.js', '/js/components/app/tools-manager.js', '/js/components/app/routing-manager.js', '/js/components/app/apps-manager.js' ] }); // App Tabbed Manager - Essential for app detail pages this.components.set('app-tabbed-manager', { system: 'managers', initializer: () => { // console.log('🔧 DEBUG: Attempting to initialize AppTabbedManager...'); // console.log('🔧 DEBUG: AppTabbedManager class available:', typeof AppTabbedManager !== 'undefined'); // console.log('🔧 DEBUG: Available globals:', Object.keys(window).filter(key => key.includes('Tabbed') || key.includes('Manager'))); if (typeof AppTabbedManager !== 'undefined') { window.appTabbedManager = new AppTabbedManager(); // console.log('✅ App Tabbed Manager initialized'); return window.appTabbedManager; } console.warn('⚠️ AppTabbedManager not available'); return null; }, global: 'appTabbedManager', dependencies: [], script: '/js/components/app/app-tabbed-manager.js' }); // console.log('TEST: Components added. Total components:', this.components.size); // console.log('TEST: Method completed'); } // Initialize all system definitions initializeSystems() { this.systems.set('core', { name: 'Core System', description: 'Essential system components', priority: 1, dependencies: [], checks: ['dom', 'spa', 'utils'] }); // console.log('🔍 Core System checks:', ['dom', 'spa', 'utils']); this.systems.set('data', { name: 'Data Functions', description: 'Data loading functions availability', priority: 2, dependencies: ['core'], checks: [] }); // console.log('🔍 Data Functions checks:', []); this.systems.set('components', { name: 'UI Components', description: 'User interface components', priority: 3, dependencies: ['core'], checks: ['topbar', 'confirmation-dialog', 'notifications', 'icon-preloading'] }); this.systems.set('task', { name: 'Task System', description: 'Task management and operations', priority: 4, dependencies: ['core'], checks: ['task-manager'] }); this.systems.set('managers', { name: 'System Managers', description: 'Application managers and services', priority: 5, dependencies: ['core', 'task'], checks: ['apps-manager', 'config-manager', 'app-tabbed-manager'] }); this.systems.set('backend', { name: 'Backend Services', description: 'External service connectivity', priority: 6, dependencies: ['core'], checks: ['server-connectivity', 'api-endpoints'] }); this.systems.set('config-validation', { name: 'Config Files', description: 'Configuration files validation', priority: 7, dependencies: ['core', 'task'], checks: ['config-files'], critical: true }); this.systems.set('update-lock', { name: 'Update Lock Check', description: 'Check for update lock file to prevent concurrent updates', priority: 8, dependencies: ['core'], checks: ['update-lock'], critical: false, check: async () => this.checkUpdateLock() }); this.systems.set('pause-lock', { name: 'Pause Lock Check', description: 'Check for pause lock file to prevent operations during pause', priority: 8.1, dependencies: ['core'], checks: ['pause-lock'], critical: false, check: async () => this.checkPauseLock() }); } // Initialize health check functions initializeHealthChecks() { // Core checks this.checks.set('dom', { name: 'DOM Ready', description: 'Document Object Model loaded', weight: 1, timeout: 5000, check: () => document.readyState === 'complete' || document.readyState === 'interactive' }); this.checks.set('spa', { name: 'SPA Router', description: 'Single Page Application router', weight: 2, timeout: 5000, check: () => typeof LibrePortalSPAClean !== 'undefined' }); this.checks.set('utils', { name: 'Utility Functions', description: 'Helper utilities and tools', weight: 1, timeout: 5000, check: async () => { // Wait a moment for scripts to load await new Promise(resolve => setTimeout(resolve, 500)); // Check for essential utilities that should be loaded statically const hasSafeGetElement = typeof safeGetElement === 'function'; const hasSafeQuerySelector = typeof safeQuerySelector === 'function'; const hasGetAppIcon = typeof getAppIcon === 'function'; // Check for utilities that will be loaded dynamically const hasDataLoader = typeof DataLoader !== 'undefined'; const hasInitializeData = typeof initializeData === 'function'; // console.log('🔍 Utils Check:', { //hasDataLoader, //hasSafeGetElement, //hasSafeQuerySelector, //hasGetAppIcon, //hasSetupThemeToggle, //hasInitializeData, // Check what's actually available globally //availableGlobals: Object.keys(window).filter(key => //key.includes('safe') || //key.includes('get') || //key.includes('Data') || //key.includes('Theme') || //key.includes('initialize') //).slice(0, 10) //}); // More flexible check - require at least 2 of the essential utilities // Data loader and theme toggle will be loaded dynamically, so don't require them here const essentialUtilitiesCount = [hasSafeGetElement, hasSafeQuerySelector, hasGetAppIcon].filter(Boolean).length; return essentialUtilitiesCount >= 2; } }); // Data Loader health check this.checks.set('data-loader', { name: 'Data Loader', description: 'Data loading and management system', weight: 2, timeout: 5000, check: async () => { // Wait a moment for data-loader to load await new Promise(resolve => setTimeout(resolve, 1000)); const hasDataLoader = typeof DataLoader !== 'undefined'; const hasInitializeData = typeof initializeData === 'function'; const hasLoadDashboardData = typeof loadDashboardData === 'function'; const hasLoadAppsPageData = typeof loadAppsPageData === 'function'; const hasLoadConfigDetailData = typeof loadConfigDetailData === 'function'; const hasLoadMinimalData = typeof loadMinimalData === 'function'; // console.log('🔍 Data Loader Check:', { //hasDataLoader, //hasInitializeData, //hasLoadDashboardData, //hasLoadAppsPageData, //hasLoadConfigDetailData, //hasLoadMinimalData, //availableDataFunctions: Object.keys(window).filter(key => //key.includes('load') || //key.includes('Data') || //key.includes('initialize') //).slice(0, 10) //}); // Check if data loader functions are available return hasInitializeData && (hasLoadDashboardData || hasLoadAppsPageData || hasLoadConfigDetailData); } }); // Note: Individual data checks (apps-data, categories-data, config-data) removed // They are now handled by the comprehensive config-files check above // Component checks - always pass since they're loaded dynamically this.checks.set('topbar', { name: 'Topbar Component', description: 'Navigation topbar', weight: 2, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 Topbar Check: Will be loaded dynamically during component initialization'); // Always pass - TopbarComponent will be loaded dynamically by mobile-menu component return true; } }); this.checks.set('confirmation-dialog', { name: 'Confirmation Dialog', description: 'User confirmation dialog system', weight: 1, timeout: 1000, check: async () => { // console.log('🔍 Confirmation Dialog Check: Will be loaded dynamically during component initialization'); return true; } }); this.checks.set('notifications', { name: 'Notification System', description: 'User notification system', weight: 1, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 Notification System Check: Will be loaded dynamically during component initialization'); // Always pass - NotificationSystem will be loaded dynamically by notifications component return true; } }); this.checks.set('icon-preloading', { name: 'Icon Preloading', description: 'Preload app center icons for faster loading', weight: 2, timeout: 15000, check: async () => this.checkIconPreloading() }); // Manager checks - always pass since they're loaded dynamically this.checks.set('apps-manager', { name: 'Apps Manager', description: 'Application management system', weight: 3, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 Apps Manager Check: Will be loaded dynamically during component initialization'); // Always pass - AppsManager will be loaded dynamically by apps-manager component return true; } }); this.checks.set('task-manager', { name: 'Task Manager', description: 'Background task management', weight: 3, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 Task Manager Check: Will be loaded dynamically during component initialization'); // Always pass - TasksManager will be loaded dynamically by task-manager component return true; } }); this.checks.set('config-manager', { name: 'Configuration Manager', description: 'System configuration management', weight: 2, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 Config Manager Check: Will be loaded dynamically during component initialization'); // Always pass - ConfigManager will be loaded dynamically by config-manager component return true; } }); this.checks.set('app-tabbed-manager', { name: 'App Tabbed Manager', description: 'Application tabbed interface management', weight: 3, timeout: 1000, // Reduced timeout since we're not actually checking check: async () => { // console.log('🔍 App Tabbed Manager Check: Will be loaded dynamically during component initialization'); // Always pass - AppTabbedManager will be loaded dynamically by app-tabbed-manager component return true; } }); // Backend checks this.checks.set('server-connectivity', { name: 'Server Connectivity', description: 'Basic server connectivity', weight: 2, timeout: 8000, check: async () => { try { const response = await fetch('/', { method: 'HEAD' }); return response.ok; } catch { return false; } } }); this.checks.set('api-endpoints', { name: 'API Endpoints', description: 'Backend API availability', weight: 3, timeout: 10000, check: async () => { const endpoints = [ '/data/apps/generated/apps.json', '/data/apps/apps-categories.json', '/data/config/generated/configs.json' ]; try { const results = await Promise.allSettled( endpoints.map(endpoint => fetch(endpoint, { method: 'HEAD' })) ); return results.some(result => result.status === 'fulfilled' && result.value.ok ); } catch { return false; } } }); // Update Lock Check this.checks.set('update-lock', { name: 'Update Lock', description: 'Check for update lock file to prevent concurrent updates', weight: 2, timeout: 10000, check: async () => this.checkUpdateLock() }); // Pause Lock Check this.checks.set('pause-lock', { name: 'Pause Lock', description: 'Check for pause lock file to prevent operations during pause', weight: 2, timeout: 10000, check: async () => this.checkPauseLock() }); // Config Files this.checks.set('config-files', { name: 'Config Files', description: 'Required configuration files availability', weight: 2, timeout: 15000, // Increased timeout for content validation check: async () => this.checkConfigFiles() }); } // Icon Preloading Check async checkIconPreloading() { // console.log('🔄 Starting icon preloading check...'); // Load data directly since window.apps isn't populated yet let appsData = []; let categoriesData = []; try { // console.log('🌐 Fetching data from /data/apps/generated/apps.json and /data/apps/apps-categories.json'); const [appsResponse, categoriesResponse] = await Promise.all([ fetch('/data/apps/generated/apps.json'), fetch('/data/apps/apps-categories.json') ]); // console.log('📊 Apps response status:', appsResponse.status, appsResponse.statusText); // console.log('📊 Categories response status:', categoriesResponse.status, categoriesResponse.statusText); if (appsResponse.ok) { const appsText = await appsResponse.text(); // console.log('📄 Apps response text length:', appsText.length); const appsParsed = JSON.parse(appsText); appsData = appsParsed.apps || []; // console.log('✅ Apps data loaded successfully'); } else { console.warn('⚠️ Apps response not ok:', appsResponse.status); } if (categoriesResponse.ok) { const categoriesText = await categoriesResponse.text(); // console.log('📄 Categories response text length:', categoriesText.length); const categoriesParsed = JSON.parse(categoriesText); categoriesData = categoriesParsed.categories || []; // console.log('✅ Categories data loaded successfully'); } else { console.warn('⚠️ Categories response not ok:', categoriesResponse.status); } } catch (error) { console.warn('⚠️ Failed to load data for icon preloading:', error); } // console.log('📊 Apps data length:', appsData?.length || 0); // console.log('📊 Categories data length:', categoriesData?.length || 0); // Collect all icon URLs to preload const iconUrls = new Set(); // Add app icons if (appsData && Array.isArray(appsData)) { appsData.forEach(app => { if (app.icon) { iconUrls.add(app.icon.startsWith('/') ? app.icon : '/' + app.icon); } else { const cleanAppName = app.command?.split(' ').pop() || app.name; iconUrls.add(`/icons/apps/${cleanAppName}.svg`); } iconUrls.add('/icons/apps/default.svg'); }); } // Add category icons if (categoriesData && Array.isArray(categoriesData)) { categoriesData.forEach(category => { if (category.icon) { iconUrls.add(category.icon.startsWith('/') ? category.icon : '/' + category.icon); } }); } // Add 'all' and 'installed' category icons iconUrls.add('/icons/categories/all.svg'); iconUrls.add('/icons/categories/installed.svg'); // console.log(`📦 Found ${iconUrls.size} unique icons to preload`); // Preload all icons using Image objects const preloadPromises = Array.from(iconUrls).map(iconUrl => { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(); img.onerror = () => resolve(); // Resolve even on error to not block img.src = iconUrl; }); }); await Promise.all(preloadPromises); // console.log(`✅ Preloaded ${iconUrls.size} app center icons`); // Pre-render app center in background if (window.appsManager && typeof window.appsManager.showAppsList === 'function') { // console.log('🔄 Pre-rendering app center content...'); window.appsManager.showAppsList('all'); await new Promise(resolve => setTimeout(resolve, 300)); // Wait for rendering // console.log('✅ App center pre-rendered'); } return true; } // Add event callbacks on(event, callback) { if (this.callbacks[event]) { this.callbacks[event].push(callback); } } // Trigger event callbacks trigger(event, data) { if (this.callbacks[event]) { this.callbacks[event].forEach(callback => callback(data)); } } // Calculate total checks weight calculateTotalWeight() { let total = 0; for (const [systemId, system] of this.systems) { for (const checkId of system.checks) { const check = this.checks.get(checkId); if (check) { total += check.weight; } } } return total; } // Initialize all components in dependency order async initializeComponents() { // console.log('🔧 BREAKPOINT: initializeComponents() called!'); // console.log('🔧 Initializing components...'); // Get systems in priority order const sortedSystems = Array.from(this.systems.entries()) .sort(([,a], [,b]) => a.priority - b.priority); for (const [systemId, system] of sortedSystems) { // console.log(`🔄 Processing system: ${systemId}`); // Check dependencies first const dependenciesMet = system.dependencies.every(dep => this.systems.get(dep)?.initialized === true ); if (!dependenciesMet) { // console.log(`⏳ Skipping ${system.name} - dependencies not met:`, system.dependencies); continue; } // Find components for this system const systemComponents = Array.from(this.components.entries()) .filter(([, component]) => component.system === systemId); // console.log(`📦 Found ${systemComponents.length} components for ${systemId}:`, systemComponents.map(([id]) => id)); // Initialize components for this system for (const [componentId, component] of systemComponents) { try { // console.log(`🔧 Initializing ${componentId}...`); // Load component script(s) if specified if (component.script) { // console.log(`📦 Loading script for ${componentId}: ${component.script}`); await this.loadScript(component.script); } else if (component.scripts) { // console.log(`📦 Loading scripts for ${componentId}:`, component.scripts); await this.loadScripts(component.scripts); } // Check component dependencies const componentDepsMet = component.dependencies.every(dep => this.systems.get(dep)?.initialized === true ); if (!componentDepsMet) { // console.log(`⏳ Skipping ${componentId} - component dependencies not met:`, component.dependencies); continue; } // Initialize component const result = component.initializer(); // Store result if available if (result && component.global) { window[component.global] = result; // console.log(`✅ ${componentId} stored as global: ${component.global}`); } // console.log(`✅ ${componentId} initialized successfully`); } catch (error) { console.error(`❌ Failed to initialize ${componentId}:`, error); this.errors.push({ system: system.name, component: componentId, error: error.message }); } } // Mark system as initialized system.initialized = true; // console.log(`✅ ${system.name} system initialized`); } // console.log('🎉 Component initialization complete'); } // Load a script dynamically async loadScript(src) { return new Promise((resolve, reject) => { // Check if script is already loaded if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } // Run all health checks async runHealthChecks() { this.totalChecks = this.calculateTotalWeight(); this.completedChecks = 0; this.errors = []; const results = new Map(); // Process systems in priority order const sortedSystems = Array.from(this.systems.entries()) .sort(([,a], [,b]) => a.priority - b.priority); for (const [systemId, system] of sortedSystems) { // Check dependencies first const dependenciesMet = system.dependencies.every(dep => results.get(dep)?.status === 'passed' ); // console.log(`🔍 Processing system: ${system.name} (priority: ${system.priority}, critical: ${system.critical})`); // console.log(`🔍 Dependencies met: ${dependenciesMet}`); if (!dependenciesMet) { results.set(systemId, { status: 'skipped', reason: 'Dependencies not met', checks: [] }); continue; } // Run system checks const systemResults = []; // If system has no checks, it automatically passes if (system.checks.length === 0) { // console.log(`✅ System "${system.name}" (${systemId}) has no checks - automatically passing`); // Trigger status update for systems with no checks this.trigger('onSystemCheck', { systemId, systemName: system.name, checkId: 'auto-pass', checkName: 'No checks required', status: 'passed' }); } else { // Run checks for systems that have them for (const checkId of system.checks) { const check = this.checks.get(checkId); if (!check) continue; this.trigger('onSystemCheck', { systemId, systemName: system.name, checkId, checkName: check.name, status: 'checking' }); // console.log(`🔍 System "${system.name}" (${systemId}) is running check "${check.name}" (${checkId})`); try { const result = await this.runSingleCheck(checkId, check); systemResults.push(result); if (result.status === 'failed') { // console.log(`❌ Check failed: ${check.name} for system ${system.name}`); // console.log(`🔍 System critical: ${system.critical}`); this.errors.push({ system: system.name, check: check.name, error: result.error }); // If this is a critical system, stop all further processing if (system.critical) { console.error(`🚨 CRITICAL SYSTEM FAILURE: ${system.name} - stopping all further checks`); // Mark ALL remaining systems as skipped due to critical failure const currentIndex = sortedSystems.findIndex(([id]) => id === systemId); for (let i = currentIndex + 1; i < sortedSystems.length; i++) { const [remainingSystemId, remainingSystem] = sortedSystems[i]; results.set(remainingSystemId, { status: 'skipped', reason: `Critical system ${system.name} failed`, checks: [] }); } // Return immediately with critical failure const criticalResult = { results: Object.fromEntries(results), errors: this.errors, success: false, criticalFailure: true }; this.trigger('onComplete', criticalResult); return criticalResult; } } } catch (error) { // console.log(`❌ Exception in check: ${check.name} for system ${system.name}:`, error.message); // console.log(`🔍 System critical: ${system.critical}`); systemResults.push({ id: checkId, name: check.name, status: 'failed', error: error.message }); this.errors.push({ system: system.name, check: check.name, error: error.message }); // If this is a critical system, stop all further processing if (system.critical) { console.error(`🚨 CRITICAL SYSTEM FAILURE: ${system.name} - stopping all further checks`); // Mark ALL remaining systems as skipped due to critical failure const currentIndex = sortedSystems.findIndex(([id]) => id === systemId); for (let i = currentIndex + 1; i < sortedSystems.length; i++) { const [remainingSystemId, remainingSystem] = sortedSystems[i]; results.set(remainingSystemId, { status: 'skipped', reason: `Critical system ${system.name} failed`, checks: [] }); } // Return immediately with critical failure const criticalResult = { results: Object.fromEntries(results), errors: this.errors, success: false, criticalFailure: true }; this.trigger('onComplete', criticalResult); return criticalResult; } } } } const systemPassed = systemResults.every(r => r.status === 'passed'); results.set(systemId, { status: systemPassed ? 'passed' : 'failed', checks: systemResults }); } this.trigger('onComplete', { results: Object.fromEntries(results), errors: this.errors, success: this.errors.length === 0 }); return { results: Object.fromEntries(results), errors: this.errors, success: this.errors.length === 0 }; } // Run a single health check async runSingleCheck(checkId, check) { const startTime = Date.now(); try { // console.log(`🔍 Running health check: ${check.name} (${checkId})`); const result = await Promise.race([ Promise.resolve(check.check()), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), check.timeout) ) ]); const duration = Date.now() - startTime; const passed = Boolean(result); // console.log(`✅ Health check ${check.name}: ${passed ? 'PASSED' : 'FAILED'} (${duration}ms)`); this.completedChecks += check.weight; this.progress = Math.min((this.completedChecks / this.totalChecks) * 100, 100); this.trigger('onProgress', { checkId, checkName: check.name, status: passed ? 'passed' : 'failed', progress: this.progress, duration }); return { id: checkId, name: check.name, status: passed ? 'passed' : 'failed', duration, error: passed ? null : 'Check failed' }; } catch (error) { const duration = Date.now() - startTime; console.error(`❌ Health check ${check.name} FAILED:`, error); this.completedChecks += check.weight; this.progress = Math.min((this.completedChecks / this.totalChecks) * 100, 100); this.trigger('onProgress', { checkId, checkName: check.name, status: 'failed', progress: this.progress, duration, error: error.message }); return { id: checkId, name: check.name, status: 'failed', duration, error: error.message }; } } // Check if this is first-time setup async checkFirstTimeSetup() { try { // Check for essential config files const essentialFiles = [ '/data/apps/generated/apps.json', '/data/apps/apps-categories.json' ]; for (const file of essentialFiles) { const response = await fetch(file, { method: 'HEAD' }); if (!response.ok) { this.isFirstTime = true; return true; } } // Check localStorage for setup flag const setupComplete = localStorage.getItem('libreportal_setup_complete'); if (!setupComplete) { this.isFirstTime = true; return true; } this.isFirstTime = false; return false; } catch { this.isFirstTime = true; return true; } } // Check required config files async checkConfigFiles() { // console.log('🔍 Config Files Check: Starting validation'); // Critical files that MUST exist and not be empty const criticalFiles = [ '/data/tasks/current.json', '/data/tasks/queue.json', '/data/system/system_info.json', '/data/system/memory_usage.json', '/data/system/disk_usage.json', '/data/system/system_info.json', '/data/apps/generated/apps.json', '/data/apps/apps-categories.json', '/data/apps/apps-field-mappings.json', '/data/apps/apps-config-categories.json', '/data/config/generated/configs.json' ]; // console.log('🔍 Critical files to check:', criticalFiles); // Optional files (can be missing but should exist if possible) const optionalFiles = [ ]; const allFiles = [...criticalFiles, ...optionalFiles]; const results = { available: [], missing: [], empty: [], total: allFiles.length, criticalMissing: [], criticalEmpty: [] }; for (const file of allFiles) { try { // console.log(`🔍 Checking file: ${file}`); const response = await fetch(file, { method: 'HEAD' }); // console.log(`📊 Response status: ${response.status} ${response.statusText}`); // console.log(`📊 Content-Length: ${response.headers.get('content-length')}`); // console.log(`📊 Response ok: ${response.ok}`); if (response.ok) { // Get content length to check if file is empty const contentLength = response.headers.get('content-length'); const isEmpty = contentLength === '0' || contentLength === null; // console.log(`🔍 File ${file} - contentLength: ${contentLength}, isEmpty: ${isEmpty}`); // For critical files, also check actual content if (criticalFiles.includes(file) && !isEmpty) { try { const getResponse = await fetch(file); const content = await getResponse.text(); // console.log(`📄 Content preview for ${file}:`, content.substring(0, 100) + '...'); // Check if content starts with '{' or '[' (basic JSON file check) const startsWithBrace = content.trim().startsWith('{'); const startsWithBracket = content.trim().startsWith('['); const isValidJSONStart = startsWithBrace || startsWithBracket; // console.log(`🔍 JSON structure check for ${file}: starts with '{' or '['? ${isValidJSONStart} (content: ${content.trim().substring(0, 10)}...)`); if (!isValidJSONStart) { // console.log(`❌ Critical file does not start with '{' or '[' (not valid JSON): ${file}`); results.criticalEmpty.push(file); results.empty.push(file); } // Check if content is actual JSON data or fallback/error content else if (content.includes('404 Not Found') || content.includes('404') && content.includes('Not Found') || (content.includes('Error') && (content.includes('') || content.includes('status'))) || content.includes('')) { // console.log(`❌ Critical file contains error/fallback content: ${file}`); results.criticalEmpty.push(file); results.empty.push(file); } // Validate JSON syntax else { try { JSON.parse(content); // console.log(`✅ JSON syntax valid for: ${file}`); results.available.push(file); // console.log(`✅ File is available and valid: ${file}`); } catch (jsonError) { // console.log(`❌ Critical file has invalid JSON syntax: ${file} - ${jsonError.message}`); results.criticalEmpty.push(file); results.empty.push(file); } } } catch (contentError) { // console.log(`❌ Failed to read content for ${file}:`, contentError.message); results.criticalEmpty.push(file); results.empty.push(file); } } else if (isEmpty) { results.empty.push(file); if (criticalFiles.includes(file)) { results.criticalEmpty.push(file); // console.log(`❌ Critical file is EMPTY: ${file}`); } } else { results.available.push(file); // console.log(`✅ File is available: ${file}`); } } else { results.missing.push(file); if (criticalFiles.includes(file)) { results.criticalMissing.push(file); // console.log(`❌ Critical file is MISSING: ${file}`); } // console.log(`❌ File not accessible: ${file} - ${response.status} ${response.statusText}`); } } catch (error) { // console.log(`❌ Exception checking file ${file}:`, error.message); results.missing.push(file); if (criticalFiles.includes(file)) { results.criticalMissing.push(file); // console.log(`❌ Critical file exception: ${file}`); } } } // console.log('Config Files Results:', { //available: results.available.length, //missing: results.missing.length, //empty: results.empty.length, //criticalMissing: results.criticalMissing.length, //criticalEmpty: results.criticalEmpty.length, //missingFiles: results.missing, //emptyFiles: results.empty //}); // Fail if ANY critical files are missing or empty if (results.criticalMissing.length > 0 || results.criticalEmpty.length > 0) { const errorMessages = []; if (results.criticalMissing.length > 0) { errorMessages.push(`Missing critical files: ${results.criticalMissing.join(', ')}`); } if (results.criticalEmpty.length > 0) { errorMessages.push(`Empty critical files: ${results.criticalEmpty.join(', ')}`); } const errorMessage = `Critical configuration files are missing or empty. Please run: libreportal webui generate all \n\nDetails: ${errorMessages.join('\n')}`; console.error('🚨 Config Files Check FAILED - throwing error:', errorMessage); throw new Error(errorMessage); } // console.log('✅ Config Files Check PASSED'); return results; } // Check for update lock file to prevent concurrent updates async checkUpdateLock() { // console.log('🔍 Update Lock Check: Checking for update lock file'); try { const response = await fetch('/data/updater-lock', { method: 'HEAD' }); const lockExists = response.ok; // console.log(`🔒 Update lock file exists: ${lockExists}`); if (lockExists) { return { status: 'locked', message: 'Update in progress - please wait for current update to complete' }; } else { return { status: 'unlocked', message: 'No update in progress' }; } } catch (error) { console.error('❌ Error checking update lock:', error); return { status: 'error', message: `Error checking update lock: ${error.message}` }; } } // Check for pause lock file to prevent operations during pause async checkPauseLock() { // console.log('🔍 Pause Lock Check: Checking for pause lock file'); try { const response = await fetch('/data/pause.lock', { method: 'HEAD' }); const lockExists = response.ok; // console.log(`🔒 Pause lock file exists: ${lockExists}`); if (lockExists) { return { status: 'locked', message: 'System is paused - operations are temporarily disabled' }; } else { return { status: 'unlocked', message: 'System is operational' }; } } catch (error) { console.error('❌ Error checking pause lock:', error); return { status: 'error', message: `Error checking pause lock: ${error.message}` }; } } // Get system summary getSystemSummary() { return { totalSystems: this.systems.size, totalChecks: this.checks.size, totalWeight: this.calculateTotalWeight(), systems: Array.from(this.systems.entries()).map(([id, system]) => ({ id, name: system.name, description: system.description, priority: system.priority, dependencies: system.dependencies, checks: system.checks, initialized: system.initialized || false })), components: Array.from(this.components.entries()).map(([id, component]) => ({ id, system: component.system, global: component.global, dependencies: component.dependencies, script: component.script || null })) }; } // Load a script dynamically async loadScript(src) { return new Promise((resolve, reject) => { // Check if script is already loaded if (document.querySelector(`script[src="${src}"]`)) { // console.log(`📦 Script already loaded: ${src}`); resolve(); return; } // console.log(`📦 Loading script: ${src}`); const script = document.createElement('script'); script.src = src; script.onload = () => { // console.log(`✅ Script loaded: ${src}`); resolve(); }; script.onerror = () => { console.error(`❌ Failed to load script: ${src}`); reject(new Error(`Failed to load script: ${src}`)); }; document.head.appendChild(script); }); } // Load multiple scripts for a component async loadScripts(scripts) { if (!scripts || scripts.length === 0) return; // console.log(`📦 Loading ${scripts.length} scripts for component...`); await Promise.all(scripts.map(script => this.loadScript(script))); // console.log(`✅ All scripts loaded for component`); } } // Global instance window.SystemLoader = SystemLoader;