Files
p2ns/includes/admin/ui/config.js
T
2025-12-17 20:49:34 -05:00

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