diligence/diligence/models/nutrition.py
Claude c37543b009 feat: timezone auto-detection, branding fix, frontend rebuild
Timezone:
- Add diligence/utils/dates.py with today_for_user(), now_for_user(), day_start_utc()
- Replace all 20 date.today() calls across 10 files with timezone-aware helpers
- Remove hardcoded Asia/Bangkok offset hack in nutrition router
- Change defaults from Asia/Bangkok to UTC in user model, nutrition model, config
- Add timezone field to RegisterRequest schema
- Set user.timezone from browser detection during registration
- Frontend sends Intl.DateTimeFormat().resolvedOptions().timeZone on register

Branding:
- Login page: Fitness Rewards -> Diligence, updated tagline and emoji
- index.html title: Fitness Rewards -> Diligence

Frontend rebuild:
- Fresh Vite build with all changes (index-CsKoBN0D.js)
- Nav bar, timezone detection, and branding all verified in built bundle
2026-07-04 07:16:23 +00:00

89 lines
4.1 KiB
Python

from __future__ import annotations
import uuid
from datetime import datetime, timezone
from decimal import Decimal
from sqlalchemy import String, Integer, Numeric, DateTime, Boolean, Text, ForeignKey, Index, Uuid
from sqlalchemy.orm import Mapped, mapped_column
from diligence.database import Base
class NutritionGoal(Base):
"""Per-user macro + eating-window targets. One active per user."""
__tablename__ = "nutrition_goals"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False, unique=True)
# Macro targets (grams / kcal)
calorie_target: Mapped[int] = mapped_column(Integer, default=2400)
protein_g_target: Mapped[int] = mapped_column(Integer, default=180)
fat_g_target: Mapped[int] = mapped_column(Integer, default=175)
net_carbs_g_cap: Mapped[int] = mapped_column(Integer, default=20) # strict keto
# Training-day override (optional)
training_calorie_target: Mapped[int | None] = mapped_column(Integer)
training_protein_g_target: Mapped[int | None] = mapped_column(Integer)
training_fat_g_target: Mapped[int | None] = mapped_column(Integer)
# Eating window (24h local time, hours only)
eating_window_start_hour: Mapped[int] = mapped_column(Integer, default=12) # 12:00
eating_window_end_hour: Mapped[int] = mapped_column(Integer, default=20) # 20:00
default_fast_hours: Mapped[int] = mapped_column(Integer, default=16) # 16:8
diet_style: Mapped[str] = mapped_column(String(30), default="strict_keto")
timezone_str: Mapped[str] = mapped_column(String(50), default="UTC")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
class Fast(Base):
"""A single fasting bout — start/end + type + compliance flag."""
__tablename__ = "fasts"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
# Target length (hours) — 16, 18, 20, 24, 36, 48, 72
target_hours: Mapped[int] = mapped_column(Integer, nullable=False)
fast_type: Mapped[str] = mapped_column(String(20), default="daily") # daily, weekly_24, long_48, long_72, extended
# Completion state
completed: Mapped[bool] = mapped_column(Boolean, default=False) # hit target
broken_early: Mapped[bool] = mapped_column(Boolean, default=False) # ended before target
# Points granted (for idempotency — don't double-credit)
points_awarded: Mapped[int] = mapped_column(Integer, default=0)
activity_log_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("activity_log.id"))
notes: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
__table_args__ = (
Index("idx_fasts_user_started", "user_id", "started_at"),
)
class ElectrolyteLog(Base):
"""Daily electrolyte dosing checklist — lightweight tracker."""
__tablename__ = "electrolyte_log"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False)
log_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
sodium_mg: Mapped[int] = mapped_column(Integer, default=0)
potassium_mg: Mapped[int] = mapped_column(Integer, default=0)
magnesium_mg: Mapped[int] = mapped_column(Integer, default=0)
notes: Mapped[str | None] = mapped_column(Text)
__table_args__ = (
Index("idx_electrolyte_user_date", "user_id", "log_date"),
)