diff --git a/app/static/js/sync.js b/app/static/js/sync.js index 44c267f..6d5b197 100644 --- a/app/static/js/sync.js +++ b/app/static/js/sync.js @@ -129,15 +129,39 @@ const Sync = { const retryCount = photo.retryCount || 0; try { + // Re-read photo from IndexedDB to get a fresh blob reference + // (iOS Safari can invalidate blob references between reads) + const freshPhoto = await Storage.db.photos.get(photo.id); + if (!freshPhoto || (freshPhoto.status !== 'pending' && freshPhoto.status !== 'uploading')) { + console.log('[SYNC] Photo no longer pending, skipping:', photo.id); + return true; + } + + const blob = freshPhoto.blob; + console.log('[SYNC] Uploading:', photo.filename, - '(' + (photo.blob ? (photo.blob.size / 1024 / 1024).toFixed(1) + 'MB' : 'no blob') + ')', + '(' + (blob ? (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) { + // Validate blob reference exists + if (!blob || !(blob instanceof Blob) || blob.size === 0) { throw new Error('Photo data is missing or corrupted - please delete and re-capture'); } + // Actually try to read the blob to verify data is accessible + // (iOS Safari can evict blob data from IndexedDB while keeping the reference) + const readable = await this.validateBlobReadable(blob); + if (!readable) { + // Blob data is gone — no amount of retrying will fix this + console.error('[SYNC] Blob data is no longer readable:', photo.filename); + await Storage.updatePhoto(photo.id, { + status: 'failed', + lastError: 'Photo data was lost by the browser - please re-capture', + error: 'Photo data was lost by the browser - please re-capture' + }); + return false; + } + await Storage.updatePhoto(photo.id, { status: 'uploading' }); // Prevent page navigation during upload @@ -162,10 +186,8 @@ const Sync = { } } - const uploadBlob = photo.blob; - const formData = new FormData(); - formData.append('file', uploadBlob, photo.filename); + formData.append('file', blob, photo.filename); const uploadUrl = '/api/files/upload?path=' + encodeURIComponent(photo.targetPath); @@ -185,7 +207,10 @@ const Sync = { if (fetchErr.name === 'AbortError') { throw new Error('Upload timed out (' + (this.UPLOAD_TIMEOUT_MS / 1000) + 's)'); } - throw fetchErr; + // Capture detailed error info for debugging + const errDetail = fetchErr.name + ': ' + fetchErr.message; + console.error('[SYNC] Fetch threw:', errDetail); + throw new Error('Network error: ' + errDetail); } clearTimeout(timeoutId); @@ -241,12 +266,13 @@ const Sync = { } catch (error) { this._setUploading(false); - console.error('[SYNC] Upload failed:', error.message, '(attempt', retryCount + 1 + ')'); + const errMsg = (error.name || 'Error') + ': ' + error.message; + console.error('[SYNC] Upload failed:', errMsg, '(attempt', retryCount + 1 + ')'); await Storage.updatePhoto(photo.id, { status: 'pending', retryCount: retryCount + 1, - lastError: error.message, - error: error.message + lastError: errMsg, + error: errMsg }); return false; } @@ -268,6 +294,19 @@ const Sync = { } }, + async validateBlobReadable(blob) { + try { + // Try to read the first byte — if iOS has evicted the data, + // this will throw even though the Blob reference looks valid + const slice = blob.slice(0, 1); + const buf = await slice.arrayBuffer(); + return buf.byteLength > 0; + } catch (e) { + console.error('[SYNC] Blob readability check failed:', e.message); + return false; + } + }, + async checkFileExists(path) { try { const verifyUrl = '/api/files/verify?path=' + encodeURIComponent(path); diff --git a/app/static/sw.js b/app/static/sw.js index 3a7d9aa..f234262 100644 --- a/app/static/sw.js +++ b/app/static/sw.js @@ -1,9 +1,9 @@ // NextSnap Service Worker // Provides offline-first caching for the app shell -const CACHE_VERSION = 'nextsnap-v13'; -const APP_SHELL_CACHE = 'nextsnap-shell-v9'; -const RUNTIME_CACHE = 'nextsnap-runtime-v9'; +const CACHE_VERSION = 'nextsnap-v14'; +const APP_SHELL_CACHE = 'nextsnap-shell-v10'; +const RUNTIME_CACHE = 'nextsnap-runtime-v10'; // Assets to cache on install const APP_SHELL_ASSETS = [