From 3440c450895af4914bf0fc20e881f89cb38804f5 Mon Sep 17 00:00:00 2001 From: dv Date: Thu, 28 May 2026 17:37:42 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/tasks.ts | 3 +- frontend/src/pages/TaskDetail.tsx | 47 ++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts index 66acf03..e10d62c 100644 --- a/frontend/src/api/tasks.ts +++ b/frontend/src/api/tasks.ts @@ -74,11 +74,12 @@ export const useTasks = (page = 1, pageSize = 50) => refetchInterval: 5000, }) -export const useTask = (id: string | undefined) => +export const useTask = (id: string | undefined, options?: { refetchInterval?: number | false }) => useQuery({ queryKey: ['tasks', id], queryFn: () => api.get(`/api/v1/tasks/${id}`).then((r) => r.data), enabled: !!id, + refetchInterval: options?.refetchInterval, }) // Subscribe to SSE for a running task — merges results in cache, invalidates on completion. diff --git a/frontend/src/pages/TaskDetail.tsx b/frontend/src/pages/TaskDetail.tsx index 8862a32..971129d 100644 --- a/frontend/src/pages/TaskDetail.tsx +++ b/frontend/src/pages/TaskDetail.tsx @@ -78,10 +78,17 @@ function groupBy(items: T[], key: (item: T) => string): Record { export function TaskDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() - const { data: task, isLoading } = useTask(id) + const [isActive, setIsActive] = useState(true) + const { data: task, isLoading } = useTask(id, { refetchInterval: isActive ? 3000 : false }) const cancelTask = useCancelTask() const createTask = useCreateTask() + useEffect(() => { + if (task) { + setIsActive(task.status === 'pending' || task.status === 'running') + } + }, [task?.status]) + const [log, setLog] = useState([]) const [liveResults, setLiveResults] = useState>({}) const [totalDevices, setTotalDevices] = useState(null) @@ -124,13 +131,40 @@ export function TaskDetailPage() { // Merge DB results with live SSE updates const mergedResults = useMemo<(TaskResult & Partial)[]>(() => { - if (!task?.device_results) return [] - return task.device_results.map((r) => { + const dbResults = task?.device_results ?? [] + const merged = dbResults.map((r) => { const live = liveResults[r.device_id] return live ? { ...r, ...live } : r }) + const dbDeviceIds = new Set(dbResults.map((r) => r.device_id)) + for (const [deviceId, live] of Object.entries(liveResults)) { + const id = Number(deviceId) + if (!dbDeviceIds.has(id)) { + merged.push({ + id: 0, + device_id: id, + device_ip: live.device_ip, + device_hostname: live.device_hostname, + object_id: null, + object_name: null, + rack_id: null, + rack_name: null, + status: live.status, + stdout: live.stdout, + stderr: live.stderr, + exit_code: null, + data: null, + duration_ms: live.duration_ms, + executed_at: null, + }) + } + } + return merged }, [task?.device_results, liveResults]) + // Fallback: derive totalDevices from task.result or device_results count + const effectiveTotal = totalDevices ?? task?.result?.total ?? (mergedResults.length > 0 ? mergedResults.length : null) + // Determine if results span multiple objects const multiObject = useMemo(() => { const objIds = new Set(mergedResults.map((r) => r.object_id).filter(Boolean)) @@ -186,7 +220,6 @@ export function TaskDetailPage() { if (isLoading) return - const isActive = task?.status === 'pending' || task?.status === 'running' const canCancel = isActive && !task?.cancel_requested const resultColumns = [ @@ -347,13 +380,13 @@ export function TaskDetailPage() { - {totalDevices != null && totalDevices > 0 && ( + {effectiveTotal != null && effectiveTotal > 0 && ( {(() => { const completed = mergedResults.filter((r) => r.status !== 'running').length const failed = mergedResults.filter((r) => r.status === 'failed').length - const percent = Math.round((completed / totalDevices) * 100) + const percent = Math.round((completed / effectiveTotal) * 100) const isDone = !isActive const progressStatus = isDone ? (failed > 0 ? 'exception' : 'success') @@ -362,7 +395,7 @@ export function TaskDetailPage() { `${completed} / ${totalDevices}`} + format={() => `${completed} / ${effectiveTotal}`} strokeColor={failed > 0 ? undefined : '#52c41a'} /> )