From 21d784164a211a2cf630e46d45d12e5e1ec98dae Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Sun, 31 Aug 2025 18:01:47 +0000 Subject: [PATCH] fix: tag indicators now only show for guests that actually have tags - Added ToFrontend() method to StateSnapshot for proper data conversion - Modified /api/state endpoint to use frontend-formatted data - Enhanced WebSocket store to handle tag data transformation consistently - Ensures tags are properly converted between backend strings and frontend arrays --- frontend-modern/src/stores/websocket.ts | 54 ++++++++++++++++++++++++- internal/api/router.go | 3 +- internal/models/state_snapshot.go | 41 +++++++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/frontend-modern/src/stores/websocket.ts b/frontend-modern/src/stores/websocket.ts index bbc726ad6..6f335b801 100644 --- a/frontend-modern/src/stores/websocket.ts +++ b/frontend-modern/src/stores/websocket.ts @@ -99,8 +99,58 @@ export function createWebSocketStore(url: string) { console.log('[WebSocket] Updating nodes:', message.data.nodes?.length || 0); setState('nodes', message.data.nodes); } - if (message.data.vms !== undefined) setState('vms', message.data.vms); - if (message.data.containers !== undefined) setState('containers', message.data.containers); + if (message.data.vms !== undefined) { + // Transform tags from comma-separated strings to arrays + const transformedVMs = message.data.vms.map((vm: any) => { + const originalTags = vm.tags; + let transformedTags; + + if (originalTags && typeof originalTags === 'string' && originalTags.trim()) { + // String with content - split into array + transformedTags = originalTags.split(',').map((t: string) => t.trim()).filter((t: string) => t.length > 0); + } else if (Array.isArray(originalTags)) { + // Already an array - filter out empty/whitespace-only tags + transformedTags = originalTags.filter((tag: any) => + typeof tag === 'string' && tag.trim().length > 0 + ); + } else { + // null, undefined, empty string, or other - convert to empty array + transformedTags = []; + } + + return { + ...vm, + tags: transformedTags + }; + }); + setState('vms', transformedVMs); + } + if (message.data.containers !== undefined) { + // Transform tags from comma-separated strings to arrays + const transformedContainers = message.data.containers.map((container: any) => { + const originalTags = container.tags; + let transformedTags; + + if (originalTags && typeof originalTags === 'string' && originalTags.trim()) { + // String with content - split into array + transformedTags = originalTags.split(',').map((t: string) => t.trim()).filter((t: string) => t.length > 0); + } else if (Array.isArray(originalTags)) { + // Already an array - filter out empty/whitespace-only tags + transformedTags = originalTags.filter((tag: any) => + typeof tag === 'string' && tag.trim().length > 0 + ); + } else { + // null, undefined, empty string, or other - convert to empty array + transformedTags = []; + } + + return { + ...container, + tags: transformedTags + }; + }); + setState('containers', transformedContainers); + } if (message.data.storage !== undefined) setState('storage', message.data.storage); if (message.data.pbs !== undefined) setState('pbs', message.data.pbs); if (message.data.pbsBackups !== undefined) setState('pbsBackups', message.data.pbsBackups); diff --git a/internal/api/router.go b/internal/api/router.go index ad7030663..a116a0d47 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1513,8 +1513,9 @@ func (r *Router) handleState(w http.ResponseWriter, req *http.Request) { } state := r.monitor.GetState() + frontendState := state.ToFrontend() - if err := utils.WriteJSONResponse(w, state); err != nil { + if err := utils.WriteJSONResponse(w, frontendState); err != nil { log.Error().Err(err).Msg("Failed to encode state response") writeErrorResponse(w, http.StatusInternalServerError, "encoding_error", "Failed to encode state data", nil) diff --git a/internal/models/state_snapshot.go b/internal/models/state_snapshot.go index 97cd29996..b6f2a10e2 100644 --- a/internal/models/state_snapshot.go +++ b/internal/models/state_snapshot.go @@ -53,4 +53,45 @@ func (s *State) GetSnapshot() StateSnapshot { } return snapshot +} + +// ToFrontend converts a StateSnapshot to frontend format with proper tag handling +func (s StateSnapshot) ToFrontend() StateFrontend { + // Convert nodes + nodes := make([]NodeFrontend, len(s.Nodes)) + for i, n := range s.Nodes { + nodes[i] = n.ToFrontend() + } + + // Convert VMs + vms := make([]VMFrontend, len(s.VMs)) + for i, v := range s.VMs { + vms[i] = v.ToFrontend() + } + + // Convert containers + containers := make([]ContainerFrontend, len(s.Containers)) + for i, c := range s.Containers { + containers[i] = c.ToFrontend() + } + + // Convert storage + storage := make([]StorageFrontend, len(s.Storage)) + for i, st := range s.Storage { + storage[i] = st.ToFrontend() + } + + return StateFrontend{ + Nodes: nodes, + VMs: vms, + Containers: containers, + Storage: storage, + PBS: s.PBSInstances, + Metrics: make(map[string]any), + PVEBackups: s.PVEBackups, + Performance: make(map[string]any), + ConnectionHealth: s.ConnectionHealth, + Stats: make(map[string]any), + LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp + } } \ No newline at end of file