From ce5c679d6b1f1b9d8465b520d0dca93dcbd59904 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 18 Nov 2025 19:00:51 +0100 Subject: [PATCH] Update AppImage --- AppImage/components/network-metrics.tsx | 18 ++--- AppImage/components/network-traffic-chart.tsx | 51 +++++++------- AppImage/components/settings.tsx | 36 +++------- AppImage/components/system-overview.tsx | 62 +++++++++------- AppImage/lib/api-config.ts | 2 +- AppImage/scripts/flask_server.py | 2 +- AppImage/scripts/health_monitor.py | 70 ++++++++++++++++++- 7 files changed, 152 insertions(+), 89 deletions(-) diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index 3248165..f2793a1 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog" -import { Wifi, Activity, Network, Router, AlertCircle, Zap } from "lucide-react" +import { Wifi, Activity, Network, Router, AlertCircle, Zap } from 'lucide-react' import useSWR from "swr" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" @@ -135,7 +135,6 @@ const fetcher = async (url: string): Promise => { const getUnitsSettings = (): "Bytes" | "Bits" => { const raw = localStorage.getItem("proxmenux-network-unit"); const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"; - console.log("[v0] Loaded network unit from localStorage:", networkUnit); return networkUnit; }; @@ -155,6 +154,13 @@ export function NetworkMetrics() { const [modalTimeframe, setModalTimeframe] = useState<"hour" | "day" | "week" | "month" | "year">("day") const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) const [interfaceTotals, setInterfaceTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) + + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes"); + + useEffect(() => { + const networkUnitSetting = getUnitsSettings(); + setNetworkUnit(networkUnitSetting); + }, []); const { data: modalNetworkData } = useSWR(selectedInterface ? "/api/network" : null, fetcher, { refreshInterval: 17000, @@ -167,13 +173,6 @@ export function NetworkMetrics() { revalidateOnFocus: false, }) - const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes"); - - useEffect(() => { - const networkUnitSetting = getUnitsSettings(); - setNetworkUnit(networkUnitSetting); - }, []); - if (isLoading) { return (
@@ -906,6 +905,7 @@ export function NetworkMetrics() { interfaceName={displayInterface.name} onTotalsCalculated={setInterfaceTotals} refreshInterval={60000} + networkUnit={networkUnit} />
diff --git a/AppImage/components/network-traffic-chart.tsx b/AppImage/components/network-traffic-chart.tsx index 809d3bf..e34ddea 100644 --- a/AppImage/components/network-traffic-chart.tsx +++ b/AppImage/components/network-traffic-chart.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react" import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" -import { Loader2 } from "lucide-react" +import { Loader2 } from 'lucide-react' import { fetchApi } from "@/lib/api-config" interface NetworkMetricsData { @@ -17,16 +17,15 @@ interface NetworkTrafficChartProps { interfaceName?: string onTotalsCalculated?: (totals: { received: number; sent: number }) => void refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos) - networkUnit?: "Bytes" | "Bits" + networkUnit?: string // Added networkUnit prop with default value } - export function NetworkTrafficChart({ timeframe, interfaceName, onTotalsCalculated, refreshInterval = 60000, - networkUnit = "Bits", + networkUnit = "Bytes", // Added networkUnit prop with default value }: NetworkTrafficChartProps) { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) @@ -122,6 +121,7 @@ export function NetworkTrafficChart({ const netOutBytes = (item.netout || 0) * intervalSeconds if (networkUnit === "Bits") { + // Convert to Gigabits: bytes * 8 / 1024^3 return { time: timeLabel, timestamp: item.time, @@ -130,6 +130,7 @@ export function NetworkTrafficChart({ } } + // Default: Convert to Gigabytes return { time: timeLabel, timestamp: item.time, @@ -189,6 +190,28 @@ export function NetworkTrafficChart({ ) } + const CustomNetworkTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+
+ {payload.map((entry: any, index: number) => ( +
+
+ {entry.name}: + + {entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"} + +
+ ))} +
+
+ ) + } + return null + } + if (loading && isInitialLoad) { return (
@@ -214,26 +237,6 @@ export function NetworkTrafficChart({ ) } - const CustomNetworkTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { - return ( -
-

{label}

-
- {payload.map((entry: any, index: number) => ( -
-
- {entry.name}: - {entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"} -
- ))} -
-
- ) - } - return null -} - return ( diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index c84aa36..194ac3f 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -5,26 +5,11 @@ import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" -import { - Shield, - Lock, - User, - AlertCircle, - CheckCircle, - Info, - LogOut, - Wrench, - Package, - Key, - Copy, - Eye, - EyeOff, - Ruler, -} from "lucide-react" +import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler } from 'lucide-react' import { APP_VERSION } from "./release-notes-modal" import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" // Added Select components interface ProxMenuxTool { key: string @@ -61,9 +46,6 @@ export function Settings() { [APP_VERSION]: true, // Current version expanded by default }) - const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes"); - const [loadingUnitSettings, setLoadingUnitSettings] = useState(true); - // API Token state management const [showApiTokenSection, setShowApiTokenSection] = useState(false) const [apiToken, setApiToken] = useState("") @@ -73,10 +55,13 @@ export function Settings() { const [generatingToken, setGeneratingToken] = useState(false) const [tokenCopied, setTokenCopied] = useState(false) + const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes"); + const [loadingUnitSettings, setLoadingUnitSettings] = useState(true); + useEffect(() => { checkAuthStatus() loadProxmenuxTools() - getUnitsSettings(); + getUnitsSettings(); // Load network unit settings on mount }, []) const changeNetworkUnit = (unit: string) => { @@ -87,7 +72,6 @@ export function Settings() { const getUnitsSettings = () => { const networkUnit = localStorage.getItem("proxmenux-network-unit") || "Bytes"; - console.log("[v0] Loaded network unit from localStorage:", networkUnit); setNetworkUnitSettings(networkUnit); setLoadingUnitSettings(false); }; @@ -873,13 +857,13 @@ export function Settings() { )} - - {/* ProxMenux unit settings*/} + + {/* ProxMenux Unit Settings */}
- ProxMenux unit settings + ProxMenux Unit Settings
Change settings related to units
@@ -962,4 +946,4 @@ export function Settings() { />
) -} \ No newline at end of file +} diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx index 47c6c40..271aa4b 100644 --- a/AppImage/components/system-overview.tsx +++ b/AppImage/components/system-overview.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Progress } from "./ui/progress" import { Badge } from "./ui/badge" -import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react" +import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from 'lucide-react' import { NodeMetricsCharts } from "./node-metrics-charts" import { NetworkTrafficChart } from "./network-traffic-chart" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" @@ -146,13 +146,6 @@ const fetchProxmoxStorageData = async (): Promise => } } -const getUnitsSettings = (): "Bytes" | "Bits" => { - const raw = localStorage.getItem("proxmenux-network-unit"); - const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"; - console.log("[v0] Loaded network unit from localStorage:", networkUnit); - return networkUnit; -}; - export function SystemOverview() { const [systemData, setSystemData] = useState(null) const [vmData, setVmData] = useState([]) @@ -168,7 +161,7 @@ export function SystemOverview() { const [error, setError] = useState(null) const [networkTimeframe, setNetworkTimeframe] = useState("day") const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) - const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes"); + const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") useEffect(() => { const fetchAllData = async () => { @@ -186,9 +179,6 @@ export function SystemOverview() { return } - const networkUnitSetting = getUnitsSettings(); - setNetworkUnit(networkUnitSetting); - setSystemData(systemResult) setVmData(vmResult) setStorageData(storageResults[0]) @@ -309,19 +299,28 @@ export function SystemOverview() { return (bytes / 1024 ** 3).toFixed(2) } - const formatStorage = (sizeInGB: number, unit: "Bytes" | "Bits" = "Bytes"): string => { - let size = sizeInGB; - let sufix = "B"; - if (unit === "Bits") { - size = size * 8; - sufix = "b"; - } - if (size < 1) { - return `${(size * 1024).toFixed(1)} M${sufix}` - } else if (size > 999) { - return `${(size / 1024).toFixed(2)} T${sufix}` + const formatStorage = (sizeInGB: number): string => { + if (sizeInGB < 1) { + return `${(sizeInGB * 1024).toFixed(1)} MB` + } else if (sizeInGB > 999) { + return `${(sizeInGB / 1024).toFixed(2)} TB` } else { - return `${size.toFixed(2)} G${sufix}` + return `${sizeInGB.toFixed(2)} GB` + } + } + + const formatNetworkTraffic = (sizeInGB: number, unit: "Bytes" | "Bits" = "Bytes"): string => { + if (unit === "Bits") { + const sizeInGb = sizeInGB * 8 + if (sizeInGb < 1) { + return `${(sizeInGb * 1024).toFixed(1)} Mb` + } else if (sizeInGb > 999) { + return `${(sizeInGb / 1024).toFixed(2)} Tb` + } else { + return `${sizeInGb.toFixed(2)} Gb` + } + } else { + return formatStorage(sizeInGB) } } @@ -628,6 +627,15 @@ export function SystemOverview() { 1 Year + @@ -684,21 +692,21 @@ export function SystemOverview() {
Received: - ↓ {formatStorage(networkTotals.received, networkUnit)} + ↓ {formatNetworkTraffic(networkTotals.received, networkUnit)} ({getTimeframeLabel(networkTimeframe)})
Sent: - ↑ {formatStorage(networkTotals.sent, networkUnit)} + ↑ {formatNetworkTraffic(networkTotals.sent, networkUnit)} ({getTimeframeLabel(networkTimeframe)})
- +
) : ( diff --git a/AppImage/lib/api-config.ts b/AppImage/lib/api-config.ts index 3e8b352..34175c9 100644 --- a/AppImage/lib/api-config.ts +++ b/AppImage/lib/api-config.ts @@ -9,7 +9,7 @@ * Can be changed to 8009 for beta testing * This can also be set via NEXT_PUBLIC_API_PORT environment variable */ -export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8009" +export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008" /** * Gets the base URL for API calls diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 0ec3587..9edd8a8 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -6182,4 +6182,4 @@ if __name__ == '__main__': # Print only essential information # print("API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics") - app.run(host='0.0.0.0', port=8009, debug=False) + app.run(host='0.0.0.0', port=8008, debug=False) diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py index df32e82..075ea66 100644 --- a/AppImage/scripts/health_monitor.py +++ b/AppImage/scripts/health_monitor.py @@ -573,7 +573,7 @@ class HealthMonitor: def _check_storage_optimized(self) -> Dict[str, Any]: """ Optimized storage check - monitors Proxmox storages from pvesm status. - Checks for inactive storages and disk health from SMART/events. + Checks for inactive storages, disk health from SMART/events, and ZFS pool health. """ issues = [] storage_details = {} @@ -607,6 +607,13 @@ class HealthMonitor: # If pvesm not available, skip silently pass + # Check ZFS pool health status + zfs_pool_issues = self._check_zfs_pool_health() + if zfs_pool_issues: + for pool_name, pool_info in zfs_pool_issues.items(): + issues.append(f'{pool_name}: {pool_info["reason"]}') + storage_details[pool_name] = pool_info + # Check disk health from Proxmox task log or system logs disk_health_issues = self._check_disk_health_from_events() if disk_health_issues: @@ -1651,6 +1658,67 @@ class HealthMonitor: pass return disk_issues + + def _check_zfs_pool_health(self) -> Dict[str, Any]: + """ + Check ZFS pool health status using zpool status command. + Returns dict of pools with non-ONLINE status (DEGRADED, FAULTED, UNAVAIL, etc.) + """ + zfs_issues = {} + + try: + # First check if zpool command exists + result = subprocess.run( + ['which', 'zpool'], + capture_output=True, + text=True, + timeout=1 + ) + + if result.returncode != 0: + # ZFS not installed, return empty + return zfs_issues + + # Get list of all pools + result = subprocess.run( + ['zpool', 'list', '-H', '-o', 'name,health'], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + for line in lines: + if not line.strip(): + continue + + parts = line.split() + if len(parts) >= 2: + pool_name = parts[0] + pool_health = parts[1].upper() + + # ONLINE is healthy, anything else is a problem + if pool_health != 'ONLINE': + if pool_health in ['DEGRADED', 'FAULTED', 'UNAVAIL', 'REMOVED']: + status = 'CRITICAL' + reason = f'ZFS pool {pool_health.lower()}' + else: + # Any other non-ONLINE state is at least a warning + status = 'WARNING' + reason = f'ZFS pool status: {pool_health.lower()}' + + zfs_issues[f'zpool_{pool_name}'] = { + 'status': status, + 'reason': reason, + 'pool_name': pool_name, + 'health': pool_health + } + except Exception: + # If zpool command fails, silently ignore + pass + + return zfs_issues # Global instance