librelad eaafd1bb38 refactor(webui): relocate admin area into features/admin/ + shared extractions
- features/admin/: the 10 admin-owned config controllers, the 5 admin pages
  (overview/system/charts/metric/storage), ssh-page.js, peers-page.js, plus
  admin.css/ip-whitelist.css/ssh.css (eager). config-manager.js kept last in
  the load order (it news the sub-managers).
- shared/js/: config-shared.js + config-options.js (ConfigShared/ConfigOptions
  globals consumed cross-feature by backup/apps/tasks).
- shared/css/: forms.css + config.css (generic form + config-form primitives
  borrowed by apps/backup/admin).
- Updated all path strings in system-loader.js (config component) and
  config-manager.js (lazyLoad of admin/ssh/peers controllers); index.html CSS
  hrefs. No /js/components/{config,admin,ssh,peers}/ refs remain.

js/components/ now holds only shared UI (topbar, notifications, eo-modal,
update-notifier, mobile-menu, confirmation-dialog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 02:10:09 +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;