// 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;