db: fix insertMeasurement/upsertMeasurement — remove syncStatus param

This commit is contained in:
eddy 2026-07-20 16:58:55 +02:00
commit 4b7b7e3d5c
4 changed files with 1697 additions and 6 deletions

View file

@ -0,0 +1,266 @@
# 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 <token>` → 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 <token>` 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