feat: outbox-pattern sync + progressive dashboard loading
- Sync herschreven: push-vóór-pull, outbox met syncStatus (pending/pending-delete/synced), local-first merge - Dashboard: progressieve loading — laatste 2 metingen direct, grafieken volgen daarna - measurementStore versimpeld naar refreshCache/getCached/getVersion (20 regels) - db.ts: getPendingRecords(), updateSyncStatus(), migratie local→synced, syncStatus DEFAULT 'pending' - WeightChart: filtert 0-waarden uit vetmassa/spiermassa, verbergt chart bij <2 datapunten - HistoryScreen/MeasurementDetailScreen: delete flow naar DB-only (setDeletedAt + updateSyncStatus) - eas.json: appVersionSource toegevoegd tegen waarschuwing - README bijgewerkt met outbox-pattern documentatie - 16/16 tests, TypeScript clean
This commit is contained in:
parent
a32b9e0a88
commit
a77b31f95c
13 changed files with 257 additions and 213 deletions
33
README.md
33
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
_*Gemaakt met vibecoding — DeepSeek V4 Pro*_
|
||||
|
||||
React Native (Expo) app voor het invoeren en synchroniseren van lichaamsmetingen (weegschaal). Data wordt lokaal opgeslagen in SQLite en gesynchroniseerd via een eigen REST API met timestamp-based delta-sync.
|
||||
React Native (Expo) app voor het invoeren en synchroniseren van lichaamsmetingen (weegschaal). Data wordt lokaal opgeslagen in SQLite en gesynchroniseerd via een eigen REST API met outbox-pattern sync.
|
||||
|
||||
## Inhoud
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ React Native (Expo) app voor het invoeren en synchroniseren van lichaamsmetingen
|
|||
## Functionaliteit
|
||||
|
||||
- **Metingen invoeren** van een slimme weegschaal: gewicht, BMI, vetpercentage, vetmassa, spierpercentage, spiermassa, buikvet en stofwisseling
|
||||
- **Dashboard** met laatste en voorlaatste meting, gewichtsverschil, en grafiek
|
||||
- **Dashboard** met laatste en voorlaatste meting (direct zichtbaar via progressieve loading), gewichtsverschil, en grafieken (gewicht, vetmassa, spiermassa)
|
||||
- **Delen** van meetresultaten als PNG-afbeelding via WhatsApp, e-mail, etc.
|
||||
- **Historie** van alle metingen, gesorteerd op datum
|
||||
- **Offline-first**: metingen worden lokaal opgeslagen en later automatisch gesynchroniseerd
|
||||
|
|
@ -30,7 +30,7 @@ React Native (Expo) app voor het invoeren en synchroniseren van lichaamsmetingen
|
|||
|
||||
| Scherm | Beschrijving |
|
||||
|--------|-------------|
|
||||
| Dashboard | Laatste meting, voorlaatste meting met gewichtsverschil, grafiek, deel-knop |
|
||||
| Dashboard | Progressive loading: laatste 2 metingen direct zichtbaar, grafieken volgen daarna |
|
||||
| Nieuwe meting / Wijzigen | Formulier met datumkiezer, waarden vooraf ingevuld vanaf laatste meting |
|
||||
| Meting detail | Alle meetwaarden met knoppen voor wijzigen en verwijderen |
|
||||
| Historie | FlatList van alle metingen, pull-to-refresh, tik voor detail |
|
||||
|
|
@ -42,23 +42,19 @@ De app synchroniseert via een zelfgebouwde REST API. Geen WebDAV, geen bestandss
|
|||
|
||||
### Hoe het werkt
|
||||
|
||||
1. **Delta ophalen**: `GET /api/v1/measurements?since=lastSyncAt` — alleen gewijzigde records van de server
|
||||
2. **Mergen**: last-writer-wins op `updatedAt`
|
||||
3. **Pushen**: alle lokale records met `updatedAt > lastSyncAt` → POST naar API
|
||||
4. **Verwijderen**: soft delete via `deletedAt` timestamp → DELETE naar API bij volgende sync
|
||||
5. **High-water mark**: `lastSyncAt` bijgewerkt na elke succesvolle sync
|
||||
1. **Outbox pattern**: elke meting heeft een `syncStatus` — `pending` (nieuw/gewijzigd), `pending-delete` (verwijderd), of `synced` (in sync met server)
|
||||
2. **Push vóór pull**: eerst worden lokale wijzigingen (`pending` + `pending-delete`) naar de server gepusht, daarna pas remote changes opgehaald
|
||||
3. **Local-first merge**: remote data overschrijft nooit lokale `pending` of `pending-delete` records — de gebruiker wint altijd
|
||||
4. **Verwijderen**: `setDeletedAt(id)` lokaal + `updateSyncStatus(id, 'pending-delete')` → push in outbox-fase → server DELETE → hard delete in pull-fase
|
||||
5. **Cursor tracking**: `lastSyncAt` (SecureStore) voor delta-pull, alleen records sinds laatste sync
|
||||
|
||||
### Waarom timestamp-based?
|
||||
### Waarom outbox pattern?
|
||||
|
||||
Geen `syncStatus` enum, geen speciale gevallen. Hetzelfde patroon dat CouchDB en PouchDB gebruiken:
|
||||
- `updatedAt` op elke wijziging (client-tijd)
|
||||
- `deletedAt` voor soft deletes
|
||||
- `lastSyncAt` (opgeslagen in SecureStore) voor delta-tracking
|
||||
- Merge: last-writer-wins, zonder uitzonderingen
|
||||
Betrouwbaarder dan timestamp-only push: `syncStatus` markeert expliciet wat gepusht moet worden, onafhankelijk van klokverschillen. Push vóór pull voorkomt dat eigen wijzigingen overschreven worden door oudere remote data. Local-first merge garandeert dat de gebruiker nooit data verliest.
|
||||
|
||||
### Offline resilience
|
||||
|
||||
Bij netwerkfout tijdens opslaan blijft de meting lokaal staan. De volgende sync (`updatedAt > lastSyncAt`) pikt hem automatisch op. Geen handmatige tussenkomst nodig.
|
||||
Bij netwerkfout tijdens opslaan blijft de meting lokaal staan met `syncStatus='pending'`. De outbox (`getPendingRecords()`) pikt hem automatisch op bij de volgende sync of dashboard-focus.
|
||||
|
||||
## Server-side API
|
||||
|
||||
|
|
@ -141,11 +137,10 @@ Vereisten: Android SDK 36, OpenJDK 21, ADB 37+, Gradle 8.14.
|
|||
|
||||
## Database migratie
|
||||
|
||||
Bestaande SQLite-databases worden automatisch gemigreerd. `initDatabase()` voegt de `deletedAt` kolom toe via `ALTER TABLE` als deze nog niet bestaat:
|
||||
Bestaande SQLite-databases worden automatisch gemigreerd bij `initDatabase()`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE measurements ADD COLUMN deletedAt TEXT
|
||||
```
|
||||
- `deletedAt` kolom: toegevoegd via `ALTER TABLE` als deze nog niet bestaat
|
||||
- `syncStatus` migratie: bestaande records met `syncStatus='local'` worden omgezet naar `'synced'`, nieuwe records krijgen DEFAULT `'pending'`
|
||||
|
||||
## Testen
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue