mitai-jinkendo/backend/routers/nutrition.py

748 lines
28 KiB
Python
Raw Permalink Normal View History

refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
"""
Nutrition Tracking Endpoints for Mitai Jinkendo
Handles nutrition data, FDDB CSV import, correlations, and weekly aggregates.
"""
import csv
import io
import uuid
import logging
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
from typing import Optional
from datetime import datetime
from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends
from fastapi.responses import Response
from pydantic import BaseModel
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
from db import get_db, get_cursor, r2d
from auth import require_auth, check_feature_access, increment_feature_usage
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
from routers.profiles import get_pid
from feature_logger import log_feature_usage
from data_layer.nutrition_body_merge import build_merged_daily_nutrition_body_rows
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
router = APIRouter(prefix="/api/nutrition", tags=["nutrition"])
logger = logging.getLogger(__name__)
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
class RecipeIngredientIn(BaseModel):
source_name_raw: str
quantity_raw: Optional[str] = None
quantity_g: Optional[float] = None
quantity_amount: Optional[float] = None
source_unit: Optional[str] = None
grams_per_unit: Optional[float] = None
class RecipeIn(BaseModel):
name_raw: str
portions: float = 1
description: Optional[str] = None
ingredients: list[RecipeIngredientIn] = []
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
# ── Helper ────────────────────────────────────────────────────────────────────
def _pf(s):
"""Parse float from string (handles comma decimal separator)."""
try: return float(str(s).replace(',','.').strip())
except: return 0.0
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.post("/import-csv")
async def import_nutrition_csv(
file: UploadFile = File(...),
overwrite_existing: bool = False,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
"""Import FDDB nutrition CSV (optional item persist + conflict policy)."""
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
pid = get_pid(x_profile_id)
# Phase 4: Check feature access and ENFORCE
# Note: CSV import can create many entries - we check once before import
access = check_feature_access(pid, 'nutrition_entries')
log_feature_usage(pid, 'nutrition_entries', access, 'import_csv')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {pid} blocked: "
f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
)
raise HTTPException(
status_code=403,
detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). "
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
)
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
raw = await file.read()
try: text = raw.decode('utf-8')
except: text = raw.decode('latin-1')
if text.startswith('\ufeff'): text = text[1:]
if not text.strip(): raise HTTPException(400,"Leere Datei")
from data_layer.nutrition_items import get_import_policy, replace_csv_items_for_dates
overwrite = bool(overwrite_existing)
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
reader = csv.DictReader(io.StringIO(text), delimiter=';')
item_rows = []
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
days: dict = {}
count = 0
for row in reader:
rd = row.get('datum_tag_monat_jahr_stunde_minute','').strip().strip('"')
if not rd: continue
try:
parts = rd.split(' ')
p = parts[0].split('.')
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
iso = f"{p[2]}-{p[1]}-{p[0]}"
logged_at = None
if len(parts) > 1:
try:
logged_at = datetime.strptime(rd.strip(), '%d.%m.%Y %H:%M')
except ValueError:
logged_at = None
except Exception:
continue
kcal = _pf(row.get('kj', 0)) / 4.184
fat = _pf(row.get('fett_g', 0))
carbs = _pf(row.get('kh_g', 0))
prot = _pf(row.get('protein_g', 0))
days.setdefault(iso, {'kcal': 0, 'fat_g': 0, 'carbs_g': 0, 'protein_g': 0})
days[iso]['kcal'] += kcal
days[iso]['fat_g'] += fat
days[iso]['carbs_g'] += carbs
days[iso]['protein_g'] += prot
name = (row.get('bezeichnung') or '').strip().strip('"')
if name:
item_rows.append({
"date": iso,
"logged_at": logged_at,
"food_name": name,
"quantity_raw": (row.get('menge') or '').strip() or None,
"kcal": kcal,
"protein_g": prot,
"fat_g": fat,
"carbs_g": carbs,
})
count += 1
inserted = 0
new_entries = 0
ingest_result = {"conflicts": [], "items_written": 0, "policy": "prompt"}
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
with get_db() as conn:
cur = get_cursor(conn)
policy = get_import_policy(cur, pid)
override = "overwrite_catalog" if overwrite else None
if item_rows:
ingest_result = replace_csv_items_for_dates(
cur, pid, item_rows, policy=policy, policy_override=override,
)
new_entries = ingest_result.get("new_log_days") or 0
inserted = ingest_result.get("days_written") or 0
else:
for iso, vals in days.items():
kcal = round(vals['kcal'], 1)
fat = round(vals['fat_g'], 1)
carbs = round(vals['carbs_g'], 1)
prot = round(vals['protein_g'], 1)
cur.execute("SELECT id FROM nutrition_log WHERE profile_id=%s AND date=%s", (pid, iso))
is_new = not cur.fetchone()
if not is_new:
if policy in ("overwrite_catalog", "overwrite_fddb") or overwrite:
cur.execute(
"UPDATE nutrition_log SET kcal=%s,protein_g=%s,fat_g=%s,carbs_g=%s,source='csv',macro_origin='fddb' WHERE profile_id=%s AND date=%s",
(kcal, prot, fat, carbs, pid, iso),
)
else:
cur.execute(
"INSERT INTO nutrition_log (id,profile_id,date,kcal,protein_g,fat_g,carbs_g,source,macro_origin,created) VALUES (%s,%s,%s,%s,%s,%s,%s,'csv','fddb',CURRENT_TIMESTAMP)",
(str(uuid.uuid4()), pid, iso, kcal, prot, fat, carbs),
)
new_entries += 1
inserted += 1
for _ in range(new_entries):
increment_feature_usage(pid, 'nutrition_entries')
return {
"rows_parsed": count,
"days_imported": inserted,
"new_entries": new_entries,
"items_written": ingest_result.get("items_written", 0),
"conflicts": ingest_result.get("conflicts") or [],
"policy": ingest_result.get("policy", "prompt"),
"date_range": {"from": min(days) if days else None, "to": max(days) if days else None},
}
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
@router.post("")
def create_nutrition(date: str, kcal: float, protein_g: float, fat_g: float, carbs_g: float,
x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Create or update nutrition entry for a specific date."""
pid = get_pid(x_profile_id)
# Validate date format
try:
datetime.strptime(date, '%Y-%m-%d')
except ValueError:
raise HTTPException(400, "Ungültiges Datumsformat. Erwartet: YYYY-MM-DD")
with get_db() as conn:
cur = get_cursor(conn)
# Check if entry exists
cur.execute("SELECT id FROM nutrition_log WHERE profile_id=%s AND date=%s", (pid, date))
existing = cur.fetchone()
if existing:
# UPDATE existing entry
cur.execute("""
UPDATE nutrition_log
SET kcal=%s, protein_g=%s, fat_g=%s, carbs_g=%s, source='manual', macro_origin='manual'
WHERE id=%s AND profile_id=%s
""", (round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1), existing['id'], pid))
return {"success": True, "mode": "updated", "id": existing['id']}
else:
# Phase 4: Check feature access before INSERT
access = check_feature_access(pid, 'nutrition_entries')
log_feature_usage(pid, 'nutrition_entries', access, 'create')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {pid} blocked: "
f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
)
raise HTTPException(
status_code=403,
detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). "
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
)
# INSERT new entry
new_id = str(uuid.uuid4())
cur.execute("""
INSERT INTO nutrition_log (id, profile_id, date, kcal, protein_g, fat_g, carbs_g, source, macro_origin, created)
VALUES (%s, %s, %s, %s, %s, %s, %s, 'manual', 'manual', CURRENT_TIMESTAMP)
""", (new_id, pid, date, round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1)))
# Phase 2: Increment usage counter
increment_feature_usage(pid, 'nutrition_entries')
return {"success": True, "mode": "created", "id": new_id}
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
@router.get("")
def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get nutrition entries for current profile."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""
SELECT n.*, m.mark_type, m.note AS mark_note
FROM nutrition_log n
LEFT JOIN nutrition_day_marks m
ON m.profile_id = n.profile_id AND m.date = n.date
WHERE n.profile_id=%s
ORDER BY n.date DESC
LIMIT %s
""",
(pid, limit),
)
rows = [r2d(r) for r in cur.fetchall()]
dates = [r["date"] for r in rows if r.get("date") is not None]
counts = {}
if dates:
cur.execute(
"""
SELECT date, COUNT(*) AS item_count
FROM nutrition_items
WHERE profile_id = %s AND date = ANY(%s)
GROUP BY date
""",
(pid, dates),
)
counts = {str(r["date"]): int(r["item_count"] or 0) for r in cur.fetchall()}
for r in rows:
r["item_count"] = counts.get(str(r.get("date")), 0)
return rows
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
@router.get("/by-date/{date}")
def get_nutrition_by_date(date: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get nutrition entry for a specific date."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s AND date=%s", (pid, date))
row = cur.fetchone()
return r2d(row) if row else None
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
@router.get("/correlations")
def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get nutrition data correlated with weight and body fat (Layer 1 Merge, siehe nutrition_body_merge)."""
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
pid = get_pid(x_profile_id)
return build_merged_daily_nutrition_body_rows(pid)
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
@router.get("/weekly")
def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get nutrition data aggregated by week."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s",(pid,weeks*7))
rows=[r2d(r) for r in cur.fetchall()]
if not rows: return []
wm={}
for d in rows:
# Handle both datetime.date objects (from DB) and strings
date_obj = d['date'] if hasattr(d['date'], 'strftime') else datetime.strptime(d['date'],'%Y-%m-%d')
wk = date_obj.strftime('%Y-W%V')
refactor: modular backend architecture with 14 router modules Phase 2 Complete - Backend Refactoring: - Extracted all endpoints to dedicated router modules - main.py: 1878 → 75 lines (-96% reduction) - Created modular structure for maintainability Router Structure (60 endpoints total): ├── auth.py - 7 endpoints (login, logout, password reset) ├── profiles.py - 7 endpoints (CRUD + current user) ├── weight.py - 5 endpoints (tracking + stats) ├── circumference.py - 4 endpoints (body measurements) ├── caliper.py - 4 endpoints (skinfold tracking) ├── activity.py - 6 endpoints (workouts + Apple Health import) ├── nutrition.py - 4 endpoints (diet + FDDB import) ├── photos.py - 3 endpoints (progress photos) ├── insights.py - 8 endpoints (AI analysis + pipeline) ├── prompts.py - 2 endpoints (AI prompt management) ├── admin.py - 7 endpoints (user management) ├── stats.py - 1 endpoint (dashboard stats) ├── exportdata.py - 3 endpoints (CSV/JSON/ZIP export) └── importdata.py - 1 endpoint (ZIP import) Core modules maintained: - db.py: PostgreSQL connection + helpers - auth.py: Auth functions (hash, verify, sessions) - models.py: 11 Pydantic models Benefits: - Self-contained modules with clear responsibilities - Easier to navigate and modify specific features - Improved code organization and readability - 100% functional compatibility maintained - All syntax checks passed Updated CLAUDE.md with new architecture documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:15:35 +01:00
wm.setdefault(wk,[]).append(d)
result=[]
for wk in sorted(wm):
en=wm[wk]; n=len(en)
def avg(k): return round(sum(float(e.get(k) or 0) for e in en)/n,1)
result.append({'week':wk,'days':n,'kcal':avg('kcal'),'protein_g':avg('protein_g'),'fat_g':avg('fat_g'),'carbs_g':avg('carbs_g')})
return result
@router.get("/import-history")
def import_history(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get import history by grouping entries by created timestamp."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("""
SELECT
DATE(created) as import_date,
COUNT(*) as count,
MIN(date) as date_from,
MAX(date) as date_to,
MAX(created) as last_created
FROM nutrition_log
WHERE profile_id=%s AND source='csv'
GROUP BY DATE(created)
ORDER BY DATE(created) DESC
""", (pid,))
return [r2d(r) for r in cur.fetchall()]
@router.get("/items")
def list_nutrition_items(
date: Optional[str] = None,
limit: int = 200,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
if date:
cur.execute(
"""
SELECT i.*, f.name_de AS food_name_de, f.bls_code, f.catalog_kind,
r.name_raw AS recipe_name_de
FROM nutrition_items i
LEFT JOIN food_catalog f ON f.id = i.food_id
LEFT JOIN food_recipes r ON r.id = i.recipe_id
WHERE i.profile_id=%s AND i.date=%s
ORDER BY i.logged_at NULLS LAST, i.source_name_raw
""",
(pid, date),
)
else:
cur.execute(
"""
SELECT i.*, f.name_de AS food_name_de, f.bls_code, f.catalog_kind,
r.name_raw AS recipe_name_de
FROM nutrition_items i
LEFT JOIN food_catalog f ON f.id = i.food_id
LEFT JOIN food_recipes r ON r.id = i.recipe_id
WHERE i.profile_id=%s
ORDER BY i.date DESC, i.logged_at NULLS LAST
LIMIT %s
""",
(pid, min(limit, 500)),
)
from data_layer.nutrition_items import annotate_nutrition_item
return [annotate_nutrition_item(cur, pid, r2d(r)) for r in cur.fetchall()]
@router.get("/unmapped")
def list_unmapped_foods(
since_days: int = 0,
count_only: bool = False,
meta: bool = False,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_mapping import list_unmapped_payload
pid = x_profile_id or session["profile_id"]
with get_db() as conn:
return list_unmapped_payload(get_cursor(conn), pid, since_days, count_only, meta)
@router.get("/ingredient-names")
def list_food_ingredient_names(
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import list_known_ingredient_names
pid = get_pid(x_profile_id)
with get_db() as conn:
return list_known_ingredient_names(get_cursor(conn), pid)
@router.get("/recipes")
def list_food_recipes(
brief: bool = False,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import list_recipes
pid = get_pid(x_profile_id)
with get_db() as conn:
return list_recipes(get_cursor(conn), pid, with_ingredients=not brief)
@router.get("/recipes/{recipe_id}")
def get_food_recipe(
recipe_id: str,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import get_recipe
pid = get_pid(x_profile_id)
with get_db() as conn:
rec = get_recipe(get_cursor(conn), pid, recipe_id)
if not rec:
raise HTTPException(404, "Rezept nicht gefunden")
return rec
@router.post("/recipes")
def create_food_recipe(
body: RecipeIn,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import save_recipe
from data_layer.nutrition_items import schedule_daily_nutrient_rebuild
pid = get_pid(x_profile_id)
try:
with get_db() as conn:
saved = save_recipe(get_cursor(conn), pid, body.model_dump())
schedule_daily_nutrient_rebuild(pid, saved.get("dates_linked") or [], "Liste angelegt")
return saved
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.put("/recipes/{recipe_id}")
def update_food_recipe(
recipe_id: str,
body: RecipeIn,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import save_recipe
from data_layer.nutrition_items import schedule_daily_nutrient_rebuild
pid = get_pid(x_profile_id)
try:
with get_db() as conn:
saved = save_recipe(get_cursor(conn), pid, body.model_dump(), recipe_id)
schedule_daily_nutrient_rebuild(pid, saved.get("dates_linked") or [], "Liste geändert")
return saved
except KeyError:
raise HTTPException(404, "Rezept nicht gefunden") from None
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.delete("/recipes/{recipe_id}")
def delete_food_recipe(
recipe_id: str,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_recipes import delete_recipe
pid = get_pid(x_profile_id)
try:
with get_db() as conn:
delete_recipe(get_cursor(conn), pid, recipe_id)
except KeyError:
raise HTTPException(404, "Rezept nicht gefunden") from None
return {"ok": True}
@router.post("/recipes/import-fddb-lists")
async def import_fddb_lists(
file: UploadFile = File(...),
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from bls.recipe_parser import parse_fddb_lists_csv
from data_layer.food_recipes import upsert_recipes
pid = get_pid(x_profile_id)
raw = await file.read()
if not raw:
raise HTTPException(400, "Leere Datei")
try:
text = raw.decode("utf-8-sig")
except UnicodeDecodeError:
text = raw.decode("latin-1")
recipes = parse_fddb_lists_csv(text)
if not recipes:
raise HTTPException(400, "Keine Rezepte in der Datei erkannt")
with get_db() as conn:
cur = get_cursor(conn)
stats = upsert_recipes(cur, pid, recipes)
dates = stats.pop("dates_linked", []) or []
from data_layer.nutrition_items import schedule_daily_nutrient_rebuild
schedule_daily_nutrient_rebuild(pid, dates, "FDDB-Listenimport")
return {"ok": True, "recipes": len(recipes), "rebuild_pending": len(dates), **stats}
@router.post("/recipes/{recipe_id}/apply")
def apply_recipe_name(
recipe_id: str,
body: dict,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.food_mapping import normalize_food_name
from data_layer.food_recipes import apply_recipe_to_items
from data_layer.nutrition_items import dates_for_normalized_name, rebuild_daily_nutrients
pid = get_pid(x_profile_id)
source_name = (body.get("source_name") or "").strip()
if not source_name:
raise HTTPException(400, "source_name fehlt")
norm = normalize_food_name(source_name)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT id FROM food_recipes WHERE id = %s AND profile_id = %s", (recipe_id, pid))
if not cur.fetchone():
raise HTTPException(404, "Rezept nicht gefunden")
n = apply_recipe_to_items(cur, pid, norm, recipe_id)
cur.execute(
"""
SELECT DISTINCT date::text AS date
FROM nutrition_items
WHERE profile_id = %s AND recipe_id = %s
""",
(pid, recipe_id),
)
days = [r["date"] for r in cur.fetchall()]
if not days:
days = dates_for_normalized_name(cur, pid, norm)
for d in days:
rebuild_daily_nutrients(cur, pid, d)
return {"ok": True, "items_updated": n}
@router.get("/food-knowledge")
def export_food_knowledge_file(
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
import json
from data_layer.food_knowledge import export_food_knowledge
pid = get_pid(x_profile_id)
try:
with get_db() as conn:
bundle = export_food_knowledge(get_cursor(conn), pid)
body = json.dumps(bundle, ensure_ascii=False, indent=2, default=str)
except Exception:
logger.exception("Export food-knowledge fehlgeschlagen")
raise HTTPException(500, "Export fehlgeschlagen") from None
stamp = datetime.now().strftime("%Y-%m-%d")
return Response(
content=body.encode("utf-8"),
media_type="application/json; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="mitai-food-knowledge-{stamp}.json"'},
)
@router.post("/food-knowledge")
async def import_food_knowledge_file(
file: UploadFile = File(...),
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
import json
from data_layer.food_knowledge import import_food_knowledge, parse_food_knowledge_bundle
pid = get_pid(x_profile_id)
raw = await file.read()
if not raw:
raise HTTPException(400, "Leere Datei")
try:
data = json.loads(raw.decode("utf-8-sig"))
parse_food_knowledge_bundle(data)
except ValueError as e:
raise HTTPException(400, str(e)) from e
except Exception as e:
raise HTTPException(400, f"Ungültiges JSON: {e}") from e
with get_db() as conn:
return import_food_knowledge(get_cursor(conn), pid, data)
@router.post("/import-conflicts/resolve")
def resolve_import_conflicts(
body: dict,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.nutrition_items import (
apply_nutrition_day_macros,
compute_day_macro_sums,
resolve_choice_macros,
)
pid = get_pid(x_profile_id)
decisions = body.get("decisions") or []
applied = 0
with get_db() as conn:
cur = get_cursor(conn)
for dec in decisions:
day = dec.get("date")
choice = dec.get("choice")
if not day or not choice:
continue
sums = compute_day_macro_sums(cur, pid, day)
if not sums.get("existing") and choice == "existing":
continue
macros, origin = resolve_choice_macros(sums, choice)
apply_nutrition_day_macros(
cur, pid, day, macros, macro_origin=origin, source="csv", confirm=True,
)
applied += 1
return {"applied": applied}
@router.get("/days/{day}/macros")
def get_day_macros(
day: str,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.nutrition_items import day_macro_preview
pid = get_pid(x_profile_id)
with get_db() as conn:
return day_macro_preview(get_cursor(conn), pid, day)
@router.post("/days/{day}/apply-catalog-macros")
def apply_catalog_macros_for_day(
day: str,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
from data_layer.nutrition_items import apply_day_catalog_macros
pid = get_pid(x_profile_id)
try:
with get_db() as conn:
return apply_day_catalog_macros(get_cursor(conn), pid, day)
except ValueError as e:
raise HTTPException(400, str(e)) from e
@router.put("/days/{day}/mark")
def upsert_day_mark(
day: str,
body: dict,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
pid = get_pid(x_profile_id)
mark_type = (body or {}).get("mark_type")
note = (body or {}).get("note")
if mark_type not in ("fasting", "incomplete"):
raise HTTPException(400, "mark_type muss fasting oder incomplete sein")
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""
INSERT INTO nutrition_day_marks (profile_id, date, mark_type, note, source, updated_at)
VALUES (%s, %s, %s, %s, 'manual', NOW())
ON CONFLICT (profile_id, date)
DO UPDATE SET mark_type = EXCLUDED.mark_type, note = EXCLUDED.note, updated_at = NOW()
RETURNING *
""",
(pid, day, mark_type, note),
)
return r2d(cur.fetchone())
@router.delete("/days/{day}/mark")
def delete_day_mark(
day: str,
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"DELETE FROM nutrition_day_marks WHERE profile_id=%s AND date=%s",
(pid, day),
)
return {"ok": True}
@router.get("/marks")
def list_day_marks(
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"SELECT * FROM nutrition_day_marks WHERE profile_id=%s ORDER BY date DESC",
(pid,),
)
return [r2d(r) for r in cur.fetchall()]
@router.put("/{entry_id}")
def update_nutrition(entry_id: str, kcal: float, protein_g: float, fat_g: float, carbs_g: float,
x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Update nutrition entry macros."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
# Verify ownership
cur.execute("SELECT id FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid))
if not cur.fetchone():
raise HTTPException(404, "Eintrag nicht gefunden")
cur.execute("""
UPDATE nutrition_log
SET kcal=%s, protein_g=%s, fat_g=%s, carbs_g=%s, source='manual', macro_origin='manual'
WHERE id=%s AND profile_id=%s
""", (round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1), entry_id, pid))
return {"success": True}
@router.delete("/{entry_id}")
def delete_nutrition(entry_id: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Delete nutrition entry."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
# Verify ownership
cur.execute("SELECT id FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid))
if not cur.fetchone():
raise HTTPException(404, "Eintrag nicht gefunden")
cur.execute("DELETE FROM nutrition_log WHERE id=%s AND profile_id=%s", (entry_id, pid))
return {"success": True}