From 95718c889d59191c22000dee8e406700546197ad Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 2 Oct 2025 22:29:24 +0200 Subject: [PATCH] Update AppImage --- AppImage/components/proxmox-dashboard.tsx | 10 +- AppImage/components/storage-overview.tsx | 295 ++++++++++++++++++++++ AppImage/scripts/flask_server.py | 158 ++++++++++-- 3 files changed, 432 insertions(+), 31 deletions(-) create mode 100644 AppImage/components/storage-overview.tsx diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 09bd350..e2020f5 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -5,7 +5,7 @@ import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs" import { SystemOverview } from "./system-overview" -import { StorageMetrics } from "./storage-metrics" +import { StorageOverview } from "./storage-overview" import { NetworkMetrics } from "./network-metrics" import { VirtualMachines } from "./virtual-machines" import { SystemLogs } from "./system-logs" @@ -47,7 +47,8 @@ export function ProxmoxDashboard() { console.log("[v0] Fetching system data from Flask server...") console.log("[v0] Current window location:", window.location.href) - const apiUrl = "/api/system" + const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" + const apiUrl = `${baseUrl}/api/system` console.log("[v0] API URL:", apiUrl) @@ -57,6 +58,7 @@ export function ProxmoxDashboard() { headers: { "Content-Type": "application/json", }, + cache: "no-store", }) console.log("[v0] Response status:", response.status) @@ -104,7 +106,7 @@ export function ProxmoxDashboard() { useEffect(() => { fetchSystemData() - const interval = setInterval(fetchSystemData, 10000) // Updated interval to 10 seconds + const interval = setInterval(fetchSystemData, 10000) return () => clearInterval(interval) }, [fetchSystemData]) @@ -268,7 +270,7 @@ export function ProxmoxDashboard() { - + diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx new file mode 100644 index 0000000..9c2ddd8 --- /dev/null +++ b/AppImage/components/storage-overview.tsx @@ -0,0 +1,295 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Thermometer } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" + +interface DiskInfo { + name: string + size?: string + 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 +} + +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[] + error?: string +} + +export function StorageOverview() { + const [storageData, setStorageData] = useState(null) + const [loading, setLoading] = useState(true) + + const fetchStorageData = async () => { + try { + const baseUrl = + typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : "" + const response = await fetch(`${baseUrl}/api/storage`) + const data = await response.json() + setStorageData(data) + } catch (error) { + console.error("Error fetching storage data:", error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchStorageData() + const interval = setInterval(fetchStorageData, 15000) // Update every 15 seconds + 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) => { + if (temp === 0) return "text-gray-500" + if (temp < 45) return "text-green-500" + if (temp < 60) return "text-yellow-500" + return "text-red-500" + } + + if (loading) { + return ( +
+
Loading storage information...
+
+ ) + } + + if (!storageData || storageData.error) { + return ( +
+
Error loading storage data: {storageData?.error || "Unknown error"}
+
+ ) + } + + return ( +
+ {/* Storage Summary */} +
+ + + Total Storage + + + +
{storageData.total} GB
+
+
+ + + + Used Storage + + + +
{storageData.used} GB
+

+ {storageData.total > 0 ? Math.round((storageData.used / storageData.total) * 100) : 0}% used +

+
+
+ + + + Available Storage + + + +
{storageData.available} GB
+
+
+
+ + {/* ZFS Pools */} + {storageData.zfs_pools && storageData.zfs_pools.length > 0 && ( + + + + + ZFS Pools + + + +
+ {storageData.zfs_pools.map((pool) => ( +
+
+
+

{pool.name}

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

Size

+

{pool.size}

+
+
+

Allocated

+

{pool.allocated}

+
+
+

Free

+

{pool.free}

+
+
+
+ ))} +
+
+
+ )} + + {/* Physical Disks */} + + + + + Physical Disks & SMART Status + + + +
+ {storageData.disks.map((disk) => ( +
+
+
+ +
+

/dev/{disk.name}

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

{disk.model}

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

Size

+

{disk.size}

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

SMART Status

+

{disk.smart_status}

+
+ )} + {disk.power_on_hours && disk.power_on_hours > 0 && ( +
+

Power On Hours

+

{disk.power_on_hours.toLocaleString()}h

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

Serial

+

{disk.serial}

+
+ )} +
+ + {disk.mountpoint && ( +
+
+
+ Mounted at: + {disk.mountpoint} + {disk.fstype && ({disk.fstype})} +
+ {disk.usage_percent !== undefined && ( + {disk.usage_percent}% + )} +
+ {disk.usage_percent !== undefined && } + {disk.total && disk.used && disk.available && ( +
+ {disk.used} GB used + + {disk.available} GB free of {disk.total} GB + +
+ )} +
+ )} +
+ ))} +
+
+
+
+ ) +} diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 20b60bf..9e3e4f5 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -342,7 +342,8 @@ def get_storage_info(): 'total': 0, 'used': 0, 'available': 0, - 'disks': [] + 'disks': [], + 'zfs_pools': [] } # Get disk usage for root partition @@ -351,27 +352,67 @@ def get_storage_info(): storage_data['used'] = round(disk_usage.used / (1024**3), 1) # GB storage_data['available'] = round(disk_usage.free / (1024**3), 1) # GB - # Get individual disk information + try: + # List all block devices + result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME,SIZE,TYPE'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + parts = line.split() + if len(parts) >= 3 and parts[2] == 'disk': + disk_name = parts[0] + disk_size = parts[1] + + # Get SMART data for this disk + smart_data = get_smart_data(disk_name) + + disk_info = { + 'name': disk_name, + 'size': disk_size, + 'temperature': smart_data.get('temperature', 0), + 'health': smart_data.get('health', 'unknown'), + 'power_on_hours': smart_data.get('power_on_hours', 0), + 'smart_status': smart_data.get('smart_status', 'unknown'), + 'model': smart_data.get('model', 'Unknown'), + 'serial': smart_data.get('serial', 'Unknown') + } + storage_data['disks'].append(disk_info) + except Exception as e: + print(f"Error getting disk list: {e}") + + try: + result = subprocess.run(['zpool', 'list', '-H', '-o', 'name,size,alloc,free,health'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if line: + parts = line.split('\t') + if len(parts) >= 5: + pool_info = { + 'name': parts[0], + 'size': parts[1], + 'allocated': parts[2], + 'free': parts[3], + 'health': parts[4] + } + storage_data['zfs_pools'].append(pool_info) + except Exception as e: + print(f"Note: ZFS not available or no pools: {e}") + + # Get individual disk partitions disk_partitions = psutil.disk_partitions() for partition in disk_partitions: try: partition_usage = psutil.disk_usage(partition.mountpoint) + # Find corresponding disk info disk_temp = 0 - try: - # Try to get disk temperature from sensors - if hasattr(psutil, "sensors_temperatures"): - temps = psutil.sensors_temperatures() - if temps: - for name, entries in temps.items(): - if 'disk' in name.lower() or 'hdd' in name.lower() or 'sda' in name.lower(): - if entries: - disk_temp = entries[0].current - break - except: - pass + for disk in storage_data['disks']: + if disk['name'] in partition.device: + disk_temp = disk['temperature'] + break - disk_info = { + partition_info = { 'name': partition.device, 'mountpoint': partition.mountpoint, 'fstype': partition.fstype, @@ -379,10 +420,13 @@ def get_storage_info(): 'used': round(partition_usage.used / (1024**3), 1), 'available': round(partition_usage.free / (1024**3), 1), 'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1), - 'health': 'unknown', # Would need SMART data for real health 'temperature': disk_temp } - storage_data['disks'].append(disk_info) + + # Add to disks list if not already there + if not any(d['name'] == partition.device for d in storage_data['disks']): + storage_data['disks'].append(partition_info) + except PermissionError: print(f"Permission denied accessing {partition.mountpoint}") continue @@ -390,15 +434,6 @@ def get_storage_info(): print(f"Error accessing partition {partition.device}: {e}") continue - if not storage_data['disks'] and storage_data['total'] == 0: - return { - 'error': 'No storage data available - unable to access disk information', - 'total': 0, - 'used': 0, - 'available': 0, - 'disks': [] - } - return storage_data except Exception as e: @@ -408,9 +443,78 @@ def get_storage_info(): 'total': 0, 'used': 0, 'available': 0, - 'disks': [] + 'disks': [], + 'zfs_pools': [] } +def get_smart_data(disk_name): + """Get SMART data for a specific disk""" + smart_data = { + 'temperature': 0, + 'health': 'unknown', + 'power_on_hours': 0, + 'smart_status': 'unknown', + 'model': 'Unknown', + 'serial': 'Unknown' + } + + try: + # Try to get SMART data using smartctl + result = subprocess.run(['smartctl', '-a', f'/dev/{disk_name}'], + capture_output=True, text=True, timeout=10) + + if result.returncode in [0, 4]: # 0 = success, 4 = some SMART values exceeded threshold + output = result.stdout + + # Parse SMART status + if 'SMART overall-health self-assessment test result: PASSED' in output: + smart_data['smart_status'] = 'passed' + smart_data['health'] = 'healthy' + elif 'SMART overall-health self-assessment test result: FAILED' in output: + smart_data['smart_status'] = 'failed' + smart_data['health'] = 'critical' + + # Parse temperature + for line in output.split('\n'): + if 'Temperature_Celsius' in line or 'Temperature' in line: + parts = line.split() + for i, part in enumerate(parts): + if part.isdigit() and int(part) > 0 and int(part) < 100: + smart_data['temperature'] = int(part) + break + + # Parse power on hours + if 'Power_On_Hours' in line: + parts = line.split() + for part in parts: + if part.isdigit() and int(part) > 0: + smart_data['power_on_hours'] = int(part) + break + + # Parse model + if 'Device Model:' in line or 'Model Number:' in line: + smart_data['model'] = line.split(':', 1)[1].strip() + + # Parse serial + if 'Serial Number:' in line or 'Serial number:' in line: + smart_data['serial'] = line.split(':', 1)[1].strip() + + # Determine health based on temperature and SMART status + if smart_data['temperature'] > 0: + if smart_data['temperature'] > 60: + smart_data['health'] = 'warning' + elif smart_data['temperature'] > 70: + smart_data['health'] = 'critical' + elif smart_data['smart_status'] == 'passed': + smart_data['health'] = 'healthy' + + except FileNotFoundError: + print(f"smartctl not found - install smartmontools package") + except Exception as e: + print(f"Error getting SMART data for {disk_name}: {e}") + + return smart_data + def get_network_info(): """Get network interface information""" try: