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:
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user