Update claude.md, app icon

This commit is contained in:
Nguyen Ngo
2026-05-15 16:50:25 -04:00
parent 1b90c23b3e
commit 0cc5804b6e
3 changed files with 45 additions and 6 deletions
+44 -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 (Hardening session — Xcode 26 build fixes, startup race condition resolved)
> **Last reviewed:** May 2026 (Phase C complete — notification polling, assigned-issue sync, issue photo display, FlagIssueView confirmation banner, re-inspection pre-fill fix)
> **Companion:** See the web backend's `CLAUDE.md` for API contract, server-side rules, and migration chain.
---
@@ -42,12 +42,14 @@ Core capabilities:
- **Login/logout** via JWT against the JQC web backend API
- **Reference data sync** — facilities, areas, and inspection templates pulled from server
- **Inspection execution** — dynamic form rendering driven by server-side template schemas
- **Issue flagging** — severity-tagged issues with optional photos, attached to inspections
- **Re-inspection** — linked follow-up inspections with parent-form pre-fill
- **Issue flagging** — severity-tagged issues with optional photos, attached to inspections; confirmation banner shown on submit
- **Re-inspection** — linked follow-up inspections with parent-form pre-fill (template, contract, facility all pre-filled)
- **Outbox queue** — completed inspections and issues submitted to server automatically when online
- **Inspection history** — server-side read-only history for completed inspections
- **Issues list** — local read-only list of all issues flagged on this device
- **Issues list** — assigned issues pulled from server + device-created issues; stale unassigned records removed on sync
- **Issue detail** — shows local photos and server-hosted photos via `AsyncImage`
- **Facilities browser** — read-only view of synced facilities and their areas
- **Push notifications** — local notifications for issue assignments and follow-up requests via 60-second polling
---
@@ -243,6 +245,8 @@ Inspector action → SwiftData write (always succeeds immediately)
2. Submit completed inspections (processInspectionQueue)
3. Submit pending issues (processIssueQueue)
4. Pull fresh reference data (pullReferenceData)
5. Pull + reconcile assigned issues (pullAssignedIssues)
6. Poll server notifications (pollNotifications)
```
The UI never blocks on network. Every screen is driven by local SwiftData queries.
@@ -279,6 +283,16 @@ The `isAuthenticated` guard is critical. `NWPathMonitor` fires immediately on co
2. `processInspectionQueue` — submits completed inspections only when all `pendingPhotos` are settled. Clears `followUpRequired` on parent after sync.
3. `processIssueQueue` — guards against submitting when parent inspection `syncStatus == "failed"` (prevents orphaned server records). Uses fetch-all + filter in Swift.
4. `pullReferenceData` — fetches facilities, areas, and templates. Sequential `await` calls — not `async let`.
5. `pullAssignedIssues` — fetches `GET /api/v1/issues` (scoped to current user on server). Upserts into SwiftData keyed by `serverId`. **Deletion pass** removes local records with `syncStatus == "synced"` + `inspectionLocalId == ""` whose `serverId` is absent from the response — handles reassignment away from this inspector. Empty response is not short-circuited so full unassignment is handled.
6. `pollNotifications` — fetches `GET /api/v1/notifications?since=<last_fetch>`. Delivers each item as a local `UNUserNotificationCenter` banner. Updates cursor timestamp. Marks fetched IDs read on server.
### Notification polling
- `startPollTask()` creates a `Task` with a `Task.sleep(nanoseconds: 60_000_000_000)` loop. Called from `startMonitoring()` whenever connectivity is `.satisfied`.
- `stopPollTask()` cancels the task. Called when connectivity drops.
- `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]`.
### Fetch pattern — CRITICAL for Xcode 26
@@ -338,6 +352,11 @@ request(endpoint, method, body, retrying) async throws -> T
| `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 |
| `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 |
| `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 |
### Token refresh
@@ -478,6 +497,7 @@ else { continue }
- Appends the issue to `inspection.localIssues`.
- If a photo was taken: creates a `PendingPhoto` with `entityType = "issue"`.
- Saves to SwiftData.
- Shows a green **"Issue Logged"** confirmation banner (2 seconds) then dismisses.
- Triggers `SyncManager.triggerSync()` if online.
5. Area picker is absent — facility is derived directly from the inspection context.
@@ -497,6 +517,10 @@ On `CompletedInspectionView`, if `inspection.followUpRequired == true`, an orang
`StartInspectionView.startInspection()` copies non-scoring fields from the parent's `formData` into the new inspection. Excluded field types: `rating`, `pass_fail`, `image`, `signature` — these must always be re-evaluated fresh.
### Picker pre-fill (template, contract, facility)
`applyPreFill()` sets `selectedTemplateId`, `selectedProjectId`, and `selectedFacilityId` on `.onAppear`. **The `.onChange(of: selectedProjectId)` handler guards against resetting `selectedFacilityId`** when the facility already belongs to the newly selected contract — this prevents the onChange from wiping the pre-filled facility before it renders. Without this guard, `selectedProjectId` is set first, `.onChange` fires, and `selectedFacilityId` is reset to `nil` before it can be applied.
### followUpRequired clearing
Cleared at **three** points to ensure the badge disappears regardless of timing:
@@ -545,9 +569,13 @@ Uploaded (uploadStatus="uploaded", serverPath set)
While a photo is pending upload, the form field value is set to `"local://<path>"`. Before `submitInspection` sends `formData` to the server, these values are replaced with `""`. A `local://` value that reaches the server would be stored as a malformed path.
### Inspection submission gating
### Server-hosted photos in IssueDetailView
An inspection is **not submitted** until all its `pendingPhotos` have `uploadStatus == "uploaded"` or `"failed"`. It simply `continue`s to the next sync cycle.
`IssueDetailView` shows two photo sections:
1. **Local photos**`photoLocalPaths` rendered via `UIImage(contentsOfFile:)`. Present for device-created issues where photos were captured on-device.
2. **Server photos**`photoServerPaths` rendered via `AsyncImage(url: Constants.baseURL + "/" + relativePath)`. Present for issues pulled from the server (`pullAssignedIssues` stores `photo_path` + `result_photos` from the API response into `photoServerPaths`).
Both sections are shown independently; an issue can have entries in either or both.
---
@@ -630,6 +658,14 @@ The app registers a `BGProcessingTask` with identifier `com.jqc.sync`.
| 24 | **No physical `Info.plist` file when `GENERATE_INFOPLIST_FILE = YES`** | Having both causes "Multiple commands produce Info" build error |
| 25 | **Always parenthesise `try?` before `??`** | `try? context.fetch(...) ?? []` parses as `try? (fetch() ?? [])``fetch()` is non-optional so `??` is invalid inside `try?`; compiler infers `T = Any` and cascades into build errors. Write `(try? context.fetch(...)) ?? []` |
| 26 | **Delete Xcode's default `Item.swift` immediately after project creation** | Xcode generates `Item.swift` with `@Model class Item` when creating a new SwiftData project; it compiles silently via folder sync and conflicts with real models |
| 27 | **`Timer.scheduledTimer` must NOT be used for periodic work in SyncManager** | Inside `Task { @MainActor }`, `RunLoop.current``RunLoop.main`; the timer is added to a runloop that never ticks and fires silently never. Use `Task.sleep` instead |
| 28 | **`UNUserNotificationCenterDelegate` must be set for foreground notifications** | Without a delegate returning `[.banner, .sound]` from `willPresent`, iOS silently drops local notifications while the app is active. `NotificationDelegate.shared` is set in `JanitorialQCApp.init()` |
| 29 | **All `Decodable & Sendable` response structs need `nonisolated init(from:)`** | Under `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`, synthesised Decodable inits inherit `@MainActor` isolation, conflicting with the `Sendable` constraint on `_Envelope<T>`; every API model struct must declare `nonisolated init(from decoder: any Decoder)` explicitly |
| 30 | **`pullAssignedIssues` deletion pass must NOT be short-circuited on empty response** | If the server returns zero issues (all unassigned), the deletion pass must still run to remove stale local records; do not `guard !apiIssues.isEmpty else { return }` before the deletion loop |
| 31 | **Server-pulled issues identified by `syncStatus == "synced"` + `inspectionLocalId == ""`** | These are the only records safe to delete during reconciliation. Device-created issues have `inspectionLocalId != ""` and must never be deleted by `pullAssignedIssues` |
| 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 |
---
@@ -652,6 +688,8 @@ Xcode 26 sets this build setting when creating new projects with "approachable c
2. All `try? context.fetch(...)` expressions are parenthesised before `??`.
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.
**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.