"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, Download, Plus, Trash2, Settings } 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" | "history" | "schedule">("overview") const [smartJsonData, setSmartJsonData] = useState<{ has_data: boolean data?: Record timestamp?: string test_type?: string history?: Array<{ filename: string; timestamp: string; test_type: string; date_readable: string }> } | null>(null) const [loadingSmartJson, setLoadingSmartJson] = useState(false) 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, 30000) 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([]) setSmartJsonData(null) // Fetch observations and SMART JSON data in parallel setLoadingObservations(true) setLoadingSmartJson(true) // Fetch observations const fetchObservations = async () => { 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) } } // Fetch SMART JSON data from real test if available const fetchSmartJson = async () => { try { const data = await fetchApi<{ has_data: boolean data?: Record timestamp?: string test_type?: string }>(`/api/storage/smart/${disk.name}/latest`) setSmartJsonData(data) } catch { setSmartJsonData({ has_data: false }) } finally { setLoadingSmartJson(false) } } // Run both in parallel await Promise.all([fetchObservations(), fetchSmartJson()]) } 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") setSmartJsonData(null) } }}> {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 — DiskInfo (real-time, 60s refresh) for NVMe + SSD. SMART JSON as fallback. HDD: hidden. */} {(() => { let wearUsed: number | null = null let lifeRemaining: number | null = null let estimatedLife = '' let dataWritten = '' let spare: number | undefined // --- Step 1: DiskInfo = primary source (refreshed every 60s, always fresh) --- // Works for NVMe (percentage_used) and SSD (media_wearout_indicator, ssd_life_left) const wi = getWearIndicator(selectedDisk) if (wi) { wearUsed = wi.value lifeRemaining = 100 - wearUsed estimatedLife = getEstimatedLifeRemaining(selectedDisk) || '' if (selectedDisk.total_lbas_written && selectedDisk.total_lbas_written > 0) { const tb = selectedDisk.total_lbas_written / 1024 dataWritten = tb >= 1 ? `${tb.toFixed(2)} TB` : `${selectedDisk.total_lbas_written.toFixed(2)} GB` } } // --- Step 2: SMART test JSON — primary for SSD, supplement for NVMe --- if (smartJsonData?.has_data && smartJsonData.data) { const data = smartJsonData.data as Record const nvmeHealth = (data?.nvme_smart_health_information_log || data) as Record // Available spare (only from SMART/NVMe data) if (spare === undefined) { spare = (nvmeHealth?.avail_spare ?? nvmeHealth?.available_spare) as number | undefined } // Data written — use SMART JSON if DiskInfo didn't provide it if (!dataWritten) { const ataAttrs = data?.ata_smart_attributes as { table?: Array<{ id: number; name: string; value: number; raw?: { value: number } }> } const table = ataAttrs?.table || [] const lbasAttr = table.find(a => a.name?.toLowerCase().includes('total_lbas_written') || a.name?.toLowerCase().includes('writes_gib') || a.name?.toLowerCase().includes('lifetime_writes') || a.id === 241 ) if (lbasAttr && lbasAttr.raw?.value) { const n = (lbasAttr.name || '').toLowerCase() const tb = (n.includes('gib') || n.includes('_gb') || n.includes('writes_gib')) ? lbasAttr.raw.value / 1024 : (lbasAttr.raw.value * 512) / (1024 ** 4) dataWritten = tb >= 1 ? `${tb.toFixed(2)} TB` : `${(tb * 1024).toFixed(2)} GB` } else if (nvmeHealth?.data_units_written) { const tb = ((nvmeHealth.data_units_written as number) * 512000) / (1024 ** 4) dataWritten = tb >= 1 ? `${tb.toFixed(2)} TB` : `${(tb * 1024).toFixed(2)} GB` } } // Wear/life — use SMART JSON only if DiskInfo didn't provide it (SSD without backend support) if (lifeRemaining === null) { const ataAttrs = data?.ata_smart_attributes as { table?: Array<{ id: number; name: string; value: number; raw?: { value: number } }> } const table = ataAttrs?.table || [] const wearAttr = table.find(a => a.name?.toLowerCase().includes('wear_leveling') || a.name?.toLowerCase().includes('media_wearout') || a.name?.toLowerCase().includes('ssd_life_left') || a.id === 177 || a.id === 231 ) const nvmeIsPresent = nvmeHealth?.percent_used !== undefined || nvmeHealth?.percentage_used !== undefined if (wearAttr) { lifeRemaining = (wearAttr.id === 230) ? (100 - wearAttr.value) : wearAttr.value } else if (nvmeIsPresent) { lifeRemaining = 100 - ((nvmeHealth.percent_used ?? nvmeHealth.percentage_used ?? 0) as number) } if (lifeRemaining !== null) { wearUsed = 100 - lifeRemaining const poh = selectedDisk.power_on_hours || 0 if (lifeRemaining > 0 && lifeRemaining < 100 && poh > 0) { const used = 100 - lifeRemaining if (used > 0) { const ry = ((poh / (used / 100)) - poh) / (24 * 365) estimatedLife = ry >= 1 ? `~${ry.toFixed(1)} years` : `~${(ry * 12).toFixed(0)} months` } } } } } // --- Only render if we have meaningful wear data --- if (wearUsed === null && lifeRemaining === null) return null const lifeColor = lifeRemaining !== null ? (lifeRemaining >= 50 ? '#22c55e' : lifeRemaining >= 20 ? '#eab308' : '#ef4444') : '#6b7280' return (

Wear & Lifetime {smartJsonData?.has_data && !wi && ( Real Test )}

{lifeRemaining !== null && (
{lifeRemaining}% life
)}
{wearUsed !== null && (

Wear

{wearUsed}%

)}
{estimatedLife && (

Est. Life

{estimatedLife}

)} {dataWritten && (

Data Written

{dataWritten}

)} {spare !== undefined && (

Avail. Spare

{spare}%

)}
) })()}

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}

{/* OLD SMART Test Data section removed — now unified in Wear & Lifetime above */} {false && (

{(() => { // Check if this is SSD without Proxmox wear data - show as "Wear & Lifetime" const isNvme = selectedDisk.name?.includes('nvme') const hasProxmoxWear = getWearIndicator(selectedDisk) !== null if (!isNvme && !hasProxmoxWear && smartJsonData?.has_data) { return 'Wear & Lifetime' } return 'SMART Test Data' })()} {smartJsonData?.has_data && ( Real Test )}

{loadingSmartJson ? (
Loading SMART test data...
) : smartJsonData?.has_data && smartJsonData.data ? (
{/* SSD/NVMe Life Estimation from JSON - Uniform style */} {(() => { const data = smartJsonData.data as Record const ataAttrs = data?.ata_smart_attributes as { table?: Array<{ id: number; name: string; value: number; raw?: { value: number } }> } const table = ataAttrs?.table || [] // Look for wear-related attributes for SSD const wearAttr = table.find(a => a.name?.toLowerCase().includes('wear_leveling') || a.name?.toLowerCase().includes('media_wearout') || a.name?.toLowerCase().includes('percent_lifetime') || a.name?.toLowerCase().includes('ssd_life_left') || a.id === 177 || a.id === 231 || a.id === 233 ) // Look for total LBAs written const lbasAttr = table.find(a => a.name?.toLowerCase().includes('total_lbas_written') || a.id === 241 ) // Look for power on hours from SMART data const pohAttr = table.find(a => a.name?.toLowerCase().includes('power_on_hours') || a.id === 9 ) // For NVMe, check nvme_smart_health_information_log const nvmeHealth = data?.nvme_smart_health_information_log as Record // Calculate data written let dataWrittenTB = 0 let dataWrittenLabel = '' if (lbasAttr && lbasAttr.raw?.value) { dataWrittenTB = (lbasAttr.raw.value * 512) / (1024 ** 4) dataWrittenLabel = dataWrittenTB >= 1 ? `${dataWrittenTB.toFixed(2)} TB` : `${(dataWrittenTB * 1024).toFixed(2)} GB` } else if (nvmeHealth?.data_units_written) { const units = nvmeHealth.data_units_written as number dataWrittenTB = (units * 512000) / (1024 ** 4) dataWrittenLabel = dataWrittenTB >= 1 ? `${dataWrittenTB.toFixed(2)} TB` : `${(dataWrittenTB * 1024).toFixed(2)} GB` } // Get wear percentage (life remaining %) let wearPercent: number | null = null let wearLabel = 'Life Remaining' if (wearAttr) { if (wearAttr.id === 230) { // Media_Wearout_Indicator (WD/SanDisk): value = endurance used % wearPercent = 100 - wearAttr.value } else { // Standard: value = normalized life remaining % wearPercent = wearAttr.value } wearLabel = 'Life Remaining' } else if (nvmeHealth?.percentage_used !== undefined) { wearPercent = 100 - (nvmeHealth.percentage_used as number) wearLabel = 'Life Remaining' } // Calculate estimated life remaining let estimatedLife = '' const powerOnHours = pohAttr?.raw?.value || selectedDisk.power_on_hours || 0 if (wearPercent !== null && wearPercent > 0 && wearPercent < 100 && powerOnHours > 0) { const usedPercent = 100 - wearPercent if (usedPercent > 0) { const totalEstimatedHours = powerOnHours / (usedPercent / 100) const remainingHours = totalEstimatedHours - powerOnHours const remainingYears = remainingHours / (24 * 365) if (remainingYears >= 1) { estimatedLife = `~${remainingYears.toFixed(1)} years` } else { const remainingMonths = remainingYears * 12 estimatedLife = `~${remainingMonths.toFixed(0)} months` } } } // Available spare for NVMe const availableSpare = nvmeHealth?.available_spare as number | undefined if (wearPercent !== null || dataWrittenLabel) { return ( <> {/* Wear Progress Bar - Blue style matching NVMe */} {wearPercent !== null && (

{wearLabel}

{wearPercent}%

div]:bg-red-500' : '[&>div]:bg-blue-500'}`} />
)} {/* Stats Grid - Same layout as NVMe Wear & Lifetime */}
{estimatedLife && (

Estimated Life Remaining

{estimatedLife}

)} {dataWrittenLabel && (

Total Data Written

{dataWrittenLabel}

)} {availableSpare !== undefined && (

Available Spare

{availableSpare}%

)}
) } return null })()}
) : (

No SMART test data available for this disk.

Run a SMART test in the SMART Test tab to get detailed health information.

)}
)} {/* 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" && ( )} {/* History Tab */} {selectedDisk && activeModalTab === "history" && ( )} {/* Schedule Tab */} {selectedDisk && activeModalTab === "schedule" && ( )}
) } // Generate SMART Report HTML and open in new window (same pattern as Lynis/Latency reports) function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: SmartAttribute[], observations: DiskObservation[] = [], lastTestDate?: string, targetWindow?: Window, isHistorical = false) { const now = new Date().toLocaleString() const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` const reportId = `SMART-${Date.now().toString(36).toUpperCase()}` // --- Enriched device fields from smart_data --- const sd = testStatus.smart_data const modelFamily = sd?.model_family || '' const formFactor = sd?.form_factor || '' const physBlockSize = sd?.physical_block_size ?? 512 const trimSupported = sd?.trim_supported ?? false const sataVersion = sd?.sata_version || '' const ifaceSpeed = sd?.interface_speed || '' const pollingShort = sd?.polling_minutes_short const pollingExt = sd?.polling_minutes_extended const errorLogCount = sd?.error_log_count ?? 0 const selfTestHistory = sd?.self_test_history || [] // SMR detection (WD Red without Plus, known SMR families) const isSMR = modelFamily.toLowerCase().includes('smr') || /WD (Red|Blue|Green) \d/.test(modelFamily) || /WDC WD\d{4}[EZ]/.test(disk.model || '') // Seagate proprietary Raw_Read_Error_Rate detection const isSeagate = modelFamily.toLowerCase().includes('seagate') || modelFamily.toLowerCase().includes('barracuda') || modelFamily.toLowerCase().includes('ironwolf') || (disk.model || '').startsWith('ST') // Test age warning let testAgeDays = 0 let testAgeWarning = '' if (lastTestDate) { const testDate = new Date(lastTestDate) testAgeDays = Math.floor((Date.now() - testDate.getTime()) / (1000 * 60 * 60 * 24)) if (testAgeDays > 90) { testAgeWarning = `This report is based on a SMART test performed ${testAgeDays} days ago (${testDate.toLocaleDateString()}). Disk health may have changed since then. We recommend running a new SMART test for up-to-date results.` } } // Determine disk type (SAS detected via backend flag or connection_type) const isSasDisk = sd?.is_sas === true || disk.connection_type === 'sas' let diskType = "HDD" if (disk.name.startsWith("nvme")) { diskType = "NVMe" } else if (isSasDisk) { diskType = "SAS" } else if (!disk.rotation_rate || disk.rotation_rate === 0) { diskType = "SSD" } // Health status styling const healthStatus = testStatus.smart_status || (testStatus.smart_data?.smart_status) || 'unknown' const isHealthy = healthStatus.toLowerCase() === 'passed' const healthColor = isHealthy ? '#16a34a' : healthStatus.toLowerCase() === 'failed' ? '#dc2626' : '#ca8a04' const healthLabel = isHealthy ? 'PASSED' : healthStatus.toUpperCase() // Format power on time — force 'en' locale for consistent comma separator const fmtNum = (n: number) => n.toLocaleString('en-US') const powerOnHours = disk.power_on_hours || testStatus.smart_data?.power_on_hours || 0 const powerOnDays = Math.round(powerOnHours / 24) const powerOnYears = Math.floor(powerOnHours / 8760) const powerOnRemainingDays = Math.floor((powerOnHours % 8760) / 24) const powerOnFormatted = powerOnYears > 0 ? `${powerOnYears}y ${powerOnRemainingDays}d (${fmtNum(powerOnHours)}h)` : `${powerOnDays}d (${fmtNum(powerOnHours)}h)` // Build attributes table - format differs for NVMe vs SATA const isNvmeForTable = diskType === 'NVMe' // Explanations for NVMe metrics const nvmeExplanations: Record = { 'Critical Warning': 'Active alert flags from the NVMe controller. Any non-zero value requires immediate investigation.', 'Temperature': 'Composite temperature reported by the controller. Sustained high temps cause thermal throttling and reduce NAND lifespan.', 'Temperature Sensor 1': 'Primary temperature sensor, usually the NAND flash. Most representative of flash health.', 'Temperature Sensor 2': 'Secondary sensor, often the controller die. Typically runs hotter than Sensor 1.', 'Temperature Sensor 3': 'Tertiary sensor, if present. Location varies by manufacturer.', 'Available Spare': 'Percentage of spare NAND blocks remaining for bad-block replacement. Alert triggers below threshold.', 'Available Spare Threshold': 'Manufacturer-set minimum for Available Spare. Below this, the drive flags a critical warning.', 'Percentage Used': "Drive's own estimate of endurance consumed based on actual vs. rated write cycles. 100% = rated TBW reached; drive may continue working beyond this.", 'Percent Used': "Drive's own estimate of endurance consumed based on actual vs. rated write cycles. 100% = rated TBW reached; drive may continue working beyond this.", 'Media Errors': 'Unrecoverable read/write errors on the NAND flash. Any non-zero value indicates permanent cell damage. Growing count = replace soon.', 'Media and Data Integrity Errors': 'Unrecoverable errors detected by the controller. Non-zero means data corruption risk.', 'Unsafe Shutdowns': 'Power losses without proper flush/shutdown. Very high counts risk metadata corruption and firmware issues.', 'Power Cycles': 'Total on/off cycles. Frequent cycling increases connector and capacitor wear.', 'Power On Hours': 'Total cumulative hours the drive has been powered on since manufacture.', 'Data Units Read': 'Total data read in 512KB units. Multiply by 512,000 for bytes. Useful for calculating daily read workload.', 'Data Units Written': 'Total data written in 512KB units. Compare with TBW rating to estimate remaining endurance.', 'Host Read Commands': 'Total read commands issued by the host. High ratio vs. write commands indicates read-heavy workload.', 'Host Write Commands': 'Total write commands issued by the host. Includes filesystem metadata writes.', 'Controller Busy Time': 'Total minutes the controller spent processing I/O commands. High values indicate sustained heavy workload.', 'Error Log Entries': 'Number of entries in the error information log. Often includes benign self-test artifacts; cross-check with Media Errors.', 'Error Information Log Entries': 'Number of entries in the error information log. Often includes benign self-test artifacts.', 'Warning Temp Time': 'Total minutes spent above the warning temperature threshold. Causes performance throttling. Zero is ideal.', 'Critical Temp Time': 'Total minutes spent above the critical temperature threshold. Drive may shut down to prevent damage. Should always be zero.', 'Warning Composite Temperature Time': 'Total minutes the composite temperature exceeded the warning threshold.', 'Critical Composite Temperature Time': 'Total minutes the composite temperature exceeded the critical threshold. Must be zero.', 'Thermal Management T1 Trans Count': 'Number of times the drive entered light thermal throttling (T1). Indicates cooling issues.', 'Thermal Management T2 Trans Count': 'Number of times the drive entered heavy thermal throttling (T2). Significant performance impact.', 'Thermal Management T1 Total Time': 'Total seconds spent in light thermal throttling. Indicates sustained cooling problems.', 'Thermal Management T2 Total Time': 'Total seconds spent in heavy thermal throttling. Severe performance degradation.', } // Explanations for SATA/SSD attributes — covers HDD, SSD, and mixed-use attributes const sataExplanations: Record = { // === Read/Write Errors === 'Raw Read Error Rate': 'Hardware read errors detected. High raw values on Seagate/Samsung drives are normal (proprietary formula where VALUE, not raw, matters).', 'Write Error Rate': 'Errors encountered during write operations. Growing count may indicate head or media issues.', 'Multi Zone Error Rate': 'Errors when writing to multi-zone regions. Manufacturer-specific; rising trend is concerning.', 'Soft Read Error Rate': 'Read errors corrected by firmware without data loss. High values may indicate degrading media.', 'Read Error Retry Rate': 'Number of read retries needed. Occasional retries are normal; persistent growth indicates wear.', 'Reported Uncorrect': 'Errors that ECC could not correct. Any non-zero value means data was lost or unreadable.', 'Reported Uncorrectable Errors': 'Errors that ECC could not correct. Non-zero = data loss risk.', // === Reallocated / Pending / Offline === 'Reallocated Sector Ct': 'Bad sectors replaced by spare sectors from the reserve pool. Growing count = drive degradation.', 'Reallocated Sector Count': 'Bad sectors replaced by spare sectors. Growing count indicates drive degradation.', 'Reallocated Sectors': 'Bad sectors replaced by spare sectors. Growing count indicates drive degradation.', 'Retired Block Count': 'NAND blocks retired due to wear or failure (SSD). Similar to Reallocated Sector Count for HDDs.', 'Reallocated Event Count': 'Number of remap operations performed. Each event means a bad sector was replaced.', 'Current Pending Sector': 'Unstable sectors waiting to be remapped on next write. May resolve or become permanently reallocated.', 'Current Pending Sector Count': 'Unstable sectors waiting to be remapped on next write. Non-zero warrants monitoring.', 'Pending Sectors': 'Sectors waiting to be remapped. May resolve or become reallocated.', 'Offline Uncorrectable': 'Sectors that failed during offline scan and could not be corrected. Indicates potential data loss.', 'Offline Uncorrectable Sector Count': 'Uncorrectable sectors found during background scan. Data on these sectors is lost.', // === Temperature === 'Temperature': 'Current drive temperature. Sustained high temps accelerate wear and reduce lifespan.', 'Temperature Celsius': 'Current drive temperature in Celsius. HDDs: keep below 45°C; SSDs: below 60°C.', 'Airflow Temperature Cel': 'Temperature measured by the airflow sensor. Usually slightly lower than the main temp sensor.', 'Temperature Case': 'Temperature of the drive casing. Useful for monitoring enclosure ventilation.', 'Temperature Internal': 'Internal temperature sensor. May read higher than case temperature.', // === Power & Uptime === 'Power On Hours': 'Total cumulative hours the drive has been powered on. Used to estimate age and plan replacements.', 'Power On Hours and Msec': 'Total powered-on time with millisecond precision.', 'Power Cycle Count': 'Total number of complete power on/off cycles. Frequent cycling stresses electronics.', 'Power Off Retract Count': 'Times the heads were retracted due to power loss (HDD). High values indicate unstable power supply.', 'Unexpected Power Loss Ct': 'Unexpected power losses (SSD). Can cause metadata corruption if write-cache was active.', 'Unsafe Shutdown Count': 'Power losses without proper shutdown (SSD). High values risk firmware corruption.', 'Start Stop Count': 'Spindle motor start/stop cycles (HDD). Each cycle causes mechanical wear.', // === Mechanical (HDD-specific) === 'Spin Up Time': 'Time for platters to reach full operating speed (HDD). Increasing values may indicate motor bearing wear.', 'Spin Retry Count': 'Failed attempts to spin up the motor (HDD). Non-zero usually indicates power supply or motor issues.', 'Calibration Retry Count': 'Number of head calibration retries (HDD). Non-zero may indicate mechanical issues.', 'Seek Error Rate': 'Errors during head positioning (HDD). High raw values on Seagate are often normal (proprietary formula).', 'Seek Time Performance': 'Average seek operation performance (HDD). Declining values suggest mechanical degradation.', 'Load Cycle Count': 'Head load/unload cycles (HDD). Rated for 300K-600K cycles on most drives.', 'Load Unload Cycle Count': 'Head load/unload cycles (HDD). Each cycle causes micro-wear on the ramp mechanism.', 'Head Flying Hours': 'Hours the read/write heads have been positioned over the platters (HDD).', 'High Fly Writes': 'Writes where the head flew higher than expected (HDD). Data may not be written correctly.', 'G Sense Error Rate': 'Shock/vibration events detected by the accelerometer (HDD). High values indicate physical disturbance.', 'Disk Shift': 'Distance the disk has shifted from its original position (HDD). Temperature or shock-related.', 'Loaded Hours': 'Hours spent with heads loaded over the platters (HDD).', 'Load In Time': 'Time of the head loading process. Manufacturer-specific diagnostic metric.', 'Torque Amplification Count': 'Times the drive needed extra torque to spin up. May indicate stiction or motor issues.', 'Flying Height': 'Head-to-platter distance during operation (HDD). Critical for read/write reliability.', 'Load Friction': 'Friction detected during head loading (HDD). Increasing values suggest ramp mechanism wear.', 'Load Unload Retry Count': 'Failed head load/unload attempts (HDD). Non-zero indicates mechanical issues.', // === Interface Errors === 'UDMA CRC Error Count': 'Data transfer checksum errors on the SATA cable. Usually caused by a bad cable, loose connection, or port issue.', 'CRC Errors': 'Interface communication errors. Usually caused by cable or connection issues.', 'CRC Error Count': 'Data transfer checksum errors. Replace the SATA cable if this value grows.', 'Command Timeout': 'Commands that took too long and timed out. May indicate controller or connection issues.', 'Interface CRC Error Count': 'CRC errors on the interface link. Cable or connector problem.', // === ECC & Data Integrity === 'Hardware ECC Recovered': 'Read errors corrected by hardware ECC. Non-zero is normal; rapid growth warrants attention.', 'ECC Error Rate': 'Rate of ECC-corrected errors. Proprietary formula; VALUE matters more than raw count.', 'End to End Error': 'Data corruption detected between the controller cache and host interface. Should always be zero.', 'End to End Error Detection Count': 'Number of parity errors in the data path. Non-zero indicates controller issues.', // === SSD Wear & Endurance === 'Wear Leveling Count': 'Average erase cycles per NAND block (SSD). Lower VALUE = more wear consumed.', 'Wear Range Delta': 'Difference between most-worn and least-worn blocks (SSD). High values indicate uneven wear.', 'Media Wearout Indicator': 'Intel SSD life remaining estimate. Starts at 100, decreases to 0 as endurance is consumed.', 'SSD Life Left': 'Estimated remaining SSD lifespan percentage based on NAND wear.', 'Percent Lifetime Remain': 'Estimated remaining lifespan percentage. 100 = new; 0 = end of rated life.', 'Percent Lifetime Used': 'Percentage of rated endurance consumed. Inverse of Percent Lifetime Remain.', 'Available Reservd Space': 'Remaining spare blocks as a percentage of total reserves (SSD). Similar to NVMe Available Spare.', 'Available Reserved Space': 'Remaining spare blocks as a percentage (SSD). Low values reduce the drive\'s ability to handle bad blocks.', 'Used Rsvd Blk Cnt Tot': 'Total reserve blocks consumed for bad-block replacement (SSD). Growing = aging.', 'Used Reserved Block Count': 'Number of reserve blocks used for bad-block replacement (SSD).', 'Unused Rsvd Blk Cnt Tot': 'Remaining reserve blocks available (SSD). Zero = no more bad-block replacement possible.', 'Unused Reserve Block Count': 'Reserve blocks still available for bad-block replacement (SSD).', 'Program Fail Cnt Total': 'Total NAND program (write) failures (SSD). Non-zero indicates flash cell degradation.', 'Program Fail Count': 'NAND write failures (SSD). Growing count means flash cells are wearing out.', 'Program Fail Count Chip': 'Program failures at chip level (SSD). Non-zero indicates NAND degradation.', 'Erase Fail Count': 'NAND erase operation failures (SSD). Non-zero indicates severe flash wear.', 'Erase Fail Count Total': 'Total NAND erase failures (SSD). Combined with Program Fail Count shows overall NAND health.', 'Erase Fail Count Chip': 'Erase failures at chip level (SSD). Non-zero = NAND degradation.', 'Runtime Bad Block': 'Bad blocks discovered during normal operation (SSD). Different from factory-mapped bad blocks.', 'Runtime Bad Blocks': 'Blocks that failed during use (SSD). Growing count = flash wearing out.', // === Data Volume === 'Total LBAs Written': 'Total logical block addresses written. Multiply by 512 bytes for total data volume.', 'Total LBAs Read': 'Total logical block addresses read. Useful for calculating daily workload.', 'Lifetime Writes GiB': 'Total data written in GiB over the drive\'s lifetime.', 'Lifetime Reads GiB': 'Total data read in GiB over the drive\'s lifetime.', 'Total Writes GiB': 'Total data written in GiB. Compare with TBW rating for endurance estimate.', 'Total Reads GiB': 'Total data read in GiB.', 'NAND Writes GiB': 'Raw NAND writes in GiB. Higher than host writes due to write amplification.', 'Host Writes 32MiB': 'Total data written by the host in 32MiB units.', 'Host Reads 32MiB': 'Total data read by the host in 32MiB units.', 'Host Writes MiB': 'Total data written by the host in MiB.', 'Host Reads MiB': 'Total data read by the host in MiB.', 'NAND GB Written TLC': 'Total data written to TLC NAND cells in GB. Includes write amplification overhead.', 'NAND GiB Written': 'Total NAND writes in GiB. Higher than host writes due to write amplification and garbage collection.', // === SSD-Specific Advanced === 'Ave Block Erase Count': 'Average number of erase cycles per NAND block (SSD). Drives are typically rated for 3K-100K cycles.', 'Average Erase Count': 'Average erase cycles per block. Compare with rated endurance for remaining life estimate.', 'Max Erase Count': 'Maximum erase cycles on any single block. Large gap with average indicates uneven wear.', 'Total Erase Count': 'Sum of all erase cycles across all blocks. Overall NAND write volume indicator.', 'Power Loss Cap Test': 'Result of the power-loss protection capacitor self-test (SSD). Failed = risk of data loss on power failure.', 'Power Loss Protection': 'Status of the power-loss protection mechanism. Enterprise SSDs use capacitors to flush cache on power loss.', 'Successful RAIN Recov Cnt': 'Successful recoveries using RAIN (Redundant Array of Independent NAND). Shows NAND parity is working.', 'SSD Erase Fail Count': 'Total erase failures across the SSD. Indicates overall NAND degradation.', 'SSD Program Fail Count': 'Total write failures across the SSD. Indicates flash cell reliability issues.', // === Throughput === 'Throughput Performance': 'Overall throughput performance rating (HDD). Declining values indicate degradation.', // === Other / Vendor-specific === 'Unknown Attribute': 'Vendor-specific attribute not defined in the SMART standard. Check manufacturer documentation.', 'Free Fall Sensor': 'Free-fall events detected (laptop HDD). The heads are parked to prevent damage during drops.', } // Explanations for SAS/SCSI metrics const sasExplanations: Record = { 'Grown Defect List': 'Sectors remapped due to defects found during operation. Equivalent to Reallocated Sectors on SATA. Growing count = drive degradation.', 'Read Errors Corrected': 'Read errors corrected by ECC. Normal for enterprise drives under heavy workload — only uncorrected errors are critical.', 'Read ECC Fast': 'Errors corrected by fast (on-the-fly) ECC during read operations. Normal in SAS drives.', 'Read ECC Delayed': 'Errors requiring delayed (offline) ECC correction during reads. Non-zero is acceptable but should not grow rapidly.', 'Read Uncorrected Errors': 'Read errors that ECC could not correct. Non-zero means data was lost or unreadable. Critical metric.', 'Read Data Processed': 'Total data read by the drive. Useful for calculating daily workload.', 'Write Errors Corrected': 'Write errors corrected by ECC. Normal for enterprise drives.', 'Write Uncorrected Errors': 'Write errors that ECC could not correct. Non-zero = potential data loss. Critical.', 'Write Data Processed': 'Total data written to the drive. Useful for workload analysis.', 'Verify Errors Corrected': 'Verification errors corrected during background verify operations.', 'Verify Uncorrected Errors': 'Verify errors that could not be corrected. Non-zero indicates media degradation.', 'Non-Medium Errors': 'Controller/bus errors not related to the media itself. High count may indicate backplane or cable issues.', 'Temperature': 'Current drive temperature. Enterprise SAS drives tolerate up to 55-60°C under sustained load.', 'Power On Hours': 'Total hours the drive has been powered on. Enterprise drives are rated for 24/7 operation.', 'Start-Stop Cycles': 'Motor start/stop cycles. Enterprise SAS drives are rated for 50,000+ cycles.', 'Load-Unload Cycles': 'Head load/unload cycles. Enterprise drives are rated for 600,000+ cycles.', 'Background Scan Status': 'Status of the SCSI background media scan. Runs continuously to detect surface defects.', } const getAttrExplanation = (name: string, diskKind: string): string => { const cleanName = name.replace(/_/g, ' ') if (diskKind === 'NVMe') { return nvmeExplanations[cleanName] || nvmeExplanations[name] || '' } if (diskKind === 'SAS') { return sasExplanations[cleanName] || sasExplanations[name] || '' } return sataExplanations[cleanName] || sataExplanations[name] || '' } // SAS and NVMe use simplified table format (Metric | Value | Status) const useSimpleTable = isNvmeForTable || isSasDisk const attributeRows = smartAttributes.map((attr, i) => { const statusColor = attr.status === 'ok' ? '#16a34a' : attr.status === 'warning' ? '#ca8a04' : '#dc2626' const statusBg = attr.status === 'ok' ? '#16a34a15' : attr.status === 'warning' ? '#ca8a0415' : '#dc262615' const explanation = getAttrExplanation(attr.name, diskType) const explainRow = explanation ? `
${explanation}
` : '' if (useSimpleTable) { // NVMe/SAS format: Metric | Value | Status const displayValue = isSasDisk ? attr.raw_value : attr.value return ` ${attr.name} ${displayValue} ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} ${explainRow}` } else { // SATA format: ID | Attribute | Val | Worst | Thr | Raw | Status return ` ${attr.id} ${attr.name.replace(/_/g, ' ')} ${attr.value} ${attr.worst} ${attr.threshold} ${attr.raw_value} ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} ${explainRow}` } }).join('') // Critical attributes to highlight const criticalAttrs = smartAttributes.filter(a => a.status !== 'ok') const hasCritical = criticalAttrs.length > 0 // Temperature color based on disk type const getTempColorForReport = (temp: number): string => { if (temp <= 0) return '#94a3b8' // gray for N/A switch (diskType) { case 'NVMe': // NVMe: <=70 green, 71-80 yellow, >80 red if (temp <= 70) return '#16a34a' if (temp <= 80) return '#ca8a04' return '#dc2626' case 'SSD': // SSD: <=59 green, 60-70 yellow, >70 red if (temp <= 59) return '#16a34a' if (temp <= 70) return '#ca8a04' return '#dc2626' case 'SAS': // SAS enterprise: <=55 green, 56-65 yellow, >65 red if (temp <= 55) return '#16a34a' if (temp <= 65) return '#ca8a04' return '#dc2626' case 'HDD': default: // HDD: <=45 green, 46-55 yellow, >55 red if (temp <= 45) return '#16a34a' if (temp <= 55) return '#ca8a04' return '#dc2626' } } // Temperature thresholds for display const tempThresholds = diskType === 'NVMe' ? { optimal: '<=70°C', warning: '71-80°C', critical: '>80°C' } : diskType === 'SSD' ? { optimal: '<=59°C', warning: '60-70°C', critical: '>70°C' } : diskType === 'SAS' ? { optimal: '<=55°C', warning: '56-65°C', critical: '>65°C' } : { optimal: '<=45°C', warning: '46-55°C', critical: '>55°C' } const isNvmeDisk = diskType === 'NVMe' // NVMe Wear & Lifetime data const nvmePercentUsed = testStatus.smart_data?.nvme_raw?.percent_used ?? disk.percentage_used ?? 0 const nvmeAvailSpare = testStatus.smart_data?.nvme_raw?.avail_spare ?? 100 const nvmeDataWritten = testStatus.smart_data?.nvme_raw?.data_units_written ?? 0 // Data units are in 512KB blocks, convert to TB const nvmeDataWrittenTB = (nvmeDataWritten * 512 * 1024) / (1024 * 1024 * 1024 * 1024) // Calculate estimated life remaining for NVMe let nvmeEstimatedLife = 'N/A' if (nvmePercentUsed > 0 && disk.power_on_hours && disk.power_on_hours > 0) { const totalEstimatedHours = disk.power_on_hours / (nvmePercentUsed / 100) const remainingHours = totalEstimatedHours - disk.power_on_hours const remainingYears = remainingHours / (24 * 365) if (remainingYears >= 1) { nvmeEstimatedLife = `~${remainingYears.toFixed(1)} years` } else if (remainingHours >= 24) { nvmeEstimatedLife = `~${Math.floor(remainingHours / 24)} days` } else { nvmeEstimatedLife = `~${Math.floor(remainingHours)} hours` } } else if (nvmePercentUsed === 0) { nvmeEstimatedLife = 'Excellent' } // Wear color based on percentage const getWearColorHex = (pct: number): string => { if (pct <= 50) return '#16a34a' // green if (pct <= 80) return '#ca8a04' // yellow return '#dc2626' // red } // Life remaining color (inverse) const getLifeColorHex = (pct: number): string => { const remaining = 100 - pct if (remaining >= 50) return '#16a34a' // green if (remaining >= 20) return '#ca8a04' // yellow return '#dc2626' // red } // Build recommendations const recommendations: string[] = [] if (isHealthy) { recommendations.push('
Disk is Healthy

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

') } else { recommendations.push('
Critical: Disk Health Issue Detected

SMART has reported a health issue. Backup all data immediately and plan for disk replacement.

') } if ((disk.reallocated_sectors ?? 0) > 0) { recommendations.push(`
Reallocated Sectors Detected (${disk.reallocated_sectors})

The disk has bad sectors that have been remapped. Monitor closely and consider replacement if count increases.

`) } if ((disk.pending_sectors ?? 0) > 0) { recommendations.push(`
Pending Sectors (${disk.pending_sectors})

There are sectors waiting to be reallocated. This may indicate impending failure.

`) } if (disk.temperature > 55 && diskType === 'HDD') { recommendations.push(`
High Temperature (${disk.temperature}°C)

HDD is running hot. Improve case airflow or add cooling.

`) } else if (disk.temperature > 70 && diskType === 'SSD') { recommendations.push(`
High Temperature (${disk.temperature}°C)

SSD is running hot. Check airflow around the drive.

`) } else if (disk.temperature > 80 && diskType === 'NVMe') { recommendations.push(`
High Temperature (${disk.temperature}°C)

NVMe is overheating. Consider adding a heatsink or improving case airflow.

`) } // NVMe critical warning if (diskType === 'NVMe') { const critWarnVal = testStatus.smart_data?.nvme_raw?.critical_warning ?? 0 const mediaErrVal = testStatus.smart_data?.nvme_raw?.media_errors ?? 0 const unsafeVal = testStatus.smart_data?.nvme_raw?.unsafe_shutdowns ?? 0 if (critWarnVal !== 0) { recommendations.push(`
NVMe Critical Warning Active (0x${critWarnVal.toString(16).toUpperCase()})

The NVMe controller has raised an alert flag. Back up data immediately and investigate further.

`) } if (mediaErrVal > 0) { recommendations.push(`
NVMe Media Errors Detected (${mediaErrVal})

Unrecoverable errors in NAND flash cells. Any non-zero value indicates physical flash damage. Back up data and plan for replacement.

`) } if (unsafeVal > 200) { recommendations.push(`
High Unsafe Shutdown Count (${unsafeVal})

Frequent power losses without proper shutdown increase the risk of firmware corruption. Ensure stable power supply or use a UPS.

`) } } // Seagate Raw_Read_Error_Rate note if (isSeagate) { const hasRawReadAttr = smartAttributes.some(a => a.name === 'Raw_Read_Error_Rate' || a.id === 1) if (hasRawReadAttr) { recommendations.push('
Seagate Raw_Read_Error_Rate — Normal Behavior

Seagate drives report very large raw values for attribute #1 (Raw_Read_Error_Rate). This is expected and uses a proprietary formula — a high raw number does NOT indicate errors. Only the normalized value (column Val) matters, and it should remain at 100.

') } } // SMR disk note if (isSMR) { recommendations.push('
SMR Drive Detected — Write Limitations

This appears to be a Shingled Magnetic Recording (SMR) disk. SMR drives have slower random-write performance and may stall during heavy mixed workloads. They are suitable for sequential workloads (backups, archives) but not recommended as primary Proxmox storage or ZFS vdevs.

') } if (recommendations.length === 1 && isHealthy) { recommendations.push('
Regular Maintenance

Schedule periodic extended SMART tests (monthly) to catch issues early.

') recommendations.push('
Backup Strategy

Ensure critical data is backed up regularly regardless of disk health status.

') } // Build observations HTML separately to avoid nested template literal issues let observationsHtml = '' if (observations.length > 0) { const totalOccurrences = observations.reduce((sum, o) => sum + o.occurrence_count, 0) // Group observations by error type const groupedObs: Record = {} observations.forEach(obs => { const type = obs.error_type || 'unknown' if (!groupedObs[type]) groupedObs[type] = [] groupedObs[type].push(obs) }) let groupsHtml = '' Object.entries(groupedObs).forEach(([type, obsList]) => { const typeLabel = type === 'io_error' ? 'I/O Errors' : type === 'smart_error' ? 'SMART Errors' : type === 'filesystem_error' ? 'Filesystem Errors' : type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) const groupOccurrences = obsList.reduce((sum, o) => sum + o.occurrence_count, 0) let obsItemsHtml = '' obsList.forEach(obs => { // Use blue (info) as base color for all observations const infoColor = '#3b82f6' const infoBg = '#3b82f615' // Severity badge color based on actual severity const severityBadgeColor = obs.severity === 'critical' ? '#dc2626' : obs.severity === 'warning' ? '#ca8a04' : '#3b82f6' const severityLabel = obs.severity ? obs.severity.charAt(0).toUpperCase() + obs.severity.slice(1) : 'Info' const firstDate = obs.first_occurrence ? new Date(obs.first_occurrence).toLocaleString() : 'N/A' const lastDate = obs.last_occurrence ? new Date(obs.last_occurrence).toLocaleString() : 'N/A' const dismissedBadge = obs.dismissed ? 'Dismissed' : '' const errorTypeLabel = type === 'io_error' ? 'I/O Error' : type === 'smart_error' ? 'SMART Error' : type === 'filesystem_error' ? 'Filesystem Error' : type.replace(/_/g, ' ') obsItemsHtml += `
${errorTypeLabel} ${severityLabel} ID: #${obs.id} Occurrences: ${obs.occurrence_count} ${dismissedBadge}
Error Signature:
${obs.error_signature}
Raw Message:
${obs.raw_message || 'N/A'}
Device: ${obs.device_name || disk.name}
Serial: ${obs.serial || disk.serial || 'N/A'}
Model: ${obs.model || disk.model || 'N/A'}
First Seen: ${firstDate}
Last Seen: ${lastDate}
` }) groupsHtml += `
${typeLabel} ${obsList.length} unique, ${groupOccurrences} total
${obsItemsHtml}
` }) const obsSecNum = isNvmeDisk ? '6' : '5' observationsHtml = `
${obsSecNum}. Observations & Events (${observations.length} recorded, ${totalOccurrences} total occurrences)

The following events have been detected and logged for this disk. These observations may indicate potential issues that require attention.

${groupsHtml}
` } const html = ` SMART Health Report - /dev/${disk.name}
SMART Health Report /dev/${disk.name}
ProxMenux

SMART Health Report

ProxMenux Monitor - Disk Health Analysis

Date: ${now}
Device: /dev/${disk.name}
ID: ${reportId}
1. Executive Summary
${isHealthy ? '✓' : '✗'}
${healthLabel}
SMART Status

Disk Health Assessment

${isHealthy ? `This disk is operating within normal parameters. All SMART attributes are within acceptable thresholds. The disk has been powered on for approximately ${powerOnFormatted} and is currently operating at ${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}. ${(disk.reallocated_sectors ?? 0) === 0 ? 'No bad sectors have been detected.' : `${disk.reallocated_sectors} reallocated sector(s) detected - monitor closely.`}` : `This disk has reported a SMART health failure. Immediate action is required. Backup all critical data and plan for disk replacement.` }

${isHealthy ? 'What does this mean? Your disk is healthy!' : (hasCritical ? 'ATTENTION REQUIRED: Problems detected' : 'Some issues need monitoring')}

${isHealthy ? 'In simple terms: This disk is working properly. You can continue using it normally. We recommend running periodic SMART tests (monthly) to catch any issues early.' : (hasCritical ? 'In simple terms: This disk has problems that could cause data loss. You should back up your important files immediately and consider replacing the disk soon.' : 'In simple terms: The disk is working but shows some signs of wear. It is not critical yet, but you should monitor it closely and ensure your backups are up to date.' ) }

${!isHealthy && criticalAttrs.length > 0 ? `
Issues found:
    ${criticalAttrs.slice(0, 3).map(a => `
  • ${a.name.replace(/_/g, ' ')}: ${a.status === 'critical' ? 'Critical - requires immediate attention' : 'Warning - should be monitored'}
  • `).join('')} ${criticalAttrs.length > 3 ? `
  • ...and ${criticalAttrs.length - 3} more issues (see details below)
  • ` : ''}
` : ''}
Report Generated
${now}
${isHistorical ? 'Test Type' : 'Last Test Type'}
${testStatus.last_test?.type || 'N/A'}
Test Result
${testStatus.last_test?.status || 'N/A'}
Attributes Checked
${smartAttributes.length}
${testAgeWarning ? `
Outdated Test Data (${testAgeDays} days old)

${testAgeWarning}

` : ''}
2. Disk Information
Model
${disk.model || sd?.model || 'Unknown'}
Serial
${disk.serial || sd?.serial || 'Unknown'}
Capacity
${disk.size_formatted || 'Unknown'}
Type
${diskType === 'SAS' ? (disk.rotation_rate ? `SAS ${disk.rotation_rate} RPM` : 'SAS SSD') : diskType === 'HDD' && disk.rotation_rate ? `HDD ${disk.rotation_rate} RPM` : diskType}
${(modelFamily || formFactor || sataVersion || ifaceSpeed) ? `
${modelFamily ? `
Family
${modelFamily}
` : ''} ${formFactor ? `
Form Factor
${formFactor}
` : ''} ${sataVersion ? `
Interface
${sataVersion}${ifaceSpeed ? ` · ${ifaceSpeed}` : ''}
` : (ifaceSpeed ? `
${isSasDisk ? 'Transport' : 'Link Speed'}
${ifaceSpeed}
` : '')} ${!isNvmeDisk && !isSasDisk ? `
TRIM
${trimSupported ? 'Supported' : 'Not supported'}${physBlockSize === 4096 ? ' · 4K AF' : ''}
` : ''} ${isSasDisk && sd?.logical_block_size ? `
Block Size
${sd.logical_block_size} bytes
` : ''}
` : ''}
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
Temperature
Optimal: ${tempThresholds.optimal}
${fmtNum(powerOnHours)}h
Power On Time
${powerOnYears}y ${powerOnRemainingDays}d
${fmtNum(disk.power_cycles ?? 0)}
Power Cycles
${disk.smart_status || 'N/A'}
SMART Status
${!isNvmeDisk ? `
${disk.pending_sectors ?? 0}
${isSasDisk ? 'Uncorrected Errors' : 'Pending Sectors'}
${isSasDisk ? 'N/A' : (disk.crc_errors ?? 0)}
CRC Errors
${disk.reallocated_sectors ?? 0}
${isSasDisk ? 'Grown Defects' : 'Reallocated Sectors'}
` : ''}
${isNvmeDisk ? `
3. NVMe Wear & Lifetime
LIFE REMAINING
${100 - nvmePercentUsed}%
Estimated: ${nvmeEstimatedLife}
USAGE STATISTICS
Percentage Used ${nvmePercentUsed}%
Available Spare ${nvmeAvailSpare}%
Data Written
${nvmeDataWrittenTB >= 1 ? nvmeDataWrittenTB.toFixed(2) + ' TB' : (nvmeDataWrittenTB * 1024).toFixed(1) + ' GB'}
Power Cycles
${testStatus.smart_data?.nvme_raw?.power_cycles != null ? fmtNum(testStatus.smart_data.nvme_raw.power_cycles) : (disk.power_cycles ? fmtNum(disk.power_cycles) : 'N/A')}
${(() => { const nr = testStatus.smart_data?.nvme_raw if (!nr) return '' const mediaErr = nr.media_errors ?? 0 const unsafeSd = nr.unsafe_shutdowns ?? 0 const critWarn = nr.critical_warning ?? 0 const warnTempMin = nr.warning_temp_time ?? 0 const critTempMin = nr.critical_comp_time ?? 0 const ctrlBusy = nr.controller_busy_time ?? 0 const errLog = nr.num_err_log_entries ?? 0 const dataReadTB = ((nr.data_units_read ?? 0) * 512 * 1024) / (1024 ** 4) const hostReads = nr.host_read_commands ?? 0 const hostWrites = nr.host_write_commands ?? 0 const endGrpWarn = nr.endurance_grp_critical_warning_summary ?? 0 const sensors = (nr.temperature_sensors ?? []).filter((s: number | null) => s !== null) as number[] const metricCard = (label: string, value: string, colorHex: string, note?: string) => `
${label}
${value}
${note ? `
${note}
` : ''}
` return `
Extended NVMe Health
${metricCard('Critical Warning', critWarn === 0 ? 'None' : `0x${critWarn.toString(16).toUpperCase()}`, critWarn === 0 ? '#16a34a' : '#dc2626', 'Controller alert flags')} ${metricCard('Media Errors', fmtNum(mediaErr), mediaErr === 0 ? '#16a34a' : '#dc2626', 'Flash cell damage')} ${metricCard('Unsafe Shutdowns', fmtNum(unsafeSd), unsafeSd < 50 ? '#16a34a' : unsafeSd < 200 ? '#ca8a04' : '#dc2626', 'Power loss without flush')} ${metricCard('Endurance Warning', endGrpWarn === 0 ? 'None' : `0x${endGrpWarn.toString(16).toUpperCase()}`, endGrpWarn === 0 ? '#16a34a' : '#ca8a04', 'Group endurance alert')}
${metricCard('Controller Busy', `${fmtNum(ctrlBusy)} min`, '#1e293b', 'Total busy time')} ${metricCard('Error Log Entries', fmtNum(errLog), errLog === 0 ? '#16a34a' : '#ca8a04', 'May include benign artifacts')} ${metricCard('Warning Temp Time', `${fmtNum(warnTempMin)} min`, warnTempMin === 0 ? '#16a34a' : '#ca8a04', 'Minutes in warning range')} ${metricCard('Critical Temp Time', `${fmtNum(critTempMin)} min`, critTempMin === 0 ? '#16a34a' : '#dc2626', 'Minutes in critical range')}
${metricCard('Data Read', dataReadTB >= 1 ? dataReadTB.toFixed(2) + ' TB' : (dataReadTB * 1024).toFixed(1) + ' GB', '#1e293b', 'Total host reads')} ${metricCard('Host Read Cmds', fmtNum(hostReads), '#1e293b', 'Total read commands')} ${metricCard('Host Write Cmds', fmtNum(hostWrites), '#1e293b', 'Total write commands')} ${sensors.length >= 2 ? metricCard('Hotspot Temp', `${sensors[1]}°C`, sensors[1] > 80 ? '#dc2626' : sensors[1] > 70 ? '#ca8a04' : '#16a34a', 'Sensor[1] hotspot') : '
Sensors
N/A
'}
` })()}
` : ''} ${!isNvmeDisk && diskType === 'SSD' ? (() => { // Try to find SSD wear indicators from SMART attributes const wearAttr = smartAttributes.find(a => a.name?.toLowerCase().includes('wear_leveling') || a.name?.toLowerCase().includes('media_wearout') || a.name?.toLowerCase().includes('percent_lifetime') || a.name?.toLowerCase().includes('ssd_life_left') || a.id === 177 || a.id === 231 || a.id === 233 ) const lbasWrittenAttr = smartAttributes.find(a => a.name?.toLowerCase().includes('total_lbas_written') || a.id === 241 ) // Also check disk properties — cast to number since SmartAttribute.value is number | string const wearRaw = (wearAttr?.value !== undefined ? Number(wearAttr.value) : undefined) ?? disk.wear_leveling_count ?? disk.ssd_life_left if (wearRaw !== undefined && wearRaw !== null) { // ID 230 (Media_Wearout_Indicator on WD/SanDisk): value = endurance used % // All others (ID 177, 231, etc.): value = life remaining % const lifeRemaining = (wearAttr?.id === 230) ? (100 - wearRaw) : wearRaw const lifeUsed = 100 - lifeRemaining // Calculate data written — detect unit from attribute name let dataWrittenTB = 0 if (lbasWrittenAttr?.raw_value) { const rawValue = parseInt(lbasWrittenAttr.raw_value.replace(/[^0-9]/g, '')) if (!isNaN(rawValue)) { const attrName = (lbasWrittenAttr.name || '').toLowerCase() if (attrName.includes('gib') || attrName.includes('_gb')) { // Raw value already in GiB (WD Blue, Kingston, etc.) dataWrittenTB = rawValue / 1024 } else { // Raw value in LBAs — multiply by 512 bytes (Seagate, standard) dataWrittenTB = (rawValue * 512) / (1024 ** 4) } } } else if (disk.total_lbas_written) { dataWrittenTB = disk.total_lbas_written / 1024 // Already in GB from backend } return `
3. SSD Wear & Lifetime
LIFE REMAINING
${lifeRemaining}%
Source: ${wearAttr?.name?.replace(/_/g, ' ') || 'SSD Life Indicator'}
USAGE STATISTICS
Wear Level ${lifeUsed}%
${dataWrittenTB > 0 ? `
Data Written
${dataWrittenTB >= 1 ? dataWrittenTB.toFixed(2) + ' TB' : (dataWrittenTB * 1024).toFixed(1) + ' GB'}
Power On Hours
${fmtNum(powerOnHours)}h
` : ''}
Note: SSD life estimates are based on manufacturer-reported wear indicators. Actual lifespan may vary based on workload and usage patterns.
` } return '' })() : ''}
${isNvmeDisk ? '4' : (diskType === 'SSD' && (disk.wear_leveling_count !== undefined || disk.ssd_life_left !== undefined || smartAttributes.some(a => a.name?.toLowerCase().includes('wear'))) ? '4' : '3')}. ${isNvmeDisk ? 'NVMe Health Metrics' : isSasDisk ? 'SAS/SCSI Health Metrics' : 'SMART Attributes'} (${smartAttributes.length} total${hasCritical ? `, ${criticalAttrs.length} warning(s)` : ''})
${useSimpleTable ? '' : ''} ${useSimpleTable ? '' : ''} ${useSimpleTable ? '' : ''} ${useSimpleTable ? '' : ''} ${attributeRows || ''}
ID${isNvmeDisk ? 'Metric' : isSasDisk ? 'Metric' : 'Attribute'} ValueWorstThrRaw
No ' + (isNvmeDisk ? 'NVMe metrics' : isSasDisk ? 'SAS metrics' : 'SMART attributes') + ' available
${isNvmeDisk ? '5' : '4'}. ${isHistorical ? 'Self-Test Result' : 'Last Self-Test Result'}
${testStatus.last_test ? `
Test Type
${testStatus.last_test.type}
Result
${testStatus.last_test.status}
Completed
${testStatus.last_test.timestamp || 'N/A'}
At Power-On Hours
${testStatus.last_test.lifetime_hours ? fmtNum(testStatus.last_test.lifetime_hours) + 'h' : 'N/A'}
${(pollingShort || pollingExt) ? `
${pollingShort ? `
Short test: ~${pollingShort} min
` : ''} ${pollingExt ? `
Extended test: ~${pollingExt} min
` : ''} ${errorLogCount > 0 ? `
ATA error log: ${errorLogCount} entr${errorLogCount === 1 ? 'y' : 'ies'}
` : ''}
` : ''} ${selfTestHistory.length > 1 ? `
Full Self-Test History (${selfTestHistory.length} entries)
${selfTestHistory.map((e, i) => ` `).join('')}
# Type Status At POH
${i + 1} ${e.type_str || e.type} ${e.status_str || e.status} ${e.lifetime_hours != null ? fmtNum(e.lifetime_hours) + 'h' : 'N/A'}
` : ''} ` : lastTestDate ? `
${isHistorical ? 'Test Type' : 'Last Test Type'}
${testStatus.test_type || 'Extended'}
Result
Passed
Date
${new Date(lastTestDate).toLocaleString()}
At Power-On Hours
${fmtNum(powerOnHours)}h
Note: This disk's firmware does not maintain an internal self-test log. Test results are tracked by ProxMenux Monitor.
` : `
No self-test history available. Run a SMART self-test to see results here.
`}
${observationsHtml}
${observations.length > 0 ? (isNvmeDisk ? '7' : '6') : (isNvmeDisk ? '6' : '5')}. Recommendations
${recommendations.join('')}
` const blob = new Blob([html], { type: "text/html" }) const url = URL.createObjectURL(blob) if (targetWindow && !targetWindow.closed) { // Navigate the already-open window to the blob URL (proper navigation with back/close in webapp) targetWindow.location.href = url } else { window.open(url, "_blank") } } // SMART Test Tab Component interface SmartTestTabProps { disk: DiskInfo observations?: DiskObservation[] lastTestDate?: string } interface SmartSelfTestEntry { type: 'short' | 'long' | 'other' type_str: string status: 'passed' | 'failed' status_str: string lifetime_hours: number | null } interface SmartAttribute { id: number name: string value: number | string worst: number | string threshold: number | string raw_value: string status: 'ok' | 'warning' | 'critical' prefailure?: boolean flags?: string } interface NvmeRaw { critical_warning: number temperature: number avail_spare: number spare_thresh: number percent_used: number endurance_grp_critical_warning_summary: number data_units_read: number data_units_written: number host_read_commands: number host_write_commands: number controller_busy_time: number power_cycles: number power_on_hours: number unsafe_shutdowns: number media_errors: number num_err_log_entries: number warning_temp_time: number critical_comp_time: number temperature_sensors: (number | null)[] } interface SmartTestStatus { status: 'idle' | 'running' | 'completed' | 'failed' test_type?: string progress?: number result?: string supports_progress_reporting?: boolean supports_self_test?: boolean last_test?: { type: string status: string timestamp: string duration?: string lifetime_hours?: number } smart_data?: { device: string model: string model_family?: string serial: string firmware: string nvme_version?: string smart_status: string temperature: number temperature_sensors?: (number | null)[] power_on_hours: number power_cycles?: number rotation_rate?: number form_factor?: string physical_block_size?: number trim_supported?: boolean sata_version?: string interface_speed?: string polling_minutes_short?: number polling_minutes_extended?: number supports_progress_reporting?: boolean error_log_count?: number self_test_history?: SmartSelfTestEntry[] attributes: SmartAttribute[] nvme_raw?: NvmeRaw is_sas?: boolean logical_block_size?: number } tools_installed?: { smartctl: boolean nvme: boolean } } function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabProps) { const [testStatus, setTestStatus] = useState({ status: 'idle' }) const [loading, setLoading] = useState(true) const [runningTest, setRunningTest] = useState<'short' | 'long' | null>(null) // Extract SMART attributes from testStatus for the report const smartAttributes = testStatus.smart_data?.attributes || [] const fetchSmartStatus = async () => { try { setLoading(true) const data = await fetchApi(`/api/storage/smart/${disk.name}`) setTestStatus(data) return data } catch { setTestStatus({ status: 'idle' }) return { status: 'idle' } } finally { setLoading(false) } } // Fetch current SMART status on mount and start polling if test is running useEffect(() => { let pollInterval: NodeJS.Timeout | null = null const checkAndPoll = async () => { const data = await fetchSmartStatus() // If a test is already running, start polling if (data.status === 'running') { pollInterval = setInterval(async () => { try { const status = await fetchApi(`/api/storage/smart/${disk.name}`) setTestStatus(status) if (status.status !== 'running' && pollInterval) { clearInterval(pollInterval) pollInterval = null } } catch { if (pollInterval) { clearInterval(pollInterval) pollInterval = null } } }, 5000) } } checkAndPoll() return () => { if (pollInterval) clearInterval(pollInterval) } }, [disk.name]) const [testError, setTestError] = useState(null) const [installing, setInstalling] = useState(false) // Check if required tools are installed for this disk type const isNvme = disk.name.includes('nvme') const toolsAvailable = testStatus.tools_installed ? (isNvme ? testStatus.tools_installed.nvme : testStatus.tools_installed.smartctl) : true // Assume true until we get the status const installSmartTools = async () => { try { setInstalling(true) setTestError(null) const data = await fetchApi<{ success: boolean; error?: string }>('/api/storage/smart/tools/install', { method: 'POST', body: JSON.stringify({ install_all: true }) }) if (data.success) { fetchSmartStatus() } else { setTestError(data.error || 'Installation failed. Try manually: apt-get install smartmontools nvme-cli') } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to install tools' setTestError(`${message}. Try manually: apt-get install smartmontools nvme-cli`) } finally { setInstalling(false) } } const runSmartTest = async (testType: 'short' | 'long') => { try { setRunningTest(testType) setTestError(null) await fetchApi(`/api/storage/smart/${disk.name}/test`, { method: 'POST', body: JSON.stringify({ test_type: testType }) }) // Immediately fetch status to show progress bar fetchSmartStatus() // Poll for status updates // For disks that don't report progress, we keep polling but show an indeterminate progress bar let pollCount = 0 const maxPolls = testType === 'short' ? 36 : 720 // 3 min for short, 1 hour for long (at 5s intervals) const pollInterval = setInterval(async () => { pollCount++ try { const statusData = await fetchApi(`/api/storage/smart/${disk.name}`) setTestStatus(statusData) // Only clear runningTest when we get a definitive "not running" status if (statusData.status !== 'running') { clearInterval(pollInterval) setRunningTest(null) // Refresh SMART JSON data to get new test results fetchSmartStatus() } } catch { // Don't clear on error - keep showing progress } // Safety timeout: stop polling after max duration if (pollCount >= maxPolls) { clearInterval(pollInterval) setRunningTest(null) // Refresh status one more time to get final result fetchSmartStatus() } }, 5000) } catch (err) { const message = err instanceof Error ? err.message : 'Failed to start test' setTestError(message) setRunningTest(null) } } if (loading) { return (

Loading SMART data...

) } // If tools not available, show install button only if (!toolsAvailable && !loading) { return (

SMART Tools Not Installed

{isNvme ? 'nvme-cli is required to run SMART tests on NVMe disks.' : 'smartmontools is required to run SMART tests on this disk.'}

{testError && (

Installation Failed

{testError}

)}
) } 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.

{/* Error Message */} {testError && (

Failed to start test

{testError}

)}
{/* Test Progress - Show when API reports running OR when we just started a test */} {(testStatus.status === 'running' || runningTest !== null) && (

{(runningTest || testStatus.test_type) === 'short' ? 'Short' : 'Extended'} test in progress

Please wait while the test completes. Buttons will unlock when it finishes.

{/* Progress bar if disk reports percentage */} {testStatus.progress !== undefined ? ( ) : ( <>

This disk's firmware does not support progress reporting. The test is running in the background.

)}
)} {/* Last Test Result — only show if a test was executed from ProxMenux (lastTestDate exists) or if currently running/just completed a test. Tests from the drive's internal log (e.g. factory tests) are only shown in the full SMART report. */} {testStatus.last_test && lastTestDate && (
{testStatus.last_test.status === 'passed' ? ( ) : ( )} Last Test: {testStatus.last_test.type === 'short' ? 'Short' : 'Extended'} {testStatus.last_test.status} {new Date(lastTestDate).toLocaleString()}
)} {/* SMART Attributes Summary */} {testStatus.smart_data?.attributes && testStatus.smart_data.attributes.length > 0 && (

{isNvme ? 'NVMe Health Metrics' : testStatus.smart_data?.is_sas ? 'SAS/SCSI Health Metrics' : 'SMART Attributes'}

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

Generate a comprehensive professional report with detailed analysis and recommendations.

) } // ─── History Tab Component ────────────────────────────────────────────────────── interface SmartHistoryEntry { filename: string path: string timestamp: string test_type: string date_readable: string } function HistoryTab({ disk }: { disk: DiskInfo }) { const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) const [deleting, setDeleting] = useState(null) const [viewingReport, setViewingReport] = useState(null) const fetchHistory = async () => { try { setLoading(true) const data = await fetchApi<{ history: SmartHistoryEntry[] }>(`/api/storage/smart/${disk.name}/history?limit=50`) setHistory(data.history || []) } catch { setHistory([]) } finally { setLoading(false) } } useEffect(() => { fetchHistory() }, [disk.name]) const handleDelete = async (filename: string) => { try { setDeleting(filename) await fetchApi(`/api/storage/smart/${disk.name}/history/${filename}`, { method: 'DELETE' }) setHistory(prev => prev.filter(h => h.filename !== filename)) } catch { // Silently fail } finally { setDeleting(null) } } const handleDownload = async (filename: string) => { try { const response = await fetchApi>(`/api/storage/smart/${disk.name}/history/${filename}`) const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${disk.name}_${filename}` a.click() URL.revokeObjectURL(url) } catch { // Silently fail } } const handleViewReport = async (entry: SmartHistoryEntry) => { // Open window IMMEDIATELY on user click (before async) to avoid popup blocker const reportWindow = window.open('about:blank', '_blank') if (reportWindow) { reportWindow.document.write('

Loading report...

') } try { setViewingReport(entry.filename) // Fetch full SMART status from backend (same data as SMART tab uses) const fullStatus = await fetchApi(`/api/storage/smart/${disk.name}`) const attrs = fullStatus.smart_data?.attributes || [] openSmartReport(disk, fullStatus, attrs, [], entry.timestamp, reportWindow || undefined, true) } catch { if (reportWindow && !reportWindow.closed) { reportWindow.document.body.innerHTML = '

Failed to load report data.

' } } finally { setViewingReport(null) } } if (loading) { return (

Loading test history...

) } if (history.length === 0) { return (
No test history Run a SMART test to start building history for this disk.
) } return (

Test History {history.length}

{history.map((entry, i) => { const isLatest = i === 0 const testDate = new Date(entry.timestamp) const ageDays = Math.floor((Date.now() - testDate.getTime()) / (1000 * 60 * 60 * 24)) const isDeleting = deleting === entry.filename const isViewing = viewingReport === entry.filename return (
!isDeleting && handleViewReport(entry)} className={`border rounded-lg p-3 flex items-center gap-3 transition-colors cursor-pointer hover:bg-white/5 ${ isLatest ? 'border-orange-500/30' : 'border-border' } ${isDeleting ? 'opacity-50 pointer-events-none' : ''} ${isViewing ? 'opacity-70' : ''}`} > {isViewing ? ( ) : ( {entry.test_type === 'long' ? 'Extended' : 'Short'} )}

{testDate.toLocaleString()} {isLatest && latest}

{ageDays === 0 ? 'Today' : ageDays === 1 ? 'Yesterday' : `${ageDays} days ago`}

) })}

Test results are stored locally and used to generate detailed SMART reports.

) } // ─── Schedule Tab Component ───────────────────────────────────────────────────── interface SmartSchedule { id: string active: boolean test_type: 'short' | 'long' frequency: 'daily' | 'weekly' | 'monthly' hour: number minute: number day_of_week: number day_of_month: number disks: string[] retention: number notify_on_complete: boolean notify_only_on_failure: boolean } interface ScheduleConfig { enabled: boolean schedules: SmartSchedule[] } function ScheduleTab({ disk }: { disk: DiskInfo }) { const [config, setConfig] = useState({ enabled: true, schedules: [] }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [showForm, setShowForm] = useState(false) const [editingSchedule, setEditingSchedule] = useState(null) // Form state const [formData, setFormData] = useState>({ test_type: 'short', frequency: 'weekly', hour: 3, minute: 0, day_of_week: 0, day_of_month: 1, disks: [disk.name], retention: 10, active: true, notify_on_complete: true, notify_only_on_failure: false }) const fetchSchedules = async () => { try { setLoading(true) const data = await fetchApi('/api/storage/smart/schedules') setConfig(data) } catch { console.error('Failed to load schedules') } finally { setLoading(false) } } useEffect(() => { fetchSchedules() }, []) const handleToggleGlobal = async () => { try { setSaving(true) await fetchApi('/api/storage/smart/schedules/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !config.enabled }) }) setConfig(prev => ({ ...prev, enabled: !prev.enabled })) } catch { console.error('Failed to toggle schedules') } finally { setSaving(false) } } const handleSaveSchedule = async () => { try { setSaving(true) const scheduleData = { ...formData, id: editingSchedule?.id || undefined } await fetchApi('/api/storage/smart/schedules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(scheduleData) }) await fetchSchedules() setShowForm(false) setEditingSchedule(null) resetForm() } catch { console.error('Failed to save schedule') } finally { setSaving(false) } } const handleDeleteSchedule = async (id: string) => { try { setSaving(true) await fetchApi(`/api/storage/smart/schedules/${id}`, { method: 'DELETE' }) await fetchSchedules() } catch { console.error('Failed to delete schedule') } finally { setSaving(false) } } const resetForm = () => { setFormData({ test_type: 'short', frequency: 'weekly', hour: 3, minute: 0, day_of_week: 0, day_of_month: 1, disks: [disk.name], retention: 10, active: true, notify_on_complete: true, notify_only_on_failure: false }) } const editSchedule = (schedule: SmartSchedule) => { setEditingSchedule(schedule) setFormData(schedule) setShowForm(true) } const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const formatScheduleTime = (schedule: SmartSchedule) => { const time = `${schedule.hour.toString().padStart(2, '0')}:${schedule.minute.toString().padStart(2, '0')}` if (schedule.frequency === 'daily') return `Daily at ${time}` if (schedule.frequency === 'weekly') return `${dayNames[schedule.day_of_week]}s at ${time}` return `Day ${schedule.day_of_month} of month at ${time}` } if (loading) { return (
Loading schedules...
) } return (
{/* Global Toggle */}

Automatic SMART Tests

Enable or disable all scheduled tests

{/* Schedules List */} {config.schedules.length > 0 ? (

Configured Schedules

{config.schedules.map(schedule => (
{schedule.test_type} {formatScheduleTime(schedule)}
Disks: {schedule.disks.includes('all') ? 'All disks' : schedule.disks.join(', ')} | Keep {schedule.retention} results
))}
) : (

No scheduled tests configured

Create a schedule to automatically run SMART tests

)} {/* Add/Edit Form */} {showForm ? (

{editingSchedule ? 'Edit Schedule' : 'New Schedule'}

{formData.frequency === 'weekly' && (
)} {formData.frequency === 'monthly' && (
)}
) : ( )}

Scheduled tests run automatically via cron. Results are saved to the SMART history.

) }