fix(upload): fix upload progress counter showing done count always as 0

Replace the fragile global DOM query approach in `updateOverallStatus`
with per-batch closure-based counters inside `processFiles`.

- Add `total`, `done`, `updateStatus()`, and `markDone()` as closure
  variables/functions within each `processFiles` invocation
- Change `_uploadSingleFile` to accept an `onTerminal` callback instead
  of `statusMessage`, called when a file reaches a terminal state
- Pass `markDone` as the `onTerminal` callback from `scheduleNext`
- Remove the now-unused global `updateOverallStatus` function

The previous implementation queried `document.querySelectorAll('.file-status')`
globally and relied on text `startsWith` checks to count completed files.
This was fragile and could produce a stale done=0 count in practice.
The new approach uses deterministic closure counters, so the displayed
"Uploading files (X/N)" count correctly increments as files complete.

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-02 18:44:05 +00:00
parent 1f0331117a
commit 3be1fa5040
+28 -42
View File
@@ -274,6 +274,25 @@ function processFiles(files, progressContainer, statusMessage) {
statusMessage.textContent = `Queued ${fileArray.length} file(s) for upload…`;
}
// Per-batch counters tracked in closure variables (avoids fragile DOM queries).
const total = fileArray.length;
let done = 0;
function updateStatus() {
if (!statusMessage) return;
if (done === total) {
statusMessage.textContent = `All uploads completed (${done}/${total})`;
window.dispatchEvent(new CustomEvent('allUploadsComplete', { detail: { total, completed: done } }));
} else {
statusMessage.textContent = `Uploading files (${done}/${total})`;
}
}
function markDone() {
done++;
updateStatus();
}
// Pre-create one progress row per file.
const queueItems = fileArray.map((file) => {
const row = document.createElement('div');
@@ -319,7 +338,7 @@ function processFiles(files, progressContainer, statusMessage) {
item.statusEl.textContent = 'Unsupported file type';
item.statusEl.className = 'text-xs text-red-500 mt-1';
active--;
updateOverallStatus(statusMessage);
markDone();
// No HTTP request skip straight to next without adding delay.
scheduleNext();
continue;
@@ -330,12 +349,12 @@ function processFiles(files, progressContainer, statusMessage) {
item.statusEl.textContent = 'Exceeds 500 MB limit';
item.statusEl.className = 'text-xs text-red-500 mt-1';
active--;
updateOverallStatus(statusMessage);
markDone();
scheduleNext();
continue;
}
_uploadSingleFile(item.file, item.progressBar, item.statusEl, statusMessage)
_uploadSingleFile(item.file, item.progressBar, item.statusEl, markDone)
.then((result) => {
active--;
if (result.rateLimited) {
@@ -348,7 +367,7 @@ function processFiles(files, progressContainer, statusMessage) {
item.progressBar.className = 'file-progress-bar bg-red-500 h-2 rounded-full';
item.statusEl.textContent = 'Failed: rate limit retries exhausted';
item.statusEl.className = 'text-xs text-red-500 mt-1';
updateOverallStatus(statusMessage);
markDone();
}
// Resume after the backoff window.
const wait = Math.max(_adaptiveState.pauseUntil - Date.now() + 50, 0);
@@ -383,10 +402,10 @@ function _isAcceptedFile(file) {
* @param {File} file
* @param {HTMLElement} progressBar
* @param {HTMLElement} statusEl
* @param {HTMLElement} statusMessage
* @param {Function} onTerminal - Called when the file reaches a terminal state (success or permanent error).
* @returns {Promise<{rateLimited: boolean, retryAfterSeconds: number}>}
*/
function _uploadSingleFile(file, progressBar, statusEl, statusMessage) {
function _uploadSingleFile(file, progressBar, statusEl, onTerminal) {
statusEl.textContent = 'Uploading…';
statusEl.className = 'text-xs text-gray-600 mt-1';
progressBar.style.width = '0%';
@@ -418,7 +437,7 @@ function _uploadSingleFile(file, progressBar, statusEl, statusMessage) {
statusEl.textContent = `Success: Task ID: ${result.task_id}`;
statusEl.className = 'text-xs text-green-600 mt-1';
_onUploadSuccess();
updateOverallStatus(statusMessage);
onTerminal();
resolve({ rateLimited: false, retryAfterSeconds: 0 });
} else if (xhr.status === 429) {
@@ -438,7 +457,7 @@ function _uploadSingleFile(file, progressBar, statusEl, statusMessage) {
progressBar.className = 'file-progress-bar bg-red-500 h-2 rounded-full';
statusEl.textContent = `Error: HTTP ${xhr.status}`;
statusEl.className = 'text-xs text-red-500 mt-1';
updateOverallStatus(statusMessage);
onTerminal();
resolve({ rateLimited: false, retryAfterSeconds: 0 });
}
};
@@ -447,7 +466,7 @@ function _uploadSingleFile(file, progressBar, statusEl, statusMessage) {
progressBar.className = 'file-progress-bar bg-red-500 h-2 rounded-full';
statusEl.textContent = 'Error: Network error';
statusEl.className = 'text-xs text-red-500 mt-1';
updateOverallStatus(statusMessage);
onTerminal();
resolve({ rateLimited: false, retryAfterSeconds: 0 });
};
@@ -467,39 +486,6 @@ function validateAndUpload(file, progressContainer, statusMessage) {
processFiles([file], progressContainer, statusMessage);
}
// ── Status helpers ────────────────────────────────────────────────────────────
/**
* Re-calculate and display the overall upload status.
* Fires 'allUploadsComplete' when every item has a terminal status.
* @param {HTMLElement} statusMessage
*/
function updateOverallStatus(statusMessage) {
if (!statusMessage) return;
const fileStatuses = document.querySelectorAll('.file-status');
let done = 0;
const total = fileStatuses.length;
fileStatuses.forEach((s) => {
const t = s.textContent;
if (
t.startsWith('Success') ||
t.startsWith('Error') ||
t.startsWith('Unsupported') ||
t.startsWith('Exceeds') ||
t.startsWith('Failed:')
) done++;
});
if (done === total && total > 0) {
statusMessage.textContent = `All uploads completed (${done}/${total})`;
window.dispatchEvent(new CustomEvent('allUploadsComplete', { detail: { total, completed: done } }));
} else {
statusMessage.textContent = `Uploading files (${done}/${total})`;
}
}
/**
* Format a byte count for human-readable display.
* @param {number} bytes