Implement proper storage metrics (P0-009)\n\n- Add dedicated storage_metrics table\n- Create StorageMetricReport models with proper field names\n- Add ReportStorageMetrics to agent client\n- Update storage scanner to use new method\n- Implement server-side handlers and queries\n- Register new routes and update UI\n- Remove legacy Scan() method\n- Follow ETHOS principles: honest naming, clean architecture

This commit is contained in:
Fimeg
2025-12-17 16:38:36 -05:00
parent f7c8d23c5d
commit 0fff047cb5
43 changed files with 3641 additions and 248 deletions

View File

@@ -45,8 +45,8 @@ interface StorageMetrics {
export function AgentStorage({ agentId }: AgentStorageProps) {
const [isScanning, setIsScanning] = useState(false);
// Fetch agent's latest system info with enhanced disk data
const { data: agentData, refetch: refetchAgent } = useQuery({
// Fetch agent details and storage metrics
const { data: agentData } = useQuery({
queryKey: ['agent', agentId],
queryFn: async () => {
return await agentApi.getAgent(agentId);
@@ -54,6 +54,15 @@ export function AgentStorage({ agentId }: AgentStorageProps) {
refetchInterval: 30000, // Refresh every 30 seconds
});
// Fetch storage metrics from dedicated endpoint
const { data: storageData, refetch: refetchStorage } = useQuery({
queryKey: ['storage-metrics', agentId],
queryFn: async () => {
return await storageMetricsApi.getStorageMetrics(agentId);
},
refetchInterval: 30000, // Refresh every 30 seconds
});
const handleFullStorageScan = async () => {
setIsScanning(true);
try {
@@ -72,28 +81,49 @@ export function AgentStorage({ agentId }: AgentStorageProps) {
}
};
// Extract storage metrics from agent metadata
const storageMetrics: StorageMetrics | null = agentData ? {
cpu_percent: 0,
memory_percent: agentData.metadata?.memory_percent || 0,
memory_used_gb: agentData.metadata?.memory_used_gb || 0,
memory_total_gb: agentData.metadata?.memory_total_gb || 0,
disk_used_gb: agentData.metadata?.disk_used_gb || 0,
disk_total_gb: agentData.metadata?.disk_total_gb || 0,
disk_percent: agentData.metadata?.disk_percent || 0,
largest_disk_used_gb: agentData.metadata?.largest_disk_used_gb || 0,
largest_disk_total_gb: agentData.metadata?.largest_disk_total_gb || 0,
largest_disk_percent: agentData.metadata?.largest_disk_percent || 0,
largest_disk_mount: agentData.metadata?.largest_disk_mount || '',
uptime: agentData.metadata?.uptime || '',
} : null;
// Process storage metrics data
const storageMetrics: StorageMetrics | null = useMemo(() => {
if (!storageData?.metrics || storageData.metrics.length === 0) {
return null;
}
// Parse disk info from system information if available
// Find root disk for summary metrics
const rootDisk = storageData.metrics.find((m: any) => m.is_root) || storageData.metrics[0];
const largestDisk = storageData.metrics.find((m: any) => m.is_largest) || rootDisk;
return {
cpu_percent: 0, // CPU not included in storage metrics, comes from system metrics
memory_percent: 0, // Memory not included in storage metrics, comes from system metrics
memory_used_gb: 0,
memory_total_gb: 0,
disk_used_gb: largestDisk ? largestDisk.used_bytes / (1024 * 1024 * 1024) : 0,
disk_total_gb: largestDisk ? largestDisk.total_bytes / (1024 * 1024 * 1024) : 0,
disk_percent: largestDisk ? largestDisk.used_percent : 0,
largest_disk_used_gb: largestDisk ? largestDisk.used_bytes / (1024 * 1024 * 1024) : 0,
largest_disk_total_gb: largestDisk ? largestDisk.total_bytes / (1024 * 1024 * 1024) : 0,
largest_disk_percent: largestDisk ? largestDisk.used_percent : 0,
largest_disk_mount: largestDisk ? largestDisk.mountpoint : '',
uptime: '', // Uptime not included in storage metrics
};
}, [storageData]);
// Parse disk info from storage metrics
const parseDiskInfo = (): DiskInfo[] => {
const systemInfo = agentData?.system_info;
if (!systemInfo?.disk_info) return [];
if (!storageData?.metrics) return [];
return systemInfo.disk_info.map((disk: any) => ({
return storageData.metrics.map((disk: any) => ({
mountpoint: disk.mountpoint,
device: disk.device,
disk_type: disk.disk_type,
total: disk.total_bytes,
available: disk.available_bytes,
used: disk.used_bytes,
used_percent: disk.used_percent,
filesystem: disk.filesystem,
is_root: disk.is_root || false,
is_largest: disk.is_largest || false,
}));
};
mountpoint: disk.mountpoint,
total: disk.total,
available: disk.available,
@@ -170,29 +200,103 @@ export function AgentStorage({ agentId }: AgentStorageProps) {
</div>
)}
{/* All Disks from system_info.disk_info - BLUE matching Overview */}
{disks.length > 0 && disks.map((disk, index) => (
<div key={index}>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600 flex items-center">
<HardDrive className="h-4 w-4 mr-1" />
Disk ({disk.mountpoint})
</p>
<p className="text-sm font-medium text-gray-900">
{formatBytes(disk.used)} / {formatBytes(disk.total)}
</p>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-1">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${Math.min(disk.used_percent, 100)}%` }}
/>
</div>
<p className="text-xs text-gray-500 mt-1">
{disk.used_percent.toFixed(0)}% used
</p>
{/* Quick Overview - Simple disk bars for at-a-glance view */}
{disks.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900">Disk Usage (Overview)</h3>
{disks.map((disk, index) => (
<div key={`overview-${index}`}>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600 flex items-center">
<HardDrive className="h-4 w-4 mr-1" />
{disk.mountpoint} ({disk.filesystem})
</p>
<p className="text-sm font-medium text-gray-900">
{formatBytes(disk.used)} / {formatBytes(disk.total)} ({disk.used_percent.toFixed(0)}%)
</p>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-1">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${Math.min(disk.used_percent, 100)}%` }}
/>
</div>
</div>
))}
</div>
))}
)}
{/* Enhanced Disk Table - Shows all partitions with full details */}
{disks.length > 0 && (
<div className="overflow-hidden">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-900">Disk Partitions (Detailed)</h3>
<span className="text-xs text-gray-500">{disks.length} {disks.length === 1 ? 'partition' : 'partitions'} detected</span>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-50">
<tr className="border-b border-gray-200">
<th className="text-left py-2 px-3 font-medium text-gray-700">Mount</th>
<th className="text-left py-2 px-3 font-medium text-gray-700">Device</th>
<th className="text-left py-2 px-3 font-medium text-gray-700">Type</th>
<th className="text-left py-2 px-3 font-medium text-gray-700">FS</th>
<th className="text-right py-2 px-3 font-medium text-gray-700">Size</th>
<th className="text-right py-2 px-3 font-medium text-gray-700">Used</th>
<th className="text-center py-2 px-3 font-medium text-gray-700">%</th>
<th className="text-center py-2 px-3 font-medium text-gray-700">Flags</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{disks.map((disk, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="py-2 px-3 text-gray-900">
<div className="flex items-center">
<HardDrive className="h-3 w-3 mr-1 text-gray-400" />
<span className="font-medium text-xs">{disk.mountpoint}</span>
{disk.is_root && <span className="ml-1 text-[10px] text-blue-600 bg-blue-100 px-1 rounded">ROOT</span>}
{disk.is_largest && <span className="ml-1 text-[10px] text-purple-600 bg-purple-100 px-1 rounded">LARGEST</span>}
</div>
</td>
<td className="py-2 px-3 text-gray-700 text-xs font-mono">{disk.device}</td>
<td className="py-2 px-3 text-gray-700 text-xs capitalize">{disk.disk_type.toLowerCase()}</td>
<td className="py-2 px-3 text-gray-700 text-xs font-mono">{disk.filesystem}</td>
<td className="py-2 px-3 text-gray-900 text-xs text-right">{formatBytes(disk.total)}</td>
<td className="py-2 px-3 text-gray-900 text-xs text-right">{formatBytes(disk.used)}</td>
<td className="py-2 px-3 text-center">
<div className="inline-flex items-center">
<div className="w-12 bg-gray-200 rounded-full h-1.5 mr-2">
<div
className="bg-blue-600 h-1.5 rounded-full"
style={{ width: `${Math.min(disk.used_percent, 100)}%` }}
/>
</div>
<span className="text-xs font-medium">{disk.used_percent.toFixed(0)}%</span>
</div>
</td>
<td className="py-2 px-3 text-center text-xs text-gray-500">
{disk.severity !== 'low' && (
<span className={cn(
disk.severity === 'critical' ? 'text-red-600' :
disk.severity === 'important' ? 'text-amber-600' :
'text-yellow-600'
)}>
{disk.severity.toUpperCase()}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-3 text-xs text-gray-500">
Showing {disks.length} disk partitions Auto-refreshes every 30 seconds
</div>
</div>
)}
{/* Fallback if no disk array but we have metadata */}
{disks.length === 0 && storageMetrics && storageMetrics.disk_total_gb > 0 && (