mirror of
https://github.com/escalante29/healthy-fit.git
synced 2026-03-21 10:48:46 +01:00
- Supplement tracking: CRUD endpoints, /today, /logs, Supplements page - Kettlebell workouts: session tracking, analytics endpoint, ActiveSession page - Calendar module: events CRUD, calendar components - Push notifications: VAPID keys, PushSubscription model, APScheduler reminders, service worker with push/notificationclick handlers, Profile notifications UI - PWA: vite-plugin-pwa, manifest, icons, service worker generation - Frontend: TypeScript types, API modules, ConfirmModal, toast notifications - Auth fixes: password hashing, nutrition endpoint auth - CLAUDE.md: project documentation and development guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
4.4 KiB
Python
156 lines
4.4 KiB
Python
from datetime import date as date_type
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
import litellm
|
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
|
|
from pydantic import BaseModel
|
|
from sqlmodel import Session, select
|
|
|
|
from app.ai.nutrition import NutritionalInfo, analyze_nutrition_from_image, nutrition_module
|
|
from app.api import deps
|
|
from app.models.food import FoodLog # Added FoodItem
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class AnalyzeRequest(BaseModel):
|
|
description: str
|
|
|
|
|
|
class FoodLogCreate(BaseModel):
|
|
name: str
|
|
calories: float
|
|
protein: float
|
|
carbs: float
|
|
fats: float
|
|
|
|
|
|
@router.post("/analyze", response_model=NutritionalInfo)
|
|
def analyze_food(
|
|
request: AnalyzeRequest,
|
|
current_user: deps.CurrentUser,
|
|
) -> Any:
|
|
"""
|
|
Analyze food description and return nutritional info using DSPy.
|
|
"""
|
|
try:
|
|
result = nutrition_module(description=request.description)
|
|
return result.nutritional_info
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/analyze/image", response_model=NutritionalInfo)
|
|
async def analyze_food_image(
|
|
current_user: deps.CurrentUser,
|
|
file: UploadFile = File(...),
|
|
description: str = Form(""),
|
|
) -> Any:
|
|
"""
|
|
Analyze food image and return nutritional info.
|
|
"""
|
|
try:
|
|
contents = await file.read()
|
|
return analyze_nutrition_from_image(contents, description)
|
|
except litellm.exceptions.BadRequestError as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid image or request: {str(e)}")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/log", response_model=FoodLog)
|
|
def log_food(
|
|
*,
|
|
session: Session = Depends(deps.get_session),
|
|
nutrition_info: FoodLogCreate,
|
|
current_user: deps.CurrentUser,
|
|
) -> Any:
|
|
"""
|
|
Save food log to database.
|
|
"""
|
|
food_log = FoodLog(
|
|
user_id=current_user.id,
|
|
name=nutrition_info.name,
|
|
calories=nutrition_info.calories,
|
|
protein=nutrition_info.protein,
|
|
carbs=nutrition_info.carbs,
|
|
fats=nutrition_info.fats,
|
|
)
|
|
session.add(food_log)
|
|
session.commit()
|
|
session.refresh(food_log)
|
|
return food_log
|
|
|
|
|
|
@router.get("/logs", response_model=list[FoodLog])
|
|
def read_logs(
|
|
current_user: deps.CurrentUser,
|
|
session: Session = Depends(deps.get_session),
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
) -> Any:
|
|
"""
|
|
Get food logs for current user.
|
|
"""
|
|
statement = (
|
|
select(FoodLog)
|
|
.where(FoodLog.user_id == current_user.id)
|
|
.order_by(FoodLog.timestamp.desc())
|
|
.offset(skip)
|
|
.limit(limit)
|
|
)
|
|
return session.exec(statement).all()
|
|
|
|
|
|
class NutritionSummary(BaseModel):
|
|
date: str
|
|
total_calories: float
|
|
total_protein: float
|
|
total_carbs: float
|
|
total_fats: float
|
|
log_count: int
|
|
target_calories: float | None
|
|
target_protein: float | None
|
|
target_carbs: float | None
|
|
target_fat: float | None
|
|
|
|
|
|
@router.get("/summary", response_model=NutritionSummary)
|
|
def get_nutrition_summary(
|
|
current_user: deps.CurrentUser,
|
|
session: Session = Depends(deps.get_session),
|
|
date: str = Query(default=None, description="Date in YYYY-MM-DD format, defaults to today"),
|
|
) -> Any:
|
|
"""
|
|
Get aggregated macro totals for a given day.
|
|
"""
|
|
if date:
|
|
target_date = datetime.strptime(date, "%Y-%m-%d").date()
|
|
else:
|
|
target_date = date_type.today()
|
|
|
|
start = datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0)
|
|
end = datetime(target_date.year, target_date.month, target_date.day, 23, 59, 59)
|
|
|
|
statement = (
|
|
select(FoodLog)
|
|
.where(FoodLog.user_id == current_user.id)
|
|
.where(FoodLog.timestamp >= start)
|
|
.where(FoodLog.timestamp <= end)
|
|
)
|
|
logs = session.exec(statement).all()
|
|
|
|
return NutritionSummary(
|
|
date=target_date.isoformat(),
|
|
total_calories=sum(log.calories for log in logs),
|
|
total_protein=sum(log.protein for log in logs),
|
|
total_carbs=sum(log.carbs for log in logs),
|
|
total_fats=sum(log.fats for log in logs),
|
|
log_count=len(logs),
|
|
target_calories=getattr(current_user, "target_calories", None),
|
|
target_protein=getattr(current_user, "target_protein", None),
|
|
target_carbs=getattr(current_user, "target_carbs", None),
|
|
target_fat=getattr(current_user, "target_fat", None),
|
|
)
|