Initial commit: full fitness-rewards app

Backend: FastAPI + PostgreSQL + SQLAlchemy async
- Auth (JWT), onboarding (PAR-Q+, TTM, BREQ-2), activities, food log
- Points engine with daily gate + weekly targets
- Strava + Polar OAuth integration
- Open Food Facts barcode lookup
- Resource recommendation engine
- 10 seeded Darebee/StrongLifts/YouTube programs

Frontend: React + Vite, mobile-first dark theme
- Login/Register, 8-step onboarding flow
- Dashboard with daily gate status
- Activity logger, food logger (manual + barcode scan + search)
- Reward shop with redemption
- Weekly summary view
- Settings (point rules, targets, integrations)

Deployment: Docker Compose for Coolify (Traefik)
This commit is contained in:
claude 2026-03-14 23:53:00 +00:00
commit 4db2b0846b
61 changed files with 4280 additions and 0 deletions

View file

View file

@ -0,0 +1,30 @@
from datetime import date, datetime
from pydantic import BaseModel
class ActivityCreate(BaseModel):
category: str # workout, food_log, steps_target, screen_free, daily_checkin
title: str | None = None
description: str | None = None
duration_minutes: int | None = None
activity_date: date
program_id: str | None = None
program_day: int | None = None
metadata: dict = {}
class ActivityResponse(BaseModel):
id: str
category: str
title: str | None
description: str | None
duration_minutes: int | None
source: str
points_earned: int
activity_date: date
logged_at: datetime
program_id: str | None
program_day: int | None
metadata_json: dict
model_config = {"from_attributes": True}

View file

@ -0,0 +1,28 @@
from pydantic import BaseModel
class LoginRequest(BaseModel):
username: str
password: str
class RegisterRequest(BaseModel):
username: str
password: str
display_name: str
email: str | None = None
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserResponse(BaseModel):
id: str
username: str
display_name: str
email: str | None
timezone: str
model_config = {"from_attributes": True}

View file

@ -0,0 +1,53 @@
from datetime import date, datetime
from pydantic import BaseModel
class FoodCreate(BaseModel):
meal_type: str # breakfast, lunch, dinner, snack
food_name: str
brand: str | None = None
barcode: str | None = None
serving_size: str | None = None
servings: float = 1.0
calories: float | None = None
protein_g: float | None = None
carbs_g: float | None = None
fat_g: float | None = None
fiber_g: float | None = None
sodium_mg: float | None = None
sugar_g: float | None = None
food_date: date
off_product_id: str | None = None
off_data: dict = {}
class FoodResponse(BaseModel):
id: str
meal_type: str
food_name: str
brand: str | None
barcode: str | None
serving_size: str | None
servings: float
calories: float | None
protein_g: float | None
carbs_g: float | None
fat_g: float | None
food_date: date
logged_at: datetime
model_config = {"from_attributes": True}
class FoodSearchResult(BaseModel):
barcode: str | None = None
product_name: str | None = None
brand: str | None = None
calories_100g: float | None = None
protein_100g: float | None = None
carbs_100g: float | None = None
fat_100g: float | None = None
fiber_100g: float | None = None
sugar_100g: float | None = None
serving_size: str | None = None
image_url: str | None = None

View file

@ -0,0 +1,42 @@
from pydantic import BaseModel
class Phase1Request(BaseModel):
primary_goal: str # lose_weight, build_strength, get_active, feel_better
ttm_stage: str # precontemplation, contemplation, preparation, action, maintenance
class Phase2Request(BaseModel):
# Body metrics (optional)
age: int | None = None
height_cm: float | None = None
weight_kg: float | None = None
gender: str | None = None
# PAR-Q+
parq_heart_condition: bool = False
parq_joint_issues: bool = False
parq_medications: bool = False
parq_other_conditions: str | None = None
# BREQ-2 motivation (1-5 scale)
motivation_external: float | None = None
motivation_introjected: float | None = None
motivation_identified: float | None = None
motivation_intrinsic: float | None = None
motivation_amotivation: float | None = None
# Preferences
activity_preferences: list[str] = []
equipment_access: str | None = None # none, basic_home, full_gym
days_per_week: int = 3
minutes_per_session: int = 30
class OnboardingStatusResponse(BaseModel):
phase1_completed: bool
phase2_completed: bool
primary_goal: str | None = None
ttm_stage: str | None = None
model_config = {"from_attributes": True}

View file

@ -0,0 +1,69 @@
from datetime import date
from pydantic import BaseModel
class PointRuleResponse(BaseModel):
id: str
category: str
description: str
points: int
unit: str | None
is_active: bool
model_config = {"from_attributes": True}
class PointRuleUpdate(BaseModel):
points: int | None = None
description: str | None = None
is_active: bool | None = None
class DailyTargetResponse(BaseModel):
daily_minimum_pts: int
weekly_target_pts: int
weekly_bonus_pts: int
model_config = {"from_attributes": True}
class DailyTargetUpdate(BaseModel):
daily_minimum_pts: int | None = None
weekly_target_pts: int | None = None
weekly_bonus_pts: int | None = None
class DailySummary(BaseModel):
date: date
points_earned: int
points_spent: int
gate_passed: bool
daily_minimum: int
activities: list[dict] = []
class WeeklySummary(BaseModel):
week_start: date
week_end: date
total_points_earned: int
total_points_spent: int
active_days: int
gate_passed_days: int
hit_weekly_target: bool
weekly_target: int
weekly_bonus_earned: int
daily_breakdown: list[DailySummary] = []
class TodayStatus(BaseModel):
date: date
points_earned: int
daily_minimum: int
gate_passed: bool
points_remaining: int
activities_today: list[dict] = []
rewards_available: list[dict] = []
week_points: int
weekly_target: int
program_day: int | None = None
program_total_days: int | None = None

View file

@ -0,0 +1,29 @@
from datetime import date
from pydantic import BaseModel
class RewardCreate(BaseModel):
name: str
description: str | None = None
point_cost: int
class RewardResponse(BaseModel):
id: str
name: str
description: str | None
point_cost: int
is_active: bool
model_config = {"from_attributes": True}
class RewardRedeemRequest(BaseModel):
date: date | None = None # defaults to today
class RedemptionResponse(BaseModel):
id: str
reward_name: str
points_spent: int
redemption_date: date