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.
This commit is contained in:
Pulse Monitor
2025-09-04 15:24:22 +00:00
parent 810ec40ce1
commit b488f4db0a
4 changed files with 95 additions and 8 deletions
@@ -66,7 +66,13 @@ const CompactNodeCard: Component<CompactNodeCardProps> = (props) => {
} hover:shadow-sm transition-all cursor-pointer hover:scale-[1.01]`}
onClick={props.onClick}>
{/* Status dot */}
<span class={`w-2 h-2 rounded-full ${isOnline() ? 'bg-green-500' : 'bg-red-500'}`} />
<span class={`w-2 h-2 rounded-full ${
props.node.connectionHealth === 'degraded'
? 'bg-yellow-500'
: isOnline()
? 'bg-green-500'
: 'bg-red-500'
}`} />
{/* Node name */}
<a
@@ -127,7 +133,13 @@ const CompactNodeCard: Component<CompactNodeCardProps> = (props) => {
onClick={props.onClick}>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class={`w-2 h-2 rounded-full ${isOnline() ? 'bg-green-500' : 'bg-red-500'}`} />
<span class={`w-2 h-2 rounded-full ${
props.node.connectionHealth === 'degraded'
? 'bg-yellow-500'
: isOnline()
? 'bg-green-500'
: 'bg-red-500'
}`} />
<a
href={props.node.host || `https://${props.node.name}:8006`}
target="_blank"
@@ -886,11 +886,15 @@ const Settings: Component = () => {
// 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)
+31
View File
@@ -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',
},
});
+38 -2
View File
@@ -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,
}