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>
125 lines
3.7 KiB
JavaScript
Executable File
125 lines
3.7 KiB
JavaScript
Executable File
class ConfigForm {
|
|
constructor() {
|
|
this.form = null;
|
|
this._submitHandlerAttached = false;
|
|
}
|
|
|
|
resetForm() {
|
|
this.form = document.getElementById('config-form');
|
|
if (this.form) {
|
|
this.form.reset();
|
|
}
|
|
}
|
|
|
|
snapshotOriginal() {
|
|
const original = {};
|
|
const data = window.configData && window.configData.config;
|
|
if (!data) return original;
|
|
Object.entries(data).forEach(([key, entry]) => {
|
|
original[key] = entry && entry.value !== undefined ? String(entry.value) : '';
|
|
});
|
|
return original;
|
|
}
|
|
|
|
collectChanges(original) {
|
|
const changes = [];
|
|
if (!this.form) return changes;
|
|
|
|
const current = {};
|
|
const inputs = this.form.querySelectorAll('input, select, textarea');
|
|
inputs.forEach((input) => {
|
|
const name = input.name;
|
|
if (!name || !name.startsWith('CFG_')) return;
|
|
if (name.endsWith('_PORT_MANAGER')) return; // UI-only aggregate
|
|
let value;
|
|
if (input.type === 'checkbox') {
|
|
value = input.checked ? 'true' : 'false';
|
|
} else {
|
|
value = (input.value || '').trim();
|
|
}
|
|
current[name] = value;
|
|
});
|
|
|
|
Object.keys(current).forEach((name) => {
|
|
const oldValue = original[name] !== undefined ? original[name] : '';
|
|
const newValue = current[name];
|
|
if (oldValue === newValue) return;
|
|
const encoded = newValue.replace(/\|/g, '%7C');
|
|
changes.push(`${name}=${encoded}`);
|
|
});
|
|
|
|
return changes;
|
|
}
|
|
|
|
async saveConfig() {
|
|
this.form = document.getElementById('config-form');
|
|
if (!this.form) {
|
|
console.error('ConfigForm: Form not found');
|
|
return;
|
|
}
|
|
|
|
const original = this.snapshotOriginal();
|
|
const changes = this.collectChanges(original);
|
|
|
|
if (changes.length === 0) {
|
|
this.showNotification('No configuration changes to save.', 'info');
|
|
return;
|
|
}
|
|
|
|
const encoded = changes.join('|');
|
|
|
|
try {
|
|
if (!window.tasksManager || !window.tasksManager.router) {
|
|
throw new Error('Task system not available');
|
|
}
|
|
const task = await window.tasksManager.router.routeAction('config_update', {
|
|
changes: `'${encoded.replace(/'/g, "'\\''")}'`
|
|
});
|
|
|
|
this.showNotification(
|
|
`Saving ${changes.length} configuration change${changes.length === 1 ? '' : 's'}...`,
|
|
'success'
|
|
);
|
|
|
|
if (task && window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') {
|
|
setTimeout(() => window.librePortalSPA.navigate(`/tasks/all?task=${task.id}`), 400);
|
|
} else if (task && window.navigateToRoute) {
|
|
setTimeout(() => window.navigateToRoute(`tasks/all?task=${task.id}`), 400);
|
|
}
|
|
} catch (error) {
|
|
console.error('ConfigForm: Error saving configuration:', error);
|
|
this.showNotification('Failed to save configuration: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// preventDefault stops the form from falling back to GET (which dumps every
|
|
// CFG into the URL).
|
|
attachSubmitHandler() {
|
|
const form = document.getElementById('config-form');
|
|
if (!form) return;
|
|
if (form.dataset.submitWired === '1') return;
|
|
form.dataset.submitWired = '1';
|
|
form.addEventListener('submit', (event) => {
|
|
event.preventDefault();
|
|
this.saveConfig();
|
|
});
|
|
}
|
|
|
|
showNotification(message, type) {
|
|
type = type || 'info';
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.show(message, type);
|
|
return;
|
|
}
|
|
const notification = document.createElement('div');
|
|
notification.className = 'notification notification-' + type;
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
setTimeout(() => {
|
|
if (notification.parentNode) notification.parentNode.removeChild(notification);
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
window.ConfigForm = ConfigForm;
|