mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
Update UI components and JavaScript files for improved functionality
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user