Final modularization layout (user-chosen): every page is a self-contained folder under components/<id>/ (controllers + CSS + its html fragment), and all shared/framework code folds into core/: core/kernel (feature-registry, lifecycle, services, spa) core/boot (auth, system-loader/orchestrator, setup, loaders) core/lib (data-loader, router, helpers, the task kernel, shared modules) core/ui (topbar, modal, notifications, … + topbar.html) core/css (all shared stylesheets) core/icons Top level is now just: components/, core/, themes/, index.html (+ runtime data/). Every path reference rewritten (index.html, scripts arrays, fetch()/ loadFragment()/loadScript() literals, system-loader + config-manager controller paths, kernel manifest URL, feature.json, backend FEATURES_DIR). The /api/features/list endpoint NAME is unchanged (it now scans components/). Deleted 3 dead files (app-content.html, apps-content.html, html-cache.js). Verified: 0 stale prefixes, 0 double-rewrites, all JS/JSON valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
249 lines
8.5 KiB
JavaScript
Executable File
249 lines
8.5 KiB
JavaScript
Executable File
// Config Validation System for LibrePortal Web UI
|
||
// Validates JSON config files before loading the main interface
|
||
|
||
// Global validator instance
|
||
window.ConfigValidator = function() {
|
||
let validationResults = null;
|
||
let hasValidated = false;
|
||
|
||
// Validate all config files
|
||
this.validateAllConfigs = async function() {
|
||
// Use client-side validation directly
|
||
return this.fallbackValidation();
|
||
};
|
||
|
||
// Fallback client-side validation
|
||
this.fallbackValidation = async function() {
|
||
const results = {
|
||
valid: true,
|
||
errors: [],
|
||
warnings: [],
|
||
suggestions: []
|
||
};
|
||
|
||
// Check if unified config file exists (file existence check only)
|
||
const configFiles = [
|
||
{ name: 'Unified System Config', path: '/data/config/generated/configs.json' }
|
||
];
|
||
|
||
for (const config of configFiles) {
|
||
try {
|
||
const response = await fetch(config.path, { method: 'HEAD' });
|
||
if (!response.ok) {
|
||
results.valid = false;
|
||
results.errors.push(`Config file '${config.name}' not found: ${config.path}`);
|
||
continue;
|
||
}
|
||
|
||
//console.log(`Config file exists: ${config.name}`);
|
||
|
||
} catch (error) {
|
||
results.valid = false;
|
||
results.errors.push(`Failed to check ${config.name}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Add suggestions if there are issues
|
||
if (!results.valid) {
|
||
results.suggestions = [
|
||
"Run 'libreportal run' to fix configuration issues",
|
||
"Check config file permissions and ownership",
|
||
"Ensure Docker is running and accessible"
|
||
];
|
||
}
|
||
|
||
validationResults = results;
|
||
hasValidated = true;
|
||
return results;
|
||
};
|
||
|
||
// Show validation error message
|
||
this.showValidationError = function() {
|
||
if (!hasValidated) {
|
||
return;
|
||
}
|
||
|
||
if (validationResults.valid) {
|
||
return; // No error to show
|
||
}
|
||
|
||
// Create error overlay
|
||
const errorOverlay = document.createElement('div');
|
||
errorOverlay.className = 'config-validation-overlay';
|
||
errorOverlay.innerHTML = `
|
||
<div class="config-validation-dialog">
|
||
<div class="validation-header">
|
||
<h3>⚠️ Configuration Issues Detected</h3>
|
||
<button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button>
|
||
</div>
|
||
<div class="validation-content">
|
||
<div class="validation-errors">
|
||
<h4>Errors:</h4>
|
||
<ul>
|
||
${validationResults.errors.map(error => `<li>${this.escapeHtml(error)}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
${validationResults.warnings.length > 0 ? `
|
||
<div class="validation-warnings">
|
||
<h4>Warnings:</h4>
|
||
<ul>
|
||
${validationResults.warnings.map(warning => `<li>${this.escapeHtml(warning)}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
<div class="validation-suggestions">
|
||
<h4>Suggestions:</h4>
|
||
<ul>
|
||
${validationResults.suggestions.map(suggestion => `<li>${this.escapeHtml(suggestion)}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
<div class="validation-actions">
|
||
<button class="primary-btn" onclick="window.location.reload()">Reload</button>
|
||
<button class="secondary-btn" onclick="window.open('https://github.com/your-repo/wiki/troubleshooting', '_blank')">Help</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Add styles
|
||
errorOverlay.innerHTML += `
|
||
<style>
|
||
.config-validation-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.config-validation-dialog {
|
||
background: var(--surface-color);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.validation-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.validation-header h3 {
|
||
margin: 0;
|
||
color: var(--error-color);
|
||
font-size: 18px;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: var(--text-color);
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.validation-errors h4 {
|
||
color: var(--error-color);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.validation-warnings h4 {
|
||
color: var(--warning-color);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.validation-content ul {
|
||
margin: 0 0 16px 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.validation-content li {
|
||
margin-bottom: 8px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.validation-suggestions {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--border-color);
|
||
}
|
||
|
||
.validation-suggestions h4 {
|
||
color: var(--success-color);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.validation-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 24px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.primary-btn, .secondary-btn {
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.secondary-btn {
|
||
background: var(--surface-color);
|
||
color: var(--text-color);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
</style>
|
||
`;
|
||
|
||
document.body.appendChild(errorOverlay);
|
||
};
|
||
|
||
// Escape HTML to prevent XSS
|
||
this.escapeHtml = function(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
};
|
||
|
||
// Get validation status
|
||
this.isValid = function() {
|
||
return hasValidated && validationResults.valid;
|
||
};
|
||
|
||
// Get validation errors
|
||
this.getErrors = function() {
|
||
return hasValidated ? validationResults.errors : [];
|
||
};
|
||
|
||
// Get validation warnings
|
||
this.getWarnings = function() {
|
||
return hasValidated ? validationResults.warnings : [];
|
||
};
|
||
};
|