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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user