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

320 lines
12 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.

// Universal Toggle Manager - Complete solution for all config toggles
class ToggleManager {
constructor() {
this.toggles = new Map();
this.init();
}
init() {
//console.log('ToggleManager: Initializing...');
// Auto-discover all toggle configurations
this.discoverToggles();
//console.log('ToggleManager: Discovered', this.toggles.size, 'toggle configurations');
// Debug: Log discovered toggles
if (this.toggles.size > 0) {
//console.log('ToggleManager: No toggles found - will retry after config loads...');
// Set up a mutation observer to detect when config content is added
this.setupContentObserver();
} else {
//console.log('ToggleManager: Discovered toggles:', Array.from(this.toggles.keys()));
}
// Also set up observer in case more toggles are added later
this.setupContentObserver();
// Retry discovery after a short delay to handle timing issues
setTimeout(() => {
//console.log('ToggleManager: Retrying toggle discovery...');
this.rediscoverToggles();
}, 100);
}
// Set up observer to detect when config content is loaded
setupContentObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Check if any new elements with data-toggle-config were added
const newToggles = Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE && (node.dataset?.toggleConfig || node.querySelector('[data-toggle-config]')));
if (newToggles.length > 0) {
//console.log('ToggleManager: New toggle elements detected, re-discovering...');
this.rediscoverToggles();
observer.disconnect(); // Stop observing once we find toggles
}
}
});
});
// Start observing the main content area
const contentContainer = document.getElementById('main-content') || document.querySelector('.main');
if (contentContainer) {
observer.observe(contentContainer, {
childList: true,
subtree: true
});
}
}
// Re-discover toggles (called after content is loaded)
rediscoverToggles() {
//console.log('ToggleManager: Re-discovering toggles...');
this.toggles.clear(); // Clear existing toggles
this.discoverToggles();
//console.log('ToggleManager: Re-discovered', this.toggles.size, 'toggle configurations');
if (this.toggles.size > 0) {
//console.log('ToggleManager: Successfully discovered toggles:', Array.from(this.toggles.keys()));
}
}
// Auto-discover toggle configurations from the page
discoverToggles() {
//console.log('ToggleManager: Looking for elements with [data-toggle-config]...');
// Find all elements with data-toggle-config attribute
const toggleElements = document.querySelectorAll('[data-toggle-config]');
//console.log('ToggleManager: Found', toggleElements.length, 'elements with data-toggle-config');
toggleElements.forEach((element, index) => {
const config = element.dataset.toggleConfig;
const sectionId = element.dataset.sectionId;
const toggleType = element.dataset.toggleType || 'checkbox';
//console.log(`ToggleManager: Processing element ${index}:`, {
//config: config,
//sectionId: sectionId,
//toggleType: toggleType,
//element: element.tagName + (element.id ? '#' + element.id : '') + (element.name ? '[name=' + element.name + ']' : '')
//});
if (config && sectionId) {
this.toggles.set(config, {
config: config,
sectionId: sectionId,
toggleType: toggleType,
element: element
});
//console.log(`ToggleManager: Registered toggle for config: ${config}`);
} else {
//console.log(`ToggleManager: Skipping element - missing config or sectionId`);
}
});
}
// Universal toggle function - works for any config option
toggle(configKey, isEnabled) {
//console.log('=== TOGGLE MANAGER DEBUG ===');
//console.log('configKey:', configKey);
//console.log('isEnabled:', isEnabled);
const toggle = this.toggles.get(configKey);
if (!toggle) {
console.error('ToggleManager: No toggle found for config:', configKey);
return false;
}
//console.log('Toggle found:', toggle);
const sectionContent = document.getElementById(toggle.sectionId);
const fields = sectionContent?.querySelectorAll('.config-fields input, .config-fields select, .config-fields textarea');
//console.log('sectionContent found:', !!sectionContent);
//console.log('fields found:', fields ? fields.length : 0);
if (sectionContent && fields) {
if (isEnabled) {
//console.log('Enabling section...');
sectionContent.classList.remove('hidden');
fields.forEach((field, index) => {
//console.log(`Enabling field ${index}:`, field);
field.disabled = false;
const fieldGroup = field?.closest('.field-group');
if (fieldGroup) {
fieldGroup.style.opacity = '1';
fieldGroup.style.pointerEvents = 'auto';
}
});
} else {
//console.log('Disabling section...');
sectionContent.classList.add('hidden');
fields.forEach((field, index) => {
//console.log(`Disabling field ${index}:`, field);
field.disabled = true;
const fieldGroup = field?.closest('.field-group');
if (fieldGroup) {
fieldGroup.style.opacity = '0.5';
fieldGroup.style.pointerEvents = 'none';
}
});
}
//console.log('=== TOGGLE MANAGER SUCCESS ===');
return true;
} else {
console.error('ToggleManager: Section content or fields not found');
return false;
}
}
// Universal toggle section renderer - works for ANY config option
static renderToggleSection(configKey, configItems, displaySubcategory, subcategoryDescription, config) {
const isEnabled = configKey.value === 'true' || configKey.value === 'git';
const sectionId = `${configKey.key.replace(/[^a-zA-Z0-9]/g, '-')}-${configKey.key}`;
const toggleId = `${configKey.key.toLowerCase()}-toggle`;
// Determine toggle type and class based on config key
let toggleType = 'checkbox';
let toggleClass = 'generic-master-toggle';
if (configKey.key.includes('INSTALL_MODE')) {
toggleType = 'select';
toggleClass = 'git-master-toggle';
} else if (configKey.key.includes('MAIL')) {
toggleType = 'checkbox';
toggleClass = 'mail-master-toggle';
} else if (configKey.key.includes('BACKUP_REMOTE_')) {
// Specifically target BACKUP_REMOTE_1_ENABLED, BACKUP_REMOTE_2_ENABLED, etc.
toggleType = 'checkbox';
toggleClass = 'backup-remote-toggle';
}
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="${toggleClass}">
<div class="field-group">
`;
if (toggleType === 'select') {
// Git/Select toggle
html += `
<label for="${toggleId}">
${configKey.title || 'Install Mode'}
<span class="tooltip" title="${configKey.description || 'Choose installation mode'}"></span>
</label>
<select id="${toggleId}"
name="${configKey.key}"
class="form-control"
data-toggle-config="${configKey.key}"
data-section-id="section-content-${sectionId}"
data-toggle-type="select"
onchange="window.toggleManager.toggle('${configKey.key}', this.value === 'git')">
${window.ConfigOptions?.getSelectOptions(configKey.key)?.map(opt =>
`<option value="${opt.value}" ${opt.value === configKey.value ? 'selected' : ''}>${opt.label}</option>`
).join('') || ''}
</select>
`;
} else {
// Checkbox toggle
html += `
<label class="checkbox-label ${toggleClass}">
<input type="checkbox"
id="${toggleId}"
name="${configKey.key}"
value="true"
${isEnabled ? 'checked' : ''}
data-toggle-config="${configKey.key}"
data-section-id="section-content-${sectionId}"
data-toggle-type="checkbox"
onchange="window.toggleManager.toggle('${configKey.key}', this.checked)">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
${configKey.title || 'Enable Configuration'}
<span class="tooltip" title="${configKey.description || 'Enable this configuration option'}"></span>
</span>
</label>
`;
}
html += `
</div>
</div>
<div id="section-content-${sectionId}" class="git-section-content ${isEnabled ? '' : 'hidden'}">
<div class="config-fields">
`;
// Add all other fields (excluding the toggle key itself)
configItems.filter(item => item.key !== configKey.key).forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
});
// Add special buttons for specific config types
if (configKey.key.includes('MAIL')) {
html += `
<div class="field-group">
<button type="button" class="test-connection-btn" onclick="window.configManager?.testMailConnection('${configKey.key}')">
<span class="test-icon">📧</span>
<span class="test-text">Test Mail Connection</span>
</button>
<div id="mail-test-result" class="test-result" style="display: none;"></div>
</div>
`;
}
html += `
</div>
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
// Register a new toggle configuration
register(configKey, sectionId, toggleType = 'checkbox') {
this.toggles.set(configKey, {
config: configKey,
sectionId: sectionId,
toggleType: toggleType
});
}
// Get all registered toggles
getToggles() {
return Array.from(this.toggles.keys());
}
// Check if a toggle exists
hasToggle(configKey) {
return this.toggles.has(configKey);
}
}
// Global instance
window.toggleManager = new ToggleManager();
// Make static method available on instance too
window.toggleManager.renderToggleSection = ToggleManager.renderToggleSection;
// Add method to manually trigger discovery when config is loaded
window.toggleManager.forceRediscover = function() {
//console.log('ToggleManager: Force re-discovering toggles...');
window.toggleManager.rediscoverToggles();
};
// Static access for backward compatibility
window.ConfigShared = window.ConfigShared || {};
window.ConfigShared.toggleSection = function(sectionId, isEnabled) {
// Try to find the config key from the section
const toggleElements = document.querySelectorAll(`[data-section-id="${sectionId}"]`);
if (toggleElements.length > 0) {
const configKey = toggleElements[0].dataset.toggleConfig;
return window.toggleManager.toggle(configKey, isEnabled);
} else {
console.error('ConfigShared.toggleSection: No toggle found for section:', sectionId);
return false;
}
};