Update UI components and JavaScript files for improved functionality

This commit is contained in:
courtmanr@gmail.com
2025-05-27 19:52:49 +01:00
parent bd55316ebe
commit 07c31c167b
5 changed files with 299 additions and 28 deletions
+6 -6
View File
@@ -98,8 +98,8 @@ class DiagnosticTool {
console.error('Error generating recommendations:', e);
}
// Sanitize sensitive data before returning
return this.sanitizeReport(report);
// Return unsanitized report - sanitization will be done client-side for copy/download
return report;
}
sanitizeReport(report) {
@@ -232,7 +232,7 @@ class DiagnosticTool {
const permCheck = {
id: id,
name: clientObj.config?.name || id,
host: this.sanitizeUrl(clientObj.config?.host),
host: clientObj.config?.host,
canConnect: false,
canListNodes: false,
canListVMs: false,
@@ -357,7 +357,7 @@ class DiagnosticTool {
const permCheck = {
id: id,
name: clientObj.config?.name || id,
host: this.sanitizeUrl(clientObj.config?.host),
host: clientObj.config?.host,
node_name: clientObj.config?.nodeName || clientObj.config?.node_name || 'NOT SET',
canConnect: false,
canListDatastores: false,
@@ -445,7 +445,7 @@ class DiagnosticTool {
if (!id.startsWith('pbs_') && clientObj && clientObj.config) {
config.proxmox.push({
id: id,
host: this.sanitizeUrl(clientObj.config.host),
host: clientObj.config.host,
name: clientObj.config.name || id,
port: clientObj.config.port || '8006',
tokenConfigured: !!clientObj.config.tokenId,
@@ -464,7 +464,7 @@ class DiagnosticTool {
const nodeName = clientObj.config.nodeName || clientObj.config.node_name;
config.pbs.push({
id: id,
host: this.sanitizeUrl(clientObj.config.host),
host: clientObj.config.host,
name: clientObj.config.name || id,
port: clientObj.config.port || '8007',
node_name: nodeName || 'NOT SET',
+66 -4
View File
@@ -43,6 +43,7 @@ const cors = require('cors');
const compression = require('compression');
const { Server } = require('socket.io');
const { URL } = require('url'); // <--- ADD: Import URL constructor
const axios = require('axios');
const axiosRetry = require('axios-retry').default; // Import axios-retry
// Development specific dependencies
@@ -351,17 +352,78 @@ app.delete('/api/alerts/rules/:id', (req, res) => {
}
});
// Example API Route (Add your actual API routes here)
app.get('/api/version', (req, res) => {
// Version check functionality
let latestVersionCache = null;
let lastVersionCheck = 0;
const VERSION_CHECK_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
async function checkLatestVersion() {
const now = Date.now();
// Return cached version if still fresh
if (latestVersionCache && (now - lastVersionCheck) < VERSION_CHECK_INTERVAL) {
return latestVersionCache;
}
try {
const response = await axios.get('https://api.github.com/repos/rcourtman/Pulse/releases/latest', {
timeout: 5000,
headers: {
'Accept': 'application/vnd.github.v3+json'
}
});
if (response.data && response.data.tag_name) {
// Remove 'v' prefix if present
const version = response.data.tag_name.replace(/^v/, '');
latestVersionCache = version;
lastVersionCheck = now;
return version;
}
} catch (error) {
console.error('Error checking latest version:', error.message);
}
return null;
}
// Version API endpoint
app.get('/api/version', async (req, res) => {
try {
const packageJson = require('../package.json');
res.json({ version: packageJson.version || 'N/A' });
const currentVersion = packageJson.version || 'N/A';
// Check for latest version
const latestVersion = await checkLatestVersion();
res.json({
version: currentVersion,
latestVersion: latestVersion,
updateAvailable: latestVersion && latestVersion !== currentVersion &&
compareVersions(latestVersion, currentVersion) > 0
});
} catch (error) {
console.error("Error reading package.json for version:", error);
console.error("Error in version endpoint:", error);
res.status(500).json({ error: "Could not retrieve version" });
}
});
// Simple version comparison function
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 > part2) return 1;
if (part1 < part2) return -1;
}
return 0;
}
app.get('/api/storage', async (req, res) => {
try {
// Get current nodes from state manager
+185 -16
View File
@@ -254,20 +254,18 @@
let html = '';
// Add sanitization notice if data is sanitized
if (data._sanitized) {
html += `
<div style="background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px; padding: 15px; margin-bottom: 20px;">
<h3 style="margin: 0 0 10px 0; color: #1976d2;">🛡️ Privacy Protected</h3>
<p style="margin: 0; font-size: 14px; color: #1565c0;">
${data._sanitized.notice}
</p>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #1976d2; font-weight: bold;">
✅ This report is safe to share in GitHub issues or support requests
</p>
</div>
`;
}
// Show notice about sensitive data
html += `
<div style="background: #fff3e0; border: 1px solid #ff9800; border-radius: 4px; padding: 15px; margin-bottom: 20px;">
<h3 style="margin: 0 0 10px 0; color: #f57c00;">⚠️ Sensitive Data Visible</h3>
<p style="margin: 0; font-size: 14px; color: #e65100;">
This page displays real hostnames, IPs, and other potentially sensitive information for diagnostic purposes.
</p>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #e65100; font-weight: bold;">
📋 When you copy or download this report, all sensitive data will be automatically sanitized for safe sharing.
</p>
</div>
`;
// Errors section (if any)
if (data.errors && data.errors.length > 0) {
@@ -557,10 +555,179 @@
return html;
}
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 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),
// Remove potentially sensitive fields, keep only structure info
tokenConfigured: pve.tokenConfigured,
selfSignedCerts: pve.selfSignedCerts
}));
}
if (sanitized.configuration.pbs) {
sanitized.configuration.pbs = sanitized.configuration.pbs.map(pbs => ({
...pbs,
host: sanitizeUrl(pbs.host),
// Remove potentially sensitive fields, keep only structure info
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),
// Keep diagnostic info but sanitize error messages
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),
// Keep diagnostic info but sanitize error messages
errors: perm.errors ? perm.errors.map(err => sanitizeErrorMessage(err)) : []
}));
}
}
// Sanitize state section
if (sanitized.state) {
// Remove potentially sensitive node names, keep only counts and structure
if (sanitized.state.nodes && sanitized.state.nodes.names) {
sanitized.state.nodes.names = sanitized.state.nodes.names.map((name, index) => `node-${index + 1}`);
}
// Remove specific backup IDs, keep only counts
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)
}));
}
// Sanitize connectivity section
if (sanitized.connectivity) {
if (sanitized.connectivity.proxmox) {
sanitized.connectivity.proxmox = sanitized.connectivity.proxmox.map(conn => ({
...conn,
host: sanitizeUrl(conn.host),
error: conn.error ? sanitizeErrorMessage(conn.error) : conn.error
}));
}
if (sanitized.connectivity.pbs) {
sanitized.connectivity.pbs = sanitized.connectivity.pbs.map(conn => ({
...conn,
host: sanitizeUrl(conn.host),
error: conn.error ? sanitizeErrorMessage(conn.error) : conn.error
}));
}
}
// Sanitize dataFlow section
if (sanitized.dataFlow && sanitized.dataFlow.pbs && sanitized.dataFlow.pbs.backup_matching) {
sanitized.dataFlow.pbs.backup_matching = sanitized.dataFlow.pbs.backup_matching.map(instance => ({
...instance,
sample_backups: instance.sample_backups ? instance.sample_backups.map((backup, idx) => ({
...backup,
backup_id: `backup-${idx + 1}`
})) : []
}));
}
// Sanitize errors section
if (sanitized.errors) {
sanitized.errors = sanitized.errors.map(err => ({
...err,
error: sanitizeErrorMessage(err.error)
}));
}
// 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 copyReport() {
if (!diagnosticData) return;
const text = JSON.stringify(diagnosticData, null, 2);
// 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.textContent;
@@ -576,9 +743,11 @@
function downloadReport() {
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(diagnosticData, null, 2);
const text = JSON.stringify(sanitizedData, null, 2);
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
+40
View File
@@ -112,6 +112,40 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Version data received:', data);
if (data.version) {
versionSpan.textContent = data.version;
// Check if update is available
if (data.updateAvailable && data.latestVersion) {
// Check if update indicator already exists
const existingIndicator = document.getElementById('update-indicator');
if (!existingIndicator) {
// Create update indicator
const updateIndicator = document.createElement('span');
updateIndicator.id = 'update-indicator';
updateIndicator.className = 'ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
updateIndicator.innerHTML = `
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.293l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" clip-rule="evenodd"/>
</svg>
v${data.latestVersion} available
`;
updateIndicator.title = 'Click to view the latest release';
updateIndicator.style.cursor = 'pointer';
updateIndicator.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
window.open('https://github.com/rcourtman/Pulse/releases/latest', '_blank');
});
// Insert after version link
versionSpan.parentNode.insertBefore(updateIndicator, versionSpan.nextSibling);
}
} else {
// Remove update indicator if no update available
const existingIndicator = document.getElementById('update-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
}
} else {
versionSpan.textContent = 'unknown';
}
@@ -141,4 +175,10 @@ document.addEventListener('DOMContentLoaded', function() {
fetchVersion();
}
}, 2000);
// Periodically check for updates (every 6 hours)
setInterval(() => {
console.log('[Main] Checking for version updates...');
fetchVersion();
}, 6 * 60 * 60 * 1000);
});
+2 -2
View File
@@ -163,7 +163,7 @@ PulseApp.ui.storage = (() => {
const thead = document.createElement('thead');
thead.innerHTML = `
<tr class="border-b border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 sticky top-0 z-10 text-xs font-medium tracking-wider text-left text-gray-600 uppercase dark:text-gray-300">
<th class="p-1 px-2">Storage</th>
<th class="sticky left-0 bg-gray-50 dark:bg-gray-700 z-20 p-1 px-2">Storage</th>
<th class="p-1 px-2">Content</th>
<th class="p-1 px-2">Type</th>
<th class="p-1 px-2">Shared</th>
@@ -231,7 +231,7 @@ PulseApp.ui.storage = (() => {
const contentBadges = getContentBadgesHTML(store.content);
row.innerHTML = `
<td class="p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100">${store.storage || 'N/A'}</td>
<td class="sticky left-0 bg-white dark:bg-gray-800 z-10 p-1 px-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-0 text-gray-900 dark:text-gray-100">${store.storage || 'N/A'}</td>
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs flex items-center">${contentBadges}</td>
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${store.type || 'N/A'}</td>
<td class="p-1 px-2 whitespace-nowrap storage-tooltip-trigger cursor-default" data-tooltip="${sharedIconTooltip}">${sharedIcon}</td>