Surface when LibrePortal is behind upstream and let users update from the WebUI, reusing the proven git-update path instead of reinventing it. Detection (host): webuiSystemUpdateCheck writes frontend/data/system/update_status.json from a throttled git fetch + behind-count + VERSION compare, off the existing per-minute `webui generate system` cron. A new /VERSION file is the canonical version. Display (frontend): update-notifier.js/.css render a global topbar badge (every page) and a dashboard banner (prominent when behind, subtle "up to date" with a manual check otherwise), plus a details panel. Actions go through the task pipeline: - `libreportal update apply` -> webuiRunUpdate (non-interactive: guards, forced check, gitPerformUpdate, then dockerInstallApp libreportal) - `libreportal update check` -> forced recheck gitFolderResetAndBackup's body is extracted into gitPerformUpdate (no exit) so the WebUI path can reuse it; the interactive CLI flow is unchanged. Detection JSON verified against the repo (up-to-date and behind cases). webuiRunUpdate's re-clone + redeploy still needs validation on a live host. The latest-version source is git for now and is the single swap point for get.libreportal.org later — the JSON contract and frontend stay unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
1344 lines
46 KiB
JavaScript
Executable File
1344 lines
46 KiB
JavaScript
Executable File
// 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/update-notifier.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('<!DOCTYPE') || content.includes('<html>') || content.includes('status'))) ||
|
|
content.includes('<!DOCTYPE') || content.includes('<html>')) {
|
|
// 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;
|