Update claude.md, app icon
This commit is contained in:
+44
-6
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user