// Setup Detection - First-time user setup and installer detection class SetupDetector { constructor() { this.setupSteps = new Map(); this.currentStep = 0; this.isFirstTime = false; this.setupComplete = false; } // Initialize setup detection async initialize() { this.setupSteps = this.defineSetupSteps(); this.isFirstTime = await this.detectFirstTime(); this.setupComplete = !this.isFirstTime; return { isFirstTime: this.isFirstTime, setupComplete: this.setupComplete }; } // Define setup steps defineSetupSteps() { return new Map([ ['welcome', { title: 'Welcome to LibrePortal', description: 'Let\'s get your Docker management system set up', component: 'welcome-step' }], ['requirements', { title: 'System Requirements', description: 'Checking your system compatibility', component: 'requirements-step', check: () => this.checkSystemRequirements() }], ['directories', { title: 'Directory Setup', description: 'Creating necessary directories', component: 'directories-step', check: () => this.checkDirectories() }], ['permissions', { title: 'Permissions Check', description: 'Verifying file permissions', component: 'permissions-step', check: () => this.checkPermissions() }], ['configuration', { title: 'Basic Configuration', description: 'Setting up initial configuration', component: 'config-step', check: () => this.checkBasicConfig() }], ['complete', { title: 'Setup Complete', description: 'Your LibrePortal system is ready', component: 'complete-step' }] ]); } // Detect if this is first-time setup. Source of truth is the server-side // lock file at /libreportal/frontend/data/.setup_complete // — that's what the bash setupApply function creates after a successful // wizard run. localStorage is no longer used because per-browser state // gives wrong answers (different browsers would each see "first install"). async detectFirstTime() { try { const res = await fetch('/api/setup/status'); if (!res.ok) return true; const data = await res.json(); return !data.complete; } catch (error) { console.error('Error detecting first-time setup:', error); return true; // Assume first time on error } } // Check system requirements async checkSystemRequirements() { const checks = []; // Check browser compatibility const browserCheck = this.checkBrowserCompatibility(); checks.push({ name: 'Browser Compatibility', status: browserCheck.passed, details: browserCheck.details }); // Check JavaScript features const jsCheck = this.checkJavaScriptFeatures(); checks.push({ name: 'JavaScript Features', status: jsCheck.passed, details: jsCheck.details }); // Check storage availability const storageCheck = this.checkStorageAvailability(); checks.push({ name: 'Local Storage', status: storageCheck.passed, details: storageCheck.details }); // Check network connectivity const networkCheck = await this.checkNetworkConnectivity(); checks.push({ name: 'Network Connectivity', status: networkCheck.passed, details: networkCheck.details }); return { passed: checks.every(check => check.status), checks }; } // Check browser compatibility checkBrowserCompatibility() { const userAgent = navigator.userAgent; const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor); const isFirefox = /Firefox/.test(userAgent); const isSafari = /Safari/.test(userAgent) && /Apple Computer/.test(navigator.vendor); const isEdge = /Edg/.test(userAgent); const supported = isChrome || isFirefox || isSafari || isEdge; return { passed: supported, details: { browser: this.getBrowserName(), supported, version: navigator.userAgent.match(/(?:Chrome|Firefox|Safari|Edge)\/(\d+)/)?.[1] || 'Unknown' } }; } // Get browser name getBrowserName() { const userAgent = navigator.userAgent; if (/Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor)) return 'Chrome'; if (/Firefox/.test(userAgent)) return 'Firefox'; if (/Safari/.test(userAgent) && /Apple Computer/.test(navigator.vendor)) return 'Safari'; if (/Edg/.test(userAgent)) return 'Edge'; return 'Unknown'; } // Check JavaScript features checkJavaScriptFeatures() { const features = [ { name: 'Fetch API', check: () => typeof fetch !== 'undefined' }, { name: 'Promises', check: () => typeof Promise !== 'undefined' }, { name: 'Arrow Functions', check: () => { try { eval('() => {}'); return true; } catch { return false; } } }, { name: 'Async/Await', check: () => { try { eval('async () => {}'); return true; } catch { return false; } } }, { name: 'Map/Set', check: () => typeof Map !== 'undefined' && typeof Set !== 'undefined' }, { name: 'LocalStorage', check: () => typeof Storage !== 'undefined' } ]; const results = features.map(feature => ({ name: feature.name, supported: feature.check() })); const allSupported = results.every(result => result.supported); return { passed: allSupported, details: { features: results, totalSupported: results.filter(r => r.supported).length, totalFeatures: results.length } }; } // Check storage availability checkStorageAvailability() { try { const testKey = 'libreportal_test'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return { passed: true, details: { available: true, quota: 'Available' } }; } catch (error) { return { passed: false, details: { available: false, error: error.message } }; } } // Check network connectivity async checkNetworkConnectivity() { try { const response = await fetch('/', { method: 'HEAD', cache: 'no-cache' }); return { passed: response.ok, details: { status: response.status, statusText: response.statusText, online: navigator.onLine } }; } catch (error) { return { passed: false, details: { error: error.message, online: navigator.onLine } }; } } // Check required directories async checkDirectories() { const requiredPaths = [ '/data/apps/', '/data/config/', '/data/backup/', '/containers/libreportal/' ]; const checks = []; for (const path of requiredPaths) { try { // Try to access a file in the directory to check if it exists const testFile = path.endsWith('/') ? path + '.gitkeep' : path; const response = await fetch(testFile, { method: 'HEAD' }); checks.push({ path, exists: response.ok || response.status === 404 // 404 means directory exists but file doesn't }); } catch (error) { checks.push({ path, exists: false, error: error.message }); } } const allExist = checks.every(check => check.exists); return { passed: allExist, details: { directories: checks, totalExists: checks.filter(c => c.exists).length, totalDirectories: checks.length } }; } // Check file permissions async checkPermissions() { const testFiles = [ '/data/apps/generated/apps.json', '/data/apps/apps-categories.json', '/data/config/generated/configs.json' ]; const checks = []; for (const file of testFiles) { try { const response = await fetch(file, { method: 'HEAD' }); checks.push({ file, readable: response.ok, status: response.status }); } catch (error) { checks.push({ file, readable: false, error: error.message }); } } const allReadable = checks.every(check => check.readable); return { passed: allReadable, details: { files: checks, totalReadable: checks.filter(c => c.readable).length, totalFiles: checks.length } }; } // Check basic configuration async checkBasicConfig() { try { // Check for basic config files const configFiles = [ '/data/config/generated/configs.json' ]; for (const file of configFiles) { const response = await fetch(file, { method: 'HEAD' }); if (!response.ok) { return { passed: false, details: { missing: file, error: 'Configuration file not found' } }; } } // Try to load and validate config structure const configResponse = await fetch('/data/config/generated/configs.json'); if (configResponse.ok) { const config = await configResponse.json(); return { passed: true, details: { configLoaded: true, configType: typeof config, hasData: Object.keys(config).length > 0 } }; } return { passed: false, details: { error: 'Failed to load configuration' } }; } catch (error) { return { passed: false, details: { error: error.message } }; } } // Get setup step by ID getStep(stepId) { return this.setupSteps.get(stepId); } // Get all setup steps getAllSteps() { return Array.from(this.setupSteps.entries()).map(([id, step]) => ({ id, ...step })); } // Get next step getNextStep() { const steps = Array.from(this.setupSteps.keys()); const nextIndex = this.currentStep + 1; if (nextIndex < steps.length) { return steps[nextIndex]; } return null; } // Get previous step getPreviousStep() { const steps = Array.from(this.setupSteps.keys()); const prevIndex = this.currentStep - 1; if (prevIndex >= 0) { return steps[prevIndex]; } return null; } // Move to specific step goToStep(stepId) { if (this.setupSteps.has(stepId)) { this.currentStep = Array.from(this.setupSteps.keys()).indexOf(stepId); return true; } return false; } // Mark setup as complete markSetupComplete() { this.setupComplete = true; this.isFirstTime = false; localStorage.setItem('libreportal_setup_complete', 'true'); localStorage.setItem('libreportal_setup_date', new Date().toISOString()); } // Reset setup (for testing/re-setup) resetSetup() { this.setupComplete = false; this.isFirstTime = true; this.currentStep = 0; localStorage.removeItem('libreportal_setup_complete'); localStorage.removeItem('libreportal_setup_date'); } // Get setup status getSetupStatus() { return { isFirstTime: this.isFirstTime, setupComplete: this.setupComplete, currentStep: this.currentStep, totalSteps: this.setupSteps.size, setupDate: localStorage.getItem('libreportal_setup_date') }; } } // Global instance window.SetupDetector = SetupDetector;