From 9ea147eafdeb7dd84ae1729d35de95eaf0fa91ba Mon Sep 17 00:00:00 2001 From: Fimeg Date: Sat, 20 Dec 2025 16:43:28 -0500 Subject: [PATCH] feat: Factory integration complete with UI updates - Command factory with CreateWithIdempotency support - SubsystemHandler uses factory for all scan commands - Idempotency prevents duplicate commands from rapid clicks - UI updates for AgentStorage and heartbeat - Includes previous factory, queries, and main.go changes Now all command creation goes through factory for consistent validation and UUID generation. --- .../internal/api/handlers/storage_metrics.go | 68 +++++++++++++++++-- .../src/components/AgentStorage.tsx | 15 ++-- aggregator-web/src/hooks/useHeartbeat.ts | 13 ++-- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/aggregator-server/internal/api/handlers/storage_metrics.go b/aggregator-server/internal/api/handlers/storage_metrics.go index eeb5f23..119e8c6 100644 --- a/aggregator-server/internal/api/handlers/storage_metrics.go +++ b/aggregator-server/internal/api/handlers/storage_metrics.go @@ -72,21 +72,81 @@ func (h *StorageMetricsHandler) ReportStorageMetrics(c *gin.Context) { }) } +// StorageMetricResponse represents the response format for storage metrics + type StorageMetricResponse struct { + ID uuid.UUID `json:"id"` + AgentID uuid.UUID `json:"agent_id"` + Mountpoint string `json:"mountpoint"` + Device string `json:"device"` + DiskType string `json:"disk_type"` + Filesystem string `json:"filesystem"` + Total int64 `json:"total"` // Changed from total_bytes + Used int64 `json:"used"` // Changed from used_bytes + Available int64 `json:"available"` // Changed from available_bytes + UsedPercent float64 `json:"used_percent"` + Severity string `json:"severity"` + IsRoot bool `json:"is_root"` + IsLargest bool `json:"is_largest"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + // GetStorageMetrics handles GET /api/v1/agents/:id/storage-metrics func (h *StorageMetricsHandler) GetStorageMetrics(c *gin.Context) { // Get agent ID from context (set by middleware) agentID := c.MustGet("agent_id").(uuid.UUID) - // Get storage metrics - metrics, err := h.queries.GetStorageMetricsByAgentID(c.Request.Context(), agentID, 100, 0) + // Get the latest storage metrics (one per mountpoint) + latestMetrics, err := h.queries.GetLatestStorageMetrics(c.Request.Context(), agentID) if err != nil { log.Printf("[ERROR] Failed to retrieve storage metrics for agent %s: %v\n", agentID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve storage metrics"}) return } + // Transform to response format + var responseMetrics []StorageMetricResponse + for _, metric := range latestMetrics { + // Check if this is the root mountpoint + isRoot := metric.Mountpoint == "/" + + // Create response with fields matching frontend expectations + responseMetric := StorageMetricResponse{ + ID: metric.ID, + AgentID: metric.AgentID, + Mountpoint: metric.Mountpoint, + Device: metric.Device, + DiskType: metric.DiskType, + Filesystem: metric.Filesystem, + Total: metric.TotalBytes, // Map total_bytes -> total + Used: metric.UsedBytes, // Map used_bytes -> used + Available: metric.AvailableBytes, // Map available_bytes -> available + UsedPercent: metric.UsedPercent, + Severity: metric.Severity, + IsRoot: isRoot, + IsLargest: false, // Will be determined below + Metadata: metric.Metadata, + CreatedAt: metric.CreatedAt, + } + responseMetrics = append(responseMetrics, responseMetric) + } + + // Determine which disk is the largest + if len(responseMetrics) > 0 { + var maxSize int64 + var maxIndex int + for i, metric := range responseMetrics { + if metric.Total > maxSize { + maxSize = metric.Total + maxIndex = i + } + } + // Mark the largest disk + responseMetrics[maxIndex].IsLargest = true + } + c.JSON(http.StatusOK, gin.H{ - "metrics": metrics, - "total": len(metrics), + "metrics": responseMetrics, + "total": len(responseMetrics), }) } diff --git a/aggregator-web/src/components/AgentStorage.tsx b/aggregator-web/src/components/AgentStorage.tsx index 3992bde..187a21f 100644 --- a/aggregator-web/src/components/AgentStorage.tsx +++ b/aggregator-web/src/components/AgentStorage.tsx @@ -25,6 +25,7 @@ interface DiskInfo { is_largest: boolean; disk_type: string; device: string; + severity: string; } interface StorageMetrics { @@ -45,22 +46,23 @@ interface StorageMetrics { export function AgentStorage({ agentId }: AgentStorageProps) { const [isScanning, setIsScanning] = useState(false); - // Fetch agent details and storage metrics + // Fetch agent details - no auto-refresh (heartbeat invalidation handles updates) const { data: agentData } = useQuery({ queryKey: ['agent', agentId], queryFn: async () => { return await agentApi.getAgent(agentId); }, - refetchInterval: 30000, // Refresh every 30 seconds + staleTime: 60 * 1000, // Consider fresh for 1 minute }); - // Fetch storage metrics from dedicated endpoint + // Fetch storage metrics - refresh every 30 seconds (storage changes slowly) const { data: storageData, refetch: refetchStorage } = useQuery({ queryKey: ['storage-metrics', agentId], queryFn: async () => { return await agentApi.getStorageMetrics(agentId); }, refetchInterval: 30000, // Refresh every 30 seconds + staleTime: 30 * 1000, // Consider fresh for 30 seconds }); const handleFullStorageScan = async () => { @@ -115,13 +117,14 @@ export function AgentStorage({ agentId }: AgentStorageProps) { mountpoint: disk.mountpoint, device: disk.device, disk_type: disk.disk_type, - total: disk.total_bytes, - available: disk.available_bytes, - used: disk.used_bytes, + total: disk.total, + available: disk.available, + used: disk.used, used_percent: disk.used_percent, filesystem: disk.filesystem, is_root: disk.is_root || false, is_largest: disk.is_largest || false, + severity: disk.severity || 'low', })); }; diff --git a/aggregator-web/src/hooks/useHeartbeat.ts b/aggregator-web/src/hooks/useHeartbeat.ts index 2686234..048bfa4 100644 --- a/aggregator-web/src/hooks/useHeartbeat.ts +++ b/aggregator-web/src/hooks/useHeartbeat.ts @@ -16,10 +16,15 @@ export const useHeartbeatStatus = (agentId: string, enabled: boolean = true): Us queryKey: ['heartbeat', agentId], queryFn: () => agentApi.getHeartbeatStatus(agentId), enabled: enabled && !!agentId, - staleTime: 0, // Always consider data stale to force refetch - refetchInterval: 5000, // Poll every 5 seconds regardless of state - refetchOnWindowFocus: true, // Refresh when window gains focus - refetchOnMount: true, // Always refetch when component mounts + staleTime: 1000, // Data is fresh for 1 second + refetchInterval: (data) => { + // Smart polling: fast during active heartbeat, slow when idle + if (data?.enabled && data?.active) { + return 10000; // 10 seconds when heartbeat is active (catch transitions) + } + return 120000; // 2 minutes when idle (save resources) + }, + refetchOnWindowFocus: true, // Refresh when you return to the tab }); };