diff --git a/server/diagnostics.js b/server/diagnostics.js
index a9ce56a74..9918989a2 100644
--- a/server/diagnostics.js
+++ b/server/diagnostics.js
@@ -98,6 +98,14 @@ class DiagnosticTool {
console.error('Error generating recommendations:', e);
}
+ // Add summary for UI
+ report.summary = {
+ hasIssues: report.recommendations.some(r => r.severity === 'critical' || r.severity === 'warning'),
+ criticalIssues: report.recommendations.filter(r => r.severity === 'critical').length,
+ warnings: report.recommendations.filter(r => r.severity === 'warning').length,
+ isTimingIssue: report.state.loadTimeout || (report.state.serverUptime < 60 && (!report.state.guests || report.state.guests.total === 0))
+ };
+
// Return unsanitized report - sanitization will be done client-side for copy/download
return report;
}
diff --git a/src/public/diagnostics.html b/src/public/diagnostics.html
deleted file mode 100644
index b45774b3c..000000000
--- a/src/public/diagnostics.html
+++ /dev/null
@@ -1,769 +0,0 @@
-
-
-
-
@@ -1638,10 +1710,463 @@ PulseApp.ui.settings = (() => {
}
}
+ // Diagnostics functions
+ let diagnosticData = null;
+
+ async function runDiagnostics() {
+ const statusEl = document.getElementById('diagnostics-status');
+ const resultsEl = document.getElementById('diagnostics-results');
+ const runButton = document.getElementById('runDiagnostics');
+ const copyButton = document.getElementById('copyReport');
+ const downloadButton = document.getElementById('downloadReport');
+
+ // Reset UI
+ statusEl.className = 'block mb-4 p-3 rounded-lg text-sm font-medium';
+ statusEl.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-700', 'dark:text-blue-300');
+ statusEl.textContent = 'Running diagnostics...';
+ statusEl.style.display = 'block';
+ resultsEl.style.display = 'none';
+ runButton.disabled = true;
+
+ try {
+ const response = await fetch('/api/diagnostics');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ diagnosticData = await response.json();
+
+ // Show success status
+ statusEl.className = 'block mb-4 p-3 rounded-lg text-sm font-medium';
+ if (diagnosticData.summary?.hasIssues) {
+ statusEl.classList.add('bg-yellow-50', 'dark:bg-yellow-900/20', 'text-yellow-700', 'dark:text-yellow-300');
+ statusEl.textContent = `Diagnostics complete. Found ${diagnosticData.summary.criticalIssues} critical issues and ${diagnosticData.summary.warnings} warnings.`;
+ } else {
+ statusEl.classList.add('bg-green-50', 'dark:bg-green-900/20', 'text-green-700', 'dark:text-green-300');
+ statusEl.textContent = 'Diagnostics complete. No critical issues found!';
+ }
+
+ // Display results
+ displayDiagnosticResults(diagnosticData);
+ resultsEl.style.display = 'block';
+
+ // Show action buttons
+ copyButton.style.display = 'inline-flex';
+ downloadButton.style.display = 'inline-flex';
+
+ } catch (error) {
+ statusEl.className = 'block mb-4 p-3 rounded-lg text-sm font-medium';
+ statusEl.classList.add('bg-red-50', 'dark:bg-red-900/20', 'text-red-700', 'dark:text-red-300');
+ statusEl.textContent = `Error running diagnostics: ${error.message}`;
+ } finally {
+ runButton.disabled = false;
+ }
+ }
+
+ function displayDiagnosticResults(data) {
+ const resultsEl = document.getElementById('diagnostics-results');
+
+ let html = '';
+
+ // Recommendations section
+ if (data.recommendations && data.recommendations.length > 0) {
+ html += createDiagnosticSection('Recommendations', renderRecommendations(data.recommendations), true);
+ }
+
+ // Configuration section
+ if (data.configuration) {
+ html += createDiagnosticSection('Configuration', renderConfiguration(data.configuration));
+ }
+
+ // Permissions section
+ if (data.permissions) {
+ html += createDiagnosticSection('API Token Permissions', renderPermissions(data.permissions));
+ }
+
+ // System Information section
+ if (data.state || data.version) {
+ html += createDiagnosticSection('System Information', renderSystemInfo(data));
+ }
+
+ resultsEl.innerHTML = html;
+
+ // Add click handlers for collapsible sections
+ resultsEl.querySelectorAll('.diagnostic-section-header').forEach(header => {
+ header.addEventListener('click', () => {
+ const section = header.parentElement;
+ const content = section.querySelector('.diagnostic-section-content');
+ const indicator = header.querySelector('.diagnostic-indicator');
+
+ if (content.style.display === 'none') {
+ content.style.display = 'block';
+ indicator.textContent = '▼';
+ } else {
+ content.style.display = 'none';
+ indicator.textContent = '▶';
+ }
+ });
+ });
+ }
+
+ function createDiagnosticSection(title, content, expanded = false) {
+ return `
+
+ `;
+ }
+
+ function renderRecommendations(recommendations) {
+ if (recommendations.length === 0) {
+ return '
✓ No issues found - everything looks good!
';
+ }
+
+ return recommendations.map(rec => {
+ let bgColor, textColor, borderColor;
+ switch(rec.severity) {
+ case 'critical':
+ bgColor = 'bg-red-50 dark:bg-red-900/20';
+ textColor = 'text-red-800 dark:text-red-200';
+ borderColor = 'border-red-500';
+ break;
+ case 'warning':
+ bgColor = 'bg-yellow-50 dark:bg-yellow-900/20';
+ textColor = 'text-yellow-800 dark:text-yellow-200';
+ borderColor = 'border-yellow-500';
+ break;
+ default:
+ bgColor = 'bg-blue-50 dark:bg-blue-900/20';
+ textColor = 'text-blue-800 dark:text-blue-200';
+ borderColor = 'border-blue-500';
+ }
+
+ return `
+
+ [${rec.severity.toUpperCase()}] ${rec.category}: ${rec.message}
+
+ `;
+ }).join('');
+ }
+
+ function renderConfiguration(config) {
+ let html = '
';
+
+ html += '
Proxmox VE Instances
';
+ if (!config.proxmox || config.proxmox.length === 0) {
+ html += '
No Proxmox instances configured
';
+ } else {
+ html += '
';
+ html += '';
+ html += '| Host | ';
+ html += 'Name | ';
+ html += 'Token | ';
+ html += '
';
+
+ config.proxmox.forEach(pve => {
+ html += `
+ | ${pve.host} |
+ ${pve.name} |
+ ${pve.tokenConfigured ? '✓' : '✗'} |
+
`;
+ });
+ html += '
';
+ }
+
+ html += '
PBS Instances
';
+ if (!config.pbs || config.pbs.length === 0) {
+ html += '
No PBS instances configured
';
+ } else {
+ html += '
';
+ html += '';
+ html += '| Host | ';
+ html += 'Name | ';
+ html += 'Node Name | ';
+ html += 'Token | ';
+ html += '
';
+
+ config.pbs.forEach(pbs => {
+ const nodeNameStyle = pbs.node_name === 'NOT SET' ? 'text-red-600 dark:text-red-400 font-bold' : '';
+ html += `
+ | ${pbs.host} |
+ ${pbs.name} |
+ ${pbs.node_name} |
+ ${pbs.tokenConfigured ? '✓' : '✗'} |
+
`;
+ });
+ html += '
';
+ }
+
+ html += '
';
+ return html;
+ }
+
+ function renderPermissions(permissions) {
+ let html = '
';
+
+ if (permissions.proxmox && permissions.proxmox.length > 0) {
+ html += '
Proxmox VE Token Permissions
';
+ html += '
';
+ html += '';
+ html += '| Instance | ';
+ html += 'Connect | ';
+ html += 'Nodes | ';
+ html += 'VMs | ';
+ html += 'Containers | ';
+ html += '
';
+
+ permissions.proxmox.forEach(perm => {
+ const checkIcon = (canDo) => canDo ?
+ '✓' :
+ '✗';
+ html += `
+ | ${perm.name} |
+ ${checkIcon(perm.canConnect)} |
+ ${checkIcon(perm.canListNodes)} ${perm.nodeCount ? `(${perm.nodeCount})` : ''} |
+ ${checkIcon(perm.canListVMs)} ${perm.vmCount !== undefined ? `(${perm.vmCount})` : ''} |
+ ${checkIcon(perm.canListContainers)} ${perm.containerCount !== undefined ? `(${perm.containerCount})` : ''} |
+
`;
+ });
+ html += '
';
+ }
+
+ if (permissions.pbs && permissions.pbs.length > 0) {
+ html += '
PBS Token Permissions
';
+ html += '
';
+ html += '';
+ html += '| Instance | ';
+ html += 'Connect | ';
+ html += 'Datastores | ';
+ html += 'Backups | ';
+ html += '
';
+
+ permissions.pbs.forEach(perm => {
+ const checkIcon = (canDo) => canDo ?
+ '✓' :
+ '✗';
+ html += `
+ | ${perm.name} |
+ ${checkIcon(perm.canConnect)} |
+ ${checkIcon(perm.canListDatastores)} ${perm.datastoreCount !== undefined ? `(${perm.datastoreCount})` : ''} |
+ ${checkIcon(perm.canListBackups)} ${perm.backupCount !== undefined ? `(${perm.backupCount})` : ''} |
+
`;
+ });
+ html += '
';
+ }
+
+ html += '
';
+ return html;
+ }
+
+ function renderSystemInfo(data) {
+ let html = '
';
+
+ if (data.version) {
+ html += `
Pulse Version: ${data.version}
`;
+ }
+
+ if (data.state) {
+ if (data.state.lastUpdate) {
+ html += `
Last Update: ${new Date(data.state.lastUpdate).toLocaleString()}
`;
+ }
+
+ if (data.state.serverUptime) {
+ html += `
Server Uptime: ${Math.floor(data.state.serverUptime)} seconds
`;
+ }
+
+ if (data.state.nodes) {
+ html += `
Nodes: ${data.state.nodes.count} (${data.state.nodes.names.join(', ') || 'none'})
`;
+ }
+
+ if (data.state.guests) {
+ html += `
Total Guests: ${data.state.guests.total} (${data.state.guests.vms} VMs, ${data.state.guests.containers} Containers)
`;
+ html += `
Guest Status: ${data.state.guests.running} running, ${data.state.guests.stopped} stopped
`;
+ }
+
+ if (data.state.pbs) {
+ html += `
PBS Instances: ${data.state.pbs.instances}
`;
+ html += `
Total Backups: ${data.state.pbs.totalBackups}
`;
+ }
+ }
+
+ html += '
';
+ return html;
+ }
+
+ function sanitizeReport(report) {
+ // Deep clone the report to avoid modifying the original
+ const sanitized = JSON.parse(JSON.stringify(report));
+
+ // Sanitize configuration section
+ if (sanitized.configuration) {
+ if (sanitized.configuration.proxmox) {
+ sanitized.configuration.proxmox = sanitized.configuration.proxmox.map(pve => ({
+ ...pve,
+ host: sanitizeUrl(pve.host),
+ tokenConfigured: pve.tokenConfigured,
+ selfSignedCerts: pve.selfSignedCerts
+ }));
+ }
+
+ if (sanitized.configuration.pbs) {
+ sanitized.configuration.pbs = sanitized.configuration.pbs.map(pbs => ({
+ ...pbs,
+ host: sanitizeUrl(pbs.host),
+ tokenConfigured: pbs.tokenConfigured,
+ selfSignedCerts: pbs.selfSignedCerts,
+ node_name: pbs.node_name
+ }));
+ }
+ }
+
+ // Sanitize permissions section
+ if (sanitized.permissions) {
+ if (sanitized.permissions.proxmox) {
+ sanitized.permissions.proxmox = sanitized.permissions.proxmox.map(perm => ({
+ ...perm,
+ host: sanitizeUrl(perm.host),
+ name: sanitizeUrl(perm.name),
+ errors: perm.errors ? perm.errors.map(err => sanitizeErrorMessage(err)) : []
+ }));
+ }
+
+ if (sanitized.permissions.pbs) {
+ sanitized.permissions.pbs = sanitized.permissions.pbs.map(perm => ({
+ ...perm,
+ host: sanitizeUrl(perm.host),
+ name: sanitizeUrl(perm.name),
+ errors: perm.errors ? perm.errors.map(err => sanitizeErrorMessage(err)) : []
+ }));
+ }
+ }
+
+ // Sanitize state section
+ if (sanitized.state) {
+ if (sanitized.state.nodes && sanitized.state.nodes.names) {
+ sanitized.state.nodes.names = sanitized.state.nodes.names.map((name, index) => `node-${index + 1}`);
+ }
+
+ if (sanitized.state.pbs && sanitized.state.pbs.sampleBackupIds) {
+ sanitized.state.pbs.sampleBackupIds = sanitized.state.pbs.sampleBackupIds.map((id, index) => `backup-${index + 1}`);
+ }
+ }
+
+ // Sanitize recommendations
+ if (sanitized.recommendations) {
+ sanitized.recommendations = sanitized.recommendations.map(rec => ({
+ ...rec,
+ message: sanitizeRecommendationMessage(rec.message)
+ }));
+ }
+
+ // Add notice about sanitization
+ sanitized._sanitized = {
+ notice: "This diagnostic report has been sanitized for safe sharing. Hostnames, IPs, node names, and backup IDs have been anonymized while preserving structural information needed for troubleshooting.",
+ timestamp: new Date().toISOString()
+ };
+
+ return sanitized;
+ }
+
+ function sanitizeUrl(url) {
+ if (!url) return url;
+
+ // Remove protocol if present
+ let sanitized = url.replace(/^https?:\/\//, '');
+
+ // Replace IP addresses
+ sanitized = sanitized.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP-ADDRESS]');
+
+ // Replace hostnames (anything before port or path)
+ sanitized = sanitized.replace(/^[^:/]+/, '[HOSTNAME]');
+
+ // Replace ports
+ sanitized = sanitized.replace(/:\d+/, ':[PORT]');
+
+ return sanitized;
+ }
+
+ function sanitizeErrorMessage(errorMsg) {
+ if (!errorMsg) return errorMsg;
+
+ // Remove potential IP addresses, hostnames, and ports
+ let sanitized = errorMsg
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?\b/g, '[IP-ADDRESS]')
+ .replace(/https?:\/\/[^\/\s:]+(?::\d+)?/g, '[HOSTNAME]')
+ .replace(/([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}/g, '[HOSTNAME]')
+ .replace(/:\d{4,5}\b/g, ':[PORT]');
+
+ return sanitized;
+ }
+
+ function sanitizeRecommendationMessage(message) {
+ if (!message) return message;
+
+ // Replace specific hostnames and IPs in common recommendation patterns
+ let sanitized = message
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP-ADDRESS]')
+ .replace(/https?:\/\/[^\/\s:]+/g, '[HOSTNAME]')
+ .replace(/host\s*'[^']+'/g, "host '[HOSTNAME]'")
+ .replace(/host\s*"[^"]+"/g, 'host "[HOSTNAME]"')
+ .replace(/node\s+'[^']+'/g, "node '[NODE-NAME]'")
+ .replace(/node\s+"[^"]+"/g, 'node "[NODE-NAME]"')
+ .replace(/:\d{4,5}\b/g, ':[PORT]');
+
+ return sanitized;
+ }
+
+ function copyDiagnosticReport() {
+ if (!diagnosticData) return;
+
+ // Sanitize the data before copying
+ const sanitizedData = sanitizeReport(diagnosticData);
+ const text = JSON.stringify(sanitizedData, null, 2);
+
+ navigator.clipboard.writeText(text).then(() => {
+ const button = document.getElementById('copyReport');
+ const originalText = button.innerHTML;
+ button.innerHTML = '
Copied!';
+ button.classList.remove('bg-gray-600', 'hover:bg-gray-700');
+ button.classList.add('bg-green-600', 'hover:bg-green-700');
+
+ setTimeout(() => {
+ button.innerHTML = originalText;
+ button.classList.remove('bg-green-600', 'hover:bg-green-700');
+ button.classList.add('bg-gray-600', 'hover:bg-gray-700');
+ }, 3000);
+ });
+ }
+
+ function downloadDiagnosticReport() {
+ if (!diagnosticData) return;
+
+ // Sanitize the data before downloading
+ const sanitizedData = sanitizeReport(diagnosticData);
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+ const filename = `pulse_diagnostics_${timestamp}.json`;
+ const text = JSON.stringify(sanitizedData, null, 2);
+
+ const blob = new Blob([text], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+
// Public API
return {
init,
openModal,
+ openModalWithTab,
closeModal,
addPveEndpoint,
addPbsEndpoint,
@@ -1649,7 +2174,10 @@ PulseApp.ui.settings = (() => {
testConnections,
checkForUpdates,
applyUpdate,
- changeTheme
+ changeTheme,
+ runDiagnostics,
+ copyDiagnosticReport,
+ downloadDiagnosticReport
};
})();