weegschaal-app/docs/superpowers/specs/2026-07-20-rest-api-sync-design.md

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 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).

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:

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:

  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