353 lines
17 KiB
JavaScript
353 lines
17 KiB
JavaScript
// Configuration constants
|
|
window.updateMap = {
|
|
'update-database': ['domains', 'entries'],
|
|
'update-peers': ['peers'],
|
|
'update-certs': ['certs'],
|
|
'update-interfaces': ['interfaces'],
|
|
'update-local-dns': ['local-dns', 'dns-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: 20 },
|
|
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 },
|
|
plugins: { current: 1, size: 10 }
|
|
};
|
|
|
|
// Helper function to get consensus status badge
|
|
function getConsensusStatusBadge(status) {
|
|
const badges = {
|
|
'resolved': '<span class="px-2 py-1 bg-green-500 text-white rounded text-xs">Resolved</span>',
|
|
'tie': '<span class="px-2 py-1 bg-yellow-500 text-white rounded text-xs">Tie</span>',
|
|
'insufficient_quorum': '<span class="px-2 py-1 bg-orange-500 text-white rounded text-xs">No Quorum</span>',
|
|
'no_claims': '<span class="px-2 py-1 bg-gray-500 text-white rounded text-xs">No Claims</span>',
|
|
'error': '<span class="px-2 py-1 bg-red-500 text-white rounded text-xs">Error</span>',
|
|
'internal': '<span class="px-2 py-1 bg-blue-500 text-white rounded text-xs">Internal</span>',
|
|
'unknown': '<span class="px-2 py-1 bg-gray-400 text-white rounded text-xs">Unknown</span>'
|
|
};
|
|
return badges[status] || badges['unknown'];
|
|
}
|
|
|
|
// Make function globally available
|
|
window.getConsensusStatusBadge = getConsensusStatusBadge;
|
|
|
|
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),
|
|
renderItem: (item) => {
|
|
const tr = document.createElement('tr');
|
|
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
|
|
|
let consensusInfo = '';
|
|
if (item.consensusState) {
|
|
const statusBadge = getConsensusStatusBadge(item.consensusStatus);
|
|
consensusInfo = `<td class="p-3">${statusBadge}</td>`;
|
|
} else if (item.consensusStatus === 'internal') {
|
|
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('internal')}</td>`;
|
|
} else {
|
|
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('unknown')}</td>`;
|
|
}
|
|
|
|
tr.innerHTML = `<td class="p-3">${item.domain}${item.isLocal ? '🏠' : ''}</td>
|
|
<td class="p-3 break-all">${item.hash}</td>
|
|
${consensusInfo}
|
|
<td class="p-3">${item.isLocal && item.hash !== 'internal' ? `<button onclick="removeDomain('${item.domain}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>` : ''}</td>`;
|
|
return tr;
|
|
},
|
|
postFetch: (data) => {
|
|
return data.map(item => {
|
|
if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') {
|
|
return { ...item, consensusStatus: 'internal' };
|
|
}
|
|
return {
|
|
...item,
|
|
consensusStatus: item.consensusState?.status || 'unknown'
|
|
};
|
|
});
|
|
}
|
|
},
|
|
entries: {
|
|
api: '/api/entries',
|
|
searchId: 'search-entries',
|
|
dataKey: 'entriesData',
|
|
filteredKey: 'filteredEntries',
|
|
containerId: 'entriesTable',
|
|
paginationId: 'entriesPagination',
|
|
sort: (a, b) => a.key.localeCompare(b.key, undefined, { sensitivity: 'base' }),
|
|
filter: (item, query) => item.key.toLowerCase().includes(query) || item.value.toLowerCase().includes(query),
|
|
renderItem: (item) => {
|
|
const tr = document.createElement('tr');
|
|
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
|
tr.innerHTML = `<td class="p-3 break-all">${item.key}</td>
|
|
<td class="p-3 break-all">${item.value}</td>`;
|
|
return tr;
|
|
}
|
|
},
|
|
peers: {
|
|
api: '/api/peers',
|
|
searchId: 'search-peers',
|
|
dataKey: 'peersData',
|
|
filteredKey: 'filteredPeers',
|
|
containerId: 'peersList',
|
|
paginationId: 'peersPagination',
|
|
sort: (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }),
|
|
filter: (item, query) => item.toLowerCase().includes(query),
|
|
renderItem: (peer) => {
|
|
const li = document.createElement('li');
|
|
li.className = 'p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow break-all';
|
|
li.textContent = peer;
|
|
return li;
|
|
},
|
|
preRender: (total) => {
|
|
const el = document.getElementById('peers-count');
|
|
if (el) el.textContent = `(${total})`;
|
|
}
|
|
},
|
|
certs: {
|
|
api: '/api/certs',
|
|
searchId: 'search-certs',
|
|
dataKey: 'certsData',
|
|
filteredKey: 'filteredCerts',
|
|
containerId: 'certsList',
|
|
paginationId: 'certsPagination',
|
|
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 = 'p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow flex justify-between items-center';
|
|
li.innerHTML = `<span class="cursor-pointer flex-1 break-all" onclick="showCertDetails('${cert}')">${cert}</span>
|
|
<div>
|
|
<button onclick="deleteCert('${cert}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 mr-2">Delete</button>
|
|
<button onclick="regenerateCert('${cert}')" class="px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">Regenerate</button>
|
|
</div>`;
|
|
return li;
|
|
}
|
|
},
|
|
interfaces: {
|
|
api: '/api/interfaces',
|
|
searchId: 'search-interfaces',
|
|
dataKey: 'interfacesData',
|
|
filteredKey: 'filteredInterfaces',
|
|
containerId: 'interfacesTable',
|
|
paginationId: 'interfacesPagination',
|
|
sort: (a, b) => a.domain.localeCompare(b.domain, undefined, { sensitivity: 'base' }),
|
|
filter: (item, query) => item.domain.toLowerCase().includes(query) || item.ip.toLowerCase().includes(query),
|
|
renderItem: (item) => {
|
|
const tr = document.createElement('tr');
|
|
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
|
tr.innerHTML = `<td class="p-3">${item.domain}</td>
|
|
<td class="p-3">${item.ip}</td>`;
|
|
return tr;
|
|
}
|
|
},
|
|
'local-dns': {
|
|
api: '/api/local-dns',
|
|
searchId: 'search-local-dns',
|
|
dataKey: 'localDnsData',
|
|
filteredKey: 'filteredLocalDns',
|
|
containerId: 'localDnsTable',
|
|
paginationId: 'localDnsPagination',
|
|
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) => {
|
|
let valueStr = '';
|
|
if (item.type === 'MX') {
|
|
valueStr = `${item.preference || ''} ${item.exchange || ''}`.trim();
|
|
} else if (item.type === 'SRV') {
|
|
valueStr = `${item.priority || ''} ${item.weight || ''} ${item.port || ''} ${item.target || ''}`.trim();
|
|
} else if (item.type === 'SOA') {
|
|
valueStr = `${item.mname || ''} ${item.rname || ''} ${item.serial || ''} ${item.refresh || ''} ${item.retry || ''} ${item.expire || ''} ${item.minimum || ''}`.trim();
|
|
} else if (item.type === 'CAA') {
|
|
valueStr = `${item.flags || ''} ${item.tag || ''} ${item.value || ''}`.trim();
|
|
} else {
|
|
valueStr = item.data || item.value || '';
|
|
}
|
|
const tr = document.createElement('tr');
|
|
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
|
tr.innerHTML = `
|
|
<td class="p-3">${item.name}</td>
|
|
<td class="p-3">${item.type}</td>
|
|
<td class="p-3 break-all">${valueStr}</td>
|
|
<td class="p-3">${item.ttl}</td>
|
|
<td class="p-3">
|
|
<button onclick="editLocalDns(${item.index})" class="px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 mr-2">Edit</button>
|
|
<button onclick="deleteLocalDns(${item.index})" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
|
|
</td>`;
|
|
return tr;
|
|
},
|
|
postFetch: (data) => {
|
|
window.dnsConflictsData = data.conflicts || [];
|
|
window.filteredDnsConflicts = window.dnsConflictsData;
|
|
data.records = data.records.map((rec, index) => ({ ...rec, index }));
|
|
if (window.renderDnsConflicts) window.renderDnsConflicts();
|
|
return data.records;
|
|
}
|
|
},
|
|
'dns-conflicts': {
|
|
api: '/api/local-dns',
|
|
searchId: 'search-dns-conflicts',
|
|
dataKey: 'dnsConflictsData',
|
|
filteredKey: 'filteredDnsConflicts',
|
|
containerId: 'dnsConflictsTable',
|
|
paginationId: 'dnsConflictsPagination',
|
|
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 tr = document.createElement('tr');
|
|
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
|
|
tr.innerHTML = `
|
|
<td class="p-3">${item.domain}</td>
|
|
<td class="p-3">${item.publicIP}</td>
|
|
<td class="p-3">
|
|
<label class="inline-flex items-center cursor-pointer">
|
|
<span class="mr-2">${item.version === 'public' ? 'Public' : 'P2P'}</span>
|
|
<input type="checkbox" ${item.version === 'public' ? 'checked' : ''} onchange="toggleVersionPreference('${item.domain}', this.checked)" class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:bg-primary">
|
|
<div class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
|
|
</div>
|
|
</label>
|
|
</td>`;
|
|
return tr;
|
|
},
|
|
postFetch: (data) => {
|
|
window.localDnsData = data.records.map((rec, index) => ({ ...rec, index }));
|
|
window.filteredLocalDns = window.localDnsData;
|
|
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
|
|
? `<button disabled class="px-2 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 mr-2">Restarting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span></button>`
|
|
: `<button onclick="restartHolesailServer('${item.id}')" class="px-2 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 mr-2">Restart</button>`;
|
|
const deleteBtn = isPendingDelete
|
|
? `<button disabled class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Deleting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span></button>`
|
|
: `<button onclick="deleteHolesailServer('${item.id}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>`;
|
|
const protocol = item.opts.udp ? 'UDP' : 'TCP';
|
|
const url = item.info.url || 'N/A';
|
|
const truncatedUrl = window.truncateUrl ? window.truncateUrl(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 = `
|
|
<td class="p-3 cursor-pointer text-blue-500 hover:underline" onclick="openHolesailLog('${item.id}', '${item.opts.name || item.id}')" title="${item.opts.name || item.id}">${item.opts.name || item.id}</td>
|
|
<td class="p-3">${item.opts.port}</td>
|
|
<td class="p-3">${item.opts.host || '0.0.0.0'}</td>
|
|
<td class="p-3" title="${url}">${truncatedUrl}</td>
|
|
<td class="p-3">${protocol}</td>
|
|
<td class="p-3">${window.renderStatusBadge ? window.renderStatusBadge(item.info.state) : item.info.state}</td>
|
|
<td class="p-3">
|
|
${restartBtn}
|
|
${deleteBtn}
|
|
</td>`;
|
|
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
|
|
? `<button disabled class="px-2 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 mr-2">Restarting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span></button>`
|
|
: `<button onclick="restartHolesailClient('${item.id}')" class="px-2 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 mr-2">Restart</button>`;
|
|
const deleteBtn = isPendingDelete
|
|
? `<button disabled class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Deleting... <span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white ml-2"></span></button>`
|
|
: `<button onclick="deleteHolesailClient('${item.id}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>`;
|
|
const protocol = (item.opts.protocol || 'tcp').toUpperCase();
|
|
const key = item.opts.key || '';
|
|
const truncatedKey = window.truncateUrl ? window.truncateUrl(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 = `
|
|
<td class="p-3 cursor-pointer text-blue-500 hover:underline" onclick="openHolesailLog('${item.id}', '${item.opts.domain}:${item.opts.port}')" title="${item.opts.domain}">${item.opts.domain}</td>
|
|
<td class="p-3" title="${key}">${truncatedKey}</td>
|
|
<td class="p-3">${item.opts.port}</td>
|
|
<td class="p-3">${protocol}</td>
|
|
<td class="p-3">${window.renderStatusBadge ? window.renderStatusBadge(item.info.state) : item.info.state}</td>
|
|
<td class="p-3">
|
|
${restartBtn}
|
|
${deleteBtn}
|
|
</td>`;
|
|
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) => {
|
|
window.settingsMetadata = data.metadata || {};
|
|
return data.settings || {};
|
|
}
|
|
}
|
|
};
|