All tweaks added
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Float, DateTime, Boolean,
|
||||
ForeignKey, Text, JSON, Index, UniqueConstraint
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
def now_utc():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(64), unique=True, nullable=False, index=True)
|
||||
email = Column(String(256), unique=True, nullable=True)
|
||||
hashed_password = Column(String(256), nullable=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
pocketid_sub = Column(String(256), unique=True, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=now_utc)
|
||||
|
||||
# Health profile
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
resting_heart_rate = Column(Integer, nullable=True)
|
||||
birth_year = Column(Integer, nullable=True)
|
||||
height_cm = Column(Float, nullable=True)
|
||||
|
||||
# PocketID config (stored per-user so admin can set via UI)
|
||||
pocketid_issuer = Column(String(512), nullable=True)
|
||||
pocketid_client_id = Column(String(256), nullable=True)
|
||||
pocketid_client_secret = Column(String(256), nullable=True)
|
||||
|
||||
activities = relationship("Activity", back_populates="user", cascade="all, delete-orphan")
|
||||
health_metrics = relationship("HealthMetric", back_populates="user", cascade="all, delete-orphan")
|
||||
named_routes = relationship("NamedRoute", back_populates="user", cascade="all, delete-orphan")
|
||||
weight_logs = relationship("WeightLog", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class WeightLog(Base):
|
||||
"""Manual weight entries separate from health_metrics for easy tracking."""
|
||||
__tablename__ = "weight_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
date = Column(DateTime(timezone=True), nullable=False)
|
||||
weight_kg = Column(Float, nullable=False)
|
||||
body_fat_pct = Column(Float, nullable=True)
|
||||
note = Column(String(256), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_weight_user_date", "user_id", "date"),
|
||||
)
|
||||
|
||||
user = relationship("User", back_populates="weight_logs")
|
||||
|
||||
|
||||
class Activity(Base):
|
||||
__tablename__ = "activities"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String(256), nullable=False)
|
||||
sport_type = Column(String(64), nullable=False)
|
||||
start_time = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
end_time = Column(DateTime(timezone=True), nullable=True)
|
||||
distance_m = Column(Float, nullable=True)
|
||||
duration_s = Column(Float, nullable=True)
|
||||
elevation_gain_m = Column(Float, nullable=True)
|
||||
elevation_loss_m = Column(Float, nullable=True)
|
||||
avg_heart_rate = Column(Float, nullable=True)
|
||||
max_heart_rate = Column(Float, nullable=True)
|
||||
avg_cadence = Column(Float, nullable=True)
|
||||
avg_power = Column(Float, nullable=True)
|
||||
normalized_power = Column(Float, nullable=True)
|
||||
avg_speed_ms = Column(Float, nullable=True)
|
||||
max_speed_ms = Column(Float, nullable=True)
|
||||
avg_temperature_c = Column(Float, nullable=True)
|
||||
calories = Column(Float, nullable=True)
|
||||
training_stress_score = Column(Float, nullable=True)
|
||||
vo2max_estimate = Column(Float, nullable=True)
|
||||
named_route_id = Column(Integer, ForeignKey("named_routes.id"), nullable=True)
|
||||
polyline = Column(Text, nullable=True)
|
||||
bounding_box = Column(JSON, nullable=True)
|
||||
source_file = Column(String(512), nullable=True)
|
||||
source_type = Column(String(32), nullable=True)
|
||||
garmin_activity_id = Column(String(64), nullable=True, unique=True)
|
||||
strava_activity_id = Column(String(64), nullable=True, unique=True)
|
||||
hr_zones = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=now_utc)
|
||||
|
||||
user = relationship("User", back_populates="activities")
|
||||
data_points = relationship("ActivityDataPoint", back_populates="activity", cascade="all, delete-orphan")
|
||||
named_route = relationship("NamedRoute", back_populates="activities")
|
||||
laps = relationship("ActivityLap", back_populates="activity", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ActivityDataPoint(Base):
|
||||
__tablename__ = "activity_data_points"
|
||||
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False, primary_key=True)
|
||||
timestamp = Column(DateTime(timezone=True), nullable=False, primary_key=True)
|
||||
latitude = Column(Float, nullable=True)
|
||||
longitude = Column(Float, nullable=True)
|
||||
altitude_m = Column(Float, nullable=True)
|
||||
heart_rate = Column(Float, nullable=True)
|
||||
cadence = Column(Float, nullable=True)
|
||||
speed_ms = Column(Float, nullable=True)
|
||||
power = Column(Float, nullable=True)
|
||||
temperature_c = Column(Float, nullable=True)
|
||||
distance_m = Column(Float, nullable=True)
|
||||
|
||||
activity = relationship("Activity", back_populates="data_points")
|
||||
|
||||
|
||||
class ActivityLap(Base):
|
||||
__tablename__ = "activity_laps"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False, index=True)
|
||||
lap_number = Column(Integer, nullable=False)
|
||||
start_time = Column(DateTime(timezone=True), nullable=True)
|
||||
duration_s = Column(Float, nullable=True)
|
||||
distance_m = Column(Float, nullable=True)
|
||||
avg_heart_rate = Column(Float, nullable=True)
|
||||
avg_cadence = Column(Float, nullable=True)
|
||||
avg_speed_ms = Column(Float, nullable=True)
|
||||
avg_power = Column(Float, nullable=True)
|
||||
|
||||
activity = relationship("Activity", back_populates="laps")
|
||||
|
||||
|
||||
class NamedRoute(Base):
|
||||
__tablename__ = "named_routes"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String(256), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
sport_type = Column(String(64), nullable=True)
|
||||
reference_polyline = Column(Text, nullable=True)
|
||||
bounding_box = Column(JSON, nullable=True)
|
||||
distance_m = Column(Float, nullable=True)
|
||||
auto_detected = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), default=now_utc)
|
||||
|
||||
user = relationship("User", back_populates="named_routes")
|
||||
activities = relationship("Activity", back_populates="named_route")
|
||||
segments = relationship("RouteSegment", back_populates="route", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RouteSegment(Base):
|
||||
__tablename__ = "route_segments"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
route_id = Column(Integer, ForeignKey("named_routes.id"), nullable=False, index=True)
|
||||
name = Column(String(256), nullable=False)
|
||||
start_distance_m = Column(Float, nullable=False)
|
||||
end_distance_m = Column(Float, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
route = relationship("NamedRoute", back_populates="segments")
|
||||
|
||||
|
||||
class PersonalRecord(Base):
|
||||
__tablename__ = "personal_records"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False)
|
||||
sport_type = Column(String(64), nullable=False)
|
||||
distance_m = Column(Float, nullable=False)
|
||||
distance_label = Column(String(32), nullable=False)
|
||||
duration_s = Column(Float, nullable=False)
|
||||
achieved_at = Column(DateTime(timezone=True), nullable=False)
|
||||
is_current_record = Column(Boolean, default=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "sport_type", "distance_m", "is_current_record",
|
||||
name="uq_pr_current"),
|
||||
)
|
||||
|
||||
|
||||
class HealthMetric(Base):
|
||||
__tablename__ = "health_metrics"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
date = Column(DateTime(timezone=True), nullable=False)
|
||||
resting_hr = Column(Float, nullable=True)
|
||||
max_hr_day = Column(Float, nullable=True)
|
||||
avg_hr_day = Column(Float, nullable=True)
|
||||
hrv_status = Column(String(32), nullable=True)
|
||||
hrv_nightly_avg = Column(Float, nullable=True)
|
||||
hrv_5min_high = Column(Float, nullable=True)
|
||||
hrv_5min_low = Column(Float, nullable=True)
|
||||
sleep_duration_s = Column(Float, nullable=True)
|
||||
sleep_deep_s = Column(Float, nullable=True)
|
||||
sleep_light_s = Column(Float, nullable=True)
|
||||
sleep_rem_s = Column(Float, nullable=True)
|
||||
sleep_awake_s = Column(Float, nullable=True)
|
||||
sleep_score = Column(Float, nullable=True)
|
||||
sleep_start = Column(DateTime(timezone=True), nullable=True)
|
||||
sleep_end = Column(DateTime(timezone=True), nullable=True)
|
||||
weight_kg = Column(Float, nullable=True)
|
||||
bmi = Column(Float, nullable=True)
|
||||
body_fat_pct = Column(Float, nullable=True)
|
||||
muscle_mass_kg = Column(Float, nullable=True)
|
||||
vo2max = Column(Float, nullable=True)
|
||||
fitness_age = Column(Integer, nullable=True)
|
||||
training_load = Column(Float, nullable=True)
|
||||
recovery_time_h = Column(Float, nullable=True)
|
||||
avg_stress = Column(Float, nullable=True)
|
||||
steps = Column(Integer, nullable=True)
|
||||
floors_climbed = Column(Integer, nullable=True)
|
||||
active_calories = Column(Float, nullable=True)
|
||||
total_calories = Column(Float, nullable=True)
|
||||
spo2_avg = Column(Float, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "date", name="uq_health_user_date"),
|
||||
Index("ix_health_user_date", "user_id", "date"),
|
||||
)
|
||||
|
||||
user = relationship("User", back_populates="health_metrics")
|
||||
Reference in New Issue
Block a user