diff --git a/AppImage/app/hardware/page.tsx b/AppImage/app/hardware/page.tsx new file mode 100644 index 0000000..395f3a4 --- /dev/null +++ b/AppImage/app/hardware/page.tsx @@ -0,0 +1,5 @@ +import Hardware from "@/components/hardware" + +export default function HardwarePage() { + return +} diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx new file mode 100644 index 0000000..9de2311 --- /dev/null +++ b/AppImage/components/hardware.tsx @@ -0,0 +1,636 @@ +"use client" + +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Cpu, MemoryStick, HardDrive, Network, Monitor, Thermometer, Fan, Battery, Server } from "lucide-react" +import useSWR from "swr" + +const fetcher = (url: string) => fetch(url).then((res) => res.json()) + +interface CPUInfo { + model?: string + total_threads?: number + cores_per_socket?: number + sockets?: number + current_mhz?: number + max_mhz?: number + virtualization?: string + l1d_cache?: string + l2_cache?: string + l3_cache?: string +} + +interface MotherboardInfo { + manufacturer?: string + model?: string + version?: string + serial?: string + bios?: { + vendor?: string + version?: string + date?: string + } +} + +interface MemoryModule { + size: string + type: string + speed: string + manufacturer?: string + slot?: string +} + +interface StorageDevice { + name: string + size: string + model: string + temperature: number + health: string + power_on_hours: number + rotation_rate: number +} + +interface NetworkCard { + name: string + type: string +} + +interface GraphicsCard { + name: string + memory?: string + temperature?: number + power_draw?: string + vendor: string +} + +interface TemperatureSensor { + name: string + current: number + high?: number + critical?: number +} + +interface FanSensor { + name: string + current_rpm: number +} + +interface UPSInfo { + model?: string + status?: string + battery_charge?: string + time_left?: string + load_percent?: string + line_voltage?: string +} + +interface HardwareData { + cpu: CPUInfo + motherboard: MotherboardInfo + memory_modules: MemoryModule[] + storage_devices: StorageDevice[] + network_cards: NetworkCard[] + graphics_cards: GraphicsCard[] + sensors: { + temperatures: TemperatureSensor[] + fans: FanSensor[] + } + power: UPSInfo +} + +export default function Hardware() { + const { data: hardwareData, error } = useSWR("/api/hardware", fetcher, { + refreshInterval: 5000, + }) + + if (error) { + return ( +
+
+

Failed to load hardware information

+
+
+ ) + } + + if (!hardwareData) { + return ( +
+
+
+
+
+ ) + } + + const getHealthColor = (health: string) => { + switch (health.toLowerCase()) { + case "healthy": + return "text-green-500" + case "warning": + return "text-yellow-500" + case "critical": + case "failed": + return "text-red-500" + default: + return "text-muted-foreground" + } + } + + const getHealthBadge = (health: string) => { + switch (health.toLowerCase()) { + case "healthy": + return Healthy + case "warning": + return Warning + case "critical": + case "failed": + return Critical + default: + return Unknown + } + } + + const getTempColor = (temp: number, high?: number, critical?: number) => { + if (critical && temp >= critical) return "text-red-500" + if (high && temp >= high) return "text-yellow-500" + if (temp >= 70) return "text-red-500" + if (temp >= 60) return "text-yellow-500" + return "text-green-500" + } + + const getTempProgress = (temp: number, critical?: number) => { + const max = critical || 100 + return (temp / max) * 100 + } + + const hasSensors = hardwareData.sensors.temperatures.length > 0 || hardwareData.sensors.fans.length > 0 + const hasUPS = hardwareData.power && Object.keys(hardwareData.power).length > 0 + + const storageSummary = hardwareData.storage_devices.reduce( + (acc, disk) => { + const sizeMatch = disk.size.match(/(\d+\.?\d*)\s*([KMGT]B)/) + if (sizeMatch) { + let sizeInGB = Number.parseFloat(sizeMatch[1]) + const unit = sizeMatch[2] + if (unit === "TB") sizeInGB *= 1024 + else if (unit === "MB") sizeInGB /= 1024 + else if (unit === "KB") sizeInGB /= 1024 * 1024 + acc.totalCapacity += sizeInGB + } + + if (disk.rotation_rate === 0) acc.ssd++ + else if (disk.rotation_rate > 0) acc.hdd++ + + return acc + }, + { totalCapacity: 0, ssd: 0, hdd: 0 }, + ) + + return ( +
+ {/* System Information */} + +
+ +

System Information

+
+ +
+ {/* CPU */} + {hardwareData.cpu.model && ( +
+
+ +

CPU

+
+
+
+ Model + {hardwareData.cpu.model} +
+ {hardwareData.cpu.sockets && hardwareData.cpu.cores_per_socket && ( +
+ Cores + + {hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "} + {hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores + +
+ )} + {hardwareData.cpu.total_threads && ( +
+ Threads + {hardwareData.cpu.total_threads} +
+ )} + {hardwareData.cpu.current_mhz && ( +
+ Frequency + {hardwareData.cpu.current_mhz.toFixed(0)} MHz +
+ )} + {hardwareData.cpu.l3_cache && ( +
+ L3 Cache + {hardwareData.cpu.l3_cache} +
+ )} + {hardwareData.cpu.virtualization && ( +
+ Virtualization + {hardwareData.cpu.virtualization} +
+ )} +
+
+ )} + + {/* Motherboard */} + {hardwareData.motherboard.manufacturer && ( +
+
+ +

Motherboard

+
+
+
+ Manufacturer + {hardwareData.motherboard.manufacturer} +
+ {hardwareData.motherboard.model && ( +
+ Model + {hardwareData.motherboard.model} +
+ )} + {hardwareData.motherboard.bios && ( + <> +
+ BIOS + {hardwareData.motherboard.bios.vendor} +
+
+ Version + {hardwareData.motherboard.bios.version} +
+ {hardwareData.motherboard.bios.date && ( +
+ Date + {hardwareData.motherboard.bios.date} +
+ )} + + )} +
+
+ )} +
+
+ + {/* Memory Modules */} + {hardwareData.memory_modules.length > 0 && ( + +
+ +

Memory Modules

+ + {hardwareData.memory_modules.length} installed + +
+ +
+ {hardwareData.memory_modules.map((module, index) => ( + +
+ {module.slot &&
{module.slot}
} +
+ Size + {module.size} +
+
+ Type + {module.type} +
+
+ Speed + {module.speed} +
+ {module.manufacturer && module.manufacturer !== "Unknown" && ( +
+ Manufacturer + {module.manufacturer} +
+ )} +
+
+ ))} +
+
+ )} + + {/* Storage Summary - Simplified */} + {hardwareData.storage_devices.length > 0 && ( + +
+ +

Storage Summary

+ + {hardwareData.storage_devices.length} devices + +
+ +
+
+

Total Capacity

+

{storageSummary.totalCapacity.toFixed(1)} GB

+
+ + {storageSummary.ssd > 0 && ( +
+

SSD Drives

+

{storageSummary.ssd}

+
+ )} + + {storageSummary.hdd > 0 && ( +
+

HDD Drives

+

{storageSummary.hdd}

+
+ )} +
+ +

+ For detailed storage information, see the Storage section +

+
+ )} + + {/* Storage Devices */} + {hardwareData.storage_devices.length > 0 && ( + +
+ +

Storage Devices

+ + {hardwareData.storage_devices.length} devices + +
+ +
+ {hardwareData.storage_devices.map((disk, index) => ( + +
+
+
+ /dev/{disk.name} + {getHealthBadge(disk.health)} + {disk.rotation_rate === 0 && ( + + SSD + + )} + {disk.rotation_rate > 0 && ( + + {disk.rotation_rate} RPM + + )} +
+ +
+
+ Model + {disk.model} +
+
+ Size + {disk.size} +
+ {disk.temperature > 0 && ( +
+ Temperature + {disk.temperature}°C +
+ )} + {disk.power_on_hours > 0 && ( +
+ Power On Hours + {disk.power_on_hours.toLocaleString()}h +
+ )} +
+
+
+
+ ))} +
+
+ )} + + {/* Graphics Cards */} + {hardwareData.graphics_cards.length > 0 && ( + +
+ +

Graphics Cards

+ + {hardwareData.graphics_cards.length} GPU{hardwareData.graphics_cards.length > 1 ? "s" : ""} + +
+ +
+ {hardwareData.graphics_cards.map((gpu, index) => ( + +
+
{gpu.name}
+
+ Vendor + {gpu.vendor} +
+ {gpu.memory && ( +
+ Memory + {gpu.memory} +
+ )} + {gpu.temperature && gpu.temperature > 0 && ( +
+ Temperature + {gpu.temperature}°C +
+ )} + {gpu.power_draw && ( +
+ Power Draw + {gpu.power_draw} +
+ )} +
+
+ ))} +
+
+ )} + + {/* Network Summary - Simplified */} + {hardwareData.network_cards.length > 0 && ( + +
+ +

Network Summary

+ + {hardwareData.network_cards.length} interfaces + +
+ +
+ {hardwareData.network_cards.map((nic, index) => ( +
+ {nic.name} + + {nic.type} + +
+ ))} +
+ +

+ For detailed network information, see the Network section +

+
+ )} + + {/* Network Cards */} + {hardwareData.network_cards.length > 0 && ( + +
+ +

Network Cards

+ + {hardwareData.network_cards.length} NIC{hardwareData.network_cards.length > 1 ? "s" : ""} + +
+ +
+ {hardwareData.network_cards.map((nic, index) => ( + +
+ {nic.name} + + {nic.type} + +
+
+ ))} +
+
+ )} + + {/* Sensors (Temperature & Fans) */} + {hasSensors && ( + +
+ +

Thermal & Fan Monitoring

+
+ +
+ {/* Temperatures */} + {hardwareData.sensors.temperatures.length > 0 && ( +
+

Temperatures

+
+ {hardwareData.sensors.temperatures.map((sensor, index) => ( +
+
+ {sensor.name} + + {sensor.current.toFixed(1)}°C + +
+ +
+ ))} +
+
+ )} + + {/* Fans */} + {hardwareData.sensors.fans.length > 0 && ( +
+

Fans

+
+ {hardwareData.sensors.fans.map((fan, index) => ( +
+
+ + {fan.name} +
+ {fan.current_rpm} RPM +
+ ))} +
+
+ )} +
+
+ )} + + {/* Power Supply / UPS */} + {hasUPS && ( + +
+ +

Power Supply / UPS

+
+ +
+ {hardwareData.power.model && ( +
+ Model + {hardwareData.power.model} +
+ )} + {hardwareData.power.status && ( +
+ Status + {hardwareData.power.status} +
+ )} + {hardwareData.power.battery_charge && ( +
+ Battery Charge + {hardwareData.power.battery_charge} +
+ )} + {hardwareData.power.time_left && ( +
+ Time Left + {hardwareData.power.time_left} +
+ )} + {hardwareData.power.load_percent && ( +
+ Load + {hardwareData.power.load_percent} +
+ )} + {hardwareData.power.line_voltage && ( +
+ Line Voltage + {hardwareData.power.line_voltage} +
+ )} +
+
+ )} +
+ ) +} diff --git a/AppImage/components/sidebar.tsx b/AppImage/components/sidebar.tsx new file mode 100644 index 0000000..eae7fc3 --- /dev/null +++ b/AppImage/components/sidebar.tsx @@ -0,0 +1,10 @@ +import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText } from "path-to-icons" + +const menuItems = [ + { name: "Overview", href: "/", icon: LayoutDashboard }, + { name: "Storage", href: "/storage", icon: HardDrive }, + { name: "Network", href: "/network", icon: Network }, + { name: "Virtual Machines", href: "/virtual-machines", icon: Server }, + { name: "Hardware", href: "/hardware", icon: Cpu }, // New Hardware section + { name: "System Logs", href: "/logs", icon: FileText }, +] diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 2a8e2fb..849924e 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -1295,6 +1295,261 @@ def get_proxmox_vms(): 'vms': [] } +def get_hardware_info(): + """Get comprehensive hardware information""" + hardware_data = { + 'cpu': {}, + 'motherboard': {}, + 'memory_modules': [], + 'storage_devices': [], + 'network_cards': [], + 'graphics_cards': [], + 'sensors': { + 'temperatures': [], + 'fans': [] + }, + 'power': {} + } + + try: + # CPU Information + try: + result = subprocess.run(['lscpu'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + cpu_info = {} + for line in result.stdout.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + key = key.strip() + value = value.strip() + + if key == 'Model name': + cpu_info['model'] = value + elif key == 'CPU(s)': + cpu_info['total_threads'] = int(value) + elif key == 'Core(s) per socket': + cpu_info['cores_per_socket'] = int(value) + elif key == 'Socket(s)': + cpu_info['sockets'] = int(value) + elif key == 'CPU MHz': + cpu_info['current_mhz'] = float(value) + elif key == 'CPU max MHz': + cpu_info['max_mhz'] = float(value) + elif key == 'CPU min MHz': + cpu_info['min_mhz'] = float(value) + elif key == 'Virtualization': + cpu_info['virtualization'] = value + elif key == 'L1d cache': + cpu_info['l1d_cache'] = value + elif key == 'L1i cache': + cpu_info['l1i_cache'] = value + elif key == 'L2 cache': + cpu_info['l2_cache'] = value + elif key == 'L3 cache': + cpu_info['l3_cache'] = value + + hardware_data['cpu'] = cpu_info + print(f"[v0] CPU: {cpu_info.get('model', 'Unknown')}") + except Exception as e: + print(f"[v0] Error getting CPU info: {e}") + + # Motherboard Information + try: + result = subprocess.run(['dmidecode', '-t', 'baseboard'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + mb_info = {} + for line in result.stdout.split('\n'): + line = line.strip() + if line.startswith('Manufacturer:'): + mb_info['manufacturer'] = line.split(':', 1)[1].strip() + elif line.startswith('Product Name:'): + mb_info['model'] = line.split(':', 1)[1].strip() + elif line.startswith('Version:'): + mb_info['version'] = line.split(':', 1)[1].strip() + elif line.startswith('Serial Number:'): + mb_info['serial'] = line.split(':', 1)[1].strip() + + hardware_data['motherboard'] = mb_info + print(f"[v0] Motherboard: {mb_info.get('manufacturer', 'Unknown')} {mb_info.get('model', 'Unknown')}") + except Exception as e: + print(f"[v0] Error getting motherboard info: {e}") + + # BIOS Information + try: + result = subprocess.run(['dmidecode', '-t', 'bios'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + bios_info = {} + for line in result.stdout.split('\n'): + line = line.strip() + if line.startswith('Vendor:'): + bios_info['vendor'] = line.split(':', 1)[1].strip() + elif line.startswith('Version:'): + bios_info['version'] = line.split(':', 1)[1].strip() + elif line.startswith('Release Date:'): + bios_info['date'] = line.split(':', 1)[1].strip() + + hardware_data['motherboard']['bios'] = bios_info + print(f"[v0] BIOS: {bios_info.get('vendor', 'Unknown')} {bios_info.get('version', 'Unknown')}") + except Exception as e: + print(f"[v0] Error getting BIOS info: {e}") + + # Memory Modules + try: + result = subprocess.run(['dmidecode', '-t', 'memory'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + current_module = {} + for line in result.stdout.split('\n'): + line = line.strip() + + if line.startswith('Memory Device'): + if current_module and current_module.get('size') != 'No Module Installed': + hardware_data['memory_modules'].append(current_module) + current_module = {} + elif line.startswith('Size:'): + size = line.split(':', 1)[1].strip() + current_module['size'] = size + elif line.startswith('Type:'): + current_module['type'] = line.split(':', 1)[1].strip() + elif line.startswith('Speed:'): + current_module['speed'] = line.split(':', 1)[1].strip() + elif line.startswith('Manufacturer:'): + current_module['manufacturer'] = line.split(':', 1)[1].strip() + elif line.startswith('Serial Number:'): + current_module['serial'] = line.split(':', 1)[1].strip() + elif line.startswith('Locator:'): + current_module['slot'] = line.split(':', 1)[1].strip() + + if current_module and current_module.get('size') != 'No Module Installed': + hardware_data['memory_modules'].append(current_module) + + print(f"[v0] Memory modules: {len(hardware_data['memory_modules'])} installed") + except Exception as e: + print(f"[v0] Error getting memory info: {e}") + + # Storage Devices (reuse existing function) + storage_info = get_storage_info() + hardware_data['storage_devices'] = storage_info.get('disks', []) + + # Network Cards + try: + result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if 'Ethernet controller' in line or 'Network controller' in line: + parts = line.split(':', 2) + if len(parts) >= 3: + hardware_data['network_cards'].append({ + 'name': parts[2].strip(), + 'type': 'Ethernet' if 'Ethernet' in line else 'Network' + }) + + print(f"[v0] Network cards: {len(hardware_data['network_cards'])} found") + except Exception as e: + print(f"[v0] Error getting network cards: {e}") + + # Graphics Cards + try: + # Try nvidia-smi first + result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,temperature.gpu,power.draw', '--format=csv,noheader'], + capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.strip().split('\n'): + if line: + parts = line.split(',') + if len(parts) >= 4: + hardware_data['graphics_cards'].append({ + 'name': parts[0].strip(), + 'memory': parts[1].strip(), + 'temperature': int(parts[2].strip()) if parts[2].strip().isdigit() else 0, + 'power_draw': parts[3].strip(), + 'vendor': 'NVIDIA' + }) + else: + # Fallback to lspci + result = subprocess.run(['lspci'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if 'VGA compatible controller' in line or '3D controller' in line: + parts = line.split(':', 2) + if len(parts) >= 3: + hardware_data['graphics_cards'].append({ + 'name': parts[2].strip(), + 'vendor': 'Unknown' + }) + + print(f"[v0] Graphics cards: {len(hardware_data['graphics_cards'])} found") + except Exception as e: + print(f"[v0] Error getting graphics cards: {e}") + + # Sensors (Temperature and Fans) + try: + if hasattr(psutil, "sensors_temperatures"): + temps = psutil.sensors_temperatures() + if temps: + for sensor_name, entries in temps.items(): + for entry in entries: + hardware_data['sensors']['temperatures'].append({ + 'name': f"{sensor_name} - {entry.label}" if entry.label else sensor_name, + 'current': entry.current, + 'high': entry.high if entry.high else 0, + 'critical': entry.critical if entry.critical else 0 + }) + + print(f"[v0] Temperature sensors: {len(hardware_data['sensors']['temperatures'])} found") + + if hasattr(psutil, "sensors_fans"): + fans = psutil.sensors_fans() + if fans: + for fan_name, entries in fans.items(): + for entry in entries: + hardware_data['sensors']['fans'].append({ + 'name': f"{fan_name} - {entry.label}" if entry.label else fan_name, + 'current_rpm': entry.current + }) + + print(f"[v0] Fan sensors: {len(hardware_data['sensors']['fans'])} found") + except Exception as e: + print(f"[v0] Error getting sensors: {e}") + + # Power Supply / UPS + try: + result = subprocess.run(['apcaccess'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + ups_info = {} + for line in result.stdout.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + key = key.strip() + value = value.strip() + + if key == 'MODEL': + ups_info['model'] = value + elif key == 'STATUS': + ups_info['status'] = value + elif key == 'BCHARGE': + ups_info['battery_charge'] = value + elif key == 'TIMELEFT': + ups_info['time_left'] = value + elif key == 'LOADPCT': + ups_info['load_percent'] = value + elif key == 'LINEV': + ups_info['line_voltage'] = value + + if ups_info: + hardware_data['power'] = ups_info + print(f"[v0] UPS found: {ups_info.get('model', 'Unknown')}") + except FileNotFoundError: + print("[v0] apcaccess not found - no UPS monitoring") + except Exception as e: + print(f"[v0] Error getting UPS info: {e}") + + return hardware_data + + except Exception as e: + print(f"[v0] Error getting hardware info: {e}") + return hardware_data + + @app.route('/api/system', methods=['GET']) def api_system(): """Get system information""" @@ -1430,7 +1685,8 @@ def api_info(): '/api/network', '/api/vms', '/api/logs', - '/api/health' + '/api/health', + '/api/hardware' # Added new endpoint ] }) @@ -1576,6 +1832,6 @@ def api_vm_control(vmid): if __name__ == '__main__': print("Starting ProxMenux Flask Server on port 8008...") print("Server will be accessible on all network interfaces (0.0.0.0:8008)") - print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health") + print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware") app.run(host='0.0.0.0', port=8008, debug=False)