Add supplements, kettlebell, calendar, push notifications, and PWA support

- 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>
This commit is contained in:
Carlos Escalante
2026-03-20 18:57:03 -06:00
parent bd91eb4171
commit f279907ae3
61 changed files with 9256 additions and 85 deletions

View File

@@ -1,9 +1,11 @@
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, UploadFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import BaseModel
from sqlmodel import Session
from sqlmodel import Session, select
from app.ai.nutrition import NutritionalInfo, analyze_nutrition_from_image, nutrition_module
from app.api import deps
@@ -16,9 +18,18 @@ 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.
@@ -32,6 +43,7 @@ def analyze_food(
@router.post("/analyze/image", response_model=NutritionalInfo)
async def analyze_food_image(
current_user: deps.CurrentUser,
file: UploadFile = File(...),
description: str = Form(""),
) -> Any:
@@ -51,7 +63,7 @@ async def analyze_food_image(
def log_food(
*,
session: Session = Depends(deps.get_session),
nutrition_info: NutritionalInfo,
nutrition_info: FoodLogCreate,
current_user: deps.CurrentUser,
) -> Any:
"""
@@ -81,8 +93,6 @@ def read_logs(
"""
Get food logs for current user.
"""
from sqlmodel import select
statement = (
select(FoodLog)
.where(FoodLog.user_id == current_user.id)
@@ -91,3 +101,55 @@ def read_logs(
.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),
)