From b488f4db0af0177d60f22669bd0fbd2b40198322 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Thu, 4 Sep 2025 15:24:22 +0000 Subject: [PATCH] feat: add yellow status indicator for degraded cluster connectivity (addresses #379) When some cluster nodes are offline but the main node can still reach others, show a yellow status dot instead of red to indicate partial connectivity. This better represents the actual cluster health state. --- .../components/Dashboard/CompactNodeCard.tsx | 16 +++++++- .../src/components/Settings/Settings.tsx | 16 ++++++-- frontend-modern/vite.config.dev.ts | 31 ++++++++++++++ internal/monitoring/monitor.go | 40 ++++++++++++++++++- 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 frontend-modern/vite.config.dev.ts diff --git a/frontend-modern/src/components/Dashboard/CompactNodeCard.tsx b/frontend-modern/src/components/Dashboard/CompactNodeCard.tsx index df71f67c9..90fe70177 100644 --- a/frontend-modern/src/components/Dashboard/CompactNodeCard.tsx +++ b/frontend-modern/src/components/Dashboard/CompactNodeCard.tsx @@ -66,7 +66,13 @@ const CompactNodeCard: Component = (props) => { } hover:shadow-sm transition-all cursor-pointer hover:scale-[1.01]`} onClick={props.onClick}> {/* Status dot */} - + {/* Node name */} = (props) => { onClick={props.onClick}>
- + { // Find the corresponding node in the WebSocket state const stateNode = state.nodes.find(n => n.instance === node.name); // Check if the node has an unhealthy connection or is offline - if (stateNode?.connectionHealth === 'unhealthy' || stateNode?.status === 'offline') { + if (stateNode?.connectionHealth === 'unhealthy' || stateNode?.connectionHealth === 'error' || stateNode?.status === 'offline') { return 'bg-red-500'; } + // Check if connection is degraded (partial cluster connectivity) + if (stateNode?.connectionHealth === 'degraded') { + return 'bg-yellow-500'; + } // Check if we have a healthy connection - if (stateNode && stateNode.status === 'online') { + if (stateNode && (stateNode.status === 'online' || stateNode.connectionHealth === 'healthy')) { return 'bg-green-500'; } // Default to red if no state data (node is offline/unreachable) @@ -1156,11 +1160,15 @@ const Settings: Component = () => { // Find the corresponding PBS instance in the WebSocket state const statePBS = state.pbs.find(p => p.name === node.name); // Check if the PBS has an unhealthy connection or is offline - if (statePBS?.connectionHealth === 'unhealthy' || statePBS?.status === 'offline') { + if (statePBS?.connectionHealth === 'unhealthy' || statePBS?.connectionHealth === 'error' || statePBS?.status === 'offline') { return 'bg-red-500'; } + // Check if connection is degraded (not commonly used for PBS but keeping consistent) + if (statePBS?.connectionHealth === 'degraded') { + return 'bg-yellow-500'; + } // Check if we have a healthy connection - if (statePBS && statePBS.status === 'online') { + if (statePBS && (statePBS.status === 'online' || statePBS.connectionHealth === 'healthy')) { return 'bg-green-500'; } // Default to red if no state data (server is offline/unreachable) diff --git a/frontend-modern/vite.config.dev.ts b/frontend-modern/vite.config.dev.ts new file mode 100644 index 000000000..f697aeb4b --- /dev/null +++ b/frontend-modern/vite.config.dev.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite'; +import solid from 'vite-plugin-solid'; +import path from 'path'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 7655, + host: '0.0.0.0', + strictPort: true, // FAIL if port 7655 is not available + proxy: { + '/api': { + target: 'http://127.0.0.1:7656', + changeOrigin: true, + }, + '/ws': { + target: 'ws://127.0.0.1:7656', + ws: true, + changeOrigin: true, + }, + }, + }, + build: { + target: 'esnext', + }, +}); diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index aec7e67b6..ca8f4c3ee 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -665,7 +665,43 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie // Reset auth failures on successful connection m.resetAuthFailures(instanceName, "pve") - m.state.SetConnectionHealth(instanceName, true) + + // Check if client is a ClusterClient to determine health status + connectionHealthStr := "healthy" + if clusterClient, ok := client.(*proxmox.ClusterClient); ok { + // For cluster clients, check if all endpoints are healthy + healthStatus := clusterClient.GetHealthStatus() + healthyCount := 0 + totalCount := len(healthStatus) + + for _, isHealthy := range healthStatus { + if isHealthy { + healthyCount++ + } + } + + if healthyCount == 0 { + // All endpoints are down + connectionHealthStr = "error" + m.state.SetConnectionHealth(instanceName, false) + } else if healthyCount < totalCount { + // Some endpoints are down - degraded state + connectionHealthStr = "degraded" + m.state.SetConnectionHealth(instanceName, true) // Still functional but degraded + log.Warn(). + Str("instance", instanceName). + Int("healthy", healthyCount). + Int("total", totalCount). + Msg("Cluster is in degraded state - some nodes are unreachable") + } else { + // All endpoints are healthy + connectionHealthStr = "healthy" + m.state.SetConnectionHealth(instanceName, true) + } + } else { + // Regular client - simple healthy/unhealthy + m.state.SetConnectionHealth(instanceName, true) + } // Convert to models var modelNodes []models.Node @@ -693,7 +729,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie Uptime: int64(node.Uptime), LoadAverage: []float64{}, LastSeen: time.Now(), - ConnectionHealth: "healthy", + ConnectionHealth: connectionHealthStr, // Use the determined health status IsClusterMember: instanceCfg.IsCluster, ClusterName: instanceCfg.ClusterName, }