diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 2c7dd1cf3..7777de35c 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -11,6 +11,7 @@ import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector'; import { MetricBar } from './MetricBar'; import { formatBytes, formatUptime } from '@/utils/format'; import { DashboardFilter } from './DashboardFilter'; +import { DiskHealthSummary } from './DiskHealthSummary'; interface DashboardProps { vms: VM[]; @@ -24,7 +25,7 @@ type GroupingMode = 'grouped' | 'flat'; export function Dashboard(props: DashboardProps) { - const { connected, activeAlerts, initialDataReceived } = useWebSocket(); + const { connected, activeAlerts, initialDataReceived, state } = useWebSocket(); const [search, setSearch] = createSignal(''); const [isSearchLocked, setIsSearchLocked] = createSignal(false); const [selectedNode, setSelectedNode] = createSignal(null); @@ -475,6 +476,11 @@ export function Dashboard(props: DashboardProps) { searchTerm={search()} /> + {/* Disk Health Summary Widget */} +
+ +
+ {/* Removed old node table - keeping the rest unchanged */}
diff --git a/frontend-modern/src/components/Dashboard/DiskHealthSummary.tsx b/frontend-modern/src/components/Dashboard/DiskHealthSummary.tsx new file mode 100644 index 000000000..473784db8 --- /dev/null +++ b/frontend-modern/src/components/Dashboard/DiskHealthSummary.tsx @@ -0,0 +1,129 @@ +import { Component, createMemo, Show } from 'solid-js'; +import type { PhysicalDisk } from '@/types/api'; + +interface DiskHealthSummaryProps { + disks: PhysicalDisk[]; +} + +export const DiskHealthSummary: Component = (props) => { + const diskStats = createMemo(() => { + const disks = props.disks || []; + const total = disks.length; + const healthy = disks.filter(d => d.health === 'PASSED').length; + const failing = disks.filter(d => d.health === 'FAILED').length; + const unknown = disks.filter(d => d.health === 'UNKNOWN' || !d.health).length; + const lowLife = disks.filter(d => d.wearout > 0 && d.wearout < 10).length; + const avgWearout = disks.filter(d => d.wearout > 0).reduce((sum, d) => sum + d.wearout, 0) / + disks.filter(d => d.wearout > 0).length || 0; + + // Group by node + const byNode: Record = {}; + disks.forEach(d => { + byNode[d.node] = (byNode[d.node] || 0) + 1; + }); + + return { + total, + healthy, + failing, + unknown, + lowLife, + avgWearout, + byNode + }; + }); + + const healthColor = createMemo(() => { + const stats = diskStats(); + if (stats.failing > 0) return 'text-red-600 dark:text-red-400'; + if (stats.lowLife > 0) return 'text-yellow-600 dark:text-yellow-400'; + if (stats.unknown > 0) return 'text-gray-600 dark:text-gray-400'; + return 'text-green-600 dark:text-green-400'; + }); + + const healthBg = createMemo(() => { + const stats = diskStats(); + if (stats.failing > 0) return 'bg-red-50 dark:bg-red-950/20 border-red-200 dark:border-red-800'; + if (stats.lowLife > 0) return 'bg-yellow-50 dark:bg-yellow-950/20 border-yellow-200 dark:border-yellow-800'; + return 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700'; + }); + + return ( + 0}> +
+
+

+ Disk Health Summary +

+ + {diskStats().healthy}/{diskStats().total} + +
+ +
+ {/* Health Status */} +
+ Status +
+ 0}> + + ✅ {diskStats().healthy} + + + 0}> + + ❌ {diskStats().failing} + + + 0}> + + ⚠️ {diskStats().lowLife} low + + + 0}> + + ❓ {diskStats().unknown} + + +
+
+ + {/* Average SSD Life */} + 0}> +
+ Avg SSD Life +
+
+
= 50 ? 'bg-green-500' : + diskStats().avgWearout >= 20 ? 'bg-yellow-500' : + diskStats().avgWearout >= 10 ? 'bg-orange-500' : + 'bg-red-500' + }`} + style={`width: ${diskStats().avgWearout}%`} + /> +
+ + {Math.round(diskStats().avgWearout)}% + +
+
+ + + {/* Disk Distribution */} +
+
Distribution
+
+ {Object.entries(diskStats().byNode).map(([node, count]) => ( + + {node}: {count} + + ))} +
+
+
+
+ + ); +}; \ No newline at end of file diff --git a/frontend-modern/src/components/shared/NodeSummaryTable.tsx b/frontend-modern/src/components/shared/NodeSummaryTable.tsx index f4fccbf81..f45825950 100644 --- a/frontend-modern/src/components/shared/NodeSummaryTable.tsx +++ b/frontend-modern/src/components/shared/NodeSummaryTable.tsx @@ -18,7 +18,7 @@ interface NodeSummaryTableProps { } export const NodeSummaryTable: Component = (props) => { - const { activeAlerts } = useWebSocket(); + const { activeAlerts, state } = useWebSocket(); // Combine and sort nodes based on tab const sortedItems = createMemo(() => { const items: Array<{ type: 'pve' | 'pbs'; data: Node | PBSInstance }> = []; @@ -56,7 +56,7 @@ export const NodeSummaryTable: Component = (props) => { const getCountHeader = () => { switch (props.currentTab) { case 'dashboard': return ['VMs', 'Containers']; - case 'storage': return ['Storage']; + case 'storage': return ['Storage', 'Disks']; case 'backups': return ['Backups']; default: return []; } @@ -71,8 +71,8 @@ export const NodeSummaryTable: Component = (props) => { // PBS doesn't have VMs/Containers, return dashes return ['-', '-']; case 'storage': - // PBS doesn't have storage count - return ['-']; + // PBS doesn't have storage or disk count + return ['-', '-']; case 'backups': // PBS shows backup count return [props.backupCounts?.[item.data.name] || 0]; @@ -89,7 +89,8 @@ export const NodeSummaryTable: Component = (props) => { return [vmCount, containerCount]; case 'storage': const storageCount = props.storage?.filter(s => s.node === node.name).length || 0; - return [storageCount]; + const diskCount = state.physicalDisks?.filter(d => d.node === node.name).length || 0; + return [storageCount, diskCount]; case 'backups': return [props.backupCounts?.[node.name] || 0]; default: