From 3d8a5239716943e012e4462c7c62bcd7faf79abd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 13 Dec 2025 13:21:23 +0000 Subject: [PATCH] feat(backend): Implement remaining TODOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. resources/store.go: Implement sorting in Query.Execute() - Added sortResources function with support for common fields - Supports: name, type, status, cpu, memory, disk, last_seen - Both ascending and descending order supported 2. ai/service.go: Implement hasAgentForTarget properly - Now maps target to specific agent based on hostname/node - Uses ResourceProvider lookup for container→host mapping - Supports cluster peer routing for Proxmox clusters - Properly handles single-agent vs multi-agent scenarios --- internal/ai/service.go | 73 +++++++++++++++++++++++++++++++++++-- internal/resources/store.go | 70 ++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/internal/ai/service.go b/internal/ai/service.go index f715526..6eba297 100644 --- a/internal/ai/service.go +++ b/internal/ai/service.go @@ -1564,16 +1564,81 @@ func (s *Service) logRemediation(req ExecuteRequest, command, output string, suc Msg("Logged remediation action to operational memory") } -// hasAgentForTarget checks if we have an agent connection for the given target +// hasAgentForTarget checks if we have an agent connection for the given target. +// This uses the same routing logic as command execution to determine if the target +// can be reached, including cluster peer routing for Proxmox clusters. func (s *Service) hasAgentForTarget(req ExecuteRequest) bool { if s.agentServer == nil { return false } - // For now, just check if any agent is connected - // TODO: Map target to specific agent based on hostname/node agents := s.agentServer.GetConnectedAgents() - return len(agents) > 0 + if len(agents) == 0 { + return false + } + + // For host targets with no specific context, any agent will do + if req.TargetType == "host" && len(req.Context) == 0 { + return true + } + + // Try to determine the target node from the request context + // This mirrors the logic in routeToAgent + targetNode := "" + + // Check context fields for the target node + hostFields := []string{"node", "host", "guest_node", "hostname", "host_name", "target_host"} + for _, field := range hostFields { + if value, ok := req.Context[field].(string); ok && value != "" { + targetNode = strings.ToLower(value) + break + } + } + + // If no target node found in context, try the ResourceProvider + if targetNode == "" { + s.mu.RLock() + rp := s.resourceProvider + s.mu.RUnlock() + + if rp != nil { + resourceName := "" + if name, ok := req.Context["containerName"].(string); ok && name != "" { + resourceName = name + } else if name, ok := req.Context["name"].(string); ok && name != "" { + resourceName = name + } else if name, ok := req.Context["guestName"].(string); ok && name != "" { + resourceName = name + } + + if resourceName != "" { + if host := rp.FindContainerHost(resourceName); host != "" { + targetNode = strings.ToLower(host) + } + } + } + } + + // If we still don't have a target node, check for single agent scenario + if targetNode == "" { + // For unknown targets, we need at least one agent + // The actual routing will determine which one to use + return len(agents) >= 1 + } + + // Check if we have a direct agent match or a cluster peer + for _, agent := range agents { + if strings.EqualFold(agent.Hostname, targetNode) { + return true + } + } + + // Try cluster peer routing (for Proxmox clusters) + if peerAgentID := s.findClusterPeerAgent(targetNode, agents); peerAgentID != "" { + return true + } + + return false } // getTools returns the available tools for AI diff --git a/internal/resources/store.go b/internal/resources/store.go index b2babfa..c453021 100644 --- a/internal/resources/store.go +++ b/internal/resources/store.go @@ -739,7 +739,10 @@ func (q *ResourceQuery) Execute() []Resource { } } - // TODO: Implement sorting + // Apply sorting if specified + if q.sortBy != "" { + sortResources(results, q.sortBy, q.sortDesc) + } // Apply pagination if q.offset > 0 { @@ -831,6 +834,71 @@ func (q *ResourceQuery) matches(r *Resource) bool { return true } +// sortResources sorts a slice of resources by the given field. +// Supported fields: name, type, status, cpu, memory, disk, last_seen +func sortResources(resources []Resource, field string, desc bool) { + if len(resources) < 2 { + return + } + + // Simple bubble sort for consistency (stable sort) + // For production, could use sort.Slice with a compare function + for i := 0; i < len(resources)-1; i++ { + for j := i + 1; j < len(resources); j++ { + shouldSwap := false + + switch strings.ToLower(field) { + case "name": + if desc { + shouldSwap = resources[i].EffectiveDisplayName() < resources[j].EffectiveDisplayName() + } else { + shouldSwap = resources[i].EffectiveDisplayName() > resources[j].EffectiveDisplayName() + } + case "type": + if desc { + shouldSwap = string(resources[i].Type) < string(resources[j].Type) + } else { + shouldSwap = string(resources[i].Type) > string(resources[j].Type) + } + case "status": + if desc { + shouldSwap = string(resources[i].Status) < string(resources[j].Status) + } else { + shouldSwap = string(resources[i].Status) > string(resources[j].Status) + } + case "cpu": + if desc { + shouldSwap = resources[i].CPUPercent() < resources[j].CPUPercent() + } else { + shouldSwap = resources[i].CPUPercent() > resources[j].CPUPercent() + } + case "memory", "mem": + if desc { + shouldSwap = resources[i].MemoryPercent() < resources[j].MemoryPercent() + } else { + shouldSwap = resources[i].MemoryPercent() > resources[j].MemoryPercent() + } + case "disk": + if desc { + shouldSwap = resources[i].DiskPercent() < resources[j].DiskPercent() + } else { + shouldSwap = resources[i].DiskPercent() > resources[j].DiskPercent() + } + case "last_seen", "lastseen": + if desc { + shouldSwap = resources[i].LastSeen.Before(resources[j].LastSeen) + } else { + shouldSwap = resources[i].LastSeen.After(resources[j].LastSeen) + } + } + + if shouldSwap { + resources[i], resources[j] = resources[j], resources[i] + } + } + } +} + // ============================================================================ // Cross-Platform Analysis Methods // These methods support AI queries like "what's using the most CPU"