8.3 KiB
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 alsgit.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).
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 tegenauth_tokenstabel- Zet
user_idin 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 ASCPOST→INSERT ... ON DUPLICATE KEY UPDATE— upsert op client-IDDELETE→ setdeleted=1, updateupdated_at=NOW()(soft delete, zodat delta-sync werkt voor andere clients)- Geretourneerde records bevatten
deleted: trueveld 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:
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
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:
- Bouw map van alle IDs (lokaal + remote)
- Voor elke ID: als beide bestaan → behoud de met de hoogste
updatedAt - Remote metingen die winnen krijgen
deletedAtovergenomen uit server indien de server recorddeleted: truehad
syncAll()
- Lees
lastSyncAtuit SecureStore (weegschaal_last_sync) GET /api/v1/measurements?since=lastSyncAt→ remote deltagetAllMeasurements()→ lokale metingen- Merge remote delta in lokale DB (last-writer-wins)
- Verwijder lokale records waar remote
deleted: trueen remoteupdatedAt > local updatedAt - Verzamel lokale metingen met
updatedAt > lastSyncAt(alleen niet-verwijderde) - Voor
deletedAtmetingen metdeletedAt > lastSyncAt→ DELETE naar API → bij succes lokaal verwijderen - Voor overige metingen met
updatedAt > lastSyncAt→ POST naar API lastSyncAt = now()
uploadMeasurement(m)
Losstaande functie voor directe push na opslaan:
- POST naar API
- Bij succes → return
- Bij netwerkfout → throw; meting blijft lokaal,
syncAllpakt hem later op (wantupdatedAt > lastSyncAt)
NewMeasurementScreen
- Lokaal opslaan (insert of upsert, met
updatedAt = now()) - Direct
uploadMeasurement(m)proberen - Bij succes → navigeer terug met "Opgeslagen & gesynct"
- Bij falen → navigeer terug met "Opgeslagen (sync mislukt)"
MeasurementDetailScreen
Bij delete:
- Zet
deletedAt = now()lokaal - Fire-and-forget: DELETE API call
- Bij succes → record lokaal verwijderen
- Bij falen → record blijft met
deletedAt, syncAll pakt het op
HistoryScreen
Bij delete (vanuit alert in HistoryScreen):
- Zet
deletedAt = now()lokaal - Fire-and-forget: DELETE API call
- Bij succes → record lokaal verwijderen
- 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 metupdatedAt > sincesetDeletedAt(id: string)— zetdeletedAt = now()removeMeasurement(id: string)— harde delete
Verwijderd/onbruikbaar:
getUnsyncedMeasurements()— niet meer nodiggetPendingMeasurements()— niet meer nodigmarkAsDeleted()— vervangen doorsetDeletedAtsyncStatuskolom — 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 localapi.tscalls mocken met fetch-mocksyncAll()integratietest met gemockte API
Server-side
- Handmatig testen met curl na deployment
Implementatievolgorde
- Server-side: nginx, MariaDB, PHP API → testen met curl
- Client-side: types, api.ts, db.ts, syncService.ts, SettingsScreen → testen
- Client-side: DashboardScreen, HistoryScreen, MeasurementDetailScreen, NewMeasurementScreen
- Client-side: unit tests
- Build APK, sideloaden, testen op telefoon