// Local DNS UI functions
const LOCAL_DNS_FIELD = 'theme-input w-full';
function classifyDnsRecordType(type) {
const t = String(type || '').toUpperCase();
if (t === 'A' || t === 'AAAA') return { label: t, className: 'dns-type-badge--address' };
if (t === 'CNAME') return { label: t, className: 'dns-type-badge--cname' };
if (t === 'MX' || t === 'TXT') return { label: t, className: 'dns-type-badge--mail' };
if (t === 'SRV' || t === 'NS' || t === 'PTR') return { label: t, className: 'dns-type-badge--service' };
return { label: t || 'OTHER', className: 'dns-type-badge--muted' };
}
function formatLocalDnsValue(item) {
if (!item) return '';
if (item.type === 'MX') {
return `${item.preference || ''} ${item.exchange || ''}`.trim();
}
if (item.type === 'SRV') {
return `${item.priority || ''} ${item.weight || ''} ${item.port || ''} ${item.target || ''}`.trim();
}
if (item.type === 'SOA') {
return `${item.mname || ''} ${item.rname || ''} ${item.serial || ''} ${item.refresh || ''} ${item.retry || ''} ${item.expire || ''} ${item.minimum || ''}`.trim();
}
if (item.type === 'CAA') {
return `${item.flags || ''} ${item.tag || ''} ${item.value || ''}`.trim();
}
return item.data || item.value || '';
}
function computeLocalDnsStats(data) {
const records = Array.isArray(data) ? data : [];
let address = 0;
let cname = 0;
let other = 0;
for (const rec of records) {
const type = String(rec?.type || '').toUpperCase();
if (type === 'A' || type === 'AAAA') address += 1;
else if (type === 'CNAME') cname += 1;
else other += 1;
}
return { total: records.length, address, cname, other };
}
function setSubtabBadge(id, count) {
const el = document.getElementById(id);
if (!el) return;
if (count > 0) {
el.textContent = count.toLocaleString();
el.hidden = false;
} else {
el.hidden = true;
}
}
function updateLocalDnsSubtabBadges() {
setSubtabBadge('localDnsSubtabBadge', (window.localDnsData || []).length);
setSubtabBadge('dnsConflictsSubtabBadge', (window.dnsConflictsData || []).length);
setSubtabBadge('p2pConflictsSubtabBadge', (window.p2pDomainConflictsData || []).length);
}
function updateListChrome({ totalData, filteredData, searchId, emptyId, filteredEmptyId, tableSelector, endId, countId, countEmptyLabel, countSingular, countPlural }) {
const totalCount = (totalData || []).length;
const visibleCount = (filteredData || totalData || []).length;
const searchEl = document.getElementById(searchId);
const hasSearch = Boolean(searchEl?.value?.trim());
const trulyEmpty = totalCount === 0;
const filteredToZero = !trulyEmpty && visibleCount === 0 && hasSearch;
const emptyEl = document.getElementById(emptyId);
const filteredEmptyEl = document.getElementById(filteredEmptyId);
const endEl = document.getElementById(endId);
const tableEl = document.querySelector(tableSelector);
const countEl = document.getElementById(countId);
if (emptyEl) emptyEl.classList.toggle('local-dns-empty--visible', trulyEmpty);
if (filteredEmptyEl) filteredEmptyEl.classList.toggle('local-dns-empty--visible', filteredToZero);
if (tableEl) tableEl.classList.toggle('local-dns-table--hidden', trulyEmpty || filteredToZero);
if (endEl) endEl.classList.remove('local-dns-list-end--visible');
if (countEl) {
if (trulyEmpty) {
countEl.textContent = countEmptyLabel;
} else if (hasSearch && visibleCount !== totalCount) {
countEl.textContent = `${visibleCount.toLocaleString()} of ${totalCount.toLocaleString()} shown`;
} else {
const noun = totalCount === 1 ? countSingular : countPlural;
countEl.textContent = `${totalCount.toLocaleString()} ${noun}`;
}
}
}
function updateLocalDnsChrome(filteredCount) {
const visibleCount = filteredCount != null
? filteredCount
: (window.filteredLocalDns || window.localDnsData || []).length;
updateListChrome({
totalData: window.localDnsData,
filteredData: window.filteredLocalDns,
searchId: 'search-local-dns',
emptyId: 'localDnsEmpty',
filteredEmptyId: 'localDnsFilteredEmpty',
tableSelector: '.local-dns-table',
endId: 'localDnsEnd',
countId: 'localDnsCount',
countEmptyLabel: 'No local DNS records',
countSingular: 'record',
countPlural: 'records'
});
const stats = computeLocalDnsStats(window.localDnsData || []);
const setStat = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value.toLocaleString();
};
setStat('localDnsStatTotal', stats.total);
setStat('localDnsStatAddress', stats.address);
setStat('localDnsStatCname', stats.cname);
setStat('localDnsStatOther', stats.other);
const overviewEl = document.getElementById('localDnsOverviewLine');
if (overviewEl) {
overviewEl.textContent = stats.total === 0
? 'No overrides configured'
: `${stats.address} address ยท ${stats.cname} CNAME`;
}
updateLocalDnsSubtabBadges();
}
function updateDnsConflictsChrome(filteredCount) {
updateListChrome({
totalData: window.dnsConflictsData,
filteredData: window.filteredDnsConflicts,
searchId: 'search-dns-conflicts',
emptyId: 'dnsConflictsEmpty',
filteredEmptyId: 'dnsConflictsFilteredEmpty',
tableSelector: '.local-dns-conflicts-table',
endId: 'dnsConflictsEnd',
countId: 'dnsConflictsCount',
countEmptyLabel: 'No DNS conflicts',
countSingular: 'conflict',
countPlural: 'conflicts'
});
updateLocalDnsSubtabBadges();
}
function updateP2pConflictsChrome(filteredCount) {
updateListChrome({
totalData: window.p2pDomainConflictsData,
filteredData: window.filteredP2pDomainConflicts,
searchId: 'search-p2p-domain-conflicts',
emptyId: 'p2pConflictsEmpty',
filteredEmptyId: 'p2pConflictsFilteredEmpty',
tableSelector: '.local-dns-p2p-table',
endId: 'p2pConflictsEnd',
countId: 'p2pConflictsCount',
countEmptyLabel: 'No P2P conflicts',
countSingular: 'conflict',
countPlural: 'conflicts'
});
updateLocalDnsSubtabBadges();
}
function showLocalDnsListEnd() {
const totalCount = (window.localDnsData || []).length;
const visibleCount = (window.filteredLocalDns || window.localDnsData || []).length;
const endEl = document.getElementById('localDnsEnd');
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
}
function showDnsConflictsListEnd() {
const totalCount = (window.dnsConflictsData || []).length;
const visibleCount = (window.filteredDnsConflicts || window.dnsConflictsData || []).length;
const endEl = document.getElementById('dnsConflictsEnd');
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
}
function showP2pConflictsListEnd() {
const totalCount = (window.p2pDomainConflictsData || []).length;
const visibleCount = (window.filteredP2pDomainConflicts || window.p2pDomainConflictsData || []).length;
const endEl = document.getElementById('p2pConflictsEnd');
if (endEl && totalCount > 0 && visibleCount > 0) endEl.classList.add('local-dns-list-end--visible');
}
function localDnsFieldHtml(label, inputHtml) {
return ``;
}
function openLocalDnsModal(editIndex = -1) {
const modal = document.getElementById('localDnsModal');
const title = document.getElementById('local-dns-title');
const nameInput = document.getElementById('local-name');
const typeSelect = document.getElementById('local-type');
const ttlInput = document.getElementById('local-ttl');
const submitBtn = document.getElementById('local-submit');
if (!modal || !title || !nameInput || !typeSelect || !ttlInput || !submitBtn) return;
nameInput.value = '';
typeSelect.value = 'A';
ttlInput.value = 3600;
if (window.updateLocalForm) updateLocalForm();
if (editIndex >= 0) {
const rec = window.localDnsData?.find(r => r.index === editIndex);
if (rec) {
nameInput.value = rec.name;
typeSelect.value = rec.type;
if (window.updateLocalForm) updateLocalForm(rec);
ttlInput.value = rec.ttl;
title.textContent = 'Edit Local DNS Record';
submitBtn.textContent = 'Update';
window.editLocalIndex = editIndex;
}
} else {
title.textContent = 'Add Local DNS Record';
submitBtn.textContent = 'Add';
window.editLocalIndex = -1;
}
modal.showModal();
}
function updateLocalForm(rec = null) {
const typeEl = document.getElementById('local-type');
const fields = document.getElementById('local-value-fields');
if (!typeEl || !fields) return;
const type = typeEl.value;
fields.innerHTML = '';
let inputHtml = '';
switch (type) {
case 'A':
case 'AAAA':
inputHtml = localDnsFieldHtml('Value', ``);
break;
case 'CNAME':
inputHtml = localDnsFieldHtml('Target', ``);
break;
case 'TXT':
inputHtml = localDnsFieldHtml('Text', ``);
break;
case 'MX':
inputHtml = localDnsFieldHtml('Preference', ``)
+ localDnsFieldHtml('Exchange', ``);
break;
case 'SRV':
inputHtml = localDnsFieldHtml('Priority', ``)
+ localDnsFieldHtml('Weight', ``)
+ localDnsFieldHtml('Port', ``)
+ localDnsFieldHtml('Target', ``);
break;
case 'SOA':
inputHtml = localDnsFieldHtml('Primary NS', ``)
+ localDnsFieldHtml('Responsible', ``)
+ localDnsFieldHtml('Serial', ``)
+ localDnsFieldHtml('Refresh', ``)
+ localDnsFieldHtml('Retry', ``)
+ localDnsFieldHtml('Expire', ``)
+ localDnsFieldHtml('Minimum TTL', ``);
break;
case 'CAA':
inputHtml = localDnsFieldHtml('Flags', ``)
+ localDnsFieldHtml('Tag', ``)
+ localDnsFieldHtml('Value', ``);
break;
case 'NS':
case 'PTR':
inputHtml = localDnsFieldHtml('Name server', ``);
break;
default:
inputHtml = localDnsFieldHtml('Record data', ``);
break;
}
fields.innerHTML = inputHtml;
if (rec) {
switch (type) {
case 'MX':
const prefEl = document.getElementById('local-preference');
const exchEl = document.getElementById('local-exchange');
if (prefEl) prefEl.value = rec.preference || '';
if (exchEl) exchEl.value = rec.exchange || '';
break;
case 'SRV':
const priEl = document.getElementById('local-priority');
const weightEl = document.getElementById('local-weight');
const portEl = document.getElementById('local-port');
const targetEl = document.getElementById('local-target');
if (priEl) priEl.value = rec.priority || '';
if (weightEl) weightEl.value = rec.weight || '';
if (portEl) portEl.value = rec.port || '';
if (targetEl) targetEl.value = rec.target || '';
break;
case 'SOA':
const mnameEl = document.getElementById('local-mname');
const rnameEl = document.getElementById('local-rname');
const serialEl = document.getElementById('local-serial');
const refreshEl = document.getElementById('local-refresh');
const retryEl = document.getElementById('local-retry');
const expireEl = document.getElementById('local-expire');
const minimumEl = document.getElementById('local-minimum');
if (mnameEl) mnameEl.value = rec.mname || '';
if (rnameEl) rnameEl.value = rec.rname || '';
if (serialEl) serialEl.value = rec.serial || '';
if (refreshEl) refreshEl.value = rec.refresh || '';
if (retryEl) retryEl.value = rec.retry || '';
if (expireEl) expireEl.value = rec.expire || '';
if (minimumEl) minimumEl.value = rec.minimum || '';
break;
case 'CAA':
const flagsEl = document.getElementById('local-flags');
const tagEl = document.getElementById('local-tag');
const valueEl = document.getElementById('local-value');
if (flagsEl) flagsEl.value = rec.flags || '';
if (tagEl) tagEl.value = rec.tag || '';
if (valueEl) valueEl.value = rec.value || '';
break;
default:
const dataEl = document.getElementById('local-data');
if (dataEl) dataEl.value = rec.data || rec.value || '';
break;
}
}
}
async function submitLocalDns(buttonElement) {
if (!buttonElement) return;
const originalText = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = 'Saving... ';
try {
const nameEl = document.getElementById('local-name');
const typeEl = document.getElementById('local-type');
const ttlEl = document.getElementById('local-ttl');
if (!nameEl || !typeEl || !ttlEl) return;
const name = nameEl.value;
const type = typeEl.value;
const ttl = parseInt(ttlEl.value) || 3600;
let record = { name, type, ttl, class: 'IN' };
switch (type) {
case 'MX':
const prefEl = document.getElementById('local-preference');
const exchEl = document.getElementById('local-exchange');
record.preference = parseInt(prefEl?.value) || 10;
record.exchange = exchEl?.value || '';
break;
case 'SRV':
record.priority = parseInt(document.getElementById('local-priority')?.value) || 0;
record.weight = parseInt(document.getElementById('local-weight')?.value) || 0;
record.port = parseInt(document.getElementById('local-port')?.value) || 0;
record.target = document.getElementById('local-target')?.value || '';
break;
case 'SOA':
record.mname = document.getElementById('local-mname')?.value || '';
record.rname = document.getElementById('local-rname')?.value || '';
record.serial = parseInt(document.getElementById('local-serial')?.value) || 0;
record.refresh = parseInt(document.getElementById('local-refresh')?.value) || 0;
record.retry = parseInt(document.getElementById('local-retry')?.value) || 0;
record.expire = parseInt(document.getElementById('local-expire')?.value) || 0;
record.minimum = parseInt(document.getElementById('local-minimum')?.value) || 0;
break;
case 'CAA':
record.flags = parseInt(document.getElementById('local-flags')?.value) || 0;
record.tag = document.getElementById('local-tag')?.value || '';
record.value = document.getElementById('local-value')?.value || '';
break;
default:
record.data = document.getElementById('local-data')?.value || '';
break;
}
const isEdit = window.editLocalIndex >= 0;
const url = isEdit ? '/api/update-local-dns' : '/api/add-local-dns';
const body = isEdit ? JSON.stringify({ index: window.editLocalIndex, record }) : JSON.stringify(record);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
});
if (!response.ok) {
throw new Error(await response.text());
}
if (window.showNotification) window.showNotification(isEdit ? 'Record updated successfully' : 'Record added successfully');
const modal = document.getElementById('localDnsModal');
if (modal) modal.close();
if (window.genericFetch) window.genericFetch('local-dns', true);
} catch (err) {
console.error('Failed to submit local DNS record:', err);
if (window.showNotification) window.showNotification('Failed to submit record: ' + err.message, 'error');
} finally {
buttonElement.disabled = false;
buttonElement.innerHTML = originalText;
}
}
function editLocalDns(index) {
openLocalDnsModal(index);
}
function deleteLocalDns(index) {
if (window.showConfirm) {
window.showConfirm('Delete this record?', async () => {
try {
const response = await fetch('/api/delete-local-dns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ index })
});
if (!response.ok) {
throw new Error(await response.text());
}
if (window.showNotification) window.showNotification('Record deleted successfully');
if (window.genericFetch) window.genericFetch('local-dns', true);
} catch (err) {
console.error('Failed to delete local DNS record:', err);
if (window.showNotification) window.showNotification('Failed to delete record: ' + err.message, 'error');
}
});
}
}
async function toggleVersionPreference(domain, isPublic) {
try {
const version = isPublic ? 'public' : 'p2p';
const response = await fetch('/api/update-version-preference', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain, version })
});
if (!response.ok) {
throw new Error(await response.text());
}
if (window.showNotification) window.showNotification(`Version preference for ${domain} set to ${version}`);
if (window.genericFetch) window.genericFetch('dns-conflicts', true);
} catch (err) {
console.error('Failed to update version preference:', err);
if (window.showNotification) window.showNotification('Failed to update version preference: ' + err.message, 'error');
}
}
async function toggleHashPreference(domain, useLocal) {
const newPreference = useLocal ? 'local' : 'resolved';
// Immediately update UI to show pending state
if (window.showNotification) {
window.showNotification(`Switching hash preference for ${domain}...`, 'info');
}
try {
// Step 1: Update the preference
const response = await fetch('/api/update-hash-preference', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain, preference: newPreference })
});
if (!response.ok) {
throw new Error(await response.text());
}
// Step 2: Clear DNS cache for immediate effect
await fetch('/api/clear-dns-cache', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain })
});
// Step 3: Wait for Holesail clients to fully restart
await fetch('/api/restart-holesail-clients-for-domain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain })
});
// Step 4: Verify the new connections are ready (small delay for startup)
await new Promise(resolve => setTimeout(resolve, 500));
// Success - update UI
if (window.showNotification) {
window.showNotification(`Hash preference for ${domain} set to ${newPreference}`);
}
if (window.genericFetch) {
window.genericFetch('p2p-domain-conflicts', true);
}
} catch (err) {
console.error('Failed to update hash preference:', err);
if (window.showNotification) {
window.showNotification('Failed to update hash preference: ' + err.message, 'error');
}
}
}
window.openLocalDnsModal = openLocalDnsModal;
window.updateLocalForm = updateLocalForm;
window.submitLocalDns = submitLocalDns;
window.editLocalDns = editLocalDns;
window.deleteLocalDns = deleteLocalDns;
window.toggleVersionPreference = toggleVersionPreference;
window.toggleHashPreference = toggleHashPreference;
window.classifyDnsRecordType = classifyDnsRecordType;
window.formatLocalDnsValue = formatLocalDnsValue;
window.updateLocalDnsChrome = updateLocalDnsChrome;
window.updateDnsConflictsChrome = updateDnsConflictsChrome;
window.updateP2pConflictsChrome = updateP2pConflictsChrome;
window.updateLocalDnsSubtabBadges = updateLocalDnsSubtabBadges;
window.showLocalDnsListEnd = showLocalDnsListEnd;
window.showDnsConflictsListEnd = showDnsConflictsListEnd;
window.showP2pConflictsListEnd = showP2pConflictsListEnd;