Initial commit: HabitForge - habit tracking application with FastAPI backend, React frontend, and Zepp integration
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""SQLAlchemy models module."""
|
||||
from backend.models.user import User
|
||||
from backend.models.challenge import Challenge
|
||||
from backend.models.tracking import Tracking
|
||||
from backend.models.daily_health_metrics import DailyHealthMetrics
|
||||
from backend.models.workout_session import WorkoutSession
|
||||
|
||||
__all__ = ["User", "Challenge", "Tracking", "DailyHealthMetrics", "WorkoutSession"]
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Challenge SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String, Date, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import date
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class Challenge(Base):
|
||||
"""Challenge model for tracking habits."""
|
||||
__tablename__ = "challenges"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
period_type = Column(String) # 'week', 'month', 'year'
|
||||
daily_target = Column(Integer)
|
||||
start_date = Column(Date)
|
||||
end_date = Column(Date)
|
||||
created_at = Column(Date, default=date.today)
|
||||
display_order = Column(Integer, default=0)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
unit_type = Column(String, default="reps") # 'reps', 'minutes', 'pages', 'km'
|
||||
rest_days = Column(String, nullable=True) # comma-separated list of days
|
||||
icon = Column(String, default="fa-dumbbell")
|
||||
image_url = Column(String, nullable=True)
|
||||
description = Column(String, nullable=True)
|
||||
frequency_count = Column(Integer, nullable=True)
|
||||
selected_days = Column(String, nullable=True)
|
||||
|
||||
owner = relationship("User", back_populates="challenges")
|
||||
trackings = relationship("Tracking", back_populates="challenge", cascade="all, delete-orphan")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""DailyHealthMetrics SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, Date, Float, ForeignKey, UniqueConstraint, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class DailyHealthMetrics(Base):
|
||||
"""Daily health metrics model for tracking health data."""
|
||||
__tablename__ = "daily_health_metrics"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'date', name='uq_user_date'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
date = Column(Date, index=True)
|
||||
step_count = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories_burned = Column(Float, default=0.0)
|
||||
sleep_duration_minutes = Column(Integer, default=0)
|
||||
deep_sleep_minutes = Column(Integer, default=0)
|
||||
light_sleep_minutes = Column(Integer, default=0)
|
||||
rem_sleep_minutes = Column(Integer, default=0)
|
||||
awake_duration_minutes = Column(Integer, default=0)
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
avg_spo2 = Column(Float, nullable=True)
|
||||
pai_score = Column(Float, default=0.0)
|
||||
weight = Column(Float, nullable=True)
|
||||
resting_heart_rate = Column(Integer, nullable=True)
|
||||
hrv = Column(Float, nullable=True)
|
||||
|
||||
# Data provenance tracking
|
||||
data_source = Column(String(50), default="google_fit") # "google_fit", "health_connect", "manual"
|
||||
|
||||
user = relationship("User", back_populates="health_metrics")
|
||||
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Health Connect Data Models - Storage for data pushed from Android companion app.
|
||||
|
||||
These models store health data received from the Health Connect Android app,
|
||||
enabling the HealthConnectProvider to serve data to the API layer.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Float, Date, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
import datetime
|
||||
|
||||
|
||||
class HealthConnectSyncLog(Base):
|
||||
"""
|
||||
Tracks sync operations from the Android companion app.
|
||||
Used for debugging and monitoring data flow.
|
||||
"""
|
||||
__tablename__ = "health_connect_sync_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
device_id = Column(String(100)) # Android device identifier
|
||||
sync_timestamp = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
records_pushed = Column(Integer, default=0)
|
||||
status = Column(String(50)) # "success", "partial", "error"
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class HealthConnectDailyMetrics(Base):
|
||||
"""
|
||||
Daily health metrics received from Health Connect.
|
||||
Mirrors DailyHealthMetrics but with Health Connect-specific metadata.
|
||||
"""
|
||||
__tablename__ = "health_connect_daily_metrics"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
date = Column(Date, index=True)
|
||||
|
||||
# Activity metrics
|
||||
step_count = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories_burned = Column(Float, default=0.0) # ActiveCaloriesBurnedRecord
|
||||
total_calories = Column(Float, default=0.0) # TotalCaloriesBurnedRecord
|
||||
|
||||
# Sleep metrics (from SleepSessionRecord)
|
||||
sleep_duration_minutes = Column(Integer, default=0)
|
||||
deep_sleep_minutes = Column(Integer, default=0)
|
||||
light_sleep_minutes = Column(Integer, default=0)
|
||||
rem_sleep_minutes = Column(Integer, default=0)
|
||||
awake_duration_minutes = Column(Integer, default=0)
|
||||
|
||||
# Heart rate metrics (from HeartRateRecord)
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
resting_heart_rate = Column(Integer, nullable=True)
|
||||
|
||||
# SpO2 (from OxygenSaturationRecord)
|
||||
avg_spo2 = Column(Float, nullable=True)
|
||||
min_spo2 = Column(Float, nullable=True)
|
||||
max_spo2 = Column(Float, nullable=True)
|
||||
|
||||
# Body metrics
|
||||
weight = Column(Float, nullable=True) # WeightRecord
|
||||
|
||||
# Metadata
|
||||
last_synced = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class HealthConnectExerciseSession(Base):
|
||||
"""
|
||||
Exercise sessions from Health Connect ExerciseSessionRecord.
|
||||
"""
|
||||
__tablename__ = "health_connect_exercise_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||
|
||||
# Session timing
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
|
||||
# Exercise type (Health Connect ExerciseType enum)
|
||||
exercise_type = Column(Integer) # Health Connect type ID
|
||||
exercise_type_name = Column(String(100)) # Human-readable name
|
||||
|
||||
# Metrics
|
||||
duration_seconds = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, nullable=True)
|
||||
calories_burned = Column(Float, nullable=True)
|
||||
|
||||
# Heart rate during exercise
|
||||
avg_heart_rate = Column(Integer, nullable=True)
|
||||
max_heart_rate = Column(Integer, nullable=True)
|
||||
min_heart_rate = Column(Integer, nullable=True)
|
||||
|
||||
# Additional metrics
|
||||
steps = Column(Integer, nullable=True)
|
||||
elevation_gained = Column(Float, nullable=True)
|
||||
|
||||
# Metadata
|
||||
notes = Column(Text, nullable=True)
|
||||
last_synced = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
sync_id = Column(Integer, ForeignKey("health_connect_sync_logs.id"), nullable=True)
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
# Health Connect Exercise Type mapping (subset)
|
||||
HEALTH_CONNECT_EXERCISE_TYPES = {
|
||||
0: "Unknown",
|
||||
2: "Badminton",
|
||||
4: "Baseball",
|
||||
5: "Basketball",
|
||||
8: "Biking",
|
||||
9: "Biking Stationary",
|
||||
10: "Boot Camp",
|
||||
11: "Boxing",
|
||||
13: "Calisthenics",
|
||||
14: "Cricket",
|
||||
16: "Dancing",
|
||||
25: "Elliptical",
|
||||
26: "Exercise Class",
|
||||
27: "Fencing",
|
||||
28: "Football American",
|
||||
29: "Football Australian",
|
||||
31: "Frisbee Disc",
|
||||
32: "Golf",
|
||||
33: "Guided Breathing",
|
||||
34: "Gymnastics",
|
||||
35: "Handball",
|
||||
36: "High Intensity Interval Training",
|
||||
37: "Hiking",
|
||||
38: "Ice Hockey",
|
||||
39: "Ice Skating",
|
||||
44: "Martial Arts",
|
||||
46: "Paddling",
|
||||
48: "Pilates",
|
||||
50: "Racquetball",
|
||||
51: "Rock Climbing",
|
||||
52: "Roller Hockey",
|
||||
53: "Rowing",
|
||||
54: "Rowing Machine",
|
||||
55: "Rugby",
|
||||
56: "Running",
|
||||
57: "Running Treadmill",
|
||||
58: "Sailing",
|
||||
59: "Scuba Diving",
|
||||
60: "Skating",
|
||||
61: "Skiing",
|
||||
62: "Snowboarding",
|
||||
63: "Snowshoeing",
|
||||
64: "Soccer",
|
||||
65: "Softball",
|
||||
67: "Squash",
|
||||
68: "Stair Climbing",
|
||||
69: "Stair Climbing Machine",
|
||||
70: "Strength Training",
|
||||
71: "Stretching",
|
||||
72: "Surfing",
|
||||
73: "Swimming Open Water",
|
||||
74: "Swimming Pool",
|
||||
75: "Table Tennis",
|
||||
76: "Tennis",
|
||||
78: "Volleyball",
|
||||
79: "Walking",
|
||||
80: "Water Polo",
|
||||
81: "Weightlifting",
|
||||
82: "Wheelchair",
|
||||
84: "Yoga",
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Tracking SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, Date, Boolean, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class Tracking(Base):
|
||||
"""Tracking model for recording challenge progress."""
|
||||
__tablename__ = "tracking"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
challenge_id = Column(Integer, ForeignKey("challenges.id"))
|
||||
date = Column(Date)
|
||||
reps = Column(Integer, default=0)
|
||||
completed = Column(Boolean, default=False)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
challenge = relationship("Challenge", back_populates="trackings")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""User SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model for authentication and ownership."""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
|
||||
challenges = relationship("Challenge", back_populates="owner")
|
||||
health_metrics = relationship("DailyHealthMetrics", back_populates="user")
|
||||
workouts = relationship("WorkoutSession", back_populates="user")
|
||||
@@ -0,0 +1,48 @@
|
||||
"""WorkoutSession SQLAlchemy model."""
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from backend.database import Base
|
||||
|
||||
|
||||
# Zepp activity type mapping
|
||||
ZEPP_ACTIVITY_TYPES = {
|
||||
1: "Running",
|
||||
6: "Walking",
|
||||
8: "Cycling",
|
||||
10: "Elliptical",
|
||||
16: "Free Training",
|
||||
23: "HIIT",
|
||||
52: "Stretching",
|
||||
60: "Yoga",
|
||||
}
|
||||
|
||||
|
||||
class WorkoutSession(Base):
|
||||
"""Workout data model."""
|
||||
__tablename__ = "workout_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
start_time = Column(DateTime, index=True)
|
||||
end_time = Column(DateTime)
|
||||
activity_type = Column(String) # Human readable name
|
||||
activity_type_id = Column(Integer, nullable=True) # Original Zepp type ID
|
||||
duration_seconds = Column(Integer, default=0)
|
||||
distance_meters = Column(Float, default=0.0)
|
||||
calories = Column(Float, default=0.0)
|
||||
avg_pace = Column(Float, nullable=True) # seconds per meter
|
||||
max_pace = Column(Float, nullable=True)
|
||||
min_pace = Column(Float, nullable=True)
|
||||
avg_hr = Column(Integer, nullable=True)
|
||||
max_hr = Column(Integer, nullable=True)
|
||||
min_hr = Column(Integer, nullable=True)
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
# Data provenance tracking
|
||||
data_source = Column(String(50), default="google_fit") # "google_fit", "health_connect", "manual"
|
||||
|
||||
user = relationship("User", back_populates="workouts")
|
||||
|
||||
@staticmethod
|
||||
def get_activity_name(type_id: int) -> str:
|
||||
return ZEPP_ACTIVITY_TYPES.get(type_id, f"Activity {type_id}")
|
||||
Reference in New Issue
Block a user