librelad 16571134b5 refactor(paths): scrub residual /docker references in display text + comments
Audit follow-up — after a full-repo sweep, the only remaining functional /docker
refs are intentional (the legacy compat shim + the env-overridden legacy-safe
backend default). Fix the last user-visible/stale ones:
- config-options.js: backup PATH_MODE 'auto' label no longer hardcodes
  /docker/backups (the path is relocatable) — describes the behaviour instead.
- config.js / setup-detector.js / webui_install_image.sh: refresh comments that
  named /docker to the relocatable system/containers roots.

No behaviour change. Active container app scripts already use $containers_dir;
the remaining /docker hits across the tree are docker-compose.yml filenames,
/var/lib/docker, the docker binary, relative array paths, docs/site, and the
unused/ graveyard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 17:18:46 +01:00

441 lines
11 KiB
JavaScript
Executable File

// 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 <containers-root>/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;