12 Commits

Author SHA1 Message Date
59bb0f24db fix: instant power action refresh with optimistic UI update
Update VM/LXC cards immediately on click instead of waiting for
API response or polling interval. On failure, revert the optimistic
update. Background polling reconciles after 5 seconds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 19:18:35 +01:00
ad328efa3c fix: power control 502 error and add instant refresh
- Power operations use /api2/extjs endpoint (not /api2/json)
- Remove unprivileged parameter that causes 400 validation error
- Refetch VM/LXC/node/status after successful power action via SWR mutate

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:56:29 +01:00
33d2f6e42a feat: merge power control and console link into main
- Start/stop/restart buttons for VMs and LXCs
- Console link using /vncproxy endpoint
- Various fixes to get console working
2026-05-10 17:49:30 +01:00
2ab78012ba fix: use /vncproxy endpoint with websocket:0 parameter
The /vncproxy endpoint requires websocket: 0 (not -1) in the POST body.
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 18:44:13 +01:00
607844f184 fix: use correct Proxmox console URL format from docs
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 11:38:46 +01:00
e79221642b update: mark console link as done in backlog
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 10:38:31 +01:00
f27bc3e4eb fix: use correct Proxmox console URL path (/pve/console.html)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 10:31:53 +01:00
77b067ea35 feat: add console link next to VM/LXC names
Clicking the name opens the Proxmox web console in a new tab.
URLs are constructed in the API routes using the configured PROXMOX_API_URL.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 10:22:29 +01:00
6b61c3240a fix: restore broken getSummary function declaration
Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 09:07:55 +01:00
51753269ca feat: add start/stop/restart buttons for VMs and LXCs
- New POST /api/power route for Proxmox power operations
- powerControl() method in proxmox.js library
- Start/Stop/Restart action buttons in VMList and LXCList tables
- Status feedback banner in Dashboard on power action

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-07 08:07:38 +01:00
47f10cc2c2 Merge branch 'main' of https://gitea.rdbcloud.co.uk/robbond/proxmox-monitor 2026-05-07 07:19:10 +01:00
783400884a initial commit 2026-05-07 06:41:36 +01:00
2 changed files with 43 additions and 45 deletions

View File

@@ -5,5 +5,5 @@ PROXMOX_API_URL=http://proxmox.lan:8006/api2/json
PROXMOX_WEB_URL=http://proxmox.lan:8006 PROXMOX_WEB_URL=http://proxmox.lan:8006
PROXMOX_CONSOLE_TOKEN=root@pam!monitorapp PROXMOX_CONSOLE_TOKEN=root@pam!monitorapp
PROXMOX_TOKEN_ID=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 API_POLL_INTERVAL=30000

View File

@@ -31,60 +31,58 @@ export default function Dashboard() {
const [activeTab, setActiveTab] = useState('nodes'); const [activeTab, setActiveTab] = useState('nodes');
const [activeConsole, setActiveConsole] = useState(null); const [activeConsole, setActiveConsole] = useState(null);
const [powerActionStatus, setPowerActionStatus] = useState(null); const [powerActionStatus, setPowerActionStatus] = useState(null);
// Optimistic local state — updated before API response
const [vmsOptimistic, setVmsOptimistic] = useState(null);
const [lxcOptimistic, setLxcOptimistic] = useState(null);
const handlePowerAction = async (node, vmid, type, action) => { const handlePowerAction = async (node, vmid, type, action) => {
const label = action.charAt(0).toUpperCase() + action.slice(1); setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)}ing ${type === 'qemu' ? 'VM' : 'LXC'} ${vmid}...`, type: 'pending' });
const kind = type === 'qemu' ? 'VM' : 'LXC';
setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' }); // Optimistic UI update — change happens instantly
// Flush the pending status so it paints before the API call if (type === 'qemu') {
await new Promise((r) => requestAnimationFrame(r)); 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 { try {
await axios.post('/api/power', { node, vmid, type, action }); await axios.post('/api/power', { node, vmid, type, action });
// Re-fetch after success setPowerActionStatus({ text: `${action.charAt(0).toUpperCase() + action.slice(1)} successful`, type: 'success' });
const [nodesRes, vmsRes, lxcRes, statusRes] = await Promise.all([ // Clear optimistic after successful API call — next refetch will reconcile
axios.get('/api/nodes'), setTimeout(() => {
axios.get('/api/vms'), setVmsOptimistic(null);
axios.get('/api/lxc'), setLxcOptimistic(null);
axios.get('/api/status'), }, 5000);
]);
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) { } catch (err) {
setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' }); // Revert optimistic update on failure
setVmsOptimistic(null);
setLxcOptimistic(null);
setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
} }
}; };
const [nodes, setNodes] = useState([]); const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, {
const [vms, setVms] = useState([]); refreshInterval: POLL_INTERVAL,
const [lxc, setLxc] = useState([]); });
const [summary, setSummary] = useState(null); 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 { error: nodesError } = useSWR('/api/nodes', fetchData, { // Use optimistic data when available, fall back to SWR data
refreshInterval: POLL_INTERVAL, const nodes = nodesData?.data ?? [];
fallbackData: { data: [] }, const vms = vmsOptimistic ?? (vmsData?.data ?? []);
onSuccess: (data) => setNodes(data.data), const lxc = lxcOptimistic ?? (lxcData?.data ?? []);
}); const summary = summaryData?.data ?? null;
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 = [ const tabs = [
{ id: 'nodes', label: 'Nodes' }, { id: 'nodes', label: 'Nodes' },