diff --git a/webui/backend/data/archive_tmp/ae8c717c-02a9-4179-b7f1-307675d64ee3.zip b/webui/backend/data/archive_tmp/ae8c717c-02a9-4179-b7f1-307675d64ee3.zip new file mode 100644 index 0000000..ceeb345 Binary files /dev/null and b/webui/backend/data/archive_tmp/ae8c717c-02a9-4179-b7f1-307675d64ee3.zip differ diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 6918d01..59d9159 100644 Binary files a/webui/backend/data/tasks.db and b/webui/backend/data/tasks.db differ diff --git a/webui/backend/tests/golden/__pycache__/test_api_tasks_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_api_tasks_golden.cpython-313.pyc index d2e3f20..c7c1256 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_api_tasks_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_api_tasks_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 56b1a57..8ffe301 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_api_tasks_golden.py b/webui/backend/tests/golden/test_api_tasks_golden.py index bd14b72..ccaa171 100644 --- a/webui/backend/tests/golden/test_api_tasks_golden.py +++ b/webui/backend/tests/golden/test_api_tasks_golden.py @@ -263,6 +263,51 @@ class TasksApiGoldenTest(unittest.TestCase): self.assertEqual(body["status"], "ready") self.assertEqual(body["destination"], "docs.zip") + def test_get_task_detail_requested_archive_download(self) -> None: + self._insert_task( + task_id="task-download-requested", + operation="download", + status="requested", + source="storage1/docs", + destination="docs.zip", + created_at="2026-03-10T10:00:00Z", + done_items=0, + total_items=1, + ) + + response = self._get("/api/tasks/task-download-requested") + + self.assertEqual(response.status_code, 200) + body = response.json() + self.assertEqual(body["operation"], "download") + self.assertEqual(body["status"], "requested") + self.assertEqual(body["done_items"], 0) + self.assertEqual(body["total_items"], 1) + + def test_get_task_detail_preparing_archive_download_with_current_item(self) -> None: + self._insert_task( + task_id="task-download-preparing", + operation="download", + status="preparing", + source="storage1/docs", + destination="docs.zip", + created_at="2026-03-10T10:00:00Z", + started_at="2026-03-10T10:00:01Z", + done_items=1, + total_items=3, + current_item="storage1/docs/b.txt", + ) + + response = self._get("/api/tasks/task-download-preparing") + + self.assertEqual(response.status_code, 200) + body = response.json() + self.assertEqual(body["operation"], "download") + self.assertEqual(body["status"], "preparing") + self.assertEqual(body["done_items"], 1) + self.assertEqual(body["total_items"], 3) + self.assertEqual(body["current_item"], "storage1/docs/b.txt") + def test_get_task_detail_cancelled_archive_download(self) -> None: self._insert_task( task_id="task-download-cancelled", diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index 9e25d32..61533ed 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -229,6 +229,10 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('function closeFeedbackModal()', app_js) self.assertIn('function downloadModalElements()', app_js) self.assertIn('function isZipDownloadSelection(items)', app_js) + self.assertIn('function archiveTaskStatusLabel(status)', app_js) + self.assertIn('function archiveTaskCountText(task)', app_js) + self.assertIn('function archiveTaskCurrentItemText(task)', app_js) + self.assertIn('function archiveTaskProgressPercent(task)', app_js) self.assertIn('function openZipDownloadModal(selectedItems)', app_js) self.assertIn('function markZipDownloadReady(fileName)', app_js) self.assertIn('function markZipDownloadFailed(err)', app_js) @@ -248,16 +252,18 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('async function downloadFileRequest(paths)', app_js) self.assertIn('const zipDownload = isZipDownloadSelection(selectedItems);', app_js) self.assertIn('openZipDownloadModal(selectedItems);', app_js) - self.assertIn('targetText: "Preparing download..."', app_js) - self.assertIn('statusText: "Preparing download..."', app_js) - self.assertIn('countText: "Preparing zip download"', app_js) - self.assertIn('countText: "Zip preflight and packaging"', app_js) - self.assertIn('statusText: "Download started"', app_js) - self.assertIn('countText: "Browser download started"', app_js) - self.assertIn('countText: "Zip download failed"', app_js) - self.assertIn('countText: "Zip download cancelled"', app_js) + self.assertIn('targetText: "Archive download requested"', app_js) + self.assertIn('statusText: "Requested"', app_js) + self.assertIn('countText: "Waiting for archive task"', app_js) + self.assertIn('targetText: "Archive download task"', app_js) + self.assertIn('statusText: "Ready"', app_js) + self.assertIn('countText: "Browser download requested"', app_js) + self.assertIn('countText: "Archive task failed"', app_js) + self.assertIn('countText: "Archive task cancelled"', app_js) self.assertIn('statusText: "Cancelling download..."', app_js) - self.assertIn('statusText: err.message || "Download failed"', app_js) + self.assertIn('statusText: `Failed: ${err.message || "Archive download failed"}`', app_js) + self.assertIn('return `${task.done_items}/${task.total_items} top-level items`;', app_js) + self.assertIn('return `Current: ${task.current_item}`;', app_js) self.assertIn('downloadProgressState.requestKey === requestKey', app_js) self.assertIn('setStatus("Preparing download...");', app_js) self.assertIn('"/api/files/download/archive-prepare"', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 68638a8..587cfc8 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -381,6 +381,59 @@ function selectedItemCountLabel(totalItems) { return `${totalItems} selected item${totalItems === 1 ? "" : "s"}`; } +function archiveTaskStatusLabel(status) { + switch (status) { + case "requested": + return "Requested"; + case "preparing": + return "Preparing"; + case "ready": + return "Ready"; + case "failed": + return "Failed"; + case "cancelled": + return "Cancelled"; + default: + return "Preparing"; + } +} + +function archiveTaskCountText(task) { + if (typeof task.done_items === "number" && typeof task.total_items === "number") { + return `${task.done_items}/${task.total_items} top-level items`; + } + if (typeof task.total_items === "number") { + return `0/${task.total_items} top-level items`; + } + if (task.status === "requested") { + return "Waiting for archive worker"; + } + return "Preparing archive"; +} + +function archiveTaskCurrentItemText(task) { + if (task.current_item) { + return `Current: ${task.current_item}`; + } + if (task.status === "requested") { + return `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`; + } + if (task.status === "ready") { + return `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`; + } + return `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`; +} + +function archiveTaskProgressPercent(task) { + if (task.status === "ready") { + return 100; + } + if (typeof task.done_items === "number" && typeof task.total_items === "number" && task.total_items > 0) { + return Math.max(0, Math.min(100, Math.round((task.done_items / task.total_items) * 100))); + } + return 0; +} + function isDownloadModalOpen() { return !downloadModalElements().overlay.classList.contains("hidden"); } @@ -420,29 +473,14 @@ function openZipDownloadModal(selectedItems) { setDownloadModalVisible(true); updateDownloadModalDisplay({ active: true, - targetText: "Preparing download...", + targetText: "Archive download requested", currentFileText: `Selection: ${selectedItemCountLabel(selectedItems.length)}`, - countText: "Preparing zip download", - statusText: "Preparing download...", - percent: 20, + countText: "Waiting for archive task", + statusText: "Requested", + percent: 0, cancelVisible: true, cancelDisabled: true, }); - requestAnimationFrame(() => { - if (!downloadProgressState.active) { - return; - } - updateDownloadModalDisplay({ - active: true, - targetText: "Preparing download...", - currentFileText: `Packaging ${selectedItemCountLabel(downloadProgressState.totalItems)}`, - countText: "Zip preflight and packaging", - statusText: "Preparing download...", - percent: 55, - cancelVisible: true, - cancelDisabled: !downloadProgressState.taskId || downloadProgressState.cancelRequested, - }); - }); } function markZipDownloadReady(fileName) { @@ -451,10 +489,10 @@ function markZipDownloadReady(fileName) { downloadProgressState.archiveLabel = fileName || "ZIP archive"; updateDownloadModalDisplay({ active: false, - targetText: `Download started: ${downloadProgressState.archiveLabel}`, + targetText: `Archive ready: ${downloadProgressState.archiveLabel}`, currentFileText: `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`, - countText: "Browser download started", - statusText: "Download started", + countText: "Browser download requested", + statusText: "Ready", percent: 100, cancelVisible: false, }); @@ -466,10 +504,10 @@ function markZipDownloadFailed(err) { downloadProgressState.cancelRequested = false; updateDownloadModalDisplay({ active: false, - targetText: "Preparing download...", + targetText: "Archive prepare failed", currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`, - countText: "Zip download failed", - statusText: err.message || "Download failed", + countText: "Archive task failed", + statusText: `Failed: ${err.message || "Archive download failed"}`, percent: 0, cancelVisible: false, }); @@ -480,10 +518,10 @@ function markZipDownloadCancelled() { downloadProgressState.cancelRequested = false; updateDownloadModalDisplay({ active: false, - targetText: "Download cancelled", + targetText: "Archive prepare cancelled", currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`, - countText: "Zip download cancelled", - statusText: "Download cancelled", + countText: "Archive task cancelled", + statusText: "Cancelled", percent: 0, cancelVisible: false, }); @@ -495,11 +533,11 @@ function updateZipDownloadTaskProgress(task) { } updateDownloadModalDisplay({ active: true, - targetText: "Preparing download...", - currentFileText: task.current_item ? `Current: ${task.current_item}` : `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`, - countText: task.total_items ? `${task.done_items || 0}/${task.total_items} top-level items` : "Preparing zip download", - statusText: downloadProgressState.cancelRequested ? "Cancelling download..." : task.status === "ready" ? "Download started" : "Preparing download...", - percent: task.status === "ready" ? 100 : 55, + targetText: "Archive download task", + currentFileText: archiveTaskCurrentItemText(task), + countText: archiveTaskCountText(task), + statusText: downloadProgressState.cancelRequested ? "Cancelling download..." : archiveTaskStatusLabel(task.status), + percent: archiveTaskProgressPercent(task), cancelVisible: true, cancelDisabled: !downloadProgressState.taskId || downloadProgressState.cancelRequested, }); @@ -720,7 +758,7 @@ async function startDownloadSelected() { const created = await createArchiveDownloadTask(selectedPaths); downloadProgressState.taskId = created.task_id; updateZipDownloadTaskProgress({ - status: "preparing", + status: created.status || "requested", current_item: null, done_items: 0, total_items: selectedItems.length,