"use client" import { useEffect, useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { getApiUrl } from "../lib/api-config" interface DiskInfo { name: string size?: number // Changed from string to number (KB) for formatMemory() size_formatted?: string // Added formatted size string for display temperature: number health: string power_on_hours?: number smart_status?: string model?: string serial?: string mountpoint?: string fstype?: string total?: number used?: number available?: number usage_percent?: number reallocated_sectors?: number pending_sectors?: number crc_errors?: number rotation_rate?: number power_cycles?: number percentage_used?: number // NVMe: Percentage Used (0-100) media_wearout_indicator?: number // SSD: Media Wearout Indicator wear_leveling_count?: number // SSD: Wear Leveling Count total_lbas_written?: number // SSD/NVMe: Total LBAs Written (GB) ssd_life_left?: number // SSD: SSD Life Left percentage } interface ZFSPool { name: string size: string allocated: string free: string health: string } interface StorageData { total: number used: number available: number disks: DiskInfo[] zfs_pools: ZFSPool[] disk_count: number healthy_disks: number warning_disks: number critical_disks: number error?: string } interface ProxmoxStorage { name: string type: string status: string total: number used: number available: number percent: number node: string // Added node property for detailed debug logging } interface ProxmoxStorageData { storage: ProxmoxStorage[] error?: string } const formatStorage = (sizeInGB: number): string => { if (sizeInGB < 1) { // Less than 1 GB, show in MB return `${(sizeInGB * 1024).toFixed(1)} MB` } else if (sizeInGB > 999) { return `${(sizeInGB / 1024).toFixed(2)} TB` } else { // Between 1 and 999 GB, show in GB return `${sizeInGB.toFixed(2)} GB` } } export function StorageOverview() { const [storageData, setStorageData] = useState(null) const [proxmoxStorage, setProxmoxStorage] = useState(null) const [loading, setLoading] = useState(true) const [selectedDisk, setSelectedDisk] = useState(null) const [detailsOpen, setDetailsOpen] = useState(false) const fetchStorageData = async () => { try { const [storageResponse, proxmoxResponse] = await Promise.all([ fetch(getApiUrl("/api/storage")), fetch(getApiUrl("/api/proxmox-storage")), ]) const data = await storageResponse.json() const proxmoxData = await proxmoxResponse.json() setStorageData(data) setProxmoxStorage(proxmoxData) } catch (error) { console.error("Error fetching storage data:", error) } finally { setLoading(false) } } useEffect(() => { fetchStorageData() const interval = setInterval(fetchStorageData, 60000) return () => clearInterval(interval) }, []) const getHealthIcon = (health: string) => { switch (health.toLowerCase()) { case "healthy": case "passed": case "online": return case "warning": return case "critical": case "failed": case "degraded": return default: return } } const getHealthBadge = (health: string) => { switch (health.toLowerCase()) { case "healthy": case "passed": case "online": return Healthy case "warning": return Warning case "critical": case "failed": case "degraded": return Critical default: return Unknown } } const getTempColor = (temp: number, diskName?: string, rotationRate?: number) => { if (temp === 0) return "text-gray-500" // Determinar el tipo de disco let diskType = "HDD" // Por defecto if (diskName) { if (diskName.startsWith("nvme")) { diskType = "NVMe" } else if (!rotationRate || rotationRate === 0) { diskType = "SSD" } } // Aplicar rangos de temperatura según el tipo switch (diskType) { case "NVMe": // NVMe: ≤70°C verde, 71-80°C amarillo, >80°C rojo if (temp <= 70) return "text-green-500" if (temp <= 80) return "text-yellow-500" return "text-red-500" case "SSD": // SSD: ≤59°C verde, 60-70°C amarillo, >70°C rojo if (temp <= 59) return "text-green-500" if (temp <= 70) return "text-yellow-500" return "text-red-500" case "HDD": default: // HDD: ≤45°C verde, 46-55°C amarillo, >55°C rojo if (temp <= 45) return "text-green-500" if (temp <= 55) return "text-yellow-500" return "text-red-500" } } const formatHours = (hours: number) => { if (hours === 0) return "N/A" const years = Math.floor(hours / 8760) const days = Math.floor((hours % 8760) / 24) if (years > 0) { return `${years}y ${days}d` } return `${days}d` } const formatRotationRate = (rpm: number | undefined) => { if (!rpm || rpm === 0) return "SSD" return `${rpm.toLocaleString()} RPM` } const getDiskType = (diskName: string, rotationRate: number | undefined): string => { if (diskName.startsWith("nvme")) { return "NVMe" } // rotation_rate = -1 means HDD but RPM is unknown (detected via kernel rotational flag) // rotation_rate = 0 or undefined means SSD // rotation_rate > 0 means HDD with known RPM if (rotationRate === -1) { return "HDD" } if (!rotationRate || rotationRate === 0) { return "SSD" } return "HDD" } const getDiskTypeBadge = (diskName: string, rotationRate: number | undefined) => { const diskType = getDiskType(diskName, rotationRate) const badgeStyles: Record = { NVMe: { className: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "NVMe", }, SSD: { className: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "SSD", }, HDD: { className: "bg-blue-500/10 text-blue-500 border-blue-500/20", label: "HDD", }, } return badgeStyles[diskType] } const handleDiskClick = (disk: DiskInfo) => { setSelectedDisk(disk) setDetailsOpen(true) } const getStorageTypeBadge = (type: string) => { const typeColors: Record = { pbs: "bg-purple-500/10 text-purple-500 border-purple-500/20", dir: "bg-blue-500/10 text-blue-500 border-blue-500/20", lvmthin: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", zfspool: "bg-green-500/10 text-green-500 border-green-500/20", nfs: "bg-orange-500/10 text-orange-500 border-orange-500/20", cifs: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", } return typeColors[type.toLowerCase()] || "bg-gray-500/10 text-gray-500 border-gray-500/20" } const getStatusIcon = (status: string) => { switch (status.toLowerCase()) { case "active": case "online": return case "inactive": case "offline": return case "error": case "failed": return default: return } } const getWearIndicator = (disk: DiskInfo): { value: number; label: string } | null => { const diskType = getDiskType(disk.name, disk.rotation_rate) if (diskType === "NVMe" && disk.percentage_used !== undefined && disk.percentage_used !== null) { return { value: disk.percentage_used, label: "Percentage Used" } } if (diskType === "SSD") { // Prioridad: Media Wearout Indicator > Wear Leveling Count > SSD Life Left if (disk.media_wearout_indicator !== undefined && disk.media_wearout_indicator !== null) { return { value: disk.media_wearout_indicator, label: "Media Wearout" } } if (disk.wear_leveling_count !== undefined && disk.wear_leveling_count !== null) { return { value: disk.wear_leveling_count, label: "Wear Level" } } if (disk.ssd_life_left !== undefined && disk.ssd_life_left !== null) { return { value: 100 - disk.ssd_life_left, label: "Life Used" } } } return null } const getWearColor = (wearPercent: number): string => { if (wearPercent <= 50) return "text-green-500" if (wearPercent <= 80) return "text-yellow-500" return "text-red-500" } const getEstimatedLifeRemaining = (disk: DiskInfo): string | null => { const wearIndicator = getWearIndicator(disk) if (!wearIndicator || !disk.power_on_hours || disk.power_on_hours === 0) { return null } const wearPercent = wearIndicator.value const hoursUsed = disk.power_on_hours // Si el desgaste es 0, no podemos calcular if (wearPercent === 0) { return "N/A" } // Calcular horas totales estimadas: hoursUsed / (wearPercent / 100) const totalEstimatedHours = hoursUsed / (wearPercent / 100) const remainingHours = totalEstimatedHours - hoursUsed // Convertir a años const remainingYears = remainingHours / 8760 // 8760 horas en un año if (remainingYears < 1) { const remainingMonths = Math.round(remainingYears * 12) return `~${remainingMonths} months` } return `~${remainingYears.toFixed(1)} years` } const getDiskHealthBreakdown = () => { if (!storageData || !storageData.disks) { return { normal: 0, warning: 0, critical: 0 } } let normal = 0 let warning = 0 let critical = 0 storageData.disks.forEach((disk) => { if (disk.temperature === 0) { // Si no hay temperatura, considerarlo normal normal++ return } const diskType = getDiskType(disk.name, disk.rotation_rate) switch (diskType) { case "NVMe": if (disk.temperature <= 70) normal++ else if (disk.temperature <= 80) warning++ else critical++ break case "SSD": if (disk.temperature <= 59) normal++ else if (disk.temperature <= 70) warning++ else critical++ break case "HDD": default: if (disk.temperature <= 45) normal++ else if (disk.temperature <= 55) warning++ else critical++ break } }) return { normal, warning, critical } } const getDiskTypesBreakdown = () => { if (!storageData || !storageData.disks) { return { nvme: 0, ssd: 0, hdd: 0 } } let nvme = 0 let ssd = 0 let hdd = 0 storageData.disks.forEach((disk) => { const diskType = getDiskType(disk.name, disk.rotation_rate) if (diskType === "NVMe") nvme++ else if (diskType === "SSD") ssd++ else if (diskType === "HDD") hdd++ }) return { nvme, ssd, hdd } } const getWearProgressColor = (wearPercent: number): string => { if (wearPercent < 70) return "[&>div]:bg-blue-500" if (wearPercent < 85) return "[&>div]:bg-yellow-500" return "[&>div]:bg-red-500" } const getUsageColor = (percent: number): string => { if (percent < 70) return "text-blue-500" if (percent < 85) return "text-yellow-500" if (percent < 95) return "text-orange-500" return "text-red-500" } const diskHealthBreakdown = getDiskHealthBreakdown() const diskTypesBreakdown = getDiskTypesBreakdown() const localStorageTypes = ["dir", "lvmthin", "lvm", "zfspool", "btrfs"] const remoteStorageTypes = ["pbs", "nfs", "cifs", "smb", "glusterfs", "iscsi", "iscsidirect", "rbd", "cephfs"] const totalLocalUsed = proxmoxStorage?.storage .filter( (storage) => storage && storage.name && storage.status === "active" && storage.total > 0 && storage.used >= 0 && storage.available >= 0 && localStorageTypes.includes(storage.type.toLowerCase()), ) .reduce((sum, storage) => sum + storage.used, 0) || 0 const totalLocalCapacity = proxmoxStorage?.storage .filter( (storage) => storage && storage.name && storage.status === "active" && storage.total > 0 && storage.used >= 0 && storage.available >= 0 && localStorageTypes.includes(storage.type.toLowerCase()), ) .reduce((sum, storage) => sum + storage.total, 0) || 0 const localUsagePercent = totalLocalCapacity > 0 ? ((totalLocalUsed / totalLocalCapacity) * 100).toFixed(2) : "0.00" const totalRemoteUsed = proxmoxStorage?.storage .filter( (storage) => storage && storage.name && storage.status === "active" && storage.total > 0 && storage.used >= 0 && storage.available >= 0 && remoteStorageTypes.includes(storage.type.toLowerCase()), ) .reduce((sum, storage) => sum + storage.used, 0) || 0 const totalRemoteCapacity = proxmoxStorage?.storage .filter( (storage) => storage && storage.name && storage.status === "active" && storage.total > 0 && storage.used >= 0 && storage.available >= 0 && remoteStorageTypes.includes(storage.type.toLowerCase()), ) .reduce((sum, storage) => sum + storage.total, 0) || 0 const remoteUsagePercent = totalRemoteCapacity > 0 ? ((totalRemoteUsed / totalRemoteCapacity) * 100).toFixed(2) : "0.00" const remoteStorageCount = proxmoxStorage?.storage.filter( (storage) => storage && storage.name && storage.status === "active" && remoteStorageTypes.includes(storage.type.toLowerCase()), ).length || 0 if (loading) { return (
Loading storage information...
) } if (!storageData || storageData.error) { return (
Error loading storage data: {storageData?.error || "Unknown error"}
) } return (
{/* Storage Summary */}
Total Storage
{storageData.total.toFixed(1)} TB

{storageData.disk_count} physical disks

Local Used
{formatStorage(totalLocalUsed)}

{localUsagePercent}% of {formatStorage(totalLocalCapacity)}

Remote Used
{remoteStorageCount > 0 ? formatStorage(totalRemoteUsed) : "None"}

{remoteStorageCount > 0 ? ( <> {remoteUsagePercent}% of {formatStorage(totalRemoteCapacity)} ) : ( No remote storage )}

Physical Disks
{storageData.disk_count} disks

{diskTypesBreakdown.nvme > 0 && {diskTypesBreakdown.nvme} NVMe} {diskTypesBreakdown.ssd > 0 && ( <> {diskTypesBreakdown.nvme > 0 && ", "} {diskTypesBreakdown.ssd} SSD )} {diskTypesBreakdown.hdd > 0 && ( <> {(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0) && ", "} {diskTypesBreakdown.hdd} HDD )}

{diskHealthBreakdown.normal} normal {diskHealthBreakdown.warning > 0 && ( <> {", "} {diskHealthBreakdown.warning} warning )} {diskHealthBreakdown.critical > 0 && ( <> {", "} {diskHealthBreakdown.critical} critical )}

{proxmoxStorage && proxmoxStorage.storage && proxmoxStorage.storage.length > 0 && ( Proxmox Storage
{proxmoxStorage.storage .filter( (storage) => storage && storage.name && storage.total > 0 && storage.used >= 0 && storage.available >= 0, ) .sort((a, b) => a.name.localeCompare(b.name)) .map((storage) => (
{/* Desktop: Icon + Name + Badge tipo alineados horizontalmente */}

{storage.name}

{storage.type}
{storage.type}

{storage.name}

{getStatusIcon(storage.status)}
{/* Desktop: Badge active + Porcentaje */}
{storage.status} {storage.percent}%
90 ? "[&>div]:bg-red-500" : storage.percent > 75 ? "[&>div]:bg-yellow-500" : "[&>div]:bg-blue-500" }`} />

Total

{formatStorage(storage.total)}

Used

90 ? "text-red-400" : storage.percent > 75 ? "text-yellow-400" : "text-blue-400" }`} > {formatStorage(storage.used)}

Available

{formatStorage(storage.available)}

))}
)} {/* ZFS Pools */} {storageData.zfs_pools && storageData.zfs_pools.length > 0 && ( ZFS Pools
{storageData.zfs_pools.map((pool) => (

{pool.name}

{getHealthBadge(pool.health)}
{getHealthIcon(pool.health)}

Size

{pool.size}

Allocated

{pool.allocated}

Free

{pool.free}

))}
)} {/* Physical Disks */} Physical Disks & SMART Status
{storageData.disks.map((disk) => (
handleDiskClick(disk)} >
{/* Row 1: Device name and type badge */}

/dev/{disk.name}

{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
{/* Row 2: Model, temperature, and health status */}
{disk.model && disk.model !== "Unknown" && (

{disk.model}

)}
{disk.temperature > 0 && (
{disk.temperature}°C
)} {getHealthBadge(disk.health)}
{disk.size_formatted && (

Size

{disk.size_formatted}

)} {disk.smart_status && disk.smart_status !== "unknown" && (

SMART Status

{disk.smart_status}

)} {disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (

Power On Time

{formatHours(disk.power_on_hours)}

)} {disk.serial && disk.serial !== "Unknown" && (

Serial

{disk.serial}

)}
handleDiskClick(disk)} >
{/* Row 1: Device name and type badge */}

/dev/{disk.name}

{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
{/* Row 2: Model, temperature, and health status */}
{disk.model && disk.model !== "Unknown" && (

{disk.model}

)}
{disk.temperature > 0 && (
{disk.temperature}°C
)} {getHealthBadge(disk.health)}
{disk.size_formatted && (

Size

{disk.size_formatted}

)} {disk.smart_status && disk.smart_status !== "unknown" && (

SMART Status

{disk.smart_status}

)} {disk.power_on_hours !== undefined && disk.power_on_hours > 0 && (

Power On Time

{formatHours(disk.power_on_hours)}

)} {disk.serial && disk.serial !== "Unknown" && (

Serial

{disk.serial}

)}
))}
{/* Disk Details Dialog */} Disk Details: /dev/{selectedDisk?.name} Complete SMART information and health status {selectedDisk && (

Model

{selectedDisk.model}

Serial Number

{selectedDisk.serial}

Capacity

{selectedDisk.size_formatted}

Health Status

{getHealthBadge(selectedDisk.health)}
{/* Wear & Lifetime Section */} {getWearIndicator(selectedDisk) && (

Wear & Lifetime

{getWearIndicator(selectedDisk)!.label}

{getWearIndicator(selectedDisk)!.value}%

{getEstimatedLifeRemaining(selectedDisk) && (

Estimated Life Remaining

{getEstimatedLifeRemaining(selectedDisk)}

{selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0 && (

Total Data Written

{selectedDisk.total_lbas_written >= 1024 ? `${(selectedDisk.total_lbas_written / 1024).toFixed(2)} TB` : `${selectedDisk.total_lbas_written.toFixed(2)} GB`}

)}
)}
)}

SMART Attributes

Temperature

{selectedDisk.temperature > 0 ? `${selectedDisk.temperature}°C` : "N/A"}

Power On Hours

{selectedDisk.power_on_hours && selectedDisk.power_on_hours > 0 ? `${selectedDisk.power_on_hours.toLocaleString()}h (${formatHours(selectedDisk.power_on_hours)})` : "N/A"}

Rotation Rate

{formatRotationRate(selectedDisk.rotation_rate)}

Power Cycles

{selectedDisk.power_cycles && selectedDisk.power_cycles > 0 ? selectedDisk.power_cycles.toLocaleString() : "N/A"}

SMART Status

{selectedDisk.smart_status}

Reallocated Sectors

0 ? "text-yellow-500" : ""}`} > {selectedDisk.reallocated_sectors ?? 0}

Pending Sectors

0 ? "text-yellow-500" : ""}`} > {selectedDisk.pending_sectors ?? 0}

CRC Errors

0 ? "text-yellow-500" : ""}`} > {selectedDisk.crc_errors ?? 0}

)}
) }