Dynamisches kcal-Budget — Vertrag (live)
StratoClaude → MacClaude, 21.06.2026 (SILV-336, Sub von SILV-298 „Ring-/Budget-Redesign Weg B"). Antwort auf die Backend-Bestellung über den Agent-Bus. Der Server erzeugt die Budget-Bedeutung, der Client rendert nur (Anti-Drift). Code-belegt:
app/backend/main.py(Budget-Kern + Endpoints),app/backend/db.py(Tabellenbudget_settings,day_budget); Contract-Testsapp/tests/contract/test_budget.py.
Modell (gelockt, SILV-298)
- Erhaltung = gemessene TDEE (
/api/activity/tdee, SILV-307). Quellemeasured. Bei <14 getrackten Tagen Fallback aufusers.kcal_budget(Quellefallback). Manueller Override = Quelleoverride. - Ziel = Erhaltung − Defizit. Defizit/Tag =
rate_kg_woche × 7700 / 7(Vorzeichen: + abnehmen, 0 halten, − aufbauen). - Boden =
boden_pct × Erhaltung(Default 50 %). - Trainingstag: dedizierte Aktivitäts-kcal des Tages >
training_threshold(Default 500) ⇒ Ziel bekommttraining_bonus_pct × activity_kcal(Default 50 %) obendrauf. Schwelle und Anteil sind konfigurierbar. - Keine generelle Aktivitäts-Addition mehr. Das alte
kcal_budget = base + activityfällt weg.activity_kcalbleibt als Info-Feld - Trainingstag-Trigger, wird nicht mehr aufs Basis-Budget addiert.
Formeln (Client-Live-Vorschau MUSS identisch rechnen)
defizit_tag = rate_kg_week × 7700 / 7
boden = boden_pct × erhaltung
ziel = max(erhaltung − defizit_tag, boden) # gesund gekappt
ziel_eff = ziel + (activity_kcal > schwelle ? bonus_pct × activity_kcal : 0)
kcal_left = ziel_eff − eaten
Gesund-Kappung (ehrlicher Default, StratoClaude-Entscheidung): das Ziel wird
nie unter den Boden gedrückt — der Boden ist der ehrliche Mindestwert
(boden_pct × Erhaltung, konfigurierbar). So gibt es nur einen Schwellenwert
statt einer zweiten Magic-Zahl. Greift die Kappung, ist capped=true und
cap_hint trägt einen Hinweistext. Wer einen härteren absoluten Boden will, hebt
boden_pct.
Per-Tag einfrieren — kein Zurückrechnen (hart, Dennis 21.06.)
Die Erhaltung (TDEE) ist ein rollender Messwert. Ohne Einfrieren würde ein Tag von vor drei Wochen mit dem heutigen TDEE-Fenster gerendert — das ist explizit verboten. Darum:
- Vergangener Tag mit eingefrorenem Snapshot (
day_budget) → der Snapshot wird wörtlich zurückgegeben. Nie neu gerechnet (auch die Erhaltung nicht aus einem späteren TDEE-Fenster). - Heute rechnet live bis zum Einfrieren (liest nie aus
day_budget). - Vergangener Tag ohne Snapshot (Alt-Tag vor diesem Feature, oder Tag ohne Signal) → Best-Effort live, nicht persistiert (keine Rückdatierung mit falschem Fenster).
- Eingefroren wird per Maintenance-Freeze
POST /api/budget/freeze(Cron 03:45, friert „gestern" je Nutzer mit Signal).INSERT OR IGNORE⇒ ein einmal eingefrorener Tag ist unantastbar; erneutes Einfrieren ist idempotent (frozen=0). Anker: die TDEE wird mit Fenster endend am einzufrierenden Tag gerechnet (_tdee_as_of), nicht „jetzt". Konsistent zu SILV-111.
Was man isst (eaten) ist immer live — nur die Bedeutung (Erhaltung/Ziel/
Boden/Bonus) friert ein. kcal_left = ziel_eff(eingefroren) − eaten(live).
DTO (additiv in day_summary, /api/diary/range-Einträgen, Snapshot)
| Feld | Typ | Bedeutung |
|---|---|---|
erhaltung_kcal |
int | Erhaltung des Tages |
erhaltung_source |
measured|fallback|override |
Herkunft der Erhaltung |
erhaltung_calc_kcal |
int|null | errechneter TDEE als Hinweis (auch bei Override); null bei cold-start |
ziel_kcal |
int | Ziel (gekappt) |
boden_kcal |
int | Boden |
training_bonus_kcal |
int | angewandter Trainingstag-Bonus (0, wenn unter Schwelle) |
ziel_eff_kcal |
int | effektives Ziel = ziel + bonus |
capped |
bool | wurde aufs Boden-Minimum angehoben? |
cap_hint |
string|null | Hinweistext, wenn capped |
activity_kcal |
int | Tages-Aktivitäts-kcal (nur Info, eGYM-Verdrängung ADR-32) |
zielrichtung |
abnehmen|halten|aufbauen |
SILV-407, rein abgeleitet aus dem Vorzeichen von rate_kg_per_week (+ abnehmen, 0 halten, − aufbauen) — keine eigene Spalte |
deadband_kcal |
float | SILV-407, wirksame Totzone für die Client-Einfärbung der Aufnahmezahl (Aufnahme vs. verbrauch_kcal, s. aktivitaet-konsolidierung-vertrag.md); Default 150, Bereich 0..1000 |
Legacy-Felder bleiben (Web-Frontend lauffähig), aber neu gemappt:
kcal_base = erhaltung_kcal, kcal_budget = ziel_eff_kcal,
kcal_left = ziel_eff_kcal − kcal_eaten, kcal_activity = activity_kcal.
Settings-API (versioniert, valid_from = heute)
GET /api/budget/settings?user_id=&date= → effektive Einstellungen für den Tag
(Default heute) + erhaltung_calc_kcal (TDEE-Hinweis) + valid_from (null, wenn
nur Defaults greifen):
{ "rate_kg_per_week": 0.0, "erhaltung_override_kcal": null, "boden_pct": 0.5,
"training_threshold_kcal": 500.0, "training_bonus_pct": 0.5,
"valid_from": "2026-06-21", "erhaltung_calc_kcal": null }
PUT /api/budget/settings (Body: user_id, rate_kg_per_week,
erhaltung_override_kcal, boden_pct, training_threshold_kcal,
training_bonus_pct, deadband_kcal) → speichert eine neue Version ab heute;
alte Tage bleiben unberührt. Mehrfaches PUT am selben Tag überschreibt die heutige
Version. Gibt die effektiven Werte zurück. Effektiv für Tag D = jüngste Version
mit valid_from ≤ D; ohne Version greifen die Defaults. Achtung: eine heute
gesetzte Version wirkt NIE auf einen Tag vor valid_from — auch nicht rückwirkend
per Freeze (der Freeze nutzt die zum Zieltag effektive Version, s. o.).
deadband_kcal ist serverseitig validiert (0 ≤ x ≤ 1000, HTTP 422 sonst).
zielrichtung ist NICHT Teil von Settings (rein abgeleitet, s. DTO oben) und
erscheint daher nicht in der GET/PUT-Antwort.
Defaults
rate_kg_per_week=0 · erhaltung_override_kcal=null · boden_pct=0.5 ·
training_threshold_kcal=500 · training_bonus_pct=0.5 · deadband_kcal=150.