fix: improve upload error messages with progress and mobile detection (#645)

- show upload progress percentage in error messages (e.g. "failed at 45%")
- detect mobile devices and show targeted guidance for large file failures
- warn mobile users proactively when uploading files >50MB
- provide actionable suggestions (WiFi, desktop browser) instead of generic errors

context: user reported upload failures on mobile with unhelpful error messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub 93d46c29 b7077079

Changed files
+51 -4
frontend
src
lib
routes
upload
+50 -4
frontend/src/lib/uploader.svelte.ts
··· 23 23 onError?: (_error: string) => void; 24 24 } 25 25 26 + function isMobileDevice(): boolean { 27 + if (!browser) return false; 28 + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 29 + } 30 + 31 + const MOBILE_LARGE_FILE_THRESHOLD_MB = 50; 32 + 33 + function buildNetworkErrorMessage(progressPercent: number, fileSizeMB: number, isMobile: boolean): string { 34 + const progressInfo = progressPercent > 0 ? ` (failed at ${progressPercent}%)` : ''; 35 + 36 + if (isMobile && fileSizeMB > MOBILE_LARGE_FILE_THRESHOLD_MB) { 37 + return `upload failed${progressInfo}: large files often fail on mobile networks. try uploading from a desktop or use WiFi`; 38 + } 39 + 40 + if (progressPercent > 0 && progressPercent < 100) { 41 + return `upload failed${progressInfo}: connection was interrupted. check your network and try again`; 42 + } 43 + 44 + return `upload failed${progressInfo}: connection failed. check your internet connection and try again`; 45 + } 46 + 47 + function buildTimeoutErrorMessage(progressPercent: number, fileSizeMB: number, isMobile: boolean): string { 48 + const progressInfo = progressPercent > 0 ? ` (stopped at ${progressPercent}%)` : ''; 49 + 50 + if (isMobile) { 51 + return `upload timed out${progressInfo}: mobile uploads can be slow. try WiFi or a desktop browser`; 52 + } 53 + 54 + if (fileSizeMB > 100) { 55 + return `upload timed out${progressInfo}: large file (${Math.round(fileSizeMB)}MB) - try a faster connection`; 56 + } 57 + 58 + return `upload timed out${progressInfo}: try again with a better connection`; 59 + } 60 + 26 61 // global upload manager using Svelte 5 runes 27 62 class UploaderState { 28 63 activeUploads = $state<Map<string, UploadTask>>(new Map()); ··· 40 75 ): void { 41 76 const taskId = crypto.randomUUID(); 42 77 const fileSizeMB = file.size / 1024 / 1024; 78 + const isMobile = isMobileDevice(); 79 + 80 + // warn about large files on mobile 81 + if (isMobile && fileSizeMB > MOBILE_LARGE_FILE_THRESHOLD_MB) { 82 + toast.info(`uploading ${Math.round(fileSizeMB)}MB file on mobile - ensure stable connection`, 5000); 83 + } 84 + 43 85 const uploadMessage = fileSizeMB > 10 44 86 ? 'uploading track... (large file, this may take a moment)' 45 87 : 'uploading track...'; 46 88 // 0 means infinite/persist until dismissed 47 89 const toastId = toast.info(uploadMessage, 0); 90 + 91 + // track upload progress for error messages 92 + let lastProgressPercent = 0; 48 93 49 94 if (!browser) return; 50 95 const formData = new FormData(); ··· 74 119 xhr.upload.addEventListener('progress', (e) => { 75 120 if (e.lengthComputable && !uploadComplete) { 76 121 const percent = Math.round((e.loaded / e.total) * 100); 122 + lastProgressPercent = percent; 77 123 const progressMsg = `retrieving your file... ${percent}%`; 78 124 toast.update(toastId, progressMsg); 79 125 if (callbacks?.onProgress) { ··· 172 218 errorMsg = error.detail || errorMsg; 173 219 } catch { 174 220 if (xhr.status === 0) { 175 - errorMsg = 'network error: connection failed. check your internet connection and try again'; 221 + errorMsg = buildNetworkErrorMessage(lastProgressPercent, fileSizeMB, isMobile); 176 222 } else if (xhr.status >= 500) { 177 223 errorMsg = 'server error: please try again in a moment'; 178 224 } else if (xhr.status === 413) { 179 225 errorMsg = 'file too large: please use a smaller file'; 180 226 } else if (xhr.status === 408 || xhr.status === 504) { 181 - errorMsg = 'upload timed out: please try again with a better connection'; 227 + errorMsg = buildTimeoutErrorMessage(lastProgressPercent, fileSizeMB, isMobile); 182 228 } 183 229 } 184 230 toast.error(errorMsg); ··· 190 236 191 237 xhr.addEventListener('error', () => { 192 238 toast.dismiss(toastId); 193 - const errorMsg = 'network error: connection failed. check your internet connection and try again'; 239 + const errorMsg = buildNetworkErrorMessage(lastProgressPercent, fileSizeMB, isMobile); 194 240 toast.error(errorMsg); 195 241 if (callbacks?.onError) { 196 242 callbacks.onError(errorMsg); ··· 199 245 200 246 xhr.addEventListener('timeout', () => { 201 247 toast.dismiss(toastId); 202 - const errorMsg = 'upload timed out: please try again with a better connection'; 248 + const errorMsg = buildTimeoutErrorMessage(lastProgressPercent, fileSizeMB, isMobile); 203 249 toast.error(errorMsg); 204 250 if (callbacks?.onError) { 205 251 callbacks.onError(errorMsg);
+1
frontend/src/routes/upload/+page.svelte
··· 379 379 > 380 380 <span>upload track</span> 381 381 </button> 382 + 382 383 </form> 383 384 </main> 384 385 {/if}