feat: add clickable host names in dashboard and PBS views

- 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 <noreply@anthropic.com>
This commit is contained in:
courtmanr@gmail.com
2025-05-30 00:16:08 +01:00
parent 57e90ab227
commit 328e67bc61
6 changed files with 109 additions and 4 deletions
+3
View File
@@ -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');
+23
View File
@@ -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,
+10
View File
@@ -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 = {
+9 -1
View File
@@ -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 = `<a href="${hostUrl}" target="_blank" rel="noopener noreferrer" class="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer" title="Open ${text} web interface">${text}</a>`;
}
// 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}</${cellTag}>`;
let html = `<${cellTag} class="sticky left-0 bg-gray-200 dark:bg-gray-700 z-10 ${baseClasses}">${nodeContent}</${cellTag}>`;
// Add empty cells for remaining columns
for (let i = 1; i < colspan; i++) {
html += `<${cellTag} class="bg-gray-200 dark:bg-gray-700"></${cellTag}>`;
+25 -2
View File
@@ -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 = `<span class="font-medium text-sm">${instanceName}</span>`;
if (hostUrl) {
instanceNameHtml = `<a href="${hostUrl}" target="_blank" rel="noopener noreferrer" class="font-medium text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-150 cursor-pointer" title="Open ${instanceName} web interface">${instanceName}</a>`;
}
headerContent.innerHTML = `
<div class="flex items-center gap-2 mb-1">
<span class="${statusClass} text-sm">${statusIcon}</span>
<span class="font-medium text-sm">${instanceName}</span>
${instanceNameHtml}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">${statusInfo.statusText}</div>
`;
+39 -1
View File
@@ -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
};
})();