diff --git a/app/static/js/sync.js b/app/static/js/sync.js index 06415f1..fd679b9 100644 --- a/app/static/js/sync.js +++ b/app/static/js/sync.js @@ -7,8 +7,11 @@ const Sync = { currentUsername: null, isOnline: navigator.onLine, isSyncing: false, - MAX_RETRIES: 5, + 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], init(username) { this.currentUsername = username; @@ -96,6 +99,19 @@ 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); } }, @@ -104,7 +120,9 @@ const Sync = { const retryCount = photo.retryCount || 0; try { - console.log('[SYNC] Uploading:', photo.filename, '(attempt', retryCount + 1, 'of', this.MAX_RETRIES + ')'); + console.log('[SYNC] Uploading:', photo.filename, + '(' + (photo.blob ? (photo.blob.size / 1024 / 1024).toFixed(1) + 'MB' : 'no blob') + ')', + 'attempt', retryCount + 1, 'of', this.MAX_RETRIES); // Validate blob before attempting upload if (!photo.blob || !(photo.blob instanceof Blob) || photo.blob.size === 0) { @@ -116,7 +134,7 @@ const Sync = { // Prevent page navigation during upload this._setUploading(true); - // Only check for duplicates on retries (skip on first attempt to reduce latency) + // Check for duplicates on retries (skip on first attempt to reduce latency) if (retryCount > 0) { const fullPath = photo.targetPath.replace(/\/$/, '') + '/' + photo.filename; const alreadyExists = await this.checkFileExists(fullPath); @@ -134,23 +152,32 @@ const Sync = { } } - // Resize if over 10MB - let uploadBlob = photo.blob; - if (uploadBlob.size > 10 * 1024 * 1024) { - console.log('[SYNC] Photo too large (' + (uploadBlob.size / 1024 / 1024).toFixed(1) + 'MB), resizing...'); - uploadBlob = await this.resizeImage(uploadBlob, 10 * 1024 * 1024); - console.log('[SYNC] Resized to ' + (uploadBlob.size / 1024 / 1024).toFixed(1) + 'MB'); - } + const uploadBlob = photo.blob; const formData = new FormData(); formData.append('file', uploadBlob, photo.filename); const uploadUrl = '/api/files/upload?path=' + encodeURIComponent(photo.targetPath); - const uploadResponse = await fetch(uploadUrl, { - method: 'POST', - body: formData - }); + // Upload with timeout via AbortController + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.UPLOAD_TIMEOUT_MS); + + let uploadResponse; + try { + uploadResponse = await fetch(uploadUrl, { + method: 'POST', + body: formData, + signal: controller.signal + }); + } catch (fetchErr) { + clearTimeout(timeoutId); + if (fetchErr.name === 'AbortError') { + throw new Error('Upload timed out (' + (this.UPLOAD_TIMEOUT_MS / 1000) + 's)'); + } + throw fetchErr; + } + clearTimeout(timeoutId); if (!uploadResponse.ok) { let errorMsg = 'Upload failed'; @@ -203,7 +230,7 @@ const Sync = { } catch (error) { this._setUploading(false); - console.error('[SYNC] Upload failed:', error, '(attempt', retryCount + 1 + ')'); + console.error('[SYNC] Upload failed:', error.message, '(attempt', retryCount + 1 + ')'); await Storage.updatePhoto(photo.id, { status: 'pending', retryCount: retryCount + 1, @@ -213,55 +240,6 @@ const Sync = { } }, - async resizeImage(blob, maxBytes) { - return new Promise((resolve, reject) => { - const img = new Image(); - const url = URL.createObjectURL(blob); - - img.onload = () => { - URL.revokeObjectURL(url); - let quality = 0.85; - let scale = 1.0; - - const attempt = () => { - const canvas = document.createElement('canvas'); - canvas.width = Math.round(img.width * scale); - canvas.height = Math.round(img.height * scale); - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - canvas.toBlob((result) => { - if (!result) { - reject(new Error('Failed to resize image')); - return; - } - if (result.size <= maxBytes || (quality <= 0.4 && scale <= 0.3)) { - resolve(result); - } else { - // Reduce quality first, then scale down - if (quality > 0.5) { - quality -= 0.1; - } else { - scale *= 0.8; - quality = 0.7; - } - attempt(); - } - }, 'image/jpeg', quality); - }; - - attempt(); - }; - - img.onerror = () => { - URL.revokeObjectURL(url); - reject(new Error('Failed to load image for resize')); - }; - - img.src = url; - }); - }, - async pruneHistory() { // Keep only the last 20 completed (verified/failed) entries per user const completed = await Storage.db.photos