From 328e67bc61284c985e2e7c5afe5786fe9ec623a4 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Fri, 30 May 2025 00:16:08 +0100 Subject: [PATCH] feat: add clickable host names in dashboard and PBS views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add endpoint configuration sharing between server and client - Make Proxmox node group headers clickable using API-based mapping - Make PBS instance names clickable in both desktop and mobile views - Links open correct cluster endpoints based on node's actual endpointId - Subtle hover styling without blue underlines for cleaner UI - All links open in new tabs with security attributes Resolves issue with users wanting direct access to host web interfaces. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- server/index.js | 3 +++ server/state.js | 23 ++++++++++++++++++++++ src/public/js/state.js | 10 ++++++++++ src/public/js/ui/common.js | 10 +++++++++- src/public/js/ui/pbs.js | 27 +++++++++++++++++++++++-- src/public/js/utils.js | 40 +++++++++++++++++++++++++++++++++++++- 6 files changed, 109 insertions(+), 4 deletions(-) diff --git a/server/index.js b/server/index.js index baa493e72..6d4a2473f 100644 --- a/server/index.js +++ b/server/index.js @@ -35,6 +35,9 @@ try { // Set the placeholder status in stateManager *after* config loading is complete stateManager.setConfigPlaceholderStatus(configIsPlaceholder); +// Set endpoint configurations for client use +stateManager.setEndpointConfigurations(endpoints, pbsConfigs); + const fs = require('fs'); // Add fs module const express = require('express'); const http = require('http'); diff --git a/server/state.js b/server/state.js index 65d73e092..6f97364d9 100644 --- a/server/state.js +++ b/server/state.js @@ -12,6 +12,8 @@ const state = { guestSnapshots: [] }, isConfigPlaceholder: false, // Add this flag + endpoints: [], // Add endpoint configurations for client + pbsConfigs: [], // Add PBS configurations for client // Enhanced monitoring data performance: { @@ -88,6 +90,8 @@ function getState() { pbs: state.pbs, // This is what's sent to the client and should now be correct pveBackups: state.pveBackups, // Add PVE backup data isConfigPlaceholder: state.isConfigPlaceholder, + endpoints: state.endpoints, // Add endpoint configurations + pbsConfigs: state.pbsConfigs, // Add PBS configurations // Enhanced monitoring data performance: state.performance, @@ -343,6 +347,24 @@ function setConfigPlaceholderStatus(isPlaceholder) { state.isConfigPlaceholder = isPlaceholder; } +function setEndpointConfigurations(endpoints, pbsConfigs) { + // Store endpoint configurations for client use + state.endpoints = endpoints.map(endpoint => ({ + id: endpoint.id, + name: endpoint.name, + host: endpoint.host, + port: endpoint.port, + enabled: endpoint.enabled + })); + + state.pbsConfigs = pbsConfigs.map(config => ({ + id: config.id, + name: config.name, + host: config.host, + port: config.port + })); +} + function getPerformanceHistory(limit = 50) { return performanceHistory.slice(0, limit); } @@ -405,6 +427,7 @@ module.exports = { init, getState, setConfigPlaceholderStatus, + setEndpointConfigurations, updateDiscoveryData, updateMetricsData, clearMetricsData, diff --git a/src/public/js/state.js b/src/public/js/state.js index 356c96025..97626894d 100644 --- a/src/public/js/state.js +++ b/src/public/js/state.js @@ -10,6 +10,8 @@ PulseApp.state = (() => { metricsData: [], dashboardData: [], pbsDataArray: [], + endpoints: [], // Add endpoint configurations + pbsConfigs: [], // Add PBS configurations pveBackups: { // Add PVE backup data backupTasks: [], storageBackups: [], @@ -176,6 +178,14 @@ PulseApp.state = (() => { } }); + // Update endpoint configurations (these don't need change tracking) + if (newData.endpoints) { + internalState.endpoints = newData.endpoints; + } + if (newData.pbsConfigs) { + internalState.pbsConfigs = newData.pbsConfigs; + } + // Only update enhanced monitoring data if provided if (newData.alerts) { internalState.alerts = { diff --git a/src/public/js/ui/common.js b/src/public/js/ui/common.js index c22972aed..c0a97aa51 100644 --- a/src/public/js/ui/common.js +++ b/src/public/js/ui/common.js @@ -360,8 +360,16 @@ PulseApp.ui.common = (() => { function generateNodeGroupHeaderCellHTML(text, colspan, cellTag = 'td') { const baseClasses = 'py-0.5 px-2 text-left font-medium text-xs sm:text-sm text-gray-700 dark:text-gray-300'; + // Check if we can make this node name clickable + const hostUrl = PulseApp.utils.getHostUrl(text); + let nodeContent = text; + + if (hostUrl) { + nodeContent = `${text}`; + } + // Always create individual cells so first one can be sticky - let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${text}`; + let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${nodeContent}`; // Add empty cells for remaining columns for (let i = 1; i < colspan; i++) { html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700">`; diff --git a/src/public/js/ui/pbs.js b/src/public/js/ui/pbs.js index f42b1c831..86da70468 100644 --- a/src/public/js/ui/pbs.js +++ b/src/public/js/ui/pbs.js @@ -994,7 +994,22 @@ PulseApp.ui.pbs = (() => { headerDiv.className = `${CSS_CLASSES.FLEX} ${CSS_CLASSES.JUSTIFY_BETWEEN} ${CSS_CLASSES.ITEMS_CENTER} ${CSS_CLASSES.MB3}`; const instanceTitleElement = document.createElement('h3'); instanceTitleElement.className = `${CSS_CLASSES.TEXT_LG} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.TEXT_GRAY_800_DARK_GRAY_200} ${CSS_CLASSES.FLEX} ${CSS_CLASSES.ITEMS_CENTER}`; - instanceTitleElement.appendChild(document.createTextNode(instanceName)); + + // Check if we can make this PBS instance name clickable + const hostUrl = PulseApp.utils.getHostUrl(instanceName); + if (hostUrl) { + const linkElement = document.createElement('a'); + linkElement.href = hostUrl; + linkElement.target = '_blank'; + linkElement.rel = 'noopener noreferrer'; + linkElement.className = 'text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer'; + linkElement.title = `Open ${instanceName} web interface`; + linkElement.appendChild(document.createTextNode(instanceName)); + instanceTitleElement.appendChild(linkElement); + } else { + instanceTitleElement.appendChild(document.createTextNode(instanceName)); + } + headerDiv.appendChild(instanceTitleElement); return headerDiv; }; @@ -1471,10 +1486,18 @@ PulseApp.ui.pbs = (() => { statusClass = 'text-yellow-600 dark:text-yellow-400'; } + // Check if we can make this PBS instance name clickable + const hostUrl = PulseApp.utils.getHostUrl(instanceName); + let instanceNameHtml = `${instanceName}`; + + if (hostUrl) { + instanceNameHtml = `${instanceName}`; + } + headerContent.innerHTML = `
${statusIcon} - ${instanceName} + ${instanceNameHtml}
${statusInfo.statusText}
`; diff --git a/src/public/js/utils.js b/src/public/js/utils.js index 8d4300047..83aa666f3 100644 --- a/src/public/js/utils.js +++ b/src/public/js/utils.js @@ -322,6 +322,43 @@ PulseApp.utils = (() => { return null; } + // Get URL for a host based on endpoint configuration and API data + function getHostUrl(nodeName) { + const endpoints = PulseApp.state.get('endpoints') || []; + const pbsConfigs = PulseApp.state.get('pbsConfigs') || []; + + // First check PBS configs for exact name match + for (const config of pbsConfigs) { + if (config.name === nodeName) { + return config.host; + } + } + + // For Proxmox nodes, we need to find which endpoint this node belongs to + // by looking at the nodes data from the API + const nodesData = PulseApp.state.get('nodesData') || []; + + // Find the node in the API data to get its endpointId + const nodeInfo = nodesData.find(node => node.node === nodeName); + + if (nodeInfo && nodeInfo.endpointId) { + // Find the endpoint that matches this endpointId + const endpoint = endpoints.find(ep => ep.id === nodeInfo.endpointId); + if (endpoint) { + return endpoint.host; + } + } + + // Fallback: try direct name matches with endpoints + for (const endpoint of endpoints) { + if (endpoint.name === nodeName) { + return endpoint.host; + } + } + + return null; + } + // Return the public API for this module return { sanitizeForId: (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'), @@ -342,6 +379,7 @@ PulseApp.utils = (() => { updateProgressBarTexts, updateProgressBarTextsDebounced, preserveScrollPosition, - getScrollableParent + getScrollableParent, + getHostUrl }; })(); \ No newline at end of file