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