Zuletzt aktualisiert:

Energie — Vertrag (Budget v2 · Erhaltungsmessung · Energiekurve · Trainingsplan · Vorlagen · Woche)

StratoClaude → MacClaude, 28.08.2026 (Energie-Epic SILV-415, Backend-Subs SILV-416–421; Coach-Spec 28.08. = app/ios-native/reimplementierung/energiekurve/ spec-coach-2026-08-28.md, Synthese app/ios-native/syntheses/energie.md). Antwort auf die Bus-Bestellung; byte-genau gegen §5 der Synthese, Abweichungen markiert. Der Server erzeugt die Bedeutung, der Client zeichnet nur (Anti-Drift). Code: app/backend/energy.py (Rechenkern), app/backend/main.py (Endpoints, Budget-Kern, Hooks), app/backend/intervals.py (Pull), db.py (Migration). Contract-Tests: app/tests/contract/test_budget.py, test_energy_maintenance.py, test_energy_curve.py, test_plan_sessions.py, test_template_meals.py, test_energy_week.py. Ersetzt die Formel in budget-vertrag.md (Weg B, SILV-336 — dort nur noch Verweis).

Alle neuen Felder additiv; Legacy-Felder bleiben lesbar (Mapping unten). Zeiten sind lokal Europe/Berlin zum jeweiligen Tag (HH:MM); ISO-Samples mit Offset werden serverseitig nach Berlin umgerechnet (Spec §11: Zone des Tages, nicht des Geräts).

1. Budget v2 (SILV-416) — ersetzt Weg B

basis    = erhaltung_gemessen − activity_mean(56 d)      # Quelle: override > measured > fallback
defizit  = rate_kg_per_week × 7700 / 7                    # = rate × 1100
budget   = max(basis − defizit + activity_kcal, boden)    # gesund gekappt (capped=true)
boden    = boden_pct × (basis + activity_kcal)            # boden_pct Default 0,6

DTO (in day_summary = /api/day, /api/bootstrap, alle Diary-/Aktivitäts-Mutationsantworten; /api/diary/range je Eintrag; Snapshot day_budget)

Feld Typ Bedeutung
erhaltung_basis_kcal int Basis des Tages (Nicht-Trainings-Erhaltung)
erhaltung_basis_source measured|fallback|override Herkunft der Basis
activity_mean_kcal int|null Ø activity_kcal im 56-d-Fenster (auch ohne Erhaltungs-Messung definiert)
budget_kcal int basis − defizit + activity (gekappt) — die Zahl fürs Ring-Ziel
defizit_kcal int rate × 1100
boden_kcal int v2-Boden = boden_pct × (basis + activity)
boden_pct float wirksamer Boden-Anteil
activity_kcal int s. o. (gemessen + assumed/confirmed Plan)
capped / cap_hint bool / string|null Gesund-Kappung
formula_version int 2 (live + neue Freezes); 1 = Weg-B-Snapshot (Felder abgeleitet)
recomputed_at ISO|null Spec §4.4: Tag nach dem Freeze durch Nachsync neu verrechnet
zielrichtung, deadband_kcal unverändert (SILV-407)
erhaltung_kcal, erhaltung_source, erhaltung_calc_kcal Legacy, s. u.

Legacy-Mapping (volle Ring-Runde bleibt lesbar): erhaltung_kcal = basis + activity_kcal · ziel_kcal = budget − activity_kcal · ziel_eff_kcal = budget_kcal · training_bonus_kcal = 0 · kcal_budget = budget_kcal · kcal_base = erhaltung_kcal · kcal_left = budget − eaten · erhaltung_calc_kcal = erhaltung_gemessen (Hinweis, auch bei Override). v1-Snapshots (formula_version=1, vor 28.08. eingefroren): erhaltung_basis_kcal := erhaltung (Weg-B-Erhaltung), budget_kcal := ziel_eff, defizit_kcal := rate × 1100, activity_mean_kcal = null — kein Zurückrechnen (Dennis 21.06.).

Settings (GET/PUT /api/budget/settings, versioniert valid_from = heute)

PUT Body: user_id, rate_kg_per_week (−2…2), erhaltung_basis_override_kcal (null | 800…6000), boden_pct (0,3…0,9; Default 0,6), deadband_kcal (0…1000), protein_g_per_kg (0,8…3,0; Default 1,6). 422 außerhalb der Bereiche. GET ?user_id&date= liefert die effektiven Werte + valid_from, formula_version und die Hinweise erhaltung_calc_kcal (gemessene Erhaltung), erhaltung_basis_calc_kcal, activity_mean_kcal, erhaltung_se_kcal, maintenance_quality — die Client-Live-Vorschau MUSS mit genau diesen Zahlen die Formel oben rechnen.

Freeze (03:45) + Nachsync (Spec §4.4, AC 6)

Der Freeze friert erhaltung_basis, activity_mean, erhaltung_basis_source, defizit, boden_pct (+ formula_version=2) ein. Nur activity_kcal darf danach nachziehen: bei jedem Reconcile (Health-Sync/Garmin/eGYM), manueller Aktivität oder Plan-Status- Änderung für einen eingefrorenen Tag werden activity_kcal, budget, boden, ziel(_eff), erhaltung, capped neu verrechnet und recomputed_at gesetzt (unveränderte Aktivität → kein Schreiben). v1-Tage (Weg B, formula_version 1) werden seit SILV-426 NUR in activity_kcal + v1-Trainingsbonus/ziel_eff nachgezogen (v1-Formel: bonus = pct × activity, wenn activity > threshold) — nie mit v2-Mathe; Erhaltung/Ziel/Boden bleiben eingefroren. Grund: ein nach dem Freeze korrigierter Tag (Dedup-Reparatur, später Uhr-Sync) bliebe sonst für immer auf dem falschen activity_kcal. SSE day_changed feuert über den bestehenden Chokepoint (_la_budget_sync).

2. Erhaltungsmessung (SILV-417) — GET /api/energy/maintenance?user_id&days=56[&today=]

{ erhaltung_gemessen, erhaltung_basis, activity_mean, intake_mean,
  slope_kg_per_week, slope_se, erhaltung_se, n_weight_points, n_logged_days,
  window_from, window_to, quality: "ok"|"short"|"gap",
  suspect_incomplete_days: [YYYY-MM-DD…], phantom_days: [YYYY-MM-DD…],
  days, ref_date, reason? }

3. Energiekurve (SILV-418) — GET /api/energy/curve?user_id&date[&today=&now=HH:MM]

{ date, now: "HH:MM"|null, bmr_kcal, resting_source: "basis",     # seit SILV-428 immer "basis"
  points:   [{t:"HH:MM", bilanz:int, state: measured|modeled|projected}],   # 00:00 … 23:55 + "24:00"
  segments: [{start, end, kcal, kind: resting|activity|neat,
              state: modeled|measured|discarded,
              source: formula|garmin|apple_health|iphone|egym|manual|plan,
              planned_session_id?, activity_id?}],
  intake:   [{t, kcal, state: actual|planned, entry_id?, planned_meal_id?, label?}],
  reference: { floor_kcal: -400, target_end_kcal: -defizit },
  kpi: { low_kcal, low_at:"HH:MM", hours_below_400, end_kcal, end_delta_kcal },
  budget_kcal, activity_kcal, recomputed_at: ISO|null }

4. Trainingsplan (SILV-419) — Intervals.icu read-only

4a. Einheiten bearbeiten, sync-geschützt (SILV-449, Coach-Spec §6.5/N9 — Lock Dennis 04.09.: lokal überschreiben, Intervals unangetastet)

5. Vorlagen-Mahlzeiten im Wochenplan (SILV-420)

SILV-451 (N13, P3, N14 — 05.09.2026): - Produkt-Zeilen (N13): trägt ein Artikel foods.plan_role = "gel" (PATCH /api/foods/{id} {plan_role: "gel"}, genau ein Artikel je Rolle, "" löscht), bucht die long-Vorlage ihre Gel-Zeilen mit den Etikettwerten je Portion (portion_g, sonst 100 g) und food_id statt des 3,5-%-Anteils (NRGY Gel 45: 180 kcal / 45 g KH statt 111 / 27,8). Preview + PlanEntry tragen food_id auf der Vorlagen-Zeile. Prod: Artikel 1350 „Nrgy unit gel" trägt die Rolle. - Vortags-Zeile (Defect P3): der KH-Bonus (long +300 / threshold +200) hängt sich an die nächstgelegene geplante Vorlagen-Abendzeile des Vortags (17:30–21:30, Vorrang: schon KH-betont, dann „Abendessen…", dann Nähe zu 19:00) — egal aus welcher Vorlage/Einheit — statt nur an eine Zeile exakt 19:00; Label wird <Original> · KH-betont (+300 kcal vor dem Long Run), idempotent. Nur wenn der Vortag keine Abendzeile hat, entsteht die eigene 19:00-Zeile. Keine doppelten Abendzeilen mehr (Fall 04.09.). - planned_session_id in POST /api/plan (N14): optional für jede Zeile (Gericht/Artikel/frei, auch Coach-create); 404, wenn die Einheit nicht dem Nutzer gehört; PlanEntry trägt planned_session_id für alle Arten (null = ungebunden).

5a. Planen v2 — freie Plan-Mahlzeiten, Bearbeiten, Kopieren (SILV-431, Welle 1)

StratoClaude → MacClaude, 28.08.2026 (Epic SILV-429, Dennis-Lock „Planen v2"). Additiv. Contract-Tests test_plan_v2.py.

5b. Vorschau + Vorlagen-Katalog (SILV-433, Welle 2)

5c. Coach schreibt (SILV-435, Welle 3) — Verweis

Der Coach-MCP v2 schreibt über dieselbe Plan-API (Loopback) mit origin=coach + note; Einträge mit user_edited=1 fasst er nie an, nur heute/zukünftig, Audit-Log plan_audit (intern GET /api/plan/audit). Für den Client: Coach-Einträge erkennt man an origin=coach (+ note als Begründung); bearbeitet der Nutzer sie, werden sie user_edited und damit coach-unantastbar. Wahrheit: docs/syntheses/coach-mcp.md §9.

5d. Sync-Regel für den Vorlagen-Generator (SILV-438, Coach-Defect D2)

Vorfall 29.08.2026: der 07:50-Intervals-Sync regenerierte zwei Tage und überschrieb vier vom Coach bearbeitete Zeilen (+ zwei Löschungen), weil Coach-Bearbeitungen origin=template ließen. Seitdem gilt je berührtem Tag ab heute (_sync_plan_day, Worker + Testmode plan_sync):

  1. Reiner Vorlagen-Tag (alle offenen Plan-Mahlzeiten origin=template AND user_edited=0) → regenerieren wie bisher (Tombstones §5a bleiben weg) — "regenerated".
  2. Berührter Tag (mindestens eine offene Plan-Mahlzeit mit origin ∈ {user, coach} oder user_edited=1) → nichts ersetzen, nichts anlegen. Hat sich die Einheit des Tages geändert (Ingest-Signatur je Tag: external_id, Slot, Typ, Status, expected — changed in der Ingest-Antwort), werden nur planned_session_id der Vorlagen-Zeilen auf die Einheit des Tages nachgezogen (null, wenn entfernt) und session_changed_at (ISO) auf allen offenen Plan-Mahlzeiten des Tages gesetzt — "relinked"; sonst "unchanged". Der Coach sieht session_changed_at in plan_get/PlanEntry und passt die Zeiten selbst an; expected_kcal der Einheit steht am session-Objekt (§5e).
  3. Vergangene Tage nie. Gericht-/Artikel-Einträge (kind=dish) zählen nicht als „berührt" (der Generator hat sie nie angefasst) — nur Plan-Mahlzeiten.

Ein expliziter generate (App/Coach) bleibt davon unberührt: er ersetzt nur unbearbeitete Generator-Zeilen (would_replace), lässt Coach-/Nutzer-Zeilen stehen (±60-min-Regel) und setzt Tombstones zurück.

5e. Wochen-DTO — GET /api/plan/week?user_id&from=<datum>[&today=] (SILV-442, Welle 4)

Für den Wochen-Editor (SILV-436) und den Coach (plan_get liest genau dies). from wird auf den Montag normiert; today pinnt den Stichtag (Tests). MUSS vor /api/plan/{start} liegen.

{ user_id, monday, sunday, today,
  days: [{ date,
           entries: [PlanEntry… (eigene + shared; mit origin/user_edited/note/outcome/session_changed_at)],
           planned_kcal, planned_protein_g,        # offene Plan-Einträge (status planned), 1 Portion
           kcal_eaten, protein_eaten_g,            # Tagebuch
           projected_kcal, projected_protein_g,    # gegessen + offen (= „Tag laut Plan")
           budget_kcal, protein_target_g,          # Ziel des Tages (Budget v2; Eiweiß kg × g/kg, sonst users.protein_target_g)
           activity_kcal, defizit_kcal,
           session: {id, type, slot_start, slot_end, expected_kcal, status, name,
                     planned_duration_min, template_id} | null,    # Einheit des Tages (§5 Wahl)
           session_changed_at: ISO|null,           # max über die Einträge (§5d)
           kpi: {low_kcal, low_at, hours_below_400, end_kcal, end_delta_kcal},   # Kurve MIT Plan (§3)
           reference: {floor_kcal, target_end_kcal},
           two_day: {dates:[d, d+1], projected_kcal, budget_kcal, delta_kcal} | null }] }

SILV-450 (N11/N12): jeder PlanEntry (auch kind=dish/Artikel) trägt time (HH:MM | null; Kurve und Zwei-Tage-Spange nehmen die Uhrzeit, Slot-Anker nur als Fallback) und lint[]; jeder Tag trägt lint[{rule, text, severity}] (Plan-Linter, sieben Regeln, nur heute + Zukunft — Vertrag im coach-app-vertrag §10). Supplement-Artikel (foods.is_supplement) zählen nicht in kcal_eaten/protein_eaten_g.

two_day nur an Tagen mit session.type ∈ {long, interval} (harter Tag + Folgetag gegen 2 × Budget — die Regel, nach der der Coach plant; Sonntag ohne Folgetag → null). Die Kurven- Kennzahlen sind dieselben wie /api/energy/curve (Zukunft = volle Projektion inkl. Plan).

6. Wochen-Rollup (SILV-421) — GET /api/energy/week?user_id[&monday&today]

{ user_id, monday, sunday, today, wochenbilanz_kcal, logged_days, hours_below_400_mean,
  protein_goal_g,
  days: [{ date, logged, partial, budget_kcal, activity_kcal, intake_kcal, bilanz_vs_budget_kcal,
           end_kcal, end_delta_kcal, hours_below_400, low_kcal, session_types: [type…],
           two_day_sum_kcal, two_day_target_kcal,
           carbs_24h_before_quality_g, carbs_target_g }],
  ea: { value, ffm_kg, ffm_source, ffm_date, band: ok|low|critical|null, uncertainty: 4,
        days, exercise_kcal_sum } | null }

7. Live-Signale + Betrieb

8. Testfixtures

Spec §9.1 (Long Run mit Vorlage) und §9.3 (Ruhetag) als Contract-Tests mit ±5 kcal (test_energy_curve.py), Beispieltage §1.2 (1.870 / 2.370 / 3.020) in test_budget.py, AC 2/4/5/6/7/8 in den jeweiligen Test-Dateien. Testmode-Helfer: POST /api/test/body-weight, POST /api/test/intervals-events (roher Intervals-Payload durch denselben Ingest; plan_sync: true fährt zusätzlich die Sync-Regel §5d wie der Worker, Antwort changed + plan_sync), POST /api/test/freeze-v1 (eingefrorener v1-Tag wie Prod vor valid_from, SILV-441). SILV-438/439/440/442 in test_plan_v2.py + test_coach_write.py, SILV-441 in test_energy_curve.py.