// Peers UI — flat admin layout with overview stats and connection details let peerChart = null; async function renderPeers() { if (window.genericFetch) { await window.genericFetch('peers', true); } renderPeerGraph(); } function filterPeers() { if (window.genericFilter) { window.genericFilter('peers'); } } function setPeerStat(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; } function escapePeerHtml(value) { return window.escapeHtml ? window.escapeHtml(value) : String(value); } async function showPeerDetails(peerId) { try { const [peerRes, historyRes] = await Promise.all([ fetch(`/api/peers/${encodeURIComponent(peerId)}`), fetch(`/api/peers/${encodeURIComponent(peerId)}/history`) ]); if (!peerRes.ok || !historyRes.ok) { throw new Error('Failed to fetch peer details'); } const peer = await peerRes.json(); const history = await historyRes.json(); const modal = document.getElementById('peerDetailsModal'); if (!modal) return; const content = document.getElementById('peer-details-content'); if (!content) return; const uptime = peer.uptime ? (window.formatUptime ? window.formatUptime(peer.uptime) : `${Math.floor(peer.uptime / 1000)}s`) : 'N/A'; const connectTime = peer.connectTime ? new Date(peer.connectTime).toLocaleString() : 'N/A'; const lastSeen = peer.metrics?.lastSeen ? new Date(peer.metrics.lastSeen).toLocaleString() : 'N/A'; const totalDuration = peer.metrics?.totalDuration ? (window.formatDuration ? window.formatDuration(peer.metrics.totalDuration) : `${Math.floor(peer.metrics.totalDuration / 1000)}s`) : '0s'; const avgDuration = peer.metrics?.avgDuration ? (window.formatDuration ? window.formatDuration(peer.metrics.avgDuration) : `${Math.floor(peer.metrics.avgDuration / 1000)}s`) : 'N/A'; const statusBadge = peer.connected ? 'Connected' : 'Disconnected'; const blockedBadge = peer.isBlocked ? 'Blocked' : 'Not blocked'; content.innerHTML = `

Peer information

ID
${escapePeerHtml(peer.id)}
Status
${statusBadge}
Blocked
${blockedBadge}
Uptime
${escapePeerHtml(uptime)}
Connected at
${escapePeerHtml(connectTime)}

Metrics

Connections
${peer.metrics?.connections || 0}
Total duration
${escapePeerHtml(totalDuration)}
Average duration
${escapePeerHtml(avgDuration)}
Last seen
${escapePeerHtml(lastSeen)}

Connection history

${history.length === 0 ? '

No history available.

' : history.slice(-50).reverse().map((event) => `
${event.type === 'connect' ? 'Connected' : 'Disconnected'}
${event.duration ? `
Duration: ${escapePeerHtml(window.formatDuration ? window.formatDuration(event.duration) : `${Math.floor(event.duration / 1000)}s`)}
` : ''} ${event.error ? `
Error: ${escapePeerHtml(event.error)}
` : ''}
`).join('') }
`; modal.showModal(); } catch (err) { console.error('Failed to fetch peer details:', err); if (window.showNotification) window.showNotification('Failed to load peer details: ' + err.message, 'error'); } } async function blockPeer(peerId) { if (window.showConfirm) { window.showConfirm(`Block peer ${peerId.substring(0, 16)}...?`, async () => { try { const response = await fetch(`/api/peers/${encodeURIComponent(peerId)}/block`, { method: 'POST' }); if (!response.ok) { throw new Error('Failed to block peer'); } if (window.showNotification) window.showNotification('Peer blocked successfully'); await renderPeers(); } catch (err) { console.error('Failed to block peer:', err); if (window.showNotification) window.showNotification('Failed to block peer: ' + err.message, 'error'); } }); } } async function unblockPeer(peerId) { if (window.showConfirm) { window.showConfirm(`Unblock peer ${peerId.substring(0, 16)}...?`, async () => { try { const response = await fetch(`/api/peers/${encodeURIComponent(peerId)}/unblock`, { method: 'POST' }); if (!response.ok) { throw new Error('Failed to unblock peer'); } if (window.showNotification) window.showNotification('Peer unblocked successfully'); await renderPeers(); } catch (err) { console.error('Failed to unblock peer:', err); if (window.showNotification) window.showNotification('Failed to unblock peer: ' + err.message, 'error'); } }); } } function renderPeerGraph() { const canvas = document.getElementById('peer-graph-chart'); if (!canvas) return; const ctx = canvas.getContext('2d'); const peersData = window.peersData || []; const connected = peersData.filter((p) => p.connected).length; const disconnected = peersData.filter((p) => !p.connected).length; const blocked = peersData.filter((p) => p.isBlocked).length; const total = peersData.length; setPeerStat('peerStatConnected', connected); setPeerStat('peerStatDisconnected', disconnected); setPeerStat('peerStatBlocked', blocked); const overviewEl = document.getElementById('peersOverviewLine'); if (overviewEl) { overviewEl.textContent = total === 0 ? 'No peers on this node' : `${connected} active of ${total} total`; } if (peerChart) { peerChart.destroy(); peerChart = null; } const colors = window.chartColors || { success: 'rgb(34, 197, 94)', gray: 'rgb(107, 114, 128)', danger: 'rgb(239, 68, 68)' }; peerChart = new Chart(ctx, { type: 'doughnut', data: { labels: ['Connected', 'Disconnected', 'Blocked'], datasets: [{ data: [connected, disconnected, blocked], backgroundColor: [colors.success, colors.gray, colors.danger], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '62%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8', boxWidth: 10, boxHeight: 10, padding: 14, font: { size: 11 } } } } } }); } window.renderPeers = renderPeers; window.showPeerDetails = showPeerDetails; window.blockPeer = blockPeer; window.unblockPeer = unblockPeer; window.filterPeers = filterPeers;