# REST API Sync — Design Doc Vervang WebDAV-sync door een eigen REST API op `weegschaal.eddydevink.nl`, met MariaDB-backend en Bearer token-authenticatie. Incrementele sync i.p.v. volledig bestand. ## Fase 1 — Server-side: PHP API ### Infrastructuur - **DNS**: `weegschaal.eddydevink.nl` → zelfde server als `git.eddydevink.nl` (nextcloud) - **nginx**: server block in `/etc/nginx/conf.d/weegschaal-api.conf`, proxy naar PHP-FPM socket - **SSL**: `certbot --nginx -d weegschaal.eddydevink.nl` - **Document root**: `/usr/share/nginx/html/weegschaal-api/` ### MariaDB Database `weegschaal` met gebruiker `weegschaal@localhost` (random wachtwoord). ```sql CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE auth_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), token VARCHAR(64) NOT NULL UNIQUE, description VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE measurements ( id VARCHAR(36) NOT NULL, user_id INT NOT NULL, weight DECIMAL(5,2) NOT NULL, bmi DECIMAL(4,1) NOT NULL, body_fat_percentage DECIMAL(4,1) NOT NULL, fat_mass DECIMAL(5,2) NOT NULL, muscle_percentage DECIMAL(4,1) NOT NULL, muscle_mass DECIMAL(5,2) NOT NULL, visceral_fat TINYINT NOT NULL, bmr DECIMAL(6,1) NOT NULL, measured_at DATE NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, deleted TINYINT NOT NULL DEFAULT 0, PRIMARY KEY (user_id, id), INDEX idx_updated_at (user_id, updated_at) ); ``` `deleted` flag voor soft deletes op server. De client stuurt harde DELETEs; de server zet `deleted=1` en update `updated_at` zodat de volgende `?since=` query deze doorgeeft zodat andere clients de delete kunnen verwerken. ### Endpoints (Slim Framework) | Methode | Pad | Beschrijving | |---------|-----|-------------| | `GET` | `/api/v1/measurements` | Alle metingen (`?since=ISO` voor delta) | | `POST` | `/api/v1/measurements` | Nieuwe meting of upsert op ID | | `DELETE` | `/api/v1/measurements/:id` | Meting verwijderen (soft delete) | **Geen** auth endpoints — eerste token wordt handmatig via SQL INSERT aangemaakt. ### Auth middleware - `Authorization: Bearer ` → valideert tegen `auth_tokens` tabel - Zet `user_id` in request context - 401 bij ongeldige/missing token ### Bestandsstructuur ``` /usr/share/nginx/html/weegschaal-api/ ├── composer.json ├── public/ │ └── index.php ← Entry point, routing ├── src/ │ ├── Database.php ← PDO singleton │ ├── Auth.php ← Token validation middleware │ ├── Measurements.php ← CRUD + sync queries │ └── helpers.php └── config.php ← DB credentials ``` ### Sync-logica server - `GET ?since=ISO` → `WHERE user_id = ? AND updated_at > ? ORDER BY updated_at ASC` - `POST` → `INSERT ... ON DUPLICATE KEY UPDATE` — upsert op client-ID - `DELETE` → set `deleted=1`, update `updated_at=NOW()` (soft delete, zodat delta-sync werkt voor andere clients) - Geretourneerde records bevatten `deleted: true` veld zodat de client weet dat dit een soft-deleted record is --- ## Fase 2 — Client-side: App aanpassen ### Types (`src/types.ts`) `WebDAVConfig` en `SyncStatus` vervallen. Toegevoegd: `ApiConfig`, `deletedAt`: ```typescript export type Measurement = { id: string weight: number bmi: number bodyFatPercentage: number fatMass: number musclePercentage: number muscleMass: number visceralFat: number bmr: number measuredAt: string createdAt: string updatedAt: string deletedAt?: string } export type ApiConfig = { url: string token: string } ``` ### SettingsScreen (`src/screens/SettingsScreen.tsx`) - Vervang URL/username/password door API-URL + token (met oogje) - SecureStore key: `weegschaal_api_config` - `getApiConfig()` (exported) i.p.v. `getWebDAVConfig()` - Sync knop blijft ### Nieuwe module `src/sync/api.ts` ```typescript const createApi = (config: ApiConfig) => ({ getMeasurements: (since?: string) => GET /api/v1/measurements?since=... createMeasurement: (m: Measurement) => POST /api/v1/measurements deleteMeasurement: (id: string) => DELETE /api/v1/measurements/:id }) ``` Alle calls met `Authorization: Bearer ` header. ### syncService.ts — volledig herschreven #### `mergeMeasurements(local, remote)` Simpele last-writer-wins op `updatedAt`: 1. Bouw map van alle IDs (lokaal + remote) 2. Voor elke ID: als beide bestaan → behoud de met de hoogste `updatedAt` 3. Remote metingen die winnen krijgen `deletedAt` overgenomen uit server indien de server record `deleted: true` had #### `syncAll()` 1. Lees `lastSyncAt` uit SecureStore (`weegschaal_last_sync`) 2. `GET /api/v1/measurements?since=lastSyncAt` → remote delta 3. `getAllMeasurements()` → lokale metingen 4. Merge remote delta in lokale DB (last-writer-wins) 5. Verwijder lokale records waar remote `deleted: true` en remote `updatedAt > local updatedAt` 6. Verzamel lokale metingen met `updatedAt > lastSyncAt` (alleen niet-verwijderde) 7. Voor `deletedAt` metingen met `deletedAt > lastSyncAt` → DELETE naar API → bij succes lokaal verwijderen 8. Voor overige metingen met `updatedAt > lastSyncAt` → POST naar API 9. `lastSyncAt = now()` #### `uploadMeasurement(m)` Losstaande functie voor directe push na opslaan: 1. POST naar API 2. Bij succes → return 3. Bij netwerkfout → throw; meting blijft lokaal, `syncAll` pakt hem later op (want `updatedAt > lastSyncAt`) ### NewMeasurementScreen 1. Lokaal opslaan (insert of upsert, met `updatedAt = now()`) 2. Direct `uploadMeasurement(m)` proberen 3. Bij succes → navigeer terug met "Opgeslagen & gesynct" 4. Bij falen → navigeer terug met "Opgeslagen (sync mislukt)" ### MeasurementDetailScreen Bij delete: 1. Zet `deletedAt = now()` lokaal 2. Fire-and-forget: DELETE API call 3. Bij succes → record lokaal verwijderen 4. Bij falen → record blijft met `deletedAt`, syncAll pakt het op ### HistoryScreen Bij delete (vanuit alert in HistoryScreen): 1. Zet `deletedAt = now()` lokaal 2. Fire-and-forget: DELETE API call 3. Bij succes → record lokaal verwijderen 4. Bij falen → record blijft met `deletedAt` Filter bij laden: `!m.deletedAt`. ### DashboardScreen Laatste meting en grafiek: alleen metingen zonder `deletedAt`. Laatste meting = meest recente `measuredAt` zonder `deletedAt`. ### Database (`db.ts`) Nieuw kolom in schema: `deletedAt TEXT`. Nieuwe functies: - `getMeasurementsUpdatedSince(since: string)` — metingen met `updatedAt > since` - `setDeletedAt(id: string)` — zet `deletedAt = now()` - `removeMeasurement(id: string)` — harde delete Verwijderd/onbruikbaar: - `getUnsyncedMeasurements()` — niet meer nodig - `getPendingMeasurements()` — niet meer nodig - `markAsDeleted()` — vervangen door `setDeletedAt` - `syncStatus` kolom — blijft in DB maar wordt niet meer gebruikt in code --- ## Fase 3 — Migratie (eerste sync) Eerste keer dat de app de nieuwe API gebruikt (`lastSyncAt` is `null`): - `GET /api/v1/measurements` (zonder `?since`) → alle remote metingen - Merge met lokale DB: last-writer-wins, remote deleted overschrijft lokaal - Alle lokale metingen (die niet deleted zijn) worden gepusht (`updatedAt > null` = alles) - `lastSyncAt = now()` - Vanaf dan: delta-sync --- ## Foutafhandeling | Situatie | Gedrag | |----------|--------| | Geen netwerk bij opslaan | Lokaal opgeslagen, `updatedAt > lastSyncAt` dus next sync pakt op | | Geen netwerk bij sync | Throw error, scherm toont melding | | Geen API-config | Throw `'Geen API configuratie. Stel deze eerst in in Instellingen.'` | | HTTP 401 (ongeldige token) | Throw `'Authenticatie mislukt. Controleer je token in Instellingen.'` | | HTTP 5xx | Throw met statuscode, scherm toont melding | --- ## Testen ### Client-side (Jest) - `mergeMeasurements()` unit tests: local wins, remote wins, deleted remote vs local - `api.ts` calls mocken met fetch-mock - `syncAll()` integratietest met gemockte API ### Server-side - Handmatig testen met curl na deployment --- ## Implementatievolgorde 1. Server-side: nginx, MariaDB, PHP API → testen met curl 2. Client-side: types, api.ts, db.ts, syncService.ts, SettingsScreen → testen 3. Client-side: DashboardScreen, HistoryScreen, MeasurementDetailScreen, NewMeasurementScreen 4. Client-side: unit tests 5. Build APK, sideloaden, testen op telefoon