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
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -1,6 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "AppIcon-1024.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"
+44 -6
View File
@@ -2,7 +2,7 @@
> **Audience:** AI assistants and developers working on the JanitorialQC iPad app. > **Audience:** AI assistants and developers working on the JanitorialQC iPad app.
> **Purpose:** Authoritative reference for architecture, conventions, constraints, and decisions. > **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. > **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 - **Login/logout** via JWT against the JQC web backend API
- **Reference data sync** — facilities, areas, and inspection templates pulled from server - **Reference data sync** — facilities, areas, and inspection templates pulled from server
- **Inspection execution** — dynamic form rendering driven by server-side template schemas - **Inspection execution** — dynamic form rendering driven by server-side template schemas
- **Issue flagging** — severity-tagged issues with optional photos, attached to inspections - **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 - **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 - **Outbox queue** — completed inspections and issues submitted to server automatically when online
- **Inspection history** — server-side read-only history for completed inspections - **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 - **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) 2. Submit completed inspections (processInspectionQueue)
3. Submit pending issues (processIssueQueue) 3. Submit pending issues (processIssueQueue)
4. Pull fresh reference data (pullReferenceData) 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. 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. 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. 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`. 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 ### 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 | | `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 | | `submitIssue` | `POST /api/v1/issues` | Sends `inspection_id` only if `inspection.serverId` is non-nil |
| `fetchInspectionHistory` | `GET /api/v1/inspections` | Paginated, returns `InspectionHistoryResponseData` | | `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 ### Token refresh
@@ -478,6 +497,7 @@ else { continue }
- Appends the issue to `inspection.localIssues`. - Appends the issue to `inspection.localIssues`.
- If a photo was taken: creates a `PendingPhoto` with `entityType = "issue"`. - If a photo was taken: creates a `PendingPhoto` with `entityType = "issue"`.
- Saves to SwiftData. - Saves to SwiftData.
- Shows a green **"Issue Logged"** confirmation banner (2 seconds) then dismisses.
- Triggers `SyncManager.triggerSync()` if online. - Triggers `SyncManager.triggerSync()` if online.
5. Area picker is absent — facility is derived directly from the inspection context. 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. `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 ### followUpRequired clearing
Cleared at **three** points to ensure the badge disappears regardless of timing: 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. 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 | | 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(...)) ?? []` | | 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 | | 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 `??`. 2. All `try? context.fetch(...)` expressions are parenthesised before `??`.
3. Chained optional patterns `(try? fetch(...))?.filter { }` are split into two `let` statements. 3. Chained optional patterns `(try? fetch(...))?.filter { }` are split into two `let` statements.
4. `triggerSync()` guards on `isAuthenticated` to prevent 401 cascades during startup. 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. **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.