Add Core RPC section to admin Stats tab

Surface invite diagnostics (RPC health, per-peer state, queues,
recommendations) under Resource Usage via /api/stats core payload
and auto-refreshing renderCoreStats in the stats UI.
This commit is contained in:
Raven Scott
2026-05-28 11:24:41 -04:00
parent 51184d2446
commit 1d309aa82d
3 changed files with 288 additions and 0 deletions
@@ -305,6 +305,17 @@ async function handleStatsRoutes(req, res) {
const hyperdriveStats = collectHyperdriveStats(); const hyperdriveStats = collectHyperdriveStats();
stats.hyperdrive = hyperdriveStats; stats.hyperdrive = hyperdriveStats;
if (typeof state.diagnoseInviteIssues === 'function') {
try {
stats.core = state.diagnoseInviteIssues();
} catch (err) {
logDebug('Admin', `Core RPC stats unavailable: ${err.message}`);
stats.core = { initializing: true, error: err.message };
}
} else {
stats.core = { initializing: true };
}
const responseTime = Date.now() - startTime; const responseTime = Date.now() - startTime;
trackRequestWithTiming('/api/stats', true, responseTime); trackRequestWithTiming('/api/stats', true, responseTime);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
+40
View File
@@ -653,6 +653,46 @@
</div> </div>
</div> </div>
<!-- Core RPC Section -->
<div class="theme-table rounded-lg p-6 mb-6">
<h3 class="text-xl font-bold mb-2">Core</h3>
<p class="text-sm theme-text-tertiary mb-4">
Control plane on <code class="text-indigo-300">p2ns.core-request-rpc</code> — invite flow via
<code class="text-indigo-300">invite.request</code>, <code class="text-indigo-300">invite.deliver</code>, <code class="text-indigo-300">invite.ack</code>.
</p>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-4">
<div class="text-center">
<p class="text-sm theme-text-tertiary">DNS Pass</p>
<p id="core-dns-pass" class="text-2xl font-bold">-</p>
</div>
<div class="text-center">
<p class="text-sm theme-text-tertiary">RPC Open</p>
<p id="core-rpc-open" class="text-2xl font-bold">-</p>
</div>
<div class="text-center">
<p class="text-sm theme-text-tertiary">Request Ch.</p>
<p id="core-request-open" class="text-2xl font-bold">-</p>
</div>
<div class="text-center">
<p class="text-sm theme-text-tertiary">Pending ACK</p>
<p id="core-pending-ack" class="text-2xl font-bold">-</p>
</div>
<div class="text-center">
<p class="text-sm theme-text-tertiary">Failed Peers</p>
<p id="core-failed-peers" class="text-2xl font-bold">-</p>
</div>
<div class="text-center">
<p class="text-sm theme-text-tertiary">Invite Failures</p>
<p id="core-consecutive-failures" class="text-2xl font-bold">-</p>
</div>
</div>
<p id="core-protocol-summary" class="text-xs theme-text-secondary font-mono mb-4 hidden"></p>
<div id="core-details" class="space-y-4">
<p class="theme-text-tertiary text-center">Loading core RPC data...</p>
</div>
<div id="core-recommendations" class="mt-4 hidden"></div>
</div>
<!-- Peer Channels Section --> <!-- Peer Channels Section -->
<div class="theme-table rounded-lg p-6 mb-6"> <div class="theme-table rounded-lg p-6 mb-6">
<h3 class="text-xl font-bold mb-4">Peer Channels</h3> <h3 class="text-xl font-bold mb-4">Peer Channels</h3>
+237
View File
@@ -110,6 +110,239 @@ function renderPeerChannelsStats(peerChannels) {
}).join(''); }).join('');
} }
function coreShortPeerId(peerId) {
if (!peerId || peerId.length <= 16) return peerId || '—';
return peerId.slice(0, 8) + '…' + peerId.slice(-6);
}
function coreStatusColor(ok, warn) {
if (ok) return 'text-green-500';
if (warn) return 'text-yellow-500';
return 'text-red-500';
}
// Render Core RPC / invite diagnostics (from /api/stats core payload)
function renderCoreStats(core) {
const esc = window.escapeHtml || ((s) => String(s));
const dnsEl = document.getElementById('core-dns-pass');
const rpcEl = document.getElementById('core-rpc-open');
const reqEl = document.getElementById('core-request-open');
const ackEl = document.getElementById('core-pending-ack');
const failedEl = document.getElementById('core-failed-peers');
const failuresEl = document.getElementById('core-consecutive-failures');
const protocolEl = document.getElementById('core-protocol-summary');
const detailsEl = document.getElementById('core-details');
const recsEl = document.getElementById('core-recommendations');
if (!detailsEl) return;
if (!core || core.initializing) {
if (dnsEl) dnsEl.textContent = '—';
if (rpcEl) rpcEl.textContent = '—';
if (reqEl) reqEl.textContent = '—';
if (ackEl) ackEl.textContent = '—';
if (failedEl) failedEl.textContent = '—';
if (failuresEl) failuresEl.textContent = '—';
if (protocolEl) {
protocolEl.classList.add('hidden');
protocolEl.textContent = '';
}
detailsEl.innerHTML = '<p class="theme-text-tertiary text-center">Core RPC stats unavailable — swarm still initializing</p>';
if (recsEl) {
recsEl.classList.add('hidden');
recsEl.innerHTML = '';
}
return;
}
const summary = core.summary || {};
const totalPeers = core.connectedPeers || 0;
const rpcOpen = summary.rpcOpen ?? 0;
const requestOpen = summary.requestChannelOpen ?? 0;
const pendingAcks = summary.pendingAcks ?? (core.pendingInviteAcks?.length || 0);
const failedPeers = summary.failedPeers ?? (core.failedInvitePeers?.length || 0);
const consecutive = core.consecutiveInviteFailures || 0;
const masterQueue = summary.masterQueue ?? 0;
const inFlight = summary.inFlightHandlers ?? 0;
const protocol = core.protocol || {};
if (dnsEl) {
dnsEl.textContent = core.dnsPassInitialized ? 'Ready' : 'Missing';
dnsEl.className = `text-2xl font-bold ${core.dnsPassInitialized ? 'text-green-500' : 'text-red-500'}`;
}
if (rpcEl) {
rpcEl.textContent = `${rpcOpen}/${totalPeers}`;
rpcEl.className = `text-2xl font-bold ${coreStatusColor(totalPeers === 0 || rpcOpen === totalPeers, rpcOpen > 0)}`;
}
if (reqEl) {
reqEl.textContent = `${requestOpen}/${totalPeers}`;
reqEl.className = `text-2xl font-bold ${coreStatusColor(totalPeers === 0 || requestOpen === totalPeers, requestOpen > 0)}`;
}
if (ackEl) {
ackEl.textContent = String(pendingAcks);
ackEl.className = `text-2xl font-bold ${pendingAcks === 0 ? 'text-green-500' : 'text-yellow-500'}`;
}
if (failedEl) {
failedEl.textContent = String(failedPeers);
failedEl.className = `text-2xl font-bold ${failedPeers === 0 ? 'text-green-500' : 'text-red-500'}`;
}
if (failuresEl) {
failuresEl.textContent = String(consecutive);
failuresEl.className = `text-2xl font-bold ${consecutive === 0 ? 'text-green-500' : consecutive >= 3 ? 'text-red-500' : 'text-yellow-500'}`;
}
if (protocolEl) {
const parts = [
core.nodeType ? `node=${core.nodeType}` : null,
protocol.control ? `control=${protocol.control}` : null,
protocol.inviteWire ? `wire=${protocol.inviteWire}` : null,
core.schemaVersion ? `schema=v${core.schemaVersion}` : null
].filter(Boolean);
if (parts.length > 0) {
protocolEl.textContent = parts.join(' · ');
protocolEl.classList.remove('hidden');
} else {
protocolEl.classList.add('hidden');
}
}
const peerEntries = core.peers ? Object.entries(core.peers) : [];
let html = '';
if (masterQueue > 0 || inFlight > 0) {
html += `
<div class="theme-glass rounded-lg p-3 text-sm">
${masterQueue > 0 ? `<span class="text-yellow-500">Master queue: ${masterQueue}</span>` : ''}
${masterQueue > 0 && inFlight > 0 ? ' · ' : ''}
${inFlight > 0 ? `<span class="text-yellow-500">In-flight invite handlers: ${inFlight}</span>` : ''}
</div>
`;
}
if (peerEntries.length === 0) {
html += '<p class="theme-text-tertiary text-center">No connected peers</p>';
} else {
const rows = peerEntries.map(([peerId, p]) => {
const flags = [];
if (p.failedInvite) flags.push('<span class="text-red-400 text-xs">unavailable</span>');
if (p.pendingAck?.waiting) {
flags.push(`<span class="text-yellow-400 text-xs">ack pending${p.pendingAck.retryCount ? ` (+${p.pendingAck.retryCount})` : ''}</span>`);
}
return `
<tr class="border-t border-gray-600/50">
<td class="py-2 pr-2 font-mono text-sm" title="${esc(peerId)}">${esc(coreShortPeerId(peerId))}</td>
<td class="py-2 text-center">${p.connectionOk ? '<i class="fas fa-circle-check text-green-500"></i>' : '<i class="fas fa-circle-xmark text-red-500"></i>'}</td>
<td class="py-2 text-center">${p.requestChannel?.opened ? '<i class="fas fa-circle-check text-green-500"></i>' : '<i class="fas fa-circle text-yellow-500"></i>'}</td>
<td class="py-2 text-center">${p.rpc?.ready ? '<i class="fas fa-circle-check text-green-500"></i>' : '<i class="fas fa-circle text-yellow-500"></i>'}</td>
<td class="py-2 text-xs theme-text-secondary">${flags.join(' ') || '—'}</td>
</tr>
`;
}).join('');
html += `
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="theme-text-tertiary text-xs">
<th class="text-left pb-2">Peer</th>
<th class="text-center pb-2" title="Hyperswarm">Conn</th>
<th class="text-center pb-2" title="p2ns.core-request">Req</th>
<th class="text-center pb-2" title="p2ns.core-request-rpc">RPC</th>
<th class="text-left pb-2">State</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
if (core.pendingInviteAcks?.length > 0) {
html += `
<details class="theme-glass rounded-lg p-3">
<summary class="text-sm font-semibold cursor-pointer">Pending invite.ack (${core.pendingInviteAcks.length})</summary>
<ul class="mt-2 space-y-1 text-xs font-mono theme-text-secondary">
${core.pendingInviteAcks.map((a) => `
<li>${esc(coreShortPeerId(a.peerId))}
${a.inviteId ? ` · id ${esc(String(a.inviteId).slice(0, 16))}` : ''}
· retries ${a.retryCount ?? 0}
</li>
`).join('')}
</ul>
</details>
`;
}
if (core.pendingMasterInviteQueue?.length > 0) {
html += `
<details class="theme-glass rounded-lg p-3">
<summary class="text-sm font-semibold cursor-pointer">Master invite.request queue (${core.pendingMasterInviteQueue.length})</summary>
<ul class="mt-2 space-y-1 text-xs font-mono theme-text-secondary">
${core.pendingMasterInviteQueue.map((q) => `
<li>${esc(coreShortPeerId(q.peerId))} · age ${Math.round((q.ageMs || 0) / 1000)}s · retries ${q.retryCount ?? 0}</li>
`).join('')}
</ul>
</details>
`;
}
if (core.config && Object.keys(core.config).length > 0) {
const c = core.config;
html += `
<details class="theme-glass rounded-lg p-3 text-xs theme-text-secondary">
<summary class="text-sm font-semibold cursor-pointer theme-text-primary">Policy &amp; timeouts</summary>
<ul class="mt-2 space-y-0.5 list-none">
<li>ALLOW_ANY_WRITER_INVITES: <span class="font-mono">${c.allowAnyWriterInvites ? 'true' : 'false'}</span></li>
<li>INVITE_ACK_TIMEOUT: <span class="font-mono">${c.inviteAckTimeoutMs ?? '—'} ms</span></li>
<li>INVITE_HANDLER_TIMEOUT_MS: <span class="font-mono">${c.inviteHandlerTimeoutMs ?? '—'} ms</span></li>
<li>INVITE_RELAY_MAX_HOPS: <span class="font-mono">${c.inviteRelayMaxHops ?? '—'}</span></li>
</ul>
</details>
`;
}
if (protocol.rpcMethods?.length) {
html += `
<details class="theme-glass rounded-lg p-3">
<summary class="text-sm font-semibold cursor-pointer">RPC methods (${protocol.rpcMethods.length})</summary>
<ul class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-0.5 text-xs font-mono text-indigo-300">
${protocol.rpcMethods.map((m) => `<li>${esc(m)}</li>`).join('')}
</ul>
</details>
`;
}
if ((core.connectionIssues || []).length > 0) {
html += `
<details class="theme-glass rounded-lg p-3">
<summary class="text-sm font-semibold cursor-pointer text-orange-400">Connection issues (${core.connectionIssues.length})</summary>
<ul class="mt-2 text-xs font-mono text-orange-300 space-y-0.5">
${core.connectionIssues.map((issue) => `<li>${esc(issue)}</li>`).join('')}
</ul>
</details>
`;
}
detailsEl.innerHTML = html;
if (recsEl) {
if (core.recommendations?.length > 0) {
recsEl.classList.remove('hidden');
recsEl.innerHTML = `
<div class="theme-glass rounded-lg p-3 border-l-4 border-blue-500">
<p class="text-sm font-semibold text-blue-400 mb-2">Recommendations</p>
<ul class="text-xs theme-text-secondary space-y-1">
${core.recommendations.map((rec) => `<li>• ${esc(rec)}</li>`).join('')}
</ul>
</div>
`;
} else {
recsEl.classList.add('hidden');
recsEl.innerHTML = '';
}
}
}
// Render HyperDB stats // Render HyperDB stats
function renderHyperDBStats(hyperdb) { function renderHyperDBStats(hyperdb) {
// Update summary cards // Update summary cards
@@ -365,6 +598,9 @@ function updateStatsDisplay(stats, historical) {
if (resourcesChannels) resourcesChannels.textContent = peerChannelsCount || 0; if (resourcesChannels) resourcesChannels.textContent = peerChannelsCount || 0;
} }
// Core RPC
renderCoreStats(stats.core);
// Process Usage // Process Usage
if (stats.process) { if (stats.process) {
// Process Info // Process Info
@@ -1183,6 +1419,7 @@ window.renderStats = renderStats;
window.updateStatsDisplay = updateStatsDisplay; window.updateStatsDisplay = updateStatsDisplay;
window.renderHolesailChildren = renderHolesailChildren; window.renderHolesailChildren = renderHolesailChildren;
window.renderPeerChannelsStats = renderPeerChannelsStats; window.renderPeerChannelsStats = renderPeerChannelsStats;
window.renderCoreStats = renderCoreStats;
window.renderHyperDBStats = renderHyperDBStats; window.renderHyperDBStats = renderHyperDBStats;
window.renderHyperdriveStats = renderHyperdriveStats; window.renderHyperdriveStats = renderHyperdriveStats;
window.downsampleChartData = downsampleChartData; window.downsampleChartData = downsampleChartData;