// Simple Config Manager - Direct approach without complex dependencies class ConfigManager { constructor() { this.cache = new Map(); } getRandomLoadingMessage() { const messages = [ "Preparing your configuration settings...", "Gathering the finest configuration options...", "Tuning up your system preferences...", "Organizing your configuration categories...", "Loading the perfect settings for you...", "Crafting your personalized configuration...", "Aligning your configuration stars...", "Brewing the ideal configuration blend...", "Setting up your configuration masterpiece...", "Polishing your configuration preferences...", "Configuring things just right for you...", "Preparing your digital control panel...", "Gathering your system's best settings...", "Optimizing your configuration experience...", "Loading your configuration superpowers..." ]; return messages[Math.floor(Math.random() * messages.length)]; } async loadConfig(category) { //console.log(`ConfigManager: Loading ${category} config...`); // Check cache first if (this.cache.has('unified')) { //console.log(`ConfigManager: Using cached unified config`); const unifiedData = this.cache.get('unified'); return this.filterConfigByCategory(unifiedData, category); } try { // Load unified config data const response = await fetch('/data/config/generated/configs.json'); if (!response.ok) { throw new Error(`Failed to load configs.json: ${response.status}`); } const configData = await response.json(); //console.log(`ConfigManager: Loaded unified config:`, configData); // Cache the result this.cache.set('unified', configData); // Filter by requested category const categoryData = this.filterConfigByCategory(configData, category); //console.log(`ConfigManager: Filtered ${category} config:`, categoryData); return categoryData; } catch (error) { console.error(`ConfigManager: Error loading ${category} config:`, error); return { config: {}, description: 'Failed to load configuration' }; } } filterConfigByCategory(unifiedData, category) { if (!unifiedData || !unifiedData.config) { return { config: {}, categories: {} }; } // Filter config items by category const filteredConfig = {}; Object.entries(unifiedData.config).forEach(([key, value]) => { if (value.category === category) { filteredConfig[key] = value; } }); return { config: filteredConfig, categories: unifiedData.categories || {}, subcategories: unifiedData.subcategories || {}, configType: unifiedData.configType, name: unifiedData.name }; } async renderConfig(category) { //console.log(`ConfigManager: Rendering ${category} config...`); const configSection = document.getElementById('config-section'); if (!configSection) { console.error('ConfigManager: config-section element not found'); return; } // Show loading with enhanced visual configSection.innerHTML = `
Loading configuration...
${this.getRandomLoadingMessage()}
`; // Update loading bar if available if (typeof router !== 'undefined' && router.updateProgress) { router.updateProgress(60); } try { // Load config data const configData = await this.loadConfig(category); const config = configData.config || {}; // Update loading bar if (typeof router !== 'undefined' && router.updateProgress) { router.updateProgress(70); } if (Object.keys(config).length === 0) { configSection.innerHTML = '
No configuration available
'; return; } // Use the original ConfigShared system for beautiful rendering if (typeof ConfigShared !== 'undefined') { await this.renderWithOriginalStyling(category, configData); } else { // Fallback: load ConfigShared and then render if (typeof router !== 'undefined' && router.updateProgress) { router.updateProgress(75); } await this.loadScript('/js/components/config/config-options.js'); await this.loadScript('/js/components/config/config-shared.js'); await this.renderWithOriginalStyling(category, configData); } // Final progress update if (typeof router !== 'undefined' && router.updateProgress) { router.updateProgress(80); } //console.log(`ConfigManager: Successfully rendered ${category} config`); // Initialize git field visibility after rendering if (typeof initializeGitFieldVisibility === 'function') { setTimeout(() => { initializeGitFieldVisibility(); }, 100); } } catch (error) { console.error(`ConfigManager: Error rendering ${category} config:`, error); configSection.innerHTML = `
Failed to load ${category} configuration: ${error.message}
`; } } async renderWithOriginalStyling(category, configData) { //console.log(`renderWithOriginalStyling: category=${category}, CFG_INSTALL_MODE=${configData.config?.CFG_INSTALL_MODE?.value}`); const configSection = document.getElementById('config-section'); const config = configData.config || {}; const subcategories = configData.subcategories || {}; // Check if we have subcategories data available const hasSubcategories = Object.keys(subcategories).length > 0; // Use shared categorization functionality const categorized = ConfigShared.categorizeConfigs(config); const { groupedConfigs, regularCategories, advancedCategories, unusedCategories } = categorized; // Render using the original system's approach with advanced/unused sections let formHTML = `

${this.formatCategoryName(category)} Configuration

${configData.description || 'Configure settings for ' + category}

`; // Add requirements warning for requirements category if (category === 'requirements') { formHTML += ConfigShared.generateRequirementsWarning(); } // Add danger zone warning for features category if (category === 'features') { formHTML += `
âš ī¸
Danger Zone - These options are for advanced users and may affect system stability
`; } // Render using subcategories structure if available, otherwise fall back to original if (hasSubcategories) { // Render using new subcategories structure const regularSubcategories = []; const advancedSubcategories = []; const unusedSubcategories = []; // Filter subcategories by category and separate into regular, advanced, and unused for (const [subcategoryName, subcategoryData] of Object.entries(subcategories)) { if (subcategoryData.category === category) { if (subcategoryData.description.includes('**ADVANCED**')) { advancedSubcategories.push(subcategoryName); } else if (subcategoryData.description.includes('**UNUSED**')) { unusedSubcategories.push(subcategoryName); } else { regularSubcategories.push(subcategoryName); } } } // Render regular subcategories with proper sectioning for (const subcategoryName of regularSubcategories) { const subcategoryData = subcategories[subcategoryName]; const rawSubcategoryTitle = subcategoryData.title || ConfigShared.formatCategoryName(subcategoryName); const displaySubcategory = ConfigShared.stripCategoryPrefix(rawSubcategoryTitle, category); const subcategoryDescription = subcategoryData.description || 'Subcategory configuration'; // Find config items for this subcategory const configItems = Object.entries(config) .filter(([key, value]) => value.subcategory === subcategoryName) .map(([key, value]) => ({ key, ...value })); if (configItems.length > 0) { // Check for master toggle in this subcategory const masterKey = configItems.find(item => item.master === true); // Find any ENABLED options and use universal toggle renderer const enabledKey = configItems.find(item => item.key.includes('ENABLED') || item.key === 'CFG_INSTALL_MODE'); //console.log('ConfigManager: Checking for toggle - subcategoryName:', subcategoryName, 'enabledKey found:', !!enabledKey, enabledKey ? enabledKey.key : null); // Special handling for domains section const isDomains = subcategoryName.includes('domains'); if (enabledKey) { // Use universal toggle renderer for any ENABLED option or CFG_INSTALL_MODE formHTML += ToggleManager.renderToggleSection(enabledKey, configItems, displaySubcategory, subcategoryDescription, config); } else if (masterKey) { // Render with master toggle formHTML += this.renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription); } else if (isDomains) { // Render domains section with special handling formHTML += await this.renderDomainsSection(configItems, displaySubcategory, subcategoryDescription); } else { // Render regular subcategory with proper sectioning formHTML += this.renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription, config); } } } // Render advanced subcategories (hidden by default) if (advancedSubcategories.length > 0) { // Add danger zone toggle for advanced sections formHTML += ConfigShared.generateToggleControls(true, false); // Generate advanced sections using shared functionality const advancedGroupedConfigs = {}; advancedSubcategories.forEach(subcategoryName => { const configItems = Object.entries(config) .filter(([key, value]) => value.subcategory === subcategoryName) .map(([key, value]) => ({ key, ...value })); if (configItems.length > 0) { advancedGroupedConfigs[subcategoryName] = configItems.map(item => item.key); } }); formHTML += await ConfigShared.generateAdvancedSections( advancedSubcategories, advancedGroupedConfigs, config, (category) => this.cleanDescription(subcategories[category]?.description || 'Advanced settings') ); } // Render unused subcategories (hidden by default) if (unusedSubcategories.length > 0) { // Add unused section toggle formHTML += ConfigShared.generateToggleControls(false, true); // Wrap unused sections in hidden container formHTML += ` `; } } else { // Fall back to original categorization system const categorized = ConfigShared.categorizeConfigs(config); const { groupedConfigs, regularCategories, advancedCategories, unusedCategories } = categorized; // Render regular categories (always visible) for (const cat of regularCategories) { const keys = groupedConfigs[cat]; if (keys && keys.length > 0 && cat !== 'Hidden/Unused Options') { const displayCategory = ConfigShared.formatCategoryName(cat); const categoryDescription = await ConfigShared.getCategoryDescription(cat); // Check if this category has a master toggle (any key with master: true) const masterKey = keys.find(key => { const configItem = config[key] || {}; return configItem.master === true; }); if (masterKey) { // Dynamic master toggle handling const masterValue = config[masterKey]?.value || 'false'; const isMasterEnabled = masterValue === 'true'; const masterTitle = config[masterKey]?.title || ConfigShared.formatConfigLabel(masterKey); const masterDescription = config[masterKey]?.description || ''; const sectionId = `${cat.toLowerCase().replace(/[^a-z0-9]/g, '-')}`; const toggleId = `${masterKey.toLowerCase()}-toggle`; formHTML += `

${displayCategory}

${categoryDescription}

`; // Add all other fields (excluding the master toggle) keys.filter(key => key !== masterKey).forEach(key => { const configItem = config[key] || {}; const value = configItem.value || ''; const title = configItem.title || ConfigShared.formatConfigLabel(key); const description = configItem.description || ''; const options = configItem.options || ''; const fieldId = `config-${key}`; formHTML += ConfigShared.generateField(fieldId, key, value, title, description, options, config); }); formHTML += `
`; } else if (cat === 'DOMAINS') { // Check if Traefik is installed const traefikInstalled = await this.checkTraefikInstallation(); // Always show the domains section, but add a warning banner if Traefik is not installed formHTML += `

${displayCategory}

${categoryDescription}

`; if (!traefikInstalled) { // Show smaller warning banner formHTML += `
âš ī¸
Traefik Not Installed - Domain settings won't be applied until Traefik is installed. You can configure domains now and install Traefik later.
`; } formHTML += `
`; // Only show domains that have content (non-empty values) const allDomainKeys = keys.filter(key => key.startsWith('CFG_DOMAIN_')); const domainKeysWithContent = allDomainKeys.filter(key => { const configItem = config[key] || {}; const value = configItem.value || ''; return value.trim() !== ''; }); // Check if we've reached the maximum of 9 domains (count only existing domains, not empty slots) const isMaxDomains = domainKeysWithContent.length >= 9; domainKeysWithContent.forEach(key => { const configItem = config[key] || {}; const value = configItem.value || ''; const title = configItem.title || ConfigShared.formatConfigLabel(key); const fieldId = `config-${key}`; // Extract domain number const domainNum = parseInt(key.match(/CFG_DOMAIN_(\d+)/)[1]); const isHighestDomain = domainNum === Math.max(...domainKeysWithContent.map(k => parseInt(k.match(/CFG_DOMAIN_(\d+)/)[1]) )); // Domain 1 can never be deleted, and only highest numbered domain WITH CONTENT can be deleted const canDelete = isHighestDomain && domainNum !== 1; formHTML += `
${ConfigShared.generateField(fieldId, key, value, title, '', { placeholder: 'example.com', className: 'domain-input' })}
`; }); formHTML += `
`; } else { // Regular category handling (no master toggle) formHTML += `

${displayCategory}

${categoryDescription}

${ConfigShared.generateFieldsForCategory(keys, cat, config, (fieldId, key, value, title, description, options, config) => ConfigShared.generateField(fieldId, key, value, title, description, options, config))}
`; } } } } // Add danger zone before advanced/unused sections (so content appears below) formHTML += ConfigShared.generateToggleControls( advancedCategories.length > 0, unusedCategories.length > 0 ); // Add advanced and unused sections using shared functionality formHTML += await ConfigShared.generateAdvancedSections( advancedCategories, groupedConfigs, config, (category) => ConfigShared.getCategoryDescription(category) ); formHTML += await ConfigShared.generateUnusedSections( unusedCategories, groupedConfigs, config, (category) => ConfigShared.getCategoryDescription(category) ); formHTML += `
`; configSection.innerHTML = formHTML; // Initialize all master toggles dynamically setTimeout(() => { // Find all master toggle checkboxes (any input with id ending in "-toggle" where the name ends with "_ENABLED") const masterToggles = document.querySelectorAll('input[id$="-toggle"][name$="_ENABLED"]'); masterToggles.forEach(toggle => { const sectionId = toggle.id.replace('-toggle', ''); const section = document.getElementById(`section-content-${sectionId}`); if (section && typeof ConfigShared.toggleSectionVisibility === 'function') { // Initialize the section state based on the toggle ConfigShared.toggleSectionVisibility(`section-content-${sectionId}`, toggle.checked); } }); }, 100); } // Check if Traefik is installed async checkTraefikInstallation() { try { // Use the generic app installation checker return await DataLoader.isAppInstalled('traefik'); } catch (error) { //console.log('Traefik check failed:', error.message); return false; } } // Domain management functions addNewDomain() { //console.log('Add Domain button clicked!'); try { // Find the highest existing domain number const domainInputs = document.querySelectorAll('input[name^="CFG_DOMAIN_"]'); const domainNumbers = []; domainInputs.forEach(input => { const match = input.name.match(/CFG_DOMAIN_(\d+)/); if (match) { domainNumbers.push(parseInt(match[1])); } }); // Check if we've reached the maximum of 9 domains if (domainNumbers.length >= 9) { //console.log('Maximum of 9 domains reached'); return; } const nextDomainNumber = domainNumbers.length > 0 ? Math.max(...domainNumbers) + 1 : 1; const newDomainKey = `CFG_DOMAIN_${nextDomainNumber}`; const newFieldId = `config-${newDomainKey}`; // Create new domain building block HTML const newDomainHTML = `
${ConfigShared.generateField(newFieldId, newDomainKey, '', `Domain ${nextDomainNumber}`, '', { placeholder: 'example.com' })}
`; // Find the domain-building-blocks container and add the new block const domainContainer = document.querySelector('.domain-building-blocks'); if (domainContainer) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = newDomainHTML; const newBlock = tempDiv.firstElementChild; domainContainer.appendChild(newBlock); // Focus on the new input field const newInput = newBlock.querySelector('input'); if (newInput) { newInput.focus(); } // Update add domain button state const addBtn = document.getElementById('add-domain-btn'); if (addBtn) { const totalDomains = document.querySelectorAll('.domain-building-block').length; if (totalDomains >= 9) { addBtn.disabled = true; addBtn.className = 'btn btn-secondary'; addBtn.innerHTML = '✓Maximum Domains Reached'; } } // Update delete button states (only highest numbered domain should be deletable) const allDomainBlocks = document.querySelectorAll('.domain-building-block'); allDomainBlocks.forEach((block, index) => { const deleteBtn = block.querySelector('.delete-domain-btn'); if (deleteBtn) { const input = block.querySelector('input'); const inputName = input ? input.name : ''; const domainNum = inputName.match(/CFG_DOMAIN_(\d+)/); const domainNumber = domainNum ? parseInt(domainNum[1]) : 0; // Find the highest domain number among all visible blocks const allDomainNumbers = Array.from(allDomainBlocks).map(b => { const inp = b.querySelector('input'); const name = inp ? inp.name : ''; const match = name.match(/CFG_DOMAIN_(\d+)/); return match ? parseInt(match[1]) : 0; }); const highestDomainNumber = Math.max(...allDomainNumbers); // Only highest numbered domain can be deleted, but NEVER Domain 1 const canDelete = domainNumber === highestDomainNumber && domainNumber !== 1; deleteBtn.disabled = !canDelete; deleteBtn.className = `delete-domain-btn ${!canDelete ? 'disabled' : ''}`; deleteBtn.title = canDelete ? 'Delete domain' : 'Can only delete highest numbered domain'; } }); } } catch (error) { console.error('Error adding new domain:', error); } } deleteDomain(domainKey, buttonElement) { //console.log(`Delete domain button clicked for: ${domainKey}`); try { // Find the domain-building-block and remove it const domainBlock = buttonElement.closest('.domain-building-block'); if (domainBlock) { // Clear the input value first const input = domainBlock.querySelector('input'); if (input) { input.value = ''; } // Remove the entire building block domainBlock.remove(); // Update add domain button state (re-enable if we're below 9 domains) const addBtn = document.getElementById('add-domain-btn'); if (addBtn) { const totalDomains = document.querySelectorAll('.domain-building-block').length; if (totalDomains < 9) { addBtn.disabled = false; addBtn.className = 'btn btn-primary'; addBtn.innerHTML = '+Add Domain'; } } // Update delete button states (only highest numbered domain should be deletable) const allDomainBlocks = document.querySelectorAll('.domain-building-block'); allDomainBlocks.forEach((block, index) => { const deleteBtn = block.querySelector('.delete-domain-btn'); if (deleteBtn) { const input = block.querySelector('input'); const inputName = input ? input.name : ''; const domainNum = inputName.match(/CFG_DOMAIN_(\d+)/); const domainNumber = domainNum ? parseInt(domainNum[1]) : 0; // Find the highest domain number among all visible blocks const allDomainNumbers = Array.from(allDomainBlocks).map(b => { const inp = b.querySelector('input'); const name = inp ? inp.name : ''; const match = name.match(/CFG_DOMAIN_(\d+)/); return match ? parseInt(match[1]) : 0; }); const highestDomainNumber = Math.max(...allDomainNumbers); // Only highest numbered domain can be deleted, but NEVER Domain 1 const canDelete = domainNumber === highestDomainNumber && domainNumber !== 1; deleteBtn.disabled = !canDelete; deleteBtn.className = `delete-domain-btn ${!canDelete ? 'disabled' : ''}`; deleteBtn.title = canDelete ? 'Delete domain' : 'Can only delete highest numbered domain'; } }); } } catch (error) { console.error('Error deleting domain:', error); } } async loadScript(src) { const scriptId = src.replace(/[^a-zA-Z0-9]/g, '_'); const existingScript = document.getElementById(scriptId); if (existingScript && src.includes('config-shared.js')) { existingScript.remove(); } else if (existingScript) { return; } return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.id = scriptId; script.onload = resolve; script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); document.head.appendChild(script); }); } formatCategoryName(category) { return category .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); } static async saveConfig(category) { const form = document.getElementById(`config-form-${category}`); if (!form) return; const formData = new FormData(form); const config = {}; for (const [key, value] of formData.entries()) { const checkbox = form.querySelector(`input[name="${key}"][type="checkbox"]`); if (checkbox) { config[key] = checkbox.checked; } else { config[key] = value; } } } static async resetConfig(category) { if (confirm('Are you sure you want to reset all settings to their default values?')) { window.location.reload(); } } // Helper method to render subcategory with master toggle renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription, isAdvanced = false) { const masterValue = masterKey.value || 'false'; const isMasterEnabled = masterValue === 'true'; const masterTitle = masterKey.title || ConfigShared.formatConfigLabel(masterKey.key); const masterDescription = masterKey.description || ''; const sectionId = `${displaySubcategory.toLowerCase().replace(/[^a-z0-9]/g, '-')}`; const toggleId = `${masterKey.key.toLowerCase()}-toggle`; let html = `

${displaySubcategory}

${subcategoryDescription}

`; // Add all other fields (excluding the master toggle) configItems.filter(item => item.key !== masterKey.key).forEach(item => { const fieldId = `config-${item.key}`; html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config); }); html += `
`; return html; } // Helper method to render domains section with special handling async renderDomainsSection(configItems, displaySubcategory, subcategoryDescription) { // Check if Traefik is installed const traefikInstalled = await this.checkTraefikInstallation(); let html = `

${displaySubcategory}

${subcategoryDescription}

`; if (!traefikInstalled) { // Show smaller warning banner html += `
âš ī¸
Traefik Not Installed - Domain settings won't be applied until Traefik is installed. You can configure domains now and install Traefik later.
`; } html += `
`; // Only show domains that have content (non-empty values) const allDomainKeys = configItems.filter(item => item.key.startsWith('CFG_DOMAIN_')); const domainKeysWithContent = allDomainKeys.filter(item => { const value = item.value || ''; return value.trim() !== ''; }); // Only render domains that have content domainKeysWithContent.forEach(item => { const value = item.value || ''; const title = item.title || ConfigShared.formatConfigLabel(item.key); const fieldId = `config-${item.key}`; // Extract domain number const domainNum = parseInt(item.key.match(/CFG_DOMAIN_(\d+)/)[1]); const isHighestDomain = domainNum === Math.max(...domainKeysWithContent.map(k => parseInt(k.key.match(/CFG_DOMAIN_(\d+)/)[1]) )); // Domain 1 can never be deleted, and only highest numbered domain WITH CONTENT can be deleted const canDelete = isHighestDomain && domainNum !== 1; html += `
${ConfigShared.generateField(fieldId, item.key, value, title, '', { placeholder: 'example.com', className: 'domain-input', onchange: 'window.configManager.validateDomainFormat(this, true)', oninput: 'window.configManager.validateDomainFormat(this, true)', onblur: 'window.configManager.validateDomainFormat(this, true)' })}
`; }); // Add "Add Domain" button outside the grid const isMaxDomains = domainKeysWithContent.length >= 9; html += `
`; return html; } // Helper method to render remote backup section with toggle renderRemoteBackupSection(backupKey, configItems, displaySubcategory, subcategoryDescription, config = {}) { const isEnabled = backupKey.value === 'true'; const sectionId = `backup-${backupKey.key}`; const toggleId = `${backupKey.key.toLowerCase()}-toggle`; let html = `

${displaySubcategory}

${subcategoryDescription}

`; // Add all other fields (excluding the ENABLED toggle) configItems.filter(item => item.key !== backupKey.key).forEach(item => { const fieldId = `config-${item.key}`; html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config); }); html += `
`; return html; } // Helper method to render mail section with toggle renderMailSection(mailKey, configItems, displaySubcategory, subcategoryDescription, config = {}) { const isEnabled = mailKey.value === 'true'; const sectionId = `mail-${mailKey.key}`; const toggleId = `${mailKey.key.toLowerCase()}-toggle`; let html = `

${displaySubcategory}

${subcategoryDescription}

`; // Add all other fields (excluding the ENABLED toggle) configItems.filter(item => item.key !== mailKey.key).forEach(item => { const fieldId = `config-${item.key}`; html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config); }); // Add test connection button after all mail fields html += `
`; return html; } // Test mail server connection async testMailConnection(mailKey) { const resultDiv = document.getElementById('mail-test-result'); const button = event.target.closest('button'); // Show loading state button.disabled = true; button.innerHTML = 'âŗTesting...'; resultDiv.style.display = 'block'; resultDiv.className = 'test-result testing'; resultDiv.innerHTML = 'Testing mail server connection...'; // Initialize mailConfig outside try block for catch block access let mailConfig = {}; try { // Get current mail configuration values from the form mailConfig = { host: document.querySelector('input[name="CFG_MAIL_HOST"]')?.value || '', port: document.querySelector('input[name="CFG_MAIL_PORT"]')?.value || '', secure: document.querySelector('select[name="CFG_MAIL_SECURE"]')?.value || '', username: document.querySelector('input[name="CFG_MAIL_USERNAME"]')?.value || '', password: document.querySelector('input[name="CFG_MAIL_PASSWORD"]')?.value || '', from: document.querySelector('input[name="CFG_MAIL_FROM"]')?.value || '' }; // Validate required fields if (!mailConfig.host || !mailConfig.port || !mailConfig.username || !mailConfig.password) { throw new Error('Please fill in all required mail server fields (host, port, username, password)'); } // Call backend test script const response = await fetch('/api/test-mail-connection', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(mailConfig) }); const result = await response.json(); if (result.success) { resultDiv.className = 'test-result success'; resultDiv.innerHTML = `✅ ${result.message || 'Mail server connection successful!'}${result.details ? `
${result.details}` : ''}`; } else { resultDiv.className = 'test-result error'; let errorHtml = `❌ ${result.message || 'Mail server connection failed'}`; // Add detailed error information directly underneath if (result.details || result.error || result.config) { errorHtml += `
Error Details:
${result.details || result.error || 'No additional details available'} ${result.stack ? `

Stack Trace:
${result.stack}` : ''} ${result.config ? `

Connection Config:
${JSON.stringify(result.config, null, 2)}` : ''}
`; } resultDiv.innerHTML = errorHtml; } } catch (error) { resultDiv.className = 'test-result error'; resultDiv.innerHTML = ` ❌ ${error.message || 'Failed to test mail connection'}
Error Details:
${error.message || 'Unknown error'} ${error.stack ? `

Stack Trace:
${error.stack}` : ''} ${error.response ? `

Response:
${JSON.stringify(error.response, null, 2)}` : ''} ${mailConfig ? `

Mail Config:
${JSON.stringify({...mailConfig, password: mailConfig.password ? '[REDACTED]' : undefined}, null, 2)}` : ''}
`; } finally { // Restore button state button.disabled = false; button.innerHTML = '📧Test Mail Connection'; } } // Helper method to render Git section with toggle renderGitSection(gitKey, configItems, displaySubcategory, subcategoryDescription, config = {}) { // CFG_INSTALL_MODE controls git section: 'git' = enabled, 'local' = disabled const isEnabled = gitKey.value === 'git'; const sectionId = `git-${gitKey.key}`; const toggleId = `${gitKey.key.toLowerCase()}-toggle`; let html = `

${displaySubcategory}

${subcategoryDescription}

`; // Add all other git fields (excluding the CFG_INSTALL_MODE itself) configItems.filter(item => item.key !== gitKey.key && item.key.startsWith('CFG_GIT_')).forEach(item => { const fieldId = `config-${item.key}`; html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config); }); html += `
`; return html; } // Helper method to clean description text by removing tags cleanDescription(description) { return description .replace(/\*\*ADVANCED\*\*/g, '') .replace(/\*\*UNUSED\*\*/g, '') .replace(/^\s+|\s+$/g, '') // Trim whitespace .replace(/\s{2,}/g, ' '); // Replace multiple spaces with single space } // Update all domain delete button states updateDomainDeleteButtons() { const allDomainBlocks = document.querySelectorAll('.domain-building-block'); // Find the highest domain number (regardless of content) const domainNumbers = Array.from(allDomainBlocks).map(block => { const input = block.querySelector('input[id^="config-CFG_DOMAIN_"]'); if (input) { const match = input.id.match(/CFG_DOMAIN_(\d+)/); return match ? parseInt(match[1]) : 0; } return 0; }).filter(num => num > 0); const highestDomain = Math.max(...domainNumbers, 0); // Update delete buttons allDomainBlocks.forEach(block => { const input = block.querySelector('input[id^="config-CFG_DOMAIN_"]'); const deleteBtn = block.querySelector('.delete-domain-btn'); if (input && deleteBtn) { const match = input.id.match(/CFG_DOMAIN_(\d+)/); const domainNum = match ? parseInt(match[1]) : 0; // SIMPLE RULE: Only highest numbered domain can be deleted (except Domain 1) const canDelete = domainNum === highestDomain && domainNum !== 1; if (canDelete) { deleteBtn.classList.remove('disabled'); deleteBtn.disabled = false; deleteBtn.title = 'Delete domain'; } else { deleteBtn.classList.add('disabled'); deleteBtn.disabled = true; if (domainNum === 1) { deleteBtn.title = 'Domain 1 cannot be deleted'; } else { deleteBtn.title = 'Can only delete highest numbered domain'; } } } }); } // Validate domain format when user tries to add a new domain validateDomainFormat(input, showNotifications = true) { const value = input.value.trim(); if (!value) { return true; // Allow empty for initial state } // Basic domain validation regex - supports subdomains and multiple TLD levels const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*\.[a-zA-Z]{2,}$/; const isValidFormat = domainRegex.test(value); // Check for duplicates const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); const duplicates = Array.from(allDomainInputs).filter(otherInput => { if (otherInput === input) return false; // Skip self return otherInput.value.trim().toLowerCase() === value.toLowerCase(); }); const hasDuplicate = duplicates.length > 0; if (!isValidFormat) { input.style.borderColor = '#dc3545'; input.title = 'Invalid domain format (e.g., example.com)'; if (showNotifications && window.notificationSystem) { window.notificationSystem.error(`Invalid domain format: "${value}". Please use a valid domain like example.com`); } return false; } else if (hasDuplicate) { input.style.borderColor = '#ffc107'; input.title = 'Domain already exists!'; if (showNotifications && window.notificationSystem) { window.notificationSystem.warning(`Domain "${value}" already exists. Please use a unique domain.`); } return false; } else { input.style.borderColor = ''; input.title = ''; return true; } } // Validate email format for mail fields validateEmailFormat(input, showNotifications = true) { const value = input.value.trim(); if (!value) { return true; // Allow empty for initial state } // Email validation regex const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const isValidFormat = emailRegex.test(value); if (!isValidFormat) { input.style.borderColor = '#dc3545'; input.title = 'Invalid email format (e.g., user@example.com)'; if (showNotifications && window.notificationSystem) { window.notificationSystem.error(`Invalid email format: "${value}". Please use a valid email like user@example.com`); } return false; } else { input.style.borderColor = ''; input.title = ''; return true; } } // Validate hostname format for mail server validateHostnameFormat(input, showNotifications = true) { const value = input.value.trim(); if (!value) { return true; // Allow empty for initial state } // Hostname validation regex - allows subdomains and multiple TLD levels const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*\.[a-zA-Z]{2,}$/; const isValidFormat = hostnameRegex.test(value); if (!isValidFormat) { input.style.borderColor = '#dc3545'; input.title = 'Invalid hostname format (e.g., mail.domain.com)'; if (showNotifications && window.notificationSystem) { window.notificationSystem.error(`Invalid hostname format: "${value}". Please use a valid hostname like mail.domain.com`); } return false; } else { input.style.borderColor = ''; input.title = ''; return true; } } // Validate port number for mail server validatePortNumber(input, showNotifications = true) { const value = input.value.trim(); if (!value) { return true; // Allow empty for initial state } const port = parseInt(value, 10); const isValidPort = !isNaN(port) && port >= 1 && port <= 65535; if (!isValidPort) { input.style.borderColor = '#dc3545'; input.title = 'Invalid port number (1-65535)'; if (showNotifications && window.notificationSystem) { window.notificationSystem.error(`Invalid port number: "${value}". Please use a valid port between 1 and 65535`); } return false; } else { input.style.borderColor = ''; input.title = ''; return true; } } // Check if all domains are valid before allowing new domain addition canAddNewDomain() { const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); for (const input of allDomainInputs) { if (!this.validateDomainFormat(input, true)) { // Don't show notifications during bulk check return false; // At least one domain has invalid format or duplicate } } return true; // All domains are valid } // Add a new domain field addDomain(button) { // Find all existing domain blocks const allDomainBlocks = document.querySelectorAll('.domain-building-block'); const domainData = Array.from(allDomainBlocks).map(block => { const input = block.querySelector('input[id^="config-CFG_DOMAIN_"]'); if (input) { const match = input.id.match(/CFG_DOMAIN_(\d+)/); return { num: match ? parseInt(match[1]) : 0, value: input.value.trim(), input: input, block: block }; } return null; }).filter(item => item !== null); // Sort by domain number domainData.sort((a, b) => a.num - b.num); // Find the highest domain number with content const domainsWithContent = domainData.filter(d => d.value); const highestDomainWithContent = domainsWithContent.length > 0 ? Math.max(...domainsWithContent.map(d => d.num)) : 0; // Find the highest domain number overall (including empty) const highestDomainOverall = Math.max(...domainData.map(d => d.num), 0); // Check if the highest domain (overall) is empty const highestDomainData = domainData.find(d => d.num === highestDomainOverall); if (highestDomainData && !highestDomainData.value) { // Flash the empty highest domain without validation checks const emptyInput = document.querySelector(`input[id="config-CFG_DOMAIN_${highestDomainOverall}"]`); if (emptyInput) { emptyInput.style.animation = 'flash 0.5s ease-in-out 2'; emptyInput.focus(); setTimeout(() => { emptyInput.style.animation = ''; }, 1000); return; } // Remove animation after it completes setTimeout(() => { input.style.animation = ''; }, 1000); return; } // Find the next available domain slot (only if highest with content is filled) const usedNumbers = domainData.map(d => d.num); let nextDomain = 1; while (usedNumbers.includes(nextDomain) && nextDomain <= 9) { nextDomain++; } // Only add if we have domains with content and the highest with content is filled if (highestDomainWithContent === 0) { // No domains with content yet, this shouldn't happen but handle it } else if (nextDomain > 9) { if (window.notificationSystem) { window.notificationSystem.warning('Maximum of 9 domains reached!'); } return; } // Before adding new domain, validate that all existing domains have valid format if (!this.canAddNewDomain()) { // Find the first invalid domain and focus it with flash const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); for (const input of allDomainInputs) { if (!this.validateDomainFormat(input, false)) { // Don't show notification here input.style.animation = 'flash 0.5s ease-in-out 2'; input.focus(); setTimeout(() => { input.style.animation = ''; }, 1000); return; // Just flash and focus, no extra notification } } } // Create new domain field with proper structure const domainKey = `CFG_DOMAIN_${nextDomain}`; const fieldId = `config-${domainKey}`; const title = `Domain ${nextDomain}`; const newDomainHTML = `
${ConfigShared.generateField(fieldId, domainKey, '', title, '', { placeholder: 'example.com', className: 'domain-input', onchange: 'window.configManager.validateDomainFormat(this, true)', oninput: 'window.configManager.validateDomainFormat(this, true)', onblur: 'window.configManager.validateDomainFormat(this, true)' })}
`; // Insert inside the domain-building-blocks container before the domain-actions const domainBlocks = button.closest('.domains-wrapper').querySelector('.domain-building-blocks'); domainBlocks.insertAdjacentHTML('beforeend', newDomainHTML); // Update all delete button states after DOM is ready setTimeout(() => this.updateDomainDeleteButtons(), 10); // Update button state if we're now at max domains const totalDomains = document.querySelectorAll('[id^="config-CFG_DOMAIN_"]').length; if (totalDomains >= 9) { button.classList.add('disabled'); button.disabled = true; const iconSpan = button.querySelector('.add-icon'); const textSpan = button.querySelector('.add-text'); iconSpan.textContent = '✓'; textSpan.textContent = 'Maximum Domains Reached'; } } // Delete a domain field deleteDomain(domainKey, button) { const domainBlock = button.closest('.domain-building-block'); // Clear the domain value const input = document.getElementById(`config-${domainKey}`); if (input) { input.value = ''; } // Remove the domain block if it's empty if (!input || input.value === '') { domainBlock.remove(); } // Update all delete button states after DOM is ready setTimeout(() => this.updateDomainDeleteButtons(), 10); // Update add button state const addButton = document.querySelector('.add-domain-btn'); if (addButton) { const totalDomains = document.querySelectorAll('[id^="config-CFG_DOMAIN_"]').length; const domainsWithContent = Array.from(document.querySelectorAll('[id^="config-CFG_DOMAIN_"]')) .filter(input => input.value.trim() !== '').length; if (totalDomains < 9) { addButton.classList.remove('disabled'); addButton.disabled = false; const iconSpan = addButton.querySelector('.add-icon'); const textSpan = addButton.querySelector('.add-text'); iconSpan.textContent = '+'; textSpan.textContent = 'Add Domain'; } else { addButton.classList.add('disabled'); addButton.disabled = true; const iconSpan = addButton.querySelector('.add-icon'); const textSpan = addButton.querySelector('.add-text'); iconSpan.textContent = '✓'; textSpan.textContent = 'Maximum Domains Reached'; } } } // Helper method to render subcategory with proper sectioning, dividers and headers renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription, config = {}) { //console.log(`renderSubcategorySection: subcategory=${subcategoryDescription}, configKeys=${Object.keys(config)}, CFG_INSTALL_MODE=${config.CFG_INSTALL_MODE?.value}`); const cleanDescription = this.cleanDescription(subcategoryDescription); let html = `

${displaySubcategory}

${cleanDescription}

`; // Add all config items using standard layout configItems.forEach((item, index) => { const fieldId = `config-${item.key}`; const cleanItemDescription = this.cleanDescription(item.description || ''); html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, cleanItemDescription, item.options); }); html += `
`; return html; } // Helper method to render regular subcategory renderRegularSubcategory(configItems, displaySubcategory, subcategoryDescription, config = {}) { let html = `

${displaySubcategory}

${subcategoryDescription}

`; configItems.forEach(item => { const fieldId = `config-${item.key}`; html += ConfigShared.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config); }); html += `
`; return html; } } // Global instance window.configManager = new ConfigManager();