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

228 lines
8.8 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Config Renderer - Handles all rendering logic
class ConfigRenderer {
constructor() {
this.fieldTypes = {
text: this.renderTextField.bind(this),
number: this.renderNumberField.bind(this),
email: this.renderEmailField.bind(this),
password: this.renderPasswordField.bind(this),
select: this.renderSelectField.bind(this),
checkbox: this.renderCheckboxField.bind(this),
textarea: this.renderTextareaField.bind(this)
};
}
renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription, isAdvanced = false) {
const isEnabled = masterKey.value === 'true';
const sectionId = `master-${masterKey.key}`;
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="git-master-toggle">
<div class="field-group">
<label class="checkbox-label master-toggle">
<input type="checkbox" id="${masterKey.key.toLowerCase()}-toggle" name="${masterKey.key}" value="true" ${isEnabled ? 'checked' : ''} onchange="window.ConfigShared.toggleSection('section-content-${sectionId}', this.checked)">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
${masterKey.title || 'Enable Advanced Configuration'}
<span class="tooltip" title="${masterKey.description || 'Enable advanced configuration options'}"></span>
</span>
</label>
</div>
</div>
<div id="section-content-${sectionId}" class="git-section-content ${isEnabled ? '' : 'hidden'}">
<div class="config-fields">
`;
// Add all other fields (excluding the master toggle)
configItems.filter(item => item.key !== masterKey.key).forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options) || '';
});
html += `
</div>
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription, config = {}) {
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="config-fields">
`;
// Add all fields
configItems.forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
});
html += `
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
renderRegularSubcategory(configItems, displaySubcategory, subcategoryDescription, config = {}) {
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="config-fields">
`;
// Add all fields
configItems.forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
});
html += `
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
// Field rendering methods
renderTextField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="text" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderNumberField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="number" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" min="${options?.min || ''}" max="${options?.max || ''}" step="${options?.step || '1'}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderEmailField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="email" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderPasswordField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<div class="password-input-group">
<input type="password" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
<button type="button" class="password-toggle-btn" onclick="window.configRenderer.togglePasswordVisibility('${fieldId}')">
<span class="password-icon">👁️</span>
</button>
</div>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderSelectField(fieldId, key, value, title, description, fieldOptions) {
const options = window.ConfigOptions?.getSelectOptions(key) || [];
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<select id="${fieldId}" name="${key}" class="form-control">
${options.map(opt => `<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>${opt.label}</option>`).join('')}
</select>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderCheckboxField(fieldId, key, value, title, description, options) {
const isChecked = value === 'true' || value === true;
return `
<div class="field-group">
<label class="checkbox-label">
<input type="checkbox" id="${fieldId}" name="${key}" value="true" ${isChecked ? 'checked' : ''}>
<span class="checkbox-custom"></span>
<span class="checkbox-text">${title || this.cleanDescription(key)}</span>
</label>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderTextareaField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<textarea id="${fieldId}" name="${key}" class="form-control" rows="${options?.rows || '4'}" placeholder="${options?.placeholder || ''}">${value || ''}</textarea>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
togglePasswordVisibility(fieldId) {
const input = document.getElementById(fieldId);
const button = document.querySelector(`button[onclick*="${fieldId}"]`);
if (input && button) {
if (input.type === 'password') {
input.type = 'text';
button.innerHTML = '<span class="password-icon">🙈️</span>';
} else {
input.type = 'password';
button.innerHTML = '<span class="password-icon">👁️</span>';
}
}
}
cleanDescription(description) {
if (!description) return '';
return description.replace(/CFG_[A-Z_]+/g, '').replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
}
// Export for use in other modules
window.ConfigRenderer = ConfigRenderer;