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

File diff suppressed because it is too large Load diff

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

View file

@ -28,9 +28,9 @@ export async function initDatabase(): Promise<void> {
export async function insertMeasurement(m: Measurement): Promise<void> {
await db!.runAsync(
`INSERT INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, syncStatus, deletedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.syncStatus, m.deletedAt ?? null]
`INSERT INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, deletedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.deletedAt ?? null]
)
}
@ -59,9 +59,9 @@ export async function getMeasurementByDate(date: string): Promise<Measurement |
export async function upsertMeasurement(m: Measurement): Promise<void> {
await db!.runAsync(
`INSERT OR REPLACE INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, syncStatus, deletedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.syncStatus, m.deletedAt ?? null]
`INSERT OR REPLACE INTO measurements (id, weight, bmi, bodyFatPercentage, fatMass, musclePercentage, muscleMass, visceralFat, bmr, measuredAt, createdAt, updatedAt, deletedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[m.id, m.weight, m.bmi, m.bodyFatPercentage, m.fatMass, m.musclePercentage, m.muscleMass, m.visceralFat, m.bmr, m.measuredAt, m.createdAt, m.updatedAt, m.deletedAt ?? null]
)
}

140
upgrade.md Normal file
View file

@ -0,0 +1,140 @@
# Upgrade: REST API sync i.p.v. WebDAV
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
### Subdomein & nginx
- DNS: `weegschaal.eddydevink.nl` → zelfde server als `git.eddydevink.nl`
- nginx server block in `/etc/nginx/conf.d/weegschaal-api.conf`
- SSL via Let's Encrypt (`sudo certbot`)
- Document root: `/usr/share/nginx/html/weegschaal-api/`
### MariaDB
```sql
CREATE DATABASE weegschaal;
CREATE USER 'weegschaal'@'localhost' IDENTIFIED BY '<random>';
GRANT ALL ON weegschaal.* TO 'weegschaal'@'localhost';
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,
PRIMARY KEY (user_id, id),
INDEX idx_updated_at (user_id, updated_at)
);
```
### PHP API endpoints (Slim Framework via Composer)
| Methode | Pad | Beschrijving |
|---------|-----|-------------|
| `POST` | `/api/v1/auth/register` | (later) Nieuwe gebruiker |
| `POST` | `/api/v1/auth/tokens` | (later) Nieuwe token aanmaken |
| `GET` | `/api/v1/measurements` | Alle metingen (of `?since=ISO` voor delta) |
| `POST` | `/api/v1/measurements` | Nieuwe meting aanmaken |
| `PUT` | `/api/v1/measurements/:id` | Meting bijwerken |
| `DELETE` | `/api/v1/measurements/:id` | Meting verwijderen |
Authenticatie: `Authorization: Bearer <token>`, gevalideerd tegen `auth_tokens`-tabel.
Sync-logica server:
- `GET /api/v1/measurements?since=2025-01-01T12:00:00Z` → alleen metingen met `updated_at > since`
- Bij PUT/DELETE: controleer `updated_at` client, bij conflict → HTTP 409 met server-versie
### Bestandsstructuur API
```
/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 (uit .env of env vars)
```
## Fase 2 — Client-side: App aanpassen
### Types (`src/types.ts`)
- Vervang `WebDAVConfig` door `ApiConfig { url: string; token: string }`
- Verwijder `SyncStatus` (niet meer nodig — sync status volgt uit `updatedAt`)
### SettingsScreen (`src/screens/SettingsScreen.tsx`)
- Vervang URL/username/password door API-URL + token (met oogje)
- Opslaan → SecureStore
- Wissen → SecureStore verwijderen
- 'Nu synchroniseren' knop blijft
### Nieuwe module `src/sync/api.ts`
```typescript
const api = (config: ApiConfig) => ({
getMeasurements: (since?: string) => GET /api/v1/measurements?since=...
createMeasurement: (m: Measurement) => POST /api/v1/measurements
updateMeasurement: (m: Measurement) => PUT /api/v1/measurements/:id
deleteMeasurement: (id: string) => DELETE /api/v1/measurements/:id
})
```
### syncService.ts — volledig herschreven
Nieuwe sync-flow:
| Stap | Actie |
|------|-------|
| 1 | Lees lokale `lastSyncAt` (uit SecureStore) |
| 2 | `GET ?since=lastSyncAt` → alleen wijzigingen ophalen |
| 3 | Upsert remote wijzigingen lokaal (last-writer-wins) |
| 4 | Voor elke lokale meting met `updatedAt > lastSyncAt`: push naar API |
| 5 | Update `lastSyncAt` naar server-tijd |
Bij opslaan in `NewMeasurementScreen`:
1. Direct naar lokale SQLite (offline-first)
2. Direct erna: push naar API (async, niet-blokkerend via `Promise.allSettled`)
## Fase 3 — Migratie
Eerste keer na API-setup:
1. App stuurt alle lokale metingen naar de API (1-voor-1 via POST)
2. Alle metingen nu in MariaDB
3. Vanaf dan: alleen delta-sync
## Benodigde server-toegang
- SSH naar nextcloud (`aiaiaiaiagent@nextcloud`) — key werkte niet
- `sudo -A mysql` voor DB + user aanmaken
- nginx config aanpassen (`/etc/nginx/conf.d/`)
- DNS controleren voor subdomein