diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index f6d2ae4..cde13ec 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -6,7 +6,6 @@ import NodeList from './NodeList'; import VMList from './VMList'; import LXCList from './LXCList'; import axios from 'axios'; -import { SWRConfig } from 'swr'; import ConsolePanel from './ConsolePanel'; const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000); @@ -17,8 +16,6 @@ const POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_API_POLL_INTERVAL || 30000) * @returns {Promise<{ data: *, error: string | null }>} */ async function fetchData(url) { - - try { const res = await axios.get(url); return res.data; @@ -35,39 +32,59 @@ export default function Dashboard() { const [activeConsole, setActiveConsole] = useState(null); const [powerActionStatus, setPowerActionStatus] = useState(null); - /** - * @param {string} node - * @param {number} vmid - * @param {string} type - * @param {string} action - */ const handlePowerAction = async (node, vmid, type, action) => { - setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)}ing ${type === 'qemu' ? 'VM' : 'LXC'} ${vmid}...`, type: 'pending' }); + const label = action.charAt(0).toUpperCase() + action.slice(1); + const kind = type === 'qemu' ? 'VM' : 'LXC'; + setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' }); + // Flush the pending status so it paints before the API call + await new Promise((r) => requestAnimationFrame(r)); + try { await axios.post('/api/power', { node, vmid, type, action }); - setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' }); + // Re-fetch after success + const [nodesRes, vmsRes, lxcRes, statusRes] = await Promise.all([ + axios.get('/api/nodes'), + axios.get('/api/vms'), + axios.get('/api/lxc'), + axios.get('/api/status'), + ]); + setPowerActionStatus({ text: `${label} successful`, type: 'success' }); + setNodes(nodesRes.data.data); + setVms(vmsRes.data.data); + setLxc(lxcRes.data.data); + setSummary(statusRes.data.data); + // Auto-dismiss after 3 seconds + setTimeout(() => setPowerActionStatus(null), 3000); } catch (err) { - setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' }); + setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' }); } }; - const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, { - refreshInterval: POLL_INTERVAL, - }); - const { data: vmsData, error: vmsError } = useSWR('/api/vms', fetchData, { - refreshInterval: POLL_INTERVAL, - }); - const { data: lxcData, error: lxcError } = useSWR('/api/lxc', fetchData, { - refreshInterval: POLL_INTERVAL, - }); - const { data: summaryData, error: summaryError } = useSWR('/api/status', fetchData, { - refreshInterval: POLL_INTERVAL, - }); + const [nodes, setNodes] = useState([]); + const [vms, setVms] = useState([]); + const [lxc, setLxc] = useState([]); + const [summary, setSummary] = useState(null); - const nodes = nodesData?.data ?? []; - const vms = vmsData?.data ?? []; - const lxc = lxcData?.data ?? []; - const summary = summaryData?.data ?? null; + const { error: nodesError } = useSWR('/api/nodes', fetchData, { + refreshInterval: POLL_INTERVAL, + fallbackData: { data: [] }, + onSuccess: (data) => setNodes(data.data), + }); + const { error: vmsError } = useSWR('/api/vms', fetchData, { + refreshInterval: POLL_INTERVAL, + fallbackData: { data: [] }, + onSuccess: (data) => setVms(data.data), + }); + const { error: lxcError } = useSWR('/api/lxc', fetchData, { + refreshInterval: POLL_INTERVAL, + fallbackData: { data: [] }, + onSuccess: (data) => setLxc(data.data), + }); + const { error: summaryError } = useSWR('/api/status', fetchData, { + refreshInterval: POLL_INTERVAL, + fallbackData: { data: null }, + onSuccess: (data) => setSummary(data.data), + }); const tabs = [ { id: 'nodes', label: 'Nodes' }, diff --git a/src/lib/proxmox.js b/src/lib/proxmox.js index 46c4a02..af09823 100644 --- a/src/lib/proxmox.js +++ b/src/lib/proxmox.js @@ -8,33 +8,13 @@ function getEnv(key) { return val; } -/** - * Generates the Proxmox API2 Authorization header. - * Format: PVEAPIToken=@!= - * @param {string} _method - * @param {string} _path - * @param {string} _body - * @returns {{ Authorization: string }} - */ function createAuthHeaders(_method, _path, _body) { - const TOKEN_ID = getEnv('PROXMOX_TOKEN_ID'); // e.g. "root@pam!monitorapp" + const TOKEN_ID = getEnv('PROXMOX_TOKEN_ID'); const TOKEN_SECRET = getEnv('PROXMOX_TOKEN_SECRET'); return { Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}` }; } -/** - * Fetches the Proxmox API with authentication. - * @param {string} path - * @param {'GET'|'POST'|'PUT'|'DELETE'} [method='GET'] - * @param {string} [body] - * @returns {Promise<*>} - */ async function proxmoxFetch(path, method = 'GET', body) { - const headers = { - ...createAuthHeaders(method, path, body), - 'Content-Type': 'application/json', - }; - const baseUrl = getEnv('PROXMOX_API_URL'); const base = baseUrl.replace(/\/+$/, ''); const cleanPath = path.replace(/^\/+/, ''); @@ -46,18 +26,47 @@ async function proxmoxFetch(path, method = 'GET', body) { const res = await axios({ url: fullUrl, method, - headers, + headers: { + ...createAuthHeaders(method, path, body), + 'Content-Type': 'application/json', + }, data: body, }); - return res.data; } catch (err) { const status = err.response?.status || 'Unknown Status'; const message = err.response?.data?.message || err.response?.data || err.message; throw new Error(`Proxmox API error ${status}: ${message}`); } +} +/** + * Sends a power action to a VM or LXC. + * Uses /api2/extjs endpoint (different from /api2/json used by other routes). + * @param {string} nodeName + * @param {number} vmid + * @param {'qemu'|'lxc'} type + * @param {'start'|'stop'|'restart'} action + * @returns {Promise} + */ +export async function powerControl(nodeName, vmid, type, action) { + const baseUrl = getEnv('PROXMOX_API_URL'); + const extjsUrl = baseUrl.replace('/api2/json', '/api2/extjs'); + const res = await axios({ + url: `${extjsUrl}/nodes/${nodeName}/${type}/${vmid}/status/${action}`, + method: 'POST', + headers: { + ...createAuthHeaders('POST', '', ''), + 'Content-Type': 'application/json', + }, + data: '{}', + }); + + const data = res.data?.data; + if (res.data?.success === 0 || !data) { + throw new Error(res.data?.message || 'Power operation failed'); + } } /** @@ -71,10 +80,6 @@ async function proxmoxFetch(path, method = 'GET', body) { * @property {number} load */ -/** - * Fetches all Proxmox nodes and returns mapped stats. - * @returns {Promise} - */ export async function getNodes() { const data = await proxmoxFetch('/nodes'); return (data.data || []).map((node) => ({ @@ -102,11 +107,6 @@ export async function getNodes() { * @property {{ total: number; used: number }} disk */ -/** - * Fetches QEMU VMs for a given node. - * @param {string} nodeName - * @returns {Promise} - */ export async function getVMs(nodeName) { const data = await proxmoxFetch(`/nodes/${nodeName}/qemu`); return (data.data || []).map((vm) => ({ @@ -126,11 +126,6 @@ export async function getVMs(nodeName) { })); } -/** - * Fetches LXC containers for a given node. - * @param {string} nodeName - * @returns {Promise} - */ export async function getLXC(nodeName) { const data = await proxmoxFetch(`/nodes/${nodeName}/lxc`); return (data.data || []).map((vm) => ({ @@ -159,26 +154,6 @@ export async function getLXC(nodeName) { * @property {{ total: number; used: number }} totalMemory */ -/** - * Aggregated summary across all nodes. - * @returns {Promise} - */ -/** - * Sends a power action to a VM or LXC. - * @param {string} nodeName - * @param {number} vmid - * @param {'qemu'|'lxc'} type - * @param {'start'|'stop'|'restart'} action - * @returns {Promise} - */ -export async function powerControl(nodeName, vmid, type, action) { - await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/${action}`, 'POST'); -} - -/** - * Aggregated summary across all nodes. - * @returns {Promise} - */ export async function getSummary() { const nodes = await getNodes(); let totalVMs = [];