From d459f3c5248e0e04d2dd01ffbe69376d057aa7c1 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 14:49:15 +0100 Subject: [PATCH] feat: B4 - progressbar --- .../ae8c717c-02a9-4179-b7f1-307675d64ee3.zip | Bin 0 -> 34381 bytes webui/backend/data/tasks.db | Bin 212992 -> 221184 bytes .../test_api_tasks_golden.cpython-313.pyc | Bin 11835 -> 13763 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 40925 -> 41518 bytes .../tests/golden/test_api_tasks_golden.py | 45 ++++++++ .../tests/golden/test_ui_smoke_golden.py | 24 ++-- webui/html/app.js | 106 ++++++++++++------ 7 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 webui/backend/data/archive_tmp/ae8c717c-02a9-4179-b7f1-307675d64ee3.zip 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 0000000000000000000000000000000000000000..ceeb345c37319e84f48aaff001d2d796e22c3ec6 GIT binary patch literal 34381 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 W0p6^@mLdZK4-i%|GcfFH0`UL`(tpnY literal 0 HcmV?d00001 diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 6918d0192394bf7e3ca9a99a3055fc5fc0a866ae..59d91599b16250b4aff1720baa353830f53d88b6 100644 GIT binary patch delta 2554 zcma)8eQZ*i zHkt*=|GbZT&OPUM&iS2t?!Z83;B;tzjqm~?#Qsin6}<5LL*G}VkTSs+h&3k9TSS$K z6Z4~o6t-;8cjA>u)BZ41`aP+woK=o0{U`K!-3l~dWDkw|_;M7tU4{mUe?=Nt2gOr= zqcKXZ{PeW%H2b1ZgW>?BD1SE%z)C8tDeWA9Cw=XnY9{Yy9o@;QS~8PTP2EYUolcjo z8k%D}R?ar_d4mjlnq8*V)n#^es=2JjHBH-8GZ{0h>UJ*Q>Dc@;ZIo?U|m7z`Tr?JxVKfzmaEuBx+k-rmY61EBV2r=JP-&x;}d^>&ZzPJy? zDX63DcNm+3Hp-sCZ%x6iI6egj@#F=FACy@P4@|=@&-@{79^rn^yxXaFT9(c6X-P{p zc?4=I@4Bj)NoBK!mU3-ZhZ;}Q%DJhOmFG%!KChZ7{^-nWS=G?9PBx!&+@#qBi_4H7 z?e?;Htdu$r{ZKk`8M1fDkHc~T8ux)~P?(yQ3YX(W(x)aNhL7(Q5_>C!$rZ;7-PfP_=wG)m9W$4lRRj{Zud!aLmRn*u&M zK^JJ{G9)b9GI;*TaFnf{T@9_#a1&0-uw5t3Nz8YRG}p>>D+xan+9W>}oC^HGUoS<) zF5xxbFW5NU%B{CkvtSo$<&L^rA}xgtymhTg@S_rBs$iI|k;LX8i{pL={CMbH`dOSG zU=8?{Mv!o24C;0*k1o@-=)--R^Ru{9X4PD4g+QUk!?Wb!!HkEel6J`T^>6I$%j7oa zGr32$^lbckZ~noqp5FW#Oi$CJyRK1nmb)^2xx2;JyQ`$9Zs@LeOJ{y-ceI{6G8>|5 zbe(3b(UQ1nnm%zq2T|^dSs==5LyHR497J`bWyo0dWy`AMoVo@AH~QJ_ycU|_mTNH< zy&z!Z68(Z#idO8MrggX@!0xOq1K{NOKLC8K)FraRl0?JXiLxTRoik%&I5F5n#g4)1 z7Hf4yocKnB-5RS%^VtZibv*JUw)bqh9*)_l--s6p+tzeDnZ^|nHYri%I8okFj`Iog zJw>f&9Z-p5dI#(9qb9M~L5=yikuya3Ksm#WysXqMViwGWh2Fv}KmIntJ|leZE{tK|qaXBuUWz6t!VZ}9mDTeRz1wq*Hw zjJ%{?zOEe$bBbJDOB%C_UfyNfrfWIr;=(~;At&FE!DEnscogJ8e2RlLHyD;43Gsmm zxEUXM5f;xyJrBL)k<+X7SaWgtT-@|J^!np#Rv=}Ot2vHt%?@Nnq?=mGNe6iL6VfPA z-d3Jfb}MN`Q^fGO@X7FKI1_fl;n4e`pN9^FdP1v0H_0E$zm%Vp`{XrpZSZ0|_-gQQ z@SDNAgSQ2)1kMD05ZDn|7l;SI|F(bJzuW&czwQ^MbJEYGQC{~uB&^v>%-&4s^uv>ZG2`xuBg$uBv$aD@H_HwT&mDbQvBQ=D)`uqu<8WTAwm;vC9QLeDWQ@t`u8}Lk#-@nQpxi94K%T zEV)8tJe@zmf4!IYVu@TwY8Z;BBn(S4)Ps)c+K61IJ?$GVVDIMsUD+Gxo^acUCd>yF=oh*^igG|{z$)| z??Z=PrspYZm3keVL(Z$p8E3>9R99i4I_xZ@(_yxQ+y~en%CEt>^m>F@RMW%~)Dyy4 z<1>7HC$v-*s`R81eRf+jy9cCiLO&G`FpEh)s6ZUX*~DOa-p^yRcVA3rKhTzHfi4S) zFfoMlXjc%ubfxCM<68s-^)lAb5 zWi?jN<{Df(Za%P4n3vkABP?qsUkMiocfEkd?fKUoe3V5(j2t$%Nl+4V3X!6-u i?x1`Pt(P!hECAwWmnQb z{0t2Z_#LCYV=x&tGGbyiVlkOCGGQ`nWX5FC$b!kLkrh*mMlG0Z8rd+lYSfC!u8}?I zfR3z=4!cX==uf9u6VA{?QQ+V<+73kN0R|<1>a<{buRbi*G;6Mj9acq71o z*0@e|m(!=1KAH@GqI^`A3fm@@+^Sfdjb`$TN-i&EqEMP&kcz65fsb%e#n|vG*u%#} z1R~@bs9*^*NU(S42ZOF!8)6?>O zN%-|iEM8D?3zlp z&7b+lAb>C&aH3d4c?0eW1N`r^^aV z4qb08;?j+QT2E=J`y?>G!6lXEIZ;;NTeKSGz!utwrrXAKfL-VI_(++L)F|SrDHi!+ F-k%}zG*|!t delta 402 zcmX?{y*q~QGcPX}0}#ZdY|XUR*vR*cQR5SkJCz}Zp@=beW>DKVBzVgy38zjgN3Kvx5>BF{{u4vixk)t0C=HYhX4Qo 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 56b1a57f57aa0fd58bdc90a216787baa4d196881..8ffe3018d5e1e1cade5d6ebd198ca0a06b4b29cb 100644 GIT binary patch delta 1167 zcmZuuT};zZ6yEK^sIsn8p$ru)>n59lp#%+Tq7oGX7(;}{=u}p=yV^Kf>^jsbAwH@B ze@>zz{_x0?iD^g-J}8L~>XRf(^u_q%v+*zTrnYw*psq>pcg}aebG~!${qUT6{F3r~ zXPDIv>)Vo?RgCNt58X<+8%r z54v0J0_a)qC8$|wcWUaX^T38S4*dd8o8bH{I^oOlAhK7%iAW|msXQX5x#!qb1T)b^ z(4rj7o(O>y4Il<$(IE1Wl_jeeZk!0BHFoS=E0&RF>k3#Yx=3CnmJ{SenqalK%0}da z7bkqR$VH$cm&W@ck2>Z`T>x}B8jfjy@L!fYxq>=MHl7%vQ0axT$hrS;$) zs4ru!$ZthKR@hG27wmBG4%rb3mW zPKDAW4rhf@!u?uCHTv;+BV<@*;cU31D5L9GHM$Hu^q;i(ALm|OlXaX<_hLP6)fCz` zF13YuESTR0e^6uAmM&vrzKi#_=ZEJL+Br3)NeN!ZXEQi$;DkS0oj-!@Z7Gi|Zv^AT z@h$$Sq!@}S^GQuFUIlL-%AXZVIc;9H%B$A><02(4__GZ#ovaP4n1x?ySsdDK03VZ_ z`!52?Eo1E~o5ngMC502a_`pT1>yaVrnj{(XmoAk!ev(N2qp|Eh9UB>4-;2#nha? ySB=VVYSZU)^j7#hy=9qi_%j| zLQ*SAtQ3?2ic$*_i!$@l6;krc^K$YNQ}p!ol&X}AOA<>;iwP)7&M(cwG^i@G0B)F4 zl?Jk+sLXH6|73}Y{F8Ug z1oDAm$7k?^6izmqz`uF_L{)B%3t}28)UI$CPS%|u0aCqLc&-?u*o6EA6<0V6ZgBAQ z^LFxH5K~*h0a9ZyPhxZUyhtX-^vxR=crzNsu`{|d&fvH%p?Oh4^9uu*`@xByfrY1| b>N2zB4Hlku-zMKy{}0RzEK)^cz!U)h>6I87 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,