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>
441 lines
11 KiB
JavaScript
Executable File
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;
|