// Configuration constants window.updateMap = { 'update-database': ['domains', 'entries'], 'update-peers': ['peers'], 'update-certs': ['certs'], 'update-interfaces': ['interfaces'], 'update-local-dns': ['local-dns', 'dns-conflicts', 'p2p-domain-conflicts'], 'update-holesail': [], 'update-holesail-clients': [], 'update-settings': ['settings'], 'update-stats': [], 'system-reset': () => location.reload() }; window.paginationState = { domains: { current: 1, size: 8 }, entries: { current: 1, size: 10 }, peers: { current: 1, size: 4 }, certs: { current: 1, size: 5 }, interfaces: { current: 1, size: 10 }, 'local-dns': { current: 1, size: 10 }, 'dns-conflicts': { current: 1, size: 10 }, 'host-servers': { current: 1, size: 10 }, 'host-clients': { current: 1, size: 10 }, settings: { current: 1, size: 20 }, backups: { current: 1, size: 10 }, plugins: { current: 1, size: 10 } }; // Helper function to get consensus status badge function getConsensusStatusBadge(status) { const badges = { 'resolved': 'Resolved', 'tie': 'Tie', 'conflict': 'Conflict', 'insufficient_quorum': 'No Quorum', 'no_claims': 'No Claims', 'error': 'Error', 'internal': 'Internal', 'unknown': 'Unknown' }; return badges[status] || badges['unknown']; } // Make function globally available window.getConsensusStatusBadge = getConsensusStatusBadge; function updateCertsListChrome() { const totalCount = (window.certsData || []).length; const emptyEl = document.getElementById('certsEmpty'); const endEl = document.getElementById('certsEnd'); const listEl = document.getElementById('certsList'); const trulyEmpty = totalCount === 0; if (emptyEl) { emptyEl.classList.toggle('cert-empty--visible', trulyEmpty); } if (listEl) { listEl.classList.toggle('cert-list--hidden', trulyEmpty); } if (endEl) { endEl.classList.remove('cert-list-end--visible'); } } function showCertsListEnd() { const totalCount = (window.certsData || []).length; const visibleCount = (window.filteredCerts || window.certsData || []).length; const endEl = document.getElementById('certsEnd'); if (endEl && totalCount > 0 && visibleCount > 0) { endEl.classList.add('cert-list-end--visible'); } } function updatePeersListChrome() { const totalCount = (window.peersData || []).length; const filteredCount = (window.filteredPeers || window.peersData || []).length; const searchEl = document.getElementById('search-peers'); const hasFilter = Boolean(searchEl?.value?.trim()); const emptyEl = document.getElementById('peersEmpty'); const filteredEmptyEl = document.getElementById('peersFilteredEmpty'); const listEl = document.getElementById('peersList'); const endEl = document.getElementById('peersEnd'); const trulyEmpty = totalCount === 0; const filteredEmpty = !trulyEmpty && filteredCount === 0; if (emptyEl) emptyEl.classList.toggle('peer-empty--visible', trulyEmpty); if (filteredEmptyEl) filteredEmptyEl.classList.toggle('peer-empty--visible', filteredEmpty); if (listEl) listEl.classList.toggle('peer-list--hidden', trulyEmpty || filteredEmpty); if (endEl) endEl.classList.remove('peer-list-end--visible'); const countEl = document.getElementById('peersCount'); if (countEl) { if (trulyEmpty) countEl.textContent = 'No peers connected'; else if (hasFilter) countEl.textContent = `${filteredCount} of ${totalCount} shown`; else countEl.textContent = `${totalCount} peer${totalCount === 1 ? '' : 's'}`; } } function showPeersListEnd() { const totalCount = (window.peersData || []).length; const visibleCount = (window.filteredPeers || window.peersData || []).length; const endEl = document.getElementById('peersEnd'); if (endEl && totalCount > 0 && visibleCount > 0) { endEl.classList.add('peer-list-end--visible'); } } window.updateCertsListChrome = updateCertsListChrome; window.showCertsListEnd = showCertsListEnd; window.updatePeersListChrome = updatePeersListChrome; window.showPeersListEnd = showPeersListEnd; window.chartColors = { primary: 'rgb(59, 130, 246)', success: 'rgb(34, 197, 94)', warning: 'rgb(234, 179, 8)', danger: 'rgb(239, 68, 68)', info: 'rgb(59, 130, 246)', gray: 'rgb(107, 114, 128)', dark: 'rgb(17, 24, 39)' }; window.darkModeColors = { primary: 'rgb(96, 165, 250)', success: 'rgb(74, 222, 128)', warning: 'rgb(250, 204, 21)', danger: 'rgb(248, 113, 113)', info: 'rgb(96, 165, 250)', gray: 'rgb(156, 163, 175)', dark: 'rgb(243, 244, 246)' }; // Tabs configuration - uses functions from utils.js and other modules window.tabs = { domains: { api: '/api/resolved-domains', searchId: 'search-domains', dataKey: 'domainsData', filteredKey: 'filteredDomains', containerId: 'domainsTable', paginationId: 'domainsPagination', sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }), filter: (item, query) => item.domain.toLowerCase().includes(query) || item.hash.toLowerCase().includes(query) || (item.consensusStatus || '').toLowerCase().includes(query), renderItem: (item) => { const tr = document.createElement('tr'); tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700'; // Use consensus status from postFetch (which may include 'conflict') let consensusInfo = ''; if (item.consensusState) { const consensusState = item.consensusState; const statusBadge = getConsensusStatusBadge(item.consensusStatus); const voteInfo = Object.keys(consensusState.voteCounts || {}).length > 0 ? ` (${Object.values(consensusState.voteCounts).reduce((a, b) => a + b, 0)} votes)` : ''; const quorumInfo = consensusState.quorumMet ? '✓' : '✗'; consensusInfo = `
${statusBadge} ${voteInfo} ${quorumInfo}
`; } else if (item.consensusStatus === 'internal') { consensusInfo = `${getConsensusStatusBadge('internal')}`; } else { consensusInfo = `${getConsensusStatusBadge('unknown')}`; } tr.innerHTML = `${item.domain}${item.isLocal ? ' ' : ''} ${item.hash} ${consensusInfo} ${item.isLocal && item.hash !== 'internal' ? `` : ''}`; return tr; }, postFetch: (data) => { // Data already contains consensusState from backend return data.map(item => { if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') { return { ...item, consensusStatus: 'internal' }; } // Use consensusStatus set by backend (including 'conflict'), fallback to consensusState status return { ...item, consensusStatus: item.consensusStatus || item.consensusState?.status || 'unknown' }; }); } }, entries: { api: '/api/entries', searchId: 'search-entries', dataKey: 'entriesData', filteredKey: 'filteredEntries', containerId: 'entriesTable', paginationId: 'entriesPagination', sentinelId: 'entriesScrollSentinel', sort: (a, b) => a.key.localeCompare(b.key, undefined, { sensitivity: 'base' }), filter: (item, query) => { const q = query.toLowerCase(); const key = (item.key || '').toLowerCase(); const value = (item.value || '').toLowerCase(); const type = key.startsWith('claim:') ? 'claim' : key.startsWith('vote:') ? 'vote' : 'other'; return key.includes(q) || value.includes(q) || type.includes(q); }, renderItem: (item) => { const esc = window.escapeHtml || ((value) => String(value)); const classify = window.classifyEntryKey || ((key) => ({ label: 'Other', className: 'entry-badge--muted' })); const type = classify(item.key); const tr = document.createElement('tr'); tr.className = 'entry-row'; tr.innerHTML = ` ${esc(type.label)} ${esc(item.key)} ${esc(item.value)} `; return tr; }, preRender: (visibleCount) => { if (window.updateEntriesChrome) window.updateEntriesChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showEntriesListEnd) window.showEntriesListEnd(); } }, peers: { api: '/api/peers', searchId: 'search-peers', dataKey: 'peersData', filteredKey: 'filteredPeers', containerId: 'peersList', paginationId: 'peersPagination', sentinelId: 'peersScrollSentinel', sort: (a, b) => (a.id || '').localeCompare(b.id || '', undefined, { sensitivity: 'base' }), filter: (item, query) => { const queryLower = query.toLowerCase(); return (item.id || '').toLowerCase().includes(queryLower) || (item.connected ? 'connected' : 'disconnected').includes(queryLower) || (item.isBlocked ? 'blocked' : '').includes(queryLower); }, renderItem: (peer) => { const li = document.createElement('li'); li.className = 'peer-row'; const esc = window.escapeHtml || ((value) => String(value)); const peerIdAttr = String(peer.id || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const peerId = esc(peer.id || ''); const uptime = peer.uptime ? (window.sdk?.utils?.format?.formatUptime ? window.sdk.utils.format.formatUptime(peer.uptime) : `${Math.floor(peer.uptime / 1000)}s`) : 'N/A'; const avgDuration = peer.metrics?.avgDuration ? (window.sdk?.utils?.format?.formatDuration ? window.sdk.utils.format.formatDuration(peer.metrics.avgDuration) : `${Math.floor(peer.metrics.avgDuration / 1000)}s`) : 'N/A'; const statusClass = peer.connected ? 'peer-badge--ok' : 'peer-badge--muted'; const statusLabel = peer.connected ? 'Connected' : 'Disconnected'; const blockedBadge = peer.isBlocked ? 'Blocked' : ''; li.innerHTML = `
${statusLabel} ${blockedBadge}
Uptime
${esc(uptime)}
Connections
${peer.metrics?.connections || 0}
Avg duration
${esc(avgDuration)}
${peer.isBlocked ? `` : `` }
`; return li; }, preRender: () => { updatePeersListChrome(); }, onAllItemsLoaded: () => { showPeersListEnd(); } }, certs: { api: '/api/certs', searchId: 'search-certs', dataKey: 'certsData', filteredKey: 'filteredCerts', containerId: 'certsList', paginationId: 'certsPagination', sentinelId: 'certsScrollSentinel', sort: (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }), filter: (item, query) => item.toLowerCase().includes(query), renderItem: (cert) => { const li = document.createElement('li'); li.className = 'cert-row'; const esc = window.escapeHtml || ((value) => String(value)); const domainAttr = String(cert).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); li.innerHTML = `
`; return li; }, preRender: () => { updateCertsListChrome(); }, onAllItemsLoaded: () => { showCertsListEnd(); } }, interfaces: { api: '/api/interfaces', searchId: 'search-interfaces', dataKey: 'interfacesData', filteredKey: 'filteredInterfaces', containerId: 'interfacesTable', paginationId: 'interfacesPagination', postFetch: (data) => { if (data && Array.isArray(data.interfaces)) { if (window.renderInterfacesSummary) { window.renderInterfacesSummary(data.summary, data.subnets, data.orphanedIps); } return data.interfaces; } return Array.isArray(data) ? data : []; }, sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }), filter: (item, query) => { const q = query.toLowerCase(); return ( item.domain.toLowerCase().includes(q) || item.ip.toLowerCase().includes(q) || (item.subnetName || '').toLowerCase().includes(q) || (item.type || '').toLowerCase().includes(q) || (item.configuredOnSystem ? 'configured' : 'missing').includes(q) ); }, renderItem: (item) => { const esc = window.escapeHtml || ((value) => String(value)); const domainAttr = esc(item.domain).replace(/'/g, "\\'"); const ipAttr = esc(item.ip).replace(/'/g, "\\'"); const typeBadge = item.type === 'internal' ? 'Internal' : 'Virtual'; const subnetLabel = item.subnetName ? esc(item.subnetName) : (item.type === 'internal' ? '—' : 'Unassigned'); let statusBadge; if (item.type === 'internal') { statusBadge = 'Loopback'; } else if (item.configuredOnSystem) { statusBadge = 'On system'; } else { statusBadge = 'Not on OS'; } const tunnels = item.activeConnections + item.activeClients; const tunnelLabel = tunnels === 0 ? '' : `${item.activeConnections} conn${item.activeConnections === 1 ? '' : 's'}${item.activeClients > 0 ? ` · ${item.activeClients} client${item.activeClients === 1 ? '' : 's'}` : ''}`; const tr = document.createElement('tr'); tr.className = 'iface-row'; tr.innerHTML = ` ${esc(item.domain)} ${esc(item.ip)} ${typeBadge} ${subnetLabel} ${statusBadge} ${tunnelLabel} `; return tr; }, preRender: (visibleCount) => { if (window.updateInterfacesChrome) window.updateInterfacesChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showInterfacesListEnd) window.showInterfacesListEnd(); } }, 'local-dns': { api: '/api/local-dns', searchId: 'search-local-dns', dataKey: 'localDnsData', filteredKey: 'filteredLocalDns', containerId: 'localDnsTable', paginationId: 'localDnsPagination', sentinelId: 'localDnsScrollSentinel', sort: (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), filter: (item, query) => { const queryLower = query.toLowerCase(); return ( item.name.toLowerCase().includes(queryLower) || item.type.toLowerCase().includes(queryLower) || Object.values(item).some(val => typeof val === 'string' && val.toLowerCase().includes(queryLower)) ); }, renderItem: (item) => { const esc = window.escapeHtml || ((value) => String(value)); const typeInfo = window.classifyDnsRecordType ? window.classifyDnsRecordType(item.type) : { label: item.type, className: 'dns-type-badge--muted' }; const valueStr = window.formatLocalDnsValue ? window.formatLocalDnsValue(item) : (item.data || item.value || ''); const tr = document.createElement('tr'); tr.className = 'local-dns-row'; tr.innerHTML = ` ${esc(item.name)} ${esc(typeInfo.label)} ${esc(valueStr)} ${esc(String(item.ttl ?? ''))}
`; return tr; }, preRender: (visibleCount) => { if (window.updateLocalDnsChrome) window.updateLocalDnsChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showLocalDnsListEnd) window.showLocalDnsListEnd(); }, postFetch: (data) => { window.dnsConflictsData = data.conflicts || []; window.filteredDnsConflicts = window.dnsConflictsData; data.records = data.records.map((rec, index) => ({ ...rec, index })); if (window.renderDnsConflicts) window.renderDnsConflicts(); if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges(); return data.records; } }, 'dns-conflicts': { api: '/api/local-dns', searchId: 'search-dns-conflicts', dataKey: 'dnsConflictsData', filteredKey: 'filteredDnsConflicts', containerId: 'dnsConflictsTable', paginationId: 'dnsConflictsPagination', sentinelId: 'dnsConflictsScrollSentinel', sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }), filter: (item, query) => item.domain.toLowerCase().includes(query) || item.version.toLowerCase().includes(query) || item.publicIP.toLowerCase().includes(query), renderItem: (item) => { const esc = window.escapeHtml || ((value) => String(value)); const domainAttr = esc(item.domain).replace(/"/g, '"'); const isPublic = item.version === 'public'; const tr = document.createElement('tr'); tr.className = 'local-dns-row'; tr.innerHTML = ` ${esc(item.domain)} ${esc(item.publicIP || '—')} `; return tr; }, preRender: (visibleCount) => { if (window.updateDnsConflictsChrome) window.updateDnsConflictsChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showDnsConflictsListEnd) window.showDnsConflictsListEnd(); }, postFetch: (data) => { window.localDnsData = data.records.map((rec, index) => ({ ...rec, index })); window.filteredLocalDns = window.localDnsData; if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges(); return data.conflicts || []; } }, 'p2p-domain-conflicts': { api: '/api/p2p-domain-conflicts', searchId: 'search-p2p-domain-conflicts', dataKey: 'p2pDomainConflictsData', filteredKey: 'filteredP2pDomainConflicts', containerId: 'p2pDomainConflictsTable', paginationId: 'p2pDomainConflictsPagination', sentinelId: 'p2pDomainConflictsScrollSentinel', sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }), filter: (item, query) => item.domain.toLowerCase().includes(query) || item.localHash.toLowerCase().includes(query) || item.resolvedHash.toLowerCase().includes(query), renderItem: (item) => { const esc = window.escapeHtml || ((value) => String(value)); const domainAttr = esc(item.domain).replace(/"/g, '"'); const useLocal = item.hashPreference === 'local'; const tr = document.createElement('tr'); tr.className = 'local-dns-row'; tr.innerHTML = ` ${esc(item.domain)} ${esc(item.localHash || 'N/A')} ${esc(item.resolvedHash || 'N/A')} `; return tr; }, preRender: (visibleCount) => { if (window.updateP2pConflictsChrome) window.updateP2pConflictsChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showP2pConflictsListEnd) window.showP2pConflictsListEnd(); }, postFetch: (data) => { if (window.updateLocalDnsSubtabBadges) window.updateLocalDnsSubtabBadges(); return data.conflicts || []; } }, 'host-servers': { api: '/api/holesail-servers', searchId: 'search-holesail', dataKey: 'holesailServersData', filteredKey: 'filteredHolesailServers', containerId: 'holesailTable', paginationId: 'holesailPagination', sort: (a, b) => (a.opts.name || a.id).localeCompare(b.opts.name || b.id, undefined, { sensitivity: 'base' }), filter: (item, query) => (item.opts.name || '').toLowerCase().includes(query) || item.id.toLowerCase().includes(query) || item.opts.port.toString().includes(query) || (item.info.url || '').toLowerCase().includes(query), renderItem: (item) => { const isPendingRestart = window.pendingServerRestarts && window.pendingServerRestarts.has(item.id); const isPendingDelete = window.pendingServerDeletions && window.pendingServerDeletions.has(item.id); const restartBtn = isPendingRestart ? `` : ``; const deleteBtn = isPendingDelete ? `` : ``; const protocol = item.opts.udp ? 'UDP' : 'TCP'; const url = item.info.url || 'N/A'; const truncatedUrl = window.sdk?.utils?.dom?.truncate ? window.sdk.utils.dom.truncate(url, 40) : (url.length > 40 ? url.substring(0, 37) + '...' : url); const tr = document.createElement('tr'); tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700'; tr.innerHTML = ` ${item.opts.name || item.id} ${item.opts.port} ${item.opts.host || '0.0.0.0'} ${truncatedUrl} ${protocol} ${window.renderStatusBadge ? window.renderStatusBadge(item.info.state) : item.info.state} ${restartBtn} ${deleteBtn} `; return tr; } }, 'host-clients': { api: '/api/holesail-clients', searchId: 'search-holesail-clients', dataKey: 'holesailClientsData', filteredKey: 'filteredHolesailClients', containerId: 'holesailClientsTable', paginationId: 'holesailClientsPagination', sort: (a, b) => a.opts.domain.localeCompare(b.opts.domain, undefined, { sensitivity: 'base' }), filter: (item, query) => item.opts.domain.toLowerCase().includes(query) || item.opts.key.toLowerCase().includes(query) || item.opts.port.toString().includes(query), renderItem: (item) => { const isPendingRestart = window.pendingClientRestarts && window.pendingClientRestarts.has(item.id); const isPendingDelete = window.pendingClientDeletions && window.pendingClientDeletions.has(item.id); const restartBtn = isPendingRestart ? `` : ``; const deleteBtn = isPendingDelete ? `` : ``; const protocol = (item.opts.protocol || 'tcp').toUpperCase(); const key = item.opts.key || ''; const truncatedKey = window.sdk?.utils?.dom?.truncate ? window.sdk.utils.dom.truncate(key, 30) : (key.length > 30 ? key.substring(0, 27) + '...' : key); const tr = document.createElement('tr'); tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700'; tr.innerHTML = ` ${item.opts.domain} ${truncatedKey} ${item.opts.port} ${protocol} ${window.renderStatusBadge ? window.renderStatusBadge(item.info.state) : item.info.state} ${restartBtn} ${deleteBtn} `; return tr; } }, settings: { api: '/api/settings', searchId: 'search-settings', dataKey: 'settingsData', filteredKey: 'filteredSettings', containerId: 'settingsContainer', paginationId: 'settingsPagination', sort: null, filter: null, renderItem: null, postFetch: (data) => { // Store metadata globally for renderSettings to use window.settingsMetadata = data.metadata || {}; // Return the full data object so metadata is preserved return data; } }, backups: { api: '/api/backups', searchId: 'search-backups', dataKey: 'backupsData', filteredKey: 'filteredBackups', containerId: 'backupsTable', paginationId: 'backupsPagination', sentinelId: 'backupsScrollSentinel', sort: (a, b) => new Date(b.timestamp) - new Date(a.timestamp), filter: (item, query) => { const queryLower = query.toLowerCase(); return ( item.name.toLowerCase().includes(queryLower) || item.timestamp.toLowerCase().includes(queryLower) || (item.version && item.version.toLowerCase().includes(queryLower)) ); }, renderItem: (backup) => { const esc = window.escapeHtml || ((value) => String(value)); const name = backup.name || ''; const nameAttr = esc(name).replace(/"/g, '"'); const slug = name.replace(/[^a-zA-Z0-9]/g, '-') || 'backup'; const dateStr = backup.timestamp ? new Date(backup.timestamp).toLocaleString() : '—'; const fileCount = backup.files ? backup.files.length : 0; const tr = document.createElement('tr'); tr.className = 'backups-row'; tr.innerHTML = ` ${esc(name)} ${esc(dateStr)} ${esc(backup.sizeFormatted || '0 B')} ${fileCount} ${esc(backup.version || 'unknown')}
`; return tr; }, preRender: (visibleCount) => { if (window.updateBackupsChrome) window.updateBackupsChrome(visibleCount); }, onAllItemsLoaded: () => { if (window.showBackupsListEnd) window.showBackupsListEnd(); }, postFetch: (data) => { if (window.updateBackupsStats) window.updateBackupsStats(data); return data; } } };