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>
This commit is contained in:
2026-05-10 19:18:35 +01:00
parent 175beae735
commit bfc6bf8526

View File

@@ -1,12 +1,11 @@
'use client';
import { useState } from 'react';
import useSWR, { mutate } from 'swr';
import useSWR from 'swr';
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,24 +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 });
// Immediately refetch all data instead of waiting for polling interval
await mutate('/api/nodes');
await mutate('/api/vms');
await mutate('/api/lxc');
await mutate('/api/status');
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' });
}
};
@@ -69,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 = [