"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, Info, Clock, Usb, Server, Activity, FileText, Play, Loader2 } 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 { Button } from "@/components/ui/button" import { fetchApi } 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 io_errors?: { count: number severity: string sample: string reason: string error_type?: string // 'io' | 'filesystem' } observations_count?: number connection_type?: 'usb' | 'sata' | 'nvme' | 'sas' | 'internal' | 'unknown' removable?: boolean is_system_disk?: boolean system_usage?: string[] } interface DiskObservation { id: number error_type: string error_signature: string first_occurrence: string last_occurrence: string occurrence_count: number raw_message: string severity: string dismissed: boolean device_name: string serial: string model: string } 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 [diskObservations, setDiskObservations] = useState([]) const [loadingObservations, setLoadingObservations] = useState(false) const [activeModalTab, setActiveModalTab] = useState<"overview" | "smart">("overview") const fetchStorageData = async () => { try { const [data, proxmoxData] = await Promise.all([ fetchApi("/api/storage"), fetchApi("/api/proxmox-storage"), ]) 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 = async (disk: DiskInfo) => { setSelectedDisk(disk) setDetailsOpen(true) setDiskObservations([]) // Always attempt to fetch observations -- the count enrichment may lag // behind the actual observation recording (especially for USB disks). setLoadingObservations(true) try { const params = new URLSearchParams() if (disk.name) params.set('device', disk.name) if (disk.serial && disk.serial !== 'Unknown') params.set('serial', disk.serial) const data = await fetchApi<{ observations: DiskObservation[] }>(`/api/storage/observations?${params.toString()}`) setDiskObservations(data.observations || []) } catch { setDiskObservations([]) } finally { setLoadingObservations(false) } } const formatObsDate = (iso: string) => { if (!iso) return 'N/A' try { const d = new Date(iso) const day = d.getDate().toString().padStart(2, '0') const month = (d.getMonth() + 1).toString().padStart(2, '0') const year = d.getFullYear() const hours = d.getHours().toString().padStart(2, '0') const mins = d.getMinutes().toString().padStart(2, '0') return `${day}/${month}/${year} ${hours}:${mins}` } catch { return iso } } const obsTypeLabel = (t: string) => ({ smart_error: 'SMART Error', io_error: 'I/O Error', filesystem_error: 'Filesystem Error', zfs_pool_error: 'ZFS Pool Error', connection_error: 'Connection Error' }[t] || t) 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, usb: 0 } } let nvme = 0 let ssd = 0 let hdd = 0 let usb = 0 storageData.disks.forEach((disk) => { if (disk.connection_type === 'usb') { usb++ return } 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, usb } } 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 data...

Scanning disks, partitions and storage pools

) } 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 )} {diskTypesBreakdown.usb > 0 && ( <> {(diskTypesBreakdown.nvme > 0 || diskTypesBreakdown.ssd > 0 || diskTypesBreakdown.hdd > 0) && ", "} {diskTypesBreakdown.usb} USB )}

{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.used >= 0 && storage.available >= 0) .sort((a, b) => a.name.localeCompare(b.name)) .map((storage) => { // Check if storage is excluded from monitoring const isExcluded = storage.excluded === true const hasError = storage.status === "error" && !isExcluded return (
{/* Desktop: Icon + Name + Badge tipo alineados horizontalmente */}

{storage.name}

{storage.type} {isExcluded && ( excluded )}
{storage.type}

{storage.name}

{isExcluded ? ( excluded ) : ( getStatusIcon(storage.status) )}
{/* Desktop: Badge active + Porcentaje */}
{isExcluded ? "not monitored" : 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 (internal only) */} Physical Disks & SMART Status
{storageData.disks.filter(d => d.connection_type !== 'usb').map((disk) => (
handleDiskClick(disk)} >
{/* Row 1: Device name and type badge */}

/dev/{disk.name}

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

{disk.model}

)}
{disk.temperature > 0 && (
{disk.temperature}°C
)} {(disk.observations_count ?? 0) > 0 && ( {disk.observations_count} obs. )} {getHealthBadge(disk.health)}
{disk.io_errors && disk.io_errors.count > 0 && (
{disk.io_errors.error_type === 'filesystem' ? `Filesystem corruption detected` : `${disk.io_errors.count} I/O error${disk.io_errors.count !== 1 ? 's' : ''} in 5 min`}
)}
{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} {disk.is_system_disk && ( System )}
{/* Row 2: Model, temperature, and health status */}
{disk.model && disk.model !== "Unknown" && (

{disk.model}

)}
{disk.temperature > 0 && (
{disk.temperature}°C
)} {(disk.observations_count ?? 0) > 0 && ( {disk.observations_count} obs. )} {getHealthBadge(disk.health)}
{disk.io_errors && disk.io_errors.count > 0 && (
{disk.io_errors.error_type === 'filesystem' ? ( <> Filesystem corruption detected {disk.io_errors.reason && (

{disk.io_errors.reason}

)} ) : ( <> {disk.io_errors.count} I/O error{disk.io_errors.count !== 1 ? 's' : ''} in 5 min {disk.io_errors.sample && (

{disk.io_errors.sample}

)} )}
)}
{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.replace(/\\x[0-9a-fA-F]{2}/g, '')}

)}
))}
{/* External Storage (USB) */} {storageData.disks.filter(d => d.connection_type === 'usb').length > 0 && ( External Storage (USB)
{storageData.disks.filter(d => d.connection_type === 'usb').map((disk) => (
{/* Mobile card */}
handleDiskClick(disk)} >

/dev/{disk.name}

USB
{disk.model && disk.model !== "Unknown" && (

{disk.model}

)}
{disk.temperature > 0 && (
{disk.temperature}°C
)} {(disk.observations_count ?? 0) > 0 && ( {disk.observations_count} )} {getHealthBadge(disk.health)}
{/* USB Mobile: Size, SMART, Serial grid */}
{disk.size_formatted && (

Size

{disk.size_formatted}

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

SMART Status

{disk.smart_status}

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

Serial

{disk.serial.replace(/\\x[0-9a-fA-F]{2}/g, '')}

)}
{/* Desktop */}
handleDiskClick(disk)} >

/dev/{disk.name}

USB
{disk.temperature > 0 && (
{disk.temperature}°C
)} {getHealthBadge(disk.health)} {(disk.observations_count ?? 0) > 0 && ( {disk.observations_count} obs. )}
{disk.model && disk.model !== "Unknown" && (

{disk.model}

)} {disk.io_errors && disk.io_errors.count > 0 && (
{disk.io_errors.error_type === 'filesystem' ? ( <> Filesystem corruption detected {disk.io_errors.reason && (

{disk.io_errors.reason}

)} ) : ( <> {disk.io_errors.count} I/O error{disk.io_errors.count !== 1 ? 's' : ''} in 5 min {disk.io_errors.sample && (

{disk.io_errors.sample}

)} )}
)}
{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.replace(/\\x[0-9a-fA-F]{2}/g, '')}

)}
))}
)} {/* Disk Details Dialog */} { setDetailsOpen(open) if (!open) setActiveModalTab("overview") }}> {selectedDisk?.connection_type === 'usb' ? ( ) : ( )} Disk Details: /dev/{selectedDisk?.name} {selectedDisk?.connection_type === 'usb' && ( USB )} {selectedDisk?.is_system_disk && ( System )} {selectedDisk?.model !== "Unknown" ? selectedDisk?.model : "Physical disk"} - {selectedDisk?.size_formatted} {/* Tab Navigation */}
{/* Tab Content */}
{selectedDisk && activeModalTab === "overview" && (

Model

{selectedDisk.model}

Serial Number

{selectedDisk.serial?.replace(/\\x[0-9a-fA-F]{2}/g, '') || 'Unknown'}

Capacity

{selectedDisk.size_formatted}

Health Status

{getHealthBadge(selectedDisk.health)} {(selectedDisk.observations_count ?? 0) > 0 && ( {selectedDisk.observations_count} obs. )}
{/* 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}

{/* Observations Section */} {(diskObservations.length > 0 || loadingObservations) && (

Observations {diskObservations.length}

The following observations have been recorded for this disk:

{loadingObservations ? (
Loading observations...
) : (
{diskObservations.map((obs) => (
{/* Header with type badge */}
{obsTypeLabel(obs.error_type)}
{/* Error message - responsive text wrap */}

{obs.raw_message}

{/* Dates - stacked on mobile, inline on desktop */}
First: {formatObsDate(obs.first_occurrence)} Last: {formatObsDate(obs.last_occurrence)}
{/* Occurrences count */}
Occurrences: {obs.occurrence_count}
))}
)}
)}
)} {/* SMART Test Tab */} {selectedDisk && activeModalTab === "smart" && ( )}
) } // SMART Test Tab Component interface SmartTestTabProps { disk: DiskInfo } interface SmartTestStatus { status: 'idle' | 'running' | 'completed' | 'failed' test_type?: string progress?: number result?: string last_test?: { type: string status: string timestamp: string duration?: string } smart_data?: { device: string model: string serial: string firmware: string smart_status: string temperature: number power_on_hours: number attributes: Array<{ id: number name: string value: number worst: number threshold: number raw_value: string status: 'ok' | 'warning' | 'critical' }> } } function SmartTestTab({ disk }: SmartTestTabProps) { const [testStatus, setTestStatus] = useState({ status: 'idle' }) const [loading, setLoading] = useState(true) const [runningTest, setRunningTest] = useState<'short' | 'long' | null>(null) const [showReport, setShowReport] = useState(false) const [reportTab, setReportTab] = useState<'overview' | 'attributes' | 'history' | 'recommendations'>('overview') // Fetch current SMART status on mount useEffect(() => { fetchSmartStatus() }, [disk.name]) const fetchSmartStatus = async () => { try { setLoading(true) const data = await fetchApi(`/api/storage/smart/${disk.name}`) setTestStatus(data) } catch { setTestStatus({ status: 'idle' }) } finally { setLoading(false) } } const runSmartTest = async (testType: 'short' | 'long') => { try { setRunningTest(testType) await fetchApi(`/api/storage/smart/${disk.name}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test_type: testType }) }) // Poll for status updates const pollInterval = setInterval(async () => { try { const data = await fetchApi(`/api/storage/smart/${disk.name}`) setTestStatus(data) if (data.status !== 'running') { clearInterval(pollInterval) setRunningTest(null) } } catch { clearInterval(pollInterval) setRunningTest(null) } }, 5000) } catch { setRunningTest(null) } } if (loading) { return (

Loading SMART data...

) } return (
{/* Quick Actions */}

Run SMART Test

Short test takes ~2 minutes. Extended test runs in the background and can take several hours for large disks. You will receive a notification when the test completes.

{/* Test Progress */} {testStatus.status === 'running' && (

{testStatus.test_type === 'short' ? 'Short' : 'Extended'} test in progress

Please wait while the test completes...

{testStatus.progress !== undefined && ( )}
)} {/* Last Test Result */} {testStatus.last_test && (

Last Test Result

{testStatus.last_test.status === 'passed' ? ( ) : ( )} {testStatus.last_test.type === 'short' ? 'Short' : 'Extended'} Test - {' '} {testStatus.last_test.status === 'passed' ? 'Passed' : 'Failed'}
{testStatus.last_test.status}

Completed

{testStatus.last_test.timestamp}

{testStatus.last_test.duration && (

Duration

{testStatus.last_test.duration}

)}
)} {/* SMART Attributes Summary */} {testStatus.smart_data?.attributes && testStatus.smart_data.attributes.length > 0 && (

SMART Attributes

ID
Attribute
Value
Worst
Status
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
{attr.id}
{attr.name}
{attr.value}
{attr.worst}
{attr.status === 'ok' ? ( ) : attr.status === 'warning' ? ( ) : ( )}
))}
)} {/* View Full Report Button */}

Generate a comprehensive professional report with detailed analysis and recommendations.

{/* Full SMART Report Dialog */} SMART Health Report: /dev/{disk.name} Comprehensive analysis of disk health, SMART attributes, and recommendations {/* Report Tabs */}
{/* Report Content */}
{/* Overview Tab */} {reportTab === 'overview' && (
{/* Health Score Card */}

Overall Health

{testStatus.smart_status === 'passed' ? ( ) : testStatus.smart_status === 'failed' ? ( ) : ( )} {testStatus.smart_status || 'Unknown'}

Temperature

{disk.temperature > 0 ? `${disk.temperature}°C` : 'N/A'}

Power On Time

{disk.power_on_hours ? `${disk.power_on_hours.toLocaleString()}h` : 'N/A'}
{/* Executive Summary */}

Executive Summary

{testStatus.smart_status === 'passed' ? ( <> This disk is operating within normal parameters. All SMART attributes are within acceptable thresholds, indicating good health. The disk has been powered on for approximately{' '} {disk.power_on_hours ? `${Math.round(disk.power_on_hours / 24)} days` : 'an unknown period'} {' '} and is currently operating at{' '} {disk.temperature || 'N/A'}°C. {disk.reallocated_sectors === 0 && disk.pending_sectors === 0 ? ' No bad sectors have been detected.' : disk.reallocated_sectors && disk.reallocated_sectors > 0 ? ` ${disk.reallocated_sectors} sectors have been reallocated, which may indicate early signs of wear.` : ''} ) : testStatus.smart_status === 'failed' ? ( <> Warning: This disk has failed SMART health assessment.{' '} One or more critical SMART attributes have exceeded their failure threshold. It is strongly recommended to backup all data immediately and consider replacing this disk. {disk.reallocated_sectors && disk.reallocated_sectors > 0 ? ` The disk has ${disk.reallocated_sectors} reallocated sectors, indicating physical media degradation.` : ''} ) : ( <> The disk health status could not be fully determined. Some SMART attributes may be showing warning signs. It is recommended to run a full SMART self-test and monitor the disk closely. )}

{/* Key Metrics */}

Key Metrics

Model

{disk.model || 'Unknown'}

Serial

{disk.serial?.replace(/\\x[0-9a-fA-F]{2}/g, '') || 'Unknown'}

Capacity

{disk.size_formatted || 'Unknown'}

Power Cycles

{disk.power_cycles?.toLocaleString() || 'N/A'}

Reallocated Sectors

0 ? 'text-yellow-500' : ''}`}> {disk.reallocated_sectors ?? 0}

Pending Sectors

0 ? 'text-yellow-500' : ''}`}> {disk.pending_sectors ?? 0}

CRC Errors

0 ? 'text-yellow-500' : ''}`}> {disk.crc_errors ?? 0}

Disk Type

{disk.name.startsWith('nvme') ? 'NVMe' : !disk.rotation_rate || disk.rotation_rate === 0 ? 'SSD' : 'HDD'}

)} {/* Attributes Tab */} {reportTab === 'attributes' && (

Understanding SMART Attributes

SMART (Self-Monitoring, Analysis and Reporting Technology) attributes are sensors built into hard drives and SSDs. Each attribute has a current value, a worst recorded value, and a threshold. When the current value drops below the threshold, the attribute is considered failed. Values typically decrease from 100 (or 200/253 on some drives) as the attribute degrades.

{testStatus.smart_data?.attributes && testStatus.smart_data.attributes.length > 0 ? (
ID
Attribute Name
Value
Worst
Threshold
Status
{testStatus.smart_data.attributes.map((attr) => (
{attr.id}

{attr.name.replace(/_/g, ' ')}

Raw: {attr.raw_value}

{attr.value}
{attr.worst}
{attr.threshold}
{attr.status === 'ok' ? ( ) : attr.status === 'warning' ? ( ) : ( )}
))}
) : (

No SMART attribute data available.

Run a SMART test to collect attribute data.

)}
)} {/* History Tab */} {reportTab === 'history' && (
{testStatus.last_test ? (

{testStatus.last_test.status === 'passed' ? ( ) : ( )} Last Test Result

{testStatus.last_test.status}

Test Type

{testStatus.last_test.type}

Completed

{testStatus.last_test.timestamp}

) : (

No test history available.

Run a SMART self-test to see results here.

)}

About Self-Tests

Short Test (~2 minutes): Performs a quick check of the disk's basic functionality including read/seek tests on a small portion of the disk surface.

Extended Test (hours): Performs a comprehensive surface scan of the entire disk. Duration depends on disk size - typically 1-2 hours per TB.

)} {/* Recommendations Tab */} {reportTab === 'recommendations' && (
{/* Status-based recommendations */} {testStatus.smart_status === 'passed' && (

Disk is Healthy

All SMART attributes are within normal ranges. Continue with regular monitoring.

)} {testStatus.smart_status === 'failed' && (

Critical: Disk Replacement Recommended

This disk has failed SMART health assessment. Backup all data immediately and plan for disk replacement.

)} {/* Conditional recommendations */}

Recommendations

{(disk.reallocated_sectors ?? 0) > 0 && (

Reallocated Sectors Detected

{disk.reallocated_sectors} sectors have been reallocated. This indicates the disk has found and remapped bad sectors. Monitor this value - if it increases rapidly, consider replacing the disk.

)} {(disk.pending_sectors ?? 0) > 0 && (

Pending Sectors Detected

{disk.pending_sectors} sectors are pending reallocation. These sectors may be unreadable. Run an extended self-test to force reallocation attempts.

)} {disk.temperature > 55 && (

Elevated Temperature

Current temperature ({disk.temperature}°C) is above optimal. Improve airflow or reduce disk activity. Sustained high temperatures can reduce disk lifespan.

)} {(disk.power_on_hours ?? 0) > 35000 && (

High Power-On Hours

This disk has been running for {Math.round((disk.power_on_hours ?? 0) / 8760)} years. While still operational, consider planning for replacement as disks typically have a 3-5 year lifespan.

)} {/* General best practices */}

Best Practices

  • Run a short SMART test monthly to catch early issues
  • Run an extended test quarterly for comprehensive verification
  • Maintain regular backups - SMART can detect some failures but not all
  • Keep disk temperatures below 50°C for optimal lifespan
  • Replace disks proactively after 4-5 years of heavy use
)}
{/* Report Footer */}

Report generated by ProxMenux Monitor

) }