From 5da22ab9738c356f36964763ce6bb07fb517247c Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sat, 31 May 2025 17:46:19 +0100 Subject: [PATCH] feat: integrate diagnostics into settings menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move diagnostics from standalone page to settings modal tab - Add Diagnostics tab to settings navigation - Port all diagnostic functionality to settings UI module - Update diagnostics button to open settings modal directly - Remove standalone diagnostics.html page - Preserve all features: report generation, sanitization, copy/download 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- server/diagnostics.js | 8 + src/public/diagnostics.html | 769 ----------------------------------- src/public/index.html | 7 +- src/public/js/ui/settings.js | 536 +++++++++++++++++++++++- 4 files changed, 545 insertions(+), 775 deletions(-) delete mode 100644 src/public/diagnostics.html 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 @@ - - - - - - Pulse Diagnostics - - - - -
-

Pulse Diagnostics

-

Generate a comprehensive diagnostic report to help troubleshoot issues

- -
- - - -
- - - - -
- - - - \ No newline at end of file diff --git a/src/public/index.html b/src/public/index.html index ed6537369..c67f5f4f2 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -455,12 +455,12 @@ Pulse
-
diff --git a/src/public/js/ui/settings.js b/src/public/js/ui/settings.js index bd1b49d89..e06a0a884 100644 --- a/src/public/js/ui/settings.js +++ b/src/public/js/ui/settings.js @@ -20,6 +20,12 @@ PulseApp.ui.settings = (() => { if (settingsButton) { settingsButton.addEventListener('click', openModal); } + + // Set up diagnostics button + const diagnosticsButton = document.getElementById('diagnostics-icon'); + if (diagnosticsButton) { + diagnosticsButton.addEventListener('click', () => openModalWithTab('diagnostics')); + } if (closeButton) { closeButton.addEventListener('click', closeModal); @@ -91,7 +97,11 @@ PulseApp.ui.settings = (() => { } async function openModal() { - console.log('[Settings] Opening modal...'); + await openModalWithTab('proxmox'); + } + + async function openModalWithTab(tabName) { + console.log('[Settings] Opening modal with tab:', tabName); const modal = document.getElementById('settings-modal'); if (!modal) return; @@ -103,8 +113,8 @@ PulseApp.ui.settings = (() => { // Load current configuration await loadConfiguration(); - // Reset to first tab - switchTab('proxmox'); + // Switch to requested tab + switchTab(tabName); } function closeModal() { @@ -162,6 +172,9 @@ PulseApp.ui.settings = (() => { case 'system': content = renderSystemTab(advanced, safeConfig); break; + case 'diagnostics': + content = renderDiagnosticsTab(); + break; } container.innerHTML = `
${content}
`; @@ -661,6 +674,65 @@ PulseApp.ui.settings = (() => { `; } + function renderDiagnosticsTab() { + return ` +
+
+

System Diagnostics

+

+ Generate a comprehensive diagnostic report to help troubleshoot issues with your Pulse configuration. +

+ +
+ + + +
+ + + + +
+ +
+
+
+ + + +
+
+

Privacy Notice

+
+

The diagnostic report displays real hostnames, IPs, and other potentially sensitive information for troubleshooting purposes.

+

When you copy or download the report, all sensitive data is automatically sanitized for safe sharing.

+
+
+
+
+
+ `; + } + function renderThresholdsTab() { return `
@@ -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 ` +
+
+ ${title} + ${expanded ? '▼' : '▶'} +
+
+ ${content} +
+
+ `; + } + + 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 += ''; + html += ''; + html += ''; + html += ''; + + config.proxmox.forEach(pve => { + html += ` + + + + `; + }); + html += '
HostNameToken
${pve.host}${pve.name}${pve.tokenConfigured ? '✓' : '✗'}
'; + } + + html += '

PBS Instances

'; + if (!config.pbs || config.pbs.length === 0) { + html += '

No PBS instances configured

'; + } else { + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + config.pbs.forEach(pbs => { + const nodeNameStyle = pbs.node_name === 'NOT SET' ? 'text-red-600 dark:text-red-400 font-bold' : ''; + html += ` + + + + + `; + }); + html += '
HostNameNode NameToken
${pbs.host}${pbs.name}${pbs.node_name}${pbs.tokenConfigured ? '✓' : '✗'}
'; + } + + html += '
'; + return html; + } + + function renderPermissions(permissions) { + let html = '
'; + + if (permissions.proxmox && permissions.proxmox.length > 0) { + html += '

Proxmox VE Token Permissions

'; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + permissions.proxmox.forEach(perm => { + const checkIcon = (canDo) => canDo ? + '' : + ''; + html += ` + + + + + + `; + }); + html += '
InstanceConnectNodesVMsContainers
${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})` : ''}
'; + } + + if (permissions.pbs && permissions.pbs.length > 0) { + html += '

PBS Token Permissions

'; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + permissions.pbs.forEach(perm => { + const checkIcon = (canDo) => canDo ? + '' : + ''; + html += ` + + + + + `; + }); + html += '
InstanceConnectDatastoresBackups
${perm.name}${checkIcon(perm.canConnect)}${checkIcon(perm.canListDatastores)} ${perm.datastoreCount !== undefined ? `(${perm.datastoreCount})` : ''}${checkIcon(perm.canListBackups)} ${perm.backupCount !== undefined ? `(${perm.backupCount})` : ''}
'; + } + + 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 }; })();