feat: real-time server CRUD sync and pve CLI auto-install
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Backend: - servers.py: instant connectivity check + VM cache sync after create/update; broadcast server_added/updated/deleted events via WebSocket - websocket_manager: add broadcast_event() helper for global fire-and-forget events Frontend: - use-nodes: invalidate topology, VM list and resources after server create/update/delete - use-realtime-sync: new hook with useGlobalRealtimeSync subscribing to server_* WS events and invalidating relevant React Query caches - AppLayout: mount useGlobalRealtimeSync globally Deploy: - deploy.sh: auto-install pve CLI during main/quick deploy (idempotent, --auto mode)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import type { ProxmoxServer, ProxmoxNode, ProxmoxServerCreate } from '@/types';
|
||||
import { vmKeys } from './use-instances';
|
||||
|
||||
export const nodeKeys = {
|
||||
servers: ['servers'] as const,
|
||||
@@ -76,7 +77,14 @@ export function useCreateServer() {
|
||||
return useMutation({
|
||||
mutationFn: (data: ProxmoxServerCreate) =>
|
||||
apiClient.post<ProxmoxServer>('/proxmox/api/servers', data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: nodeKeys.servers }),
|
||||
onSuccess: () => {
|
||||
// Немедленно подтягиваем список серверов, VM и топологию кластера,
|
||||
// чтобы UI показал новый сервер со статусом и его инстансами без рефреша.
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +93,12 @@ export function useUpdateServer() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: number } & Partial<ProxmoxServerCreate>) =>
|
||||
apiClient.put<ProxmoxServer>(`/proxmox/api/servers/${id}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: nodeKeys.servers }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,7 +106,12 @@ export function useDeleteServer() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => apiClient.delete(`/proxmox/api/servers/${id}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: nodeKeys.servers }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
82
frontend/src/hooks/use-realtime-sync.ts
Normal file
82
frontend/src/hooks/use-realtime-sync.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getTasksWebSocket } from '@/lib/websocket';
|
||||
import { vmKeys } from './use-instances';
|
||||
import { nodeKeys } from './use-nodes';
|
||||
|
||||
/**
|
||||
* Глобальная подписка на real-time события WebSocket, которая инвалидирует
|
||||
* соответствующие React Query кэши.
|
||||
*
|
||||
* Покрывает события:
|
||||
* - vm_status_update / vm_created / vm_deleted — изменения VM/LXC.
|
||||
* - server_added / server_updated / server_deleted — изменения списка
|
||||
* Proxmox-серверов (в т.ч. их статус is_online).
|
||||
*
|
||||
* Подключается один раз в AppLayout — действует на всех страницах, чтобы
|
||||
* данные обновлялись мгновенно без обновления страницы.
|
||||
*/
|
||||
export function useGlobalRealtimeSync() {
|
||||
const qc = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const ws = getTasksWebSocket();
|
||||
ws.connect();
|
||||
|
||||
// ── VM events ──────────────────────────────────────────────────────────
|
||||
const unsubVmStatus = ws.onType('vm_status_update', (data: unknown) => {
|
||||
const msg = data as { vmid: number; server_id: number; status: string };
|
||||
qc.setQueryData<Array<Record<string, unknown>>>(vmKeys.all, (old) =>
|
||||
old?.map((vm) =>
|
||||
vm.server_id === msg.server_id && vm.vmid === msg.vmid
|
||||
? { ...vm, status: msg.status }
|
||||
: vm
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const unsubVmDeleted = ws.onType('vm_deleted', (data: unknown) => {
|
||||
const msg = data as { vmid: number; server_id: number };
|
||||
qc.setQueryData<Array<Record<string, unknown>>>(vmKeys.all, (old) =>
|
||||
old?.filter((vm) => !(vm.server_id === msg.server_id && vm.vmid === msg.vmid))
|
||||
);
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
});
|
||||
|
||||
const unsubVmCreated = ws.onType('vm_created', () => {
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
});
|
||||
|
||||
// ── Server events ──────────────────────────────────────────────────────
|
||||
const invalidateServerCaches = () => {
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.servers });
|
||||
qc.invalidateQueries({ queryKey: nodeKeys.topology });
|
||||
// Список VM/LXC зависит от набора серверов и их online-статуса
|
||||
qc.invalidateQueries({ queryKey: vmKeys.all });
|
||||
qc.invalidateQueries({ queryKey: vmKeys.resourcesAll });
|
||||
};
|
||||
|
||||
const unsubServerAdded = ws.onType('server_added', invalidateServerCaches);
|
||||
const unsubServerUpdated = ws.onType('server_updated', invalidateServerCaches);
|
||||
const unsubServerDeleted = ws.onType('server_deleted', (data: unknown) => {
|
||||
const msg = data as { server_id?: number };
|
||||
// Локально удаляем VM удалённого сервера, чтобы UI отреагировал мгновенно
|
||||
if (msg?.server_id != null) {
|
||||
qc.setQueryData<Array<Record<string, unknown>>>(vmKeys.all, (old) =>
|
||||
old?.filter((vm) => vm.server_id !== msg.server_id)
|
||||
);
|
||||
}
|
||||
invalidateServerCaches();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubVmStatus();
|
||||
unsubVmDeleted();
|
||||
unsubVmCreated();
|
||||
unsubServerAdded();
|
||||
unsubServerUpdated();
|
||||
unsubServerDeleted();
|
||||
};
|
||||
}, [qc]);
|
||||
}
|
||||
Reference in New Issue
Block a user