mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-18 03:26:17 +00:00
Updete AppImage
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
|
import { type HardwareData, type GPU, type PCIDevice, type StorageDevice, fetcher } from "../types/hardware"
|
||||||
|
import { usePollingConfig } from "@/lib/polling-config"
|
||||||
|
|
||||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||||
if (!sizeStr) return 0
|
if (!sizeStr) return 0
|
||||||
@@ -163,14 +164,38 @@ const groupAndSortTemperatures = (temperatures: any[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Hardware() {
|
export default function Hardware() {
|
||||||
|
const { intervals } = usePollingConfig()
|
||||||
|
|
||||||
|
// Static data (loaded once on mount): system info, memory, PCI, network/storage summaries
|
||||||
const {
|
const {
|
||||||
data: hardwareData,
|
data: staticHardwareData,
|
||||||
error,
|
error: staticError,
|
||||||
isLoading,
|
isLoading: staticLoading,
|
||||||
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
||||||
refreshInterval: 5000,
|
refreshInterval: 0, // Never refresh automatically - only load once
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Dynamic data (temperatures only) - polls at configured interval
|
||||||
|
const { data: temperatureData, error: tempError } = useSWR<{ temperatures: any[] }>(
|
||||||
|
"/api/hardware/temperatures",
|
||||||
|
fetcher,
|
||||||
|
{
|
||||||
|
refreshInterval: intervals.hardware,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const hardwareData = staticHardwareData
|
||||||
|
? {
|
||||||
|
...staticHardwareData,
|
||||||
|
temperatures: temperatureData?.temperatures || staticHardwareData.temperatures || [],
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
|
||||||
|
const error = staticError || tempError
|
||||||
|
const isLoading = staticLoading
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hardwareData?.storage_devices) {
|
if (hardwareData?.storage_devices) {
|
||||||
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
|
console.log("[v0] Storage devices data from backend:", hardwareData.storage_devices)
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ interface NetworkInterface {
|
|||||||
vm_status?: string
|
vm_status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NetworkMetricsProps {
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const getInterfaceTypeBadge = (type: string) => {
|
const getInterfaceTypeBadge = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "physical":
|
case "physical":
|
||||||
@@ -143,15 +147,16 @@ const fetcher = async (url: string): Promise<NetworkData> => {
|
|||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkMetrics() {
|
export function NetworkMetrics({ isActive = true }: NetworkMetricsProps) {
|
||||||
const {
|
const {
|
||||||
data: networkData,
|
data: networkData,
|
||||||
error,
|
error,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useSWR<NetworkData>("/api/network", fetcher, {
|
} = useSWR<NetworkData>("/api/network", fetcher, {
|
||||||
refreshInterval: 60000, // Refresh every 60 seconds
|
refreshInterval: isActive ? 60000 : 0, // Refresh every 60 seconds only if active, otherwise pause
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
|
isPaused: () => !isActive,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [selectedInterface, setSelectedInterface] = useState<NetworkInterface | null>(null)
|
const [selectedInterface, setSelectedInterface] = useState<NetworkInterface | null>(null)
|
||||||
@@ -166,10 +171,15 @@ export function NetworkMetrics() {
|
|||||||
revalidateOnReconnect: true,
|
revalidateOnReconnect: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: interfaceHistoricalData } = useSWR<any>(`/api/node/metrics?timeframe=${timeframe}`, fetcher, {
|
const { data: interfaceHistoricalData } = useSWR<any>(
|
||||||
refreshInterval: 30000,
|
isActive ? `/api/node/metrics?timeframe=${timeframe}` : null,
|
||||||
|
fetcher,
|
||||||
|
{
|
||||||
|
refreshInterval: isActive ? 30000 : 0,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
})
|
isPaused: () => !isActive,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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)
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
|
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
|
||||||
@@ -43,6 +44,7 @@ export function NetworkTrafficChart({
|
|||||||
interfaceName,
|
interfaceName,
|
||||||
onTotalsCalculated,
|
onTotalsCalculated,
|
||||||
refreshInterval = 60000,
|
refreshInterval = 60000,
|
||||||
|
isActive = true,
|
||||||
}: NetworkTrafficChartProps) {
|
}: NetworkTrafficChartProps) {
|
||||||
const [data, setData] = useState<NetworkMetricsData[]>([])
|
const [data, setData] = useState<NetworkMetricsData[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -59,16 +61,20 @@ export function NetworkTrafficChart({
|
|||||||
}, [timeframe, interfaceName])
|
}, [timeframe, interfaceName])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshInterval > 0) {
|
if (refreshInterval > 0 && isActive) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchMetrics()
|
fetchMetrics()
|
||||||
}, refreshInterval)
|
}, refreshInterval)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [timeframe, interfaceName, refreshInterval])
|
}, [timeframe, interfaceName, refreshInterval, isActive])
|
||||||
|
|
||||||
const fetchMetrics = async () => {
|
const fetchMetrics = async () => {
|
||||||
|
if (!isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ export function NodeMetricsCharts() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[v0] NodeMetricsCharts component mounted")
|
console.log("[v0] NodeMetricsCharts component mounted")
|
||||||
fetchMetrics()
|
fetchMetrics()
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchMetrics()
|
||||||
|
}, 60000) // 60 seconds instead of 30 to reduce server load
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
}, [timeframe])
|
}, [timeframe])
|
||||||
|
|
||||||
const fetchMetrics = async () => {
|
const fetchMetrics = async () => {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { SystemLogs } from "./system-logs"
|
|||||||
import { OnboardingCarousel } from "./onboarding-carousel"
|
import { OnboardingCarousel } from "./onboarding-carousel"
|
||||||
import { HealthStatusModal } from "./health-status-modal"
|
import { HealthStatusModal } from "./health-status-modal"
|
||||||
import { getApiUrl } from "../lib/api-config"
|
import { getApiUrl } from "../lib/api-config"
|
||||||
|
import { usePollingConfig, INTERVAL_OPTIONS } from "@/lib/polling-config"
|
||||||
|
import { Label } from "./ui/label"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -26,6 +29,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Cpu,
|
Cpu,
|
||||||
FileText,
|
FileText,
|
||||||
|
Settings,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { ThemeToggle } from "./theme-toggle"
|
import { ThemeToggle } from "./theme-toggle"
|
||||||
@@ -65,6 +69,7 @@ export function ProxmoxDashboard() {
|
|||||||
const [showNavigation, setShowNavigation] = useState(true)
|
const [showNavigation, setShowNavigation] = useState(true)
|
||||||
const [lastScrollY, setLastScrollY] = useState(0)
|
const [lastScrollY, setLastScrollY] = useState(0)
|
||||||
const [showHealthModal, setShowHealthModal] = useState(false)
|
const [showHealthModal, setShowHealthModal] = useState(false)
|
||||||
|
const { intervals, updateInterval } = usePollingConfig()
|
||||||
|
|
||||||
const fetchSystemData = useCallback(async () => {
|
const fetchSystemData = useCallback(async () => {
|
||||||
console.log("[v0] Fetching system data from Flask server...")
|
console.log("[v0] Fetching system data from Flask server...")
|
||||||
@@ -127,8 +132,12 @@ export function ProxmoxDashboard() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only fetch if we actually need the data (always for header)
|
||||||
fetchSystemData()
|
fetchSystemData()
|
||||||
const interval = setInterval(fetchSystemData, 10000)
|
|
||||||
|
// Poll every 30 seconds for header info (less frequent since it's just for display)
|
||||||
|
const interval = setInterval(fetchSystemData, 30000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [fetchSystemData])
|
}, [fetchSystemData])
|
||||||
|
|
||||||
@@ -216,6 +225,8 @@ export function ProxmoxDashboard() {
|
|||||||
return "Hardware"
|
return "Hardware"
|
||||||
case "logs":
|
case "logs":
|
||||||
return "System Logs"
|
return "System Logs"
|
||||||
|
case "settings":
|
||||||
|
return "Settings"
|
||||||
default:
|
default:
|
||||||
return "Navigation Menu"
|
return "Navigation Menu"
|
||||||
}
|
}
|
||||||
@@ -362,7 +373,7 @@ export function ProxmoxDashboard() {
|
|||||||
>
|
>
|
||||||
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
<div className="container mx-auto px-4 md:px-6 pt-4 md:pt-6">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
|
||||||
<TabsList className="hidden md:grid w-full grid-cols-6 bg-card border border-border">
|
<TabsList className="hidden md:grid w-full grid-cols-7 bg-card border border-border">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="overview"
|
value="overview"
|
||||||
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||||
@@ -399,6 +410,12 @@ export function ProxmoxDashboard() {
|
|||||||
>
|
>
|
||||||
System Logs
|
System Logs
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="settings"
|
||||||
|
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
|
||||||
@@ -507,6 +524,21 @@ export function ProxmoxDashboard() {
|
|||||||
<FileText className="h-5 w-5" />
|
<FileText className="h-5 w-5" />
|
||||||
<span>System Logs</span>
|
<span>System Logs</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("settings")
|
||||||
|
setMobileMenuOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full justify-start gap-3 ${
|
||||||
|
activeTab === "settings"
|
||||||
|
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -517,7 +549,7 @@ export function ProxmoxDashboard() {
|
|||||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
|
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
|
||||||
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
|
||||||
<SystemOverview key={`overview-${componentKey}`} />
|
<SystemOverview key={`overview-${componentKey}`} isActive={activeTab === "overview"} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
|
||||||
@@ -525,7 +557,7 @@ export function ProxmoxDashboard() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
|
||||||
<NetworkMetrics key={`network-${componentKey}`} />
|
<NetworkMetrics key={`network-${componentKey}`} isActive={activeTab === "network"} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
|
||||||
@@ -539,6 +571,183 @@ export function ProxmoxDashboard() {
|
|||||||
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
|
||||||
<SystemLogs key={`logs-${componentKey}`} />
|
<SystemLogs key={`logs-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
|
||||||
|
<div className="grid gap-4 md:gap-6">
|
||||||
|
<div className="rounded-lg border border-border bg-card p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="h-6 w-6" />
|
||||||
|
Settings
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
Configure your ProxMenux Monitor preferences and system settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="border-t border-border pt-4">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Appearance</h3>
|
||||||
|
<div className="flex items-center justify-between py-3">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Theme</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Choose your preferred color scheme</p>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border pt-4">
|
||||||
|
<h3 className="text-lg font-medium mb-2">System Information</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<span className="text-muted-foreground">Version:</span>
|
||||||
|
<span className="font-medium">ProxMenux Monitor v1.0.0</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<span className="text-muted-foreground">Server:</span>
|
||||||
|
<span className="font-medium">{systemStatus.serverName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<span className="text-muted-foreground">Node ID:</span>
|
||||||
|
<span className="font-medium">{systemStatus.nodeId}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<span className="text-muted-foreground">Status:</span>
|
||||||
|
<Badge variant="outline" className={statusColor}>
|
||||||
|
{statusIcon}
|
||||||
|
<span className="ml-1 capitalize">{systemStatus.status}</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border pt-4">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Polling Intervals</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Configure how frequently each section updates its data. Lower values provide more real-time data
|
||||||
|
but increase server load.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="storage-interval" className="font-medium">
|
||||||
|
Storage
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">Update frequency for storage metrics</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={intervals.storage.toString()}
|
||||||
|
onValueChange={(value) => updateInterval("storage", Number.parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="storage-interval" className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{INTERVAL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value.toString()}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="network-interval" className="font-medium">
|
||||||
|
Network
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">Update frequency for network metrics</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={intervals.network.toString()}
|
||||||
|
onValueChange={(value) => updateInterval("network", Number.parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="network-interval" className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{INTERVAL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value.toString()}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="vms-interval" className="font-medium">
|
||||||
|
VMs & LXCs
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">Update frequency for VM/LXC data</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={intervals.vms.toString()}
|
||||||
|
onValueChange={(value) => updateInterval("vms", Number.parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="vms-interval" className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{INTERVAL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value.toString()}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between py-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="hardware-interval" className="font-medium">
|
||||||
|
Hardware
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">Update frequency for temperature sensors only</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={intervals.hardware.toString()}
|
||||||
|
onValueChange={(value) => updateInterval("hardware", Number.parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="hardware-interval" className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{INTERVAL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value.toString()}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-xs text-muted-foreground bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
|
||||||
|
<strong>Note:</strong> Hardware static information (System Info, Memory Modules, PCI Devices,
|
||||||
|
Network/Storage Summaries) is loaded only once when entering the Hardware page and does not
|
||||||
|
refresh automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border pt-4">
|
||||||
|
<h3 className="text-lg font-medium mb-2">About</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
ProxMenux Monitor is a comprehensive dashboard for monitoring and managing Proxmox VE systems.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://ko-fi.com/macrimi"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center text-sm text-blue-500 hover:text-blue-600 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
Support and contribute to the project →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useMemo } from "react"
|
||||||
|
|
||||||
interface Log {
|
interface Log {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
@@ -433,11 +433,22 @@ export function SystemLogs() {
|
|||||||
return String(value).toLowerCase()
|
return String(value).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
const logsOnly: CombinedLogEntry[] = logs
|
const memoizedLogs = useMemo(() => logs, [logs])
|
||||||
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
const memoizedEvents = useMemo(() => events, [events])
|
||||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
const memoizedBackups = useMemo(() => backups, [backups])
|
||||||
|
const memoizedNotifications = useMemo(() => notifications, [notifications])
|
||||||
|
|
||||||
const eventsOnly: CombinedLogEntry[] = events
|
const logsOnly: CombinedLogEntry[] = useMemo(
|
||||||
|
() =>
|
||||||
|
memoizedLogs
|
||||||
|
.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() }))
|
||||||
|
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
|
[memoizedLogs],
|
||||||
|
)
|
||||||
|
|
||||||
|
const eventsOnly: CombinedLogEntry[] = useMemo(
|
||||||
|
() =>
|
||||||
|
memoizedEvents
|
||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
timestamp: event.starttime,
|
timestamp: event.starttime,
|
||||||
level: event.level,
|
level: event.level,
|
||||||
@@ -448,7 +459,9 @@ export function SystemLogs() {
|
|||||||
eventData: event,
|
eventData: event,
|
||||||
sortTimestamp: new Date(event.starttime).getTime(),
|
sortTimestamp: new Date(event.starttime).getTime(),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.sortTimestamp - a.sortTimestamp)
|
.sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
|
[memoizedEvents],
|
||||||
|
)
|
||||||
|
|
||||||
const filteredLogsOnly = logsOnly.filter((log) => {
|
const filteredLogsOnly = logsOnly.filter((log) => {
|
||||||
const message = log.message || ""
|
const message = log.message || ""
|
||||||
@@ -479,9 +492,11 @@ export function SystemLogs() {
|
|||||||
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
const displayedLogsOnly = filteredLogsOnly.slice(0, displayedLogsCount)
|
||||||
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
const displayedEventsOnly = filteredEventsOnly.slice(0, displayedLogsCount)
|
||||||
|
|
||||||
const combinedLogs: CombinedLogEntry[] = [
|
const combinedLogs: CombinedLogEntry[] = useMemo(
|
||||||
...logs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
() =>
|
||||||
...events.map((event) => ({
|
[
|
||||||
|
...memoizedLogs.map((log) => ({ ...log, isEvent: false, sortTimestamp: new Date(log.timestamp).getTime() })),
|
||||||
|
...memoizedEvents.map((event) => ({
|
||||||
timestamp: event.starttime,
|
timestamp: event.starttime,
|
||||||
level: event.level,
|
level: event.level,
|
||||||
service: event.type,
|
service: event.type,
|
||||||
@@ -491,9 +506,13 @@ export function SystemLogs() {
|
|||||||
eventData: event,
|
eventData: event,
|
||||||
sortTimestamp: new Date(event.starttime).getTime(),
|
sortTimestamp: new Date(event.starttime).getTime(),
|
||||||
})),
|
})),
|
||||||
].sort((a, b) => b.sortTimestamp - a.sortTimestamp) // Sort by timestamp descending
|
].sort((a, b) => b.sortTimestamp - a.sortTimestamp),
|
||||||
|
[memoizedLogs, memoizedEvents],
|
||||||
|
)
|
||||||
|
|
||||||
const filteredCombinedLogs = combinedLogs.filter((log) => {
|
const filteredCombinedLogs = useMemo(
|
||||||
|
() =>
|
||||||
|
combinedLogs.filter((log) => {
|
||||||
const message = log.message || ""
|
const message = log.message || ""
|
||||||
const service = log.service || ""
|
const service = log.service || ""
|
||||||
const searchTermLower = safeToLowerCase(searchTerm)
|
const searchTermLower = safeToLowerCase(searchTerm)
|
||||||
@@ -504,7 +523,9 @@ export function SystemLogs() {
|
|||||||
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
const matchesService = serviceFilter === "all" || log.service === serviceFilter
|
||||||
|
|
||||||
return matchesSearch && matchesLevel && matchesService
|
return matchesSearch && matchesLevel && matchesService
|
||||||
})
|
}),
|
||||||
|
[combinedLogs, searchTerm, levelFilter, serviceFilter],
|
||||||
|
)
|
||||||
|
|
||||||
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
// CHANGE: Re-assigning displayedLogs to use the filteredCombinedLogs
|
||||||
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
const displayedLogs = filteredCombinedLogs.slice(0, displayedLogsCount)
|
||||||
@@ -605,7 +626,7 @@ export function SystemLogs() {
|
|||||||
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
info: logs.filter((log) => ["info", "notice", "debug"].includes(log.level)).length,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueServices = [...new Set(logs.map((log) => log.service))]
|
const uniqueServices = useMemo(() => [...new Set(memoizedLogs.map((log) => log.service))], [memoizedLogs])
|
||||||
|
|
||||||
const getBackupType = (volid: string): "vm" | "lxc" => {
|
const getBackupType = (volid: string): "vm" | "lxc" => {
|
||||||
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
if (volid.includes("/vm/") || volid.includes("vzdump-qemu")) {
|
||||||
@@ -930,9 +951,11 @@ export function SystemLogs() {
|
|||||||
<SelectValue placeholder="Filter by service" />
|
<SelectValue placeholder="Filter by service" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Services</SelectItem>
|
<SelectItem key="service-all" value="all">
|
||||||
{uniqueServices.slice(0, 20).map((service) => (
|
All Services
|
||||||
<SelectItem key={service} value={service}>
|
</SelectItem>
|
||||||
|
{uniqueServices.slice(0, 20).map((service, idx) => (
|
||||||
|
<SelectItem key={`service-${service}-${idx}`} value={service}>
|
||||||
{service}
|
{service}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -947,9 +970,16 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
|
<ScrollArea className="h-[600px] w-full rounded-md border border-border overflow-x-hidden">
|
||||||
<div className="space-y-2 p-4 w-full box-border">
|
<div className="space-y-2 p-4 w-full box-border">
|
||||||
{displayedLogs.map((log, index) => (
|
{displayedLogs.map((log, index) => {
|
||||||
|
// Generate a more stable unique key
|
||||||
|
const timestampMs = new Date(log.timestamp).getTime()
|
||||||
|
const uniqueKey = log.eventData
|
||||||
|
? `event-${log.eventData.upid.replace(/:/g, "-")}-${timestampMs}`
|
||||||
|
: `log-${timestampMs}-${log.service?.substring(0, 10) || "unknown"}-${log.pid || "nopid"}-${index}`
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={uniqueKey}
|
||||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden box-border"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (log.eventData) {
|
if (log.eventData) {
|
||||||
@@ -991,7 +1021,8 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{displayedLogs.length === 0 && (
|
{displayedLogs.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
@@ -1052,9 +1083,12 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[500px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{backups.map((backup, index) => (
|
{memoizedBackups.map((backup, index) => {
|
||||||
|
const uniqueKey = `backup-${backup.volid.replace(/[/:]/g, "-")}-${backup.timestamp || index}`
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={uniqueKey}
|
||||||
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
className="flex items-start space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedBackup(backup)
|
setSelectedBackup(backup)
|
||||||
@@ -1089,7 +1123,8 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{backups.length === 0 && (
|
{backups.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
@@ -1105,9 +1140,13 @@ export function SystemLogs() {
|
|||||||
<TabsContent value="notifications" className="space-y-4">
|
<TabsContent value="notifications" className="space-y-4">
|
||||||
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
{notifications.map((notification, index) => (
|
{memoizedNotifications.map((notification, index) => {
|
||||||
|
const timestampMs = new Date(notification.timestamp).getTime()
|
||||||
|
const uniqueKey = `notification-${timestampMs}-${notification.service?.substring(0, 10) || "unknown"}-${notification.source?.substring(0, 10) || "unknown"}-${index}`
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={uniqueKey}
|
||||||
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
className="flex flex-col md:flex-row md:items-start space-y-2 md:space-y-0 md:space-x-4 p-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 transition-colors cursor-pointer overflow-hidden w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedNotification(notification)
|
setSelectedNotification(notification)
|
||||||
@@ -1140,7 +1179,8 @@ export function SystemLogs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{notifications.length === 0 && (
|
{notifications.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ interface ProxmoxStorageData {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SystemOverviewProps {
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const fetchSystemData = async (): Promise<SystemData | null> => {
|
const fetchSystemData = async (): Promise<SystemData | null> => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = getApiUrl("/api/system")
|
const apiUrl = getApiUrl("/api/system")
|
||||||
@@ -219,7 +223,7 @@ const fetchProxmoxStorageData = async (): Promise<ProxmoxStorageData | null> =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SystemOverview() {
|
export function SystemOverview({ isActive = true }: SystemOverviewProps) {
|
||||||
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
const [systemData, setSystemData] = useState<SystemData | null>(null)
|
||||||
const [vmData, setVmData] = useState<VMData[]>([])
|
const [vmData, setVmData] = useState<VMData[]>([])
|
||||||
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||||
@@ -231,6 +235,10 @@ export function SystemOverview() {
|
|||||||
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -253,6 +261,7 @@ export function SystemOverview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
||||||
const systemInterval = setInterval(() => {
|
const systemInterval = setInterval(() => {
|
||||||
@@ -261,10 +270,11 @@ export function SystemOverview() {
|
|||||||
})
|
})
|
||||||
}, 10000)
|
}, 10000)
|
||||||
|
|
||||||
|
// Clean up interval when component unmounts or becomes inactive
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(systemInterval)
|
clearInterval(systemInterval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [isActive]) // Add isActive as dependency
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVMs = async () => {
|
const fetchVMs = async () => {
|
||||||
@@ -721,7 +731,11 @@ export function SystemOverview() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-3 border-t border-border">
|
<div className="pt-3 border-t border-border">
|
||||||
<NetworkTrafficChart timeframe={networkTimeframe} onTotalsCalculated={setNetworkTotals} />
|
<NetworkTrafficChart
|
||||||
|
timeframe={networkTimeframe}
|
||||||
|
onTotalsCalculated={setNetworkTotals}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ export function VirtualMachines() {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ action }),
|
body: JSON.JSON.stringify({ action }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -451,7 +451,7 @@ export function VirtualMachines() {
|
|||||||
"/api/system",
|
"/api/system",
|
||||||
fetcher,
|
fetcher,
|
||||||
{
|
{
|
||||||
refreshInterval: 30000,
|
refreshInterval: 60000, // Changed from 30s to 60s
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1275,7 +1275,7 @@ export function VirtualMachines() {
|
|||||||
{/* Real IPs (green, without "Real" label) */}
|
{/* Real IPs (green, without "Real" label) */}
|
||||||
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
|
{vmDetails.lxc_ip_info.real_ips.map((ip, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={`real-${index}`}
|
key={`ip-real-${selectedVM.vmid}-${ip}-${index}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-green-500/10 text-green-500 border-green-500/20"
|
className="bg-green-500/10 text-green-500 border-green-500/20"
|
||||||
>
|
>
|
||||||
@@ -1285,7 +1285,7 @@ export function VirtualMachines() {
|
|||||||
{/* Docker bridge IPs (yellow, with "Bridge" label) */}
|
{/* Docker bridge IPs (yellow, with "Bridge" label) */}
|
||||||
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
|
{vmDetails.lxc_ip_info.docker_ips.map((ip, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={`docker-${index}`}
|
key={`ip-docker-${selectedVM.vmid}-${ip}-${index}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
|
||||||
>
|
>
|
||||||
@@ -1403,7 +1403,7 @@ export function VirtualMachines() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
|
{vmDetails.hardware_info.gpu_passthrough.map((gpu, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={index}
|
key={`gpu-${selectedVM.vmid}-${index}-${gpu.substring(0, 20)}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={
|
className={
|
||||||
gpu.includes("NVIDIA")
|
gpu.includes("NVIDIA")
|
||||||
@@ -1426,7 +1426,7 @@ export function VirtualMachines() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{vmDetails.hardware_info.devices.map((device, index) => (
|
{vmDetails.hardware_info.devices.map((device, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={index}
|
key={`device-${selectedVM.vmid}-${index}-${device.substring(0, 20)}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
|
||||||
>
|
>
|
||||||
|
|||||||
74
AppImage/lib/polling-config.tsx
Normal file
74
AppImage/lib/polling-config.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
||||||
|
|
||||||
|
export interface PollingIntervals {
|
||||||
|
storage: number
|
||||||
|
network: number
|
||||||
|
vms: number
|
||||||
|
hardware: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default intervals in milliseconds
|
||||||
|
const DEFAULT_INTERVALS: PollingIntervals = {
|
||||||
|
storage: 60000, // 60 seconds
|
||||||
|
network: 60000, // 60 seconds
|
||||||
|
vms: 30000, // 30 seconds
|
||||||
|
hardware: 60000, // 60 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "proxmenux_polling_intervals"
|
||||||
|
|
||||||
|
interface PollingConfigContextType {
|
||||||
|
intervals: PollingIntervals
|
||||||
|
updateInterval: (key: keyof PollingIntervals, value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PollingConfigContext = createContext<PollingConfigContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export function PollingConfigProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [intervals, setIntervals] = useState<PollingIntervals>(DEFAULT_INTERVALS)
|
||||||
|
|
||||||
|
// Load from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored)
|
||||||
|
setIntervals({ ...DEFAULT_INTERVALS, ...parsed })
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[v0] Failed to parse stored polling intervals:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateInterval = (key: keyof PollingIntervals, value: number) => {
|
||||||
|
setIntervals((prev) => {
|
||||||
|
const newIntervals = { ...prev, [key]: value }
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newIntervals))
|
||||||
|
return newIntervals
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PollingConfigContext.Provider value={{ intervals, updateInterval }}>{children}</PollingConfigContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePollingConfig() {
|
||||||
|
const context = useContext(PollingConfigContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("usePollingConfig must be used within PollingConfigProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interval options for the UI (in milliseconds)
|
||||||
|
export const INTERVAL_OPTIONS = [
|
||||||
|
{ label: "10 seconds", value: 10000 },
|
||||||
|
{ label: "30 seconds", value: 30000 },
|
||||||
|
{ label: "1 minute", value: 60000 },
|
||||||
|
{ label: "2 minutes", value: 120000 },
|
||||||
|
{ label: "5 minutes", value: 300000 },
|
||||||
|
{ label: "10 minutes", value: 600000 },
|
||||||
|
{ label: "30 minutes", value: 1800000 },
|
||||||
|
{ label: "1 hour", value: 3600000 },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user