/* eslint-disable react-hooks/exhaustive-deps */ import React from "react"; import { Button } from "@/components/button.tsx"; import { getProxmoxStats, startProxmoxStatsPolling, stopProxmoxStatsPolling, sendProxmoxStatsHeartbeat, getSSHHosts, } from "@/main-axios.ts"; import type { ProxmoxStatsSnapshot } from "@/types/proxmox"; import type { ProxmoxStatsConfig } from "@/types"; import { useTranslation } from "react-i18next"; import { RefreshCw, Server } from "lucide-react"; import { NodeSummaryStrip } from "./NodeSummaryStrip"; import { GuestTable } from "./GuestTable"; import { NodeNetworkCard } from "./cards/NodeNetworkCard"; import { StoragePoolsCard } from "./cards/StoragePoolsCard"; import { ClusterHealthCard } from "./cards/ClusterHealthCard"; import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; import { ConnectionLogProvider, useConnectionLog, } from "@/ssh/connection-log/ConnectionLogContext.tsx"; import { useConnectionRetry } from "@/lib/useConnectionRetry.ts"; import { runAdaptivePolling } from "@/lib/adaptive-polling.ts"; const HISTORY_LEN = 30; function statsChangeKey(data: ProxmoxStatsSnapshot): string { const bucket = (value: number | null | undefined) => value == null ? null : Math.round(value / 5) * 5; return JSON.stringify({ cpu: bucket(data.node.cpu.percent), memory: bucket(data.node.memory.percent), disk: bucket(data.node.disk.percent), guests: data.guests.counts, cluster: data.cluster, }); } const DEFAULT_POLL_INTERVAL = 60; interface HostConfig { id: number; name: string; ip: string; username: string; enableProxmoxStats?: boolean; proxmoxStatsConfig?: string | ProxmoxStatsConfig | null; authType?: string; port?: number; [key: string]: unknown; } interface ProxmoxStatsProps { hostConfig?: HostConfig; title?: string; isVisible?: boolean; isTopbarOpen?: boolean; embedded?: boolean; } interface ProxmoxStatsHistories { cpu: number[]; memory: number[]; disk: number[]; } function parseProxmoxStatsConfig( raw?: string | ProxmoxStatsConfig | null, ): Required> & ProxmoxStatsConfig { const defaults = { pollInterval: DEFAULT_POLL_INTERVAL, nodeName: null }; if (!raw) return defaults; try { const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; return { ...defaults, ...parsed }; } catch { return defaults; } } export function ProxmoxStatsTab(props: ProxmoxStatsProps): React.ReactElement { return ( ); } function ProxmoxStatsInner({ hostConfig, title, isVisible = true, isTopbarOpen = true, embedded = false, }: ProxmoxStatsProps): React.ReactElement { const { t } = useTranslation(); const { addLog, clearLogs } = useConnectionLog(); const [snapshot, setSnapshot] = React.useState( null, ); const [histories, setHistories] = React.useState({ cpu: [], memory: [], disk: [], }); const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); const [isRefreshing, setIsRefreshing] = React.useState(false); const [isPageVisible, setIsPageVisible] = React.useState(!document.hidden); const [viewerSessionId, setViewerSessionId] = React.useState( null, ); const statsConfig = React.useMemo( () => parseProxmoxStatsConfig(currentHostConfig?.proxmoxStatsConfig), [currentHostConfig?.proxmoxStatsConfig], ); React.useEffect(() => { const onVis = () => setIsPageVisible(!document.hidden); document.addEventListener("visibilitychange", onVis); return () => document.removeEventListener("visibilitychange", onVis); }, []); const isActuallyVisible = isVisible && isPageVisible; React.useEffect(() => { if (!viewerSessionId || !isActuallyVisible) return; const interval = setInterval(() => { sendProxmoxStatsHeartbeat(viewerSessionId).catch(() => {}); }, 30000); return () => clearInterval(interval); }, [viewerSessionId, isActuallyVisible]); React.useEffect(() => { if (hostConfig?.id !== currentHostConfig?.id) { setSnapshot(null); setHistories({ cpu: [], memory: [], disk: [] }); } setCurrentHostConfig(hostConfig); }, [hostConfig?.id]); React.useEffect(() => { const fetchLatest = async () => { if (!hostConfig?.id) return; try { const hosts = await getSSHHosts(); const updated = hosts.find((h) => h.id === hostConfig.id); if (updated) setCurrentHostConfig(updated as unknown as HostConfig); } catch { // keep previous config } }; fetchLatest(); const onChanged = () => fetchLatest(); window.addEventListener("ssh-hosts:changed", onChanged); return () => window.removeEventListener("ssh-hosts:changed", onChanged); }, [hostConfig?.id]); const pushHistory = React.useCallback((data: ProxmoxStatsSnapshot) => { setHistories((prev) => { const add = (arr: number[], v: number | null | undefined) => [...arr, v ?? 0].slice(-HISTORY_LEN); return { cpu: add(prev.cpu, data.node?.cpu?.percent), memory: add(prev.memory, data.node?.memory?.percent), disk: add(prev.disk, data.node?.disk?.percent), }; }); }, []); const notEnabled = currentHostConfig?.enableProxmoxStats !== true; const stopPollingRef = React.useRef<(() => void) | null>(null); const fetchSnapshot = React.useCallback(async (): Promise => { if (!currentHostConfig?.id) return; addLog({ type: "info", stage: "stats_connecting", message: t("proxmoxStats.connecting"), }); const result = await startProxmoxStatsPolling(currentHostConfig.id); if (result.viewerSessionId) setViewerSessionId(result.viewerSessionId); addLog({ type: "info", stage: "stats_polling", message: t("proxmoxStats.connecting"), }); const data = await getProxmoxStats(currentHostConfig.id); if (!data) { throw new Error(t("proxmoxStats.connectionFailed")); } setSnapshot(data); addLog({ type: "success", stage: "connected", message: t("terminal.connected"), }); const intervalMs = (statsConfig.pollInterval ?? DEFAULT_POLL_INTERVAL) * 1000; let signature = statsChangeKey(data); stopPollingRef.current?.(); stopPollingRef.current = runAdaptivePolling( async () => { const next = await getProxmoxStats(currentHostConfig.id); if (!next) throw new Error(t("proxmoxStats.connectionFailed")); const nextSignature = statsChangeKey(next); const changed = nextSignature !== signature; signature = nextSignature; setSnapshot(next); pushHistory(next); return changed; }, { minIntervalMs: intervalMs, maxIntervalMs: Math.min(120_000, intervalMs * 6), stablePollsPerStep: 3, }, { runImmediately: false }, ); }, [currentHostConfig?.id, statsConfig.pollInterval, addLog, t, pushHistory]); const retry = useConnectionRetry({ connect: async () => { try { await fetchSnapshot(); retry.markConnected(); } catch (error: unknown) { addLog({ type: "error", stage: "error", message: error instanceof Error ? error.message : t("proxmoxStats.connectionFailed"), }); retry.markFailed(); } }, enabled: isActuallyVisible && !notEnabled && !!currentHostConfig?.id, autoStart: false, }); const retryRef = React.useRef(retry); retryRef.current = retry; React.useEffect(() => { if (notEnabled || !currentHostConfig?.id) return; let cancelled = false; const debounce = setTimeout(() => { if (isActuallyVisible && !cancelled) { clearLogs(); retryRef.current.reset(); retryRef.current.retryNow(); } else if (!isActuallyVisible) { stopPollingRef.current?.(); stopPollingRef.current = null; if (currentHostConfig?.id) { stopProxmoxStatsPolling(currentHostConfig.id, undefined).catch( () => {}, ); } } }, 500); return () => { cancelled = true; clearTimeout(debounce); stopPollingRef.current?.(); stopPollingRef.current = null; if (currentHostConfig?.id) { stopProxmoxStatsPolling(currentHostConfig.id).catch(() => {}); } }; }, [currentHostConfig?.id, notEnabled, isActuallyVisible]); const wrapperStyle: React.CSSProperties = embedded ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } : { opacity: isVisible ? 1 : 0, margin: isTopbarOpen ? "74px 17px 8px 8px" : "16px 17px 8px 8px", height: isTopbarOpen ? "calc(100vh - 82px)" : "calc(100vh - 24px)", }; const handleRefresh = async () => { if (!currentHostConfig?.id) return; if (retry.status !== "connected") { retry.retryNow(); return; } try { setIsRefreshing(true); const data = await getProxmoxStats(currentHostConfig.id); if (data) { setSnapshot(data); pushHistory(data); } } finally { setIsRefreshing(false); } }; const showContent = !notEnabled && retry.status === "connected" && snapshot; return (
{retry.status === "connected" && !notEnabled && (

{title}

)} {showContent && (
{snapshot.cluster.clustered && ( )}
)}

{t("proxmoxStats.noHostSelected")}

) : undefined } />
); }