Zuletzt aktualisiert:

title: Körper-Welt Backend — Verträge (SILV-301/302/307) + eGYM/Garmin-VERIFY category: dev date: 2026-06-18


Körper-Welt Backend (Phase 0)

Drei additive Read-Endpoints + eine neue Tabelle. Alle Bewertungs-Logik (Trend, PR, TDEE) ist serverseitig — der Client zeigt nur tone/text/verdict, textet und schwellt NICHTS (Anti-Drift). Bei zu wenig Daten: ehrlich null (Client zeigt dann kein Verdikt/keinen Wert), nie geraten.

VERIFY-Ergebnisse (an echten Accounts, 18.06.)

Garmin VO2Max/Ruhe-HR — verfügbar ✅. garminconnect.get_max_metrics(cdate) liefert generic.vo2MaxPreciseValue (z. B. 48.1), get_rhr_day(cdate) liefert allMetrics.metricsMap.WELLNESS_RESTING_HEART_RATE[0].value. Beide sind Tages- Calls (kein Range) — der Sync sampelt sie daher NUR an Trainings-/Aktivitäts- Tagen (+ heute), nicht 900 Tage einzeln. VO2Max ändert sich ohnehin nur nach qualifizierenden Aktivitäten.

eGYM Satz-Zeitstempel — NICHT vorhanden ❌. Die eGYM-Workout-Rohdaten tragen einen Zeitstempel nur pro Übung (exercise.completedAt), NICHT pro Satz. Ein Set-Objekt hat: setType, numberOfReps, weight, duration, heartRate, speed, incline, recoveryTime, trainingMethod — kein eigenes Datum. Folge: eine satz-genaue Puls-Overlay über Zeitstempel ist NICHT möglich. (Jeder Satz trägt aber eine eigene duration und ein heartRate-Feld — eine satz-näherungsweise HR-Sicht wäre darüber denkbar, ohne exakte Uhrzeit.)

Tabelle wellness_metrics (SILV-301)

wellness_metrics(id, user_id, recorded_at, type, value, source, ext_id,
                 UNIQUE(user_id, type, source, ext_id))

GET /api/wellness/{type}

{type}vo2max | resting_hr (sonst 404). Query: user_id, days=365.

{
  "type": "VO2MAX",
  "points": [{"recorded_at": "...", "value": 48.1, "source": "garmin", "is_manual": false}, ...],
  "verdict": {"tone": "gut", "text": "Über deinem Schnitt, steigend",
              "direction": "steigend", "level": "über"} ,
  "latest": 48.1
}

GET /api/egym/machine/{activity_id}/volume (SILV-302)

Query: user_id, days=365. 404 bei unbekanntem Gerät.

{
  "activity_id": 998, "label": "EGYM Brustpresse",
  "points": [{"visit_id": 1, "date": "2026-06-13", "volume_kg": 660.0,
              "top_set": 55.0, "reps": 24}, ...],
  "verdict": {"is_pr": true, "tone": "gut",
              "verdict_text": "Neue Bestleistung an EGYM Brustpresse: 660 kg bewegt"}
}

GET /api/activity/tdee (SILV-307)

Query: user_id, weeks=4. Gemessenes Erhaltungs-Budget (Adaptive TDEE).

{"tdee": 2130, "avg_intake": 1981, "weight_trend_kg_per_week": -0.14,
 "span_days": 18, "weeks": 3, "logged_days": 16, "weight_points": 12}

Apple-Health-Wellness-Ingest (progressiv)

POST /api/health/sync nimmt zusätzlich wellness: [{ext_ref, recorded_at, type, value}] (additiv, progressiv — Client liefert es, sobald er HealthKit liest). Idempotent über ext_ref (HealthKit-UUID); Antwort trägt wellness_synced.

GET /api/body/headline (SILV-123)

Query: user_id. EIN server-formulierter Hero-Satz zur Verfassung + tone.

{"text": "Leichter — dein Defizit wirkt", "tone": "gut"}

GET /api/protein/goal (SILV-305)

Query: user_id. Tägliches Eiweiß-Ziel aus dem aktuellen Körpergewicht.

{"goal_g": 125, "basis_kg": 78.3, "g_per_kg": 1.6}

GET /api/body/recomp (SILV-306)

Query: user_id. Body-Recomposition-Verdikt, FITHUB-only (gleiche Messmethode — nie Garmin↔FITHUB, ADR-30/Methoden-Rauschen).

{"text": "Gewicht stabil — Muskel +0,6 kg, Fett −0,5 kg", "tone": "gut",
 "muscle_delta": 0.6, "fat_delta": -0.5, "window_days": 47}

Offen (eigenes Ticket)

kcal-Ehrlichkeit (gemessen verdrängt eGYM-Schätzung, Dämpfung des Fallbacks) = SILV-313 (Sub von SILV-121) — kalibrier-gated, NICHT hier.