fix(realtime): sync instance/workspace list state without manual refresh

- Invalidate workspace list cache after assigning servers/users so
  server/user counts on workspace cards update immediately.
- Invalidate node/VM caches after workspace server/user assignment so
  the Nodes page reflects visibility changes right away.
- Soft-delete the cached VM/LXC row and broadcast vm_deleted the moment
  a VM/container is destroyed, instead of waiting for the 10s
  background sync_vm_cache tick.
- Invalidate the VM list on the frontend when a deploy task completes,
  since the backend already persists the new instance to cache
  synchronously before marking the task done.
This commit is contained in:
markmorado
2026-07-24 05:39:23 +05:00
parent e10f9a2a03
commit 8b4c8c3a30
3 changed files with 51 additions and 5 deletions

View File

@@ -951,6 +951,13 @@ async def delete_vm(
# Удаляем VM
result = client.delete_vm(node, vmid)
if result:
# Помечаем запись в локальном кэше удалённой сразу, не дожидаясь
# фонового sync_vm_cache (тикает раз в 10с) — иначе VM ещё
# какое-то время висит в списке после реального удаления.
try:
soft_delete_vm_instance(db, server_id, vmid)
except Exception as _sde:
logger.warning(f"Failed to soft-delete cached VM {vmid}: {_sde}")
# Освобождаем IP в IPAM (если есть)
try:
ipam = IPAMService(db)
@@ -1003,6 +1010,11 @@ async def delete_vm(
success=True
)
logger.info(f"User {current_user.username} deleted VM {vmid} on {server.name}")
try:
from ...websocket_manager import broadcast_event
broadcast_event("vm_deleted", server_id=server_id, vmid=vmid, node=node, name=vm_name, vm_type="qemu")
except Exception as _we:
logger.warning(f"Failed to broadcast vm_deleted for VM {vmid}: {_we}")
return JSONResponse(content={
"status": "success",
"message": f"VM {vmid} удалена",
@@ -1312,6 +1324,13 @@ async def delete_container(
# Удаляем контейнер
result = client.delete_container(node, vmid)
if result:
# Помечаем запись в локальном кэше удалённой сразу, не дожидаясь
# фонового sync_vm_cache (тикает раз в 10с) — иначе контейнер ещё
# какое-то время висит в списке после реального удаления.
try:
soft_delete_vm_instance(db, server_id, vmid)
except Exception as _sde:
logger.warning(f"Failed to soft-delete cached container {vmid}: {_sde}")
# Освобождаем IP в IPAM (если есть)
try:
ipam = IPAMService(db)
@@ -1359,6 +1378,11 @@ async def delete_container(
)
logger.info(f"User {current_user.username} deleted container {vmid} on {server.name}")
try:
from ...websocket_manager import broadcast_event
broadcast_event("vm_deleted", server_id=server_id, vmid=vmid, node=node, name=container_name, vm_type="lxc")
except Exception as _we:
logger.warning(f"Failed to broadcast vm_deleted for container {vmid}: {_we}")
return JSONResponse(content={
"status": "success",
"message": f"Контейнер {vmid} удалён",

View File

@@ -1,4 +1,5 @@
import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import {
@@ -80,7 +81,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { StatusDot } from '@/components/shared/status-dot';
import { ColumnFilter, multiSelectFilter } from '@/components/shared/column-filter';
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync } from '@/hooks/use-instances';
import { useVirtualMachines, useBulkOperation, usePowerAction, useVMStatusSync, useInstancesMetricsSync, useBulkTasksSync, vmKeys } from '@/hooks/use-instances';
import { useServers } from '@/hooks/use-nodes';
import { useProfile } from '@/hooks/use-settings';
import { formatBytes, vmTypeLabel, formatUptime, formatVmConfig } from '@/lib/format';
@@ -291,6 +292,7 @@ export default function InstancesPage() {
}, [t]);
// ── Deploy task polling ──────────────────────────────────────────
const qc = useQueryClient();
const { tasks: deployTasks, updateTask, removeTask } = useDeployTasksStore();
const activeTasks = deployTasks.filter((t) => t.status === 'pending' || t.status === 'running');
@@ -318,6 +320,10 @@ export default function InstancesPage() {
{ description: data.error_message, duration: 15000 },
);
}
// Новая VM уже сохранена в кэше бэкендом — подтягиваем список сразу,
// не дожидаясь фонового vm_created от 10-секундного sync_vm_cache.
qc.invalidateQueries({ queryKey: vmKeys.all });
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
setTimeout(() => removeTask(task.id), 5000);
}
} catch { /* ignore */ }

View File

@@ -1,6 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import type { Workspace } from '@/types';
import { nodeKeys } from './use-nodes';
import { vmKeys } from './use-instances';
export const workspaceKeys = {
all: ['workspaces'] as const,
@@ -61,7 +63,14 @@ export function useUpdateWorkspaceServers() {
return useMutation({
mutationFn: ({ id, ids }: { id: number; ids: number[] }) =>
apiClient.post<WorkspaceDetail>(`/api/workspaces/${id}/servers`, { ids }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['workspace'] }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: workspaceKeys.all });
qc.invalidateQueries({ queryKey: ['workspace'] });
qc.invalidateQueries({ queryKey: nodeKeys.servers });
qc.invalidateQueries({ queryKey: nodeKeys.topology });
qc.invalidateQueries({ queryKey: vmKeys.all });
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
},
});
}
@@ -70,6 +79,13 @@ export function useUpdateWorkspaceUsers() {
return useMutation({
mutationFn: ({ id, ids }: { id: number; ids: number[] }) =>
apiClient.post<WorkspaceDetail>(`/api/workspaces/${id}/users`, { ids }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['workspace'] }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: workspaceKeys.all });
qc.invalidateQueries({ queryKey: ['workspace'] });
qc.invalidateQueries({ queryKey: nodeKeys.servers });
qc.invalidateQueries({ queryKey: nodeKeys.topology });
qc.invalidateQueries({ queryKey: vmKeys.all });
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
},
});
}