`;
// 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 += `
`;
});
// 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 += `