Initial commit: HabitForge - habit tracking application with FastAPI backend, React frontend, and Zepp integration

This commit is contained in:
2026-07-31 08:58:53 -04:00
commit c7fce339c2
124 changed files with 17558 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+91
View File
@@ -0,0 +1,91 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
import os
import sys
sys.path.append(os.getcwd())
# Import Base and all models to ensure they're registered with metadata
from backend.database import Base
from backend.models import User, Challenge, Tracking, DailyHealthMetrics, WorkoutSession
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# Override URL from settings
from backend.core.config import settings
configuration = config.get_section(config.config_ini_section, {})
configuration["sqlalchemy.url"] = settings.DATABASE_URL
connectable = engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+28
View File
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
@@ -0,0 +1,34 @@
"""Add icon and image_url
Revision ID: 4cc2e86d9794
Revises: 8ba5ee682bc1
Create Date: 2025-12-27 23:38:43.414279
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '4cc2e86d9794'
down_revision: Union[str, Sequence[str], None] = '8ba5ee682bc1'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('challenges', sa.Column('icon', sa.String(), nullable=True))
op.add_column('challenges', sa.Column('image_url', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('challenges', 'image_url')
op.drop_column('challenges', 'icon')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Add description
Revision ID: 5ea126309171
Revises: 4cc2e86d9794
Create Date: 2025-12-28 00:11:14.788582
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5ea126309171'
down_revision: Union[str, Sequence[str], None] = '4cc2e86d9794'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('challenges', sa.Column('description', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('challenges', 'description')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Add awake_duration_minutes to daily health metrics
Revision ID: 6c3fce1dd49a
Revises: d208fd79a572
Create Date: 2025-12-29 13:49:31.181979
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '6c3fce1dd49a'
down_revision: Union[str, Sequence[str], None] = 'd208fd79a572'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('daily_health_metrics', sa.Column('awake_duration_minutes', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('daily_health_metrics', 'awake_duration_minutes')
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""Add unit_type and rest_days
Revision ID: 8ba5ee682bc1
Revises:
Create Date: 2025-12-27 22:29:05.909161
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8ba5ee682bc1'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('challenges', sa.Column('unit_type', sa.String(), nullable=True))
op.add_column('challenges', sa.Column('rest_days', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('challenges', 'rest_days')
op.drop_column('challenges', 'unit_type')
# ### end Alembic commands ###
@@ -0,0 +1,41 @@
"""Add data_source columns for provider tracking
Revision ID: a1b2c3d4e5f6
Revises: 6c3fce1dd49a
Create Date: 2026-01-01 14:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f6'
down_revision: Union[str, None] = '6c3fce1dd49a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add data_source column to daily_health_metrics
op.add_column(
'daily_health_metrics',
sa.Column('data_source', sa.String(50), nullable=True, server_default='google_fit')
)
# Add data_source column to workout_sessions
op.add_column(
'workout_sessions',
sa.Column('data_source', sa.String(50), nullable=True, server_default='google_fit')
)
# Update existing rows to have default value
op.execute("UPDATE daily_health_metrics SET data_source = 'google_fit' WHERE data_source IS NULL")
op.execute("UPDATE workout_sessions SET data_source = 'google_fit' WHERE data_source IS NULL")
def downgrade() -> None:
op.drop_column('daily_health_metrics', 'data_source')
op.drop_column('workout_sessions', 'data_source')
@@ -0,0 +1,103 @@
"""Add Health Connect tables
Revision ID: b2c3d4e5f6g7
Revises: a1b2c3d4e5f6
Create Date: 2026-01-01 14:35:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b2c3d4e5f6g7'
down_revision: Union[str, None] = 'a1b2c3d4e5f6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create health_connect_sync_logs table
op.create_table(
'health_connect_sync_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('device_id', sa.String(100), nullable=True),
sa.Column('sync_timestamp', sa.DateTime(), nullable=True),
sa.Column('records_pushed', sa.Integer(), nullable=True),
sa.Column('status', sa.String(50), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_health_connect_sync_logs_id', 'health_connect_sync_logs', ['id'])
op.create_index('ix_health_connect_sync_logs_user_id', 'health_connect_sync_logs', ['user_id'])
# Create health_connect_daily_metrics table
op.create_table(
'health_connect_daily_metrics',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('date', sa.Date(), nullable=True),
sa.Column('step_count', sa.Integer(), nullable=True),
sa.Column('distance_meters', sa.Float(), nullable=True),
sa.Column('calories_burned', sa.Float(), nullable=True),
sa.Column('total_calories', sa.Float(), nullable=True),
sa.Column('sleep_duration_minutes', sa.Integer(), nullable=True),
sa.Column('deep_sleep_minutes', sa.Integer(), nullable=True),
sa.Column('light_sleep_minutes', sa.Integer(), nullable=True),
sa.Column('rem_sleep_minutes', sa.Integer(), nullable=True),
sa.Column('awake_duration_minutes', sa.Integer(), nullable=True),
sa.Column('avg_heart_rate', sa.Integer(), nullable=True),
sa.Column('min_heart_rate', sa.Integer(), nullable=True),
sa.Column('max_heart_rate', sa.Integer(), nullable=True),
sa.Column('resting_heart_rate', sa.Integer(), nullable=True),
sa.Column('avg_spo2', sa.Float(), nullable=True),
sa.Column('min_spo2', sa.Float(), nullable=True),
sa.Column('max_spo2', sa.Float(), nullable=True),
sa.Column('weight', sa.Float(), nullable=True),
sa.Column('last_synced', sa.DateTime(), nullable=True),
sa.Column('sync_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['sync_id'], ['health_connect_sync_logs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_health_connect_daily_metrics_id', 'health_connect_daily_metrics', ['id'])
op.create_index('ix_health_connect_daily_metrics_user_id', 'health_connect_daily_metrics', ['user_id'])
op.create_index('ix_health_connect_daily_metrics_date', 'health_connect_daily_metrics', ['date'])
# Create health_connect_exercise_sessions table
op.create_table(
'health_connect_exercise_sessions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('start_time', sa.DateTime(), nullable=True),
sa.Column('end_time', sa.DateTime(), nullable=True),
sa.Column('exercise_type', sa.Integer(), nullable=True),
sa.Column('exercise_type_name', sa.String(100), nullable=True),
sa.Column('duration_seconds', sa.Integer(), nullable=True),
sa.Column('distance_meters', sa.Float(), nullable=True),
sa.Column('calories_burned', sa.Float(), nullable=True),
sa.Column('avg_heart_rate', sa.Integer(), nullable=True),
sa.Column('max_heart_rate', sa.Integer(), nullable=True),
sa.Column('min_heart_rate', sa.Integer(), nullable=True),
sa.Column('steps', sa.Integer(), nullable=True),
sa.Column('elevation_gained', sa.Float(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('last_synced', sa.DateTime(), nullable=True),
sa.Column('sync_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['sync_id'], ['health_connect_sync_logs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_health_connect_exercise_sessions_id', 'health_connect_exercise_sessions', ['id'])
op.create_index('ix_health_connect_exercise_sessions_user_id', 'health_connect_exercise_sessions', ['user_id'])
op.create_index('ix_health_connect_exercise_sessions_start_time', 'health_connect_exercise_sessions', ['start_time'])
def downgrade() -> None:
op.drop_table('health_connect_exercise_sessions')
op.drop_table('health_connect_daily_metrics')
op.drop_table('health_connect_sync_logs')
@@ -0,0 +1,36 @@
"""Add Zepp fields
Revision ID: c3049b0cefc4
Revises: 6c3fce1dd49a
Create Date: 2025-12-29 17:26:48.144904
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'c3049b0cefc4'
down_revision: Union[str, Sequence[str], None] = '6c3fce1dd49a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('daily_health_metrics', sa.Column('weight', sa.Float(), nullable=True))
op.add_column('daily_health_metrics', sa.Column('resting_heart_rate', sa.Integer(), nullable=True))
op.add_column('daily_health_metrics', sa.Column('hrv', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('daily_health_metrics', 'hrv')
op.drop_column('daily_health_metrics', 'resting_heart_rate')
op.drop_column('daily_health_metrics', 'weight')
# ### end Alembic commands ###
@@ -0,0 +1,34 @@
"""Add_frequency_fields
Revision ID: d208fd79a572
Revises: 5ea126309171
Create Date: 2025-12-28 12:59:55.198335
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd208fd79a572'
down_revision: Union[str, Sequence[str], None] = '5ea126309171'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('challenges', sa.Column('frequency_count', sa.Integer(), nullable=True))
op.add_column('challenges', sa.Column('selected_days', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('challenges', 'selected_days')
op.drop_column('challenges', 'frequency_count')
# ### end Alembic commands ###