Update invite diagnostics for RPC-only core invite flow

Replace legacy invite-channel checks with schema v2 diagnostics:
per-peer RPC/request channel health, pending invite.ack and master
queue state, and an admin UI that matches invite.deliver on
p2ns.core-request-rpc.
This commit is contained in:
Raven Scott
2026-05-28 11:20:08 -04:00
parent 51ad117612
commit 51184d2446
3 changed files with 379 additions and 95 deletions
+199 -47
View File
@@ -892,101 +892,258 @@ async function runInviteDiagnostics() {
}
}
// Display invite diagnostics results
function shortPeerId(peerId) {
if (!peerId || peerId.length <= 16) return peerId || '—';
return peerId.slice(0, 8) + '…' + peerId.slice(-6);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function ratioClass(ok, total) {
if (total === 0) return 'text-gray-400';
return ok === total ? 'text-green-400' : 'text-yellow-400';
}
// Display invite diagnostics results (schema v2 — RPC control plane)
function displayInviteDiagnostics(diagnostics) {
const resultId = 'invite-diagnostics-' + Date.now();
const resultDiv = document.createElement('div');
resultDiv.id = resultId;
resultDiv.className = 'bg-gray-800 rounded-lg p-3 mb-2 border border-gray-600';
const summary = diagnostics.summary || {};
const protocol = diagnostics.protocol || {};
const config = diagnostics.config || {};
const totalPeers = diagnostics.connectedPeers || 0;
const rpcOpen = summary.rpcOpen ?? 0;
const requestOpen = summary.requestChannelOpen ?? 0;
const pendingAcks = summary.pendingAcks ?? (diagnostics.pendingInviteAcks?.length || 0);
const masterQueue = summary.masterQueue ?? (diagnostics.pendingMasterInviteQueue?.length || 0);
const inFlight = summary.inFlightHandlers ?? (diagnostics.inFlightInviteHandlers?.length || 0);
const failedPeers = summary.failedPeers ?? (diagnostics.failedInvitePeers?.length || 0);
const headerDiv = document.createElement('div');
headerDiv.className = 'flex justify-between items-center mb-2';
const titleDiv = document.createElement('div');
titleDiv.className = 'flex items-center';
titleDiv.className = 'flex flex-wrap items-center gap-2';
titleDiv.innerHTML = `
<h4 class="text-base font-semibold text-white">Invite Diagnostics</h4>
<span class="ml-2 text-xs text-gray-400">${new Date(diagnostics.timestamp).toLocaleString()}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-indigo-900 text-indigo-200">RPC v${diagnostics.schemaVersion || 1}</span>
<span class="text-xs text-gray-400">${new Date(diagnostics.timestamp).toLocaleString()}</span>
`;
const buttonContainer = document.createElement('div');
buttonContainer.className = 'flex space-x-1';
headerDiv.appendChild(titleDiv);
headerDiv.appendChild(buttonContainer);
const contentDiv = document.createElement('div');
contentDiv.className = 'space-y-2';
// Compact status summary
const statusItems = [
{ label: 'Type', value: diagnostics.nodeType, color: 'text-white' },
{ label: 'DNS Pass', value: diagnostics.dnsPassInitialized ? 'Yes' : 'No', color: diagnostics.dnsPassInitialized ? 'text-green-400' : 'text-red-400' },
{ label: 'Peers', value: `${diagnostics.connectedPeers} connected`, color: 'text-white' }
{ label: 'Node', value: diagnostics.nodeType, color: 'text-white' },
{ label: 'DNS Pass', value: diagnostics.dnsPassInitialized ? 'Ready' : 'Missing', color: diagnostics.dnsPassInitialized ? 'text-green-400' : 'text-red-400' },
{ label: 'Peers', value: String(totalPeers), color: 'text-white' },
{ label: 'Control', value: protocol.control || 'p2ns.core-request-rpc', color: 'text-indigo-300' }
];
contentDiv.innerHTML += `
<div class="grid grid-cols-3 gap-2 text-sm">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-sm">
${statusItems.map(item => `
<div class="bg-gray-700 rounded px-2 py-1">
<div class="text-gray-400 text-xs">${item.label}</div>
<div class="font-semibold ${item.color}">${item.value}</div>
<div class="font-semibold ${item.color} truncate" title="${escapeHtml(item.value)}">${escapeHtml(item.value)}</div>
</div>
`).join('')}
</div>
`;
// Alerts and issues in compact format
if (protocol.inviteWire || protocol.legacyInviteChannel === false) {
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2 text-xs text-gray-300">
<span class="text-gray-400">Invite wire:</span>
<code class="text-indigo-300">${escapeHtml(protocol.inviteWire || 'invite.deliver')}</code>
<span class="text-gray-500 mx-1">·</span>
<span class="text-gray-400">Lifecycle:</span>
<code class="text-gray-200">${escapeHtml(protocol.lifecycleChannel || 'p2ns.core-request')}</code>
${protocol.legacyInviteChannel === false ? '<span class="ml-2 text-gray-500">(no p2ns.core-invite channel)</span>' : ''}
</div>
`;
}
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2">
<div class="text-xs text-gray-400 mb-1">RPC &amp; channel health</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
<div class="text-gray-300">RPC open: <span class="${ratioClass(rpcOpen, totalPeers)} font-semibold">${rpcOpen}/${totalPeers}</span></div>
<div class="text-gray-300">Request ch.: <span class="${ratioClass(requestOpen, totalPeers)} font-semibold">${requestOpen}/${totalPeers}</span></div>
<div class="text-gray-300">Pending ack: <span class="${pendingAcks > 0 ? 'text-yellow-400' : 'text-green-400'} font-semibold">${pendingAcks}</span></div>
<div class="text-gray-300">Failed peers: <span class="${failedPeers > 0 ? 'text-red-400' : 'text-green-400'} font-semibold">${failedPeers}</span></div>
</div>
${(masterQueue > 0 || inFlight > 0) ? `
<div class="mt-1 text-xs text-gray-400">
${masterQueue > 0 ? `<span class="text-yellow-300">Master queue: ${masterQueue}</span>` : ''}
${masterQueue > 0 && inFlight > 0 ? ' · ' : ''}
${inFlight > 0 ? `<span class="text-yellow-300">In-flight handlers: ${inFlight}</span>` : ''}
</div>
` : ''}
</div>
`;
const alerts = [];
if (diagnostics.failedInvitePeers.length > 0) {
alerts.push(`<div class="text-red-300 text-xs">❌ ${diagnostics.failedInvitePeers.length} failed peers</div>`);
if (failedPeers > 0) {
alerts.push(`<div class="text-red-300 text-xs">❌ ${failedPeers} peer(s) cannot supply invites</div>`);
}
if (diagnostics.pendingInviteAcks.length > 0) {
alerts.push(`<div class="text-yellow-300 text-xs">⏳ ${diagnostics.pendingInviteAcks.length} pending ACKs</div>`);
if (pendingAcks > 0) {
alerts.push(`<div class="text-yellow-300 text-xs">⏳ ${pendingAcks} invite.deliver awaiting invite.ack</div>`);
}
if (diagnostics.consecutiveInviteFailures > 0) {
const severity = diagnostics.consecutiveInviteFailures >= 3 ? 'text-red-300' : 'text-orange-300';
const icon = diagnostics.consecutiveInviteFailures >= 3 ? '🚨' : '⚠️';
alerts.push(`<div class="${severity} text-xs">${icon} ${diagnostics.consecutiveInviteFailures} consecutive failures</div>`);
if (masterQueue > 0) {
alerts.push(`<div class="text-yellow-300 text-xs">📋 ${masterQueue} queued invite.request (master not ready)</div>`);
}
if (diagnostics.connectionIssues.length > 0) {
alerts.push(`<div class="text-orange-300 text-xs">🔌 ${diagnostics.connectionIssues.length} connection issues</div>`);
if (inFlight > 0) {
alerts.push(`<div class="text-yellow-300 text-xs">⚙️ ${inFlight} in-flight invite.request handler(s)</div>`);
}
if ((diagnostics.consecutiveInviteFailures || 0) > 0) {
const n = diagnostics.consecutiveInviteFailures;
const severity = n >= 3 ? 'text-red-300' : 'text-orange-300';
const icon = n >= 3 ? '🚨' : '⚠️';
alerts.push(`<div class="${severity} text-xs">${icon} ${n} consecutive invite.deliver failure(s)</div>`);
}
if ((diagnostics.connectionIssues || []).length > 0) {
alerts.push(`<div class="text-orange-300 text-xs">🔌 ${diagnostics.connectionIssues.length} swarm connection issue(s)</div>`);
}
if (totalPeers > 0 && rpcOpen < totalPeers) {
alerts.push(`<div class="text-orange-300 text-xs">📡 ${totalPeers - rpcOpen} peer(s) without open RPC</div>`);
}
if (alerts.length > 0) {
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2">
<div class="text-xs text-gray-400 mb-1">Issues:</div>
<div class="text-xs text-gray-400 mb-1">Issues</div>
<div class="space-y-1">${alerts.join('')}</div>
</div>
`;
}
// Channel health in compact format
const healthyChannels = Object.values(diagnostics.inviteChannels).filter(ch => ch.opened).length;
const totalPeers = diagnostics.connectedPeers;
const peerEntries = diagnostics.peers ? Object.entries(diagnostics.peers) : [];
if (peerEntries.length > 0) {
const rows = peerEntries.map(([peerId, p]) => {
const rpcOk = p.rpc?.ready;
const reqOk = p.requestChannel?.opened;
const connOk = p.connectionOk;
const ack = p.pendingAck;
let flags = '';
if (p.failedInvite) flags += '<span class="text-red-400" title="invite unavailable">✗inv</span> ';
if (ack?.waiting) flags += `<span class="text-yellow-400" title="pending ack">⏳ack${ack.retryCount ? '+' + ack.retryCount : ''}</span> `;
return `
<tr class="border-t border-gray-600">
<td class="py-1 pr-2 font-mono text-gray-300" title="${escapeHtml(peerId)}">${escapeHtml(shortPeerId(peerId))}</td>
<td class="py-1 text-center">${connOk ? '<span class="text-green-400">✓</span>' : '<span class="text-red-400">✗</span>'}</td>
<td class="py-1 text-center">${reqOk ? '<span class="text-green-400">✓</span>' : '<span class="text-yellow-400">○</span>'}</td>
<td class="py-1 text-center">${rpcOk ? '<span class="text-green-400">✓</span>' : '<span class="text-yellow-400">○</span>'}</td>
<td class="py-1 text-xs text-gray-400">${flags || '—'}</td>
</tr>
`;
}).join('');
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2">
<div class="text-xs text-gray-400 mb-1">Channel Health:</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="text-gray-300">Invite: <span class="${healthyChannels === totalPeers ? 'text-green-400' : 'text-yellow-400'} font-semibold">${healthyChannels}/${totalPeers}</span></div>
<div class="text-gray-300">Request: <span class="${healthyChannels === totalPeers ? 'text-green-400' : 'text-yellow-400'} font-semibold">${healthyChannels}/${totalPeers}</span></div>
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2 overflow-x-auto">
<div class="text-xs text-gray-400 mb-1">Per-peer</div>
<table class="w-full text-xs">
<thead>
<tr class="text-gray-500">
<th class="text-left pb-1">Peer</th>
<th class="text-center pb-1" title="Hyperswarm connection">Conn</th>
<th class="text-center pb-1" title="p2ns.core-request">Req</th>
<th class="text-center pb-1" title="p2ns.core-request-rpc">RPC</th>
<th class="text-left pb-1">Flags</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
`;
`;
}
// Compact recommendations
if (diagnostics.recommendations.length > 0) {
if (diagnostics.pendingInviteAcks?.length > 0) {
const ackRows = diagnostics.pendingInviteAcks.map((a) => `
<li class="font-mono text-gray-300">${escapeHtml(shortPeerId(a.peerId))}
${a.inviteId ? `<span class="text-gray-500">id=${escapeHtml(String(a.inviteId).slice(0, 12))}…</span>` : ''}
<span class="text-gray-500">retries=${a.retryCount ?? 0}</span>
</li>
`).join('');
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2 text-xs">
<div class="text-gray-400 mb-1">Pending invite.ack</div>
<ul class="space-y-0.5">${ackRows}</ul>
</div>
`;
}
if (diagnostics.pendingMasterInviteQueue?.length > 0) {
const qRows = diagnostics.pendingMasterInviteQueue.map((q) => `
<li class="font-mono text-gray-300">${escapeHtml(shortPeerId(q.peerId))}
<span class="text-gray-500">age=${Math.round((q.ageMs || 0) / 1000)}s retries=${q.retryCount ?? 0}</span>
</li>
`).join('');
contentDiv.innerHTML += `
<div class="bg-gray-700 rounded p-2 text-xs">
<div class="text-gray-400 mb-1">Master invite.request queue</div>
<ul class="space-y-0.5">${qRows}</ul>
</div>
`;
}
if (Object.keys(config).length > 0) {
contentDiv.innerHTML += `
<details class="bg-gray-700 rounded p-2 text-xs">
<summary class="text-gray-400 cursor-pointer">Timeouts &amp; policy</summary>
<ul class="mt-1 text-gray-300 space-y-0.5 list-none">
<li>INVITE_ACK_TIMEOUT: ${config.inviteAckTimeoutMs ?? '—'} ms</li>
<li>INVITE_HANDLER_TIMEOUT_MS: ${config.inviteHandlerTimeoutMs ?? '—'} ms</li>
<li>INVITE_RELAY_MAX_HOPS: ${config.inviteRelayMaxHops ?? '—'}</li>
<li>ALLOW_ANY_WRITER_INVITES: ${config.allowAnyWriterInvites ? 'true' : 'false'}</li>
</ul>
</details>
`;
}
if ((diagnostics.connectionIssues || []).length > 0) {
contentDiv.innerHTML += `
<details class="bg-gray-700 rounded p-2 text-xs">
<summary class="text-gray-400 cursor-pointer">Connection issues (${diagnostics.connectionIssues.length})</summary>
<ul class="mt-1 text-orange-300 space-y-0.5 font-mono">
${diagnostics.connectionIssues.map((issue) => `<li>${escapeHtml(issue)}</li>`).join('')}
</ul>
</details>
`;
}
if (protocol.rpcMethods?.length) {
contentDiv.innerHTML += `
<details class="bg-gray-700 rounded p-2 text-xs">
<summary class="text-gray-400 cursor-pointer">RPC methods (${protocol.rpcMethods.length})</summary>
<p class="mt-1 text-gray-500">Registered on p2ns.core-request-rpc</p>
<ul class="mt-1 text-indigo-300 font-mono columns-2 gap-x-4">
${protocol.rpcMethods.map((m) => `<li>${escapeHtml(m)}</li>`).join('')}
</ul>
</details>
`;
}
if (diagnostics.recommendations?.length > 0) {
contentDiv.innerHTML += `
<div class="bg-blue-900 border border-blue-600 rounded p-2">
<div class="text-xs text-blue-400 mb-1">💡 Recommendations:</div>
<div class="text-xs text-blue-400 mb-1">Recommendations</div>
<ul class="text-xs text-blue-300 space-y-0.5">
${diagnostics.recommendations.map(rec => `<li>• ${rec}</li>`).join('')}
${diagnostics.recommendations.map((rec) => `<li>• ${escapeHtml(rec)}</li>`).join('')}
</ul>
</div>
`;
@@ -995,17 +1152,12 @@ function displayInviteDiagnostics(diagnostics) {
resultDiv.appendChild(headerDiv);
resultDiv.appendChild(contentDiv);
// Add close button
const closeBtn = document.createElement('button');
closeBtn.id = `close-btn-${resultId}`;
closeBtn.className = 'px-1.5 py-0.5 text-xs bg-gray-500 text-white rounded hover:bg-gray-600';
closeBtn.textContent = '×';
closeBtn.addEventListener('click', () => {
closeDiagnosticResult(resultId);
});
closeBtn.addEventListener('click', () => closeDiagnosticResult(resultId));
buttonContainer.appendChild(closeBtn);
// Add to diagnostics results container
const resultsContainer = document.getElementById('diagnostics-results');
if (resultsContainer) {
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);