Aug 25 - Fix inspection lost photos
This commit is contained in:
@@ -265,6 +265,51 @@ guard isOnline, let context = modelContext, AuthManager.shared.isAuthenticated e
|
||||
|
||||
7. **`pollNotifications`** — fetches new notifications since `lastNotificationFetch` cursor.
|
||||
|
||||
### Photo loss on inspections — the recovery loop (Aug 2026)
|
||||
|
||||
An inspection can reach the server with its photo fields BLANK while the files
|
||||
sit safely on the device. The chain:
|
||||
|
||||
1. `processPhotoQueue` upload fails → `uploadRetryCount++`; after
|
||||
`maxPhotoUploadAttempts` (5) the row goes `uploadStatus = "failed"`, which is
|
||||
**terminal**.
|
||||
2. `processInspectionQueue` treats `"failed"` as ready — deliberate, so a dead
|
||||
photo cannot block a submission forever — and submits.
|
||||
3. `APIClient.submitInspection` rewrites any surviving `local://` value to `""`,
|
||||
so the field lands **blank on the server**.
|
||||
4. The inspection is marked `synced`. Nothing revisits it.
|
||||
|
||||
**Step 4 was a dead end until this fix.** "Retry Failed Items" resets the photo
|
||||
to `pending` and the re-upload can succeed, but `attachServerPath()` writes the
|
||||
recovered path into **local** form data only — and the inspection is already
|
||||
synced, so nothing carried it across. The photo was recoverable in principle and
|
||||
unreachable in practice, which is what the Photo Diagnostic screen reports as
|
||||
*"LOST ON SERVER … File present at stored path — recoverable"*.
|
||||
|
||||
`pushLateInspectionPhotoIfNeeded()` closes it, mirroring
|
||||
`pushLateIssuePhotoIfNeeded()`:
|
||||
|
||||
| | Issue | Inspection |
|
||||
|---|---|---|
|
||||
| late-attach call | `PATCH /api/v1/issues/<id>/photos` | `PATCH /api/v1/inspections/<id>` with `form_data` |
|
||||
| helper | `pushLateIssuePhotoIfNeeded` | `pushLateInspectionPhotoIfNeeded` |
|
||||
|
||||
Server-side `_merge_form_data` makes this safe: a non-empty incoming value wins,
|
||||
and an existing `uploads/...` path is never blanked by an empty one — so the
|
||||
PATCH is idempotent and cannot erase a good path. **`status` is deliberately not
|
||||
sent**: including it would re-run the draft→completed transition, which is what
|
||||
fulfils a linked schedule.
|
||||
|
||||
Normal path is unaffected — `processPhotoQueue` runs before
|
||||
`processInspectionQueue`, so a first-time inspection has no `serverId` yet and
|
||||
the helper no-ops; only a recovery reaches it.
|
||||
|
||||
`PendingPhoto.lastUploadError` records **why** the last attempt failed. Nothing
|
||||
recorded it before: a photo could burn all five attempts with the reason visible
|
||||
nowhere — the device showed only "failed", and the server logged only
|
||||
*successful* uploads (now fixed: `app/api/photos.py` logs every rejection and any
|
||||
storage-write failure at WARNING/ERROR with the username).
|
||||
|
||||
### Server-pulled issue identification
|
||||
|
||||
Records inserted by `pullAssignedIssues` are identified by: `syncStatus == "synced"` AND `inspectionLocalId == ""`. These are the only records safe to delete during reconciliation.
|
||||
|
||||
Reference in New Issue
Block a user