'use client'; import { useState, useEffect, useRef } from 'react'; import NodeList from './NodeList'; import VMList from './VMList'; import LXCList from './LXCList'; import axios from 'axios'; const DEFAULT_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000); function pollUrls() { return ['/api/nodes', '/api/vms', '/api/lxc', '/api/status']; } /** @returns {import('react').ReactNode} */ export default function Dashboard() { const [activeTab, setActiveTab] = useState('nodes'); const [powerActionStatus, setPowerActionStatus] = useState(null); const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL); const [nodes, setNodes] = useState([]); const [vms, setVms] = useState([]); const [lxc, setLxc] = useState([]); const [summary, setSummary] = useState(null); const [error, setError] = useState(null); const ref = useRef({ nodes: setNodes, vms: setVms, lxc: setLxc, status: setSummary }); useEffect(() => { ref.current = { nodes: setNodes, vms: setVms, lxc: setLxc, status: setSummary }; }, []); useEffect(() => { let cancelled = false; const poll = async () => { try { const [n, v, l, s] = await Promise.all([ axios.get('/api/nodes'), axios.get('/api/vms'), axios.get('/api/lxc'), axios.get('/api/status'), ]); if (cancelled) return; setNodes(n.data.data); setVms(v.data.data); setLxc(l.data.data); setSummary(s.data.data); setError(null); } catch (err) { if (!cancelled) setError(err.message); } }; poll(); const id = setInterval(poll, intervalMs); return () => { cancelled = true; clearInterval(id); }; }, [intervalMs]); const handlePowerAction = async (node, vmid, type, action) => { const label = action.charAt(0).toUpperCase() + action.slice(1); const kind = type === 'qemu' ? 'VM' : 'LXC'; setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' }); await new Promise((r) => requestAnimationFrame(r)); try { await axios.post('/api/power', { node, vmid, type, action }); await pollUrls().map((url) => axios.get(url)); setPowerActionStatus({ text: `${label} successful`, type: 'success' }); // Sync state from poll responses setNodes(nodes.map((n) => n)); setVms(vms.map((v) => v)); setLxc(lxc.map((l) => l)); setTimeout(() => setPowerActionStatus(null), 3000); } catch (err) { setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' }); } }; const [isRefreshing, setIsRefreshing] = useState(false); const handleRefresh = async () => { setIsRefreshing(true); try { await Promise.all(pollUrls().map((url) => axios.get(url))); } finally { setIsRefreshing(false); } }; const handleIntervalChange = (e) => { const v = Number(e.target.value); if (v > 0) setIntervalMs(v); }; const [intervalInput, setIntervalInput] = useState(DEFAULT_INTERVAL); useEffect(() => { setIntervalInput(intervalMs); }, [intervalMs]); const handleIntervalBlur = () => { handleIntervalChange({ target: { value: intervalInput } }); }; const handleOpenConsole = (vm) => { window.open(vm.consoleUrl, '_blank'); }; const tabs = [ { id: 'nodes', label: 'Nodes' }, { id: 'vms', label: 'VMs' }, { id: 'lxc', label: 'LXCs' }, ]; return (
{/* Header */}

Proxmox Monitor

{nodes.length} node{nodes.length !== 1 ? 's' : ''} • {summary?.runningVMs ?? 0} running • Updated{' '} {new Date().toLocaleTimeString()}

setIntervalInput(Number(e.target.value))} onBlur={handleIntervalBlur} onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); } }} className="w-24 rounded-md border border-border-card bg-bg-card px-2 py-1.5 text-sm text-text-primary focus:border-accent focus:outline-none" placeholder="ms" />
{/* Summary Cards */} {summary && !error && (
Nodes
{summary.totalNodes}
Running VMs
{summary.runningVMs}
Stopped VMs
{summary.stoppedVMs}
Total Memory
{summary.totalMemory?.total ? `${(summary.totalMemory.used / (1024 ** 3)).toFixed(1)} / ${(summary.totalMemory.total / (1024 ** 3)).toFixed(1)} GB` : 'N/A'}
)} {error && (
{error}
)} {powerActionStatus && (
{powerActionStatus.text}
)} {/* Tab navigation */}
{tabs.map((tab) => ( ))}
{/* Tab content */}
{activeTab === 'nodes' && } {activeTab === 'vms' && } {activeTab === 'lxc' && }
{/* Footer */}
Polling every {intervalMs / 1000}s • Proxmox VE Monitor
); }