weegschaal-app/docs/superpowers/plans/2026-07-25-sync-refactor-plan.md
eddy df05fb75dd fix: sync refactor — fix 6 bugs + cache refresh na sync
- setDeletedAt update nu ook updatedAt (bug 2)
- getAllMeasurements filtert deletedAt IS NULL, nieuwe getAllMeasurementsForSync voor sync
- mergeMeasurements detecteert server deleted:true → deletedAt (bug 1)
- syncAll: pull-push split met remoteUpdatedMap, geen double-push (bug 3)
- hard delete alleen na API-succes (bug 4)
- DashboardScreen: refreshCache() na syncAll zodat UI niet op Laden blijft hangen
- deleteMeasurement() dead code verwijderd (bug 5)
- tests: 3 nieuwe testcases voor deleted → deletedAt mapping, 15/15 pass
2026-07-25 15:33:22 +02:00

5.5 KiB

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<id, updatedAt>      // 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<id>          // 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