Aug 25 - Fix inspection lost photos

This commit is contained in:
2026-08-25 09:40:20 -04:00
parent e31cd7e1ff
commit 3f30e2c7bd
4 changed files with 138 additions and 0 deletions
+45
View File
@@ -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.