From bc748d05ac4a3115d3f376eb9a0b584619bc6a8e Mon Sep 17 00:00:00 2001 From: kamaji Date: Sat, 7 Feb 2026 15:48:44 -0600 Subject: [PATCH] Fix uploads stuck pending due to page navigation killing sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync engine was adding per-photo backoff delays (2-60s) inside the processing loop, during which the user would navigate to another page (browser -> queue -> browser), killing the sync mid-wait. The upload POST never fired because the delay ran out the clock. Changes: - Remove per-photo backoff delay from processQueue loop — uploads start immediately on page load without blocking - Move retry scheduling to after the full queue pass: if any uploads failed, schedule a new triggerSync() after 15s instead of blocking inline - Keep the duplicate check on retries (fast, catches interrupted uploads that actually succeeded server-side) - uploadPhoto now returns true/false so processQueue can track failures Co-Authored-By: Claude Opus 4.6 --- app/static/js/sync.js | 53 ++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/app/static/js/sync.js b/app/static/js/sync.js index fd679b9..44c267f 100644 --- a/app/static/js/sync.js +++ b/app/static/js/sync.js @@ -10,8 +10,7 @@ const Sync = { MAX_RETRIES: 15, VERIFY_DELAY_MS: 800, UPLOAD_TIMEOUT_MS: 120000, - // Backoff delays in ms per retry attempt - BACKOFF_DELAYS: [0, 2000, 5000, 10000, 15000, 30000, 30000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000], + _retryTimerId: null, init(username) { this.currentUsername = username; @@ -34,6 +33,10 @@ const Sync = { console.log('[SYNC] Network offline'); this.isOnline = false; this.isSyncing = false; + if (this._retryTimerId) { + clearTimeout(this._retryTimerId); + this._retryTimerId = null; + } }); }, @@ -63,14 +66,26 @@ const Sync = { console.log('[SYNC] Starting sync...'); this.isSyncing = true; + let hadFailures = false; try { - await this.processQueue(); + hadFailures = await this.processQueue(); } catch (error) { console.error('[SYNC] Error:', error); + hadFailures = true; } finally { this.isSyncing = false; this._setUploading(false); } + + // If there were failures, schedule a retry cycle after a delay + // so it doesn't block the current page load + if (hadFailures && this.isOnline) { + console.log('[SYNC] Scheduling retry in 15s...'); + this._retryTimerId = setTimeout(() => { + this._retryTimerId = null; + this.triggerSync(); + }, 15000); + } }, async processQueue() { @@ -82,9 +97,11 @@ const Sync = { console.log('[SYNC] Found', pendingPhotos.length, 'photos to process'); if (pendingPhotos.length === 0) { - return; + return false; } + let hadFailures = false; + for (const photo of pendingPhotos) { if (!this.isOnline) { console.log('[SYNC] Lost connection, stopping'); @@ -99,21 +116,13 @@ const Sync = { continue; } - // Backoff delay before retry attempts - if (retryCount > 0) { - const backoff = this.BACKOFF_DELAYS[Math.min(retryCount, this.BACKOFF_DELAYS.length - 1)]; - console.log('[SYNC] Waiting', backoff + 'ms before retry', retryCount + 1); - await this.delay(backoff); - - // Re-check connectivity after waiting - if (!this.isOnline) { - console.log('[SYNC] Lost connection during backoff, stopping'); - break; - } - } - - await this.uploadPhoto(photo); + // No delay — upload immediately. Retries are handled by + // scheduling a new triggerSync() after the full queue pass. + const success = await this.uploadPhoto(photo); + if (!success) hadFailures = true; } + + return hadFailures; }, async uploadPhoto(photo) { @@ -134,7 +143,8 @@ const Sync = { // Prevent page navigation during upload this._setUploading(true); - // Check for duplicates on retries (skip on first attempt to reduce latency) + // On retries, first check if a previous attempt actually succeeded + // (the upload may have completed but the verify/status update was interrupted) if (retryCount > 0) { const fullPath = photo.targetPath.replace(/\/$/, '') + '/' + photo.filename; const alreadyExists = await this.checkFileExists(fullPath); @@ -148,7 +158,7 @@ const Sync = { }); await this.pruneHistory(); this._setUploading(false); - return; + return true; } } @@ -227,6 +237,7 @@ const Sync = { // Clear navigation guard after successful upload this._setUploading(false); + return true; } catch (error) { this._setUploading(false); @@ -237,6 +248,7 @@ const Sync = { lastError: error.message, error: error.message }); + return false; } }, @@ -264,7 +276,6 @@ const Sync = { const result = await response.json(); return result.exists === true; } catch (e) { - // If we can't check, assume it doesn't exist and proceed with upload return false; } },