From 23da2fab55b3972582281a7577ebea8ee843dfb3 Mon Sep 17 00:00:00 2001 From: dv Date: Thu, 30 Apr 2026 16:31:33 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B8=D1=81=D0=BA=D0=B0=D0=B2=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/objects.py | 38 ++++++++++++++++++++++++++--- backend/app/schemas/object.py | 7 ++++++ frontend/src/api/objects.ts | 16 ++++++++++++ frontend/src/pages/ObjectDetail.tsx | 30 ++++++++++++++++++++--- 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/backend/app/routers/objects.py b/backend/app/routers/objects.py index 9b4d384..fcfb469 100644 --- a/backend/app/routers/objects.py +++ b/backend/app/routers/objects.py @@ -26,6 +26,7 @@ from app.schemas.object import ( ObjectUpdate, SyncResult, DiscoveryStarted, + DiscoveryStatus, ) from app.services import crypto from app.services.database import AsyncSessionLocal @@ -36,6 +37,22 @@ from app.services.sse import sse_manager, SSEEvent router = APIRouter(prefix="/api/v1/objects", tags=["objects"]) +_discovery_tasks: dict[int, asyncio.Task] = {} + + +def _discovery_task_id(obj_id: int) -> str: + return f"disc-{obj_id}" + + +def _is_discovery_running(obj_id: int) -> bool: + task = _discovery_tasks.get(obj_id) + return task is not None and not task.done() + + +def _forget_discovery_task(obj_id: int, task: asyncio.Task) -> None: + if _discovery_tasks.get(obj_id) is task: + _discovery_tasks.pop(obj_id, None) + def _apply_credentials(obj: Object, db_password: str | None, ssh_password: str | None) -> None: if db_password is not None: @@ -300,9 +317,24 @@ async def discover_devices( ): """SSH into each rack PC, parse caps config, discover network devices.""" await _load_object(db, obj_id) - task_id = f"disc-{obj_id}" - asyncio.create_task(_run_discovery_background(obj_id, task_id)) - return DiscoveryStarted(task_id=task_id) + task_id = _discovery_task_id(obj_id) + if _is_discovery_running(obj_id): + return DiscoveryStarted(task_id=task_id, running=True, already_running=True) + + task = asyncio.create_task(_run_discovery_background(obj_id, task_id)) + _discovery_tasks[obj_id] = task + task.add_done_callback(lambda done_task: _forget_discovery_task(obj_id, done_task)) + return DiscoveryStarted(task_id=task_id, running=True, already_running=False) + + +@router.get("/{obj_id}/discover/status", response_model=DiscoveryStatus) +async def discovery_status( + obj_id: int, + db: AsyncSession = Depends(get_db), + _: User = Depends(get_current_user), +): + await _load_object(db, obj_id) + return DiscoveryStatus(task_id=_discovery_task_id(obj_id), running=_is_discovery_running(obj_id)) @router.post("/import/preview", response_model=ObjectImportPreview) diff --git a/backend/app/schemas/object.py b/backend/app/schemas/object.py index 8a4d0c7..343068d 100644 --- a/backend/app/schemas/object.py +++ b/backend/app/schemas/object.py @@ -145,3 +145,10 @@ class SyncResult(BaseModel): class DiscoveryStarted(BaseModel): task_id: str + running: bool = True + already_running: bool = False + + +class DiscoveryStatus(BaseModel): + task_id: str + running: bool diff --git a/frontend/src/api/objects.ts b/frontend/src/api/objects.ts index 1c159a8..1c4d715 100644 --- a/frontend/src/api/objects.ts +++ b/frontend/src/api/objects.ts @@ -250,6 +250,13 @@ export const useSyncObject = (objId: number) => { export interface DiscoveryStarted { task_id: string + running: boolean + already_running: boolean +} + +export interface DiscoveryStatus { + task_id: string + running: boolean } export const useDiscoverObject = (objId: number) => @@ -258,6 +265,15 @@ export const useDiscoverObject = (objId: number) => api.post(`/api/v1/objects/${objId}/discover`).then((r) => r.data), }) +export const useDiscoveryStatus = (objId: number) => + useQuery({ + queryKey: ['discovery-status', objId], + queryFn: () => + api.get(`/api/v1/objects/${objId}/discover/status`).then((r) => r.data), + enabled: !!objId, + refetchInterval: (query) => (query.state.data?.running ? 2_000 : false), + }) + export interface SheetsSyncPayload { spreadsheet_id: string sheet_name?: string diff --git a/frontend/src/pages/ObjectDetail.tsx b/frontend/src/pages/ObjectDetail.tsx index 7e8c2a6..2dfe379 100644 --- a/frontend/src/pages/ObjectDetail.tsx +++ b/frontend/src/pages/ObjectDetail.tsx @@ -38,6 +38,7 @@ import { useDeleteDevice, useDeleteAllDevices, useDiscoverObject, + useDiscoveryStatus, useDevices, useObject, type DeviceItem, @@ -148,6 +149,7 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?: const { data: actions } = useActions() const discoverMutation = useDiscoverObject(objId) + const discoveryStatus = useDiscoveryStatus(objId) const deleteDevice = useDeleteDevice(objId) const createRack = useCreateRack(objId) const updateRack = useUpdateRack(objId) @@ -173,12 +175,31 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?: const [editRackForm] = Form.useForm() const currentAction = actions?.find((a) => a.name === selectedAction) + const discoveryRunning = discoverMutation.isPending || !!discoveryTaskId || !!discoveryStatus.data?.running + + useEffect(() => { + if (discoveryStatus.data?.running) { + setDiscoveryTaskId(discoveryStatus.data.task_id) + } else if (discoveryStatus.data && discoveryTaskId === discoveryStatus.data.task_id) { + setDiscoveryTaskId(null) + } + }, [discoveryStatus.data, discoveryTaskId]) const handleDiscover = async () => { + if (discoveryRunning) { + setDiscoveryTaskId(discoveryTaskId ?? discoveryStatus.data?.task_id ?? `disc-${objId}`) + setDiscoveryOpen(true) + return + } + try { - const { task_id } = await discoverMutation.mutateAsync() + const { task_id, already_running } = await discoverMutation.mutateAsync() setDiscoveryTaskId(task_id) setDiscoveryOpen(true) + queryClient.invalidateQueries({ queryKey: ['discovery-status', objId] }) + if (already_running) { + message.info('SSH discovery уже выполняется') + } } catch { message.error('Ошибка SSH discovery') } @@ -186,6 +207,8 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?: const handleDiscoveryComplete = () => { queryClient.invalidateQueries({ queryKey: ['devices', objId] }) + queryClient.invalidateQueries({ queryKey: ['discovery-status', objId] }) + setDiscoveryTaskId(null) } const handleDeleteDevice = async (row: DeviceItem) => { @@ -630,9 +653,10 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?: {mode === 'inventory' ? ( - + {discoveryRunning && выполняется}