// Config Shared Functions - Common functionality used across all config components class ConfigShared { // Toggle switch system - handles different types of toggles with proper layout static createToggleSwitch(fieldId, key, value, title, description, options = {}) { const isChecked = value === 'true'; const tooltipHtml = description ? `â„šī¸` : ''; // Determine toggle type and layout const toggleType = options.type || 'standard'; const layout = options.layout || 'inline'; let toggleHTML = ''; switch (toggleType) { case 'master': toggleHTML = this.createMasterToggle(fieldId, key, title, isChecked, tooltipHtml, options); break; case 'section': toggleHTML = this.createSectionToggle(fieldId, key, title, isChecked, tooltipHtml, options); break; case 'standard': default: toggleHTML = this.createStandardToggle(fieldId, key, title, isChecked, tooltipHtml, options); break; } // Wrap in layout container return this.wrapToggleInLayout(toggleHTML, layout, options); } // Standard toggle switch (most common) static createStandardToggle(fieldId, key, title, isChecked, tooltipHtml, options) { return `
`; } // Master toggle (for enabling/disabling entire sections) static createMasterToggle(fieldId, key, title, isChecked, tooltipHtml, options) { return `
`; } // Section toggle (for showing/hiding sections) static createSectionToggle(fieldId, key, title, isChecked, tooltipHtml, options) { return `
`; } // Wrap toggle in layout container static wrapToggleInLayout(toggleHTML, layout, options) { switch (layout) { case 'grid': return `
${toggleHTML}
`; case 'flex': return `
${toggleHTML}
`; case 'inline': default: return toggleHTML; } } // Auto-detect and create appropriate field type static createSmartField(fieldId, key, value, title, description, options = {}) { //console.log(`createSmartField: key=${key}, value=${value}, config=${!!options.config}`); // Check if value is boolean (true/false strings) const isBoolean = value === 'true' || value === 'false'; if (isBoolean) { // Auto-create toggle switch for boolean values return this.createToggleSwitch(fieldId, key, value, title, description, { type: options.type || 'standard', layout: options.layout || 'inline', category: options.category || '', sectionId: options.sectionId || '', fieldIds: options.fieldIds || [] }); } // Fall back to regular field generation for non-boolean values return this.generateField(fieldId, key, value, title, description, options.selectOptions, options.config); } // Handle standard toggle changes static handleToggleChange(checkbox, key, category) { //console.log(`Toggle changed: ${key} = ${checkbox.checked} (category: ${category})`); // Trigger custom event for other components to listen to const event = new CustomEvent('configToggleChanged', { detail: { key, value: checkbox.checked, category } }); document.dispatchEvent(event); // Auto-save if enabled if (checkbox.dataset.autoSave === 'true') { this.saveToggleValue(key, checkbox.checked); } } // Handle master toggle changes (enables/disables multiple fields) static handleMasterToggle(checkbox, sectionId, fieldIds) { //console.log(`Master toggle changed: ${sectionId} = ${checkbox.checked}`); // Enable/disable related fields if (fieldIds && fieldIds.length > 0) { fieldIds.forEach(fieldId => { const field = document.getElementById(`config-${fieldId}`); if (field) { field.disabled = !checkbox.checked; field.closest('.config-field')?.classList.toggle('disabled', !checkbox.checked); } }); } // Show/hide section content if (sectionId) { const sectionContent = document.getElementById(`${sectionId}-content`); if (sectionContent) { sectionContent.style.display = checkbox.checked ? 'block' : 'none'; } } // Trigger custom event const event = new CustomEvent('configMasterToggleChanged', { detail: { sectionId, enabled: checkbox.checked, fieldIds } }); document.dispatchEvent(event); } // Handle section toggle changes (shows/hides sections) static handleSectionToggle(checkbox, sectionId) { //console.log(`Section toggle changed: ${sectionId} = ${checkbox.checked}`); const content = document.getElementById(`${sectionId}-content`); if (content) { content.style.display = checkbox.checked ? 'block' : 'none'; } // Trigger custom event const event = new CustomEvent('configSectionToggleChanged', { detail: { sectionId, visible: checkbox.checked } }); document.dispatchEvent(event); } // Save toggle value immediately static async saveToggleValue(key, value) { try { // For now, just log the change - local implementation //console.log('Toggle value changed:', key, value ? 'true' : 'false'); // TODO: Implement local config file update // This would require backend integration to write to actual config files } catch (error) { console.error('Error saving toggle value:', error); } } // Create custom range field with start-end inputs static createRangeField(fieldId, key, value, title, description) { // Parse range value (format: "start-end") let startRange = ''; let endRange = ''; if (value && value.includes('-')) { const parts = value.split('-'); startRange = parts[0] || ''; endRange = parts[1] || ''; } return `
-
`; } // Update range value when inputs change static updateRangeValue(key) { const startInput = document.getElementById(`config-${key}-start`); const endInput = document.getElementById(`config-${key}-end`); const hiddenInput = document.getElementById(`config-${key}`); if (startInput && endInput && hiddenInput) { const startValue = startInput.value || ''; const endValue = endInput.value || ''; if (startValue && endValue) { hiddenInput.value = `${startValue}-${endValue}`; } else if (startValue) { hiddenInput.value = `${startValue}-`; } else if (endValue) { hiddenInput.value = `-${endValue}`; } else { hiddenInput.value = ''; } } } // Create custom crontab field with hour/minutes/AM-PM static createCrontabField(fieldId, key, value, title, description) { // Parse crontab value (format: "minute hour * * *") const parts = value.split(' '); let hour = '5'; let minute = '0'; let period = 'AM'; if (parts.length >= 2) { minute = parts[0] || '0'; const cronHour = parseInt(parts[1]) || 0; // Convert 24-hour to 12-hour format if (cronHour === 0) { hour = '12'; period = 'AM'; } else if (cronHour < 12) { hour = cronHour.toString(); period = 'AM'; } else if (cronHour === 12) { hour = '12'; period = 'PM'; } else { hour = (cronHour - 12).toString(); period = 'PM'; } } return `
: /
`; } // Update crontab value when fields change static updateCrontabValue(key) { const fieldId = `config-${key}`; const hourSelect = document.getElementById(`${fieldId}-hour`); const minuteSelect = document.getElementById(`${fieldId}-minute`); const periodSelect = document.getElementById(`${fieldId}-period`); const hiddenInput = document.getElementById(fieldId); if (!hourSelect || !minuteSelect || !periodSelect || !hiddenInput) { console.warn(`Crontab field elements not found for: ${key}`); return; } const hour = parseInt(hourSelect.value); const minute = minuteSelect.value; const period = periodSelect.value; // Convert 12-hour to 24-hour format let cronHour; if (period === 'AM') { cronHour = hour === 12 ? 0 : hour; } else { cronHour = hour === 12 ? 12 : hour + 12; } // Create crontab format: "minute hour * * *" const crontabValue = `${minute} ${cronHour} * * *`; hiddenInput.value = crontabValue; //console.log(`Updated crontab for ${key}: ${crontabValue}`); } // Toggle password visibility static togglePasswordVisibility(fieldId) { const passwordField = document.getElementById(fieldId); const icon = document.getElementById(`${fieldId}-icon`); if (!passwordField || !icon) { console.warn(`Password field or icon not found: ${fieldId}`); return; } if (passwordField.type === 'password') { passwordField.type = 'text'; icon.textContent = '👁‍🗨'; // Eye with strikethrough (hidden) } else { passwordField.type = 'password'; icon.textContent = '👁'; // Regular eye (visible) } } static setPasswordMode(fieldId, mode) { const wrapper = document.querySelector(`.password-mode-wrapper[data-field-id="${fieldId}"]`); const input = document.getElementById(fieldId); const tokenInput = document.getElementById(`${fieldId}-token`); if (!wrapper || !input || !tokenInput) return; const key = wrapper.dataset.fieldKey; if (mode === 'random') { input.dataset.previousCustom = input.value || ''; input.value = ''; input.readOnly = true; input.type = 'password'; input.setAttribute('placeholder', 'Will generate on save'); input.removeAttribute('name'); tokenInput.setAttribute('name', key); const icon = document.getElementById(`${fieldId}-icon`); if (icon) icon.textContent = '👁'; } else { input.readOnly = false; input.removeAttribute('placeholder'); input.value = input.dataset.previousCustom || ''; input.setAttribute('name', key); tokenInput.removeAttribute('name'); input.focus(); } } // Format config key to readable label static formatConfigLabel(key) { // Special handling for domain configuration if (key.startsWith('CFG_DOMAIN_')) { const domainNum = key.replace('CFG_DOMAIN_', ''); return `Domain ${domainNum}`; } // Special handling for requirement configuration - remove REQUIREMENT and format nicely if (key.startsWith('CFG_REQUIREMENT_')) { const requirement = key.replace('CFG_REQUIREMENT_', ''); return requirement .replace(/_/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); } return key .replace(/^CFG_/, '') .replace(/_/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); } // Group config keys by category static groupConfigKeys(config) { const groups = {}; // Group by category field from JSON config for (const key of Object.keys(config)) { const configItem = config[key] || {}; const category = configItem.category; if (!groups[category]) { groups[category] = []; } groups[category].push(key); } // Pin any per-app `*_BACKUP` toggle to the top of its category so the // user always sees it first when configuring an app. Matches both // `CFG_BACKUP` and `CFG__BACKUP` (but not `*_BACKUP_*` like // `CFG_BACKUP_CRONTAB_APP`, which would otherwise bubble up too). for (const cat of Object.keys(groups)) { groups[cat].sort((a, b) => { const aBackup = /^CFG_(?:[A-Z0-9]+_)?BACKUP$/.test(a); const bBackup = /^CFG_(?:[A-Z0-9]+_)?BACKUP$/.test(b); if (aBackup && !bBackup) return -1; if (!aBackup && bBackup) return 1; return 0; }); } return groups; } // Extract category order from config static extractCategoryOrder(config) { const order = []; const seen = new Set(); // Get categories in the order they appear in config for (const key of Object.keys(config)) { const configItem = config[key] || {}; const category = configItem.category; if (category && !seen.has(category)) { seen.add(category); order.push(category); } } return order; } // Format category name for display static formatCategoryName(category) { return category .replace(/_/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); } /* Subcategory titles often come back as " " — e.g. "General Basic" under the General category, "Webui Logins" under the Webui category. The redundant leading category word is noisy when the user is already viewing that category page, so strip it. Comparison is case-insensitive and only strips a clean leading whole-word match (won't turn "Configuration" into "iguration"). */ static stripCategoryPrefix(title, parentCategory) { if (!title || !parentCategory) return title; const prefix = ConfigShared.formatCategoryName(parentCategory) + ' '; if (title.toLowerCase().startsWith(prefix.toLowerCase())) { return title.substring(prefix.length); } return title; } // Get category description from config or fallback static async getCategoryDescription(category) { try { // First try to get description from the unified config const response = await fetch('/data/config/generated/configs.json'); const data = await response.json(); // Check if categories exist in this config if (data.categories) { // Categories are stored as simple key-value pairs: "CATEGORY": "description" const categoryDescription = data.categories[category]; if (categoryDescription) { return categoryDescription; } } // Fallback to unified config for general descriptions const unifiedConfigResponse = await fetch('/data/config/generated/configs.json'); const unifiedConfigData = await unifiedConfigResponse.json(); const fallbackCategoryData = unifiedConfigData.categories[category] || null; return fallbackCategoryData ? fallbackCategoryData.description : `${this.formatCategoryName(category)} settings and configuration`; } catch (error) { console.error('Error loading category descriptions:', error); return `${this.formatCategoryName(category)} settings and configuration`; } } // Generate appropriate field based on value type and key static generateField(fieldId, key, value, title, description, options = {}, allConfig = {}) { //console.log(`generateField: key=${key}, value=${value}, CFG_INSTALL_MODE=${allConfig.CFG_INSTALL_MODE?.value}`); // Note: Git fields (CFG_GIT_*) are now handled by the toggle system in renderGitSection // They don't need to be hidden here since the section itself is toggled // Handle boolean toggle fields from options const { onchange, oninput, onblur, className, placeholder, ...fieldOptions } = options; // Check if value is boolean (true/false strings) const isBoolean = value === 'true' || value === 'false'; if (isBoolean) { // Auto-create toggle switch for boolean values return this.createToggleSwitch(fieldId, key, value, title, description, { type: 'standard', layout: 'inline', category: '' }); } // Non-boolean fields - use exact old config structure const tooltipHtml = description ? `â„šī¸` : ''; let fieldHTML = `
`; // Determine field type based on key or options ////console.log(`Checking field type for key: ${key}`); // Debug log ////console.log(`Password check: ${key.includes('PASS') && !key.includes('LENGTH')}, ${key.includes('SECRET')}`); // Debug log // Custom crontab fields for backup schedules if (key === 'CFG_BACKUP_CRONTAB_APP') { fieldHTML += this.createCrontabField(fieldId, key, value, title, description); } else if (key.includes('PORT_RANGE')) { fieldHTML += this.createRangeField(fieldId, key, value, title, description); } else if ((key.includes('PASS') && !key.includes('LENGTH')) || key.includes('SECRET') || key.endsWith('_API_KEY') || key.endsWith('_PRIVATE_KEY')) { ////console.log(`Creating password field for: ${key}`); // Debug log const randomMatch = typeof value === 'string' && /^RANDOMIZEDPASSWORD\d+$/.test(value); const placeholderToken = randomMatch ? value : `RANDOMIZEDPASSWORD1`; const initialMode = randomMatch ? 'random' : 'custom'; const inputValue = randomMatch ? '' : value; const visibleName = initialMode === 'custom' ? `name="${key}"` : ''; const hiddenName = initialMode === 'random' ? `name="${key}"` : ''; fieldHTML += `
`; } else if (key.includes('EMAIL') || key.includes('MAIL')) { // Special handling for CFG_MAIL_SECURE - it's a dropdown if (key === 'CFG_MAIL_SECURE') { const selectOptions = ConfigOptions.getSelectOptions(key); fieldHTML += ``; } else { // Different validation for different mail fields let validationAttrs = ''; let fieldType = 'email'; let placeholder = 'user@example.com'; if (key === 'CFG_MAIL_HOST') { fieldType = 'text'; placeholder = 'mail.domain.com'; validationAttrs = `onchange="window.configManager.validateHostnameFormat(this, true)" oninput="window.configManager.validateHostnameFormat(this, true)" onblur="window.configManager.validateHostnameFormat(this, true)"`; } else if (key === 'CFG_MAIL_PORT') { fieldType = 'number'; placeholder = '587'; validationAttrs = `onchange="window.configManager.validatePortNumber(this, true)" oninput="window.configManager.validatePortNumber(this, true)" onblur="window.configManager.validatePortNumber(this, true)"`; } else if (key === 'CFG_MAIL_USERNAME' || key === 'CFG_MAIL_FROM') { validationAttrs = `onchange="window.configManager.validateEmailFormat(this, true)" oninput="window.configManager.validateEmailFormat(this, true)" onblur="window.configManager.validateEmailFormat(this, true)"`; } else { validationAttrs = `onchange="window.configManager.validateEmailFormat(this, true)" oninput="window.configManager.validateEmailFormat(this, true)" onblur="window.configManager.validateEmailFormat(this, true)"`; } fieldHTML += ` `; } } else if (key.includes('URL') || key.includes('LINK') || key.includes('HOST') || key.startsWith('CFG_DOMAIN_')) { const placeholder = key.startsWith('CFG_DOMAIN_') ? 'example.com' : 'https://example.com'; fieldHTML += ` `; } else if (/^CFG_BACKUP(_LOC_[0-9]+)?_KEEP_(LAST|DAILY|WEEKLY|MONTHLY|YEARLY)$/.test(key)) { const unitLabel = key.endsWith('_KEEP_LAST') ? 'snapshots' : key.endsWith('_KEEP_DAILY') ? 'days' : key.endsWith('_KEEP_WEEKLY') ? 'weeks' : key.endsWith('_KEEP_MONTHLY') ? 'months' : 'years'; fieldHTML += `
${unitLabel}
`; } else if (key === 'CFG_BACKUP_VERIFY_DATA_PERCENT') { fieldHTML += `
%
`; } else if (key === 'CFG_UPDATER_CHECK') { fieldHTML += `
minutes
`; } else if (key === 'CFG_GENERATED_PASS_LENGTH') { fieldHTML += `
characters
`; } else if (key === 'CFG_WEBUI_LOG_STREAM_IDLE_TIMEOUT_MINUTES') { fieldHTML += `
minutes
`; } else if (key === 'CFG_WEBUI_LOG_STREAM_MAX_DURATION_MINUTES') { fieldHTML += `
minutes
`; } else if (key === 'CFG_WEBUI_LOG_STREAM_MAX_LINES_PER_SEC') { fieldHTML += `
lines
`; } else if (key === 'CFG_SWAPFILE_SIZE') { // Extract numeric value from "2G" format let numericValue = value.replace(/[^0-9.]/g, ''); fieldHTML += `
GB
`; } else if (key.includes('SIZE') || key.includes('LENGTH') || key.includes('CHECK') || key.includes('MTU') || key.includes('PORT')) { let min = ''; let max = ''; if (key.includes('PORT')) { min = '1'; max = '65535'; } else if (key.includes('SIZE') || key.includes('LENGTH')) { min = '0'; max = key.includes('PASS_LENGTH') ? '128' : ''; } fieldHTML += ` `; } else if (key.includes('TIMEZONE')) { // Special handling for Timezone - create comprehensive timezone dropdown const timezoneOptions = ConfigOptions.getTimezoneOptions(); //console.log('Timezone key:', key, 'Current value:', value, 'Type:', typeof value); //console.log('Available timezone options:', timezoneOptions.map(opt => ({value: opt.value, label: opt.label}))); fieldHTML += ` `; //console.log('Generated timezone dropdown HTML for', key, 'with value', value); } else if (key === 'CFG_INSTALL_MODE') { const selectOptions = ConfigOptions.getSelectOptions(key); fieldHTML += ``; } else if (/^CFG_BACKUP_LOC_[0-9]+_TYPE$/.test(key)) { const selectOptions = ConfigOptions.getSelectOptions(key); fieldHTML += ``; } else if (ConfigOptions.isDropdownKey(key) || (options && Object.keys(options).length > 0)) { //console.log('=== GENERIC DROPDOWN BLOCK ENTERED for key:', key); //console.log('Dropdown detected for key:', key); //console.log('isDropdownKey result:', ConfigOptions.isDropdownKey(key)); //console.log('options available:', options); const selectOptions = (options && typeof options === 'string') ? this.parseOptions(options) : ConfigOptions.getSelectOptions(key); //console.log('selectOptions:', selectOptions); fieldHTML += ` `; //console.log('Generated dropdown for', key, 'with value', value); } else if (key.includes('DESCRIPTION') || key.includes('COMMENTS') || key.includes('NOTES')) { //console.log('Textarea detected for key:', key); fieldHTML += ` `; } else { //console.log('Default text input for key:', key); // Default text input with event handlers and options const inputClass = className ? `form-control ${className}` : 'form-control'; const inputPlaceholder = placeholder || ''; const eventHandlers = []; if (onchange) eventHandlers.push(`onchange="${onchange}"`); if (oninput) eventHandlers.push(`oninput="${oninput}"`); if (onblur) eventHandlers.push(`onblur="${onblur}"`); const eventAttrs = eventHandlers.length > 0 ? ` ${eventHandlers.join(' ')}` : ''; fieldHTML += ` `; } fieldHTML += `
`; return fieldHTML; } // Create master toggle for section enabling/disabling static createMasterToggle(sectionId, masterKey, isEnabled, title, description) { return `
`; } // Create section content wrapper static createSectionContent(sectionId, isEnabled) { return `
`; } // Toggle section fields (modular function) static toggleSection(sectionId, isEnabled) { const sectionContent = document.getElementById(`section-content-${sectionId}`); if (!sectionContent) { console.warn(`Section content not found: ${sectionId}`); return; } const fields = sectionContent.querySelectorAll('input, select, textarea'); if (isEnabled) { // Enable section sectionContent.classList.remove('disabled'); fields.forEach(field => { field.disabled = false; const fieldGroup = field.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '1'; fieldGroup.style.pointerEvents = 'auto'; } }); //console.log(`Section ${sectionId} enabled`); } else { // Disable section sectionContent.classList.add('disabled'); fields.forEach(field => { field.disabled = true; const fieldGroup = field.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '0.6'; fieldGroup.style.pointerEvents = 'none'; } }); //console.log(`Section ${sectionId} disabled`); } } // Toggle section visibility (hide/show entire section) static toggleSectionVisibility(sectionId, isEnabled) { const sectionContent = document.getElementById(sectionId); if (!sectionContent) { console.warn(`Section content not found: ${sectionId}`); return; } if (isEnabled) { // Show section sectionContent.classList.remove('hidden'); //console.log(`Section ${sectionId} shown`); } else { // Hide section sectionContent.classList.add('hidden'); //console.log(`Section ${sectionId} hidden`); } } // Initialize section toggles on page load static initializeSectionToggles() { // Find all master toggles and initialize their sections document.querySelectorAll('[id^="config-CFG_"][onchange*="toggleSection"]').forEach(toggle => { // Extract section ID from the onchange attribute const onchangeAttr = toggle.getAttribute('onchange'); const match = onchangeAttr.match(/toggleSection\('([^']+)'/); if (match) { const sectionId = match[1]; const isEnabled = toggle.checked; this.toggleSection(sectionId, isEnabled); } }); } // Git section toggle function (moved from global scope) static toggleGitSectionFields(isEnabled) { // If no parameter provided, get the state from the checkbox if (typeof isEnabled === 'undefined') { const gitLoginCheckbox = document.getElementById('git-login-toggle'); isEnabled = gitLoginCheckbox ? gitLoginCheckbox.checked : false; } const gitSectionContent = document.getElementById('git-section-content'); const gitFields = gitSectionContent?.querySelectorAll('.config-fields input, .config-fields select, .config-fields textarea'); if (gitSectionContent && gitFields) { if (isEnabled) { gitSectionContent.classList.remove('hidden'); gitFields.forEach(field => { field.disabled = false; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '1'; fieldGroup.style.pointerEvents = 'auto'; } }); } else { gitSectionContent.classList.add('hidden'); gitFields.forEach(field => { field.disabled = true; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '0.5'; fieldGroup.style.pointerEvents = 'none'; } }); } } // Force re-initialization of form to ensure proper state setTimeout(() => { const form = document.getElementById('config-form'); if (form) { // Trigger a change event to update any dependent fields const event = new Event('change', { bubbles: true }); gitSectionContent?.dispatchEvent(event); } }, 100); } // Universal toggle function for all _ENABLED options static toggleSection(sectionId, isEnabled) { //console.log('=== UNIVERSAL TOGGLE DEBUG ==='); //console.log('sectionId:', sectionId); //console.log('isEnabled:', isEnabled); const sectionContent = document.getElementById(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('=== UNIVERSAL TOGGLE DEBUG END ==='); } // Remote backup section toggle function static toggleMailSection(sectionId, isEnabled) { //console.log('=== TOGGLE MAIL SECTION DEBUG ==='); //console.log('sectionId:', sectionId); //console.log('isEnabled:', isEnabled); alert('toggleMailSection called: ' + sectionId + ', enabled: ' + isEnabled); //console.log('Looking for sectionContent...'); const sectionContent = document.getElementById(sectionId); //console.log('sectionContent found:', !!sectionContent); //console.log('Looking for mailFields...'); const mailFields = sectionContent?.querySelectorAll('.config-fields input, .config-fields select, .config-fields textarea'); //console.log('mailFields found:', mailFields ? mailFields.length : 0); if (sectionContent && mailFields) { //console.log('Enabling mail section...'); if (isEnabled) { //console.log('Removing hidden class...'); sectionContent.classList.remove('hidden'); //console.log('Enabling fields...'); mailFields.forEach((field, index) => { //console.log(`Disabling field ${index}:`, field); field.disabled = false; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { //console.log('Enabling field group...'); fieldGroup.style.opacity = '1'; fieldGroup.style.pointerEvents = 'auto'; } }); } else { //console.log('Disabling mail section...'); sectionContent.classList.add('hidden'); //console.log('Disabling fields...'); mailFields.forEach((field, index) => { //console.log(`Enabling field ${index}:`, field); field.disabled = true; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { //console.log('Disabling field group...'); fieldGroup.style.opacity = '0.5'; fieldGroup.style.pointerEvents = 'none'; } }); } } //console.log('=== TOGGLE MAIL SECTION DEBUG END ==='); } static toggleRemoteBackupSection(sectionId, isEnabled) { const sectionContent = document.getElementById(sectionId); const backupFields = sectionContent?.querySelectorAll('.config-fields input, .config-fields select, .config-fields textarea'); if (sectionContent && backupFields) { if (isEnabled) { sectionContent.classList.remove('hidden'); backupFields.forEach(field => { field.disabled = false; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '1'; fieldGroup.style.pointerEvents = 'auto'; } }); } else { sectionContent.classList.add('hidden'); backupFields.forEach(field => { field.disabled = true; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '0.5'; fieldGroup.style.pointerEvents = 'none'; } }); } } // Force re-initialization of form to ensure proper state setTimeout(() => { const form = document.getElementById('config-form'); if (form) { // Trigger a change event to update any dependent fields const event = new Event('change', { bubbles: true }); sectionContent?.dispatchEvent(event); } }, 100); } // Git section toggle function static toggleGitSection(sectionId, isEnabled) { const sectionContent = document.getElementById(sectionId); const gitFields = sectionContent?.querySelectorAll('.config-fields input, .config-fields select, .config-fields textarea'); if (sectionContent && gitFields) { if (isEnabled) { sectionContent.classList.remove('hidden'); gitFields.forEach(field => { field.disabled = false; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '1'; fieldGroup.style.pointerEvents = 'auto'; } }); } else { sectionContent.classList.add('hidden'); gitFields.forEach(field => { field.disabled = true; const fieldGroup = field?.closest('.field-group'); if (fieldGroup) { fieldGroup.style.opacity = '0.5'; fieldGroup.style.pointerEvents = 'none'; } }); } } // Force re-initialization of form to ensure proper state setTimeout(() => { const form = document.getElementById('config-form'); if (form) { // Trigger a change event to update any dependent fields const event = new Event('change', { bubbles: true }); sectionContent?.dispatchEvent(event); } }, 100); } // Parse options string into array of {value, label} objects static parseOptions(options) { if (!options || typeof options !== 'string') { return []; } return options.split('|').map(opt => { const parts = opt.split('='); if (parts.length === 2) { return { value: parts[0].trim(), label: parts[1].trim() }; } return { value: opt.trim(), label: opt.trim() }; }); } // Generate fields for category with 3-per-line layout and smart field detection static generateFieldsForCategory(keys, category, config, generateFieldCallback = null) { let formHTML = '
'; keys.forEach((key, index) => { const configItem = config[key] || {}; const value = configItem.value || ''; const title = configItem.title || this.formatConfigLabel(key); const description = configItem.description || ''; const options = configItem.options || ''; const fieldId = `config-${key}`; // Add line break every 3 items if (index > 0 && index % 3 === 0) { formHTML += `
`; } // Use smart field creation if no callback provided, otherwise use callback if (generateFieldCallback) { formHTML += generateFieldCallback(fieldId, key, value, title, description, options, config); } else { formHTML += this.createSmartField(fieldId, key, value, title, description, { selectOptions: options, category: category, layout: 'inline', config: config }); } }); formHTML += `
`; return formHTML; } // Generate fields for category WITHOUT the leading divider (for master toggle sections) static generateFieldsForCategoryNoDivider(keys, category, config, generateFieldCallback = null) { let formHTML = ''; keys.forEach((key, index) => { const configItem = config[key] || {}; const value = configItem.value || ''; const title = configItem.title || this.formatConfigLabel(key); const description = configItem.description || ''; const options = configItem.options || ''; const fieldId = `config-${key}`; let fieldHTML; if (generateFieldCallback) { fieldHTML = generateFieldCallback(fieldId, key, value, title, description, options, config); } else { fieldHTML = this.generateField(fieldId, key, value, title, description, options, config); } formHTML += fieldHTML; }); return formHTML; } // Separate categories into regular, advanced, and unused static categorizeConfigs(config) { const groupedConfigs = this.groupConfigKeys(config); const categoryOrder = this.extractCategoryOrder(config); const regularCategories = []; const advancedCategories = []; const unusedCategories = []; for (const category of categoryOrder) { const keys = groupedConfigs[category]; if (keys && keys.length > 0 && category !== 'Hidden/Unused Options') { // Check if category has advanced items const hasAdvanced = keys.some(key => { const configItem = config[key] || {}; return configItem.advanced === true; }); // Check if category has unused items const hasUnused = keys.some(key => { const configItem = config[key] || {}; return configItem.unused === true; }); // Categorize based on the presence of advanced/unused flags if (hasUnused) { unusedCategories.push(category); } else if (hasAdvanced) { advancedCategories.push(category); } else { regularCategories.push(category); } } } return { groupedConfigs, categoryOrder, regularCategories, advancedCategories, unusedCategories }; } // Generate warning notice for requirements page static generateRequirementsWarning() { return `

âš ī¸ System Requirements Warning

Disabling any of the following system requirements may break LibrePortal functionality. Always create a backup before making changes.

`; } // Generate toggle controls HTML for advanced/unused sections static generateToggleControls(hasAdvanced = false, hasUnused = false) { if (!hasAdvanced && !hasUnused) { return ''; // Don't show danger zone if no content } let formHTML = `

âš ī¸ Danger Zone

These options are for advanced users and may affect system stability

`; if (hasAdvanced) { formHTML += `
`; } if (hasUnused) { formHTML += `
`; } formHTML += `
`; return formHTML; } // Generate advanced sections HTML static async generateAdvancedSections(advancedCategories, groupedConfigs, config, getCategoryDescriptionCallback) { if (advancedCategories.length === 0) { return ''; } let formHTML = ` `; return formHTML; } // Generate unused sections HTML static async generateUnusedSections(unusedCategories, groupedConfigs, config, getCategoryDescriptionCallback) { if (unusedCategories.length === 0) { return ''; } let formHTML = ` `; return formHTML; } // Toggle advanced sections visibility static toggleAdvancedSections() { const checkbox = document.getElementById('show-advanced'); const advancedSections = document.getElementById('advanced-sections'); if (advancedSections) { advancedSections.style.display = checkbox.checked ? 'block' : 'none'; } } // Toggle unused sections visibility static toggleUnusedSections() { const checkbox = document.getElementById('show-unused'); const unusedSections = document.getElementById('unused-sections'); if (unusedSections) { unusedSections.style.display = checkbox.checked ? 'block' : 'none'; } } } // Export for global access window.ConfigShared = ConfigShared; // Global toggle change function for checkbox handling window.handleToggleChange = function(checkbox, key) { //console.log(`Toggle changed: ${key} = ${checkbox.checked}`); // This function can be extended to handle specific toggle logic // For now, it just logs change }; // Global flag to prevent multiple config reloads window.isReloadingConfig = false; // Global function to handle CFG_INSTALL_MODE change window.handleInstallModeChange = function(selectElement) { // Prevent multiple simultaneous reloads if (window.isReloadingConfig) { //console.log('Config reload already in progress, ignoring...'); return; } const installMode = selectElement.value; //console.log(`Install mode changed to: ${installMode}`); // Set flag to prevent multiple reloads window.isReloadingConfig = true; // Use a longer delay and onchange instead of onblur to avoid the loop setTimeout(() => { if (window.configManager) { // Clear cache to ensure we get fresh data with updated CFG_INSTALL_MODE window.configManager.cache.clear(); //console.log('Cache cleared for fresh config data'); // Get the current category from the URL or default to 'general' const currentCategory = window.configCategory || 'general'; //console.log(`Reloading config category: ${currentCategory}`); window.configManager.renderConfig(currentCategory).finally(() => { // Clear the flag after reload is complete setTimeout(() => { window.isReloadingConfig = false; }, 500); // Extra delay to ensure DOM is fully settled }); } else if (window.configRouter) { // Fallback to configRouter if configManager is not available const currentCategory = window.configCategory || 'general'; //console.log(`Using configRouter to reload: ${currentCategory}`); window.configRouter.loadConfigComponentManual(currentCategory).finally(() => { // Clear the flag after reload is complete setTimeout(() => { window.isReloadingConfig = false; }, 500); // Extra delay to ensure DOM is fully settled }); } else { console.warn('Neither configManager nor configRouter available for install mode change'); window.isReloadingConfig = false; } }, 500); // Longer delay to avoid conflicts }; // Global function to initialize git field visibility based on current CFG_INSTALL_MODE window.initializeGitFieldVisibility = function() { const installModeSelect = document.querySelector('select[name="CFG_INSTALL_MODE"]'); if (installModeSelect) { // Trigger the change handler to set initial visibility handleInstallModeChange(installModeSelect); } }; // Global password toggle function for onclick handlers window.togglePasswordVisibility = function(fieldId) { ConfigShared.togglePasswordVisibility(fieldId); };