From 6eae386b19c566e911a3c461052df08a32a987c9 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 02:44:38 +0200 Subject: [PATCH] feat: live transfer progress on remote-download task cards (#6) The card progress bar froze at 100% once the download finished, giving no signal during the import/transfer phase. Poll the existing /workflow/progress endpoint while a task is in transfer phase and drive both the bar and the status label from the live upload counters. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/component/Pages/Tasks/TaskCard.tsx | 46 +++++++++++++++++-- .../Pages/Tasks/TaskSummaryStatus.tsx | 10 +++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/frontend/src/component/Pages/Tasks/TaskCard.tsx b/frontend/src/component/Pages/Tasks/TaskCard.tsx index 31c9c57d..49d74ac7 100644 --- a/frontend/src/component/Pages/Tasks/TaskCard.tsx +++ b/frontend/src/component/Pages/Tasks/TaskCard.tsx @@ -17,7 +17,8 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; import { FileType } from "../../../api/explorer.ts"; -import { TaskResponse, TaskType } from "../../../api/workflow.ts"; +import { getTasksPhaseProgress } from "../../../api/api.ts"; +import { TaskResponse, TaskStatus, TaskType } from "../../../api/workflow.ts"; import { useAppDispatch } from "../../../redux/hooks.ts"; import { DefaultButton } from "../../Common/StyledComponents.tsx"; import FileIcon from "../../FileManager/Explorer/FileIcon.tsx"; @@ -120,6 +121,42 @@ const TaskCard = ({ loading, showProgress, onLoad, onRetried, task }: TaskCardPr }); const [expanded, setExpanded] = useState(false); + const [transferPercent, setTransferPercent] = useState(undefined); + + // During the transfer phase the summary's download counters are frozen at + // 100%; poll the live phase progress so the card bar keeps moving while + // files are imported to storage. + const transferring = + task?.type === TaskType.remote_download && + task?.status === TaskStatus.processing && + task?.summary?.phase === "transfer"; + + useEffect(() => { + if (!transferring || !task) { + setTransferPercent(undefined); + return; + } + let cancelled = false; + const fetchProgress = () => { + dispatch(getTasksPhaseProgress(task.id)) + .then((res) => { + if (cancelled) { + return; + } + const upload = res?.["upload"]; + if (upload && upload.total > 0) { + setTransferPercent((100 * upload.current) / upload.total); + } + }) + .catch(() => {}); + }; + fetchProgress(); + const iv = setInterval(fetchProgress, 2500); + return () => { + cancelled = true; + clearInterval(iv); + }; + }, [transferring, task?.id]); useEffect(() => { if (!inView) { @@ -152,8 +189,10 @@ const TaskCard = ({ loading, showProgress, onLoad, onRetried, task }: TaskCardPr fullWidth percentage={ showProgress - ? ((task?.summary?.props?.download?.downloaded ?? 0) * 100) / - Math.max(task?.summary?.props?.download?.total ?? 0, 1) + ? transferring && transferPercent != undefined + ? transferPercent + : ((task?.summary?.props?.download?.downloaded ?? 0) * 100) / + Math.max(task?.summary?.props?.download?.total ?? 0, 1) : undefined } startIcon={ @@ -209,6 +248,7 @@ const TaskCard = ({ loading, showProgress, onLoad, onRetried, task }: TaskCardPr status={task.status} error={task.error} summary={task.summary} + transferPercent={transferPercent} /> )} diff --git a/frontend/src/component/Pages/Tasks/TaskSummaryStatus.tsx b/frontend/src/component/Pages/Tasks/TaskSummaryStatus.tsx index 9f86107d..143d8148 100644 --- a/frontend/src/component/Pages/Tasks/TaskSummaryStatus.tsx +++ b/frontend/src/component/Pages/Tasks/TaskSummaryStatus.tsx @@ -27,6 +27,8 @@ export interface TaskSummaryStatusProps { summary?: TaskSummary; error?: string; simplified?: boolean; + // Live transfer-phase progress percent for remote downloads. + transferPercent?: number; } interface TaskStatusContentProps { @@ -46,7 +48,7 @@ const TaskStatusContent = forwardRef(({ icon, title, color, ...props }: TaskStat ); }); -const TaskSummaryStatus = ({ type, status, summary, error, simplified }: TaskSummaryStatusProps) => { +const TaskSummaryStatus = ({ type, status, summary, error, simplified, transferPercent }: TaskSummaryStatusProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const theme = useTheme(); @@ -111,7 +113,11 @@ const TaskSummaryStatus = ({ type, status, summary, error, simplified }: TaskSum } else if (summary?.phase == "transfer") { return ( } color={theme.palette.primary.main} />