mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-18 11:36:17 +00:00
Update AppImage
This commit is contained in:
@@ -20,8 +20,14 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
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 {
|
||||||
import { API_PORT } from "@/lib/api-config"
|
type HardwareData,
|
||||||
|
type GPU,
|
||||||
|
type PCIDevice,
|
||||||
|
type StorageDevice,
|
||||||
|
fetcher as swrFetcher,
|
||||||
|
} from "../types/hardware"
|
||||||
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
const parseLsblkSize = (sizeStr: string | undefined): number => {
|
||||||
if (!sizeStr) return 0
|
if (!sizeStr) return 0
|
||||||
@@ -169,7 +175,7 @@ export default function Hardware() {
|
|||||||
data: staticHardwareData,
|
data: staticHardwareData,
|
||||||
error: staticError,
|
error: staticError,
|
||||||
isLoading: staticLoading,
|
isLoading: staticLoading,
|
||||||
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
refreshInterval: 0, // No auto-refresh for static data
|
refreshInterval: 0, // No auto-refresh for static data
|
||||||
@@ -180,7 +186,7 @@ export default function Hardware() {
|
|||||||
data: dynamicHardwareData,
|
data: dynamicHardwareData,
|
||||||
error: dynamicError,
|
error: dynamicError,
|
||||||
isLoading: dynamicLoading,
|
isLoading: dynamicLoading,
|
||||||
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
} = useSWR<HardwareData>("/api/hardware", swrFetcher, {
|
||||||
refreshInterval: 7000,
|
refreshInterval: 7000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -231,6 +237,21 @@ export default function Hardware() {
|
|||||||
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
const [selectedNetwork, setSelectedNetwork] = useState<PCIDevice | null>(null)
|
||||||
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
const [selectedUPS, setSelectedUPS] = useState<any>(null)
|
||||||
|
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const data = await fetchApi(url)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: hardwareDataSWR,
|
||||||
|
error: swrError,
|
||||||
|
isLoading: swrLoading,
|
||||||
|
mutate,
|
||||||
|
} = useSWR<HardwareData>("/api/hardware", fetcher, {
|
||||||
|
refreshInterval: 30000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedGPU) return
|
if (!selectedGPU) return
|
||||||
|
|
||||||
@@ -243,30 +264,10 @@ export default function Hardware() {
|
|||||||
|
|
||||||
const fetchRealtimeData = async () => {
|
const fetchRealtimeData = async () => {
|
||||||
try {
|
try {
|
||||||
const { protocol, hostname, port } = window.location
|
const data = await fetchApi(`/api/gpu/${fullSlot}/realtime`)
|
||||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
|
||||||
|
|
||||||
const apiUrl = isStandardPort
|
|
||||||
? `/api/gpu/${fullSlot}/realtime`
|
|
||||||
: `${protocol}//${hostname}:${API_PORT}/api/gpu/${fullSlot}/realtime`
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: abortController.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setRealtimeGPUData(data)
|
setRealtimeGPUData(data)
|
||||||
setDetailsLoading(false)
|
setDetailsLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only log non-abort errors
|
|
||||||
if (error instanceof Error && error.name !== "AbortError") {
|
if (error instanceof Error && error.name !== "AbortError") {
|
||||||
console.error("[v0] Error fetching GPU realtime data:", error)
|
console.error("[v0] Error fetching GPU realtime data:", error)
|
||||||
}
|
}
|
||||||
@@ -275,10 +276,7 @@ export default function Hardware() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
fetchRealtimeData()
|
fetchRealtimeData()
|
||||||
|
|
||||||
// Poll every 3 seconds
|
|
||||||
const interval = setInterval(fetchRealtimeData, 3000)
|
const interval = setInterval(fetchRealtimeData, 3000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -294,14 +292,14 @@ export default function Hardware() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
|
const findPCIDeviceForGPU = (gpu: GPU): PCIDevice | null => {
|
||||||
if (!hardwareData?.pci_devices || !gpu.slot) return null
|
if (!hardwareDataSWR?.pci_devices || !gpu.slot) return null
|
||||||
|
|
||||||
// Try to find exact match first (e.g., "00:02.0")
|
// Try to find exact match first (e.g., "00:02.0")
|
||||||
let pciDevice = hardwareData.pci_devices.find((d) => d.slot === gpu.slot)
|
let pciDevice = hardwareDataSWR.pci_devices.find((d) => d.slot === gpu.slot)
|
||||||
|
|
||||||
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
|
// If not found, try to match by partial slot (e.g., "00" matches "00:02.0")
|
||||||
if (!pciDevice && gpu.slot.length <= 2) {
|
if (!pciDevice && gpu.slot.length <= 2) {
|
||||||
pciDevice = hardwareData.pci_devices.find(
|
pciDevice = hardwareDataSWR.pci_devices.find(
|
||||||
(d) =>
|
(d) =>
|
||||||
d.slot.startsWith(gpu.slot + ":") &&
|
d.slot.startsWith(gpu.slot + ":") &&
|
||||||
(d.type.toLowerCase().includes("vga") ||
|
(d.type.toLowerCase().includes("vga") ||
|
||||||
@@ -320,7 +318,7 @@ export default function Hardware() {
|
|||||||
return realtimeGPUData.has_monitoring_tool === true
|
return realtimeGPUData.has_monitoring_tool === true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (swrLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
@@ -333,7 +331,7 @@ export default function Hardware() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* System Information - CPU & Motherboard */}
|
{/* System Information - CPU & Motherboard */}
|
||||||
{(hardwareData?.cpu || hardwareData?.motherboard) && (
|
{(hardwareDataSWR?.cpu || hardwareDataSWR?.motherboard) && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Cpu className="h-5 w-5 text-primary" />
|
<Cpu className="h-5 w-5 text-primary" />
|
||||||
@@ -342,44 +340,44 @@ export default function Hardware() {
|
|||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{/* CPU Info */}
|
{/* CPU Info */}
|
||||||
{hardwareData?.cpu && Object.keys(hardwareData.cpu).length > 0 && (
|
{hardwareDataSWR?.cpu && Object.keys(hardwareDataSWR.cpu).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">CPU</h3>
|
<h3 className="text-sm font-semibold">CPU</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{hardwareData.cpu.model && (
|
{hardwareDataSWR.cpu.model && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Model</span>
|
<span className="text-muted-foreground">Model</span>
|
||||||
<span className="font-medium text-right">{hardwareData.cpu.model}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.cpu.model}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.cores_per_socket && hardwareData.cpu.sockets && (
|
{hardwareDataSWR.cpu.cores_per_socket && hardwareDataSWR.cpu.sockets && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Cores</span>
|
<span className="text-muted-foreground">Cores</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{hardwareData.cpu.sockets} × {hardwareData.cpu.cores_per_socket} ={" "}
|
{hardwareDataSWR.cpu.sockets} × {hardwareDataSWR.cpu.cores_per_socket} ={" "}
|
||||||
{hardwareData.cpu.sockets * hardwareData.cpu.cores_per_socket} cores
|
{hardwareDataSWR.cpu.sockets * hardwareDataSWR.cpu.cores_per_socket} cores
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.total_threads && (
|
{hardwareDataSWR.cpu.total_threads && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Threads</span>
|
<span className="text-muted-foreground">Threads</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.total_threads}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.total_threads}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.l3_cache && (
|
{hardwareDataSWR.cpu.l3_cache && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">L3 Cache</span>
|
<span className="text-muted-foreground">L3 Cache</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.l3_cache}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.l3_cache}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.cpu.virtualization && (
|
{hardwareDataSWR.cpu.virtualization && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Virtualization</span>
|
<span className="text-muted-foreground">Virtualization</span>
|
||||||
<span className="font-medium">{hardwareData.cpu.virtualization}</span>
|
<span className="font-medium">{hardwareDataSWR.cpu.virtualization}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -387,41 +385,41 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Motherboard Info */}
|
{/* Motherboard Info */}
|
||||||
{hardwareData?.motherboard && Object.keys(hardwareData.motherboard).length > 0 && (
|
{hardwareDataSWR?.motherboard && Object.keys(hardwareDataSWR.motherboard).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">Motherboard</h3>
|
<h3 className="text-sm font-semibold">Motherboard</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{hardwareData.motherboard.manufacturer && (
|
{hardwareDataSWR.motherboard.manufacturer && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Manufacturer</span>
|
<span className="text-muted-foreground">Manufacturer</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.manufacturer}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.manufacturer}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.model && (
|
{hardwareDataSWR.motherboard.model && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Model</span>
|
<span className="text-muted-foreground">Model</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.model}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.model}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.vendor && (
|
{hardwareDataSWR.motherboard.bios?.vendor && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">BIOS</span>
|
<span className="text-muted-foreground">BIOS</span>
|
||||||
<span className="font-medium text-right">{hardwareData.motherboard.bios.vendor}</span>
|
<span className="font-medium text-right">{hardwareDataSWR.motherboard.bios.vendor}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.version && (
|
{hardwareDataSWR.motherboard.bios?.version && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Version</span>
|
<span className="text-muted-foreground">Version</span>
|
||||||
<span className="font-medium">{hardwareData.motherboard.bios.version}</span>
|
<span className="font-medium">{hardwareDataSWR.motherboard.bios.version}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hardwareData.motherboard.bios?.date && (
|
{hardwareDataSWR.motherboard.bios?.date && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-muted-foreground">Date</span>
|
<span className="text-muted-foreground">Date</span>
|
||||||
<span className="font-medium">{hardwareData.motherboard.bios.date}</span>
|
<span className="font-medium">{hardwareDataSWR.motherboard.bios.date}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -432,18 +430,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Memory Modules */}
|
{/* Memory Modules */}
|
||||||
{hardwareData?.memory_modules && hardwareData.memory_modules.length > 0 && (
|
{hardwareDataSWR?.memory_modules && hardwareDataSWR.memory_modules.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<MemoryStick className="h-5 w-5 text-primary" />
|
<MemoryStick className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Memory Modules</h2>
|
<h2 className="text-lg font-semibold">Memory Modules</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.memory_modules.length} installed
|
{hardwareDataSWR.memory_modules.length} installed
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.memory_modules.map((module, index) => (
|
{hardwareDataSWR.memory_modules.map((module, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="mb-2 font-medium text-sm">{module.slot}</div>
|
<div className="mb-2 font-medium text-sm">{module.slot}</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -479,29 +477,29 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thermal Monitoring */}
|
{/* Thermal Monitoring */}
|
||||||
{hardwareData?.temperatures && hardwareData.temperatures.length > 0 && (
|
{hardwareDataSWR?.temperatures && hardwareDataSWR.temperatures.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Thermometer className="h-5 w-5 text-primary" />
|
<Thermometer className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Thermal Monitoring</h2>
|
<h2 className="text-lg font-semibold">Thermal Monitoring</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.temperatures.length} sensors
|
{hardwareDataSWR.temperatures.length} sensors
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{/* CPU Sensors */}
|
{/* CPU Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length > 0 && (
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">CPU</h3>
|
<h3 className="text-sm font-semibold">CPU</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).CPU.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).CPU.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -532,21 +530,21 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Sensors */}
|
{/* GPU Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
|
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:col-span-2" : ""}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Gpu className="h-4 w-4 text-muted-foreground" />
|
<Gpu className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">GPU</h3>
|
<h3 className="text-sm font-semibold">GPU</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).GPU.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).GPU.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -577,21 +575,23 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* NVME Sensors */}
|
{/* NVME Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:col-span-2" : ""}
|
className={
|
||||||
|
groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:col-span-2" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">NVME</h3>
|
<h3 className="text-sm font-semibold">NVME</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).NVME.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).NVME.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -622,21 +622,21 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PCI Sensors */}
|
{/* PCI Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
|
className={groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:col-span-2" : ""}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
<CpuIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">PCI</h3>
|
<h3 className="text-sm font-semibold">PCI</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).PCI.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).PCI.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -667,21 +667,23 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OTHER Sensors */}
|
{/* OTHER Sensors */}
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 0 && (
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""}
|
className={
|
||||||
|
groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:col-span-2" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
<Thermometer className="h-4 w-4 text-muted-foreground" />
|
||||||
<h3 className="text-sm font-semibold">OTHER</h3>
|
<h3 className="text-sm font-semibold">OTHER</h3>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.length}
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${groupAndSortTemperatures(hardwareData.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
|
className={`grid gap-4 ${groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.length > 1 ? "md:grid-cols-2" : ""}`}
|
||||||
>
|
>
|
||||||
{groupAndSortTemperatures(hardwareData.temperatures).OTHER.map((temp, index) => {
|
{groupAndSortTemperatures(hardwareDataSWR.temperatures).OTHER.map((temp, index) => {
|
||||||
const percentage =
|
const percentage =
|
||||||
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
temp.critical > 0 ? (temp.current / temp.critical) * 100 : (temp.current / 100) * 100
|
||||||
const isHot = temp.current > (temp.high || 80)
|
const isHot = temp.current > (temp.high || 80)
|
||||||
@@ -715,18 +717,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GPU Information - Enhanced with on-demand data fetching */}
|
{/* GPU Information - Enhanced with on-demand data fetching */}
|
||||||
{hardwareData?.gpus && hardwareData.gpus.length > 0 && (
|
{hardwareDataSWR?.gpus && hardwareDataSWR.gpus.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Gpu className="h-5 w-5 text-primary" />
|
<Gpu className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Graphics Cards</h2>
|
<h2 className="text-lg font-semibold">Graphics Cards</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.gpus.length} GPU{hardwareData.gpus.length > 1 ? "s" : ""}
|
{hardwareDataSWR.gpus.length} GPU{hardwareDataSWR.gpus.length > 1 ? "s" : ""}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{hardwareData.gpus.map((gpu, index) => {
|
{hardwareDataSWR.gpus.map((gpu, index) => {
|
||||||
const pciDevice = findPCIDeviceForGPU(gpu)
|
const pciDevice = findPCIDeviceForGPU(gpu)
|
||||||
const fullSlot = pciDevice?.slot || gpu.slot
|
const fullSlot = pciDevice?.slot || gpu.slot
|
||||||
|
|
||||||
@@ -1104,18 +1106,18 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* PCI Devices - Changed to modal */}
|
{/* PCI Devices - Changed to modal */}
|
||||||
{hardwareData?.pci_devices && hardwareData.pci_devices.length > 0 && (
|
{hardwareDataSWR?.pci_devices && hardwareDataSWR.pci_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<CpuIcon className="h-5 w-5 text-primary" />
|
<CpuIcon className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">PCI Devices</h2>
|
<h2 className="text-lg font-semibold">PCI Devices</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.pci_devices.length} devices
|
{hardwareDataSWR.pci_devices.length} devices
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.pci_devices.map((device, index) => (
|
{hardwareDataSWR.pci_devices.map((device, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => setSelectedPCIDevice(device)}
|
onClick={() => setSelectedPCIDevice(device)}
|
||||||
@@ -1190,7 +1192,7 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Power Consumption */}
|
{/* Power Consumption */}
|
||||||
{hardwareData?.power_meter && (
|
{hardwareDataSWR?.power_meter && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Zap className="h-5 w-5 text-blue-500" />
|
<Zap className="h-5 w-5 text-blue-500" />
|
||||||
@@ -1200,13 +1202,13 @@ export default function Hardware() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
|
<div className="flex items-center justify-between rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium">{hardwareData.power_meter.name}</p>
|
<p className="text-sm font-medium">{hardwareDataSWR.power_meter.name}</p>
|
||||||
{hardwareData.power_meter.adapter && (
|
{hardwareDataSWR.power_meter.adapter && (
|
||||||
<p className="text-xs text-muted-foreground">{hardwareData.power_meter.adapter}</p>
|
<p className="text-xs text-muted-foreground">{hardwareDataSWR.power_meter.adapter}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-2xl font-bold text-blue-500">{hardwareData.power_meter.watts.toFixed(1)} W</p>
|
<p className="text-2xl font-bold text-blue-500">{hardwareDataSWR.power_meter.watts.toFixed(1)} W</p>
|
||||||
<p className="text-xs text-muted-foreground">Current Draw</p>
|
<p className="text-xs text-muted-foreground">Current Draw</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1215,18 +1217,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Power Supplies */}
|
{/* Power Supplies */}
|
||||||
{hardwareData?.power_supplies && hardwareData.power_supplies.length > 0 && (
|
{hardwareDataSWR?.power_supplies && hardwareDataSWR.power_supplies.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<PowerIcon className="h-5 w-5 text-green-500" />
|
<PowerIcon className="h-5 w-5 text-green-500" />
|
||||||
<h2 className="text-lg font-semibold">Power Supplies</h2>
|
<h2 className="text-lg font-semibold">Power Supplies</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.power_supplies.length} PSUs
|
{hardwareDataSWR.power_supplies.length} PSUs
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{hardwareData.power_supplies.map((psu, index) => (
|
{hardwareDataSWR.power_supplies.map((psu, index) => (
|
||||||
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
<div key={index} className="rounded-lg border border-border/30 bg-background/60 p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">{psu.name}</span>
|
<span className="text-sm font-medium">{psu.name}</span>
|
||||||
@@ -1243,18 +1245,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fans */}
|
{/* Fans */}
|
||||||
{hardwareData?.fans && hardwareData.fans.length > 0 && (
|
{hardwareDataSWR?.fans && hardwareDataSWR.fans.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<FanIcon className="h-5 w-5 text-primary" />
|
<FanIcon className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">System Fans</h2>
|
<h2 className="text-lg font-semibold">System Fans</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.fans.length} fans
|
{hardwareDataSWR.fans.length} fans
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{hardwareData.fans.map((fan, index) => {
|
{hardwareDataSWR.fans.map((fan, index) => {
|
||||||
const isPercentage = fan.unit === "percent" || fan.unit === "%"
|
const isPercentage = fan.unit === "percent" || fan.unit === "%"
|
||||||
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
|
const percentage = isPercentage ? fan.speed : Math.min((fan.speed / 5000) * 100, 100)
|
||||||
|
|
||||||
@@ -1278,18 +1280,18 @@ export default function Hardware() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* UPS */}
|
{/* UPS */}
|
||||||
{hardwareData?.ups && Array.isArray(hardwareData.ups) && hardwareData.ups.length > 0 && (
|
{hardwareDataSWR?.ups && Array.isArray(hardwareDataSWR.ups) && hardwareDataSWR.ups.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Battery className="h-5 w-5 text-primary" />
|
<Battery className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">UPS Status</h2>
|
<h2 className="text-lg font-semibold">UPS Status</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.ups.length} UPS
|
{hardwareDataSWR.ups.length} UPS
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{hardwareData.ups.map((ups: any, index: number) => {
|
{hardwareDataSWR.ups.map((ups: any, index: number) => {
|
||||||
const batteryCharge =
|
const batteryCharge =
|
||||||
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
|
ups.battery_charge_raw || Number.parseFloat(ups.battery_charge?.replace("%", "") || "0")
|
||||||
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
|
const loadPercent = ups.load_percent_raw || Number.parseFloat(ups.load_percent?.replace("%", "") || "0")
|
||||||
@@ -1560,19 +1562,19 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Network Summary - Clickable */}
|
{/* Network Summary - Clickable */}
|
||||||
{hardwareData?.pci_devices &&
|
{hardwareDataSWR?.pci_devices &&
|
||||||
hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<Network className="h-5 w-5 text-primary" />
|
<Network className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Network Summary</h2>
|
<h2 className="text-lg font-semibold">Network Summary</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{hardwareData.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
|
{hardwareDataSWR.pci_devices.filter((d) => d.type.toLowerCase().includes("network")).length} interfaces
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.pci_devices
|
{hardwareDataSWR.pci_devices
|
||||||
.filter((d) => d.type.toLowerCase().includes("network"))
|
.filter((d) => d.type.toLowerCase().includes("network"))
|
||||||
.map((device, index) => (
|
.map((device, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -1652,14 +1654,14 @@ export default function Hardware() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Storage Summary - Clickable */}
|
{/* Storage Summary - Clickable */}
|
||||||
{hardwareData?.storage_devices && hardwareData.storage_devices.length > 0 && (
|
{hardwareDataSWR?.storage_devices && hardwareDataSWR.storage_devices.length > 0 && (
|
||||||
<Card className="border-border/50 bg-card/50 p-6">
|
<Card className="border-border/50 bg-card/50 p-6">
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<HardDrive className="h-5 w-5 text-primary" />
|
<HardDrive className="h-5 w-5 text-primary" />
|
||||||
<h2 className="text-lg font-semibold">Storage Summary</h2>
|
<h2 className="text-lg font-semibold">Storage Summary</h2>
|
||||||
<Badge variant="outline" className="ml-auto">
|
<Badge variant="outline" className="ml-auto">
|
||||||
{
|
{
|
||||||
hardwareData.storage_devices.filter(
|
hardwareDataSWR.storage_devices.filter(
|
||||||
(device) =>
|
(device) =>
|
||||||
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
||||||
).length
|
).length
|
||||||
@@ -1669,7 +1671,7 @@ export default function Hardware() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{hardwareData.storage_devices
|
{hardwareDataSWR.storage_devices
|
||||||
.filter(
|
.filter(
|
||||||
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
(device) => device.type === "disk" && !device.name.startsWith("zd") && !device.name.startsWith("loop"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Card, CardContent } from "./ui/card"
|
|||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
import { Wifi, Zap } from "lucide-react"
|
import { Wifi, Zap } from "lucide-react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface NetworkCardProps {
|
interface NetworkCardProps {
|
||||||
interface_: {
|
interface_: {
|
||||||
@@ -94,26 +95,12 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrafficData = async () => {
|
const fetchTrafficData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`, {
|
const data = await fetchApi(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch traffic data: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// Calculate totals from the data points
|
|
||||||
if (data.data && data.data.length > 0) {
|
if (data.data && data.data.length > 0) {
|
||||||
const lastPoint = data.data[data.data.length - 1]
|
const lastPoint = data.data[data.data.length - 1]
|
||||||
const firstPoint = data.data[0]
|
const firstPoint = data.data[0]
|
||||||
|
|
||||||
// Calculate the difference between last and first data points
|
|
||||||
const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
|
const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
|
||||||
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
|
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
|
||||||
|
|
||||||
@@ -124,16 +111,13 @@ export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch traffic data for card:", error)
|
console.error("[v0] Failed to fetch traffic data for card:", error)
|
||||||
// Keep showing 0 values on error
|
|
||||||
setTrafficData({ received: 0, sent: 0 })
|
setTrafficData({ received: 0, sent: 0 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only fetch if interface is up and not a VM
|
|
||||||
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
|
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
|
||||||
fetchTrafficData()
|
fetchTrafficData()
|
||||||
|
|
||||||
// Refresh every 60 seconds
|
|
||||||
const interval = setInterval(fetchTrafficData, 60000)
|
const interval = setInterval(fetchTrafficData, 60000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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"
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface NetworkData {
|
interface NetworkData {
|
||||||
interfaces: NetworkInterface[]
|
interfaces: NetworkInterface[]
|
||||||
@@ -128,19 +129,7 @@ const formatSpeed = (speed: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetcher = async (url: string): Promise<NetworkData> => {
|
const fetcher = async (url: string): Promise<NetworkData> => {
|
||||||
const response = await fetch(url, {
|
return fetchApi<NetworkData>(url)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkMetrics() {
|
export function NetworkMetrics() {
|
||||||
|
|||||||
@@ -3,7 +3,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 { API_PORT } from "@/lib/api-config"
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface NetworkMetricsData {
|
interface NetworkMetricsData {
|
||||||
time: string
|
time: string
|
||||||
@@ -76,24 +76,13 @@ export function NetworkTrafficChart({
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { protocol, hostname, port } = window.location
|
const apiPath = interfaceName
|
||||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
? `/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
||||||
|
: `/api/node/metrics?timeframe=${timeframe}`
|
||||||
|
|
||||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}`
|
console.log("[v0] Fetching network metrics from:", apiPath)
|
||||||
|
|
||||||
const apiUrl = interfaceName
|
const result = await fetchApi<any>(apiPath)
|
||||||
? `${baseUrl}/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
|
|
||||||
: `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
|
||||||
|
|
||||||
console.log("[v0] Fetching network metrics from:", apiUrl)
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch network metrics: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (!result.data || !Array.isArray(result.data)) {
|
if (!result.data || !Array.isArray(result.data)) {
|
||||||
throw new Error("Invalid data format received from server")
|
throw new Error("Invalid data format received from server")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
|||||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||||
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
|
||||||
import { useIsMobile } from "../hooks/use-mobile"
|
import { useIsMobile } from "../hooks/use-mobile"
|
||||||
import { API_PORT } from "@/lib/api-config"
|
import { fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
const TIMEFRAME_OPTIONS = [
|
const TIMEFRAME_OPTIONS = [
|
||||||
{ value: "hour", label: "1 Hour" },
|
{ value: "hour", label: "1 Hour" },
|
||||||
@@ -89,27 +89,8 @@ export function NodeMetricsCharts() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { protocol, hostname, port } = window.location
|
const result = await fetchApi<any>(`/api/node/metrics?timeframe=${timeframe}`)
|
||||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
|
||||||
|
|
||||||
const baseUrl = isStandardPort ? "" : `${protocol}//${hostname}:${API_PORT}`
|
|
||||||
|
|
||||||
const apiUrl = `${baseUrl}/api/node/metrics?timeframe=${timeframe}`
|
|
||||||
|
|
||||||
console.log("[v0] Fetching node metrics from:", apiUrl)
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl)
|
|
||||||
|
|
||||||
console.log("[v0] Response status:", response.status)
|
|
||||||
console.log("[v0] Response ok:", response.ok)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.log("[v0] Error response text:", errorText)
|
|
||||||
throw new Error(`Failed to fetch node metrics: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
console.log("[v0] Node metrics result:", result)
|
console.log("[v0] Node metrics result:", result)
|
||||||
console.log("[v0] Result keys:", Object.keys(result))
|
console.log("[v0] Result keys:", Object.keys(result))
|
||||||
console.log("[v0] Data array length:", result.data?.length || 0)
|
console.log("[v0] Data array length:", result.data?.length || 0)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useState, useEffect, useMemo } from "react"
|
import { useState, useEffect, useMemo } from "react"
|
||||||
import { API_PORT } from "@/lib/api-config"
|
import { API_PORT, fetchApi } from "@/lib/api-config"
|
||||||
|
|
||||||
interface Log {
|
interface Log {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
@@ -135,6 +135,10 @@ export function SystemLogs() {
|
|||||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This part might not be strictly necessary if only running client-side, but good for SSR safety
|
||||||
|
// In a real SSR scenario, you'd need to handle API_PORT differently
|
||||||
|
const protocol = typeof window !== "undefined" ? window.location.protocol : "http:" // Defaulting to http for SSR safety
|
||||||
|
const hostname = typeof window !== "undefined" ? window.location.hostname : "localhost" // Defaulting to localhost for SSR safety
|
||||||
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
return `${protocol}//${hostname}:${API_PORT}${endpoint}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,27 +198,15 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
const [logsRes, backupsRes, eventsRes, notificationsRes] = await Promise.all([
|
||||||
fetchSystemLogs(),
|
fetchSystemLogs(),
|
||||||
fetch(getApiUrl("/api/backups")),
|
fetchApi("/api/backups"),
|
||||||
fetch(getApiUrl("/api/events?limit=50")),
|
fetchApi("/api/events?limit=50"),
|
||||||
fetch(getApiUrl("/api/notifications")),
|
fetchApi("/api/notifications"),
|
||||||
])
|
])
|
||||||
|
|
||||||
setLogs(logsRes)
|
setLogs(logsRes)
|
||||||
|
setBackups(backupsRes.backups || [])
|
||||||
if (backupsRes.ok) {
|
setEvents(eventsRes.events || [])
|
||||||
const backupsData = await backupsRes.json()
|
setNotifications(notificationsRes.notifications || [])
|
||||||
setBackups(backupsData.backups || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventsRes.ok) {
|
|
||||||
const eventsData = await eventsRes.json()
|
|
||||||
setEvents(eventsData.events || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notificationsRes.ok) {
|
|
||||||
const notificationsData = await notificationsRes.json()
|
|
||||||
setNotifications(notificationsData.notifications || [])
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[v0] Error fetching system logs data:", err)
|
console.error("[v0] Error fetching system logs data:", err)
|
||||||
setError("Failed to connect to server")
|
setError("Failed to connect to server")
|
||||||
@@ -225,7 +217,7 @@ export function SystemLogs() {
|
|||||||
|
|
||||||
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
const fetchSystemLogs = async (): Promise<SystemLog[]> => {
|
||||||
try {
|
try {
|
||||||
let apiUrl = getApiUrl("/api/logs")
|
let apiUrl = "/api/logs"
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
// CHANGE: Always add since_days parameter (no more "now" option)
|
// CHANGE: Always add since_days parameter (no more "now" option)
|
||||||
@@ -258,22 +250,7 @@ export function SystemLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("[v0] Making fetch request to:", apiUrl)
|
console.log("[v0] Making fetch request to:", apiUrl)
|
||||||
const response = await fetch(apiUrl, {
|
const data = await fetchApi(apiUrl)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
cache: "no-store",
|
|
||||||
signal: AbortSignal.timeout(30000), // 30 second timeout
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("[v0] Response status:", response.status, "OK:", response.ok)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
|
console.log("[v0] Received logs data, count:", data.logs?.length || 0)
|
||||||
|
|
||||||
const logsArray = Array.isArray(data) ? data : data.logs || []
|
const logsArray = Array.isArray(data) ? data : data.logs || []
|
||||||
@@ -364,10 +341,7 @@ export function SystemLogs() {
|
|||||||
if (upid) {
|
if (upid) {
|
||||||
// Try to fetch the complete task log from Proxmox
|
// Try to fetch the complete task log from Proxmox
|
||||||
try {
|
try {
|
||||||
const response = await fetch(getApiUrl(`/api/task-log/${encodeURIComponent(upid)}`))
|
const taskLog = await fetchApi(`/api/task-log/${encodeURIComponent(upid)}`, {}, "text")
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const taskLog = await response.text()
|
|
||||||
|
|
||||||
// Download the complete task log
|
// Download the complete task log
|
||||||
const blob = new Blob(
|
const blob = new Blob(
|
||||||
@@ -394,7 +368,6 @@ export function SystemLogs() {
|
|||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to fetch task log from Proxmox:", error)
|
console.error("[v0] Failed to fetch task log from Proxmox:", error)
|
||||||
// Fall through to download notification message
|
// Fall through to download notification message
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { MetricsView } from "./metrics-dialog"
|
import { MetricsView } from "./metrics-dialog"
|
||||||
import { formatStorage } from "@/lib/utils" // Import formatStorage utility
|
import { formatStorage } from "@/lib/utils" // Import formatStorage utility
|
||||||
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface VMData {
|
interface VMData {
|
||||||
vmid: number
|
vmid: number
|
||||||
@@ -133,20 +134,7 @@ interface VMDetails extends VMData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetcher = async (url: string) => {
|
const fetcher = async (url: string) => {
|
||||||
const response = await fetch(url, {
|
return fetchApi(url)
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(60000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Flask server responded with status: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes: number | undefined): string => {
|
const formatBytes = (bytes: number | undefined): string => {
|
||||||
@@ -310,20 +298,15 @@ export function VirtualMachines() {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||||
|
|
||||||
const response = await fetch(`/api/vms/${lxc.vmid}`, {
|
const details = await fetchApi(`/api/vms/${lxc.vmid}`)
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const details = await response.json()
|
|
||||||
if (details.lxc_ip_info?.primary_ip) {
|
if (details.lxc_ip_info?.primary_ip) {
|
||||||
configs[lxc.vmid] = details.lxc_ip_info.primary_ip
|
configs[lxc.vmid] = details.lxc_ip_info.primary_ip
|
||||||
} else if (details.config) {
|
} else if (details.config) {
|
||||||
configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info)
|
configs[lxc.vmid] = extractIPFromConfig(details.config, details.lxc_ip_info)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`)
|
console.log(`[v0] Could not fetch IP for LXC ${lxc.vmid}`)
|
||||||
configs[lxc.vmid] = "N/A"
|
configs[lxc.vmid] = "N/A"
|
||||||
@@ -350,11 +333,8 @@ export function VirtualMachines() {
|
|||||||
setEditedNotes("")
|
setEditedNotes("")
|
||||||
setDetailsLoading(true)
|
setDetailsLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vm.vmid}`)
|
const details = await fetchApi(`/api/vms/${vm.vmid}`)
|
||||||
if (response.ok) {
|
|
||||||
const details = await response.json()
|
|
||||||
setVMDetails(details)
|
setVMDetails(details)
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching VM details:", error)
|
console.error("Error fetching VM details:", error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -373,23 +353,16 @@ export function VirtualMachines() {
|
|||||||
const handleVMControl = async (vmid: number, action: string) => {
|
const handleVMControl = async (vmid: number, action: string) => {
|
||||||
setControlLoading(true)
|
setControlLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vmid}/control`, {
|
await fetchApi(`/api/vms/${vmid}/control`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ action }),
|
body: JSON.stringify({ action }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
mutate()
|
mutate()
|
||||||
setSelectedVM(null)
|
setSelectedVM(null)
|
||||||
setVMDetails(null)
|
setVMDetails(null)
|
||||||
} else {
|
|
||||||
console.error("Failed to control VM")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error controlling VM:", error)
|
console.error("Failed to control VM")
|
||||||
} finally {
|
} finally {
|
||||||
setControlLoading(false)
|
setControlLoading(false)
|
||||||
}
|
}
|
||||||
@@ -397,9 +370,7 @@ export function VirtualMachines() {
|
|||||||
|
|
||||||
const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${vmid}/logs`)
|
const data = await fetchApi(`/api/vms/${vmid}/logs`)
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// Format logs as plain text
|
// Format logs as plain text
|
||||||
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
|
let logText = `=== Logs for ${vmName} (VMID: ${vmid}) ===\n`
|
||||||
@@ -426,7 +397,6 @@ export function VirtualMachines() {
|
|||||||
a.download = `${vmName}-${vmid}-logs.txt`
|
a.download = `${vmName}-${vmid}-logs.txt`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error downloading logs:", error)
|
console.error("Error downloading logs:", error)
|
||||||
}
|
}
|
||||||
@@ -621,17 +591,13 @@ export function VirtualMachines() {
|
|||||||
|
|
||||||
setSavingNotes(true)
|
setSavingNotes(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/vms/${selectedVM.vmid}/config`, {
|
await fetchApi(`/api/vms/${selectedVM.vmid}/config`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
description: editedNotes, // Send as-is, pvesh will handle encoding
|
description: editedNotes, // Send as-is, pvesh will handle encoding
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setVMDetails({
|
setVMDetails({
|
||||||
...vmDetails,
|
...vmDetails,
|
||||||
config: {
|
config: {
|
||||||
@@ -640,10 +606,6 @@ export function VirtualMachines() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
setIsEditingNotes(false)
|
setIsEditingNotes(false)
|
||||||
} else {
|
|
||||||
console.error("Failed to save notes")
|
|
||||||
alert("Failed to save notes. Please try again.")
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving notes:", error)
|
console.error("Error saving notes:", error)
|
||||||
alert("Error saving notes. Please try again.")
|
alert("Error saving notes. Please try again.")
|
||||||
|
|||||||
Reference in New Issue
Block a user