Update AppImage

This commit is contained in:
MacRimi
2025-11-18 19:00:51 +01:00
parent 4f61386b21
commit ce5c679d6b
7 changed files with 152 additions and 89 deletions

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge" import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog" 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 useSWR from "swr"
import { NetworkTrafficChart } from "./network-traffic-chart" import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
@@ -135,7 +135,6 @@ const fetcher = async (url: string): Promise<NetworkData> => {
const getUnitsSettings = (): "Bytes" | "Bits" => { const getUnitsSettings = (): "Bytes" | "Bits" => {
const raw = localStorage.getItem("proxmenux-network-unit"); const raw = localStorage.getItem("proxmenux-network-unit");
const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"; const networkUnit = raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes";
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
return networkUnit; return networkUnit;
}; };
@@ -156,6 +155,13 @@ export function NetworkMetrics() {
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) 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 [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<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, { const { data: modalNetworkData } = useSWR<NetworkData>(selectedInterface ? "/api/network" : null, fetcher, {
refreshInterval: 17000, refreshInterval: 17000,
revalidateOnFocus: false, revalidateOnFocus: false,
@@ -167,13 +173,6 @@ export function NetworkMetrics() {
revalidateOnFocus: false, revalidateOnFocus: false,
}) })
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes");
useEffect(() => {
const networkUnitSetting = getUnitsSettings();
setNetworkUnit(networkUnitSetting);
}, []);
if (isLoading) { if (isLoading) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -906,6 +905,7 @@ export function NetworkMetrics() {
interfaceName={displayInterface.name} interfaceName={displayInterface.name}
onTotalsCalculated={setInterfaceTotals} onTotalsCalculated={setInterfaceTotals}
refreshInterval={60000} refreshInterval={60000}
networkUnit={networkUnit}
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts" 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" import { fetchApi } from "@/lib/api-config"
interface NetworkMetricsData { interface NetworkMetricsData {
@@ -17,16 +17,15 @@ interface NetworkTrafficChartProps {
interfaceName?: string interfaceName?: string
onTotalsCalculated?: (totals: { received: number; sent: number }) => void onTotalsCalculated?: (totals: { received: number; sent: number }) => void
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos) refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
networkUnit?: "Bytes" | "Bits" networkUnit?: string // Added networkUnit prop with default value
} }
export function NetworkTrafficChart({ export function NetworkTrafficChart({
timeframe, timeframe,
interfaceName, interfaceName,
onTotalsCalculated, onTotalsCalculated,
refreshInterval = 60000, refreshInterval = 60000,
networkUnit = "Bits", networkUnit = "Bytes", // Added networkUnit prop with default value
}: NetworkTrafficChartProps) { }: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([]) const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -122,6 +121,7 @@ export function NetworkTrafficChart({
const netOutBytes = (item.netout || 0) * intervalSeconds const netOutBytes = (item.netout || 0) * intervalSeconds
if (networkUnit === "Bits") { if (networkUnit === "Bits") {
// Convert to Gigabits: bytes * 8 / 1024^3
return { return {
time: timeLabel, time: timeLabel,
timestamp: item.time, timestamp: item.time,
@@ -130,6 +130,7 @@ export function NetworkTrafficChart({
} }
} }
// Default: Convert to Gigabytes
return { return {
time: timeLabel, time: timeLabel,
timestamp: item.time, timestamp: item.time,
@@ -189,6 +190,28 @@ export function NetworkTrafficChart({
) )
} }
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">
{entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"}
</span>
</div>
))}
</div>
</div>
)
}
return null
}
if (loading && isInitialLoad) { if (loading && isInitialLoad) {
return ( return (
<div className="flex items-center justify-center h-[300px]"> <div className="flex items-center justify-center h-[300px]">
@@ -214,26 +237,6 @@ export function NetworkTrafficChart({
) )
} }
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"}</span>
</div>
))}
</div>
</div>
)
}
return null
}
return ( return (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}> <AreaChart data={data} margin={{ bottom: 80 }}>

View File

@@ -5,26 +5,11 @@ import { Button } from "./ui/button"
import { Input } from "./ui/input" import { Input } from "./ui/input"
import { Label } from "./ui/label" import { Label } from "./ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler } from 'lucide-react'
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 { APP_VERSION } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config" import { getApiUrl, fetchApi } from "../lib/api-config"
import { TwoFactorSetup } from "./two-factor-setup" 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 { interface ProxMenuxTool {
key: string key: string
@@ -61,9 +46,6 @@ export function Settings() {
[APP_VERSION]: true, // Current version expanded by default [APP_VERSION]: true, // Current version expanded by default
}) })
const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes");
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true);
// API Token state management // API Token state management
const [showApiTokenSection, setShowApiTokenSection] = useState(false) const [showApiTokenSection, setShowApiTokenSection] = useState(false)
const [apiToken, setApiToken] = useState("") const [apiToken, setApiToken] = useState("")
@@ -73,10 +55,13 @@ export function Settings() {
const [generatingToken, setGeneratingToken] = useState(false) const [generatingToken, setGeneratingToken] = useState(false)
const [tokenCopied, setTokenCopied] = useState(false) const [tokenCopied, setTokenCopied] = useState(false)
const [networkUnitSettings, setNetworkUnitSettings] = useState("Bytes");
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true);
useEffect(() => { useEffect(() => {
checkAuthStatus() checkAuthStatus()
loadProxmenuxTools() loadProxmenuxTools()
getUnitsSettings(); getUnitsSettings(); // Load network unit settings on mount
}, []) }, [])
const changeNetworkUnit = (unit: string) => { const changeNetworkUnit = (unit: string) => {
@@ -87,7 +72,6 @@ export function Settings() {
const getUnitsSettings = () => { const getUnitsSettings = () => {
const networkUnit = const networkUnit =
localStorage.getItem("proxmenux-network-unit") || "Bytes"; localStorage.getItem("proxmenux-network-unit") || "Bytes";
console.log("[v0] Loaded network unit from localStorage:", networkUnit);
setNetworkUnitSettings(networkUnit); setNetworkUnitSettings(networkUnit);
setLoadingUnitSettings(false); setLoadingUnitSettings(false);
}; };
@@ -874,12 +858,12 @@ export function Settings() {
</Card> </Card>
)} )}
{/* ProxMenux unit settings*/} {/* ProxMenux Unit Settings */}
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Ruler className="h-5 w-5 text-green-500" /> <Ruler className="h-5 w-5 text-green-500" />
<CardTitle>ProxMenux unit settings</CardTitle> <CardTitle>ProxMenux Unit Settings</CardTitle>
</div> </div>
<CardDescription>Change settings related to units</CardDescription> <CardDescription>Change settings related to units</CardDescription>
</CardHeader> </CardHeader>

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress" import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge" 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 { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart" import { NetworkTrafficChart } from "./network-traffic-chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
@@ -146,13 +146,6 @@ const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> =>
} }
} }
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() { export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null) const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([]) const [vmData, setVmData] = useState<VMData[]>([])
@@ -168,7 +161,7 @@ export function SystemOverview() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [networkTimeframe, setNetworkTimeframe] = useState("day") const [networkTimeframe, setNetworkTimeframe] = useState("day")
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 }) 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(() => { useEffect(() => {
const fetchAllData = async () => { const fetchAllData = async () => {
@@ -186,9 +179,6 @@ export function SystemOverview() {
return return
} }
const networkUnitSetting = getUnitsSettings();
setNetworkUnit(networkUnitSetting);
setSystemData(systemResult) setSystemData(systemResult)
setVmData(vmResult) setVmData(vmResult)
setStorageData(storageResults[0]) setStorageData(storageResults[0])
@@ -309,19 +299,28 @@ export function SystemOverview() {
return (bytes / 1024 ** 3).toFixed(2) return (bytes / 1024 ** 3).toFixed(2)
} }
const formatStorage = (sizeInGB: number, unit: "Bytes" | "Bits" = "Bytes"): string => { const formatStorage = (sizeInGB: number): string => {
let size = sizeInGB; if (sizeInGB < 1) {
let sufix = "B"; return `${(sizeInGB * 1024).toFixed(1)} MB`
if (unit === "Bits") { } else if (sizeInGB > 999) {
size = size * 8; return `${(sizeInGB / 1024).toFixed(2)} TB`
sufix = "b";
}
if (size < 1) {
return `${(size * 1024).toFixed(1)} M${sufix}`
} else if (size > 999) {
return `${(size / 1024).toFixed(2)} T${sufix}`
} else { } 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() {
<SelectItem value="year">1 Year</SelectItem> <SelectItem value="year">1 Year</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={networkUnit} onValueChange={setNetworkUnit}>
<SelectTrigger className="w-28 h-8 text-xs ml-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Bytes">Bytes</SelectItem>
<SelectItem value="Bits">Bits</SelectItem>
</SelectContent>
</Select>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -684,21 +692,21 @@ export function SystemOverview() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Received:</span> <span className="text-sm text-muted-foreground">Received:</span>
<span className="text-lg font-semibold text-green-500 flex items-center gap-1"> <span className="text-lg font-semibold text-green-500 flex items-center gap-1">
{formatStorage(networkTotals.received, networkUnit)} {formatNetworkTraffic(networkTotals.received, networkUnit)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span> <span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Sent:</span> <span className="text-sm text-muted-foreground">Sent:</span>
<span className="text-lg font-semibold text-blue-500 flex items-center gap-1"> <span className="text-lg font-semibold text-blue-500 flex items-center gap-1">
{formatStorage(networkTotals.sent, networkUnit)} {formatNetworkTraffic(networkTotals.sent, networkUnit)}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span> <span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span> </span>
</div> </div>
</div> </div>
<div className="pt-3 border-t border-border"> <div className="pt-3 border-t border-border">
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} networkUnit={networkUnit} /> <NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
</div> </div>
</div> </div>
) : ( ) : (

View File

@@ -9,7 +9,7 @@
* Can be changed to 8009 for beta testing * Can be changed to 8009 for beta testing
* This can also be set via NEXT_PUBLIC_API_PORT environment variable * 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 * Gets the base URL for API calls

View File

@@ -6182,4 +6182,4 @@ if __name__ == '__main__':
# Print only essential information # 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") # 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)

View File

@@ -573,7 +573,7 @@ class HealthMonitor:
def _check_storage_optimized(self) -> Dict[str, Any]: def _check_storage_optimized(self) -> Dict[str, Any]:
""" """
Optimized storage check - monitors Proxmox storages from pvesm status. 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 = [] issues = []
storage_details = {} storage_details = {}
@@ -607,6 +607,13 @@ class HealthMonitor:
# If pvesm not available, skip silently # If pvesm not available, skip silently
pass 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 # Check disk health from Proxmox task log or system logs
disk_health_issues = self._check_disk_health_from_events() disk_health_issues = self._check_disk_health_from_events()
if disk_health_issues: if disk_health_issues:
@@ -1652,6 +1659,67 @@ class HealthMonitor:
return disk_issues 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 # Global instance
health_monitor = HealthMonitor() health_monitor = HealthMonitor()