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:
@@ -304,6 +304,17 @@ async function handleStatsRoutes(req, res) {
|
||||
// Collect Hyperdrive stats
|
||||
const hyperdriveStats = collectHyperdriveStats();
|
||||
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;
|
||||
trackRequestWithTiming('/api/stats', true, responseTime);
|
||||
|
||||
@@ -653,6 +653,46 @@
|
||||
</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 -->
|
||||
<div class="theme-table rounded-lg p-6 mb-6">
|
||||
<h3 class="text-xl font-bold mb-4">Peer Channels</h3>
|
||||
|
||||
@@ -110,6 +110,239 @@ function renderPeerChannelsStats(peerChannels) {
|
||||
}).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 & 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
|
||||
function renderHyperDBStats(hyperdb) {
|
||||
// Update summary cards
|
||||
@@ -364,6 +597,9 @@ function updateStatsDisplay(stats, historical) {
|
||||
const peerChannelsCount = stats.peers?.current || 0;
|
||||
if (resourcesChannels) resourcesChannels.textContent = peerChannelsCount || 0;
|
||||
}
|
||||
|
||||
// Core RPC
|
||||
renderCoreStats(stats.core);
|
||||
|
||||
// Process Usage
|
||||
if (stats.process) {
|
||||
@@ -1183,6 +1419,7 @@ window.renderStats = renderStats;
|
||||
window.updateStatsDisplay = updateStatsDisplay;
|
||||
window.renderHolesailChildren = renderHolesailChildren;
|
||||
window.renderPeerChannelsStats = renderPeerChannelsStats;
|
||||
window.renderCoreStats = renderCoreStats;
|
||||
window.renderHyperDBStats = renderHyperDBStats;
|
||||
window.renderHyperdriveStats = renderHyperdriveStats;
|
||||
window.downsampleChartData = downsampleChartData;
|
||||
|
||||
Reference in New Issue
Block a user