mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 19:23:31 +00:00
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:
@@ -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');
|
||||
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user