// Domain Management - Handles domain-related functionality class DomainManager { constructor() { this.domains = []; } async checkTraefikInstallation() { try { const response = await fetch('/api/traefik/status'); if (response.ok) { const data = await response.json(); return data.installed || false; } return false; } catch (error) { //console.log('DomainManager: Traefik status check failed, assuming not installed:', error.message); return false; } } async renderDomainsSection(configItems, displaySubcategory, subcategoryDescription) { const traefikStatus = await this.checkTraefikInstallation(); let html = `

${displaySubcategory}

${subcategoryDescription}

`; if (!traefikStatus.installed) { 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.map(item => item.key).filter(key => key.startsWith('CFG_DOMAIN_')); const domainKeysWithContent = allDomainKeys.filter(key => { const configItem = configItems.find(item => item.key === 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 = configItems.find(item => item.key === key) || {}; const value = configItem.value || ''; const title = configItem.title || this.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; html += `
${this.generateField(fieldId, key, value, title, '', { placeholder: 'example.com', className: 'domain-input' })}
`; }); setTimeout(() => this.attachDnsChecks(), 0); html += `
`; return html; } attachDnsChecks() { const inputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); inputs.forEach((input) => { if (input.dataset.dnsBound === '1') return; input.dataset.dnsBound = '1'; const status = document.querySelector(`[data-dns-for="${input.id}"]`); if (!status) return; let timer = null; let ctrl = null; const run = () => { if (timer) clearTimeout(timer); if (ctrl) ctrl.abort(); const domain = input.value.trim().toLowerCase(); if (!domain) { status.textContent = ''; status.className = 'setup-dns-status'; return; } if (!/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(domain)) { status.textContent = ''; status.className = 'setup-dns-status'; return; } status.textContent = 'Checking DNS…'; status.className = 'setup-dns-status checking'; timer = setTimeout(async () => { ctrl = new AbortController(); try { const res = await fetch(`/api/setup/dns-check?domain=${encodeURIComponent(domain)}`, { signal: ctrl.signal }); const data = await res.json(); if (data.matches) { status.textContent = `✓ ${domain} → ${data.server_ip} (this server)`; status.className = 'setup-dns-status ok'; } else { const detail = data.domain_ip ? `points to ${data.domain_ip}, this server is ${data.server_ip}` : `does not resolve (server: ${data.server_ip || 'unknown'})`; status.textContent = `⚠ ${domain} ${detail}. Traefik may not route this yet.`; status.className = 'setup-dns-status warn'; } } catch (err) { if (err.name !== 'AbortError') { status.textContent = 'DNS check failed.'; status.className = 'setup-dns-status warn'; } } }, 500); }; input.addEventListener('input', run); input.addEventListener('blur', run); if (input.value) run(); }); } generateField(fieldId, key, value, title, description, options = {}) { // Use the same field generation as ConfigShared if (typeof ConfigShared !== 'undefined') { return ConfigShared.generateField(fieldId, key, value, title, description, options); } // Fallback field generation const placeholder = key.startsWith('CFG_DOMAIN_') ? 'example.com' : 'https://example.com'; return ` `; } formatConfigLabel(key) { // Special handling for domain configuration if (key.startsWith('CFG_DOMAIN_')) { const domainNum = key.replace('CFG_DOMAIN_', ''); return `Domain ${domainNum}`; } // Use ConfigShared if available if (typeof ConfigShared !== 'undefined') { return ConfigShared.formatConfigLabel(key); } // Fallback formatting return key.replace(/^CFG_/, '').replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); } 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 (count only existing domains, not empty slots) 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 = `
${this.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 this.updateDomainDeleteButtons(); this.attachDnsChecks(); } } catch (error) { console.error('Error adding new domain:', error); } } deleteDomain(domainKey, buttonElement) { if (typeof window.DomainManager === 'undefined' || !window.DomainManager.deleteDomain) { console.error('DomainManager not available for deleteDomain'); return; } //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 delete button states this.updateDomainDeleteButtons(); } } catch (error) { console.error('Error deleting domain:', error); } } updateDomainDeleteButtons() { // Find all domain building blocks const allDomainBlocks = document.querySelectorAll('.domain-building-block'); // Find all domain numbers from existing blocks const allDomainNumbers = []; allDomainBlocks.forEach(block => { const input = block.querySelector('input'); if (input) { const inputName = input.name; const match = inputName.match(/CFG_DOMAIN_(\d+)/); if (match) { allDomainNumbers.push(parseInt(match[1])); } } }); // Update delete button states (only highest numbered domain can be deleted, but NEVER Domain 1) allDomainBlocks.forEach((block, index) => { const deleteBtn = block.querySelector('.delete-domain-btn'); if (deleteBtn) { const input = block.querySelector('input'); if (input) { const inputName = 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 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' : domainNumber === 1 ? 'Domain 1 cannot be deleted' : 'Can only delete highest numbered domain'; } } }); } } // Standalone domain management functions - immediately available window.addDomain = function() { //console.log('Add Domain button clicked!'); try { // Before adding new domain, validate that all existing domains have valid format if (!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 (!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; } } return; } // 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 = `
${typeof ConfigShared !== 'undefined' ? ConfigShared.generateField(newFieldId, newDomainKey, '', `Domain ${nextDomainNumber}`, '', { placeholder: 'example.com', className: 'domain-input', onchange: 'validateDomainFormat(this, true)', oninput: 'validateDomainFormat(this, true)', onblur: 'validateDomainFormat(this, true)' }) : ``}
`; // 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 updateDomainDeleteButtons(); updateAddDomainButton(); } } catch (error) { console.error('Error adding new domain:', error); } }; window.deleteDomain = function(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) updateDomainDeleteButtons(); updateAddDomainButton(); } } catch (error) { console.error('Error deleting domain:', error); } }; // Helper function to update delete button states function updateDomainDeleteButtons() { // Find all domain building blocks const allDomainBlocks = document.querySelectorAll('.domain-building-block'); // Find all domain numbers from existing blocks const allDomainNumbers = []; allDomainBlocks.forEach(block => { const input = block.querySelector('input'); if (input) { const inputName = input.name; const match = inputName.match(/CFG_DOMAIN_(\d+)/); if (match) { allDomainNumbers.push(parseInt(match[1])); } } }); // Update delete button states (only highest numbered domain can be deleted, but NEVER Domain 1) allDomainBlocks.forEach((block, index) => { const deleteBtn = block.querySelector('.delete-domain-btn'); if (deleteBtn) { const input = block.querySelector('input'); if (input) { const inputName = 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 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' : domainNumber === 1 ? 'Domain 1 cannot be deleted' : 'Can only delete highest numbered domain'; } } }); } // Helper function to update add domain button state function updateAddDomainButton() { const addDomainBtn = document.getElementById('add-domain-btn'); if (addDomainBtn) { // Find all domain building blocks const allDomainBlocks = document.querySelectorAll('.domain-building-block'); // Find all domain numbers from existing blocks const allDomainNumbers = []; allDomainBlocks.forEach(block => { const input = block.querySelector('input'); if (input) { const inputName = input.name; const match = inputName.match(/CFG_DOMAIN_(\d+)/); if (match) { allDomainNumbers.push(parseInt(match[1])); } } }); // Check if we've reached the maximum of 9 domains const isMaxDomains = allDomainNumbers.length >= 9; // Update button state addDomainBtn.disabled = isMaxDomains; addDomainBtn.className = `btn ${isMaxDomains ? 'btn-secondary' : 'btn-primary'}`; addDomainBtn.innerHTML = `${isMaxDomains ? '✓' : '+'}${isMaxDomains ? 'Maximum Domains Reached' : 'Add Domain'}`; } } // Validate domain format when user tries to add a new domain function validateDomainFormat(input, showNotifications = true) { const value = input.value.trim(); //console.log('validateDomainFormat called with:', value, 'showNotifications:', showNotifications); //console.log('window.notificationSystem available:', !!window.notificationSystem); if (!value) { // Clear styling for empty fields input.style.borderColor = '#dc3545'; input.title = 'Domain cannot be empty'; if (showNotifications && window.notificationSystem) { //console.log('Attempting to show empty domain notification'); window.notificationSystem.error('Domain cannot be empty'); } else { console.error('Domain cannot be empty - notification system not available'); } return false; // Empty fields are not valid for adding new domains } // 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 => otherInput !== input && otherInput.value.trim() === value ); const hasDuplicate = duplicates.length > 0; if (!isValidFormat) { input.style.borderColor = '#dc3545'; input.title = 'Invalid domain format (e.g., example.com)'; if (showNotifications && window.notificationSystem) { //console.log('Attempting to show invalid format notification'); window.notificationSystem.error('Invalid domain format: "' + value + '". Please use a valid domain like example.com'); } else { console.error('Invalid domain format - notification system not available'); } return false; } else if (hasDuplicate) { input.style.borderColor = '#dc3545'; input.title = 'Domain already exists'; if (showNotifications && window.notificationSystem) { //console.log('Attempting to show duplicate notification'); window.notificationSystem.error('Domain "' + value + '" already exists'); } else { console.error('Domain already exists - notification system not available'); } return false; } else { input.style.borderColor = ''; input.title = ''; return true; } } // Check if all domains are valid before allowing new domain addition function canAddNewDomain() { const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); for (const input of allDomainInputs) { const domainValue = input.value.trim(); // Check if domain is empty if (!domainValue) { // Flash the empty input field input.style.animation = 'flash 0.5s ease-in-out 2'; input.focus(); setTimeout(() => { input.style.animation = ''; }, 1000); // Show notification with safety check if (window.notificationSystem) { window.notificationSystem.error('Domain cannot be empty'); } else { console.error('Domain cannot be empty'); } return false; // Empty domain - don't allow adding new domain } // Check for duplicates with notification (always show) if (checkForDuplicateDomain(input, domainValue)) { return false; } // Check domain format with notification (always show) if (checkForInvalidDomainFormat(input, domainValue)) { return false; } } return true; // All domains are valid and non-empty } // Separate function to check for invalid format that always shows notifications function checkForInvalidDomainFormat(input, domainValue) { if (!domainValue) return false; // 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(domainValue); if (!isValidFormat) { input.style.borderColor = '#dc3545'; input.title = 'Invalid domain format (e.g., example.com)'; if (window.notificationSystem) { //console.log('Showing invalid domain format notification'); window.notificationSystem.error('Invalid domain format: "' + domainValue + '". Please use a valid domain like example.com'); } else { console.error('Invalid domain format - notification system not available'); } return true; } return false; } // Separate function to check for duplicates that always shows notifications function checkForDuplicateDomain(input, domainValue) { if (!domainValue) return false; const allDomainInputs = document.querySelectorAll('input[id^="config-CFG_DOMAIN_"]'); const duplicates = Array.from(allDomainInputs).filter(otherInput => otherInput !== input && otherInput.value.trim() === domainValue ); const hasDuplicate = duplicates.length > 0; if (hasDuplicate) { input.style.borderColor = '#dc3545'; input.title = 'Domain already exists'; if (window.notificationSystem) { //console.log('Showing duplicate domain notification'); window.notificationSystem.error('Domain "' + domainValue + '" already exists'); } else { console.error('Domain already exists - notification system not available'); } return true; } return false; }