Compare commits
12 Commits
feature/vm
...
59bb0f24db
| Author | SHA1 | Date | |
|---|---|---|---|
| 59bb0f24db | |||
| ad328efa3c | |||
| 33d2f6e42a | |||
| 2ab78012ba | |||
| 607844f184 | |||
| e79221642b | |||
| f27bc3e4eb | |||
| 77b067ea35 | |||
| 6b61c3240a | |||
| 51753269ca | |||
| 47f10cc2c2 | |||
| 783400884a |
@@ -5,5 +5,5 @@ PROXMOX_API_URL=http://proxmox.lan:8006/api2/json
|
||||
PROXMOX_WEB_URL=http://proxmox.lan:8006
|
||||
PROXMOX_CONSOLE_TOKEN=root@pam!monitorapp
|
||||
PROXMOX_TOKEN_ID=root@pam!monitorapp
|
||||
PROXMOX_TOKEN_SECRET=<your-token-secret>
|
||||
PROXMOX_TOKEN_SECRET=103d622c-05a3-4002-9356-f67691e10d40
|
||||
API_POLL_INTERVAL=30000
|
||||
|
||||
@@ -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;
|
||||
@@ -34,19 +31,36 @@ export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState('nodes');
|
||||
const [activeConsole, setActiveConsole] = useState(null);
|
||||
const [powerActionStatus, setPowerActionStatus] = useState(null);
|
||||
// Optimistic local state — updated before API response
|
||||
const [vmsOptimistic, setVmsOptimistic] = useState(null);
|
||||
const [lxcOptimistic, setLxcOptimistic] = 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' });
|
||||
|
||||
// Optimistic UI update — change happens instantly
|
||||
if (type === 'qemu') {
|
||||
setVmsOptimistic((prev) =>
|
||||
(prev ?? []).map((vm) => vm.vmid === vmid ? { ...vm, status: action === 'stop' || action === 'restart' ? 'stopped' : 'running' } : vm),
|
||||
);
|
||||
} else {
|
||||
setLxcOptimistic((prev) =>
|
||||
(prev ?? []).map((lxc) => lxc.vmid === vmid ? { ...lxc, status: action === 'stop' || action === 'restart' ? 'stopped' : 'running' } : lxc),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post('/api/power', { node, vmid, type, action });
|
||||
setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' });
|
||||
// Clear optimistic after successful API call — next refetch will reconcile
|
||||
setTimeout(() => {
|
||||
setVmsOptimistic(null);
|
||||
setLxcOptimistic(null);
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
// Revert optimistic update on failure
|
||||
setVmsOptimistic(null);
|
||||
setLxcOptimistic(null);
|
||||
setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
|
||||
}
|
||||
};
|
||||
@@ -64,9 +78,10 @@ export default function Dashboard() {
|
||||
refreshInterval: POLL_INTERVAL,
|
||||
});
|
||||
|
||||
// Use optimistic data when available, fall back to SWR data
|
||||
const nodes = nodesData?.data ?? [];
|
||||
const vms = vmsData?.data ?? [];
|
||||
const lxc = lxcData?.data ?? [];
|
||||
const vms = vmsOptimistic ?? (vmsData?.data ?? []);
|
||||
const lxc = lxcOptimistic ?? (lxcData?.data ?? []);
|
||||
const summary = summaryData?.data ?? null;
|
||||
|
||||
const tabs = [
|
||||
|
||||
@@ -8,33 +8,13 @@ function getEnv(key) {
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Proxmox API2 Authorization header.
|
||||
* Format: PVEAPIToken=<user>@<realm>!<token-id>=<secret>
|
||||
* @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<void>}
|
||||
*/
|
||||
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<ProxmoxNode[]>}
|
||||
*/
|
||||
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<ProxmoxVM[]>}
|
||||
*/
|
||||
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<ProxmoxVM[]>}
|
||||
*/
|
||||
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<SummaryStat>}
|
||||
*/
|
||||
/**
|
||||
* 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<void>}
|
||||
*/
|
||||
export async function powerControl(nodeName, vmid, type, action) {
|
||||
await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/${action}`, 'POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated summary across all nodes.
|
||||
* @returns {Promise<SummaryStat>}
|
||||
*/
|
||||
export async function getSummary() {
|
||||
const nodes = await getNodes();
|
||||
let totalVMs = [];
|
||||
|
||||
Reference in New Issue
Block a user