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
+58
View File
@@ -367,10 +367,14 @@ class SyncManager: ObservableObject {
photo.uploadRetryCount = 0
uploadedPaths[photo.localFilePath] = serverPath
photo.lastUploadError = nil
attachServerPath(serverPath, for: photo,
inspections: allInspections, issues: allIssues)
await pushLateIssuePhotoIfNeeded(serverPath, for: photo,
issues: allIssues, context: context)
await pushLateInspectionPhotoIfNeeded(serverPath, for: photo,
inspections: allInspections,
context: context)
try? context.save()
} catch {
@@ -386,6 +390,9 @@ class SyncManager: ObservableObject {
// photo permanently, silently, with the inspection still
// reported as successfully synced.
photo.uploadRetryCount += 1
// Keep the reason. Without it a lost evidence photo is
// undiagnosable after the fact see PendingPhoto.lastUploadError.
photo.lastUploadError = error.localizedDescription
if photo.uploadRetryCount >= Self.maxPhotoUploadAttempts {
photo.uploadStatus = "failed"
syncError = "A photo failed to upload after "
@@ -403,6 +410,7 @@ class SyncManager: ObservableObject {
dup.serverPath = serverPath
dup.uploadStatus = "uploaded"
dup.uploadRetryCount = 0
dup.lastUploadError = nil
attachServerPath(serverPath, for: dup,
inspections: allInspections, issues: allIssues)
await pushLateIssuePhotoIfNeeded(serverPath, for: dup,
@@ -455,6 +463,56 @@ class SyncManager: ObservableObject {
try? context.save()
}
/// Push a recovered photo onto an inspection that has ALREADY been submitted.
///
/// The inspection counterpart of pushLateIssuePhotoIfNeeded, and the gap
/// that made inspection photo loss permanent:
///
/// 1. the upload fails `maxPhotoUploadAttempts` times -> row goes "failed"
/// 2. "failed" counts as ready, so processInspectionQueue submits anyway
/// and APIClient.submitInspection rewrites the surviving local:// value
/// to "" the field lands BLANK on the server
/// 3. the inspection is marked synced; nothing ever revisits it
///
/// Step 3 was the dead end. "Retry Failed Items" could re-upload the file
/// successfully, but attachServerPath only wrote the path into LOCAL form
/// data, which no longer goes anywhere the inspection was already synced.
/// The photo sat on the device, recoverable in principle and unreachable in
/// practice. This closes the loop by PATCHing the server copy.
///
/// Normal path: processPhotoQueue runs BEFORE processInspectionQueue, so a
/// first-time inspection has no serverId yet and this does nothing the
/// path travels in the submit body as usual. Only a recovery reaches here.
///
/// Best-effort: a failure leaves the photo attached locally and retried on
/// the next pass, exactly like the issue version.
private func pushLateInspectionPhotoIfNeeded(
_ serverPath: String,
for photo: PendingPhoto,
inspections: [LocalInspection],
context: ModelContext
) async {
guard photo.entityType == "inspection",
let fieldId = photo.fieldId
else { return }
let entityId = photo.entityLocalId
guard let inspection = inspections.first(where: { $0.localId == entityId }),
let inspectionServerId = inspection.serverId
else { return }
do {
try await APIClient.shared.updateInspectionFormData(
inspectionId: inspectionServerId,
fields: [fieldId: serverPath]
)
inspection.syncErrorMessage = nil
} catch {
inspection.syncErrorMessage =
"A recovered photo could not be attached: \(error.localizedDescription)"
}
try? context.save()
}
/// Write a freshly uploaded server path onto whichever record owns the photo.
/// Both collections are pre-fetched by the caller see processPhotoQueue.
private func attachServerPath(