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.
This commit is contained in:
Fimeg
2025-12-20 16:43:28 -05:00
parent 1a7abe7004
commit 9ea147eafd
3 changed files with 82 additions and 14 deletions

View File

@@ -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),
})
}

View File

@@ -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',
}));
};

View File

@@ -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
});
};