diff --git a/README.md b/README.md index 498af7a..3ef2acb 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,121 @@ # Weegschaal App -*Gemaakt met vibecoding — DeepSeek V4 Pro* +_*Gemaakt met vibecoding — DeepSeek V4 Pro*_ -React Native (Expo) app voor het handmatig invoeren van meetgegevens van een slimme weegschaal. Data wordt lokaal opgeslagen in SQLite en gesynchroniseerd via een eigen REST API. +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. -## Meetwaarden +## Inhoud -- Gewicht (kg) -- BMI -- Vetpercentage (%) -- Vetmassa (kg) -- Spierpercentage (%) -- Spiermassa (kg) -- Buikvet (index) -- Stofwisseling in rust (kcal) +- [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, gewichtsverschil, en grafiek +- **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 -- **Dashboard** — laatste meting, voorlaatste meting met gewichtsverschil, deel-knop om de kaart als PNG te delen -- **Nieuwe meting / Meting wijzigen** — formulier met datumkiezer, vooraf ingevuld met waarden van de laatste meting -- **Meting detail** — alle meetwaarden met knoppen voor wijzigen en verwijderen -- **Historie** — FlatList gesorteerd op datum (nieuwste bovenaan), pull-to-refresh -- **Instellingen** — REST API URL en token, handmatige sync +| Scherm | Beschrijving | +|--------|-------------| +| Dashboard | Laatste meting, voorlaatste meting met gewichtsverschil, grafiek, deel-knop | +| 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 sync via een eigen REST API (`weegschaal.eddydevink.nl`) met Bearer token-authenticatie. Incrementele delta-sync op basis van `updatedAt` timestamps — alleen gewijzigde metingen worden uitgewisseld. +De app synchroniseert via een zelfgebouwde REST API. Geen WebDAV, geen bestandssync — pure delta-sync. -Vul in het Instellingen scherm in: -- **API URL**: `https://weegschaal.eddydevink.nl` -- **Token**: je persoonlijke Bearer token +### 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 + +### Waarom timestamp-based? + +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 + +### Offline resilience + +Bij netwerkfout tijdens opslaan blijft de meting lokaal staan. De volgende sync (`updatedAt > lastSyncAt`) pikt hem automatisch op. Geen handmatige tussenkomst nodig. + +## 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 @@ -38,12 +124,37 @@ 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. `initDatabase()` voegt de `deletedAt` kolom toe via `ALTER TABLE` als deze nog niet bestaat: + +```sql +ALTER TABLE measurements ADD COLUMN deletedAt TEXT +``` + ## Testen ```bash npm test ``` +Tests richten zich op de merge-logica (`mergeMeasurements`) en database-operaties. + ## Licentie Public domain (Unlicense)