librelad d39852aa3d refactor(webui): reorganize into components/ + core/ taxonomy
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>
2026-05-30 07:13:52 +01:00

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;