# 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 .* - .*$ ``` 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)