"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 } 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" && ( )}
) } // Generate SMART Report HTML and open in new window (same pattern as Lynis/Latency reports) function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttributes: Array<{id: number; name: string; value: number; worst: number; threshold: number; raw_value: string; status: 'ok' | 'warning' | 'critical'}>, observations: DiskObservation[] = []) { const now = new Date().toLocaleString() const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` const reportId = `SMART-${Date.now().toString(36).toUpperCase()}` // Determine disk type let diskType = "HDD" if (disk.name.startsWith("nvme")) { diskType = "NVMe" } 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 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 (${powerOnHours.toLocaleString()}h)` : `${powerOnDays}d (${powerOnHours.toLocaleString()}h)` // Build attributes table - format differs for NVMe vs SATA const isNvmeForTable = diskType === 'NVMe' 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' if (isNvmeForTable) { // NVMe format: Metric | Value | Status return ` ${attr.name} ${attr.value} ${attr.status === 'ok' ? 'OK' : attr.status.toUpperCase()} ` } 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()} ` } }).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 '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' } : { 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.

`) } 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 => { const severityColor = obs.severity === 'critical' ? '#dc2626' : obs.severity === 'warning' ? '#ca8a04' : '#3b82f6' const severityBg = obs.severity === 'critical' ? '#dc262615' : obs.severity === 'warning' ? '#ca8a0415' : '#3b82f615' 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' : '' obsItemsHtml += `
${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}
` }) observationsHtml = `
6. 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}

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

2. Disk Information
Model
${disk.model || testStatus.smart_data?.model || 'Unknown'}
Serial
${disk.serial || testStatus.smart_data?.serial || 'Unknown'}
Capacity
${disk.size_formatted || 'Unknown'}
Type
${diskType}
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
Temperature
Optimal: ${tempThresholds.optimal}
${powerOnHours.toLocaleString()}h
Power On Time
${(disk.power_cycles ?? 0).toLocaleString()}
Power Cycles
${isNvmeDisk ? `
${testStatus.smart_data?.nvme_raw?.media_errors ?? 0}
Media Errors
` : `
${disk.reallocated_sectors ?? 0}
Reallocated Sectors
`}
${!isNvmeDisk ? `
3. Disk Overview
Temperature
${disk.temperature > 0 ? disk.temperature + '°C' : 'N/A'}
Optimal: ${tempThresholds.optimal}
Power On Hours
${powerOnHours.toLocaleString()}h
${powerOnYears}y ${powerOnDays}d
Rotation Rate
${diskType === 'HDD' ? (disk.rotation_rate ? disk.rotation_rate + ' RPM' : 'N/A') : 'SSD'}
Power Cycles
${(disk.power_cycles ?? 0).toLocaleString()}
HEALTH INDICATORS
SMART Status
${disk.smart_status || 'N/A'}
Reallocated Sectors
${disk.reallocated_sectors ?? 0}
Pending Sectors
${disk.pending_sectors ?? 0}
CRC Errors
${disk.crc_errors ?? 0}
` : ''} ${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?.toLocaleString() ?? disk.power_cycles ?? 'N/A'}
` : ''}
4. ${isNvmeDisk ? 'NVMe Health Metrics' : 'SMART Attributes'} (${smartAttributes.length} total${hasCritical ? `, ${criticalAttrs.length} warning(s)` : ''})
${isNvmeDisk ? '' : ''} ${isNvmeDisk ? '' : ''} ${isNvmeDisk ? '' : ''} ${isNvmeDisk ? '' : ''} ${attributeRows || ''}
ID${isNvmeDisk ? 'Metric' : 'Attribute'} ValueWorstThrRaw
No ' + (isNvmeDisk ? 'NVMe metrics' : 'SMART attributes') + ' available
5. 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'}
Duration
${testStatus.last_test.duration || 'N/A'}
` : `
No self-test history available. Run a SMART self-test to see results here.
`}
${observationsHtml}
${observations.length > 0 ? '7' : '6'}. Recommendations
${recommendations.join('')}
` const blob = new Blob([html], { type: "text/html" }) const url = URL.createObjectURL(blob) window.open(url, "_blank") } // SMART Test Tab Component interface SmartTestTabProps { disk: DiskInfo observations?: DiskObservation[] } interface SmartTestStatus { status: 'idle' | 'running' | 'completed' | 'failed' test_type?: string progress?: number result?: string last_test?: { type: string status: string timestamp: string duration?: string lifetime_hours?: number } 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' }> } tools_installed?: { smartctl: boolean nvme: boolean } } function SmartTestTab({ disk, observations = [] }: 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 || [] // 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 [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 }) }) // 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 (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 */} {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}

Result

{testStatus.last_test.timestamp}

{testStatus.last_test.lifetime_hours && (

At Power-On Hours

{testStatus.last_test.lifetime_hours.toLocaleString()}h

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

{isNvme ? 'NVMe Health Metrics' : 'SMART Attributes'}

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

Generate a comprehensive professional report with detailed analysis and recommendations.

) }