2026-09-12 14:35:57 +02:00
# BLS Food Reference – technische Spec
2026-09-14 12:47:12 +02:00
**Stand:** 2026-09-14 · Migration **062** – **069**
2026-09-12 14:35:57 +02:00
## Tabellen
- `food_attributes` — dynamischer Stoff-/Merkmalskatalog (`attr_key`, `data_type` , `origin` )
- `food_catalog` — Lebensmittel (`bls_code` UNIQUE bei official_bls; manuell ohne BLS-Code)
- `food_attribute_values` — typisiertes EAV
- `food_name_mappings` — FDDB-Name → `food_id` (User / global)
- `nutrition_items` — Tagebuchzeilen inkl. `logged_at`
- `nutrition_daily_nutrients` — Tages-Rollup numerischer Attribute
- `nutrition_day_marks` — `fasting` | `incomplete`
2026-09-12 18:23:01 +02:00
- `food_recipes` / `food_recipe_ingredients` — Mitai-Listen/Kombinationen (Rohmischung, kein Kochschwund); `nutrition_items.recipe_id` . Kein Tabellen-Rename. Gekochte Gerichte später Tandoor + `cooked_yield_g` , nicht hier.
2026-09-12 14:35:57 +02:00
## Layer 1
- `data_layer/food_mapping.py` — Normalisierung, Lookup, Learn, Apply, Delete
- `data_layer/nutrition_items.py` — Ingest, drei Makro-Summen, Policy, `resolve_*_attributes`
2026-09-12 18:23:01 +02:00
- `data_layer/food_recipes.py` — Listen-Upsert, Link auf Tagebuchzeilen, Listen-Makros (Skala: gegessen_g / Summe Zutaten, sonst 1/Portionen). Diese Skala gilt nur für Roh-Kombinationen, nicht für gekochte Gerichte.
2026-09-12 15:31:20 +02:00
- `bls/recipe_parser.py` — `produkte` -Feld: Split nur vor nächstem `\d+ (g|kg|ml|l)` (Kommas im Namen bleiben)
2026-09-12 14:35:57 +02:00
## Import
BLS: Admin-Upload Components + Daten-XLSX (`backend/bls/parser.py`). Upsert über `bls_code` / `attr_key` , nie Delete+Insert offizieller Zeilen.
FDDB: Items persistieren; `nutrition_log` nur bei leerem Tag oder laut Policy / Bestätigung.
## Router
2026-09-13 15:47:49 +02:00
- `/api/bls/*` — Suche, Attribute-Suche, eigene Foods (`attributes` + `serving_g` ), eigene Mappings; `POST /bls/attributes` legt Extension-Stoff an (Name + Einheit), `GET /bls/attributes?keys=EPA,DHA`
2026-09-14 13:01:41 +02:00
- `GET /bls/foods/mine` — eigene `manual_user` -Einträge; `GET /bls/foods/shared` — zentrale `manual_admin` ; `GET /bls/foods/{id}` — Editor-Payload inkl. `visibility` und `editable` (Owner persönlich; Admin auch zentral); `PUT /bls/foods/manual/{id}` — nur Owner. Zentrale Updates: `PUT /admin/bls/foods/manual/{id}` (Admin). Suche `?scope=global` blendet persönliche Foods aus (Admin-Mappings).
2026-09-14 12:47:12 +02:00
- Sichtbarkeit: `data_layer/food_visibility.py` . Lookup-Mapping: User-Zeile nur wenn Food sichtbar; globale Mapping-Zeile nur `owner_profile_id IS NULL` . `upsert_food_mapping` erzwingt dasselbe. Vorschläge sortieren persönlich → zentral → BLS.
2026-09-14 13:01:41 +02:00
- `GET /admin/bls/foods?kind=manual` — `manual_user` /`manual_admin`. `POST /admin/bls/foods/manual` — zentrale Foods. `PUT /admin/bls/foods/manual/{id}` — zentral **und** persönlich (`attributes` + `serving_g` ). `GET /admin/bls/foods/{id}?editor=true` — Editor-Payload. Migration **069** : Owner-Check (persönlich hat Owner, BLS/Admin nicht).
2026-09-12 16:59:24 +02:00
- Migration **065** — Extension-Keys EPA/DHA/DPA/ALA/OMEGA3/OMEGA6 falls BLS sie nicht unter diesem Key hat
2026-09-14 13:09:44 +02:00
- `/api/admin/bls/*` — Import, Katalog, Attribute. `GET /admin/bls/attributes` liefert `group_key` /`group_label` (BLS-Gruppen, sortiert). `PUT /admin/bls/attributes/{id}` ändert Name/Einheit/Gruppe (nicht `attr_key` ). `POST /admin/bls/attributes/fill-labels` füllt leere Namen und fehlende Gruppen aus BLS-4.0-Glossar (`bls/attribute_labels.py`). Lebensmittel-Import schreibt fehlende Namen aus Spaltenköpfen. Makro-Rollup: `catalog_macros_for_item` liest nur ENERCC/PROT625/FAT/CHO → `nutrition_log` ; `rebuild_daily_nutrients` summiert alle `num_per_100g` nach `nutrition_daily_nutrients` .
2026-09-12 14:35:57 +02:00
- `/api/admin/food-mappings` — Admin-CRUD
- `/api/nutrition/*` — Items, Unmapped, Bulk-Map, Marken, Konflikt-Resolve
2026-09-12 16:59:24 +02:00
- `GET/POST /api/nutrition/recipes` , `PUT/DELETE /api/nutrition/recipes/{id}` , `POST …/import-fddb-lists` , `POST …/{id}/apply`
- `PUT /api/admin/food-mappings/{id}` — Ziel-Lebensmittel einer bestehenden Zuordnung wechseln
2026-09-12 18:23:01 +02:00
- Unmapped = Tagebuchzeilen ohne `food_id` /`recipe_id` **plus** Listenzutaten ohne Mapping
2026-09-14 12:47:12 +02:00
- Frontend: Inline-Vorschläge auf Zuordnen (`food_suggest.py`: Collapse-Key ohne Leerzeichen, Index 5 Min. Cache, Fett-%-Zahlen 9,5/10), `FoodSearchModal` nur noch Zusatzsuche (Abort + Debounce). Eigene Foods: Tab **Lebensmittel** (`MyFoodsPanel` + `FoodEditDialog` ). Listen-UI: `FoodRecipeDialog` (Suche gelernt + Katalog; Katalogpick upsertet Mapping). Offene Zutaten: `kind=recipe_ingredient` (API-Wert unverändert, UI: Listenzutat).
2026-09-13 10:36:08 +02:00
- Tandoor (Modul `tandoor` ): Migration **067** `profile_tandoor_settings` ; `GET/PUT /api/tandoor/settings` (Token nur `token_set` / `token_hint` ); `POST /api/tandoor/test` (10/min, kein Redirect-Follow). Client: `data_layer/tandoor_client.py` . Import und `cooked_yield_g` später.
2026-09-12 18:23:01 +02:00
- Migration **066** — `food_recipe_ingredients.quantity_amount` + `source_unit` ; `quantity_g` = Umrechnung (`resolve_quantity` in `food_mapping.py` ). `GET /bls/units` = Einheitenkatalog (EL/TL/Prise/Stück, default_g).
2026-09-13 10:36:08 +02:00
- `GET /nutrition/unmapped?since_days=28` — nur Namen mit `last_date` im Fenster; `count_only` liefert `{count, total, since_days}` ; `meta=true` liefert `{items, count, total, since_days}` . Listenzutaten-Datum: letzte `recipe_id` -Nutzung oder `food_recipes.updated_at` /`created_at` — kein Join über den Listennamen auf alle Tagebuchzeilen. Migration **068** : Partial-Index unmapped-open + `(profile_id, recipe_id, date)` .
- `GET /nutrition/recipes?brief=true` — Listen ohne Zutaten, nur `ingredient_count` . `GET /nutrition/recipes/{id}` lädt eine Liste. FDDB-Listenimport antwortet nach Upsert; `rebuild_daily_nutrients` im Hintergrund (`rebuild_pending`).
2026-09-13 12:40:53 +02:00
- Listen-Verknüpfung (`apply` / `link_recipes_to_items` ) matcht über `names_match_list` (Mengen-Prefix in gespeichertem `source_name_normalized` zählt). Unmapped-Zeilen bekommen `matching_recipe_id` + `matching_recipe_name` . Frontend: Tab Ernährung → Listen.
2026-09-13 14:38:42 +02:00
- `GET /nutrition/ingredient-names` — Distinct-Zutatennamen aller Listen inkl. Mapping-Status. Listendialog mappt manuell (`upsertMyFoodMapping`) an der Zeile.
2026-09-12 16:14:24 +02:00
- Mapping-Schreiben und Nährwert-Rebuild sind getrennte Transaktionen; Rebuild läuft nach der API-Antwort im Hintergrund (UI bleibt bedienbar)
2026-09-12 15:49:14 +02:00
- `food_name_mappings.grams_per_unit` / `source_unit` (Migration **064** )
- `GET/POST /api/nutrition/food-knowledge` — portable JSON (`mitai-food-knowledge` v1): manuelle Foods, Mappings (über `bls_code` / Name, keine UUIDs), Listen. Import löst Katalog auf dem Zielsystem auf (BLS muss dort importiert sein).