feat(backend): Implement remaining TODOs

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
This commit is contained in:
rcourtman 2025-12-13 13:21:23 +00:00
parent c5ac33e8c6
commit 3d8a523971
2 changed files with 138 additions and 5 deletions

View file

@ -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

View file

@ -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"