diff --git a/app/page.tsx b/app/page.tsx index 71005da..24c38d8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -284,6 +284,15 @@ const loadItems = useCallback(async (showLoading = true) => { // visual record. We do this AFTER the status flip so a // failure here doesn't strand the item in `assigned` — // the collection is the more important state change. + // + // The item is cleared off the list either way. An earlier + // version returned early on upload failure, which left a + // successfully-collected item sitting on screen still looking + // un-collected; the driver re-collected it, re-attempted the + // same oversized upload, and one record picked up 29 failed + // attempts in a single morning on 2026-10-12. + let photoFailed = false; + let photoFailure = ''; if (photo) { try { await uploadOrderItemAttachment({ @@ -293,29 +302,37 @@ const loadItems = useCallback(async (showLoading = true) => { notes: notes || undefined, }); } catch (uploadErr) { - // Status already changed. Surface the failure so the - // driver knows the photo did NOT persist (a success - // toast here would be a lie). Known PB bug: image/* mime - // detection rejects camera JPEGs — until that server - // side detection is fixed, photos will keep failing. + // The collection itself succeeded, so this is a warning, + // not a failure — but the driver must still know the photo + // did NOT persist. A success toast here would be a lie. + // + // (The "Known PB bug: image/* mime detection rejects camera + // JPEGs" note that used to live here was wrong: PB's own + // logs show every attempt failing as `400 unexpected EOF` + // — the multipart body truncated in transit, not a format + // rejection. `order_item_attachements` has mimeTypes=null, + // so nothing was checking the type at all. Fixed by + // compressing the photo client-side in lib/image.ts.) + photoFailed = true; + photoFailure = uploadErr instanceof Error ? uploadErr.message : 'unknown error'; console.error('Photo upload failed:', uploadErr); - const reason = - uploadErr instanceof Error ? uploadErr.message : 'unknown error'; - showToast( - `Item collected — but photo upload failed (${reason})`, - 'warning' - ); - return; // skip the success toast that follows } } setItems((prev) => prev.filter((i) => i.id !== item.id)); setShowCollectModal(false); - showToast( - photo - ? 'Collection completed · photo uploaded' - : 'Collection completed successfully' - ); + if (photoFailed) { + showToast( + `Collected, but the photo didn't upload (${photoFailure}) — add it from the office.`, + 'warning', + ); + } else { + showToast( + photo + ? 'Collection completed · photo uploaded' + : 'Collection completed successfully', + ); + } } catch (err) { showToast('Failed to update collection', 'error'); } @@ -336,6 +353,21 @@ const loadItems = useCallback(async (showLoading = true) => { // rather than just disappearing. showToast('Problem reported — item sent back to the pool', 'warning'); } catch (err) { + // PB puts the actual reason in `response.data` keyed by field + // name — the top-level `message` is always the useless + // "Something went wrong while processing your request." A 400 here + // is almost always a select-field value the schema doesn't allow + // (this cost us every problem report on 2026-10-14: problem_type + // was missing out_of_stock/partial_stock/quality_issue/wrong_item). + // Logging only the message hid that for months. + const detail = (err as { response?: { data?: Record } }) + ?.response?.data; + console.error( + 'Failed to report problem:', + item.id, + problemType, + detail ? JSON.stringify(detail) : err, + ); showToast('Failed to report problem', 'error'); } };