fix: rewrite power action handler — instant status feedback + reliable state

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 19:49:40 +01:00
parent 59bb0f24db
commit c07e448d40

View File

@@ -31,58 +31,59 @@ 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) => {
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';
// Optimistic UI update — change happens instantly setPowerActionStatus({ text: `${label}ing ${kind} ${vmid}...`, type: 'pending' });
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 { try {
await axios.post('/api/power', { node, vmid, type, action }); 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
// Clear optimistic after successful API call — next refetch will reconcile const [nodesRes, vmsRes, lxcRes, statusRes] = await Promise.all([
setTimeout(() => { axios.get('/api/nodes'),
setVmsOptimistic(null); axios.get('/api/vms'),
setLxcOptimistic(null); axios.get('/api/lxc'),
}, 5000); 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) { } catch (err) {
// Revert optimistic update on failure setPowerActionStatus({ text: err.message || `${label} failed`, type: 'error' });
setVmsOptimistic(null);
setLxcOptimistic(null);
setPowerActionStatus({ text: err.message || `${action.charAt(0).toUpperCase() + action.slice(1)} failed`, type: 'error' });
} }
}; };
const { data: nodesData, error: nodesError } = useSWR('/api/nodes', fetchData, { const [nodes, setNodes] = useState([]);
refreshInterval: POLL_INTERVAL, const [vms, setVms] = useState([]);
}); const [lxc, setLxc] = useState([]);
const { data: vmsData, error: vmsError } = useSWR('/api/vms', fetchData, { const [summary, setSummary] = useState(null);
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,
});
// Use optimistic data when available, fall back to SWR data const { error: nodesError } = useSWR('/api/nodes', fetchData, {
const nodes = nodesData?.data ?? []; refreshInterval: POLL_INTERVAL,
const vms = vmsOptimistic ?? (vmsData?.data ?? []); fallbackData: { data: [] },
const lxc = lxcOptimistic ?? (lxcData?.data ?? []); onSuccess: (data) => setNodes(data.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' },