05/16 Fix bugs 3

This commit is contained in:
Nguyen Ngo
2026-05-16 14:56:11 -04:00
parent 9c7aa72ff3
commit f436ac2ef2
3 changed files with 93 additions and 45 deletions
+11 -6
View File
@@ -2,7 +2,7 @@
> **Audience:** AI assistants and developers working on the JanitorialQC iPad app.
> **Purpose:** Authoritative reference for architecture, conventions, constraints, and decisions.
> **Last reviewed:** May 2026 (Phase C complete — notification polling, assigned-issue sync, issue photo display, FlagIssueView confirmation banner, re-inspection pre-fill fix)
> **Last reviewed:** May 2026 (Phase 18 complete — static DateFormatter, uploadPhoto retry guard, explicit LocalIssue relationship inverse, APIUser.fullName/displayName, build fixes: SyncManager.shared restored, _RefreshEnvelope nonisolated init)
> **Companion:** See the web backend's `CLAUDE.md` for API contract, server-side rules, and migration chain.
---
@@ -293,6 +293,7 @@ The `isAuthenticated` guard is critical. `NWPathMonitor` fires immediately on co
- `resetNotificationPoller()` cancels task + clears `lastNotificationFetch`. Call on logout.
- **`Timer.scheduledTimer` is banned for periodic work in SyncManager.** Timer requires `RunLoop.main` to be ticking; inside a Swift Concurrency `Task { @MainActor }` block `RunLoop.current``RunLoop.main` — the timer fires silently never. Always use `Task.sleep`.
- **`UNUserNotificationCenterDelegate` is required for foreground delivery.** Without it, iOS silently drops local notifications when the app is active. `NotificationDelegate.shared` is set as `UNUserNotificationCenter.current().delegate` in `JanitorialQCApp.init()`. Its `willPresent` returns `[.banner, .sound]`.
- **`Self.isoFormatter` for all date parsing.** `SyncManager.isoFormatter` is a `nonisolated static let DateFormatter` with `locale = Locale(identifier: "en_US_POSIX")` and `dateFormat = "yyyy-MM-dd'T'HH:mm:ss"`. Used by both `pollNotifications()` and `pullAssignedIssues()`. **Never allocate a `DateFormatter` per call or per loop iteration** — it is expensive. The `en_US_POSIX` locale is mandatory for fixed-format parsing; without it, the system locale can reinterpret the format string unpredictably.
### Fetch pattern — CRITICAL for Xcode 26
@@ -346,15 +347,15 @@ request(endpoint, method, body, retrying) async throws -> T
| Method | Endpoint | Notes |
|---|---|---|
| `request<T>` | Any | Generic GET/POST |
| `request<T>` | Any | Generic GET/POST; `retrying: Bool` prevents double-refresh loop |
| `post<T>` | Any | POST convenience |
| `uploadPhoto` | `POST /api/v1/photos/upload` | Multipart form-data, manual boundary |
| `uploadPhoto` | `POST /api/v1/photos/upload` | Multipart form-data, manual boundary; `retrying: Bool = false` matches `request()` retry pattern |
| `submitInspection` | `POST /api/v1/inspections` | Sanitises `local://` photo paths before send |
| `submitIssue` | `POST /api/v1/issues` | Sends `inspection_id` only if `inspection.serverId` is non-nil |
| `fetchInspectionHistory` | `GET /api/v1/inspections` | Paginated, returns `InspectionHistoryResponseData` |
| `fetchAssignedIssues` | `GET /api/v1/issues` | Returns issues assigned to current user |
| `fetchAssignedIssues` | `GET /api/v1/issues` | Returns issues assigned to OR reported by current user |
| `fetchIssueDetail` | `GET /api/v1/issues/<id>` | Fetches current status for detail view |
| `updateIssueStatus` | `PATCH /api/v1/issues/<id>/status` | Inspector updates status on assigned issues |
| `updateIssueStatus` | `PATCH /api/v1/issues/<id>/status` | Inspector updates status on assigned/reported issues |
| `fetchNotifications` | `GET /api/v1/notifications` | Accepts optional `since: Date`; returns `[APINotification]` |
| `markNotificationsRead` | `PATCH /api/v1/notifications/mark-read` | Marks list of IDs read on server |
@@ -666,6 +667,10 @@ The app registers a `BGProcessingTask` with identifier `com.jqc.sync`.
| 32 | **`StartInspectionView.onChange(of: selectedProjectId)` guards against resetting pre-filled facility** | Check `facilityBelongsToContract` before clearing `selectedFacilityId`; the onChange fires during `applyPreFill()` before `selectedFacilityId` is applied, wiping it if unchecked |
| 33 | **Issue photos are stored in `photoServerPaths` after `pullAssignedIssues`** | `photo_path` and `result_photos` from `_issue_payload` are merged into a single `[String]` and stored in `local.photoServerPaths` on insert/update |
| 34 | **`IssueDetailView` shows both `photoLocalPaths` and `photoServerPaths`** | Local paths use `UIImage(contentsOfFile:)`; server paths use `AsyncImage` with `Constants.baseURL` prefix. Both sections are independent |
| 35 | **`SyncManager.isoFormatter` is the only date formatter — never allocate per-call** | `DateFormatter` init is expensive. The static `nonisolated` formatter with `en_US_POSIX` locale handles all `yyyy-MM-dd'T'HH:mm:ss` parsing. Adding a new `DateFormatter` anywhere in SyncManager is wrong. |
| 36 | **`uploadPhoto(retrying:)` — pass `retrying: true` on recursive retry** | Matches `request()` pattern. Without it, a 401 on the retry triggers a second token refresh instead of throwing `notAuthenticated`. |
| 37 | **`LocalIssue.inspection` must declare explicit `@Relationship` inverse** | `@Relationship(deleteRule: .nullify, inverse: \LocalInspection.localIssues)` — without it SwiftData infers the inverse implicitly, which can produce migration warnings and incorrect cascade behaviour under some Xcode 26 versions. `deleteRule` is `.nullify` not `.cascade` because cascade is already declared on `LocalInspection.localIssues`. |
| 38 | **`APIUser.displayName` uses `fullName` when non-empty, falls back to `username`** | Server `_user_payload` sends `full_name`; iOS decodes as `fullName: String` (empty string when unset). `displayName` computed property: `fullName.isEmpty ? username : fullName`. Mirrors `User.display_name` server-side. |
---
@@ -689,7 +694,7 @@ Xcode 26 sets this build setting when creating new projects with "approachable c
3. Chained optional patterns `(try? fetch(...))?.filter { }` are split into two `let` statements.
4. `triggerSync()` guards on `isAuthenticated` to prevent 401 cascades during startup.
5. Notification polling uses `Task.sleep` not `Timer.scheduledTimer` (RunLoop dependency).
6. All API model structs declare `nonisolated init(from decoder: any Decoder)` explicitly.
6. All API model structs declare `nonisolated init(from decoder: any Decoder)` explicitly — including file-scope `private` structs like `_RefreshEnvelope` in `APIClient.swift`. A `private` struct defined in a file containing an `actor` or `@MainActor` type can have its `Decodable` conformance tainted with `@MainActor`, producing "cannot be used in actor-isolated context" errors in Swift 6 mode. The fix is always an explicit `nonisolated init(from:)` with a matching `CodingKeys` enum.
**Do NOT remove `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` from build settings** unless you fully audit every file for the resulting isolation changes. The patterns above are the correct workarounds.