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
|
// GetStorageMetrics handles GET /api/v1/agents/:id/storage-metrics
|
||||||
func (h *StorageMetricsHandler) GetStorageMetrics(c *gin.Context) {
|
func (h *StorageMetricsHandler) GetStorageMetrics(c *gin.Context) {
|
||||||
// Get agent ID from context (set by middleware)
|
// Get agent ID from context (set by middleware)
|
||||||
agentID := c.MustGet("agent_id").(uuid.UUID)
|
agentID := c.MustGet("agent_id").(uuid.UUID)
|
||||||
|
|
||||||
// Get storage metrics
|
// Get the latest storage metrics (one per mountpoint)
|
||||||
metrics, err := h.queries.GetStorageMetricsByAgentID(c.Request.Context(), agentID, 100, 0)
|
latestMetrics, err := h.queries.GetLatestStorageMetrics(c.Request.Context(), agentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Failed to retrieve storage metrics for agent %s: %v\n", agentID, err)
|
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"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve storage metrics"})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"metrics": metrics,
|
"metrics": responseMetrics,
|
||||||
"total": len(metrics),
|
"total": len(responseMetrics),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface DiskInfo {
|
|||||||
is_largest: boolean;
|
is_largest: boolean;
|
||||||
disk_type: string;
|
disk_type: string;
|
||||||
device: string;
|
device: string;
|
||||||
|
severity: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StorageMetrics {
|
interface StorageMetrics {
|
||||||
@@ -45,22 +46,23 @@ interface StorageMetrics {
|
|||||||
export function AgentStorage({ agentId }: AgentStorageProps) {
|
export function AgentStorage({ agentId }: AgentStorageProps) {
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
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({
|
const { data: agentData } = useQuery({
|
||||||
queryKey: ['agent', agentId],
|
queryKey: ['agent', agentId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return await agentApi.getAgent(agentId);
|
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({
|
const { data: storageData, refetch: refetchStorage } = useQuery({
|
||||||
queryKey: ['storage-metrics', agentId],
|
queryKey: ['storage-metrics', agentId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return await agentApi.getStorageMetrics(agentId);
|
return await agentApi.getStorageMetrics(agentId);
|
||||||
},
|
},
|
||||||
refetchInterval: 30000, // Refresh every 30 seconds
|
refetchInterval: 30000, // Refresh every 30 seconds
|
||||||
|
staleTime: 30 * 1000, // Consider fresh for 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFullStorageScan = async () => {
|
const handleFullStorageScan = async () => {
|
||||||
@@ -115,13 +117,14 @@ export function AgentStorage({ agentId }: AgentStorageProps) {
|
|||||||
mountpoint: disk.mountpoint,
|
mountpoint: disk.mountpoint,
|
||||||
device: disk.device,
|
device: disk.device,
|
||||||
disk_type: disk.disk_type,
|
disk_type: disk.disk_type,
|
||||||
total: disk.total_bytes,
|
total: disk.total,
|
||||||
available: disk.available_bytes,
|
available: disk.available,
|
||||||
used: disk.used_bytes,
|
used: disk.used,
|
||||||
used_percent: disk.used_percent,
|
used_percent: disk.used_percent,
|
||||||
filesystem: disk.filesystem,
|
filesystem: disk.filesystem,
|
||||||
is_root: disk.is_root || false,
|
is_root: disk.is_root || false,
|
||||||
is_largest: disk.is_largest || 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],
|
queryKey: ['heartbeat', agentId],
|
||||||
queryFn: () => agentApi.getHeartbeatStatus(agentId),
|
queryFn: () => agentApi.getHeartbeatStatus(agentId),
|
||||||
enabled: enabled && !!agentId,
|
enabled: enabled && !!agentId,
|
||||||
staleTime: 0, // Always consider data stale to force refetch
|
staleTime: 1000, // Data is fresh for 1 second
|
||||||
refetchInterval: 5000, // Poll every 5 seconds regardless of state
|
refetchInterval: (data) => {
|
||||||
refetchOnWindowFocus: true, // Refresh when window gains focus
|
// Smart polling: fast during active heartbeat, slow when idle
|
||||||
refetchOnMount: true, // Always refetch when component mounts
|
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