diff --git a/backend/version.py b/backend/version.py index 3e7c20b..cf4db34 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ """Single source of truth for the StackPilot release version.""" -APP_VERSION = "0.37.2" +APP_VERSION = "0.37.3" diff --git a/frontend/package.json b/frontend/package.json index a92c642..aac6442 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.37.2", + "version": "0.37.3", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/Files.tsx b/frontend/src/pages/Files.tsx index 9efc8d2..93496a0 100644 --- a/frontend/src/pages/Files.tsx +++ b/frontend/src/pages/Files.tsx @@ -89,6 +89,7 @@ export function Files() { kind: "upload" | "download"; indeterminate?: boolean; loaded?: number; + detail?: string; } | null>(null); const fileInput = useRef(null); const folderInput = useRef(null); @@ -119,7 +120,12 @@ export function Files() { mutationFn: (file: File) => { setProgress({ label: file.name, pct: 0, kind: "upload" }); return filesApi.upload(path, file, false, "", host, (pct) => - setProgress({ label: file.name, pct, kind: "upload" }) + setProgress({ + label: file.name, + pct, + kind: "upload", + detail: `${formatBytes((pct / 100) * file.size)} / ${formatBytes(file.size)}`, + }) ); }, onSuccess: (r) => { @@ -145,23 +151,30 @@ export function Files() { const uploadFolder = useMutation({ mutationFn: async (files: File[]) => { const total = files.length; + const totalBytes = files.reduce((s, f) => s + f.size, 0); let ok = 0; let failed = 0; + let doneBytes = 0; // bytes of fully-completed files for (let i = 0; i < files.length; i++) { const f = files[i]; const rel = (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name; try { - await filesApi.upload(path, f, true, rel, host, (filePct) => + await filesApi.upload(path, f, true, rel, host, (filePct) => { + const sent = doneBytes + (filePct / 100) * f.size; setProgress({ - label: `${f.name} (${i + 1}/${total})`, - pct: ((i + filePct / 100) / total) * 100, + label: f.name, + // Byte-weighted so the bar tracks real transfer, not file count + // (a folder can mix tiny files with multi-GB ones). + pct: totalBytes ? (sent / totalBytes) * 100 : ((i + filePct / 100) / total) * 100, kind: "upload", - }) - ); + detail: `${i + 1} / ${total} files ยท ${formatBytes(sent)} / ${formatBytes(totalBytes)}`, + }); + }); ok += 1; } catch { failed += 1; } + doneBytes += f.size; } return { ok, failed }; }, @@ -355,6 +368,9 @@ export function Files() { /> )} + {progress.detail && ( +
{progress.detail}
+ )} )}