- 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
155 lines
5.5 KiB
Markdown
155 lines
5.5 KiB
Markdown
# Weegschaal App
|
|
|
|
_*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 outbox-pattern sync.
|
|
|
|
## Inhoud
|
|
|
|
- [Functionaliteit](#functionaliteit)
|
|
- [Schermen](#schermen)
|
|
- [Synchronisatie](#synchronisatie)
|
|
- [Server-side API](#server-side-api)
|
|
- [Beveiliging](#beveiliging)
|
|
- [Offline resilience](#offline-resilience)
|
|
- [Ontwikkeling](#ontwikkeling)
|
|
- [Build & deploy](#build--deploy)
|
|
- [Testen](#testen)
|
|
- [Licentie](#licentie)
|
|
|
|
## Functionaliteit
|
|
|
|
- **Metingen invoeren** van een slimme weegschaal: gewicht, BMI, vetpercentage, vetmassa, spierpercentage, spiermassa, buikvet en stofwisseling
|
|
- **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
|
|
- **Instellingen** voor API-configuratie en handmatige sync
|
|
|
|
## Schermen
|
|
|
|
| Scherm | Beschrijving |
|
|
|--------|-------------|
|
|
| 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 |
|
|
| Instellingen | API URL + token, opslaan/wissen, handmatige sync |
|
|
|
|
## Synchronisatie
|
|
|
|
De app synchroniseert via een zelfgebouwde REST API. Geen WebDAV, geen bestandssync — pure delta-sync.
|
|
|
|
### Hoe het werkt
|
|
|
|
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 outbox pattern?
|
|
|
|
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 met `syncStatus='pending'`. De outbox (`getPendingRecords()`) pikt hem automatisch op bij de volgende sync of dashboard-focus.
|
|
|
|
## Server-side API
|
|
|
|
**Stack**: nginx, PHP 8.5 (Slim Framework 4), MariaDB, Let's Encrypt SSL, SELinux
|
|
|
|
**Endpoints**:
|
|
|
|
| Methode | Pad | Auth | Beschrijving |
|
|
|---------|-----|------|-------------|
|
|
| `GET` | `/api/v1/measurements?since=ISO` | Bearer | Delta-sync |
|
|
| `POST` | `/api/v1/measurements` | Bearer | Upsert (INSERT ON DUPLICATE KEY) |
|
|
| `DELETE` | `/api/v1/measurements/:id` | Bearer | Soft delete (`deleted=1`, update `updated_at`) |
|
|
|
|
**Database**: MariaDB met drie tabellen — `users`, `auth_tokens`, `measurements`. De `measurements` tabel gebruikt `ON DUPLICATE KEY UPDATE` voor conflictvrije upserts.
|
|
|
|
**Bestandsstructuur**:
|
|
```
|
|
/usr/share/nginx/html/weegschaal-api/
|
|
├── composer.json
|
|
├── config.php
|
|
├── public/
|
|
│ └── index.php
|
|
└── src/
|
|
├── Auth.php ← Bearer token validatie middleware
|
|
├── Database.php ← PDO singleton
|
|
├── Measurements.php ← CRUD + sync queries
|
|
└── helpers.php ← JSON response, datetime conversie, logging
|
|
```
|
|
|
|
## Beveiliging
|
|
|
|
### Authenticatie
|
|
|
|
Bearer token-validatie via `auth_tokens` tabel. Tokens worden handmatig aangemaakt via SQL.
|
|
|
|
### Logging
|
|
|
|
Elke mislukte authenticatie en 404 wordt gelogd naar `/var/log/weegschaal-api/auth.log`:
|
|
|
|
```
|
|
[2026-07-20 18:06:07] 401 94.157.121.179 GET /api/v1/measurements - invalid Bearer token
|
|
[2026-07-20 18:06:07] 404 94.157.121.179 GET /.env - unknown route
|
|
```
|
|
|
|
### Fail2ban
|
|
|
|
Jail `weegschaal-api`: 5 foutieve pogingen binnen 5 minuten = 1 uur IP-ban. Filter regex:
|
|
|
|
```
|
|
^\[.*\] 401 <HOST> .* - .*$
|
|
```
|
|
|
|
Logrotering wekelijks, 12 weken retentie.
|
|
|
|
### SELinux
|
|
|
|
Logmap heeft context `httpd_log_t` gezet via `semanage fcontext`.
|
|
|
|
## Ontwikkeling
|
|
|
|
```bash
|
|
npm install
|
|
npx expo start
|
|
```
|
|
|
|
Scannen met Expo Go op Android of iOS.
|
|
|
|
## Build & deploy
|
|
|
|
```bash
|
|
# APK bouwen (lokaal via EAS)
|
|
npx eas build --local --profile apk --platform android
|
|
|
|
# Draadloos sideloaden via ADB
|
|
adb-wireless-connect
|
|
adb install -r build-*.apk
|
|
```
|
|
|
|
Vereisten: Android SDK 36, OpenJDK 21, ADB 37+, Gradle 8.14.
|
|
|
|
## Database migratie
|
|
|
|
Bestaande SQLite-databases worden automatisch gemigreerd bij `initDatabase()`:
|
|
|
|
- `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
|
|
|
|
```bash
|
|
npm test
|
|
```
|
|
|
|
Tests richten zich op de merge-logica (`mergeMeasurements`) en database-operaties.
|
|
|
|
## Licentie
|
|
|
|
Public domain (Unlicense)
|