From c07e448d40f5847edd4a2ff9cfc1f5834bc69fbc Mon Sep 17 00:00:00 2001 From: Rob Bond Date: Sun, 10 May 2026 19:49:40 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20rewrite=20power=20action=20handler=20?= =?UTF-8?q?=E2=80=94=20instant=20status=20feedback=20+=20reliable=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status message always shows immediately on click (pending -> success/error) - Auto-dismiss after 3 seconds on success - Use fallbackData to prevent lists going empty during fetch - Direct state sync on success to keep summary counts in sync Co-Authored-By: Claude Opus 4.7 --- src/components/Dashboard.js | 85 +++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js index ee32bc2..8e17920 100644 --- a/src/components/Dashboard.js +++ b/src/components/Dashboard.js @@ -31,58 +31,59 @@ 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); 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), - ); - } + const label = action.charAt(0).toUpperCase() + action.slice(1); + const kind = type === 'qemu' ? 'VM' : 'LXC'; + setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' }); 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); + // 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' }); + // Update local state so summary counts stay in sync until poll + 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) { - // Revert optimistic update on failure - setVmsOptimistic(null); - setLxcOptimistic(null); - 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); - // Use optimistic data when available, fall back to SWR data - const nodes = nodesData?.data ?? []; - const vms = vmsOptimistic ?? (vmsData?.data ?? []); - const lxc = lxcOptimistic ?? (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' },