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:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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 & 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 & 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);
|
||||
|
||||
Reference in New Issue
Block a user