forked from snxraven/p2ns
Invite Diagnostics + Cleanup and Reboot
This commit is contained in:
@@ -8,7 +8,7 @@ const { trackRequest, trackRequestWithTiming } = require('../../../maintenance/m
|
|||||||
const { createErrorResponse } = require('../../../infrastructure/error_handler');
|
const { createErrorResponse } = require('../../../infrastructure/error_handler');
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const { state } = require('../../../infrastructure/state');
|
const state = require('../../../infrastructure/state');
|
||||||
|
|
||||||
async function handleDiagnosticsRoutes(req, res) {
|
async function handleDiagnosticsRoutes(req, res) {
|
||||||
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
||||||
@@ -508,7 +508,7 @@ async function handleDiagnosticsRoutes(req, res) {
|
|||||||
try {
|
try {
|
||||||
if (!state.diagnoseInviteIssues) {
|
if (!state.diagnoseInviteIssues) {
|
||||||
res.writeHead(503, { 'Content-Type': 'application/json' });
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: 'Invite diagnostics not available - system not fully initialized' }));
|
res.end(JSON.stringify({ error: 'Invite diagnostics not available - system still initializing' }));
|
||||||
trackRequest(urlPath, false);
|
trackRequest(urlPath, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,6 +515,68 @@ async function handleSettingsRoutes(req, res) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/admin/clean-dns-storage - Clean DNS Pass storage and reset initialization
|
||||||
|
if (method === 'POST' && urlPath === '/api/admin/clean-dns-storage') {
|
||||||
|
try {
|
||||||
|
const { logInfo, logWarn, logError } = require('../../../infrastructure/logger');
|
||||||
|
|
||||||
|
// Safety check: warn if there are active connections
|
||||||
|
if (state.connectedPeers && state.connectedPeers.size > 0) {
|
||||||
|
logWarn('Admin', `Cleaning storage while ${state.connectedPeers.size} peers are connected - this may disrupt the network`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check: warn if this is a master node with initialized DNS pass
|
||||||
|
if (state.isMaster && state.dnsPass) {
|
||||||
|
logWarn('Admin', 'Cleaning storage on a master node that has initialized DNS pass - other nodes may be affected');
|
||||||
|
}
|
||||||
|
|
||||||
|
logWarn('Admin', 'Cleaning DNS Pass storage as requested by user');
|
||||||
|
|
||||||
|
// Get storage directory
|
||||||
|
const storageDir = process.env.STORAGE_DIR || './my-storage';
|
||||||
|
|
||||||
|
// Clean the storage directory (same logic as --clean flag)
|
||||||
|
try {
|
||||||
|
await fs.rm(storageDir, { recursive: true, force: true });
|
||||||
|
logInfo('Admin', `Cleaned DNS Pass storage directory: ${storageDir}`);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore if directory doesn't exist
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state variables that depend on the storage
|
||||||
|
state.dnsPass = null;
|
||||||
|
state.consecutiveInviteFailures = 0; // Reset failure counter
|
||||||
|
|
||||||
|
// Note: We don't reset the keypair or other persistent state, only the DNS Pass data
|
||||||
|
|
||||||
|
logInfo('Admin', 'DNS Pass storage cleaned and state reset successfully');
|
||||||
|
|
||||||
|
// Send success response before shutting down
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'DNS Pass storage cleaned. Shutting down for reinitialization...',
|
||||||
|
storageDir: storageDir,
|
||||||
|
shutdownInitiated: true,
|
||||||
|
restartInstructions: 'Process will shut down. Restart manually with: sudo node p2ns.js' + (state.isMaster ? ' --master' : '')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Trigger graceful shutdown after sending response
|
||||||
|
setTimeout(() => {
|
||||||
|
logInfo('Admin', 'Initiating graceful shutdown after DNS storage cleanup...');
|
||||||
|
process.emit('SIGTERM');
|
||||||
|
}, 1000); // Give time for response to be sent
|
||||||
|
} catch (err) {
|
||||||
|
logError('Admin', `Failed to clean DNS storage: ${err.message}`);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: `Failed to clean DNS storage: ${err.message}` }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ async function handleStatusRoutes(req, res) {
|
|||||||
const status = {
|
const status = {
|
||||||
isMaster: state.isMaster,
|
isMaster: state.isMaster,
|
||||||
isConnected: !!state.dnsPass,
|
isConnected: !!state.dnsPass,
|
||||||
peersCount: state.connectedPeers.size
|
peersCount: state.connectedPeers.size,
|
||||||
|
pid: process.pid
|
||||||
};
|
};
|
||||||
trackRequest('/api/status', true);
|
trackRequest('/api/status', true);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
@@ -878,7 +878,10 @@
|
|||||||
<h4 class="text-lg font-semibold mb-4">Invite Diagnostics</h4>
|
<h4 class="text-lg font-semibold mb-4">Invite Diagnostics</h4>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<p class="text-sm text-gray-400">Diagnose invite system issues including channel health, peer connections, and pending acknowledgments.</p>
|
<p class="text-sm text-gray-400">Diagnose invite system issues including channel health, peer connections, and pending acknowledgments.</p>
|
||||||
<button onclick="runInviteDiagnostics()" class="w-full px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Run Invite Diagnostics</button>
|
<div class="flex space-x-2">
|
||||||
|
<button onclick="runInviteDiagnostics()" class="flex-1 px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Run Invite Diagnostics</button>
|
||||||
|
<button onclick="cleanDnsPassStorage()" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">🗑️ Clean & Restart</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -886,7 +889,7 @@
|
|||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="text-lg font-semibold mb-4">Results</h4>
|
<h4 class="text-lg font-semibold mb-4">Results</h4>
|
||||||
<div id="diagnostics-results" class="space-y-2 max-h-96 overflow-y-auto"></div>
|
<div id="diagnostics-results" class="space-y-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -667,9 +667,204 @@ function closeDiagnosticResult(resultId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run invite diagnostics
|
// Run invite diagnostics
|
||||||
|
// Clean DNS Pass storage
|
||||||
|
async function cleanDnsPassStorage() {
|
||||||
|
const buttonEl = document.querySelector('button[onclick="cleanDnsPassStorage()"]');
|
||||||
|
|
||||||
|
// Fetch current status to provide better warnings
|
||||||
|
let statusInfo = {};
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status');
|
||||||
|
if (response.ok) {
|
||||||
|
statusInfo = await response.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not fetch status for confirmation dialog:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let warningMessage = '⚠️ WARNING: This will delete all DNS Pass data and immediately shut down the system for reinitialization.\n\n';
|
||||||
|
|
||||||
|
if (statusInfo.peersCount > 0) {
|
||||||
|
warningMessage += `• ${statusInfo.peersCount} peers are currently connected - they will be disconnected\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusInfo.isMaster && statusInfo.isConnected) {
|
||||||
|
warningMessage += '• This is a master node with active connections - other nodes will lose connectivity\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
warningMessage += '\n• The system will shut down immediately after cleanup\n• You must manually restart P2NS\n• This action cannot be undone\n\nContinue?';
|
||||||
|
|
||||||
|
if (!confirm(warningMessage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
if (buttonEl) {
|
||||||
|
buttonEl.disabled = true;
|
||||||
|
buttonEl.innerHTML = '<span class="inline-block animate-spin rounded-full h-4 w-4 border-t-2 border-white mr-2"></span>Cleaning & Shutting Down...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/clean-dns-storage', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (window.showNotification) {
|
||||||
|
window.showNotification('🧹 DNS Pass storage cleaned. System shutting down for reinitialization...', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing invite diagnostics results since the state has changed
|
||||||
|
clearInviteDiagnosticsResults();
|
||||||
|
|
||||||
|
// Show shutdown message
|
||||||
|
showShutdownMessage(result);
|
||||||
|
|
||||||
|
// The system will shut down, so the connection will be lost
|
||||||
|
// This is expected behavior
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Clean DNS storage failed:', err);
|
||||||
|
if (window.showNotification) window.showNotification('Failed to clean DNS storage: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
// Restore button
|
||||||
|
if (buttonEl) {
|
||||||
|
buttonEl.disabled = false;
|
||||||
|
buttonEl.innerHTML = '🗑️ Clean DNS Storage';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing invite diagnostics results
|
||||||
|
function clearInviteDiagnosticsResults() {
|
||||||
|
const resultsContainer = document.getElementById('diagnostics-results');
|
||||||
|
if (resultsContainer) {
|
||||||
|
// Remove all invite diagnostics results (those with IDs starting with 'invite-diagnostics-')
|
||||||
|
const inviteResults = resultsContainer.querySelectorAll('[id^="invite-diagnostics-"]');
|
||||||
|
inviteResults.forEach(result => result.remove());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show restart required message prominently
|
||||||
|
async function showRestartRequiredMessage() {
|
||||||
|
const resultId = 'restart-required-' + Date.now();
|
||||||
|
|
||||||
|
// Fetch current status to get PID and master status
|
||||||
|
let pid = 'PID';
|
||||||
|
let isMaster = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status');
|
||||||
|
if (response.ok) {
|
||||||
|
const status = await response.json();
|
||||||
|
pid = status.pid || 'PID';
|
||||||
|
isMaster = status.isMaster || false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not fetch status for restart message:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultDiv = document.createElement('div');
|
||||||
|
resultDiv.id = resultId;
|
||||||
|
resultDiv.className = 'bg-orange-900 border border-orange-600 rounded-lg p-3 mb-2 border-l-4 border-l-orange-500';
|
||||||
|
|
||||||
|
const masterFlag = isMaster ? '--master' : '';
|
||||||
|
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-xl">⚠️</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<h4 class="text-base font-semibold text-orange-200 mb-1">System Restart Required</h4>
|
||||||
|
<p class="text-orange-100 text-sm mb-2">
|
||||||
|
DNS Pass storage cleaned. Process must restart for reinitialization.
|
||||||
|
</p>
|
||||||
|
<div class="bg-orange-800 rounded p-2 mb-2">
|
||||||
|
<p class="text-xs text-orange-200 font-mono">
|
||||||
|
sudo kill ${pid} && sudo node p2ns.js ${masterFlag}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<button onclick="hideRestartMessage('${resultId}')" class="px-2 py-0.5 bg-orange-700 text-orange-100 rounded hover:bg-orange-600 text-xs">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add to diagnostics results container at the top
|
||||||
|
const resultsContainer = document.getElementById('diagnostics-results');
|
||||||
|
if (resultsContainer) {
|
||||||
|
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show shutdown message
|
||||||
|
function showShutdownMessage(result) {
|
||||||
|
const resultId = 'shutdown-message-' + Date.now();
|
||||||
|
const resultDiv = document.createElement('div');
|
||||||
|
resultDiv.id = resultId;
|
||||||
|
resultDiv.className = 'bg-red-900 border border-red-600 rounded-lg p-3 mb-2 border-l-4 border-l-red-500';
|
||||||
|
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-xl">🔄</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<h4 class="text-base font-semibold text-red-200 mb-1">System Shutting Down</h4>
|
||||||
|
<p class="text-red-100 text-sm mb-2">
|
||||||
|
DNS Pass storage cleaned. Process shutting down for reinitialization.
|
||||||
|
</p>
|
||||||
|
<div class="bg-red-800 rounded p-2 mb-2">
|
||||||
|
<p class="text-xs text-red-200 font-mono">
|
||||||
|
${result.restartInstructions || 'sudo node p2ns.js' + (window.p2nsIsMaster ? ' --master' : '')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<button onclick="hideShutdownMessage('${resultId}')" class="px-2 py-0.5 bg-red-700 text-red-100 rounded hover:bg-red-600 text-xs">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add to diagnostics results container at the top
|
||||||
|
const resultsContainer = document.getElementById('diagnostics-results');
|
||||||
|
if (resultsContainer) {
|
||||||
|
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide shutdown message
|
||||||
|
function hideShutdownMessage(resultId) {
|
||||||
|
const resultDiv = document.getElementById(resultId);
|
||||||
|
if (resultDiv) {
|
||||||
|
resultDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide restart message
|
||||||
|
function hideRestartMessage(resultId) {
|
||||||
|
const resultDiv = document.getElementById(resultId);
|
||||||
|
if (resultDiv) {
|
||||||
|
resultDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runInviteDiagnostics() {
|
async function runInviteDiagnostics() {
|
||||||
const buttonEl = document.querySelector('button[onclick="runInviteDiagnostics()"]');
|
const buttonEl = document.querySelector('button[onclick="runInviteDiagnostics()"]');
|
||||||
|
|
||||||
|
// Clear previous invite diagnostics results before running new test
|
||||||
|
clearInviteDiagnosticsResults();
|
||||||
|
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
if (buttonEl) {
|
if (buttonEl) {
|
||||||
buttonEl.disabled = true;
|
buttonEl.disabled = true;
|
||||||
@@ -702,111 +897,95 @@ function displayInviteDiagnostics(diagnostics) {
|
|||||||
const resultId = 'invite-diagnostics-' + Date.now();
|
const resultId = 'invite-diagnostics-' + Date.now();
|
||||||
const resultDiv = document.createElement('div');
|
const resultDiv = document.createElement('div');
|
||||||
resultDiv.id = resultId;
|
resultDiv.id = resultId;
|
||||||
resultDiv.className = 'bg-gray-800 rounded-lg p-4 mb-4 border border-gray-600';
|
resultDiv.className = 'bg-gray-800 rounded-lg p-3 mb-2 border border-gray-600';
|
||||||
|
|
||||||
const headerDiv = document.createElement('div');
|
const headerDiv = document.createElement('div');
|
||||||
headerDiv.className = 'flex justify-between items-center mb-3';
|
headerDiv.className = 'flex justify-between items-center mb-2';
|
||||||
|
|
||||||
const titleDiv = document.createElement('div');
|
const titleDiv = document.createElement('div');
|
||||||
titleDiv.className = 'flex items-center';
|
titleDiv.className = 'flex items-center';
|
||||||
titleDiv.innerHTML = `
|
titleDiv.innerHTML = `
|
||||||
<h3 class="text-lg font-semibold text-white">Invite Diagnostics</h3>
|
<h4 class="text-base font-semibold text-white">Invite Diagnostics</h4>
|
||||||
<span class="ml-2 text-sm text-gray-400">${new Date(diagnostics.timestamp).toLocaleString()}</span>
|
<span class="ml-2 text-xs text-gray-400">${new Date(diagnostics.timestamp).toLocaleString()}</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const buttonContainer = document.createElement('div');
|
const buttonContainer = document.createElement('div');
|
||||||
buttonContainer.className = 'flex space-x-2';
|
buttonContainer.className = 'flex space-x-1';
|
||||||
|
|
||||||
headerDiv.appendChild(titleDiv);
|
headerDiv.appendChild(titleDiv);
|
||||||
headerDiv.appendChild(buttonContainer);
|
headerDiv.appendChild(buttonContainer);
|
||||||
|
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.className = 'space-y-3';
|
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' }
|
||||||
|
];
|
||||||
|
|
||||||
// Basic info
|
|
||||||
contentDiv.innerHTML += `
|
contentDiv.innerHTML += `
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||||
<div class="bg-gray-700 rounded p-3">
|
${statusItems.map(item => `
|
||||||
<div class="text-sm text-gray-400">Node Type</div>
|
<div class="bg-gray-700 rounded px-2 py-1">
|
||||||
<div class="text-lg font-semibold text-white">${diagnostics.nodeType}</div>
|
<div class="text-gray-400 text-xs">${item.label}</div>
|
||||||
</div>
|
<div class="font-semibold ${item.color}">${item.value}</div>
|
||||||
<div class="bg-gray-700 rounded p-3">
|
|
||||||
<div class="text-sm text-gray-400">DNS Pass Initialized</div>
|
|
||||||
<div class="text-lg font-semibold ${diagnostics.dnsPassInitialized ? 'text-green-400' : 'text-red-400'}">
|
|
||||||
${diagnostics.dnsPassInitialized ? 'Yes' : 'No'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Peer connections
|
// Alerts and issues in compact format
|
||||||
contentDiv.innerHTML += `
|
const alerts = [];
|
||||||
<div class="bg-gray-700 rounded p-3">
|
|
||||||
<div class="text-sm text-gray-400 mb-2">Peer Connections</div>
|
|
||||||
<div class="text-lg font-semibold text-white">${diagnostics.connectedPeers} connected</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Failed peers
|
|
||||||
if (diagnostics.failedInvitePeers.length > 0) {
|
if (diagnostics.failedInvitePeers.length > 0) {
|
||||||
contentDiv.innerHTML += `
|
alerts.push(`<div class="text-red-300 text-xs">❌ ${diagnostics.failedInvitePeers.length} failed peers</div>`);
|
||||||
<div class="bg-red-900 border border-red-600 rounded p-3">
|
|
||||||
<div class="text-sm text-red-400 mb-2">Failed Invite Peers</div>
|
|
||||||
<div class="text-sm text-red-300">${diagnostics.failedInvitePeers.join(', ')}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pending ACKs
|
|
||||||
if (diagnostics.pendingInviteAcks.length > 0) {
|
if (diagnostics.pendingInviteAcks.length > 0) {
|
||||||
contentDiv.innerHTML += `
|
alerts.push(`<div class="text-yellow-300 text-xs">⏳ ${diagnostics.pendingInviteAcks.length} pending ACKs</div>`);
|
||||||
<div class="bg-yellow-900 border border-yellow-600 rounded p-3">
|
}
|
||||||
<div class="text-sm text-yellow-400 mb-2">Pending Invite Acknowledgments</div>
|
|
||||||
<div class="text-sm text-yellow-300">${diagnostics.pendingInviteAcks.length} pending</div>
|
if (diagnostics.consecutiveInviteFailures > 0) {
|
||||||
</div>
|
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>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection issues
|
|
||||||
if (diagnostics.connectionIssues.length > 0) {
|
if (diagnostics.connectionIssues.length > 0) {
|
||||||
|
alerts.push(`<div class="text-orange-300 text-xs">🔌 ${diagnostics.connectionIssues.length} connection issues</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alerts.length > 0) {
|
||||||
contentDiv.innerHTML += `
|
contentDiv.innerHTML += `
|
||||||
<div class="bg-orange-900 border border-orange-600 rounded p-3">
|
<div class="bg-gray-700 rounded p-2">
|
||||||
<div class="text-sm text-orange-400 mb-2">Connection Issues</div>
|
<div class="text-xs text-gray-400 mb-1">Issues:</div>
|
||||||
<div class="text-sm text-orange-300">${diagnostics.connectionIssues.join('<br>')}</div>
|
<div class="space-y-1">${alerts.join('')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel health summary
|
// Channel health in compact format
|
||||||
const healthyInviteChannels = Object.values(diagnostics.inviteChannels).filter(ch => ch.opened).length;
|
const healthyChannels = Object.values(diagnostics.inviteChannels).filter(ch => ch.opened).length;
|
||||||
const healthyRequestChannels = Object.values(diagnostics.requestChannels).filter(ch => ch.opened).length;
|
const totalPeers = diagnostics.connectedPeers;
|
||||||
|
|
||||||
contentDiv.innerHTML += `
|
contentDiv.innerHTML += `
|
||||||
<div class="bg-gray-700 rounded p-3">
|
<div class="bg-gray-700 rounded p-2">
|
||||||
<div class="text-sm text-gray-400 mb-2">Channel Health</div>
|
<div class="text-xs text-gray-400 mb-1">Channel Health:</div>
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div>
|
<div class="text-gray-300">Invite: <span class="${healthyChannels === totalPeers ? 'text-green-400' : 'text-yellow-400'} font-semibold">${healthyChannels}/${totalPeers}</span></div>
|
||||||
<span class="text-gray-300">Invite Channels:</span>
|
<div class="text-gray-300">Request: <span class="${healthyChannels === totalPeers ? 'text-green-400' : 'text-yellow-400'} font-semibold">${healthyChannels}/${totalPeers}</span></div>
|
||||||
<span class="ml-2 font-semibold ${healthyInviteChannels === diagnostics.connectedPeers ? 'text-green-400' : 'text-yellow-400'}">
|
|
||||||
${healthyInviteChannels}/${diagnostics.connectedPeers} healthy
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-300">Request Channels:</span>
|
|
||||||
<span class="ml-2 font-semibold ${healthyRequestChannels === diagnostics.connectedPeers ? 'text-green-400' : 'text-yellow-400'}">
|
|
||||||
${healthyRequestChannels}/${diagnostics.connectedPeers} healthy
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Recommendations
|
// Compact recommendations
|
||||||
if (diagnostics.recommendations.length > 0) {
|
if (diagnostics.recommendations.length > 0) {
|
||||||
contentDiv.innerHTML += `
|
contentDiv.innerHTML += `
|
||||||
<div class="bg-blue-900 border border-blue-600 rounded p-3">
|
<div class="bg-blue-900 border border-blue-600 rounded p-2">
|
||||||
<div class="text-sm text-blue-400 mb-2">Recommendations</div>
|
<div class="text-xs text-blue-400 mb-1">💡 Recommendations:</div>
|
||||||
<ul class="text-sm text-blue-300 space-y-1">
|
<ul class="text-xs text-blue-300 space-y-0.5">
|
||||||
${diagnostics.recommendations.map(rec => `<li>• ${rec}</li>`).join('')}
|
${diagnostics.recommendations.map(rec => `<li>• ${rec}</li>`).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -819,8 +998,8 @@ function displayInviteDiagnostics(diagnostics) {
|
|||||||
// Add close button
|
// Add close button
|
||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.id = `close-btn-${resultId}`;
|
closeBtn.id = `close-btn-${resultId}`;
|
||||||
closeBtn.className = 'ml-2 px-2 py-1 text-xs bg-gray-500 text-white rounded hover:bg-gray-600';
|
closeBtn.className = 'px-1.5 py-0.5 text-xs bg-gray-500 text-white rounded hover:bg-gray-600';
|
||||||
closeBtn.textContent = 'Close';
|
closeBtn.textContent = '×';
|
||||||
closeBtn.addEventListener('click', () => {
|
closeBtn.addEventListener('click', () => {
|
||||||
closeDiagnosticResult(resultId);
|
closeDiagnosticResult(resultId);
|
||||||
});
|
});
|
||||||
@@ -831,9 +1010,6 @@ function displayInviteDiagnostics(diagnostics) {
|
|||||||
if (resultsContainer) {
|
if (resultsContainer) {
|
||||||
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
|
resultsContainer.insertBefore(resultDiv, resultsContainer.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to top of results
|
|
||||||
resultDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup Enter key handlers for diagnostics forms
|
// Setup Enter key handlers for diagnostics forms
|
||||||
@@ -927,6 +1103,9 @@ window.renderDiagnostics = renderDiagnostics;
|
|||||||
window.cancelStream = cancelStream;
|
window.cancelStream = cancelStream;
|
||||||
window.closeDiagnosticResult = closeDiagnosticResult;
|
window.closeDiagnosticResult = closeDiagnosticResult;
|
||||||
window.runInviteDiagnostics = runInviteDiagnostics;
|
window.runInviteDiagnostics = runInviteDiagnostics;
|
||||||
|
window.cleanDnsPassStorage = cleanDnsPassStorage;
|
||||||
|
window.hideRestartMessage = hideRestartMessage;
|
||||||
|
window.hideShutdownMessage = hideShutdownMessage;
|
||||||
|
|
||||||
// Setup Enter key handlers when DOM is ready
|
// Setup Enter key handlers when DOM is ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
@@ -88,5 +88,7 @@ module.exports = {
|
|||||||
// Master node reconnection tracking
|
// Master node reconnection tracking
|
||||||
peersToReconnect: new Set(), // Set of peer IDs that master should attempt to reconnect to
|
peersToReconnect: new Set(), // Set of peer IDs that master should attempt to reconnect to
|
||||||
reconnectionAttempts: new Map(), // Map<peerId, attempt count>
|
reconnectionAttempts: new Map(), // Map<peerId, attempt count>
|
||||||
lastReconnectionAttempt: new Map() // Map<peerId, timestamp>
|
lastReconnectionAttempt: new Map(), // Map<peerId, timestamp>
|
||||||
|
// Invite diagnostics tracking
|
||||||
|
consecutiveInviteFailures: 0
|
||||||
};
|
};
|
||||||
@@ -182,9 +182,31 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logInfo('Main', 'Plugin system initialized');
|
logInfo('Main', 'Plugin system initialized');
|
||||||
|
|
||||||
// Store shutdown function for cleanup
|
// Store shutdown function for cleanup
|
||||||
state.shutdownPlugins = shutdownAllPlugins;
|
state.shutdownPlugins = shutdownAllPlugins;
|
||||||
|
|
||||||
|
// Initialize invite diagnostics function early so admin interface can access it
|
||||||
|
// The actual diagnostics will work once swarm is initialized
|
||||||
|
state.diagnoseInviteIssues = function() {
|
||||||
|
// Return basic info if swarm not initialized yet
|
||||||
|
if (!state.connectedPeers || !state.peerChannels) {
|
||||||
|
return {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
nodeType: state.isMaster ? 'master' : 'joiner',
|
||||||
|
dnsPassInitialized: !!state.dnsPass,
|
||||||
|
connectedPeers: 0,
|
||||||
|
failedInvitePeers: [],
|
||||||
|
pendingInviteAcks: [],
|
||||||
|
consecutiveInviteFailures: 0,
|
||||||
|
connectionIssues: [],
|
||||||
|
recommendations: ['System still initializing - diagnostics will be available once swarm is ready']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the full diagnostics function once swarm is ready
|
||||||
|
return diagnoseInviteIssues();
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logWarn('Main', `Error initializing plugins: ${err.message}`);
|
logWarn('Main', `Error initializing plugins: ${err.message}`);
|
||||||
}
|
}
|
||||||
@@ -328,6 +350,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Comprehensive invite diagnostics function
|
// Comprehensive invite diagnostics function
|
||||||
function diagnoseInviteIssues() {
|
function diagnoseInviteIssues() {
|
||||||
const diagnostics = {
|
const diagnostics = {
|
||||||
@@ -337,6 +360,7 @@ async function main() {
|
|||||||
connectedPeers: connectedPeers.size,
|
connectedPeers: connectedPeers.size,
|
||||||
failedInvitePeers: Array.from(failedInvitePeers),
|
failedInvitePeers: Array.from(failedInvitePeers),
|
||||||
pendingInviteAcks: Array.from(pendingInviteAcks.keys()),
|
pendingInviteAcks: Array.from(pendingInviteAcks.keys()),
|
||||||
|
consecutiveInviteFailures: state.consecutiveInviteFailures,
|
||||||
inviteChannels: {},
|
inviteChannels: {},
|
||||||
requestChannels: {},
|
requestChannels: {},
|
||||||
connectionIssues: [],
|
connectionIssues: [],
|
||||||
@@ -392,6 +416,14 @@ async function main() {
|
|||||||
diagnostics.recommendations.push(`${diagnostics.pendingInviteAcks.length} invites pending acknowledgment - check network latency or peer responsiveness`);
|
diagnostics.recommendations.push(`${diagnostics.pendingInviteAcks.length} invites pending acknowledgment - check network latency or peer responsiveness`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (diagnostics.consecutiveInviteFailures >= 3) {
|
||||||
|
if (!diagnostics.dnsPassInitialized && !diagnostics.nodeType.includes('master')) {
|
||||||
|
diagnostics.recommendations.push(`CRITICAL: ${state.consecutiveInviteFailures} consecutive invite failures - storage corruption detected. Run: sudo node p2ns.js --clean`);
|
||||||
|
} else {
|
||||||
|
diagnostics.recommendations.push(`${state.consecutiveInviteFailures} consecutive invite processing failures - storage corruption likely, use --clean flag`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (diagnostics.connectionIssues.length > 0) {
|
if (diagnostics.connectionIssues.length > 0) {
|
||||||
diagnostics.recommendations.push(`${diagnostics.connectionIssues.length} connection issues detected - check network stability`);
|
diagnostics.recommendations.push(`${diagnostics.connectionIssues.length} connection issues detected - check network stability`);
|
||||||
}
|
}
|
||||||
@@ -404,12 +436,13 @@ async function main() {
|
|||||||
return diagnostics;
|
return diagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make diagnostics function available globally for admin interface
|
// Update diagnostics function with full implementation now that swarm is ready
|
||||||
state.diagnoseInviteIssues = diagnoseInviteIssues;
|
state.diagnoseInviteIssues = diagnoseInviteIssues;
|
||||||
const connectedPeers = new Set();
|
const connectedPeers = new Set();
|
||||||
const peerChannels = new Map();
|
const peerChannels = new Map();
|
||||||
const failedInvitePeers = new Set(); // Track peers that cannot provide invites
|
const failedInvitePeers = new Set(); // Track peers that cannot provide invites
|
||||||
const pendingInviteAcks = new Map(); // Track pending invite acknowledgments: peerId -> { timeout, retryCount }
|
const pendingInviteAcks = new Map(); // Track pending invite acknowledgments: peerId -> { timeout, retryCount }
|
||||||
|
// consecutiveInviteFailures is now tracked in state object
|
||||||
state.connectedPeers = connectedPeers;
|
state.connectedPeers = connectedPeers;
|
||||||
state.peerChannels = peerChannels;
|
state.peerChannels = peerChannels;
|
||||||
|
|
||||||
@@ -455,6 +488,9 @@ async function main() {
|
|||||||
doAutoVotes();
|
doAutoVotes();
|
||||||
// Set up update listeners
|
// Set up update listeners
|
||||||
setupListeners();
|
setupListeners();
|
||||||
|
// Reset failure counter on successful invite processing
|
||||||
|
state.consecutiveInviteFailures = 0;
|
||||||
|
|
||||||
// Send invite acknowledgment back to sender
|
// Send invite acknowledgment back to sender
|
||||||
try {
|
try {
|
||||||
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_ack');
|
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_ack');
|
||||||
@@ -464,6 +500,33 @@ async function main() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('Swarm', `Error processing received invite: ${err.message}`);
|
logError('Swarm', `Error processing received invite: ${err.message}`);
|
||||||
|
state.consecutiveInviteFailures++;
|
||||||
|
|
||||||
|
// If invite processing fails, it might be due to corrupted storage
|
||||||
|
// Check if the error suggests storage corruption and provide helpful guidance
|
||||||
|
if (err.message.includes('corrupt') || err.message.includes('inconsistent') ||
|
||||||
|
err.message.includes('feed') || err.message.includes('signature') ||
|
||||||
|
err.code === 'CORRUPTION' || err.code === 'INCONSISTENT') {
|
||||||
|
logError('Swarm', 'Invite processing failed due to possible storage corruption. Try using --clean flag to reset storage.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've had multiple consecutive failures, strongly suggest storage cleanup
|
||||||
|
if (state.consecutiveInviteFailures >= 3) {
|
||||||
|
logError('Swarm', `Multiple consecutive invite processing failures (${state.consecutiveInviteFailures}). Storage corruption is likely. Run with --clean flag to reset storage.`);
|
||||||
|
|
||||||
|
// For joiner nodes that haven't successfully joined yet, suggest targeted recovery
|
||||||
|
if (!isMaster && !getDnsPass()) {
|
||||||
|
logError('Swarm', 'CRITICAL: Joiner node cannot process invites due to likely storage corruption.');
|
||||||
|
logError('Swarm', 'RECOMMENDED ACTION: Stop the node and restart with: sudo node p2ns.js --clean');
|
||||||
|
logError('Swarm', 'This will safely clean corrupted storage without affecting network state.');
|
||||||
|
|
||||||
|
// If we've hit 5+ failures, be even more insistent
|
||||||
|
if (state.consecutiveInviteFailures >= 5) {
|
||||||
|
logError('Swarm', 'URGENT: 5+ consecutive invite failures detected. Storage cleanup REQUIRED.');
|
||||||
|
logError('Swarm', 'Command: sudo node p2ns.js --clean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpen: (peerId) => {
|
onOpen: (peerId) => {
|
||||||
@@ -668,6 +731,9 @@ async function main() {
|
|||||||
doAutoVotes();
|
doAutoVotes();
|
||||||
// Set up update listeners
|
// Set up update listeners
|
||||||
setupListeners();
|
setupListeners();
|
||||||
|
// Reset failure counter on successful invite processing
|
||||||
|
state.consecutiveInviteFailures = 0;
|
||||||
|
|
||||||
// Send invite acknowledgment back to the relay peer
|
// Send invite acknowledgment back to the relay peer
|
||||||
try {
|
try {
|
||||||
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_ack');
|
channelManager.sendToPeer(CORE_DOMAIN, 'request', peerId, 'invite_ack');
|
||||||
@@ -678,6 +744,29 @@ async function main() {
|
|||||||
logInfo('Swarm', 'Successfully processed relay invite and initialized dnsPass');
|
logInfo('Swarm', 'Successfully processed relay invite and initialized dnsPass');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('Swarm', `Error processing received relay invite: ${err.message}`);
|
logError('Swarm', `Error processing received relay invite: ${err.message}`);
|
||||||
|
state.consecutiveInviteFailures++;
|
||||||
|
|
||||||
|
// If relay invite processing fails, it might be due to corrupted storage
|
||||||
|
if (err.message.includes('corrupt') || err.message.includes('inconsistent') ||
|
||||||
|
err.message.includes('feed') || err.message.includes('signature') ||
|
||||||
|
err.code === 'CORRUPTION' || err.code === 'INCONSISTENT') {
|
||||||
|
logError('Swarm', 'Relay invite processing failed due to possible storage corruption. Try using --clean flag to reset storage.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've had multiple consecutive failures, strongly suggest storage cleanup
|
||||||
|
if (state.consecutiveInviteFailures >= 3) {
|
||||||
|
logError('Swarm', `Multiple consecutive invite processing failures (${state.consecutiveInviteFailures}). Storage corruption is likely. Run with --clean flag to reset storage.`);
|
||||||
|
|
||||||
|
// For joiner nodes that haven't successfully joined yet, be very clear
|
||||||
|
if (!isMaster && !getDnsPass()) {
|
||||||
|
logError('Swarm', 'CRITICAL: Joiner node cannot process relay invites due to storage corruption.');
|
||||||
|
logError('Swarm', 'REQUIRED ACTION: sudo node p2ns.js --clean');
|
||||||
|
|
||||||
|
if (state.consecutiveInviteFailures >= 5) {
|
||||||
|
logError('Swarm', 'EMERGENCY: 5+ consecutive relay invite failures. Immediate cleanup required.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Forward the response toward the origin
|
// Forward the response toward the origin
|
||||||
|
|||||||
Reference in New Issue
Block a user