mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-19 03:56:18 +00:00
Update AppImage
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user