# Sync refactor — Implementation Plan ✅ (completed 2026-07-25) > Fix 6 sync bugs + herstel architectuur naar best-practice bidirectional timestamp sync. > > **Status:** Alle 6 bugs gefixt. 15/15 tests passen. APK build + geinstalleerd. ## Bug Overzicht | # | Bug | Oorzaak | Fix | |---|-----|---------|-----| | 1 | Server `deleted:true` wordt genegeerd | `mergeMeasurements` mapt `deleted` niet naar `deletedAt` | Detecteer `(r as any).deleted` in merge, zet `deletedAt = r.updatedAt` | | 2 | `setDeletedAt` update `updatedAt` niet | Alleen `deletedAt` wordt gezet, `updatedAt` blijft oud | Voeg `updatedAt = ?` toe aan de UPDATE | | 3 | Pull-then-push double-push | Na upsert van remote records hebben die `updatedAt > lastSyncAt` → worden teruggestuurd | Skip in push-fase records die uit remote pull komen en waar remote.updatedAt ≥ local.updatedAt | | 4 | Hard delete ondanks falende API | `removeMeasurement()` wordt ALTIJD aangeroepen, ook na catch | Alleen hard delete bij API-succes | | 5 | `syncStatus` dead column | In schema, nooit gebruikt | Laat staan, geen impact | | 6 | `getAllMeasurements` filtert `deletedAt` niet | UI-schermen moeten zelf filteren (`.filter(m => !m.deletedAt)`) | Rename naar `getAllMeasurementsForSync` (ongefilterd), vervang UI-calls door `getAllMeasurements` met `WHERE deletedAt IS NULL` | ## Sync Protocol (vastgelegd) ``` lastSyncAt = SecureStore.getItem(LAST_SYNC_KEY) ── PULL ── remote[] = GET /api/v1/measurements?since=lastSyncAt local[] = getAllMeasurementsForSync() // ALLE records (incl. deleted) remoteUpdatedMap: Map // track welke IDs + timestamps we binnenhaalden for each r in remote: remoteUpdatedMap.set(r.id, r.updatedAt) if (r as any).deleted: r.deletedAt = r.updatedAt // Bug 1 fix ── MERGE ── remoteDeletedIds: Set // track welke remote records 'deleted' zijn for each r in remote: if (r as any).deleted: remoteDeletedIds.add(r.id) merged = mergeMeasurements(local, remote) for each m in merged: remoteUA = remoteUpdatedMap.get(m.id) if remoteUA && new Date(remoteUA) >= new Date(m.updatedAt): // remote won de merge if remoteDeletedIds.has(m.id): removeMeasurement(m.id) // hard-delete lokaal (server heeft 'm al) else: upsertMeasurement(m) // remote gewijzigd, niet verwijderd else: upsertMeasurement(m) // local won of nieuw → gewoon bewaren ── PUSH ── cutoff = lastSyncAt ?? '1970-01-01T00:00:00.000Z' localChanges[] = getMeasurementsUpdatedSince(cutoff) for each m in localChanges: remoteUA = remoteUpdatedMap.get(m.id) if remoteUA && new Date(remoteUA) >= new Date(m.updatedAt): continue // remote won merge → niet terugpushen (Bug 3 fix) if m.deletedAt: DELETE /api/v1/measurements/:id if success: removeMeasurement(m.id) // Bug 4 fix: alleen hard-delete bij API-succes else: POST /api/v1/measurements SecureStore.setItem(LAST_SYNC_KEY, now()) ``` ## Delete Protocol (vastgelegd) ``` Lokaal: setDeletedAt(id) + updatedAt = now() (Bug 2 fix) Cache: removeFromCache(id) API: DELETE /api/v1/measurements/:id if success: Lokaal: removeMeasurement(id) (Bug 4 fix: alleen bij succes) // bij falen: blijft met deletedAt in DB → syncAll pakt op ``` ## Files om te wijzigen ### 1. `src/sync/syncService.ts` — REWRITE syncAll + mergeMeasurements **Wijzigingen:** - `mergeMeasurements`: detecteer `(r as any).deleted` → zet `deletedAt = r.updatedAt` - `syncAll`: - Track `remoteUpdatedMap` in pull-fase - Merge via bestaande `mergeMeasurements` - Push: skip records waar remote.updatedAt ≥ local.updatedAt (remote won) - Alleen `removeMeasurement` na succesvolle API DELETE - Exporteer `cleanupDeletedRecord(id)` voor gebruik door screens (DRY) ### 2. `src/database/db.ts` — setDeletedAt fix + query filters **Wijzigingen:** - `setDeletedAt(id)`: voeg `updatedAt = ?` toe aan SQL - `getAllMeasurements()`: voeg `WHERE deletedAt IS NULL` toe - `getLatestMeasurement()`: voeg `WHERE deletedAt IS NULL` toe - Nieuw: `getAllMeasurementsForSync()` — ongefilterd (voor syncService) - Verwijder: `deleteMeasurement()` (dode code, duplicate van `removeMeasurement`) ### 3. `src/screens/HistoryScreen.tsx` — delete fix **Wijzigingen:** - `confirmDelete`: alleen `removeMeasurement()` na API-succes - `load`: gebruik `getMeasurementsSince('1970-01-01')` i.p.v. `getAllMeasurements()` (of de nieuwe `getAllMeasurements` die filtert) ### 4. `src/screens/MeasurementDetailScreen.tsx` — delete fix **Wijzigingen:** - `handleDelete`: alleen `removeMeasurement()` na API-succes ### 5. `src/store/measurementStore.ts` — geen wijzigingen nodig De store gebruikt `getLatestMeasurements(count)` en `getMeasurementsSince(date)` die al filteren. `/loadFromDb` gebruikt `getAllMeasurements()` → die gaat nu ook filteren. ✓ ### 6. `tests/syncService.test.ts` — nieuwe test cases **Wijzigingen:** - Test: `deleted` van server wordt correct naar `deletedAt` gemapt - Test: syncAll push skips remote-won records - Test: syncAll push stuurt local-won records wel ### 7. `tests/database.test.ts` — update voor nieuwe functies **Wijzigingen:** - Test `setDeletedAt` update ook `updatedAt` - Test `getAllMeasurementsForSync` returned deleted records ## Uitvoeringsvolgorde 1. `db.ts` — setDeletedAt fix + query filters 2. `syncService.ts` — merge fix + syncAll refactor 3. `HistoryScreen.tsx` + `MeasurementDetailScreen.tsx` — delete fix 4. Tests updaten + TypeScript check + `npm test` 5. Build APK, testen