From 45dbc32295e4fdac417d1b122d97014009c210f4 Mon Sep 17 00:00:00 2001 From: sstent Date: Wed, 14 Jan 2026 05:39:16 -0800 Subject: [PATCH] changed to db for fit streams --- FitnessSync/.dockerignore | 9 + .../backend/__pycache__/main.cpython-313.pyc | Bin 5560 -> 5560 bytes ...cc82af3f2_remove_btree_geometry_indexes.py | 26 + ...820130_add_activity_streams_and_postgis.py | 54 + ...5791dd193e_add_missing_activity_columns.py | 218 +++ .../95af0e911216_add_bike_setups_table.py | 3 + ...e_add_extended_fit_metrics.cpython-313.pyc | Bin 4717 -> 0 bytes ...7f880117_create_jobs_table.cpython-313.pyc | Bin 3038 -> 0 bytes ...ove_btree_geometry_indexes.cpython-311.pyc | Bin 0 -> 1123 bytes ...1381ac00_initial_migration.cpython-313.pyc | Bin 11349 -> 0 bytes ...dd_mfa_state_to_api_tokens.cpython-313.pyc | Bin 1045 -> 0 bytes ...tivity_streams_and_postgis.cpython-311.pyc | Bin 0 -> 4498 bytes ...add_bike_setup_to_activity.cpython-313.pyc | Bin 1537 -> 0 bytes ...kens_and_expiry_during_mfa.cpython-313.pyc | Bin 1356 -> 0 bytes ...60ed462bf_add_state_tables.cpython-313.pyc | Bin 3698 -> 0 bytes ...d_missing_activity_columns.cpython-311.pyc | Bin 0 -> 20080 bytes ...1216_add_bike_setups_table.cpython-311.pyc | Bin 2643 -> 3006 bytes ...1216_add_bike_setups_table.cpython-313.pyc | Bin 2468 -> 0 bytes ...495f5e_add_segments_tables.cpython-313.pyc | Bin 4622 -> 0 bytes ...vg_temperature_to_activity.cpython-313.pyc | Bin 2236 -> 0 bytes ...a5_add_fitbit_redirect_uri.cpython-313.pyc | Bin 1322 -> 0 bytes ...nd_activity_schema_metrics.cpython-311.pyc | Bin 5461 -> 4995 bytes ...nd_activity_schema_metrics.cpython-313.pyc | Bin 5293 -> 0 bytes ...ssion_fields_to_api_tokens.cpython-313.pyc | Bin 1830 -> 0 bytes ...a0528865_expand_activity_schema_metrics.py | 9 +- FitnessSync/backend/backfill_segment_geom.py | 52 + FitnessSync/backend/backfill_streams.py | 29 + FitnessSync/backend/requirements.txt | 1 + .../__pycache__/activities.cpython-311.pyc | Bin 48649 -> 51799 bytes .../__pycache__/activities.cpython-313.pyc | Bin 41982 -> 47100 bytes .../api/__pycache__/analysis.cpython-311.pyc | Bin 12265 -> 15290 bytes .../api/__pycache__/analysis.cpython-313.pyc | Bin 0 -> 13336 bytes .../__pycache__/bike_setups.cpython-313.pyc | Bin 6548 -> 8804 bytes .../api/__pycache__/discovery.cpython-311.pyc | Bin 3579 -> 3748 bytes .../api/__pycache__/metrics.cpython-311.pyc | Bin 19381 -> 17932 bytes .../__pycache__/scheduling.cpython-311.pyc | Bin 8785 -> 9479 bytes .../__pycache__/scheduling.cpython-313.pyc | Bin 7501 -> 7911 bytes .../api/__pycache__/segments.cpython-311.pyc | Bin 20061 -> 22094 bytes .../api/__pycache__/segments.cpython-313.pyc | Bin 12020 -> 18313 bytes .../src/api/__pycache__/sync.cpython-311.pyc | Bin 20785 -> 20721 bytes FitnessSync/backend/src/api/activities.py | 127 +- FitnessSync/backend/src/api/analysis.py | 122 +- FitnessSync/backend/src/api/discovery.py | 8 +- FitnessSync/backend/src/api/metrics.py | 38 +- FitnessSync/backend/src/api/scheduling.py | 11 + FitnessSync/backend/src/api/segments.py | 39 +- FitnessSync/backend/src/api/sync.py | 14 +- .../segment_matching_job.cpython-311.pyc | Bin 6760 -> 3836 bytes .../backend/src/jobs/segment_matching_job.py | 126 +- FitnessSync/backend/src/models/__init__.py | 3 +- .../__pycache__/__init__.cpython-311.pyc | Bin 1056 -> 1117 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 862 -> 908 bytes .../__pycache__/activity.cpython-311.pyc | Bin 4501 -> 4627 bytes .../__pycache__/activity.cpython-313.pyc | Bin 3242 -> 3400 bytes .../__pycache__/segment.cpython-311.pyc | Bin 1464 -> 1652 bytes .../__pycache__/segment.cpython-313.pyc | Bin 1198 -> 1331 bytes .../models/__pycache__/stream.cpython-311.pyc | Bin 0 -> 2128 bytes .../models/__pycache__/stream.cpython-313.pyc | Bin 0 -> 1705 bytes FitnessSync/backend/src/models/activity.py | 5 +- FitnessSync/backend/src/models/segment.py | 4 + FitnessSync/backend/src/models/stream.py | 30 + .../__pycache__/discovery.cpython-311.pyc | Bin 2185 -> 2415 bytes FitnessSync/backend/src/schemas/discovery.py | 4 + .../__pycache__/bike_matching.cpython-311.pyc | Bin 10202 -> 10684 bytes .../__pycache__/bike_matching.cpython-313.pyc | Bin 9744 -> 10277 bytes .../__pycache__/discovery.cpython-311.pyc | Bin 18344 -> 18557 bytes .../__pycache__/discovery.cpython-313.pyc | Bin 15116 -> 16194 bytes .../__pycache__/parsers.cpython-311.pyc | Bin 11135 -> 15049 bytes .../__pycache__/parsers.cpython-313.pyc | Bin 9908 -> 13438 bytes .../power_estimator.cpython-311.pyc | Bin 6599 -> 7764 bytes .../__pycache__/scheduler.cpython-311.pyc | Bin 11443 -> 12117 bytes .../__pycache__/scheduler.cpython-313.pyc | Bin 9591 -> 10125 bytes .../segment_matcher.cpython-311.pyc | Bin 15333 -> 21837 bytes .../segment_matcher.cpython-313.pyc | Bin 6980 -> 14196 bytes .../backend/src/services/bike_matching.py | 22 +- FitnessSync/backend/src/services/discovery.py | 16 +- FitnessSync/backend/src/services/parsers.py | 74 + .../backend/src/services/power_estimator.py | 52 +- FitnessSync/backend/src/services/scheduler.py | 213 ++- .../backend/src/services/segment_matcher.py | 193 ++- .../sync/__pycache__/activity.cpython-311.pyc | Bin 25617 -> 27540 bytes .../sync/__pycache__/activity.cpython-313.pyc | Bin 23935 -> 25720 bytes .../backend/src/services/sync/activity.py | 40 + .../__pycache__/definitions.cpython-311.pyc | Bin 15757 -> 15276 bytes FitnessSync/backend/src/tasks/definitions.py | 62 +- .../src/utils/__pycache__/geo.cpython-313.pyc | Bin 5750 -> 7057 bytes FitnessSync/backend/templates/activities.html | 58 +- .../backend/templates/activity_view.html | 1536 +++++++---------- FitnessSync/backend/templates/discovery.html | 67 +- FitnessSync/backend/templates/index.html | 226 +-- FitnessSync/backend/templates/segments.html | 20 + ...alysis_export.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 7505 bytes ...bike_matching.cpython-313-pytest-9.0.2.pyc | Bin 5725 -> 5790 bytes .../tests/unit/test_analysis_export.py | 105 ++ .../backend/tests/unit/test_bike_matching.py | 15 +- FitnessSync/docker-compose.yml | 42 +- FitnessSync/docker/patroni.yml | 69 + FitnessSync/docs/specs/data-architecture.md | 57 + FitnessSync/requirements.txt | 3 +- 99 files changed, 2118 insertions(+), 1684 deletions(-) create mode 100644 FitnessSync/.dockerignore create mode 100644 FitnessSync/backend/alembic/versions/20ccc82af3f2_remove_btree_geometry_indexes.py create mode 100644 FitnessSync/backend/alembic/versions/62a16d820130_add_activity_streams_and_postgis.py create mode 100644 FitnessSync/backend/alembic/versions/8c5791dd193e_add_missing_activity_columns.py delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/09c17c0f0e9e_add_extended_fit_metrics.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/1e157f880117_create_jobs_table.cpython-313.pyc create mode 100644 FitnessSync/backend/alembic/versions/__pycache__/20ccc82af3f2_remove_btree_geometry_indexes.cpython-311.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/24df1381ac00_initial_migration.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/299d39b0f13d_add_mfa_state_to_api_tokens.cpython-313.pyc create mode 100644 FitnessSync/backend/alembic/versions/__pycache__/62a16d820130_add_activity_streams_and_postgis.cpython-311.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/73e349ef1d88_add_bike_setup_to_activity.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/792840bbb2e0_allow_null_tokens_and_expiry_during_mfa.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/85c60ed462bf_add_state_tables.cpython-313.pyc create mode 100644 FitnessSync/backend/alembic/versions/__pycache__/8c5791dd193e_add_missing_activity_columns.cpython-311.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/95af0e911216_add_bike_setups_table.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/a9c00e495f5e_add_segments_tables.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/b43006af329e_add_avg_temperature_to_activity.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/b5a6d7ef97a5_add_fitbit_redirect_uri.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/bd21a0528865_expand_activity_schema_metrics.cpython-313.pyc delete mode 100644 FitnessSync/backend/alembic/versions/__pycache__/ce0f0282a142_add_mfa_session_fields_to_api_tokens.cpython-313.pyc create mode 100644 FitnessSync/backend/backfill_segment_geom.py create mode 100644 FitnessSync/backend/backfill_streams.py create mode 100644 FitnessSync/backend/src/api/__pycache__/analysis.cpython-313.pyc create mode 100644 FitnessSync/backend/src/models/__pycache__/stream.cpython-311.pyc create mode 100644 FitnessSync/backend/src/models/__pycache__/stream.cpython-313.pyc create mode 100644 FitnessSync/backend/src/models/stream.py create mode 100644 FitnessSync/backend/tests/unit/__pycache__/test_analysis_export.cpython-313-pytest-9.0.2.pyc create mode 100644 FitnessSync/backend/tests/unit/test_analysis_export.py create mode 100644 FitnessSync/docker/patroni.yml create mode 100644 FitnessSync/docs/specs/data-architecture.md diff --git a/FitnessSync/.dockerignore b/FitnessSync/.dockerignore new file mode 100644 index 0000000..2b2579d --- /dev/null +++ b/FitnessSync/.dockerignore @@ -0,0 +1,9 @@ +docker/data +.git +.venv +.gemini +__pycache__ +**/*.pyc +.env +.pytest_cache +logs/ diff --git a/FitnessSync/backend/__pycache__/main.cpython-313.pyc b/FitnessSync/backend/__pycache__/main.cpython-313.pyc index 71544e2585b9087f526bf1b5b9e249551bc6512f..3f660c4a3bf8f1e35d62f8c2df7e34d734ba422f 100644 GIT binary patch delta 22 ccmdm?y+fP#GcPX}0}vcGPR(4kk$1Hy08M@d*8l(j delta 22 ccmdm?y+fP#GcPX}0}!m None: + pass + + +def downgrade() -> None: + pass diff --git a/FitnessSync/backend/alembic/versions/62a16d820130_add_activity_streams_and_postgis.py b/FitnessSync/backend/alembic/versions/62a16d820130_add_activity_streams_and_postgis.py new file mode 100644 index 0000000..cee6cb6 --- /dev/null +++ b/FitnessSync/backend/alembic/versions/62a16d820130_add_activity_streams_and_postgis.py @@ -0,0 +1,54 @@ +"""add_activity_streams_and_postgis + +Revision ID: 62a16d820130 +Revises: 52a16d820129 +Create Date: 2026-01-13 11:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = '62a16d820130' +down_revision = '52a16d820129' +branch_labels = None +depends_on = None + +def upgrade() -> None: + # Enable PostGIS + op.execute("CREATE EXTENSION IF NOT EXISTS postgis") + + # Create table + op.create_table('activity_streams', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('activity_id', sa.Integer(), nullable=False), + sa.Column('time_offset', postgresql.ARRAY(sa.Integer()), nullable=True), + sa.Column('latitude', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('longitude', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('elevation', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('heart_rate', postgresql.ARRAY(sa.Integer()), nullable=True), + sa.Column('power', postgresql.ARRAY(sa.Integer()), nullable=True), + sa.Column('cadence', postgresql.ARRAY(sa.Integer()), nullable=True), + sa.Column('speed', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('distance', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('temperature', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('moving', postgresql.ARRAY(sa.Boolean()), nullable=True), + sa.Column('grade_smooth', postgresql.ARRAY(sa.Float()), nullable=True), + sa.Column('geom', geoalchemy2.types.Geometry(geometry_type='LINESTRING', srid=4326), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_activity_streams_activity_id'), 'activity_streams', ['activity_id'], unique=False) + + # Check if index exists before creating + conn = op.get_bind() + index_exists = conn.execute(sa.text("SELECT 1 FROM pg_indexes WHERE indexname = 'idx_activity_streams_geom'")).scalar() + if not index_exists: + op.create_index('idx_activity_streams_geom', 'activity_streams', ['geom'], unique=False, postgresql_using='gist') + + op.create_foreign_key(None, 'activity_streams', 'activities', ['activity_id'], ['id']) + + +def downgrade() -> None: + op.drop_table('activity_streams') diff --git a/FitnessSync/backend/alembic/versions/8c5791dd193e_add_missing_activity_columns.py b/FitnessSync/backend/alembic/versions/8c5791dd193e_add_missing_activity_columns.py new file mode 100644 index 0000000..1446cb2 --- /dev/null +++ b/FitnessSync/backend/alembic/versions/8c5791dd193e_add_missing_activity_columns.py @@ -0,0 +1,218 @@ +"""add_missing_activity_columns + +Revision ID: 8c5791dd193e +Revises: 62a16d820130 +Create Date: 2026-01-13 19:30:07.100001 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision: str = '8c5791dd193e' +down_revision: Union[str, None] = '62a16d820130' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('scheduled_jobs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('job_type', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('interval_minutes', sa.Integer(), nullable=False), + sa.Column('params', sa.Text(), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=True), + sa.Column('last_run', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_run', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_scheduled_jobs_id'), 'scheduled_jobs', ['id'], unique=False) + # op.drop_table('spatial_ref_sys') + op.add_column('activities', sa.Column('garmin_activity_id', sa.String(), nullable=False)) + op.add_column('activities', sa.Column('activity_type', sa.String(), nullable=True)) + op.add_column('activities', sa.Column('start_lat', sa.Float(), nullable=True)) + op.add_column('activities', sa.Column('start_lng', sa.Float(), nullable=True)) + op.add_column('activities', sa.Column('file_content', sa.LargeBinary(), nullable=True)) + op.add_column('activities', sa.Column('file_type', sa.String(), nullable=True)) + op.add_column('activities', sa.Column('download_status', sa.String(), nullable=True)) + op.add_column('activities', sa.Column('downloaded_at', sa.DateTime(), nullable=True)) + op.add_column('activities', sa.Column('bike_match_confidence', sa.Float(), nullable=True)) + op.alter_column('activities', 'activity_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('activities', 'start_time', + existing_type=postgresql.TIMESTAMP(), + nullable=True) + op.alter_column('activities', 'calories', + existing_type=sa.INTEGER(), + type_=sa.Float(), + existing_nullable=True) + op.create_unique_constraint(None, 'activities', ['garmin_activity_id']) + op.drop_column('activities', 'source') + op.drop_column('activities', 'activity_data') + op.drop_column('activities', 'end_time') + op.create_index(op.f('ix_activity_streams_geom'), 'activity_streams', ['geom'], unique=False) + op.create_index(op.f('ix_activity_streams_id'), 'activity_streams', ['id'], unique=False) + op.add_column('auth_status', sa.Column('service_type', sa.String(), nullable=False)) + op.add_column('auth_status', sa.Column('username', sa.String(), nullable=True)) + op.add_column('auth_status', sa.Column('authenticated', sa.Boolean(), nullable=True)) + op.add_column('auth_status', sa.Column('token_expires_at', sa.DateTime(), nullable=True)) + op.add_column('auth_status', sa.Column('last_login', sa.DateTime(), nullable=True)) + op.add_column('auth_status', sa.Column('is_china', sa.Boolean(), nullable=True)) + op.add_column('auth_status', sa.Column('last_check', sa.DateTime(), nullable=True)) + op.drop_constraint('auth_status_service_key', 'auth_status', type_='unique') + op.drop_column('auth_status', 'is_authenticated') + op.drop_column('auth_status', 'last_sync') + op.drop_column('auth_status', 'service') + op.add_column('configurations', sa.Column('fitbit_client_id', sa.String(), nullable=True)) + op.add_column('configurations', sa.Column('fitbit_client_secret', sa.String(), nullable=True)) + op.add_column('configurations', sa.Column('garmin_username', sa.String(), nullable=True)) + op.add_column('configurations', sa.Column('garmin_password', sa.String(), nullable=True)) + op.add_column('configurations', sa.Column('sync_settings', sa.JSON(), nullable=True)) + op.drop_constraint('configurations_key_key', 'configurations', type_='unique') + op.drop_column('configurations', 'key') + op.drop_column('configurations', 'value') + op.add_column('health_metrics', sa.Column('metric_value', sa.Float(), nullable=False)) + op.add_column('health_metrics', sa.Column('timestamp', sa.DateTime(), nullable=False)) + op.add_column('health_metrics', sa.Column('detailed_data', sa.Text(), nullable=True)) + op.alter_column('health_metrics', 'date', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=False) + op.drop_column('health_metrics', 'value') + op.add_column('segments', sa.Column('geom', geoalchemy2.types.Geometry(geometry_type='LINESTRING', srid=4326, dimension=2, from_text='ST_GeomFromEWKT', name='geometry'), nullable=True)) + + # Check if index exists before creating + conn = op.get_bind() + + idx_exists = conn.execute(sa.text("SELECT 1 FROM pg_indexes WHERE indexname = 'idx_segments_geom'")).scalar() + if not idx_exists: + op.create_index('idx_segments_geom', 'segments', ['geom'], unique=False, postgresql_using='gist') + + ix_exists = conn.execute(sa.text("SELECT 1 FROM pg_indexes WHERE indexname = 'ix_segments_geom'")).scalar() + if not ix_exists: + op.create_index(op.f('ix_segments_geom'), 'segments', ['geom'], unique=False) + op.add_column('sync_logs', sa.Column('operation', sa.String(), nullable=False)) + op.add_column('sync_logs', sa.Column('message', sa.Text(), nullable=True)) + op.add_column('sync_logs', sa.Column('records_processed', sa.Integer(), nullable=True)) + op.add_column('sync_logs', sa.Column('records_failed', sa.Integer(), nullable=True)) + op.add_column('sync_logs', sa.Column('user_id', sa.Integer(), nullable=True)) + op.drop_column('sync_logs', 'source') + op.drop_column('sync_logs', 'records_synced') + op.drop_column('sync_logs', 'error_message') + op.drop_column('sync_logs', 'destination') + op.drop_column('sync_logs', 'sync_type') + op.add_column('weight_records', sa.Column('fitbit_id', sa.String(), nullable=False)) + op.add_column('weight_records', sa.Column('unit', sa.String(), nullable=False)) + op.add_column('weight_records', sa.Column('timestamp', sa.DateTime(), nullable=False)) + op.add_column('weight_records', sa.Column('sync_status', sa.String(), nullable=True)) + op.add_column('weight_records', sa.Column('garmin_id', sa.String(), nullable=True)) + op.alter_column('weight_records', 'date', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=False) + op.create_unique_constraint(None, 'weight_records', ['fitbit_id']) + op.drop_column('weight_records', 'source') + op.drop_column('weight_records', 'body_fat') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('weight_records', sa.Column('body_fat', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.add_column('weight_records', sa.Column('source', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'weight_records', type_='unique') + op.alter_column('weight_records', 'date', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=False) + op.drop_column('weight_records', 'garmin_id') + op.drop_column('weight_records', 'sync_status') + op.drop_column('weight_records', 'timestamp') + op.drop_column('weight_records', 'unit') + op.drop_column('weight_records', 'fitbit_id') + op.add_column('sync_logs', sa.Column('sync_type', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column('sync_logs', sa.Column('destination', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column('sync_logs', sa.Column('error_message', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('sync_logs', sa.Column('records_synced', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('sync_logs', sa.Column('source', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_column('sync_logs', 'user_id') + op.drop_column('sync_logs', 'records_failed') + op.drop_column('sync_logs', 'records_processed') + op.drop_column('sync_logs', 'message') + op.drop_column('sync_logs', 'operation') + op.drop_index(op.f('ix_segments_geom'), table_name='segments') + op.drop_index('idx_segments_geom', table_name='segments', postgresql_using='gist') + op.drop_column('segments', 'geom') + op.add_column('health_metrics', sa.Column('value', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False)) + op.alter_column('health_metrics', 'date', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=False) + op.drop_column('health_metrics', 'detailed_data') + op.drop_column('health_metrics', 'timestamp') + op.drop_column('health_metrics', 'metric_value') + op.add_column('configurations', sa.Column('value', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('configurations', sa.Column('key', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.create_unique_constraint('configurations_key_key', 'configurations', ['key']) + op.drop_column('configurations', 'sync_settings') + op.drop_column('configurations', 'garmin_password') + op.drop_column('configurations', 'garmin_username') + op.drop_column('configurations', 'fitbit_client_secret') + op.drop_column('configurations', 'fitbit_client_id') + op.add_column('auth_status', sa.Column('service', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column('auth_status', sa.Column('last_sync', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('auth_status', sa.Column('is_authenticated', sa.BOOLEAN(), autoincrement=False, nullable=False)) + op.create_unique_constraint('auth_status_service_key', 'auth_status', ['service']) + op.drop_column('auth_status', 'last_check') + op.drop_column('auth_status', 'is_china') + op.drop_column('auth_status', 'last_login') + op.drop_column('auth_status', 'token_expires_at') + op.drop_column('auth_status', 'authenticated') + op.drop_column('auth_status', 'username') + op.drop_column('auth_status', 'service_type') + op.drop_index(op.f('ix_activity_streams_id'), table_name='activity_streams') + op.drop_index(op.f('ix_activity_streams_geom'), table_name='activity_streams') + op.add_column('activities', sa.Column('end_time', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('activities', sa.Column('activity_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('activities', sa.Column('source', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'activities', type_='unique') + op.alter_column('activities', 'calories', + existing_type=sa.Float(), + type_=sa.INTEGER(), + existing_nullable=True) + op.alter_column('activities', 'start_time', + existing_type=postgresql.TIMESTAMP(), + nullable=False) + op.alter_column('activities', 'activity_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.drop_column('activities', 'bike_match_confidence') + op.drop_column('activities', 'downloaded_at') + op.drop_column('activities', 'download_status') + op.drop_column('activities', 'file_type') + op.drop_column('activities', 'file_content') + op.drop_column('activities', 'start_lng') + op.drop_column('activities', 'start_lat') + op.drop_column('activities', 'activity_type') + op.drop_column('activities', 'garmin_activity_id') + # op.create_table('spatial_ref_sys', + # sa.Column('srid', sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column('auth_name', sa.VARCHAR(length=256), autoincrement=False, nullable=True), + # sa.Column('auth_srid', sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column('srtext', sa.VARCHAR(length=2048), autoincrement=False, nullable=True), + # sa.Column('proj4text', sa.VARCHAR(length=2048), autoincrement=False, nullable=True), + # sa.CheckConstraint('srid > 0 AND srid <= 998999', name='spatial_ref_sys_srid_check'), + # sa.PrimaryKeyConstraint('srid', name='spatial_ref_sys_pkey') + # ) + op.drop_index(op.f('ix_scheduled_jobs_id'), table_name='scheduled_jobs') + op.drop_table('scheduled_jobs') + # ### end Alembic commands ### diff --git a/FitnessSync/backend/alembic/versions/95af0e911216_add_bike_setups_table.py b/FitnessSync/backend/alembic/versions/95af0e911216_add_bike_setups_table.py index 267209d..9131dbe 100644 --- a/FitnessSync/backend/alembic/versions/95af0e911216_add_bike_setups_table.py +++ b/FitnessSync/backend/alembic/versions/95af0e911216_add_bike_setups_table.py @@ -25,6 +25,9 @@ def upgrade() -> None: sa.Column('frame', sa.String(), nullable=False), sa.Column('chainring', sa.Integer(), nullable=False), sa.Column('rear_cog', sa.Integer(), nullable=False), + sa.Column('purchase_date', sa.DateTime(), nullable=True), + sa.Column('weight_kg', sa.Float(), nullable=True), + sa.Column('retirement_date', sa.DateTime(), nullable=True), sa.Column('name', sa.String(), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), diff --git a/FitnessSync/backend/alembic/versions/__pycache__/09c17c0f0e9e_add_extended_fit_metrics.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/09c17c0f0e9e_add_extended_fit_metrics.cpython-313.pyc deleted file mode 100644 index 0091640423a91ea18b2788a7f8d47fbcd921ccc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4717 zcmc&&TWr%-7bAg|jy7p}31o$F7nD)4y5rIY87DbOT#`5(yEmjs z6))qldziGnRnw$(uYiWsR_X*uAV9)ajfA}IY1_lz(7I`_J10%k(1M1gjac%Z|MJ;i z&ar*|&tCR;SPE})9hCM|P}DCZSlvLuS)Rqt7u0Twrx3;4c>1)hk4AK#9oab&+fRc& z2Xf?naN3DnyaO>2cZB7g@8KhP7pjQZBW@ma(GDErJd@7@R7vs?I^rgIT(=kM?n2%3 znmU`Wd-#faY6PGv-osbsJ#P~Ds;((wJTAZq6=vfwE+iyX$UqfIF@Kq3J5!<7nD!d$egGZ5$z6P-bvJ&K^HLhcwgVJ;X5_Ot~$+B$+efgw|T&LmHJT*WU-J;Wkuh{VF zm8Axp#X`}+2)YEp6bnVCiRv!+yqyR8?cD_)?!Vx6l#$z6!kvFEibKvj|LeG2CERa& z&Y3cDyUWPUmXW)njNFxFZFjsco~^qHHDy(lGAY^Cyzs9 zZWqUrLK-GiAqV65BuYk>)l?QLidj!eNJ^$&QQKKO44)ON3IQe(ko+TlEm7BBI~$v|;@flhdP_tVt`P={&mPJ5Ho!Q8its@Ps8H`i@#AUDeF^ zQ*s9S6$MYD>OYI*5j;r6e?n3Rkr=xYBpw+sKbejBqd0>#kY7y0OjL^b#~>nlPVtxO zE8%qlust_vx<+$JB*r1ygUL7JT>)6WOi{lA=VxcGpPAnK4gIbAYxnHl1$vR0W9He1 zgE}{00RE?!0a|PCLUb`Tmzp2Z`13kzrtS=myA%C-hV15%ZYUE{Qr9H9%|wmajxZa3X&x!H27^#@hwJ~F@=s{pO_(EX+dEq7b)wQA>w zbuMCnOY6?@29ST9`q|do(L1S|saqotyL9fX0X|+wGV5OmE{5mA^S#=@Ih{LifD7vo zTI+}RYaeX8yX{`Rc5X=LhM$25+J*0imy|Wp$$4e{J-$WWL|OB+6)ntLQqDZEbZ&pm6$EWkR=h-5wnSyT4HL5sV1fh8y~&0&R4{doGUE!sF_sB1B7;CW4g(k?8-Xp zBZuH!`5Bv)b+-R{|8%d`*kgbYc`ozeOoIVB)}nd?>|Kj?7@)lvX^kNR?0a1}v&8@{ z#p276)_B+eM~X$Q@sI%ymxKolaIhroHNb(AFld0zlCaYNUBz%_$1~8LA3y#G;6zXz zMcIB|wdoRsxEvD%)2UA8Bs?QzIYDGyFkEdeX_#VeG%r!_K5q(4d2WnO^Iz_ozNmW}mZ-q5* diff --git a/FitnessSync/backend/alembic/versions/__pycache__/1e157f880117_create_jobs_table.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/1e157f880117_create_jobs_table.cpython-313.pyc deleted file mode 100644 index afbe5cfa61ba44cabf41c93c30d929b3ae380d59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3038 zcmb_eO-vg{6ke~#YkRQ`p+FjvCN3dq3<3WH6HKEV5<&!4NR*|jqM)wU-ocoxcb(aF zfV7nmmvW?OqNE0isI64@#xbX=sw%Zd9II+H?XB&hx2UA*wQr1#mefr-Fp|e_-h1=C zd2i;uw_AR{$ietJeIxt6z;VB_MSY+eu~h=%0e6CvIKoK|iBCGldBTr7i8IL7&LkRl z5m%K)NrAW}7x7Gar$kA33wBCw(lq6q@=9occflI?o+=*^ALCE)Q(ndgxw9ts*5v;G zkc(BhUux>)W)TTUe#uv*nlmUAxT~cJRxB*f8Yxq@l$3@=@jAYfHM52u92*%9MsYNL zc6w+i5{;hSmSA%@m{Ma=B@&Mf4GqS{i&co=2;d9{W0BZkUnJTWITMTy4G+YJqeK1C zNMbM+7sWr|m3~og&dSO@)Oi&H%2|Ab`I*Zo1=c!nA7DQp+7|w zJv}39N(yTx6;+(ax@yXXo@VCiqa6q*{I>4HRXyNt*7@9Zeuvy4y5OpA)~}Si4wwD% z01e)z;dxokaa~-z#*~~AN;qxWjqzMIjs3tA_8HG@!>cC3Zl56{9?6@4C%z|d#XX{V z_Jr1CL-XxE7hBoxVsCgp8(v@^@ccHsmVLkr*zj8S0k3(V@mg$nuXrmqt+qy9NC)DM zZ9crM)&#QYH_oBgY#Ohy3uwDDvZJAOzHU8Udjmmx-FUsx9$Q`MY$TW3>X@uKgju$< zCs2^faG{O~idY$@d{!NWlIYP38Wc4RQ?HT7M6sY!Qo*zos{myV{)r`9*&Lo^x{SDM z=$Lw-_*L1|lP5-oVAG`T9L!1vQvsF*&7v)7D4AG;A_{fbw6IEryizdXU)=5os;pRd zUAi%MDg^a9U}ACy6IsR63T!9M;29MQc@-)?)1+c{e!GKYRXB`L(6fLW3J}(!sIS&y zs@;Y1rb2~_hE~XdGm=HJdWL#f>zD)&sC!Jea0U~KCh@#Q(YSPVg1RplhK3cLB8xHG zu90j`Aqz>oaM93Bizr#$+Od+=RXk4}(_e90EdF#lJY(c=*aRcf zz6Vctk^2n^A6;F#x-$9;5MxVY%j*3L56A9}eUdDlD5FT7duuJWo+u_(-`;31qqBAH z%o@M$EqYg*O1O*$>RfH@!g{ip+-NHw8T%%V4a&?Yh6EBJh*zOG+9QmI+xjW7u~Dg(nuLaYuwF1@Qbc5dp_&={PYumIB!=L zi{h#eY&JgNcjWuIZ|8oPt6Y9}Gc{dCnflgUUH~Qw&`;>_Q*`*zNCmz5;&8wvyg16a z+$?+$vypovkB(H@PL_m?whiG)p@J@vK8W+KF3%oZwaYI7@&8@kmaRM89yZnn`wG~} zR$g)@_+9zmxB~6ejA~A!3GxP@5kE|j8DcdDWL?SO5ZcZTQ6)xRuD)8*3tI32!x&XX za+>XBPLh5m^0F_V?N>Gc>mK--Ct%usWmlH2-0ynWeXo1vgGy`nM#ob$#9|HXm6JcB zLsc9|W~c#Hf#?&V7V4H|)kw=S6|99kBqCv;2g()=Q@7o4nFsA2aYXzmXBAsqtCh`i~i-piu9;^K_gA?1v;NqkxO{2l_{#ia=f@vDRySLF%D&zT7keQbaEvTxI^me+Ef)YJ7^s+c!Tvr;sgrDpMr0k4*- znB~=K0pmio1nOe~h7YNA2Hd~+Fq1E23Z+z`T6<8amGjwBWwlzVsOooq^0Y{mI_z}7 zF@c6Q9kx+gprYH}ps}l)QH4zEps}ml(KvQrIr;?|ZxF*Vw{^>CfaOsYLmM3I>8?Y= z1pE$h%!`IIg@%|EoNbU{2%m&Va{|IR-S! zJ8_nsXXcNET;sr9(jtkmWg|UHBf5@VQ`c$O@3vbkf;WKhr%fZ1$}PQU*C zPXEPmaw$N0CIgfo7RnRi?qTUe=JWMnA#96$n4yAW*4skGl c?>KfpKt(14R2+o`Av!|SpiG<7?D#Hz0p>v%P5=M^ literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/alembic/versions/__pycache__/24df1381ac00_initial_migration.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/24df1381ac00_initial_migration.cpython-313.pyc deleted file mode 100644 index 6661b6e3df5c988adfd20297dc4ff8e8b0977efa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11349 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 CPxSTx diff --git a/FitnessSync/backend/alembic/versions/__pycache__/62a16d820130_add_activity_streams_and_postgis.cpython-311.pyc b/FitnessSync/backend/alembic/versions/__pycache__/62a16d820130_add_activity_streams_and_postgis.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1535a7d7078e157aea237a9dd79d7c63b67af7bd GIT binary patch literal 4498 zcmcf^T}&Ivd2QDAuoyzHdkKcX5&;PieiIU6eohI0X9A`*sJ(Me+Af~KS+IAV-8F~F zRpKEpmGV$kt*)(tluCgX;YdXyQXhHbarZRRp{eevYNftWrT21AowdCNlUz%$5u@2} zzWHYM`iJY}O^I+oM15|+&|Z53-zLUSxMbqPw#&Hp}B59H$^P#aIf4fiYy$uKYdU|j6+!y{jkJIPM>ob!`sfR8nC&a5;%8(jF&>_oN)+ zJ;|TARivj?IacsZZn#Wg8GYW|0+yq6$MND)5r>W~itiNXj?#U$vl-~rJty90Z5)=B z&|Nltiy}Mrgn?|Nezjw9z+$CN-cZoG;kVsS7 zdyD|SGe~^D=jhk{wgohn(eb%M3zXPav>4V=Yy6fqus(Zbi^js8Vmv%$kI&`~X-1l{ac|wun1r#OF3Z*MD;T|DT z)diK7PsEi{K0_+;#6*bX7@cb=gdhP>(sa=xROl#~LUeChMZ}#{RwOx2ym6K87qq0J z=#K~+M@o`#)1mPA+Dy;>is~JJhq!3{>mFn6kt-hliwCh6&QY+nQ zS$dpCYZG(h)8nJ_d@nx{nVsQNaY2$HT16WFlaIzD<9u!=i%G-}^VcN!4!)KG*GTPm zu2J2op>0mJfgU8OKCTaiv}PH~6}&7VtcD)2t}M^WTQM#Z;<$L5wXU8Fq^alw6Qv-7847>T<37WU(y# zw}QSal_8#VDtAZJ0m|R{N;D7k-a7L)*8OzKxNyg02QWKeumj(?8JFkjtl@9{8gS3b zf6=_*e9ryW``r7-v;q1}(2qfXex-A>=4Igb^)KuH>@vWJ2}Up&u~!1PsrU8Oz4qN3 zdpFFc4{_6n2h9eUHNh+fv-wT!8%wyM>y?^a-CoPCnGHeQ5Zs?Pz?2E5Fqq13>eyJr zjoq)EdtjH_W*aTq= z!g)^DHiEdW+bH486aeW5C)A-lAl$R^AygZRqZ zIu$v6v^|xbGVAW(x;y*S1(PP2#9%Vd=}a!@+$=cr`fmGPyV*31n}!cq1I(CU27{SA zr|TP2xc zGH`2u$oO&82;4FQ4{_k3vAAS_WfLr8uuP?5(D;HiK??>g|7^PGa{r5=;K%~sH0Q3+ zukZhA%%}~T{z2>?G=^u5h+zEar-mdOtYWeXW)+Ii>*lR`Hu&@5XT$4bhrUZ+4E=Iw z)A`b4Tu+(&W6VD`eJb{;U->d$`ZA{P3HCi%pFXU(bi_DZYYxl!3&!P}M*rkN>%nCs zDj4dj0Wv1YV345*z22Cu{Y|sM_x)*le|W#!nE%+A`^03Un2j2Ce?z|qCT0Pes4A&^P}*kHyOgBfvFPPc`Ap7#lf!i0Pamz8f_3I= z`c2LlHO(g5g4q_s?zwjJQU>7zv)68Q(Nd(;yWgEL5c2- z=~^xlsvXucPz!(iq|(wzIG!%3G~+xHXnsQpjoN`yG!|)sBIkmLYH6(wS)gdtw_qr# zz;ptQs1tO-svxxAQE*h|a5#>fZU=kBSfGk=Tr!vsPWD5FQVRW$X*WvyAyZ@6{fJ(f oz&bdrZdk88toBn-d2yXPVjCR~9Y@StaEyv>rPt-%AgBAk0Zy90ApigX literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/alembic/versions/__pycache__/73e349ef1d88_add_bike_setup_to_activity.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/73e349ef1d88_add_bike_setup_to_activity.cpython-313.pyc deleted file mode 100644 index dfe9e77ec758981534bfb3f4d1fdf85a65fcdef2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1537 zcma)6&2Jk;6rcU@+TQpB3Qb8>kyTPe4J2zjaZ+2M5^6zeqFQaXU#M1B>)na7&3f0J zT?dSW6!p>^&7o4bSqO=6<5*E7{skv#)o9>WIB=^7h%0Y48^<6aG15N$=KW^gn|Z%C zdyr1c2!`|YuDvH9^ny)}h8Tf^Z2*tZTS!9$X`IF{af>|R7X>0HY%VN`i!l-lvA86W zxE3RcTCyf<(%Z08i<6O>P)lm!EFXh8_!FUDBq`>v@wFuL1205;GUC(!!OJ0^)<#aF zdm_nbX)P6^Y(vatK5HA6rPS>;toS%+x0JwB3^TCT?Vuye%Xr=PZO>IMU#ckcGk9jU zj8_WQ`SXVi_A5%cWUSfvG6Pd~t3nUzp0zD}|ZL?0ls-J3Tj_ zFP3Ly`8T)%%JDRz;rngunwZ9JxR5BvQQm7+X=*qbO_1muG&P)#rY-LiS09k+Ix$>x zRd5fJc0H}u!9)o#=?i~5iGCzO=Faes~Inov_O9Ns6UpQtO=S}7D`bbWjJ@(S%WPhfXJ+4Vj8bA}kW@w=CWCoOaWtyl+tH;vtS-b_3kNgq|=% zJ@pljV7uY!Yq;|*I$%ejp1SHavFiH)b_4Y)@$O+W@YRp(;5sqPwIV|<_I<76nra<9 zpoD5TxLLPNbsZDdRlYi`vd&I(d{RB|^rJ?cZgpt9-D(iS!sIlhKM9}z0hkT+izq$3 z^59DMy|4Lil3yja)gOvKR=%%1S?Dj{+)-{l6>tA6O3%fyU2$ydY%jIleC+k#ulBFq z=wH40OuR+jfGGcH4<>Qg9(Mu!kM;;3xh9n1c@MSHpk*LPG*~NX;<5N3VysHiz*0G? zFccV+iv^t))=37KoNy>r)*@aDv~vA`7`7YGk;uWsBd1_xco8zjq9xh0I4qU|^E2@I z6JWw(Jr_rJ#nEoCRr~JDu6O~8^RW7$+Fj_4KNAaK5O|gnfqwyUtGQQbT-Pnn)O9Ms zscZ<#YD3udXgtz0>L&lbV>sq2Zg$9PY)zc45#YcytLujAdVvuz)peb)CrDVwlM!Z^ zq(ounUpGlCG(D^B;0xp&upE5859X!7aoq2`#EE}p5I6ZN8vO~KdYM44j&CG3C-=~s z`xE(%^k%!OZJq6YyptW@L;3weDRQ0b&3rkv{d)i8)J}GK50&<(iyQLhhuz8Ur#<0| c&wEz?#5+5wclS{74@ux|bAM(8t{7J4Zk diff --git a/FitnessSync/backend/alembic/versions/__pycache__/792840bbb2e0_allow_null_tokens_and_expiry_during_mfa.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/792840bbb2e0_allow_null_tokens_and_expiry_during_mfa.cpython-313.pyc deleted file mode 100644 index 59375aa915f2edaf6354be490ca1d376da2ec3e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1356 zcmds1y-(Xf6hAwOZQO(cMWsS~SZt9BPFy0A2vv&sP$^$k9CaYAPL6W~7spP|4v?xt z#L}Tl7Zy;}u1NiZ(3P!IH%i*Qcd=8VvUKiA_q+EYKfU*RKe>}g$bg>rA2%#L0>C%s z^bQ{yoh&s@fDSOwIh|kUp7NMim>pS<;HVzOF+GL_J%UAgC0!T?ct95^%C$&RdUIT` zZTC=dnzpU@?mlun#c-+$I%-(BrBs{Pa%#%vYC)EZ=)m$U*HPA%^U6X_%g(1Nm5PQ^ zK?Zqw#YCyyl$Oi7VPyD7S*DOzw3L>aO>48-BPBhbPv`RK%v>g?r53WX{GE-G z(mP0$yTaKw#XaqS`)v!^$Vwvgz!ff}R+P@xX zFzrCrU^oq=+JS*~V4xjX-!HEdK79;Ln9qt{!c&<2-L;obJ}_J@tnbC;9T zpLR}n&R$$Rn7EGdv8kT|h>2nLA`9$+SuQ@CcF zV1YpfLk#XR7-qmIz>G~YP<0QTfZ_YppP=^^!~GmiGKQ!78SdwQk}*8pEuXwYLNZCt9R4Cmf^`FL+zK`va}QD-Bikc#6*Z{1Jm@-IIbhcD z-vddDb+_8I(PKPCvmBd8qi_}BIPM28aM53bfE)V?#;zm~A8$(+LsRX<#n32)#Bf`_ O76fjpGa_)o6u$vu|0pZ~ diff --git a/FitnessSync/backend/alembic/versions/__pycache__/85c60ed462bf_add_state_tables.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/85c60ed462bf_add_state_tables.cpython-313.pyc deleted file mode 100644 index 52520ccf5406cb6636e9bcd39f52e9d2adae9f08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3698 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 Oii@*s=%S&jv;PN{z;lKG literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/alembic/versions/__pycache__/95af0e911216_add_bike_setups_table.cpython-311.pyc b/FitnessSync/backend/alembic/versions/__pycache__/95af0e911216_add_bike_setups_table.cpython-311.pyc index f74392bfc2e6faf47736979a44bbe37b723fbea0..5468518c8efedaa3d84d0ffcb70450c87e7c0241 100644 GIT binary patch delta 593 zcmcaCvQM0EIWI340}yPSla{H@wvq1!BexKc%M8SypD=I!$XLn9$Tqo=Nqlk>vl$~h zl%>JXUBU{|1_4xx!*|mxZmxvm<-1(Hx7JR&%U2uv}m#YD0dXEywl54o;sc2IDm6KjU6)a7ax?pUMw!XV z99jk%K$~xIr4;2C#Fr!{<)jvA0tJe+K!i3(iZdm@JTJW{F(tLg5Xje_j8B>_o2te#Yn2^E{%%CYa`6jCk z8%thdZfeM6H?{2KczG$)vm~7@&T@+5< ONQKFBxn)^E76Jgk5LlD| diff --git a/FitnessSync/backend/alembic/versions/__pycache__/95af0e911216_add_bike_setups_table.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/95af0e911216_add_bike_setups_table.cpython-313.pyc deleted file mode 100644 index 2c0efa53fdd4c10bb00a44aed0196844c4c3e2c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2468 zcmb_dO>7fK6rNqL|JHUwkST4c%94Z@2NM4fC)h$gKuVRQ1jGha6;W4f@7USI-Zisp z5>p{XxsYQTf84)CNGC#>p@Pwc65Rb&xo=IWCOT3OO zO!|ml_L9I%a7L7U@54^nPeL=EnV>8T^Kb+LeZbKRB+T?P{7jJP0r#}sv zL=L@;<^<9uN93?0br*$L*UgHmX;My~$C8EZYQ>UlHCMu-cm?0kE!{Mv@v*FwNvnkf z&Loqm)7Vj;Ka{ zRw=1DT(YRB;R-f1OEHZ+JFYzrfZ%~|HvscpbgivNSNM+D5qjaduC<>PUGa5*jsOw`AEP3mK2gdVhajmxWVo9aoOsXtSwcdQ-X&U&Z*Dx1O#Gu`~EKF`- zqG-6FR!cVNh9Id}t!O|PY!~g(7Zq0&imrhpU)9ioC1N4Qw}^(Drzj3bQQlIi@4Q*6 zmJRA3H*8$Qg!*K=86jh$X$XS~HeR%8^fJ-QDydE4+IiEkYyzp+9f!K1;YG?7zCyd~ z^5|TA)-2<=W!c!U2tMm~n3m8lg75aFcC&B7ElVpbHVibx+=!5@ez97EF2_Oc|&dkhM)UsvfrZ+S)0l-Bi;imX&u z<$L|BpVqsNHc;YOGTl<0cy#*9!L8`dp}~6hPy?l(4W*XEl@C|@Ru><6zPR;B+c`K; W51(wH)C*sjo96zMJlrwoX8r~|H~#Se diff --git a/FitnessSync/backend/alembic/versions/__pycache__/a9c00e495f5e_add_segments_tables.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/a9c00e495f5e_add_segments_tables.cpython-313.pyc deleted file mode 100644 index 9eda1ea3d6d7c67e41ef933d8fbb71ecfc837643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4622 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 Q0~c=_`|C;Gmb7;5AJ4nyxc~qF diff --git a/FitnessSync/backend/alembic/versions/__pycache__/b5a6d7ef97a5_add_fitbit_redirect_uri.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/b5a6d7ef97a5_add_fitbit_redirect_uri.cpython-313.pyc deleted file mode 100644 index e83e7c273e1ce295dafbcc191c06973bd8a9da12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1322 zcma)6&1)N15P$oj)oQJk8|MpBXoQ@!QH%9iC6O9>sMCV&Mig&yFcxf9yU+5bt6gPw zH?0c=kxPAa4uSS&N}=ih&{JFJRmKF&%dzbtw-C4V+Ii9nJ0*cU^k(PHZ=PoM_Z#+5 z*ENFc!OyqcPbEVB;D_mw6X);|IuFT3Vh|vPV2CTivIyd`1X7-#r4@NO1*x$wuPBf< zQlNG+9nDbQ!J9@JW;#+QW61A|DLi9b9mi$J@_0w=WOy8LDZw)duD=4;#<*_GoFjK+ z$Qink9ecAqxtRM3EXOf7+^FkDCQ!!(YDZ=auBNThdv53ketzkCE8lHcmz^tgqj|+@ zOb{Bj^0j8usW-cojcVP|7J*t3&0j}@NTqgpp;E27@-s9xujJNcv#ev{1Yh{r@}xE~WCr^5$m};|}9rzn=F9s_@3PN`BC0g7>W>i1+@ZCIG#Oc9*WEvOF4!pSU zGcmN7vN-V-BLdg&{X`CVUFMZHgFY>XVMP6?{22syP~foqi5uMn%l^E^J)ef5vE|$4 zE=G{GvgOf!*R{*{DDVjg%f}TlUokPI!4^x$gC0&4g|k@uH165@#R9gIP_4l=3)_z?dt($unZa#4^Me=#|=_<4wq+ zDT))?W*i6x32$(Uqr5DfLBle9q$d5J!}FWC!#OmQ{@eH4qm%Qai>=4<2jfdP!V1tR z2ESc=gQZQ=32f73O0+dV0f2W7{1sR_;WH5~yz5z>y-E99aDwNgd5#oDz-CR;^8Fy1 z%z|kGA6MWX9hl*UvkJVm|97rl>jzHk(GQ`3sDL{R(L9p`L3l1Ig8Xle2=jlDGf&9b zXDT^yeyDEG?~}I<3YDS09q$<5Uf5aNn?1izDhJg@61uco|8C*YsnO|$z1h+}X&jVl nLv8!xo%x+Fcclkk?>eKoclWaI?UUNyiYRo2f3l)*ZOqJn=j%V3 diff --git a/FitnessSync/backend/alembic/versions/__pycache__/bd21a0528865_expand_activity_schema_metrics.cpython-311.pyc b/FitnessSync/backend/alembic/versions/__pycache__/bd21a0528865_expand_activity_schema_metrics.cpython-311.pyc index 3af13888e04cf129dcd17bc96ed97b613089a1b6..60aa816f522af0c9b15a6b7e010a03cbab7ed32e 100644 GIT binary patch delta 353 zcmcbr)vV69oR^o20SFA{q-DzRY~*{z$ePB^z%YICK^B9}x=ha)C(mWhm|V@m2c|!= z{F>~`p0PQY4JZL*OkT*bck+2bcEwv5uY(C_87qsBUlHHrM}iqDsz7~3Y9K-#NZjH~$uG}KFG@^F zEz$<^b%D58Yjd8EIwQvg7A+tuQk#5G*qKprGOLK53dmxxx*}9#esS33=BJeAq}ml3 cO%4}1YAM1P$oPQ)gESCi)c(MLg8;h<0Ehl!UH||9 delta 483 zcmZoxzpBNzoR^o20SIpW3d_98y^-$~BWn-`1H<&m2U!d@>oPrKob1b_^ z-7~KwH9fUxavWE@iWbm~qCi~yfr){kf#E3&TZ``mouy1mI9F(GP1=&NF>6cKWFBq> zP88neTU-;F7&Rv6@Jh1&0%`<#OLFsK-VP?9t0kCHgeEWK(_|E$?8hn2Q^W@}zDN;7 zC`>jH$WT!SF*QJhCXl$rnUY_gmtK^Zl3Jt-LQEI z6&9TfELcQNEj;tb~ym2F^_iu diff --git a/FitnessSync/backend/alembic/versions/__pycache__/bd21a0528865_expand_activity_schema_metrics.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/bd21a0528865_expand_activity_schema_metrics.cpython-313.pyc deleted file mode 100644 index 93bf42eb2c7e35c80508ce8bb96b70a21376418f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5293 zcmc&&UrgIZ7-q18@>@`6`0wngxBN47td3)RTu(xV#nzX0gB_V`JwDq(REB@vC?)%;S z&Ud!&JMZ~?EQJsA?|aGT9*X*zIQs`E8+$9*_>#Ir@f4zX8&410hG;|&*^!+izI_-B zIgq1hgJCCf@eagHxu;m(c^TK_UC1+KpK|k{pLSp$movq3fV`x9ik@Pv(2M1!M2-M=-U`!f_K)HV)n)sN#j-aSR?goZ97mY**dLq5} z$3>&@SYN!iKhzf+9O#R&>~Huos32`JJbavmiUdu^q=JQlHj`E}Bc``n%w$lxAEvij z&h*LZ6Gf;5eF-Eg(u|N26ELNjtPC?yku^b8BqFzexUpf!=iVkmX6xa8IWz-Y9x_6seD(q-)`uNqqu|(kD@3 zDNxh+c|{5*a0$*5!(s}iai7Dp5Rr^m3s+~L!mAVIP-fnAWi!(V4+;w6F0|oOdw|ot zMd}ye{QU0IyGz~Q(%-qiaj$f(Ce~(hGxe14#RB(%0Y+<^=-#Ban001@-NqW@2HiF9I-2ME6;$=PuqloP64()5mE~U7yc@N{8>}z=1%1(s0v~S^orM4ZG*iK7q zmnAl1iS4$;W-YNjme^iPY@a2z-x9mQ61&k7yU7x}*%G_ufQ{GI))C~x4;11EoF{OO zz*z!k2%IJmAaII6JAsn~P7oj~0BR-BLZF#I6M;qoehfjnw77X>q-M&i8+w~W$&-yb zF_>=hfuppnbYq{K8fDvGRz{wVEL~kbWq>Q>ZBg>t3~>3tJ7a**fp^*fJx6)x3=lbp z9jf}p?YC6BjJiD+2F7aPoT>7ZcaDJDHyy%UUi#nJ4zV2CR z-D&G7G=z4k*o#nfk$v|5(uJkDW&0POEz3L2T?OxzT`KyAv&A-M`!m#H>nV=RKXd*^ A00000 diff --git a/FitnessSync/backend/alembic/versions/__pycache__/ce0f0282a142_add_mfa_session_fields_to_api_tokens.cpython-313.pyc b/FitnessSync/backend/alembic/versions/__pycache__/ce0f0282a142_add_mfa_session_fields_to_api_tokens.cpython-313.pyc deleted file mode 100644 index 9039bf82419a90dea2bd67205c3a6e5f3f53c0eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1830 zcmc&!-ESL35a0Xo*}nLr1xld;Y7#1NA@TVVqtsM9(1MhhSjZb_Oe~!)cbnuI`_A1v zmlz2V^@T`C$UGpy15$*LX#a(zQvU^8Nu_Av4S3+KA|PIwwJ)wIs?^UwwK$e%#0c2nkM;dBO>lorti`z>a4E#&A)b5)uJ-XH z9y^1s$~1+OIMLVAYjQUA@u#M38;w=d@QLp`o@=Z+q;C60;2CDqfzJkUeO0ZH>kfxA zbC-)5i)gEwKC7Gg8GV2dznIZy?A84AY~HjqO}#{k8Ia6n5P)cUVKT2z>Ut(WThv}J z>e^I6FIRP{GF49bcuhRL-SaV^3k@4JvFD=^V(mRVvn8H>?yA1pDNjwNmHBi9&$ z$rWn4*1AzQS4iDws!f{2g+qC+#nT;+I4Ba#qfLR=Z28!aD&kNbD&tVS<>M-&iZTR- z3Pgw_5wRgKRK`&u)Ukx+3sND>lld=lNB+m$%1G{hj|`98R~~-uHI$8(nR=gUEHz3- z$6f>?#N1XLN?9i?%|S~1R)ZL}8JL4;ve|UNX_!G)p}Z?#L14Az)msghiN48{O9NfO z0d?Fp7URTRbQ!h*bSh?$j^4{=sqg*u}rLm@(Z5C}c*QjX|dImOf3Z}mSVhjBuE1xagSm?a= zz4$}?yLk6}@%CGH=62`yO5Mr_z08#ZdFiJIL*BSuxl`S(?k#umm0o7)K(78Pgs)QC zE$uCI7ngdO>RtIm`V8dzM`LiB_Z#C2P#&c*u=qGs#*amOEZ<)n=n^T@BKZ zKFMREJVx>Z%F>2mx~>*heoMj%!=V?N$eW{ZgmeOP}3G+F`yB zx?b6t{$}##Q{7XOz4X*RDjZJfTk7`Por%uoj`ZaxJ9hVp^S#8Y`$+#ok%eX9&y*}o H_iyHJCLfDh diff --git a/FitnessSync/backend/alembic/versions/bd21a0528865_expand_activity_schema_metrics.py b/FitnessSync/backend/alembic/versions/bd21a0528865_expand_activity_schema_metrics.py index 6404a12..e63a5b7 100644 --- a/FitnessSync/backend/alembic/versions/bd21a0528865_expand_activity_schema_metrics.py +++ b/FitnessSync/backend/alembic/versions/bd21a0528865_expand_activity_schema_metrics.py @@ -20,8 +20,9 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('activities', sa.Column('distance', sa.Float(), nullable=True)) - op.add_column('activities', sa.Column('calories', sa.Float(), nullable=True)) + # distance and calories already exist in initial_migration + # op.add_column('activities', sa.Column('distance', sa.Float(), nullable=True)) + # op.add_column('activities', sa.Column('calories', sa.Float(), nullable=True)) op.add_column('activities', sa.Column('avg_hr', sa.Integer(), nullable=True)) op.add_column('activities', sa.Column('max_hr', sa.Integer(), nullable=True)) op.add_column('activities', sa.Column('avg_speed', sa.Float(), nullable=True)) @@ -59,6 +60,6 @@ def downgrade() -> None: op.drop_column('activities', 'avg_speed') op.drop_column('activities', 'max_hr') op.drop_column('activities', 'avg_hr') - op.drop_column('activities', 'calories') - op.drop_column('activities', 'distance') + # op.drop_column('activities', 'calories') + # op.drop_column('activities', 'distance') # ### end Alembic commands ### diff --git a/FitnessSync/backend/backfill_segment_geom.py b/FitnessSync/backend/backfill_segment_geom.py new file mode 100644 index 0000000..8526f71 --- /dev/null +++ b/FitnessSync/backend/backfill_segment_geom.py @@ -0,0 +1,52 @@ +import sys +import os +import json +import logging +from sqlalchemy import text # Import text for raw SQL + +# Add backend to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from src.services.postgresql_manager import PostgreSQLManager +from src.models.segment import Segment +from src.utils.config import config + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def main(): + logger.info("Starting Segment geometry backfill...") + db_manager = PostgreSQLManager(config.DATABASE_URL) + + with db_manager.get_db_session() as session: + segments = session.query(Segment).filter(Segment.geom == None).all() + logger.info(f"Found {len(segments)} segments needing geometry backfill.") + + count = 0 + for seg in segments: + try: + points_data = json.loads(seg.points) if isinstance(seg.points, str) else seg.points + if not points_data or len(points_data) < 2: + logger.warning(f"Segment {seg.id} has insufficient points.") + continue + + # Points format: [[lon, lat], ...] or [[lon, lat, ele], ...] + # WKT: LINESTRING(lon lat, lon lat, ...) + coords = [] + for p in points_data: + if len(p) >= 2: + coords.append(f"{p[0]} {p[1]}") + + if coords: + wkt = f"SRID=4326;LINESTRING({', '.join(coords)})" + # We can set string to geom column if using geoalchemy2, it handles WKT + seg.geom = wkt + count += 1 + except Exception as e: + logger.error(f"Error processing segment {seg.id}: {e}") + + session.commit() + logger.info(f"Backfilled {count} segments.") + +if __name__ == "__main__": + main() diff --git a/FitnessSync/backend/backfill_streams.py b/FitnessSync/backend/backfill_streams.py new file mode 100644 index 0000000..845717c --- /dev/null +++ b/FitnessSync/backend/backfill_streams.py @@ -0,0 +1,29 @@ +import sys +import os +import logging + +# Add backend to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from src.services.postgresql_manager import PostgreSQLManager +from src.services.sync_app import SyncApp +from src.services.garmin.client import GarminClient +from src.utils.config import config + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def main(): + logger.info("Starting stream backfill...") + db_manager = PostgreSQLManager(config.DATABASE_URL) + + with db_manager.get_db_session() as session: + # Client not needed for local reparse but SyncApp requires it + client = GarminClient() + app = SyncApp(session, client) + + result = app.activity_sync.reparse_local_files() + logger.info(f"Backfill complete: {result}") + +if __name__ == "__main__": + main() diff --git a/FitnessSync/backend/requirements.txt b/FitnessSync/backend/requirements.txt index e4d45b5..fa119d8 100644 --- a/FitnessSync/backend/requirements.txt +++ b/FitnessSync/backend/requirements.txt @@ -16,3 +16,4 @@ pytest==7.4.3 pytest-asyncio==0.21.1 alembic==1.13.1 fitdecode>=0.10.0 +geoalchemy2>=0.14.0 diff --git a/FitnessSync/backend/src/api/__pycache__/activities.cpython-311.pyc b/FitnessSync/backend/src/api/__pycache__/activities.cpython-311.pyc index ca80274fb94b89ae2a8826f12217147c26628501..4f41bca603cea417de59ba12b5ffa3177affd78b 100644 GIT binary patch delta 4504 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 delta 1627 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 delta 8480 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 delta 2414 zcmZ`&e{2)?75{$y`<&Q`V>_`q8#}RqT;jxW3M3dpk`hQ^ZM6`f3y?*poCD5+9i6X) zX6HGb*jf-$t$m}(1-5z!fm>4brdE?zbyBoVj6e3rnO(J$rA=u6@yAq0tF-QqY5U$C z2E(+kzy92N@AKaKzWd&LpRYWgv){GZtN^dE(~0C-_?PytELOdudd zptlJMeL{4>Vf2vL<;vu{uYFfEka$;A#KNae>kWrx-2lKl?swYJ7vvr@oc(Loz#Guk zj&_u@wIIK5F$Ys0yJ`2Gv}zY0PyB#zE~#e%razjcXR;U z!+&jMExe^@)MWG>pFPLu&w&?Q2z-!w5sKT@cC0mN0a$bM1UtYJjKNzs0k->jeX+St zWvz_yGRQXR0N-?_owq)J&ee8of2rw;`+o@cyY6?}7j%s2vV|k-$?#hecOZTj_!GZ3 z*PH}d8{f?SkZ*3RC2PmJ`B`m@WfjL~Th0N7xCOJVeCq_5^=2KsgST7+tdqC!j%VJy zop1S(u0gl)jsrOU+BU|j`uFn|#;Wj(T2(IwWZRgg%Ybj20C%v%J+;k{1#gj=r#*VRh)$9bV+>x!*(SUZ-Hy z#H}V%3u#KvzKxHmn%Tti5i0Z5L+M*M}V=qQfM+XFSdLdoHNp;HWx;{8xX;ga^$Y#3{L;PcBWhc&gB z$N$0Wu~HFbtXj_}XqKB}X)aMUE~i(}SKUGM$H;Mb4F&pMi%Uv1VGL_Pj7({}OqXZ@ zZZN{QKz}w-I{TDRvsX<^G`EyGhkoA2!gb{6-#7B5k|e4ni87+Lr;h(dUmpLp=?l}Q z<8y1-DnOz0GlO7wV0hvXWz>AgPI=IJ|F>Hy$YyX3N~hi!dqqh-_-7?GrKOHs6xIEJ zF4JP)(bCi>XFfXf;q1n2X?F93Mqxy6L`UK4=%>+D=tYi!@KCjRA-R;8n@^{>M2cfQ zI9ZiYP25|{3HEzfXRj)tb{3IHy?G&luM7Ql;27*cw%G3Bs)eOj<`|lz*_;}k;n)Oy zVUAm6f2`={WQs}7Co=2}B~4l}8J2KkqMyZlt+y1LnqN67(}Bi2T&NZ^h1sCu*pj%k zoW>uBPSHzmWs(`TjIHk}o^CuRC^#54AA&~T7C3~yH8_$RBKGDN*3(-63#FYKQK{#M zD%W&-ZF}W*-+DT){|h-FIHlP5=1adH5f9CXCuS<-X^A|Yx6~|P`_2!}UO!uklyw#V ze#yT-Z++wsN&f!4RVF)cj$$7ZaBZ&vyJMu-Q<}Qnr;2j0rxd$wlXAh``J*+H**#nX zPoWKT^%Om&9wAxj9Fsc7^2g-BuEM2~MW8By15)5X{*{{P#YS&z`j5Rryh0t6sDqky zOC5`^Rm_`lL8L!#mYv&bAlK?xfwC`D9KU%ie^@5Fii0=zmmKep<|nY5?-UftlX&MR zWPfK3=-kS1DOi9q=_^_aBZU!}iV2+?q;RslTR1IIV};4$`1{Ac!Qq}@aaio=#ar$k z5Ij2gagmdz-^6WNiCmN-7xR;aS1T?GUEcesV{{^TB&fR|v`%&z z@Avdi`i+0^8z;Na_lJ1XUEC1thv?r!|AJ-o&tX4&@Tq&>b-2@qUk|d^id-LZ&> diff --git a/FitnessSync/backend/src/api/__pycache__/analysis.cpython-313.pyc b/FitnessSync/backend/src/api/__pycache__/analysis.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8836a0e859aa8a23b5aec517a187314a3aa4404 GIT binary patch literal 13336 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 literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/api/__pycache__/bike_setups.cpython-313.pyc b/FitnessSync/backend/src/api/__pycache__/bike_setups.cpython-313.pyc index 50d2df2ec8e818d533ff0efc921b54fa82abbd43..8b785873f0021b29047c78ab899647ee1d8d673e 100644 GIT binary patch literal 8804 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 delta 2564 zcmaJ?O>7%Q6rNeH*Z+3h*f{>TQ#XyF34tak2^88ig)}Zn>P?_j2Fr~%W?Scn*^Mf= zq+Ec4L{lb66;dTQ!UaS*fsi;NAt9jrX{DkbxKu*ORe}&FcyH_mHwvt@Z{ED$@4b0D z`|J3|LI0-D=O*yH`TcYHr~b?SE=n%%pME4mRH7;cAM>dW)miXQ23P=O4%JoYm<+O@ z;N68ilbx(n@SZ|wGR(pmQu6Wl0->|rq!byV7?}{(rFvt8MY~8hQGGF@`b*9sTT2%c z(f~+1+N5zI4T5w}n>10P5TR2I-A~oLd|iqkZAS+=TIlxf@XKReEvRja znu985F%FG#w=GNW;|FXNH?^GAim93UBEM?;LkWNf>)==94I1Hnc7smv_w2)A8T(hV ztIB>@RR?p`?0F}@R~dB4OO-|KeUf+cx0I9PDRjq?vBKrDVOC6izFaX)rk`0gHKSZ+ zY0z!LOV~X~qIXCb7z35B68@Yw=KfJmv0kC%>y9X002H14bH~wsMIdo(Bk|UxyweJd z>E|?6GgnvUR>E#74o;^5D0#YLw@ftD0pOvF2L`4r)TMsdz_jen)su z=Ym_MBl40oM~fbQ*%#vdArD_qgs~?6VWNv42=&$E9djVYAnz3A?52Vet5r*xUtV0) zP0PWw1*RF#=WXl=N06Pi7F?fw*K%oCl;tO1wxHz%#%yn=2ColI!25B!Ji|mtb(rbF; zDz!<)Fh7}yDH$>+6(i8jPTq+24oreSDl*Y{pqs(yceOCsRJX%;8jQ+lI0r;baa$Mpx#Z)}!;h1%Q!hW1st-N` zMli^tOymum+&e{fz|Wvo(^z@2V$MIWFV)zypw;<}R8A3P9pH}iDPBz{JH?0QS%goZ zZuFn27#j2QjdXS`01lRS)zm>e-@I))P~?KMKe3F##l2!F_2wo5%I2R2FC=kASPEN) zKN7QS;3Ga#OI}$vOsivJc6O%x;J^drqs3!KW{-_CoaN>(0cXjwt+-ZAaW3pBekc-AOvHABB>4xKo}5h6>-rXfWy{;n zyFq?@i@G!%chM;Lk$VIhLaCxvndY^Unw^TEFbOP!UZknXhQeAm_>uzZvj%NMmR+@a-2LjtbR!+*<~GM>dhW1t?B diff --git a/FitnessSync/backend/src/api/__pycache__/discovery.cpython-311.pyc b/FitnessSync/backend/src/api/__pycache__/discovery.cpython-311.pyc index 36547b62dd7a5dd29b9060d72bb5f56f39f69b3d..acdcb6a513153952d6a23076ca5517c715e002fb 100644 GIT binary patch delta 546 zcmew@y+oF8IWI340}#Ahk(RlUb0gmk4#t4VwJa)=xjB{VYZ$VaL6RT7IIAOdRRn{!9+du+9 z32wLqYZecTy@qM>0!~F~X&4`ekDt}-G(hoTJKd{0C X!7?A28JH}tFq(g0LJ=-92D$|RVu*6W delta 357 zcmZ1?`&*iCIWI340}!NCBxXM6*vNN-gHdjBEsM%zZcgRNFIf2mYMIj+YFTQST^M3* zYFTSoCU0O8vn$~Q>4AV6)+|;eHX9O~9f{2WW3OSHe2Z04S{TM>V5ng!5k>H_xF_>* ziEA>YFsxx-#=^j`8i*kvm_d`puSfyNDUt>esz5@MXYzC|Io?~`iOD6IWtkWPNaqIDML@+?lkYj-VKm;n z+r^1du@xv#Gy_Q75-CnCD$7hxE!NA*Pm0e?%u7s9Eh=i9EaxUEHxr~luq3fKyI3zJ zH7zqQvm`S=uehiMqdRB)#51zcV$sU@FiUjkCMozPz8A_r7^=ZVo@Y zf~)WA^%V%#*`5>Or=1Jc8@O`alKmTB5pnvQ5tq+}5q-~@=q{mVqhyzLqny++Biw_C zM=Y$ce4We%J83&-=TPhz! zj_&jYVZeeP(zmT+I7N5bx8d2eY=5K2*V0?v{kr5doZ-Ovn!|EnEPi@$G8!C=2ja4W z&l|`@)PK65_A9|a)PE*6vQi1oOi3=m9*K2r5jjr6L8VOreH)7i;-3W|qyzaPd|w^| z96=aeL2)1_Z)?Grez$dCj&`(KlX!|dv!)bwj|ZOMTV5DVVTqIYMGguR61L+c916)q z90>%^hDdB8Dv9h=F*YhLr!9s8<4WF8NWNG|F-LL%wj@aaM>$~>5SALLLlU5YiR&56 zFu1{>9>Bqq1d~$?8UPfQnkb5dUDV2RfC-ixxdj&J0i+Cq@Clw2`dUUO?d_*})ZXJ+ zVMrB*GQv=C=jQ%elq0oUD(!66We3|66Pld895T0B|3SJit{hd$Od-*u}Ncebuut?N$p z=L93YX|LMU^=gaiJ((4TRbePqd!$&FCH;k`Se_u++#_}-z(Dx2^r(f^& z;@(+MkZS;Wqau^@;h?PSfTs6H0?`1wVb(o)LnJ22W6F-wxUdl-*J+=pVPm0v3WTsR zDKrFOHIYa3q^HU75=1s8N)rr-#$$|gbirexw>)L(ryh+@=Fq9kzhA8uapjlI)8>qR za~3zNxLM=MuUhT!)LKJ$pPl&rr6wPdkX#a68hHEzDzb=#P6oyeGdS+h?y`!wBJ l>@sQyFjsHYc=*4sG|cax+e4QQ7%FWnjsvAuPhT87_5<1-ac2Mk diff --git a/FitnessSync/backend/src/api/__pycache__/scheduling.cpython-311.pyc b/FitnessSync/backend/src/api/__pycache__/scheduling.cpython-311.pyc index e481cb363a9c856aa2dc2f10c6a18cb6881f9513..036b8d4c0f11bc2de87b59bc08bc3784fbba31b1 100644 GIT binary patch delta 780 zcmY*X&u|hTEL4ZuqmmM=)pwkp_*ueyrr^2N%k$l5-3e$ z(jGOKXiQHY@JD-T(4<%YgT-)Yn)H9b=%FzloCihh0TB=zr1)CGWT)UMD=FQAU*b}s-AuTp0Mw95@cTpA^x zYTw765u-NaGwzip!YuVvr-f{0c`0n5-b-Kto_uDLE&TC%75Z0jW1V`AGLjSL}AUUG@vbyPR z$!b&fw`9L9`}dO<0`|3peR`fRdH(2daHtK$5$(gF;{7Ooo!cY3@h@>(i8Pf+ONrQW zNxWVS_S+k%TAIfNe~2r+UZ)QNX`{{;;% B#w-8; delta 99 zcmZqoy6D2UoR^o20SIDMQZv7ZPUMqdwA-j|%gmO-7R;cjvUwGAx+J6KWFgsX#>~le tvRO=;+MD$iv>1WBctu&Ra-dE|ATIXZ+@|QtEWp6Z=*aj1OcrSY#R05$6`TM7 diff --git a/FitnessSync/backend/src/api/__pycache__/scheduling.cpython-313.pyc b/FitnessSync/backend/src/api/__pycache__/scheduling.cpython-313.pyc index 3a9470bd895f6262f14e4b1b5b8aca93d8d3f682..1af445ea4884385dfa135ed86eb6487c85060c2b 100644 GIT binary patch delta 1315 zcmZWoO-vg{6yDjj9c&k~fE_z)6R&?7XF*_E1zFG#L{-uP2~C)gv|$rxZLf^M*zT;2 z1X5Lc%b|xvqe@j(MY;DB`I6q+Lywh=s6@4@)KgXU<_fA_``!|0OGnyo-t2zg``)~n z{bB02>A+!cuTOySAZ=ETLQew`^6W)MCegSXUebX{kq#=t1TtVSRSz#u^N;+4lR@39 z$4)r9loa&n1UY71AvC0)yCTx_im>ilC#UKJsOL~k!hDy6M-lWs%NwsXm6r6 zk)TP`QsBZaQph{q!*JQ7rQ2VKcRfZlYZcebQmb02m9xf{QK=e*syW)+ZkU!v>;N;Z zM!ja4`OSLCtQL=(bBr#SBzO+*0DLY`Ld8X4fz$+5SjDC~V}*BpA>2Unoh&dorD8ymTQ4^|8>Z~-bI+<&jO?F z4Hy|vE{HZV4{^fjtEX<=|0bvnfN$X*=xNw|r zVH88Sjey139=MDq^b?IVi8)Q0!LRptE|PJtLHKFA7-^94|7O-)|7_UyH(8}zHW}Up zC#@#3>s(SUyX&C;jE^htkrBSFd`KwYS00cl{zf#h{0MC#2nquJRJK$$o9soZ z18k|$YO--$;*mM0V7nT1tI4nrtc-wlvO51VI^_EpLcFim*ZjNaMofE|erbK9?J6De x&L5&gMh+!GoFiQfc_OxQDMk984|WBZI@uX#{`TGxE|ItdL4}Sp2QS!!lLs$)QUi&JW)GT}7{kdLh-c@OV$jgTfBtX!=J)2k*&luHyBqec zRtv{|zMF~EcJt!~H_B{F^+utO0}q&SsU>{7%E&5Ygu(E?sK7vQkR%VDo#>f{fJB*==ysDvEy$xxRP ztN@MJcLb!rk$=O11uUM>3S0u(G2)h^{A#5TR65p2)nY%EfP~peHiT9<^>+mAOJ+H+ z2bnUj*G0!;6S-nRCAzir#V}|N+QQo6vYISrQrV?&I=7(7NN*sjmd|B1b$&IMR5SFU zfi8vdocUGM@-iPu`DC5B{I@#Q7cFbs+1^588yNDd1*D3W%U@f z=Fn-fo%GP>jp_OU1|aY{Btk{oSrcY9Ovh}?uG!tDxgG1=XMfu#f7hjD3B2=3bSaMJ{4NgJbfj& z!|;YWpDz~3Il#lzD}&UH`J7fD&R diff --git a/FitnessSync/backend/src/api/__pycache__/segments.cpython-311.pyc b/FitnessSync/backend/src/api/__pycache__/segments.cpython-311.pyc index f5a0c247c6ca8b8cb1b6aa15240609cf467b677f..e6a7cd6648f40223571923bc2c6d0d5dda193051 100644 GIT binary patch delta 3309 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 ZF9OH=?i24KZ}=bK^^Ud$0%!K$`xou_q@w@; diff --git a/FitnessSync/backend/src/api/__pycache__/segments.cpython-313.pyc b/FitnessSync/backend/src/api/__pycache__/segments.cpython-313.pyc index 3fb2d318e81732fd13b2f5ba7f5f1801046f5fc8..32cdd79be1f4f3b71bd2e4de6c14eb78175733d8 100644 GIT binary patch literal 18313 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 I6upoC8~U(EF8}}l delta 5026 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 diff --git a/FitnessSync/backend/src/api/__pycache__/sync.cpython-311.pyc b/FitnessSync/backend/src/api/__pycache__/sync.cpython-311.pyc index e6f5c06f62d89f0874594710e0d195cf3856babc..220b6cf2504b1ed7fc4b73e5c031186e43a47287 100644 GIT binary patch delta 1411 zcmbW1e@t6d6vumC-)pq+DoKI1jD*LANGSsf8)Rc~K*#vaKrmAnDW&hxK3ZPceUA-r zk}aafEYaX0i*Cs>XCg6b;__nRAMT&cIQ6eB_775*fH4~Xalx1+(?#)~S1kVRYx~JP z_j}HH=bn3SZ(Jw8y+>SYqUf}-=kfI^_1D8IE-%@8C~Op+BdU3$S-VyH6v7w#S|({Mxu!rP0eYt6wfYb zw_GV;t;)YxxU(WGU#jYlXmgRIpUO~JTqjx!dGbF+dv=S?)$8Z!DH!uLRh(wkV%2(# z;TQHfZ+n<;bNSNZzPlBN?};sGu?0T!edDk;VjPxBDg=YME}5T{+!RR|5>?G4V!mIs z&N&v?JY9qf0q_2o*f8UWreE7@bcQS*1$U2+vWKE}Mh<30Hgx16B}kqYpo)vHSqBx=EtIw{@eAbEv%x)%7)G z0Z!I8kwtj3-dAMp=mpe7jI(R0vn=e3`ZaMCZ9z{e$}C-i*`W7iEZJ{DUVdQJG9O&v1VLT=9a z5M2-x@MXu}Bm%dO9}yliTL!-KLEa;j|3F0Gug*4Mn+>NS)DHU*CA`ouN_W$5AuMPnB59pnT3jhEB delta 1569 zcmb`H+iw(A7{K>TXO_C{HkHk;yT#Z+pzXHyy3pE!l-{>M*KN>Nx7u-cc6K}M?o4xL zN?QW9ja(A@V8{n05H%Jrn8*WUUVK3l$@QC$-nBk8ow#o>)(|H`YrWO2Y?zu*7)) zxWht!LHdnJ0|jZUsC8R7#V2>46h2|uKEssu6B{h}d|4Fs9!v(6_GF#nXTEJG`PiX? zwUO@?4l`-Uo@<%kEkp{o`syd)0Ur=vEZ74=1BqH;;YU}WGw-7_niQ8!^{8sjOUAq= zOJ>}d%6n0tRZObN#y`{^R8J`Jj5#40a$J)p^@P<`p5sbhXG^Bz2nj?_i;!i*A#2o= zUL4xUE*@c%(iD8-Z;YgwJy=FxM&Aj9fM`Sb5Gulh@FJp!NyI2( zJ7O3SLQH|`d)770h7$XXui?D!9e?QB$mgTE7b0uXST3sG;wN+bL?53+&k8Gw%>Ah6%F&Uu{a=?ReP{Pl0LVcse0 z*{o{X&Cd%8r8*7rd6%hADVmg2Ws_zwLV}O#hRG#sy{?i3HgaHeDOHjxRb*3|p(;IN zHPjQ%cM${5AS=Ul( zT2eHV&eJofa;H=?p_-DMQBj+L8_oB~MObawQ-vw>#mby&n5ve}yEJ81VwLJNNl!DA zQN0VMPcl!pYU3~g~8Wh#nTwwgk_O34b0bVf)YeB2o&=U}~a?&fhlqaYP$x=Fwm~Ayc-=uw+|895Gyd)T}gfdwY#D7Dk|(s zn{o87SI54t_Wg;WAD*~i3ESLnyLYfmkGf9_>rA!RI@ROhT)(3WA_15C9upZ>`}YZt znVf>V{Xdudf#yFEGJH4CC2(x%8Q31~A;Xput|a6FoEqFu=Hcex7+C=E;M@YWXB}fP zd45_qOp5;*6i+zCc2Yd=6i=?*MT%>oO9*=@I5ovtXbp~n2yES>;(b{?Ig?TL(+Xx~ mufyP-4#&eHLdb2$(AwXx 2 else None for p in data['points']], - "speed": data.get('speed', []), - "cadence": data.get('cadence', []), - "respiration_rate": data.get('respiration_rate', []) + if stream_record: + # Map DB columns to API response format + return { + "time": stream_record.time_offset or [], + "heart_rate": stream_record.heart_rate or [], + "power": stream_record.power or [], + "altitude": stream_record.elevation or [], + "speed": stream_record.speed or [], + "cadence": stream_record.cadence or [], + "respiration_rate": [], # Add if needed, not in ActivityStream model currently? Oh, I didn't add it to model? + # Actually I should check if I added respiration_rate to ActivityStream model in step 61/105 migration. + # In migration: I did NOT adding respiration_rate. + # In parsers.py: I only capture 'time_offset', 'latitude', 'longitude', 'elevation', 'heart_rate', 'power', 'cadence', 'speed', 'distance', 'temperature', 'moving', 'grade_smooth'. + # The old endpoint had respiration_rate. + # If it's missing, I'll return empty or I should have added it. + # For now return empty list to avoid breaking frontend. + "distance": stream_record.distance or [], + "temperature": stream_record.temperature or [] } - else: - # Try FIT as fallback or empty - logger.warning(f"Unsupported file type for streams: {activity.file_type}") - streams = {} - # 3. Cache the result (downsampling happened in _extract_streams_from_fit, need it for TCX too?) - # _extract_streams_from_fit now has LTTB at the end. - # We should move LTTB to a common helper "process_streams" to apply to both. - - # Apply Downsampling if not already done (for TCX) - if activity.file_type == 'tcx' and streams.get("time"): - from ..utils.algorithms import lttb - target_points = 1500 - count = len(streams["time"]) - if count > target_points: - step = count / target_points - indices = [int(i * step) for i in range(target_points)] - if indices[-1] != count - 1: indices[-1] = count - 1 - sampled_streams = {k: [] for k in streams} - for idx in indices: - for k in streams: - if streams.get(k) and idx < len(streams[k]): - sampled_streams[k].append(streams[k][idx]) - else: - sampled_streams[k].append(None) - streams = sampled_streams - - # Save to DB - if streams: - activity.streams_json = streams - db.commit() - - return streams + # 2. Check DB Cache (Legacy) + if activity.streams_json: + return activity.streams_json + + if not activity.file_content: + # Just return empty if no file and no streams + return {} + + # 3. Fallback: Parse on the fly AND save to DB + # This mirrors the behavior of lazy loading but using the new robust table + try: + from ..services.sync.activity import GarminActivitySync # avoid circular imports if possible, or use parser directly + # Actually better to just use parser and save manually here or import the function. + # But the logic is already in GarminActivitySync._save_activity_streams. + # However, GarminActivitySync needs GarminClient init. + # Let's just use the parser directly and insert like in _save_activity_streams + + from ..services.parsers import parse_fit_to_streams + data = parse_fit_to_streams(activity.file_content) + if data: + # Save to DB + new_stream = ActivityStream(activity_id=activity.id, **data) + db.add(new_stream) + db.commit() + + return { + "time": data['time_offset'], + "heart_rate": data['heart_rate'], + "power": data['power'], + "altitude": data['elevation'], + "speed": data['speed'], + "cadence": data['cadence'], + "distance": data['distance'], + "temperature": data['temperature'], + "respiration_rate": [] + } + except Exception as e: + logger.error(f"Error lazy parsing streams: {e}") + + return {} # Return empty if all fails + except Exception as e: logger.error(f"Error processing streams: {e}") raise HTTPException(status_code=500, detail="Error processing activity streams") diff --git a/FitnessSync/backend/src/api/analysis.py b/FitnessSync/backend/src/api/analysis.py index 45cdc24..04b1d34 100644 --- a/FitnessSync/backend/src/api/analysis.py +++ b/FitnessSync/backend/src/api/analysis.py @@ -46,7 +46,7 @@ class EffortAnalysisData(BaseModel): class ComparisonResponse(BaseModel): efforts: List[EffortAnalysisData] - winners: Dict[str, int] # metric_key -> effort_id of winner + winners: Dict[str, Optional[int]] # metric_key -> effort_id of winner @router.post("/segments/efforts/compare", response_model=ComparisonResponse) def compare_efforts(effort_ids: List[int] = Body(...), db: Session = Depends(get_db)): @@ -162,58 +162,94 @@ def export_analysis(effort_ids: List[int] = Body(...), db: Session = Depends(get # Or simplistic: Fetch inside loop (N+1 query, but export is rare/manual action). effort = db.query(SegmentEffort).get(e_dict['effort_id']) - if effort and effort.activity and effort.activity.file_content: + if effort and effort.activity: try: act = effort.activity - raw_data = extract_activity_data(act.file_content, act.file_type or 'fit') + streams = {} - # Slice by time - # Timestamps in raw_data['timestamps'] - timestamps = raw_data.get('timestamps', []) - - start_time = effort.start_time - end_time = effort.end_time - - # Normalize start/end to match stream timestamps timezone - if timestamps and timestamps[0]: - stream_tz = timestamps[0].tzinfo + # 1. Use ActivityStream table (Preferred) + if act.streams and act.streams.time_offset: + ast = act.streams + base_time = act.start_time - # Helper to align + # Reconstruct absolute timestamps + full_timestamps = [base_time + __import__('datetime').timedelta(seconds=t) for t in ast.time_offset] + + # Alignment helper def align_tz(dt, target_tz): - if dt.tzinfo == target_tz: - return dt - if dt.tzinfo is None and target_tz is not None: - return dt.replace(tzinfo=target_tz) # Assume same ref - if dt.tzinfo is not None and target_tz is None: - return dt.replace(tzinfo=None) # Strip + if not target_tz: return dt.replace(tzinfo=None) # naive + if dt.tzinfo == target_tz: return dt + if dt.tzinfo is None: return dt.replace(tzinfo=target_tz) return dt.astimezone(target_tz) - start_time = align_tz(start_time, stream_tz) - end_time = align_tz(end_time, stream_tz) - - # Simple list comprehension to find indices - indices = [i for i, t in enumerate(timestamps) - if t and start_time <= t <= end_time] - - streams = {} - if indices: - first = indices[0] - last = indices[-1] + 1 + start_time = align_tz(effort.start_time, full_timestamps[0].tzinfo if full_timestamps else None) + end_time = align_tz(effort.end_time, full_timestamps[0].tzinfo if full_timestamps else None) + + # Find slice indices + # Since sorted, can optimize, but simple loop fine for export + indices = [i for i, t in enumerate(full_timestamps) if start_time <= t <= end_time] - # Keys to extract - keys = ['heart_rate', 'power', 'speed', 'cadence', 'temperature'] - for k in keys: - if k in raw_data: - streams[k] = raw_data[k][first:last] - - # Points/Elevation - if 'points' in raw_data: - sliced_points = raw_data['points'][first:last] - streams['latlng'] = [[p[1], p[0]] for p in sliced_points] # lat, lon - streams['elevation'] = [p[2] if len(p) > 2 else None for p in sliced_points] + if indices: + first = indices[0] + last = indices[-1] + 1 - streams['timestamps'] = [t.isoformat() if t else None for t in raw_data['timestamps'][first:last]] + # Extract slices + streams['timestamps'] = [t.isoformat() for t in full_timestamps[first:last]] + + if ast.heart_rate: streams['heart_rate'] = ast.heart_rate[first:last] + if ast.power: streams['power'] = ast.power[first:last] + if ast.speed: streams['speed'] = ast.speed[first:last] + if ast.cadence: streams['cadence'] = ast.cadence[first:last] + if ast.temperature: streams['temperature'] = ast.temperature[first:last] + if ast.elevation: streams['elevation'] = ast.elevation[first:last] + + # LatLng - reconstruct from parallel arrays + if ast.latitude and ast.longitude: + lats = ast.latitude[first:last] + lngs = ast.longitude[first:last] + # Zip only up to shortest length to avoid errors + min_len = min(len(lats), len(lngs)) + streams['latlng'] = [[lats[i], lngs[i]] for i in range(min_len)] + + # 2. Fallback to File Parsing + elif act.file_content: + raw_data = extract_activity_data(act.file_content, act.file_type or 'fit') + # Slice by time + timestamps = raw_data.get('timestamps', []) + start_time = effort.start_time + end_time = effort.end_time + + if timestamps and timestamps[0]: + stream_tz = timestamps[0].tzinfo + + def align_tz_fallback(dt, target_tz): + if dt.tzinfo == target_tz: return dt + if dt.tzinfo is None and target_tz is not None: return dt.replace(tzinfo=target_tz) + if dt.tzinfo is not None and target_tz is None: return dt.replace(tzinfo=None) + return dt.astimezone(target_tz) + + start_time = align_tz_fallback(start_time, stream_tz) + end_time = align_tz_fallback(end_time, stream_tz) + + indices = [i for i, t in enumerate(timestamps) if t and start_time <= t <= end_time] + + if indices: + first = indices[0] + last = indices[-1] + 1 + + keys = ['heart_rate', 'power', 'speed', 'cadence', 'temperature'] + for k in keys: + if k in raw_data: + streams[k] = raw_data[k][first:last] + + if 'points' in raw_data: + sliced_points = raw_data['points'][first:last] + streams['latlng'] = [[p[1], p[0]] for p in sliced_points] # lat, lon + streams['elevation'] = [p[2] if len(p) > 2 else None for p in sliced_points] + + streams['timestamps'] = [t.isoformat() if t else None for t in raw_data['timestamps'][first:last]] + e_dict['streams'] = streams except Exception as e: print(f"Error extracting streams for effort {effort.id}: {e}") diff --git a/FitnessSync/backend/src/api/discovery.py b/FitnessSync/backend/src/api/discovery.py index 7eba267..6b70c1d 100644 --- a/FitnessSync/backend/src/api/discovery.py +++ b/FitnessSync/backend/src/api/discovery.py @@ -61,7 +61,13 @@ def discover_single_activity( ): service = SegmentDiscoveryService(db) - candidates = service.analyze_single_activity(request.activity_id) + candidates = service.analyze_single_activity( + activity_id=request.activity_id, + pause_threshold=request.pause_threshold, + rdp_epsilon=request.rdp_epsilon, + turn_threshold=request.turn_threshold, + min_length=request.min_length + ) # Convert to schema results = [] diff --git a/FitnessSync/backend/src/api/metrics.py b/FitnessSync/backend/src/api/metrics.py index 80ad212..31cd8ca 100644 --- a/FitnessSync/backend/src/api/metrics.py +++ b/FitnessSync/backend/src/api/metrics.py @@ -296,34 +296,20 @@ from ..services.job_manager import job_manager from ..models.health_state import HealthSyncState from ..utils.config import config from ..services.postgresql_manager import PostgreSQLManager -from ..tasks.definitions import run_health_scan_job, run_health_sync_job, run_fitbit_sync_job +# Sync triggers are handled in sync.py to avoid route duplication and circular imports. +# Kept run_fitbit_sync_job for sync_fitbit_trigger above if needed, +# although sync.py likely handles that too. +# sync.py has /sync/fitbit/weight. metrics.py has /metrics/sync/fitbit. +# They might be distinct (metrics vs weight). +# run_fitbit_sync_job in definitions.py calls sync_fitbit_weight? +# "run_fitbit_sync_job: Background task wrapper for fitbit sync ... sync_fitbit_weight" +# So they are duplicate too. I should probably suppress this one too. +# But users might rely on it. +# I will fix the import for now. -# Removed inline run_health_scan_job and run_health_sync_job +from ..tasks.definitions import run_fitbit_sync_job - -# Definitions moved to tasks/definitions.py - - -@router.post("/metrics/sync/scan") -async def scan_health_trigger(background_tasks: BackgroundTasks): - """Trigger background scan of health gaps""" - job_id = job_manager.create_job("scan_health_metrics") - - db_manager = PostgreSQLManager(config.DATABASE_URL) - background_tasks.add_task(run_health_scan_job, job_id, db_manager.get_db_session) - return {"job_id": job_id, "status": "started"} - -@router.post("/metrics/sync/pending") -async def sync_pending_health_trigger( - background_tasks: BackgroundTasks, - limit: Optional[int] = Query(None, description="Limit number of days/metrics to sync") -): - """Trigger background sync of pending health metrics""" - job_id = job_manager.create_job("sync_pending_health_metrics") - - db_manager = PostgreSQLManager(config.DATABASE_URL) - background_tasks.add_task(run_health_sync_job, job_id, limit, db_manager.get_db_session) - return {"job_id": job_id, "status": "started"} +# Deleted duplicate scan and pending triggers. @router.get("/metrics/sync/status") async def get_health_sync_status_summary(db: Session = Depends(get_db)): diff --git a/FitnessSync/backend/src/api/scheduling.py b/FitnessSync/backend/src/api/scheduling.py index 5096421..c58b182 100644 --- a/FitnessSync/backend/src/api/scheduling.py +++ b/FitnessSync/backend/src/api/scheduling.py @@ -144,3 +144,14 @@ def run_scheduled_job(job_id: int): if scheduler.trigger_job(job_id): return {"status": "triggered", "message": f"Job {job_id} triggered successfully"} raise HTTPException(status_code=404, detail="Job not found") + +@router.post("/scheduling/jobs/reset-defaults", status_code=200) +def reset_scheduled_jobs_defaults(): + """Reset all scheduled jobs to system defaults.""" + from ..services.scheduler import scheduler + try: + scheduler.reset_defaults() + return {"status": "success", "message": "Scheduled jobs reset to defaults."} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/FitnessSync/backend/src/api/segments.py b/FitnessSync/backend/src/api/segments.py index 0d93312..a35c244 100644 --- a/FitnessSync/backend/src/api/segments.py +++ b/FitnessSync/backend/src/api/segments.py @@ -105,13 +105,25 @@ def create_segment(payload: SegmentCreate, db: Session = Depends(get_db)): if raw_type: rt = raw_type.lower() - if rt in ['running', 'trail_running', 'treadmill_running', 'hiking', 'walking', 'casual_walking']: + print(f"DEBUG SEGMENT TYPE: Raw='{raw_type}' Lower='{rt}'") + if rt in ['running', 'trail_running', 'treadmill_running', 'hiking', 'walking', 'casual_walking', 'run', 'track_running', 'street_running']: final_type = 'running' - elif rt in ['cycling', 'road_biking', 'mountain_biking', 'gravel_cycling', 'virtual_cycling', 'indoor_cycling']: + elif rt in ['cycling', 'road_biking', 'mountain_biking', 'gravel_cycling', 'virtual_cycling', 'indoor_cycling', 'cycle', 'bike', 'biking']: final_type = 'cycling' else: final_type = rt + print(f"DEBUG SEGMENT TYPE: Fallback to '{final_type}'") + # Create WKT for Geometry + wkt_coords = [] + for p in simplified_points: + if len(p) >= 2: + wkt_coords.append(f"{p[0]} {p[1]}") + + geom_wkt = None + if wkt_coords: + geom_wkt = f"SRID=4326;LINESTRING({', '.join(wkt_coords)})" + # Create Segment segment = Segment( name=payload.name, @@ -120,7 +132,8 @@ def create_segment(payload: SegmentCreate, db: Session = Depends(get_db)): elevation_gain=elev_gain, activity_type=final_type, points=json.dumps(simplified_points), - bounds=json.dumps(bounds) + bounds=json.dumps(bounds), + geom=geom_wkt ) db.add(segment) db.commit() @@ -275,6 +288,26 @@ def scan_segments(background_tasks: BackgroundTasks, force: bool = Query(True, d thread.start() return {"message": "Segment scan started", "job_id": job_id} +@router.post("/segments/{segment_id}/scan") +def scan_segment_matches(segment_id: int, db: Session = Depends(get_db)): + """ + Inverted Scan: Find all activities that match this specific segment. + Much faster than scanning all activities against all segments. + """ + from ..services.segment_matcher import SegmentMatcher + + segment = db.query(Segment).filter(Segment.id == segment_id).first() + if not segment: + raise HTTPException(status_code=404, detail="Segment not found") + + try: + matcher = SegmentMatcher(db) + matches = matcher.scan_segment(segment) + return {"message": "Scan complete", "matches_found": matches} + except Exception as e: + print(f"Error scanning segment {segment_id}: {e}") + raise HTTPException(status_code=500, detail=str(e)) + @router.post("/segments/scan/{activity_id}") def scan_activity_segments(activity_id: int, db: Session = Depends(get_db)): """Scan a specific activity for segment matches.""" diff --git a/FitnessSync/backend/src/api/sync.py b/FitnessSync/backend/src/api/sync.py index 448364b..80565ab 100644 --- a/FitnessSync/backend/src/api/sync.py +++ b/FitnessSync/backend/src/api/sync.py @@ -67,8 +67,6 @@ def get_db(): from ..services.garth_helper import load_and_verify_garth_session from ..tasks.definitions import ( run_activity_sync_task, - run_metrics_sync_task, - run_health_scan_job, run_fitbit_sync_job, run_garmin_upload_job, run_health_sync_job @@ -101,11 +99,11 @@ def sync_metrics(request: SyncMetricsRequest, background_tasks: BackgroundTasks, job_id = job_manager.create_job("Health Metrics Sync") db_manager = PostgreSQLManager(config.DATABASE_URL) - background_tasks.add_task(run_metrics_sync_task, job_id, request.days_back, db_manager.get_db_session) + background_tasks.add_task(run_health_sync_job, job_id, request.days_back, db_manager.get_db_session) return SyncResponse( status="started", - message="Health metrics sync started in background", + message="Health metrics sync (Scan+Sync) started in background", job_id=job_id ) @@ -114,14 +112,14 @@ async def scan_health_trigger( background_tasks: BackgroundTasks, days_back: int = Query(30, description="Number of days to scan back") ): - """Trigger background scan of health gaps""" - job_id = job_manager.create_job("scan_health_metrics") + """Trigger background health sync (Scan + Sync)""" + job_id = job_manager.create_job("Health Sync (Manual)") db_manager = PostgreSQLManager(config.DATABASE_URL) - background_tasks.add_task(run_health_scan_job, job_id, days_back, db_manager.get_db_session) + background_tasks.add_task(run_health_sync_job, job_id, days_back, db_manager.get_db_session) return SyncResponse( status="started", - message="Health metrics scan started in background", + message="Health metrics sync started in background", job_id=job_id ) diff --git a/FitnessSync/backend/src/jobs/__pycache__/segment_matching_job.cpython-311.pyc b/FitnessSync/backend/src/jobs/__pycache__/segment_matching_job.cpython-311.pyc index 625e5bd65cf732e72bbb3059cf8a686bd0d0a488..fe1541ae3fe5b57b217e9a367b826b6245e6cdf9 100644 GIT binary patch delta 2097 zcma)6O>EOv9DmPtKJ7S3Nn=Xe;HDLALC{b3-CDIOg-zsuV1yECQ-ac@at!S( zhm2bfJ7o9(8>^Jd*d{o19|sQ8G;KxgfkVQg!K@*1sWi47f&i&PJM3?}A+)U0Ui^OV z|Kb1lbN<-(Pb>R{$Kyt@4&FJFjL=`RmoV)GmWKW0ETw&le}7%}8!uSvoU2BuNWcQ2 zIF1DcBdtjkXwJBsh%Ub%=QbHp-h;hB$`gopl!~tvVGbSa2#f zv?+z02l8iKNOcu`b&EQsGAeP?^&*6+$g!$hWfc#U>P`=}BROonZ^rLHQh(07p{|Cy zyE-MfRp#8b8V?oHzz1I98P$6YE!a0#eGJOWc`tg?PV1b-vuKVyhGwxG-(rrcNARe$ zz$)M!0(s4u`Zk55RTgyq1=p*S{^&2&UToA3kl_v(`^!OswR}|sEpZABpAk2=X&<^9 z^Xn}!^w5#<|J4y|(gCw>YIEHi;&S!4a!-v$NcGmNv;f_-cZ)u4x;3V`acQ&@bdwFP zPNY#&==bu9KPD$6Ihmf~vy+K5pE<)T44)OJrp2_JjYgvpLU+zcnJG!kW_8CjU}8#C zsIkmxUZHs~nG{ngQHUOXBtVP^)?JwkqBJWdWl`aVB`G7Bj?-oY%Pp@Ee3wE-26*L^ zKL##LWo%vf$oO!c)gh$TEiPXe&LY_P;W_4dJ#R((rv?xGiJLGsi zB`YreOd<(64e*gBZJ*~g)(#$ZGLcH@WJ*kHzj%fsjA1sh?nHMD&rOOmax#&as= zYkY>9>^n}s0fg~al|4#ozxf98owlB<;VQAq)$@>Rx-xfp?sG?(>#A^F_qgu6Tz8p^ zR=DW==z~_ixc!s(XYu*Ns|;6R!o~fIgQd4JrC7p1>tSZf%uI!uDKRsv!H(jQaOmq+*y9>C`uYK3hmv4n+s;CB!4eIF&wyt;< zM%$aO4i?#ROLVcl+}u~@_E)(51?v8e=7OV2Is6m2YDYVF-3#^J4fQVe|0tG1z2(q& zB{W_bxgTgN9xVsD3x^7a)>b*bim>rMVBR_=@PojPE8~~POKtI6{ofD30nZ9|r^TX5 z$KSVza^PeoaI*Buvj(e58eV~TW=H;(if>o(!Y!(b&fmud|^)hyka<*l9kQi;St$1u8 zT4}I>XoVvUx|15+?O2K8(cRy6Zg=9oHV7+q1L)N0OeQ56-^5qw2_TXN0G>PFoX!a6 lQ{n;XAh50sE`BI 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 0 - - job_manager.update_job(job_id, progress=0, message=f"Starting scan of {total_activities} activities...") + job_manager.update_job(job_id, progress=0, message=f"Starting scan of {total_segments} segments...") matcher = SegmentMatcher(db) total_matches = 0 - skipped_far = 0 - skipped_up_to_date = 0 - - # APPROX 1000 miles in degrees - # 1 deg lat ~ 69 miles. 1000 miles ~ 14.5 degrees. - # Longitude varies but 14.5 is a safe upper bound (it's less distance at poles). - # Let's use 15 degrees buffer. - BUFFER_DEG = 15.0 - - for i, activity in enumerate(activities): + for i, segment in enumerate(segments): if job_manager.should_cancel(job_id): logger.info(f"Job {job_id} cancelled.") return # Calculate progress - prog = int((i / total_activities) * 100) - job_manager.update_job(job_id, progress=prog, message=f"Scanning {i+1}/{total_activities} (Matches: {total_matches}, Skipped Dist: {skipped_far}, Up-to-date: {skipped_up_to_date})") + prog = int((i / total_segments) * 100) + matches = 0 - # OPTIMIZATION: Check if already scanned - last_scan = activity.last_segment_scan_timestamp - scan_date_cutoff = None - - if not force and last_scan and max_seg_date: - # Fix potential TZ mismatch - # Ensure both are timezone-aware (UTC) - from datetime import timezone - - ls_aware = last_scan - if ls_aware.tzinfo is None: - ls_aware = ls_aware.replace(tzinfo=timezone.utc) - - msd_aware = max_seg_date - if msd_aware.tzinfo is None: - msd_aware = msd_aware.replace(tzinfo=timezone.utc) - - if ls_aware >= msd_aware: - # Activity is up to date with all segments - skipped_up_to_date += 1 - continue - else: - # Only scan against NEW segments - scan_date_cutoff = last_scan - - # Check for content first - if not activity.file_content: - continue - - # OPTIMIZATION: Check Coarse Distance - # If activity has start location, check if it's "close" to ANY segment - if has_segments and activity.start_lat is not None and activity.start_lng is not None: - is_near_any = False - a_lat = activity.start_lat - a_lng = activity.start_lng - - for b in segment_locations: - # b: [min_lat, min_lon, max_lat, max_lon] - # Expand bounds by buffer - # Check if point is inside expanded bounds - if (b[0] - BUFFER_DEG <= a_lat <= b[2] + BUFFER_DEG) and \ - (b[1] - BUFFER_DEG <= a_lng <= b[3] + BUFFER_DEG): - is_near_any = True - break - - if not is_near_any: - # Skip parsing! - skipped_far += 1 - continue - - # Extract points - cache this? - # For now, re-extract. It's CPU intensive but safe. try: - points = extract_points_from_file(activity.file_content, activity.file_type) - if points: - # Clear existing efforts ONLY if doing a full re-scan - if not scan_date_cutoff: - db.query(SegmentEffort).filter(SegmentEffort.activity_id == activity.id).delete() - - efforts = matcher.match_activity(activity, points, min_created_at=scan_date_cutoff) - total_matches += len(efforts) - - # Update scan timestamp - activity.last_segment_scan_timestamp = func.now() - - if efforts: - logger.info(f"Activity {activity.id}: {len(efforts)} matches") - + # Use scan_segment with overwrite=False unless forced + # This leverages PostGIS inverted index AND skips already matched activities + matches = matcher.scan_segment(segment, overwrite=force) + total_matches += matches + except Exception as e: - logger.error(f"Error processing activity {activity.id}: {e}") - # Continue to next + logger.error(f"Error scanning segment {segment.id} ({segment.name}): {e}") + + job_manager.update_job(job_id, progress=prog, message=f"Scanning Segment {i+1}/{total_segments}: {segment.name} ({matches} hits)") db.commit() # Final commit job_manager.complete_job(job_id, result={ "total_matches": total_matches, - "activities_scanned": total_activities, - "skipped_due_to_distance": skipped_far + "segments_scanned": total_segments }) - except Exception as e: logger.error(f"Job {job_id} failed: {e}") diff --git a/FitnessSync/backend/src/models/__init__.py b/FitnessSync/backend/src/models/__init__.py index 3176d28..c73649b 100644 --- a/FitnessSync/backend/src/models/__init__.py +++ b/FitnessSync/backend/src/models/__init__.py @@ -14,4 +14,5 @@ from .health_state import HealthSyncState from .scheduled_job import ScheduledJob from .bike_setup import BikeSetup from .segment import Segment -from .segment_effort import SegmentEffort \ No newline at end of file +from .segment_effort import SegmentEffort +from .stream import ActivityStream \ No newline at end of file diff --git a/FitnessSync/backend/src/models/__pycache__/__init__.cpython-311.pyc b/FitnessSync/backend/src/models/__pycache__/__init__.cpython-311.pyc index f006722183e75a7231f6062a3cea57553fab5b25..b5ead78023008e2db4c5d13cc73efc68bae0e5f0 100644 GIT binary patch delta 134 zcmZ3$ahHR4IWI340}v#4q-7RPY@mQ)6p6s0ES`e`amKE(Ko>lRxvm|r9`c_)(HJc NgVF_Ts7M8<2mox9Cm8?$ delta 75 zcmcc1v4DejIWI340}w26PR#V1$ScXXYohutR)Js!O~HwuZZi96%1wU2_-pcWCUY(o ZpfW}vE|%V`!7RbZeStv}h>DbeLI4vZ6GZ?3 diff --git a/FitnessSync/backend/src/models/__pycache__/__init__.cpython-313.pyc b/FitnessSync/backend/src/models/__pycache__/__init__.cpython-313.pyc index 036fe0cc30816b54a66ec6afbf54ed8dc1c4e3ab..c7e9689eb6a8c5cd62eb6e4ff8b259f4cf506ffe 100644 GIT binary patch delta 114 zcmcb|*2B*GnU|M~0SJ;i(lQGt@=7uunW(-~AebRoz+28szDQnyAze^YXyTKbqMD4i z_#Bf(;UFA|!(ohgw^4QL!A5Eq+IR$?~czRRF_ Lk3p$O1tM#`PiIm1z{bEY-H~%aT6>4r zg@Ditd|?;)!mjXzHMoOdQQTx@fi~q>pj(RKKtw!{_{Cw9o1apelWJF#0^~9Raq;QN cX9PODJsBCp89y-KraDSKLS(<-Q3SRL00nMW=Kufz delta 179 zcmbQNGF6#xIWI340}!leO3VDkw~)GlVjKyG5cw{ZZ_jc zWuCl*+nRC4<_FxQ;q-{P!tIwqJYFN4x8Nkl+v73yP|j?ml24I d4^DOv>~M5rWYqq^fSv3p`3RQzfsjMGVw;W|9uJW4d&n*VRaFUI9?P;(TdFgONm%1VhOA;ZHco~q%Hqt z>@pFnMQB~w-19F==guaXbLGFn8J{k%z2u zp2j+k;W6gEg)?NWIc`k&uO$;^{S(P51*qKcT2EWvF7}9Hw*$qFxFH0nZa21T-A=dX z+MYv$c#k@6i+b-s)wn$<#SVo`iVYK_D9_T`W7^sAp|S#zUsl$0S}3aZ*0xilj@Ro3 z@yO^OC|Nk`e^9p48RjA3CbOS@9^?~?LgK>PiR1D4{q+-nBl%)_-~1_q^yJax+(+~1 z?$$B7_k4}mqg>U<_+OL7XM~S}=o8m=8;_j!?tF)|2_NC#L#7kNvVq+0JaAk}^idI+ z4H56HhX!StIeM;hl{6U*AD$N2_CtgaegW-Id<{Wi2+j{d?#wUg4utpNOYb+}fH(aC D7xJc| delta 533 zcmX>hwMvrjGcPX}0}xE|Pt9DxH<3?*F>9i_zPkcLFhektH=ov&Zn8GJ}j^U?>tu=g{PQ31Vt;PFxr?@uxLo+-6h8 zDNN=Z!OX$zmP|z!F|0}qF&x31!5l!IC78zpm9v8IfaWp>b6GORSO-;1=3`M5w#6zg z>&^o-!453P7Gn?8#cRq4vYG*;pWTwV$N?-T05#n)#tEo~52mKbIheo5C0#&MaPl@5 zFGlgnjI1%ND;fMWT{ll>J;5}&h{KMNck)IKA59*hiMP0tGP6_Ti&INV3vP+V#}}6* zmSiT!Czg~HWhRxDq!!1=7s*Z5=M3c+1gb6q5o|zQyleAhP79{V*SVYdo4tynU~+yy zTdR32>u>QD7v?19Bxj`NR_f&!zy`LsJq LzA^wA+F)}4!dH7! diff --git a/FitnessSync/backend/src/models/__pycache__/segment.cpython-311.pyc b/FitnessSync/backend/src/models/__pycache__/segment.cpython-311.pyc index f62aee760b4ffc40f0d1b9d8481b49943410a856..964d5ee75643b9df243f0e79e0152882b2fc1fd2 100644 GIT binary patch delta 362 zcmdnN{e_2bIWI340}#}8re$)nOys-G#RlR6!RMxppWPW9QaE$Oa>b*>85x)uQdxjl zJcTQTdkxPrCI*JpKnwvXOlx>iMWQ59_<|WU`6t^kg_;zB6lyZw;&4yR&rK~Us=USJ zA&Fbu>8bgNImsERxs^smqClBj zEI^Um$q~#}OuCGdr!ar9{lLb+s(OJ%1sUDo7VI#apfN#XhRuqU3qr;hg^aHV8SgN; z!0m96+u;hgLxT$l7HLiHV@ZPvLodqSmp~3d9bkn=GQ8W diff --git a/FitnessSync/backend/src/models/__pycache__/segment.cpython-313.pyc b/FitnessSync/backend/src/models/__pycache__/segment.cpython-313.pyc index ee58add3451b692a7fb2beb0f2fc2a1cdbb53c6e..4c4748f083cdb6886ca6fbbdfe8e7369a1bf3929 100644 GIT binary patch delta 264 zcmZ3-xtWXaGcPX}0}#}8re$)nOys-F#SY>E!RHSXU!T?r=JFQzk|>f;V2EK=Vu+DY zVhH9A<_YGtWC~^u=Cfofk__fAl1dlQ6r8+=G0Lon38+((@fL@BYJP5NNm1o3E+0=n z*Wi#KPe1p}X98eJmg1t!6ivn=A)wlo41Su@lckxih;yf><|pPPXQbv<8Wo8FrEakR zMRF&HgdWHg4x8Nkl+v73yCS2>)htSaVvLTA9VuTKKr~nx0KN}KvH$=8 delta 149 zcmdnYwT_eTGcPX}0}%8(CuX)YPvpDI#RlR6!RIp*U!P_x5>MySauMAqZ#R8x16oiFY9TSCg~hNI5hIMJ52#U% z6$WlN-n>KN+i6j>at0qOS-NiIZwii8ykzy(+uG-Qe{)o^TwW1pgg;R zbPbuf^nvk?Wk_gAHc+-{vbjcD%tbliMTvmOKp+MnBL)Kig-{pJ{ zI-!T3dItOk>5j9=3BAozI-5;oJv^N;6z)WKa$>xVOLS^FIgz_1E_q8F-^ShSU+R{) zu3O@|+qj$K6>f>^xh1Z*jqB^Y*-lREZ{u$EZ=i)6gi*+z#ZG80ozmHCG#}!f67dej ze+CnfJ4=3|xeIw(2PwiYW=&;ZF-w~!MzTh_7`c{N*DPSSCqfHnq^^N1*Vy%p1rh8qAgRj zaQGcL`%)_Cg(595WUQx$2rEf>SwoUU;*zB4u&8=8CrPi0vf9!lCCT(GH(BhJJNBxA z1Qdw92ZkitaZw5DOH75Hs_*U^v~s=jB&=7|W)UJ1Q}ukVMIsgL`^8S;dq~EngsEpF zT+m%AO!$BCUWZwuljqddEjJeP7Av2$#cjlVoVRZth|4 zVI-j(eL!%`cjD5~x8Q>E;N$1ha-lHo-3!XYX#;1c=_Mf5m~P(MRH1}b{+vecPOZj_Sc8^j_x_agey!~YmHQ2dEZ7hs%0u_RdrG$ZfeAO(&*~9 zBX-d)*0w7L)dQz%)a@FzR<9Fx57_;+FzvZR{)l(F$KCF6>uG}%%FFg6`%!J6GGCo{ zxM7zYwlWRAx4dg_+uOCTD=(@q9Dda0N3E4BbYEqzI_GfrUG6?bU*n)3vlmYBaOh~r z5yo6$jE0l!DgR(+>`ZOEvR++xk|S<%#9D69;Ad+OY7dUXhm%K>PS3d8Gj2UQX90C_ z)}F8@YIAR3U6^r%8CRJ3{i}Ln!AUH*i3RK1_o=((gWA(~rFv@7NiDjmMeB*z@fVk0 zYdy`AxbaGr)$AV9N>^#?r>vioe&#gumA^tx>A00G37R;q?O(Q1X@~xLFn4JKPkNMZ za*9$P*|}y3&Epx$dVdq+EnUuo48t_QYW@F=1{kk*jt012?;H)#U+)~}p+RPeIS2o} P+FO77nt!g*`=R^`Fxeke literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/models/__pycache__/stream.cpython-313.pyc b/FitnessSync/backend/src/models/__pycache__/stream.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9a204d04805d47cdf770802f94f7bfddef7b2dd GIT binary patch literal 1705 zcmaJ>&1)M+6rYu}(n?x=SpH7ZV4O6LO0n9sPBFNlcIyvGi3wi&kRr0IrLnxtYFBx? za;nn9LZS2+BLWJ(q_^~zV-NiUdaTi*)qp9XQ1C6mZO?tPD=D-FjnK?{@AqbYv+rZh z5{VeWare7DeKkbLA6)4Vu_v6}N8t%kh$0k8K>(o;fIuM#!9oZ^1rfwT7{W6g4-_K> z2_#Pr7Ndn2#83_?VliHjL7pK~WR@u53q*;OgJJXhpMW*(dbpNKZNY30=U zI24XY7DEvA&=}v*$NDS82bkQq^;bv?!IDF;)BrormmY$R48bx3?EF1PhhSqvu<-#l z(f{WD3X=ouJl~XuO=GMjC91?00~qJ{VgN3bE}~6V5=s)aOG*mfs;09eEV*2E^aI^# zDh^Psb{fXyu(Z}Qs}4jY zs;Skes)~rgy;Igc$4;z}%LsF``r-SVnx=Bq7;e@I>{4zccylnt`m9T(b0p6r6 zKKN9$NS?*fk;dBFv#|@ja7esCoJNJRs9~Aao=6RP;5815?NbdL74Wi5tXq$;^&(}h zLd`PjcAe4+lPbFHXxtEYXsu4sw*eG*C$VtNI?&B3ldFK6RJ&$b&OY$Rg@vlvj1c9b z)W0YjS1~QfTvj#50e!cDF;Z2SMz1sc6Sk4t#}>}nwnI%PcNeTLXxXuIYr3-y7}*vNE_X85-C{R2en{J^Eu)jV z;%;=)6NioVPV4JV`l_4%pEff2RBTs|lAVz`x6qX{hYRh=)~${_>#lZ_V~0EKk6QOT z$*b@1u=LOMboIf}5!F7u24`=~Yk= ggx^X2SCT&=b0=ixgiQPuni5uo$J>7szItc;3p}y8GXMYp literal 0 HcmV?d00001 diff --git a/FitnessSync/backend/src/models/activity.py b/FitnessSync/backend/src/models/activity.py index 5f6aea0..c35ffc3 100644 --- a/FitnessSync/backend/src/models/activity.py +++ b/FitnessSync/backend/src/models/activity.py @@ -73,4 +73,7 @@ class Activity(Base): bike_setup_id = Column(Integer, ForeignKey("bike_setups.id"), nullable=True) bike_match_confidence = Column(Float, nullable=True) # 0.0 to 1.0 score of match confidence - bike_setup = relationship("BikeSetup") \ No newline at end of file + bike_setup = relationship("BikeSetup") + + # Relationship to streams + streams = relationship("ActivityStream", back_populates="activity", uselist=False) \ No newline at end of file diff --git a/FitnessSync/backend/src/models/segment.py b/FitnessSync/backend/src/models/segment.py index 4e603d1..75b8256 100644 --- a/FitnessSync/backend/src/models/segment.py +++ b/FitnessSync/backend/src/models/segment.py @@ -21,3 +21,7 @@ class Segment(Base): activity_type = Column(String, nullable=False) # 'cycling', 'running' created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # PostGIS Geometry + from geoalchemy2 import Geometry + geom = Column(Geometry('LINESTRING', srid=4326), index=True) diff --git a/FitnessSync/backend/src/models/stream.py b/FitnessSync/backend/src/models/stream.py new file mode 100644 index 0000000..4c016d4 --- /dev/null +++ b/FitnessSync/backend/src/models/stream.py @@ -0,0 +1,30 @@ +from sqlalchemy import Column, Integer, ForeignKey, Float, Boolean, ARRAY +from sqlalchemy.orm import relationship +from geoalchemy2 import Geometry +from .base import Base + +class ActivityStream(Base): + __tablename__ = "activity_streams" + + id = Column(Integer, primary_key=True, index=True) + activity_id = Column(Integer, ForeignKey("activities.id"), nullable=False, index=True) + + # Time Series Data (Arrays) + time_offset = Column(ARRAY(Integer)) # seconds from start + latitude = Column(ARRAY(Float)) + longitude = Column(ARRAY(Float)) + elevation = Column(ARRAY(Float)) + heart_rate = Column(ARRAY(Integer)) + power = Column(ARRAY(Integer)) + cadence = Column(ARRAY(Integer)) + speed = Column(ARRAY(Float)) + distance = Column(ARRAY(Float)) + temperature = Column(ARRAY(Float)) + moving = Column(ARRAY(Boolean)) + grade_smooth = Column(ARRAY(Float)) + + # Derived Spatial Data + # SRID 4326 = WGS 84 (Lat/Lon) + geom = Column(Geometry('LINESTRING', srid=4326), index=True) + + activity = relationship("Activity", back_populates="streams") diff --git a/FitnessSync/backend/src/schemas/__pycache__/discovery.cpython-311.pyc b/FitnessSync/backend/src/schemas/__pycache__/discovery.cpython-311.pyc index 4af798dd4066840f334f6069b03504ed0825c7ef..eedc71141ebe793b1cc75be21620feab4b8d9e9b 100644 GIT binary patch delta 493 zcmeAad@sbioR^o20SIobNXuNYk++VSF=(en!K7>wLTqWAOnMn!!7=T#M0u__>zpG)Z&c%oRnMKMJWaG zsRhNEIr(`|WquB~_)1EP@(`+^5|IwKxN-K zucSx>#FYUNq98(+fq{deNDjo5pZt!pgaxg+@vN&5fP)|PFcW$tt2uMb4av{4g zR*fnkX%HbXc@cXqqsn9f4s$lJMwQ9F9I}ksle0N0wHO&iKQLetGa^5NM86=UI41LP GCISE%5G-i` diff --git a/FitnessSync/backend/src/schemas/discovery.py b/FitnessSync/backend/src/schemas/discovery.py index 0cf08d7..5396c5c 100644 --- a/FitnessSync/backend/src/schemas/discovery.py +++ b/FitnessSync/backend/src/schemas/discovery.py @@ -13,6 +13,10 @@ class DiscoveryFilter(BaseModel): class SingleDiscoveryRequest(BaseModel): activity_id: int + pause_threshold: float = 10.0 + rdp_epsilon: float = 10.0 + turn_threshold: float = 60.0 + min_length: float = 100.0 class CandidateSegmentSchema(BaseModel): diff --git a/FitnessSync/backend/src/services/__pycache__/bike_matching.cpython-311.pyc b/FitnessSync/backend/src/services/__pycache__/bike_matching.cpython-311.pyc index 8114b9f85247ab828c7ae6b49471ef64717d1110..2ad13f1f06fc46b866aefe04ee070eb9f6903399 100644 GIT binary patch delta 1995 zcmb7ENo*Tc7=B}WJmY=FcI-ILU`J_oN}y2GC5sYLcS#zeL@Z5;8YeSJoyCrxCoAJ7 z2UMbhXr(71OA%5)P$IR5p!ASKfe6&9Hyp4~G$In>fP@6X61Q^Uy>Z5gX@uyL=ik5o z|KI=5Sbg<=pYwBv!wT@Z_vJ|JZq*mg-)&S2`r8(yk{d32fTEgFog>89yg5M89@Or5 z#Sk~6cOA=%BAYUef)tG(vqFs-ND^{j?4UMg%v%ANqDXX+&s5|yD>Up>%<#Ny)9nB# z^lb`&JNU^T(K4=~VB^cOLpPEz#e5sLaHr5l$|O7Y{i`hEgPuhoT5$sw-M#`un_?2} zE2fW3#bqdf?Ur#Z8&okyn-RgmA1pxj>ltw613oONe<8oU7%eGL46D*Mk~)zw4maRpg!ID;k6X_5!X} z&{CYj73JlMklX*6*X6Dd3b#0ThXwpyrg*s-U353#KZ$vF;7G}IFDjeJeuT{ z+w?905LwZScHNbD!j$J`KO|q+La=W#3gz&{s62c%+#8i+iEwR4dtG=W0mBQ1Q{l(P z=Z=l2baG0PM3otiic)-7S`ZFFMPd_D9)})!de2s5I(DhWA(iE5rmjE>?UT9k&SM^!^iM2+4`l<@xKH)|~*>Ukp=djFjS;n6OX8JOuJb%URZ<6?9Vnn2Mm_W)UB&|{@IJGGasM+^z=IaN$YCsu5fa* zdaHI@=*$Y8%iX(n_sy;wU70}t`rN&@G0Oz%`?unnfaU&e`#{z{kg*StCVXlBd!{VI zl%?mgp_ed6-}!nhLsYsJS8@!YT7OMwaP!Uk0~uiu{~mScIi}lOG|NRZTy!JuA2gJH z%e0rA5x@h%8u1w))Ko=y<3pZ~_%?nG95+%CR4zOpdYz1w(Z)Iwn~)_4dx%1h#30_m zG#VG`sc!V4aLPSHB3m@)BzX~_jD8VL(C=W&j~k(9`skkPB%+WIE)ca-1H>j`K>qp+9DuCKKZHgwz7lsIB_95i8X}tPx# delta 1565 zcmZ`(TWBLy7(OSHNhX=hWSV<=OKPm$ZDB9M*0Oe2)ClXYtFc9)UAmg>Oxj7)q@J8^ z+B#z&WJOR>IEaNR{!j$MQ{=RiAv4lB&vl-BF1>%jfMpM( zvx9yL0PJ^cCii31_PWRxjrh6~W%35!bmL!&tCrlRfm4F93hUaYW?T>r~zh<0cX&H^@pDj8l?-t>Oq zJy;Qf>Ua*|#9rFR0Xk_4ERlPfAvR4EsNusoNmEI$E2i*{P!&{LH(&#-a8sZGX+3el z-U=F~adZ~vAV2kF*Q-;KxX)Hw`1KVe)pb zvK}x3P6^sx5D)E@QM(Qp>o{y>hQxZ+EQm(rbN-Ci=vDB zJySO1a;h^YN*Ho8&Dt6Ps`5?bP;uFi%&-wIr#i_JM%dx=HlM6T21dF&C@aI=eu_*-N8lKFFM9IK9xu4J6q^?{L?dcjG5oaK zB><|RM#;ly4#tQa`*_si>r0xZTK?Ihs#Rw-#IDC$Vsf#FaQ1Q$&(3Ejide5@OEr{T z^PJ5-;!ZqSB7emulKqxcoW=Te9k0HDSC=%4uhbeEBA>)_eJDrMpfM^eU!^uTry+}1 zwQI|BmPc1fEB7mTjt@&>k9`sGo-<_+Drt(K+dRLi#piH+4={G&8yW(^4=Lf*O zfndIyyEihJk8?Y5KHuHC-*bk87sx;Tr!udxp4p#^ zS;?G@EHmbL8(d^NLUpo}8ROrjZ+2QRJINPrFh(aKT4nAfJ3z1K3z{WVYuL7s!JsFY zVBvO2RA81##c>{;WA+X5<>0gYN7VeK^>FYH_>#>SX3$k8UZ=w1^eVP@j>g$LwnZl= zi$m&q(grZ|efg!>{-@V0!xnN8iyLeT0m?1z~)n(FRQ4 kqF;mQ?Unn1-Df5o=LlKil2B<5wEm4`j>)}SHEc>3n z?>p!E++(kf+z!)E9S$2n*H53s6W4=x=^lGg2L;FMzdA)qAx{Ivj2??b2qetVl`i>H z0I-26`3pYsRHeI#`e)`u5db{Eyysy$T%dH#kOkNl$*gCT2T@7OvDF7av_$7h=CcIS zPoYOF4t)-QdOC(aXGoX#sa-$Fr zkf%5lLFvCk^e3)zxI-5+8vs;;s9Wb^KZ%l7K*n2{R;-d$@ai!`iVi(%fwwllfJ2|a zDcq@xsG{6gpjF8K-#XPJGXJYn7qmg)@zhWpG1cW*RrfX4sFo518p$UHL^}_3ntASN)Q9K*kCAxHkQX&;|V2~IZK!ahI>_W4# zOS{kle`bQ7R-I-@#etyhd8mY52pfG)H_?GzjWUB=w9sYXbxmYNu8UA;$)PY3BWUn; z4bGeZXCi-v(0s~hfJeX5s)cYJUVb}o=-CaaI3;I3JM3Ytffw(@`R|=npS%;e&U)O< zMiE^@Z{GqltcU1^kJ$jx18LWAL7WZYW?cbCzs?jXu@tVmeCaCPV+C>xItj}7M1>kX zW=PR_!W>fcbKh$X5Lt!lrv{70F66a15eMC-8W1NM$`(6qqjCzKyz1y}=#saT!| zpK?NDZO$Q&rxJ6C)YU7gtFy73d1_`prsk~4lp2$#lj>`zy!tv?V7jy#zEkRl-;^G@ zEn1~yj(S!qnIk9Ulnif{-M7sn@SEsKegPxpH`3*v<*_3Rpq)L+#p>KF)?9lGIIdWqpyDCw2`}QaW&cQeCRGQRiro$}TP?!SqOADhqn->3!; zs%_y4(}Vh8m@{QKbGTybny1Y~48eOz0UPFcLPZUnhorBO z2*cC9kZS=W`?TORK1Fp7F8MmmA0yBH*0%348D7DNM6-ry!XX(im(Y?kC6ttkzeBYf xFZk&4Iai@K=p(*{UBS;@>MtV7*5;1@mYel?xxKJkyw@xIp*fERNcbe^{|C#dZLa_T delta 1380 zcmZuxUu@e%7{80-#7*KqiIb*j(>Q4wwGFH-lt3FO?bt*S%%wQ9T8MyojgwmKoZ@&b z8xnm&1qo@T2MFyAA>KPx2%cb?ctB{Af;0)qf(IlX5KoifiHE_RQzt{($-eJ*-=E*z z_uXfIxpHuh`-^2m01xhyi-wfH!95%C{T@Kekw5$@RQ!j+koW}v8Bh%pCo+f{tq2Lo zpf)!|(<-fm5zNQYET8dV_<2s;3zcBUc^-~;0I2x@$bu>%`Qnx!N7pmEerC-SW7uW&q=@q3I} zfHmRr8fHD?CNy{`VQ)L7 zfEF`@=&m462xh1{=GiMz&%+F>eIi(bFFcPEil`A~90}1JERqwwOwkf95kAVJUSLm} zEOwqm+tFNdvZwWs$*CNgj;7H;jHWnchTIJ+RAO@wTUe7<`L31kSjCQ&U%Ow9;p*TT z%h%C|(SgHQA6&&CMG2~K5)_0F5(*C|lurm%UKQMHqyWxS#>6B`@1Q-2wZ-1bL#M*H zF45{!=ov8&XVC@mrR2k6LrM|d67x$>dvO(}BC7FIxWT*al2Aix;tAgrZD#QJ+i6l@FOy)GR&Qqb$&ABl>$b6D z*jL`PuWaiMz17^+E#&LZ4_l*{#+t*E6WD4t8oK4sHT~k{hT}78sL=mQ(qGeT%?Y@> zw;bkV`xY8Veiz!oaj)RF)rWpde!wuo&%rc)(EWjpROx8r>&+XRN3Y);x%o!>{A&B` zT6_4_yTWV7d#Qinz%BZ@k#i+*yA+bAsM`w#xk%k9((=^tr$aLod=dSanU+?`*~st0 z3Dk`(Teqz9WI@*rt7Tg|C?yr4g3e0i*e>y0bd8rtEv(DvK$@lB!!BviF{fX33s=&6oU5fG(lSn1n;9@;WT{>!(Wf(!dGz8 z^>~DsE|3ZT5r9J(rtQ|&nkPC^tq$ikG(9#9ucPI$k4E2uF!9;$JwW8qct?Ko_78nO QEhvAx%6oMRQR5^30aSq#FaQ7m diff --git a/FitnessSync/backend/src/services/__pycache__/discovery.cpython-311.pyc b/FitnessSync/backend/src/services/__pycache__/discovery.cpython-311.pyc index bb83f90bd3dc01c62be303f5959c4545c40c1eb1..60de270419b6ff6b4491a87e773fb6fc8eded904 100644 GIT binary patch delta 2064 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 uIKv=(N_)KZa1?9Q&{x}-+1|AQdprv3*a{TLwt delta 3560 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 B9C`o% diff --git a/FitnessSync/backend/src/services/__pycache__/parsers.cpython-311.pyc b/FitnessSync/backend/src/services/__pycache__/parsers.cpython-311.pyc index 8a90324e80748ea4abb8e1b44e68ff9bef97c4a1..384750f231d16832b1a57b1bf48c5a3a9662cd0f 100644 GIT binary patch delta 3562 zcmaKuZA=@<8OL|luNc3?#>T^ojlqEJkN^P#36Ky%ULYYr0?pesxQ^F=gTZvX2?w%k zQl$!sA~or%cUE(0-Ba6;MwQ%Bsi^&Ms?-m;=@+wUEXpsdKR_p2u8GSVg< z6H=!5w)Bdb8v=IjF;H_+V3TREQbv?wEw;)TFiN>}t10k_7W{p6W>1thci_%h=_CrU z=%8p$1Vi)a5C=-iK_^#PQZDbsGCI22T{5Wb&|q7F%a`MFin?R^n40{S9|Jy7M*%}e z=}9Uo;eIY}=#a89+;jW5PNIlMMEV;*QO|JMI@+kHEFwEjJ)p08?wRhIexh(#Xms+f z={g>A>tH0)G_fAgsPnFgn^V*~qw)wHmPX`=j>x09p!x`nYFYhu4}6JWk3|*tS1aTO zY+CNAO>3AXH>g-PO#nuATNzb~vCLe9QYVcTZC9Qt_qbHptr!<|r07}q;#gp{zmZ(N-QBQ(AT`TIE z0$nHS+5&wKO|x<#w!?o{i5_(*_3ybD8t*T1zo zZn#>eG4E>mrNsnju?Ni8EqMZO zLO1c4S@k6HGJa2ZCKO_)hLD45#vc2zhwtq^f-6#7N^tq|I>t}I$5ezmPF-z`g1dBr zyH#m4Q<1zE4w-Qi5EnGB#YY2bl)B%!H~}z3?FIScKa0xaDt*@)aDYE zsdMEhU^xf2m;f2O^GVOAJ;?*Sy-l#UrEDW>{?9^x4n6nsM@EDrBdLxzr)?x-cO=}% zjN>6Efs0@-Mi$sm1F7sI$XoD6qTyAKy|e6ShKRESA%Z!A?-L-5Me7NKLdZs@NAiIr z6V9UT33l2RO{3=rjvscnwzPK*4h)?b9XmfTbjrC%@3VakT=?6A^Tnk@XGy#vf?5aRTI26f94mHTttXshpZJlrEjdPD{f%X^YJ8N~61fC9dW*b%LfYN9`{< zyrO@lFBf9^+Qd0t?-2Bk9JNn#c%?OCvE`@+{o&UH4^Cvvm4eyvyF)@lU)tQqoBN*2 zzEGtujHmm@`Tp?@OU*AlKl7}7z*`OpmP2tmQ$h#7}RW9ZuP&Qe)$o zziwTjSFSwOtvgPp9VdB5tY2{S^Y#J3K9IsEGZk0nr&V6BNuwop->s@R{bPqiG+)Hqkp@-?0DzIUw3hV{ypbgZ%^ z=}R6-9$D)n)thnMu@$gBE!Qj0d}Qe7m1}5plcmA zr(tu_xN8ut0Grlfdm6SUSsr!^usa32|C752aJJ?oID^GB7V(-S^j8EsTD}z{&6}H> z`MTIkf?pHyT@mq`yft4D>}c<^#At)`t0(L7`G!!Pg(?`ZLCDd0A+{C}TPNBjszJHrrNuf3Brm%Fw!Z+fK{=;!4s)MZ z9FYA84=AjDQ{hugp+#)JZ+pf&KG~R2B}P_?@OK>DA*PH)({pV82J0oRZ+F?O%r_MX zGFb`!tY#(t4{;ang}okRR$traq~JEkSJbIk--jrHyXgV<&{?Cb*!sW>bANF*frs2b zoPFRD*W@w{d_+Q92&xFk{{?XyMr0PqNRYsIN^N~g$v6FLW%o>oxe@dqLCI{Az1;r*y-K_x delta 385 zcmX?E`ag_sIWI340}${fre#WMPUMqdT(VJJk8yG-qs-(?MxM!Y85LN0T3DhaCg(9K zbMpdOk|~VA44QnKKQPYWV@%)ND^SZMZU!{r7E5|jW=WAbn3kl zuoRmEP`Jo`bBwSL$7FqT$H{lK3&^f|8LF{f6P6lG1mXQam1x0%(rfQ2z$q2;79~d_;v`Ar`Y-TkX0Ic0&&Hw-a diff --git a/FitnessSync/backend/src/services/__pycache__/parsers.cpython-313.pyc b/FitnessSync/backend/src/services/__pycache__/parsers.cpython-313.pyc index 28af5fab97644c132069ce79cef19d3cb5319625..b8621831ea8c1a77026ed7a95e4ac5207b2e2c3b 100644 GIT binary patch delta 4729 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 QQNdmBWxqlc)h@30U+URF761SM delta 1595 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 qcye}@7X&<}x delta 1854 zcma)7PfXiZ7`O9}#7XQJ#{nmhz}g8IbQPtmx~WhiiYmcnKtXhr2!?Yd~xYLUM2x8{RB zGcyu*k}zPWv3>kGbVJ`^znULV`mFo!{_%a?3rd9h%qdte-MsV=pj?fhN?XRU5fbx z(On0Z<{@9G47N!p|H-m!82u25kkU#a15Od9W}BvfuXVW3F94aNCp70i)YBwyoK_f3 zz;F$P^8W)gL_l6AJLf&lI!;4D3m)KO@(1{&{t2uL1&TU9C{ScI3y3fXMe}4rkcIp# zz6q@img1ck?*gz);jHlkP8fn^rj<)W7otJlB#@Pa;gpAm z6Dh;4465iaPH|3=c|>xc(pFW4(t-CKf7-FG2cDz@(RoLn6ZR;(=Ng?CBDzq!N74Rp zzD!#5{3_ID%JsUIAKZFtcTVWhg`W8vBzLw|KifL*%3C?vXk$)1r;F#ZV#lWP-OX=rGTXDc3my7}j(G;X zDt+Vp?|f)wYW2OP^u2VBuhIFMJ*vrb<^>V+@jMq;X8dT;bK`XmJf(a&Rx_g zv^DQ?fyKtpr|(R!@(+48LTdx-1DVFP!5yg~CpGK}!NsnnrU%062WzqQ*p5(}#hrC! zCco<0j6d$rpr3@sve=s&a)y35y#86HZ!^9rKW%$5kc(a1x)?MZ^l2nW@5XEJDFQq< zl;QelB&8;RVH;6EJuxtSa%wu!4!{HeZl z5xN&WmlCwP6%TLFqM1Q*4LiPD!!D113S{&u(%jNdJ1h1mA~Q4A`&rO>XRqJLekkkL zhb;9NsoKzF#V-v*1lN_CNG1~ikpCc%B!c{g0W#D7xG#-ODATD#3z$Tp@{7yeHk#R^ Oj+ZY?`^yu&Cg{If(4zDJ diff --git a/FitnessSync/backend/src/services/__pycache__/scheduler.cpython-311.pyc b/FitnessSync/backend/src/services/__pycache__/scheduler.cpython-311.pyc index bbb986b70f660ffd82f8acb0e740f1604cfe11f4..a387bbb92e78204e4f8dfb6cffab19e4d582bac8 100644 GIT binary patch delta 4812 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 QKkp0{oWTn=@-deBAMi=&6aWAK delta 4012 zcmd5<`%hcf9Y5zj{5CINFkrAvVjczqd9b8e(ljVMy2c}ENY_EPIL7w|F8IODy(s~a zI9>Zgw^c3lRBBT(HQEnZ)@D*Ge(8U(O`#Lcyaip!Q#5VT^1~!&ODD8ny6>^UA%tn! zR%t!g_jBIoe9rfL@5kr-_U!VA^QTUy4MF%-SW%Z2oppG9v2`I`JzAZp8LdgUN8O3q z(b`1aXkDUyv>qc4Dg2~|bznhhxQ-F}2;O2CZRF4(l5MXc*?yga8hAHqywKBo9Wozn z7NJp}jE7OsagUV@1v$eaM|DM46AEp|-@*?13a;y@CKr;CgrXBQrbV>bWGtdbwJF2J ziYJt4T%U+&v1l?fnHr;4aj%V(!(gNWfEsuT#}+HA@v~Ip4oQklwkr1X0?-pi9kQKG zO;gA0`giUZ^b@}Oq`nIZHiC?o?DHs6Rmvmi$_)kWhBF$|)t6O$w!qAA zY~^X#x6q^2LPwozvw*E)T1m=ka-1~6sPq}(Bo}ni4}{b7VrMHyBw8&_n7mM>oujXc zJc^;(4=)UEt_PsiXU}k*>cl*T?A)^w-_#o z^O*X~HS`Nd6@9_o=8$bb&i-WN;@im$Jx0#4GdUG)Bj)_~#L9qJ`B#xx1rV$J>JbYh zdv+tec5|l@3Lw&Y$;9?-GhB1hUQab&HE&|B)iZ)**Co?sQ_@n5XYdTV$d8~IEW6nA zsPeoPwL6ZHa zQZovnpOu+$6TMpIqbshKnljP~jf}E!MUTj15lzuFHI<}i%YEsRFn|(hsiYyw7ZTH& z!AE7;FvU`dgsL0X{);hXT8A!%s1TAOK{IJ*1DZERDy)heXGIp}q6$=!=d>#Teua>vGz-hc|*o?AnQ4B$8+qq=h!dXRz1fup5d%#_>O1f zwr3>cc`568iFNVik*%^hC)K}u;_VZw{=QFT0QhCnQh!$JUzPfE-uAVo;96tTS}>IJ z?OvL@arjeiwf%6$cO>gOvgQxu?IvH%eFX4;!NOp^3{^L<@vfOOQd?GPTb0_@B+t7; zZx3apKvoK@N`bryHSA>s_ACf%J8@3`H>7vq++O6o>+0pD%WMATCnlTT&Njr@cK!>t zUq0FNf5dh>*)B$Q|NlZZz0DuKMAL31yT1M>S3@2#C*F#=3c4m8NDp!aKi_cojp%;S z`5zRcN9&#-x4ypq1g#29dIzy~7(T=Iak!0NI$=A}%H3*V>07O~aIJW2Z)4acu9S&T zvf>h)<{tL|AJ3{Lkyf2hgrFh&hsc;@tbi);alS*tV5>3j5 zqYbEoMoz-C}S^|kCBf+A2}rdZ^82H#(S5917x8)TR-7&>!6M z9u#VgpyF51+T)^BV{QD{ovW&UC$!l0PS@+l^JR3wFP96>J$VGMz~?QfvYO(i*3nLA zn@5{)pQRp@vJWeEN)VPNmq1p zAsH*qq(EZ>>yK;yIYfWm++_L$>a|JwdGj}01gk6OsCp~>!|-a|@f(xbu3m`YmvQuE z9et~gKG0C7i=Ji{IG`3Xc38aJmHQ0rX<)-%MFm}>-M{_#Fnih<7l%?1|7&*vbguyuggNJ`~ zLl{e?;zWb|&k$k93Hpn-4uRd~z&}0<-5w$mR2Wup zop5S=9KIwS@o>E2g|K$m7~P+8OT`${%d4Jp(@3xaw%gIJ{Yh_llK0 H1w;H3(cu*L diff --git a/FitnessSync/backend/src/services/__pycache__/scheduler.cpython-313.pyc b/FitnessSync/backend/src/services/__pycache__/scheduler.cpython-313.pyc index 675dca3be9ff951911f8cfe4fbfa491ea8b0716c..4435d33606e220db8b9b6068c5dcc052a486410b 100644 GIT binary patch delta 1533 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 delta 4342 zcmai1drVu`89(>h_xk<9HU`^)Ye-2=3uy^pFi9yiyh0#pAWd){)ZtzegCEJU69}=kHS2C-gtn>C^xxe>@QMcAVFjP@kRO;+duZ4|pb|3afo;iI;^m zJ`K8h7C@*X7`41Hi3EMyjTW5)}hNEEbGL z+>|Jf3zKm)LH&f9K@Qrku<~+&;TiOIdJ`I=i+fl>EhtO#I3Pgt%BSSm8P3VL3LuF&O_p5S`af^A0;*A>+34)`+BpCT3 z{IM8AQHX&@n(@e@+l<6o4GL_J1^1h!P!yvNW!-u+cFawS@|X|`Kr^z)4^bv`Lf%XN zg+b@rYuQmS0OB%qvcuk#lqaaMwSXp>1jEy(0hIBwBokqAO^$0c&)^;~Cd&tGR*pVW zSSdAnw5$}pa*y@FxU^!j-Rp3-LXS&IzC(^dAFw6rB%6@I$^<*h67=jM%t0#9*UEB+ z#eGWj22V#@oAPx4#teC$$f0jlrKVftlj;PRHKLpS)~)h{x^Th5|ERzQ zJ*GU?9rT!po;KCkntW1I;J*snQk2KS{;4m>3S&S4Y?qnXB~v9ds7-Bd&?dC-;e_@u zo|>dCsV~6OY{1hdbQo2a(Br7tC=0Gu)JtYmc6a@Ub0JP6@`#?Kv&LkpBd?r?-~t z^T$VyOCYk^+S(~wPDE0{b?({Uzmr3c8}4PMXfM}~J}^{FPi?xbH4>1(b5pi;V**il ziy@j?Zoq#U>mrI8Zwt~NT^}4KmSMmG5Vvz$l!b%{)Z@h@>ijt`hvR2X2#_2$B6IST z0M9@T;n_ei6c?bV9)W^CF7U(txG3|7LLzel;^2#PkQdd#7zvm^G9rlVF)31_EEb2N zd@L4?h)gKz=VPLE7~3*3K71^SZnS9~&_Uqk5r0?^^}~{-!?`IySB#s-*-&(J6f0%~ z2%{J(7zsp0g`dY3VD0QkG#n1b-6mL!VW6E52~ZynMuu;(RgL|_D#`4LO2SKSRukRI z{EVZMCN5_;dd}2T78=^nA5C>S3l@z%gfE51z^WSAiyWq}Sx{Ux8%SGgGS-?I z^=c9F6xBRvw7jTzN%w;8!f4u9l`&RbHP$Q{YgTlYjILt#c&2Lml5YDawoKb2Dc`ZB zwqt4S$W0k-Qhx<7nPFCysLE0~Z7zAK`Gw~5EibmrD6iR_GluI*T}nUov)EkIJIVKx zm?e40pBdtFd}K1qq?Kng%Cjlu*=x4)RbW*0Q#U0#({2JOZ`$APdcQyIYRR}-QaJPD86KY7wPGnv*>^42 zE;(_L&1p+-#?qUz^e&fGTrInQsqFr_Eeqbulc|Rfq{|Lw%BByV>sq#ztpdi{vRqT2 zE~$Q5HS1X^*_0|178IAXxFC7&P>wk~l4a5*flNstRT5Zs-F4MfzvQZ)JFswkQI#6- zrCo9xbzavMrOZQ%-JkX1 z0_U{Om(lrBI^Q)zSxQ+JBd@SewCy{(HT0rJ-NP`8*1LKD^C@6@nEAg|uP7)L`m`=Y z-=L6Z+gl}8EdCC8Khd@KiPVW0dAM*KUi1Ih_8|2Ps;}R=nIJyEVgf4(DI%fzG4&BqDM_j!I{kc&^zYNgx4KAt_ z%{A1okA#QuxV1q9?z)5QpUH=5}R|zYZcPoq;`s z+eL_^SI+z6ez+I?wYi)cM{4gDY5>)FE2!OQzqg5spy$0#DvI9o23YbggiXlSQbFyX z_qJ5CRVva#mc|yvPxeh4L?H9wpFX7!40^M{?JxMe+Rb=n#r-o3l%R%+bKaFYDEf2cbT|s{i=6~0HkuD;3;oFNm z9bh(G^H22tmzpMqxfziO;Oi<5X9=n_SFaO}JcixstmBtSLl+_I(f*KBe~pQ#jR}4@ za(p-x#P<_?n>0~;j6C=9uNJb+i1-_rOt}ZL&yGIyZJ&z@BUk_Xcp+Ztf1*LMhnA!y zL-KLIOvXy;Kf#(r;`7zBE_zYAME4E=G8p2Ut2*qmin^N*!)aP1r+ZJlj;{%Q;iG!#* z6%S(hQ6ZXLbmV;`O+1Vdj<_YN@YdTcJxt)Iq_Kq%$@9W!{9CylD>@Yt9)MS|2>I6* zE5{UH4HUJEOoKL5KFFfG2RBtzq(DJl298u=M$ZjC*_vP|(<-<ua!pF4j6eWLLLwK`0@Q;_ z98X^Z%{Q5lU+N(gyD^>5^L^A<{X%6r6M34Mk4~GKPLp_HCllQ2))r^`iQ}k%MwTw1buVZwi zl2rBRyA6y1%9NzKr=;7+82PlO$JA|R%zRqgQ`&7|EZtVd+HGTOe3`Dt-tAx<1R7Ni z>=el?e5=r?Yp+DeGj1oULZse}NW)%fuB>ca<}3LWXM8eFej*Z!bL#%-cqB>(COO66 z%=Bc~C*d^l$W%BKo{R_jAg3b3u^2!=QXUUqiGPZbK}Ovgj7Kg-;#UQj@u@H!hopfF z^U3zHu_zOVZb~Ntm%>aeLWlh!=rBMB!<;D?m<-NL2I67=x#$cXigEVoFf$#dLlIoe z>WcB>K8+j7Fm}_ZdDPq_73pZwk|rKYDQOnlKZC1u)J$67 zr-IgMu{E@`@+~dDrzKDXblP}0D`{tJ<8q&qE6ItiHxLg_gqcrGzy?klIv32T4rCF+ zX&L;U;M&*FuxS#$!ZVI;O8Ra}7%OCbQcfNVPmb}K7;Z!m9N9P#oeDR`VsX%u#*<9+ zd^i}7H6D+|2SK_QcH+@LS1kXOKw`u6RZi{q zN9ai0@8=xr>2DA#%+PyG1+Q08w$%C!)e}gfK28k^#VaqToQ_85c#Kmr;rI+g2gON` z;WY5$$9qDJ$4g1;a49?iBSIs&iH4wDoOGn5r4(vm$c?B5QIdgT?g>p1EvMw9^Z>>J zxJel;?^i>KoG$@*QbEi4B?6Wh8ZRq&iGU@BRzg}rYI>C2Dq2M#3JqW}YI`P${Mg zUs4a8+JuG@v?tHm|Hybp2oZOF+|^=9;ah;(OiG)}uB8*klE=PjJ2{4_2aWrEeN&36 zVmzC0ZF63V3%Q7!*7pDA3wUezQoNN~&`Y|Qv=PN1)#*iu))ncuPV5i#`-cdHKuddJ zqEoPy;YF(iktL8e3h6zvlhR(`w+<1A*3&xDM4AU`3w#$-lwQON?MwH_Xv3G+=zNIgw+d~JQ>3I{w|PBn0XV zcs^{l`?WvYO4Md(Q&I!#8v#FBH-@}uLV;Z%FI$ncTE)7;mvn)&Hr=O4H)R7k z$;B}es1lJn+SY38kn~=JJ)KXFNygA2StS~i_$p>wwZ|sH!3#uWjEGN!3DF)Q1nVY7 z1n8^81v+|}CUSO9jF!gptrq^AH%HHOLSpJCme(R>SrD83s<&M$h#7)dfTZmar0jVaZ z8bFIy1d28IsyW$YnC1+5ZpN=phdJALI0`P-M0o1z?glIZ?Bm6lZ7_UJ8@o7}t7Gu$ z&l#qo;O)g4V!UXW9Z<%b?wsP{OqjXKX~5Hhar)0)<)o1irx=Sc;5f>miz!Yy866)F zGn_m^gMf4xol*ej#wNrxHh@g@g#If?0@Fg3|yem^jRdE1U`j!zYxv z0ZPI#y!;TGJ`A?jxe~rNzo-qIhZMA#pH^X}VSP3B)?#59_Ktq5Mkf0kmgGyXmn~j= zy)tXDCA!v1khMHx@ue)jggUFUu#V}pZaV8Au6Je)HrDlG+VCRe5}m(NH$PBU|3-~8 zd$Kmy!qEKC!rA$=i-*&;-5J}#lPbH@>=b z=6n8|{D+#TeoGbyHeI&Uv?C>jiVUM)VHSUTUV;n^~clZ z4t_9e3S=n#tsd#!S*vT|#Qcf39U1SQly}eaD=BY#+Il?E{>bKDI6Z&*TO+`KcX`5i zUtRJ|-E|#n-+t@Zvi6;RcKbp0;21j|&5SdtafTg>v9YVHb@s0M+TT08iHS@l~2>C5ixOR;Q|zQxY})ALr(;-O{D53BD)(=C1M{{GcjLC{&f>6;zb zJHFX}y+7H1SHDBd_g(M1t1o|KtYFm@pMTaXLGCWe@17n+R?ib8!JIJJ7xeRbwh|LR zdwX_ya;0kJ)XFi|)0r`KrA=M`vL?fD{~W_!(2sXp`m5!CqcOw#C%cXw>aUZ2Sf@Q% zt@vYXip;eSh#Lq`Qu;IZ!g~$RWZ)t&u1}sk_d_Waf4~PLd0hoW2 zMK;nn8jkV!)x&ZATO3b-u~HhbS7B7c$$G#&fcdfE5p6tLOM>SLKRx^mK)(Xx9d06( z*tL*@Hv{EH822W&qu5vAI6@VegDM=jrA@#=6L1i1X8DQs9=88CiZ)z&||d(jt0|{r?X}p~a)fV*nnAWyO$`@sK1B15qdfjpSo3q|Ocg zj|GW*5__esyC6h-K|bt(P+R#NYAu^mOHjm@ zrq&D8dh^t-=cIQN8uMj5Co4x9MY)3izsX$RdK>uRl;$Zb-%s_ z`{VgIDEQ;BszCIlu$N#{eZxa)_~=Mcj2OIbXw_=zkn}weQU#y;KY`!$T>R2LtmV*C z0e>#CU>_4>3}2Y?+4=+@Uc%YLLoU&9_k*Ju;IxG^ui1UaqBD~rf{wZd$7%Vbyy=^C>m%~hZ3>pUFQHB&wSg@C#!wlXT?tgZ# z;G2r4b=W4UCmu5I7DTkE3SUn1sRJ&P{xtByHHqkdVopCJDZd=HPgk4qusO4s$yEJ+irD(z9}TTYmg9du^`oKBV{$0|15>HwFt(1EE%@cd zFTBw)wfF_$SHxhQpY`@d3Ginp^W4C}g7u*aDN#3DQE$2pUDtk*CdfM5|5V*;2!9BK#)em4&1G6!*?>T-bL15k!| zG!Bs~h%H6&nQ>_(=Fh8Rjy`7rFIG>nKFTm5j1N1!oL$uCu+S*y&NDq2;sl0}LG+Em z>dN52DYKt1gJ2JYCT6BM8L)$sjYCulwtnQR5@HEM7?KYtoWZFP{Nl$X$5V+p2YF7N z<2W2Q^8AKiKu#h|4A-l8o?nK51=I;X$c6QI=EgY56;6FAFc}H)q83sZ{UzL1gFDL!S*YSO%w=4u!k9eV&2-_;Hij?O&+8z7#8zQxc@?ce_`;bn z*1%uZ;`o*N;Qu$)EDmRCTT-<2#r5a#zYylPTSkD&3PV-J4LY zDN(6Cq0GWd#lQH1xyh8dda>({fxk7mx@1K1z}$P!vwOMe z2hF#e)1Cu!u1B>EncDVLZTrduTiedohFEjeLu>7=YPR9U^tK}_&FO93*~*>Eu9Z{o zo@OgM=7xT^rbTAk+|Ys{TUNQKx^Za9b?4OW(`?y`a}Hoi<(5oE@2%=oeeZii>H6OE zw%)|cpUF|hQEB4rLu=LImCV*dsjY`rJn5}{Yf^RTOOpGxvW1cP5w^N*W%&J(_eNHF z*;6DtIGPzepBg;Rk{8wxnw0cO;ZxNweTop+zz=Mv?s@ku$A577_T{wq#ktd2-|mdB zE9HYQ4(sb;eG{y0%R{H{R*>C&EZxwyayi}5m#uDEKD}yw&&5{vBpr{nh}e?0G=KY2 z(z|3{XuWYMp=K@34-d7yrAk}(e^-^a6$et~aWak=Gk;?OD3|&f%1Mf7;%X=*SnR?LLTOD3Q&T&}1Pf z(>dRn99le=hPaLIPT+QgwH`@m?p1A1+Yf!$mprv}apB@yz669w4n5S_?>ozqJ!xkh z;J%jLejw#Mkm!13cPDgNyL(|`ej<5wsVZ%+Uy7`F-}SNfwuJ6pO=H^G`rV%7#pPXZ zn&0Y47+7cPL!I-!+w(?nu|8d~^Dp)+e=WVIJyp@3b{|jlJOW)&gR(8WF#iHu-mol9 zTlOq>txDe0u$JzG`kvR9wjKDcGugUyY9a8JGofW|2Ox}g-{DTSeCzALjx+C6rW`LM zIzKU%XB`y_v-7iAC-J84hA!(OvX08Ett@MCW$hmRuQKbb&U$??GakDPChZzBXtck3 z8j&Dd1w=SNP4Al>3+40WEJ3j^4YAZPTX8yLKJ$_J432C-{l7e|L#`@-&8g4m{3u&< zl07xRo+R0-!HkXi$VOpUsQ=f`)X4h6=bUx_=bzb;5=QG1b^%YV>Ha@A2qeg zd+X7U<*iM99`rW{Rs9C}`zj@VyH)+A^7m_%@cy7t)$fph(4vI*PY$X2%jJKoR>J$I zR#m@O{?ke&ynj}&?Q_e1*6i%F$$oBAK>Ft%ZNFCb^R3Q)h3rFx0@9rH+%EX-4q^*M zd~h27p8N&8z)~qV*4RPMUjyO85)ul6E!aV);C>PQwt#b3aK%I5b`XF;r~?=tw&jn& zyp=(zJlB#BR6M68Uk5FDSiYs4k`?s;{Xq|M(Uji~S5JJ(zT>=Y$sf#oipRnmf6N4= zd7AcRGK=0DGxJ}|IQA|{mv-G$EK8Sn-B#Q* zB=p%Ygm`2nGS;1oC+-?|vFcq6zHGwq+wm|S93Qb4BJ%6vYB&nR3)v0t|G@1Y2}IaH z)U<+>LySx+#F!vPCKYZ&2sdL!FlPRt@)0jEcbgHyC&Sb4j9|G7^*U0;D}heHM5%Il zm5|S$>gtDojALy4?F%qUDa`;V5ik=%7(_pt?V|qgz4d{f}7@WIzNID@xN0$u$ zBOkj1x_UubsS?@$^2|#}6*fvCo&i1M2YP_{2L5?D&?l$LdAvHD0!VNS_`K)$ zaL$wCoM!{)D!|jMsK;Y<{hrq9Tx<1))|Df`72zQjl$$EUw1_m*#0cOOo+1w9eNusP zz7+uFTX`X#xJevy`1=$=c}0n1O`)skYO0D-^3g%a6R5wIs;1pkC8fw~!=C{cUHmPo zP2!sMz_$FYo$mu=Iqd{2QeHEhq_PF9x)5k9oK(ZOs1nNAB*ngLjo^q$eTG@P_E?k& z7mg_5%r6qm9YFVu3xnT(L`b~adPG=r{tX^}%^5tN0|93X8};MgzJZ@lU-W4hoCkB0 zo|y{s4l3p)(~S!i;5NX);veI*3=ZcwH-9PA4<~ML=Pwk#;*UpRk>alea%%BrsjsHU z7{{|;u*m;6#-eZU8yq<8A82jsqDanotbYKGf%^xL` zM0ih>ljA<|>E0E84614Hs^HL$!Nxz>@o-4UAK?Lod_oPr3V-&-l}-j?aeP3RJJZBq zQfw!W;e%#BIJDp$1!our+$20ee%Nqm$opyl#&-xVeQ<&^V>0uk@%@;=?uL7}fIm1h zB{Ve_Bg4HJ$oAHOW1aq?uD-VZAx?50E(h#Q$PDW$kgy247H8A9L2K;j}f?eN%8T5=@FPux}CQ8&&?lBsG+_bj%u9ut)ArhkFCDzZHdQtMLSX4~>MFp=w5+^encdD52tL|fKq`Gv7MYa%kHttr#iCCSI8ZI4yR zw(YlCWFo*2ThpU5Pr~?#%a`cN8l7q5_IoDhnj694XGhlV0AtdPsp`yn>sWmSkl^qx zniosIMbAked8+2LkE_5)f4mhrYS#`UqMFs0ubBWQW#69Yc&bEhPx8#`m5J^wQN`*# z(A-;tzhJ%-iGIj>Digh**xbqeix@0=uCRXt~XWUWYEobQF4tC~NDJa(gU zP6-%2jY}7o%{SxArj$i=@j>&S|nH@8ao{sev_lKXGnb+O>4* zX4A46c3kW7l@-bDYb%`q3P$e9IRzjqt4IbGE0+(YyhqaR);Seez;7H|)V|)o6iAgf zE?2G`O6@wCcAlD3JgW6&YTHt^Z7ZkP!4dZCxpZwXWvZG}%w2nAaVB3(Tedw0v)c`0 zs&1Gwg70E=B~SdqQuoMFw{&cIYi9S+6#VTt`U}U=M?30Sr|+KA^SE7t?EBU_B?zcK z`mu3K*6C%dn^?!*k4$?XUy~vCwzXGe$YkYrejI%QSDl}TycfQ1(}#GIu<8FA60a3q z;w2^G#z`wE8Lcedz)30IsEW3Z!tyR%w;8BLQ~=07l21Z~?!v7s4z*!DW97FHRrhS4RJ21a5gS*m~zAp&0CI zMGKU$4E;H-^O5UsM?8Ovxh8=n=^N1IRrFMk%=WqPxAX}`)?S&=Jyj#{2%8sA-8gVd zw$%NO@s2lDcOdOJn9#$mf>Oua)win`&;PH+<3>{Hw!`%blZ`cc zS+)164w<(u&fGEIoVs&n)%$_$U4Pot%R_$SYS!tRQ)DYD|EKP;0hKnx${{x66@H!P zlGpD}pcThh2Y~R6&u}RzDbnwa+6;P#A8dY1H&HV1CS-m5o-Rm9%3$8{;t@TGf>1t! zDo*0x=ry?fqCORE+kSjYimTfwgXcjeu?RrRK?JZ~XN(OgW5d$SiubR{Rrg={*`eW7 z`*7NL`no(J0R|`3f*;_(wBk6j&yN4Q0KPZ`|6M@q_rs;98HhFdIlbS15iZ2#YK(sP zk0o%w2`eof_4^q-qZv1TVKrj#)MHru!off0W&FYn6XI3!bJ6G|^ES?7qmjY0mH9*b z>cB6aaUVm9v-$D98jl3w8dRK#oSTV1@^AS1wU?!n5`}i<3VZTw zs^jIS5XMnx*G6S(#qK5f5_MCzh9C)8!ta*x$yMu9T(V|;S*oxuj;|s7z0xAgg>XHqe5;}`WV-p z#h-TR%Mek*y{sKklqsr~<;&D4@Ql1VT3w$={*66smiGDt?Jd z9-yiRsO*AwTAG2PPu delta 3444 zcmZuzX>1$E6`m!z%Tpq$uZKD)Ls{ANQLT)H5jKF9EG%3)4iiNajP_%EB zhg38}a^}66cg)Ot?;CE*zZ{bRIf9d&k@;Vnih3aoXAe;;bUMy zfc``dI3uM7I8pCH&*`uU=FFuzjc8EkETXZjC7LP}H&q2%W>unEv`ksWDp=cXoqDV; z&z-EeG*8*4>{IpuO?evwCef;zsS#~u1OI1?n=*hqyDGfr>OsHm1SCp0wlA*Jt80|wyGAw}W`8QyX-W_{oDijU9QPDI@ z=87f{nryHIj?WL8%a`RrfnUNg;LL&FAY?`!kf~_I5em1L?FM^^-^{bD#00xY#3=Z< za4A64ZrNTn67~HR4y=_z$LynitzWm|CpmLN3)_mkDBN21rbkgf#KE%5zfbkAk+`D& zeywDwM*X_7XAf#ZqE5CvW*KJ!F{mb$o@S=eA#D?yW_*ULljH@9|L|T@F(F4h6UlVO zlbZH~XH#;IXEvI7#)OTqgR6B%Fiv65Tr?9qpOj}j>BP)jLe6-uYDPShPJug@T9D&u#gt4Z z<#Z-0#}c@K+_1KjUt3$A@m1kE z>YpYTZlI1$;4o4$ZYXU!P0rb#;HzNSv%6s@`Gf7i;&&m2*U*Kl2>G(E>YTIv4ZwS3hrh*MQoUa|Yap1K>Hx9jBx54}J{Qexj|GtIKTf8}o_hu;H zHkyO4W%Qv|<1qczRQ3Iqm)i;~GTI9UQ8T^JyY`+(McbaV6QYlM42bx4g}LV?d@~hwd7Jw6r<%)>im#@^zmG{?*QW-*~QX zeBGHp9?l&PZwOOMrjN#?1JP9Fv_(eC)b7P zov2N!67{`6NwtIUjL-&FMX+jiN;=7@8ZwhDU;pABK<}@4OhZ2}D@xqRs|ZCQrQsb} z0F!$pc4P!*z=-6O>tOa8P<0|V@;pN7gBbw6N~#;sq%|Hyf34~v0$_(8W=7*PU#&Gg zp27(a>Fw_4Pr?399-X1vq|L`+5BYYt<57A7g~d^MCZX(!%q8VW9MC+HNkz(uI3LZN z_qldu%`m7GQzuM5?GBg~W?s=zEj&uZo|nwba|#=eBmkLRWZv7LFD6bm$@Dhs&x3CI zB13-E`?B#Nhg@At#|us}w)fQHCxUBv_~yme53O{4Bn)0>HXV)27jFcvGxwccEB=+I zZVjy3SN(S!tC!c9J1?vqTYu{PZ*4fAyv`OJ0IGT3pX2?j-VOfn+Smpk{Ig>ZB---C z2H*Lh)0cI$ePKX?>jB@oqWKfwwOQYm?Kqfq58rbPKkP@fLtCeNHL#h&gcjKxYDOxo z`nrAnzpPitSNmKBZ3%XWZ-3(%n!C^-1w4=fx*ePe)kr#;3!zp~Lob`sgA}}=$!tg* zsVPOY5hBXU+CX#Z|AQ8QL3_2Ce7fIrX0J)n!rO&u6n#2{ot{laGdKcVvC1jbKuFbR z>=K=&F;ZAHoOqhjOgzoOSght^M4knne5b#w=WjryC!o8AwhhQ$dp$ucOYD7n?W^hK z5wf^q&((E(Wba;j?7pp?{GiY6euV|huj!$sFDzNq`@*%LKj{<5-}^@wDUxFf(wH6; zU!k3fKomEyj&?oJDY|p1)GThJJgpB*?}i>UgS#pFEM;rRlEYZhUR<-3z(l z6Dq%+$)1eno;bHn7YnDD2G+E8Is4=@x$(2x2$;g7gdJ=?cv!DHw8 z&AHKu;zTy|Y>AL@HvN3|@{76jOF4NFCP`#)Xg^gdd{bv+g_X?eky|eS_OZgcZvA97 zc>2R|?${Z1B6~LW54wQeq;Y7Hhu7fZY;-nuJ~4L%)0YbS$mOBAMMnu`QPhO43EsE;tR8hNAVAd`@jinOv?ygAlj>U`d^%? B5Z?d* diff --git a/FitnessSync/backend/src/services/bike_matching.py b/FitnessSync/backend/src/services/bike_matching.py index 8d57179..2626dce 100644 --- a/FitnessSync/backend/src/services/bike_matching.py +++ b/FitnessSync/backend/src/services/bike_matching.py @@ -76,6 +76,8 @@ def calculate_observed_ratio(speed_mps: float, cadence_rpm: float) -> float: """ if not cadence_rpm or cadence_rpm == 0: return 0.0 + if not speed_mps: + return 0.0 return (speed_mps * 60) / (cadence_rpm * WHEEL_CIRCUMFERENCE_M) def match_activity_to_bike(db: Session, activity: Activity) -> Optional[BikeSetup]: @@ -112,7 +114,23 @@ def match_activity_to_bike(db: Session, activity: Activity) -> Optional[BikeSetu observed_ratio = 0.0 # helper to check if we can use streams - if activity.file_content: + observed_ratio = 0.0 + + # helper to check if we can use streams + speeds = [] + cadences = [] + + # 1. Use DB Arrays if available (Fastest) + if activity.streams: + speeds = activity.streams.speed or [] + cadences = activity.streams.cadence or [] + + if speeds and cadences and len(speeds) > 0: + observed_ratio = calculate_ratio_from_streams(speeds, cadences) + logger.debug(f"Smart Match Ratio (DB) for {activity.id}: {observed_ratio:.2f}") + + # 2. Fallback to File Parsing if DB Arrays missing but file exists + if observed_ratio == 0 and activity.file_content: try: data = extract_activity_data(activity.file_content, activity.file_type) speeds = data.get('speed') or [] @@ -121,7 +139,7 @@ def match_activity_to_bike(db: Session, activity: Activity) -> Optional[BikeSetu # If explicit streams exist, use them if speeds and cadences and len(speeds) > 0: observed_ratio = calculate_ratio_from_streams(speeds, cadences) - logger.debug(f"Smart Match Ratio for {activity.id}: {observed_ratio:.2f}") + logger.debug(f"Smart Match Ratio (File) for {activity.id}: {observed_ratio:.2f}") except Exception as e: logger.warning(f"Failed to extract streams for Smart Matching activity {activity.id}: {e}") diff --git a/FitnessSync/backend/src/services/discovery.py b/FitnessSync/backend/src/services/discovery.py index de5cc8f..939d765 100644 --- a/FitnessSync/backend/src/services/discovery.py +++ b/FitnessSync/backend/src/services/discovery.py @@ -154,7 +154,11 @@ class SegmentDiscoveryService: return final_candidates, list(activity_paths.values()) - def analyze_single_activity(self, activity_id: int) -> List[CandidateSegment]: + def analyze_single_activity(self, activity_id: int, + pause_threshold: float = 10.0, + rdp_epsilon: float = 10.0, + turn_threshold: float = 60.0, + min_length: float = 100.0) -> List[CandidateSegment]: act = self.db.query(Activity).filter(Activity.id == activity_id).first() # Fallback to Garmin ID if not found by primary key @@ -205,7 +209,7 @@ class SegmentDiscoveryService: t2 = aligned_ts[i] diff = (t2 - t1).total_seconds() - if diff > 10.0: + if diff > pause_threshold: # Pause detected, split if i - seg_start > 5: sub_segments_indices.append([seg_start, i]) # i is exclusive? @@ -222,8 +226,8 @@ class SegmentDiscoveryService: segment_points = aligned_points[start_idx:end_idx] # Get RDP simplified INDICES (relative to segment_points start) - # Use epsilon=10.0m for robust major turn detection - rdp_indices = ramer_douglas_peucker_indices(segment_points, 10.0) + # Use epsilon from params + rdp_indices = ramer_douglas_peucker_indices(segment_points, rdp_epsilon) # Check turns at RDP vertices split_points_relative = [] @@ -245,7 +249,7 @@ class SegmentDiscoveryService: diff = abs(bearing - last_bearing) if diff > 180: diff = 360 - diff - if diff > 60: + if diff > turn_threshold: # Turn detected at vertex k-1 (idx1) # Convert relative idx1 to split point split_points_relative.append(idx1) @@ -276,7 +280,7 @@ class SegmentDiscoveryService: candidates = [] for path in final_segments: d = self._calculate_path_length(path) - if d > 100: # Min 100m + if d > min_length: # Min length parameter # Simple decimation for display simplified = self._decimate_points(path, min_dist=10.0) cand = CandidateSegment(simplified, 1, [activity_id]) diff --git a/FitnessSync/backend/src/services/parsers.py b/FitnessSync/backend/src/services/parsers.py index 4131ed9..8e59246 100644 --- a/FitnessSync/backend/src/services/parsers.py +++ b/FitnessSync/backend/src/services/parsers.py @@ -135,6 +135,80 @@ def _extract_data_from_fit(file_content: bytes) -> Dict[str, List[Any]]: logger.error(f"Error parsing FIT file: {e}") return data +def parse_fit_to_streams(file_content: bytes) -> Dict[str, Any]: + """ + Parses FIT file into ActivityStream compatible dictionary including arrays and WKT geometry. + """ + streams = { + 'time_offset': [], 'latitude': [], 'longitude': [], 'elevation': [], + 'heart_rate': [], 'power': [], 'cadence': [], 'speed': [], + 'distance': [], 'temperature': [], 'moving': [], 'grade_smooth': [] + } + + start_time = None + points_for_geom = [] + + try: + with io.BytesIO(file_content) as f: + with fitdecode.FitReader(f) as fit: + for frame in fit: + if frame.frame_type == fitdecode.FIT_FRAME_DATA and frame.name == 'record': + # Timestamp + ts = frame.get_value('timestamp') + if not start_time and ts: + start_time = ts + + t_offset = int((ts - start_time).total_seconds()) if ts and start_time else None + + # Helpers + def get_val(keys): + for k in keys: + if frame.has_field(k): return frame.get_value(k) + return None + + lat_sc = frame.get_value('position_lat') + lon_sc = frame.get_value('position_long') + lat = None + lon = None + if lat_sc is not None and lon_sc is not None: + lat = lat_sc * (180.0 / 2**31) + lon = lon_sc * (180.0 / 2**31) + points_for_geom.append(f"{lon} {lat}") + + ele = get_val(['enhanced_altitude', 'altitude']) + hr = get_val(['heart_rate']) + pwr = get_val(['power']) + cad = get_val(['cadence']) + spd = get_val(['enhanced_speed', 'speed']) + dist = get_val(['distance']) + temp = get_val(['temperature']) + + streams['time_offset'].append(t_offset) + streams['latitude'].append(lat) + streams['longitude'].append(lon) + streams['elevation'].append(ele) + streams['heart_rate'].append(hr) + streams['power'].append(pwr) + streams['cadence'].append(cad) + streams['speed'].append(spd) + streams['distance'].append(dist) + streams['temperature'].append(temp) + streams['moving'].append(None) # TODO: Logic for moving? + streams['grade_smooth'].append(None) # TODO: Logic for grade? + + except Exception as e: + logger.error(f"Error parsing FIT to streams: {e}") + return {} + + # Construct Geometry WKT + # WKT: LINESTRING(lon lat, lon lat, ...) + if points_for_geom: + streams['geom'] = f"SRID=4326;LINESTRING({', '.join(points_for_geom)})" + else: + streams['geom'] = None + + return streams + def _extract_points_from_fit(file_content: bytes) -> List[List[float]]: # Deprecated internal use, redirected return _extract_data_from_fit(file_content)['points'] diff --git a/FitnessSync/backend/src/services/power_estimator.py b/FitnessSync/backend/src/services/power_estimator.py index 248d679..4b37de4 100644 --- a/FitnessSync/backend/src/services/power_estimator.py +++ b/FitnessSync/backend/src/services/power_estimator.py @@ -42,15 +42,34 @@ class PowerEstimatorService: total_mass = rider_weight + bike_weight # 2. Extract Data - data = extracted_data - if not data: - if not activity.file_content: - return [] - data = extract_activity_data(activity.file_content, activity.file_type) + timestamps = [] + speeds = [] + elevations = [] - timestamps = data.get('timestamps') - speeds = data.get('enhanced_speed') or data.get('speed') - elevations = data.get('enhanced_altitude') or data.get('altitude') + # Try to use ActivityStreams first + if activity.streams: + # Reconstruct timestamps from start_time + time_offset + if activity.streams.time_offset: + base_time = activity.start_time + # Handle tzinfo if necessary - simply add seconds + timestamps = [base_time + __import__('datetime').timedelta(seconds=t) for t in activity.streams.time_offset] + + speeds = activity.streams.speed or [] + elevations = activity.streams.elevation or [] + + # Fallback to file parsing if streams are empty or missing + if not speeds and extracted_data: + data = extracted_data + timestamps = data.get('timestamps', []) + speeds = data.get('enhanced_speed') or data.get('speed', []) + elevations = data.get('enhanced_altitude') or data.get('altitude', []) + + if not speeds and not timestamps and activity.file_content: + # Last resort: parse file content on demand + data = extract_activity_data(activity.file_content, activity.file_type) + timestamps = data.get('timestamps', []) + speeds = data.get('enhanced_speed') or data.get('speed', []) + elevations = data.get('enhanced_altitude') or data.get('altitude', []) if not speeds or not len(speeds) > 0: return [] @@ -58,10 +77,10 @@ class PowerEstimatorService: # Generate Power Stream power_stream = [] - for i in range(len(timestamps)): - if i >= len(speeds): - break - + # Ensure we don't go out of bounds if arrays are different lengths (shouldn't happen in valid FIT) + min_len = min(len(timestamps), len(speeds)) + + for i in range(min_len): t = timestamps[i] v = speeds[i] # m/s @@ -74,12 +93,17 @@ class PowerEstimatorService: grade = 0.0 accel = 0.0 - if i > 0 and i < len(speeds) - 1: + # Use a slightly wider window for smoothing grade if using raw data + # But the logic below is a basic central difference + + if i > 0 and i < min_len - 1: # Central difference if i+1 < len(timestamps) and i-1 >= 0: d_t = (timestamps[i+1] - timestamps[i-1]).total_seconds() if d_t > 0: - d_v = (speeds[i+1] - speeds[i-1]) # acc + v_next = speeds[i+1] if speeds[i+1] is not None else v + v_prev = speeds[i-1] if speeds[i-1] is not None else v + d_v = (v_next - v_prev) # acc curr_ele = elevations[i] if elevations and i < len(elevations) and elevations[i] is not None else 0 next_ele = elevations[i+1] if elevations and i+1 < len(elevations) and elevations[i+1] is not None else curr_ele diff --git a/FitnessSync/backend/src/services/scheduler.py b/FitnessSync/backend/src/services/scheduler.py index 4e6a9d5..39b3065 100644 --- a/FitnessSync/backend/src/services/scheduler.py +++ b/FitnessSync/backend/src/services/scheduler.py @@ -9,19 +9,17 @@ from ..services.postgresql_manager import PostgreSQLManager from ..models.scheduled_job import ScheduledJob from ..services.job_manager import job_manager from ..utils.config import config +# ... imports ... from ..tasks.definitions import ( run_activity_sync_task, - run_metrics_sync_task, - run_health_scan_job, run_fitbit_sync_job, run_garmin_upload_job, run_health_sync_job, - run_garmin_upload_job, - run_health_sync_job, run_activity_backfill_job, run_reparse_local_files_job, run_estimate_power_job, - run_segment_matching_task + run_segment_matching_task, + run_bike_matching_job ) logger = logging.getLogger(__name__) @@ -35,16 +33,14 @@ class SchedulerService: # Map job_type string to (function, default_params) self.TASK_MAP = { 'activity_sync': run_activity_sync_task, - 'metrics_sync': run_metrics_sync_task, - 'health_scan': run_health_scan_job, + 'health_sync': run_health_sync_job, 'fitbit_weight_sync': run_fitbit_sync_job, 'garmin_weight_upload': run_garmin_upload_job, - 'health_sync_pending': run_health_sync_job, - 'health_sync_pending': run_health_sync_job, 'activity_backfill_full': run_activity_backfill_job, 'reparse_local_files': run_reparse_local_files_job, 'estimate_power': run_estimate_power_job, - 'segment_matching': run_segment_matching_task + 'segment_matching': run_segment_matching_task, + 'bike_matching': run_bike_matching_job } def start(self): @@ -64,78 +60,127 @@ class SchedulerService: if self._thread: self._thread.join(timeout=5) + def _create_default_jobs(self, session): + """Helper to define and create the default job set.""" + defaults = [ + # Scheduled (Active) + { + "job_type": "activity_sync", + "name": "Activity Sync (Recent)", + "interval": 60, + "params": {"days_back": 14}, + "enabled": True + }, + { + "job_type": "health_sync", + "name": "Health Sync (Recent)", + "interval": 60, + "params": {"days_back": 14}, + "enabled": True + }, + { + "job_type": "fitbit_weight_sync", + "name": "Fitbit Weight Sync", + "interval": 360, + "params": {"days_back": 30}, + "enabled": True + }, + { + "job_type": "garmin_weight_upload", + "name": "Garmin Weight Upload", + "interval": 360, + "params": {"limit": 50}, + "enabled": True + }, + { + "job_type": "bike_matching", + "name": "Match Bikes", + "interval": 60, + "params": {}, + "enabled": True + }, + + # Manual / On-Demand + { + "job_type": "activity_backfill_full", + "name": "Full Activity History Backfill (20y)", + "interval": 0, + "params": {"days_back": 7200}, + "enabled": False + }, + { + "job_type": "health_sync", + "name": "Full Health History Sync (20y)", + "interval": 0, + "params": {"days_back": 7200}, + "enabled": False + }, + { + "job_type": "fitbit_weight_sync", + "name": "Full Fitbit Weight Sync (20y)", + "interval": 0, + "params": {"days_back": 7200}, + "enabled": False + }, + { + "job_type": "segment_matching", + "name": "Run Segment Matching (All)", + "interval": 0, + "params": {}, + "enabled": False + }, + { + "job_type": "estimate_power", + "name": "Estimate Power (All)", + "interval": 0, + "params": {}, + "enabled": False + }, + { + "job_type": "reparse_local_files", + "name": "Reparse Local FIT Files", + "interval": 0, + "params": {}, + "enabled": False + } + ] + + for job_def in defaults: + existing = session.query(ScheduledJob).filter_by(name=job_def["name"], job_type=job_def["job_type"]).first() + if not existing: + logger.info(f"Creating default schedule: {job_def['name']}") + new_job = ScheduledJob( + job_type=job_def["job_type"], + name=job_def["name"], + interval_minutes=job_def["interval"], + params=json.dumps(job_def["params"]), + enabled=job_def["enabled"] + ) + session.add(new_job) + def ensure_defaults(self): - """Ensure default schedules exist.""" + """Ensure default schedules exist (additive only).""" with self.db_manager.get_db_session() as session: try: - # Default 1: Fitbit Weight Sync (30 days) every 6 hours - job_type = 'fitbit_weight_sync' - name = 'Fitbit Weight Sync (30d)' - - existing = session.query(ScheduledJob).filter_by(job_type=job_type).first() - if not existing: - logger.info(f"Creating default schedule: {name}") - new_job = ScheduledJob( - job_type=job_type, - name=name, - interval_minutes=360, # 6 hours - params=json.dumps({"days_back": 30}), - enabled=True - ) - session.add(new_job) - session.commit() - - # Default 2: Manual Job - Reparse Local FIT Files - job_type = 'reparse_local_files' - name = 'Reparse Local FIT Files' - existing = session.query(ScheduledJob).filter_by(job_type=job_type).first() - if not existing: - logger.info(f"Creating default schedule: {name}") - new_job = ScheduledJob( - job_type=job_type, - name=name, - interval_minutes=0, # Manual only - params=json.dumps({}), - enabled=False # Disabled by default, visible in UI for manual trigger - ) - session.add(new_job) - session.commit() - - # Default 3: Manual Job - Estimate Power - job_type = 'estimate_power' - name = 'Estimate Power (All)' - existing = session.query(ScheduledJob).filter_by(job_type=job_type).first() - if not existing: - logger.info(f"Creating default schedule: {name}") - new_job = ScheduledJob( - job_type=job_type, - name=name, - interval_minutes=0, # Manual only - params=json.dumps({}), - enabled=False # Disabled by default, visible in UI for manual trigger - ) - session.add(new_job) - session.commit() - - # Default 4: Manual Job - Segment Matching - job_type = 'segment_matching' - name = 'Run Segment Matching (All)' - existing = session.query(ScheduledJob).filter_by(job_type=job_type).first() - if not existing: - logger.info(f"Creating default schedule: {name}") - new_job = ScheduledJob( - job_type=job_type, - name=name, - interval_minutes=0, # Manual only - params=json.dumps({}), - enabled=False # Disabled by default - ) - session.add(new_job) - session.commit() - + self._create_default_jobs(session) + session.commit() except Exception as e: logger.error(f"Error checking default schedules: {e}") + def reset_defaults(self): + """Wipe all scheduled jobs and recreate defaults.""" + with self.db_manager.get_db_session() as session: + try: + logger.warning("Resetting all scheduled jobs to defaults...") + session.query(ScheduledJob).delete() + self._create_default_jobs(session) + session.commit() + return True + except Exception as e: + logger.error(f"Error resetting defaults: {e}") + session.rollback() + raise e + def _run_loop(self): logger.info("Scheduler loop started.") while not self._stop_event.is_set(): @@ -154,10 +199,6 @@ class SchedulerService: now = datetime.now() # Find due jobs - # due if enabled AND (next_run <= now OR next_run is NULL) - # also, if last_run is NULL, we might want to run immediately or schedule for later? - # Let's run immediately if next_run is NULL (freshly created). - jobs = session.query(ScheduledJob).filter( ScheduledJob.enabled == True, or_( @@ -167,9 +208,6 @@ class SchedulerService: ).all() for job in jobs: - # Double check locking? For now, simple single-instance app is assumed. - # If we had multiple workers, we'd need 'FOR UPDATE SKIP LOCKED' or similar. - self._execute_job(session, job) session.commit() @@ -180,8 +218,6 @@ class SchedulerService: task_func = self.TASK_MAP.get(job_record.job_type) if not task_func: logger.error(f"Unknown job type: {job_record.job_type}") - # Disable to prevent spam loop? - # job_record.enabled = False return # Parse params @@ -195,11 +231,6 @@ class SchedulerService: # Create Job via Manager job_id = job_manager.create_job(f"{job_record.name} (Scheduled)") - # Launch task in thread (don't block scheduler loop) - # We pass self.db_manager.get_db_session factory - # Note: We must duplicate the factory access b/c `run_*` definitions use `with factory() as db:` - # passing self.db_manager.get_db_session is correct. - t = threading.Thread( target=task_func, kwargs={ @@ -213,7 +244,7 @@ class SchedulerService: # Update next_run job_record.last_run = datetime.now() job_record.next_run = datetime.now() + timedelta(minutes=job_record.interval_minutes) - # session commit happens in caller loop + def trigger_job(self, job_id: int) -> bool: """Manually trigger a scheduled job immediately.""" diff --git a/FitnessSync/backend/src/services/segment_matcher.py b/FitnessSync/backend/src/services/segment_matcher.py index ccbcd00..441c365 100644 --- a/FitnessSync/backend/src/services/segment_matcher.py +++ b/FitnessSync/backend/src/services/segment_matcher.py @@ -1,8 +1,9 @@ from typing import List, Optional, Tuple from datetime import timedelta import logging -from sqlalchemy.orm import Session -from sqlalchemy import text # correct import +from sqlalchemy.orm import Session, joinedload +from sqlalchemy import text, or_, func +from geoalchemy2.functions import ST_Intersects import json from ..models.activity import Activity @@ -19,20 +20,14 @@ class SegmentMatcher: def match_activity(self, activity: Activity, points: List[List[float]], min_created_at=None) -> List[SegmentEffort]: """ - Check if the activity matches any known segments. - points: List of [lon, lat] + Check if the activity matches any known segments using PostGIS. """ if not points or len(points) < 2: return [] - - # 1. Calculate bounds of activity for fast filtering - act_bounds = calculate_bounds(points) # [min_lat, min_lon, max_lat, max_lon] - - # 2. Query potential segments from DB (simple bbox overlap) - # We'll just fetch all segments for now or use a generous overlap check - # if we had PostGIS. Since we use JSON bounds, we can't easily query overlap in SQL - # without special extensions. We'll fetch all and filter in Python. - # Ideally, we'd use PostGIS geometry types. + + # Optimize: Use ActivityStream geometry if available + # But we might just have the activity object and points passed in triggered by sync. + # We can construct WKT from points for the query # Normalize activity type act_type = activity.activity_type @@ -41,47 +36,165 @@ class SegmentMatcher: elif act_type in ['trail_running', 'treadmill_running']: act_type = 'running' - query = self.db.query(Segment).filter( - (Segment.activity_type == activity.activity_type) | (Segment.activity_type == act_type) - ) + from geoalchemy2.functions import ST_Intersects + from sqlalchemy import func + # Construct simplified lineage for the query (or use the one in ActivityStream if present) + # If we have ActivityStream, use it. + from ..models.stream import ActivityStream + act_stream = self.db.query(ActivityStream).filter_by(activity_id=activity.id).first() + + candidates = [] + if act_stream and act_stream.geom is not None: + # Use DB geometry + logger.info(f"Segment Match: Using DB Geometry for Activity {activity.id}") + query = self.db.query(Segment).filter( + (Segment.activity_type == activity.activity_type) | (Segment.activity_type == act_type) + ).filter( + ST_Intersects(Segment.geom, act_stream.geom) + ) + else: + # Fallback (or if ActivityStream not yet populated, though sync should have done it) + # Create WKT from points + logger.info(f"Segment Match: Using Points Geometry for Activity {activity.id}") + wkt_points = [f"{p[0]} {p[1]}" for p in points if len(p)>=2] + wkt = f"SRID=4326;LINESTRING({', '.join(wkt_points)})" + + query = self.db.query(Segment).filter( + (Segment.activity_type == activity.activity_type) | (Segment.activity_type == act_type) + ).filter( + ST_Intersects(Segment.geom, func.ST_GeomFromText(wkt, 4326)) + ) + if min_created_at: query = query.filter(Segment.created_at >= min_created_at) - segments = query.all() + candidates = query.all() matched_efforts = [] + logger.info(f"Segment Match: Found {len(candidates)} candidate segments intersecting Activity {activity.id} path.") - print(f"DEBUG SEGMENT MATCH: Checking {len(segments)} segments against Activity {activity.id} Bounds={act_bounds}") - - for segment in segments: - # print(f"DEBUG: Checking Segment {segment.name} Bounds={segment.bounds}") - seg_bounds = json.loads(segment.bounds) if isinstance(segment.bounds, str) else segment.bounds - - if self._check_bounds_overlap(act_bounds, seg_bounds): - try: - seg_points = json.loads(segment.points) if isinstance(segment.points, str) else segment.points - print(f"DEBUG: Overlap OK. Matching {segment.name}...") - indices = self._match_segment(segment, seg_points, activity, points) - if indices: - start_idx, end_idx = indices - print(f"DEBUG: MATCH FOUND for {segment.name}! Indices {start_idx}-{end_idx}") - effort = self._create_effort(segment, activity, start_idx, end_idx) - if effort: - matched_efforts.append(effort) - except Exception as e: - logger.error(f"Error matching segment {segment.id}: {e}") + for segment in candidates: + # Proceed with detailed matching (Python logic is still good for precise start/end) + # PostGIS intersect just guarantees we cross paths, but logic needs to check direction/completeness etc. + # We reuse existing _match_segment + try: + seg_points = json.loads(segment.points) if isinstance(segment.points, str) else segment.points + indices = self._match_segment(segment, seg_points, activity, points) + if indices: + start_idx, end_idx = indices + logger.info(f"MATCH CONFIRMED for {segment.name}! Indices {start_idx}-{end_idx}") + effort = self._create_effort(segment, activity, start_idx, end_idx) + if effort: + matched_efforts.append(effort) + except Exception as e: + logger.error(f"Error matching segment {segment.id}: {e}") if matched_efforts: logger.info(f"Activity {activity.id} matched {len(matched_efforts)} segments.") - print(f"DEBUG SEGMENT MATCH: Matched {len(matched_efforts)} segments for Activity {activity.id}. Saving...") self.db.add_all(matched_efforts) self.db.commit() - else: - print(f"DEBUG SEGMENT MATCH: No segments matched for Activity {activity.id}") - + return matched_efforts + def scan_segment(self, segment: Segment, overwrite: bool = True) -> int: + """ + Inverted Index Scan: Find all activities that match this specific segment. + Uses PostGIS 'ST_Intersects' on ActivityStream.geom to find candidates efficiently. + """ + from ..models.stream import ActivityStream + from geoalchemy2.functions import ST_Intersects + from sqlalchemy import or_ + + # 1. Broad Phase: Find Spatially Intersecting Activities + # We need activities that have streams with geometry + query = self.db.query(Activity).join(ActivityStream, Activity.id == ActivityStream.activity_id) + + # Optimization: Filter out already matched activities if not overwriting + if not overwrite: + existing_subquery = self.db.query(SegmentEffort.activity_id).filter(SegmentEffort.segment_id == segment.id) + query = query.filter(Activity.id.notin_(existing_subquery)) + + # Filter by Type + # Map segment type to activity types (inverse of what we do in match_activity) + if segment.activity_type == 'running': + query = query.filter(or_( + Activity.activity_type.ilike('running'), + Activity.activity_type.ilike('trail_running'), + Activity.activity_type.ilike('treadmill_running'), # unlikely to have GPS but safe + Activity.activity_type.ilike('walking'), + Activity.activity_type.ilike('hiking') + )) + elif segment.activity_type == 'cycling': + query = query.filter(or_( + Activity.activity_type.ilike('%cycling%'), + Activity.activity_type.ilike('%road_biking%'), + Activity.activity_type.ilike('%mountain%'), + Activity.activity_type.ilike('%mtb%'), + Activity.activity_type.ilike('%cyclocross%') + )) + + # Spatial Filter + # Use segment.geom (which must be set and valid SRID 4326) + query = query.filter(ST_Intersects(ActivityStream.geom, segment.geom)) + + # Eager load streams to avoid lazy loading issues/performance penalty + query = query.options(joinedload(Activity.streams)) + + candidates = query.all() + logger.info(f"Segment Scan: Found {len(candidates)} candidate activities for Segment {segment.name} ({segment.id})") + + matches_found = 0 + + for activity in candidates: + try: + # 2. Detailed Match + points = [] + # Check for streams object availability + if activity.streams: + # Zip arrays + lats = activity.streams.latitude + lons = activity.streams.longitude + if lats and lons: + # points = [[lon, lat] for lat, lon in zip(lats, lons) if lat is not None and lon is not None] + # Ensure same length + points = [[lon, lat] for lat, lon in zip(lats, lons) if lat is not None and lon is not None] + + if not points and activity.file_content: + # Fallback to parsing + from ..services.parsers import extract_points_from_file + points = extract_points_from_file(activity.file_content, activity.file_type) + + if not points: + print(f"DEBUG: Activity {activity.id} - No points found (streams={bool(activity.streams)}).") + continue + + # Match Logic + # Reuse _match_segment + seg_points = json.loads(segment.points) if isinstance(segment.points, str) else segment.points + + indices = self._match_segment(segment, seg_points, activity, points) + + if indices: + start_idx, end_idx = indices + + # Check if effort already exists? + existing = self.db.query(SegmentEffort).filter_by(segment_id=segment.id, activity_id=activity.id).first() + if existing: + self.db.delete(existing) + + effort = self._create_effort(segment, activity, start_idx, end_idx) + if effort: + self.db.add(effort) + matches_found += 1 + logger.info(f"MATCH: Activity {activity.id} matched Segment {segment.id}") + + except Exception as e: + logger.error(f"Error checking activity {activity.id} for segment {segment.id}: {e}") + + self.db.commit() + return matches_found + def _create_effort(self, segment, activity, start_idx, end_idx) -> Optional[SegmentEffort]: # Extract timestamps # Need to re-parse file to get timestamps? Or cached? diff --git a/FitnessSync/backend/src/services/sync/__pycache__/activity.cpython-311.pyc b/FitnessSync/backend/src/services/sync/__pycache__/activity.cpython-311.pyc index 2bc71b1ca109c07f614e9fc2f2ce97bb194ae26b..127968c9a8a672b1c9368a77356cd474d29cdc8d 100644 GIT binary patch delta 4228 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 J1@wcw|3AZlD9-=@ delta 2759 zcmai0Yiv~45x(cH_i3-aYwruc+4!}#ynY+7fy6HiEq=sHDcH#C_1+6!v+H%wT|?{` zITa+K`4iD;r6iDO)rthAQi8O9kdjDgY947qRT2`&{XyJHtt#~oQTnH;Ds|?p4F)CZ zmG?VyX6DS9GiT0>e|3kQyTS6`vfFJEewY1f?3;t<@_p>XbG6SuB}=L_>m~txs^?86 zNx#Ei_8a%g(x9Z|osks#n=(r9w=VH1EF=Z;A&0%{jc#yhw60ISy$knPkh#DvR~y_) zE)>XRFedjh4>;v5@Q!RTTF`*UW96BzEO~jX5&rI~Gqsa$Be>kHre-38u;1O!y5XF= zoB83U`)$?)&v=?ki%D2TP=KJ@7Uq>mN)68@r_rNTPp7+%*oN3#N;haJt{aZ2DP9N# z-f`yuNv!dRmI`aCI>-0Gl6QNq@U)%`R=@||L#!P45z4C|I8 zS5zLJUKDHf97WZl`~>M5BJntXnwCBqHaf^a66riTf}CCR?-*Cfr0 z;K{mnHU_D>;O-$}gb5Ua5Q1*u$#{G^5D? zya?5Gr;aMLl8h)aA4sX$)#vM9d% zWrCduy5VRdm35Eo^9%5PM@faaDoV@E_ryy{;TH(dJn02{r_=OLoM>xM)!D$BGNYYO zvdqP8i|pViveP})gS(`U%)M?LR?CBkSM5Dzdm5x`^$aK18f=3VhHD*_gT;pHZX!*l%n;?Rqn{a7(hVyB#^)<0~p{p;>i%MQu5Yt2rc|)*w4zLe53}G zX<%fe=5tzh8zCU`FL2PAqERwa1trlPgr#2Cn&X101I6&^sEf_Re@FbLa*|F#^=RmO zY()Q`jg#<~(RCZCjP>arR`PqaN9ls6R;d(nthM|j+Tc(^yIqQP*)r=yQ{p=>DYj)RM25WZ;zTDC#Rk?U%&Zpv zF%d1exh&0Ys7h&BWl1u}HGl381?;yxGolmUyB;P=yHO zW8$4MkEhi-&Zi`Q}yHeCzHH{hFm%JdHY7o%BWa`LL# zWu0gDB^T6(8+|tWVRqNyy5ng6%+Y+=cgxXr+tKxBM_1-_SY{2y{gS*Y+4^@FS8dh( z?Z(x1Bhnb00B$lIkcWpky`(@9_rVuO>(nxw@Pi0?&U7*v-_-kzBv~K;0;*k{Jmn^W z7YNc~if)RB$;C?(kCD=V6Hv?20FE8|o4?(-<&t(^!r^jC439$`=4K8J1dO}yO9+>q z5W^MMmFS0US7zQX7ty^F!sqdFFD~O5S?E5L>@t;Mfwzh$kuZLNCFbU$a%%+R&($8QAxUq2ufJN^$t=ZM(= diff --git a/FitnessSync/backend/src/services/sync/__pycache__/activity.cpython-313.pyc b/FitnessSync/backend/src/services/sync/__pycache__/activity.cpython-313.pyc index 925f346a4f79541109dc02230c340d826e5d6d35..4a26e714025a1c98a4f2d85c4541f427497d7dd7 100644 GIT binary patch delta 3884 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 K{htU#bNn{}0Zdx} diff --git a/FitnessSync/backend/src/services/sync/activity.py b/FitnessSync/backend/src/services/sync/activity.py index b406352..b779165 100644 --- a/FitnessSync/backend/src/services/sync/activity.py +++ b/FitnessSync/backend/src/services/sync/activity.py @@ -10,6 +10,8 @@ from ...models.activity_state import GarminActivityState from ...models.sync_log import SyncLog from ...services.garmin.client import GarminClient from ...services.job_manager import job_manager +from ...services.parsers import parse_fit_to_streams +from ...models.stream import ActivityStream logger = logging.getLogger(__name__) @@ -263,6 +265,9 @@ class GarminActivitySync: # Backfill metrics from file content if needed self._backfill_metrics_from_file(activity) + + # Save Activity Streams to DB (PostGIS) + self._save_activity_streams(activity) self.db_session.flush() # Commit file changes so it's fresh @@ -451,6 +456,7 @@ class GarminActivitySync: try: self._backfill_metrics_from_file(activity) + self._save_activity_streams(activity) processed += 1 # Optimistically assume something might have updated, or we could track it. # Since _backfill checks for None, it only updates if missing. @@ -470,3 +476,37 @@ class GarminActivitySync: self.db_session.commit() return {"total": total_count, "processed": processed} + def _save_activity_streams(self, activity: Activity): + """ + Parse FIT content and save to activity_streams table. + """ + if not activity.file_content or activity.file_type != 'fit': + return + + try: + # Check if streams already exist? + existing = self.db_session.query(ActivityStream).filter_by(activity_id=activity.id).first() + if existing: + # Update or Skip? Let's overwrite/update + self.logger.info(f"Streams already exist for {activity.id}, updating...") + # Easier to delete and recreate or update columns? + # Update columns is better for ID stability but recreate is simpler. + # Let's recreate logic: Parse first. + pass + + data = parse_fit_to_streams(activity.file_content) + if not data: + return + + if existing: + for k, v in data.items(): + setattr(existing, k, v) + else: + stream = ActivityStream(activity_id=activity.id, **data) + self.db_session.add(stream) + + self.logger.info(f"Saved streams for Activity {activity.id}") + + except Exception as e: + self.logger.error(f"Error saving streams for {activity.id}: {e}") + diff --git a/FitnessSync/backend/src/tasks/__pycache__/definitions.cpython-311.pyc b/FitnessSync/backend/src/tasks/__pycache__/definitions.cpython-311.pyc index 074f5c49752718efd02f0b72ad5d8e3ab6cca464..800575f9620cd2cf2b59be2d6059cb2d4443d9c3 100644 GIT binary patch delta 3250 zcmaJ@T}&L;6`r4+S!S69%o=tVSTo=?X6*t78!N^?)Ht?dIi-y)MNQObJ?zXbWA6{T zGk}f3OXRA3s2tV0eQN58r^HrJMYT#|sY+zyJ~S`wl2WCesuHRcsp*4-RY$V?)N}6c zvX}_p-TCI6bMHOp+%xB%dv9G|oUZ%O@Aq-g&i^549o~PtF2uLo-uwCS1~ZuS7(0II zdAr3>&1S>T_(tGcH}L`B+mKHR#!jFkC7SNz+a>-Y7Yh~M=9dLNMkAhXeukd$Q=#sCSrh}vO&%tmzZwQ7s%fHVVo;%{A^nr)t zK7yyzc#f_KEyAKqPk7s?8}x^H!)w%hci5~%|I!hZlv!@kN9XIC+UqJRI4;VOb91uO zP?anwbfu%oTRSVi&)pGFcim+cARIEwIYIiozNb%_lW)k`8iT#3`DyN|_!c+K8w&nD z5Wr7(N{#0Xh0fQ6^9lq+oq5lvLHmJ8Ql_Den|j(A*X(QAgmyVMHl$rl=vggeI=Z1d zx|Y%>Y^~>nb`kq0%&cK$Qe2tGscs8Q1{JF)Y*alTV0^DV!piY%2IlJNAC%)l(sUKLl+y`y)l(| zVJ7{P+E4${5XlFDA*hRl5W)y805LDYkI8J2R-jzL7$c1+iXiL)uu&7n92#i{+6};p z<0ThZtl|(2(g!`L!TvSRar4}|kMp$^mDUv{vZ}O3*13?>ztFfY1eN}Osj<7AD{9ZG z+EY|}=xu-K^tUqS53Q=53x}6t5A=18yTG3nisX8k`p(P$n(&0<0G~31exT_?fAHiB zn#PJ3CX4FSiaNEbP64qFW}3cI4}7T}SaKemDXIrn)bp$A`6By2`?i5=2-E)N5M5W! z=6k?n<~D;TxPYe!RoS-GvuDq??PDGGQHazp_*28;t&zHsHetCH>E$-xsXl4BYxvYY z=~3q%2}F6q;MZJQ!Br4&JfEJa8!Qq>$eh%8H#8lYI3 zR5B&vHesri?8PT1(mCB=#ty^Cp~Bs!|B8JZ?#~1+Iy!N1>x}7%D@iMz*1E}V)E-3$ zAPms2!UOz%>T8MS2T`!C+sc;A-r}}f3&v8W6UXi7O1Kg2(kc{CTUkXp;lsEJJ>ghW zmUAs$_O3R~mU0Y+pDWyN`JzTXfesDRpYOgdv%S_tYodGmM^JhS;WU8jPm)~5v0$G& zdS-&WiZU6#1Z>^XVnXy%w7CVxGJlxgwgfCXHBZ{1_G`Dq6PMb zz+fV6K^5MVb{B9Ard=sOJAlFfZEoL(VEMbl(mT*MUfAFLiH9Gc|Nh=y`4tej0s3xF z;7uZ67KXIJJxZ^&kYYYT# zeI{p^a1vEAizILua|kysIYaLpnty8}!(H#RPVi`PMGHc~eBKgve<|!$Ptguj>-x%8 zEYl|AD7EO+;I0UZU>ESVA7FFzAouC-2iv4MkbhkGa`02J8}*J76)+}NExGa)a4cop zVo3weJJK%A1Gh&1cI03vApluvSR`=Z|8DTKqpy3=!PG9d%heVHRlim#gV}6Wv@4aRT#~o-V%l|5^=h9PGt6(nU41qGndr3=nIoR#dfx#uYWXsz!@yl#Pupyt$hS`;DJZ* zs32I3$Rz~aSY#dngL{u7?;?#itmieypfpUT%@gETz(T}1`x~B;qyJGOeE)*D&cSO* Zq>qm^(+9^si}~t6x_h02*S5g4{~xth6@~x+ delta 2815 zcmaJ@TWlLu5Z${k$Id#j8^?+3#BtIP+a-^t&!lajb$B;KUM)dB^Yf32?nJWSG;bGc}E*`%L1(jNfP&mG!_2@}nDd%?Rs3&Tjxd zx{j{{zKQeh`VjEV>-c)$W5|z+dKl=IGTi`lyi7L&-KvuZgjD1gbY%D!Q4mT8$%i7D zV_rE){$yL_BG7NjDN@J2k{=_RN-5=bLVjmOb%;f`drOVt7ld@MpQO-C-ms~ev*OmV zLN0D+t)p=o1g$iLP!Eu*reUNT5IhJ9f(+o);jYXPBRg&%$&@wG2tMA3pdv&Onh=@+ zEYuKpFWM#ugMw2}Ckv~aGh89MK@+eL_gei#5Kao0W#3B&o;$D<*#BV|0KS*x^s<~@ zlGE&d<&K8rH(GMO|Fc6&TJn-Mw5$y+@h=M`n^;Zzoos{pm`K~%f))`!BJ7D^lAVgy zvO0f|b$Wtqm%mXVx_Dk<5BfKY=VkV{znN*B)#_O=evYsev6cOBQ=6nrtFP>6Wzm{O zR+QR$)MwQbYN1N!_bDX7d)>&z ztz5R?cpwY~voJ<+%be(_mYuaHEvIUN7RRVzSt&wm;So>DaH^Ume7!STu9%+~H*6#G zWO2leJWm!6|Fy`cJhTRP^6fF898n*kbtsA>v;tHn29k?;Dd!eFYwSgsV;Ih`RthLV` zx}@dwr(@ck4S3`nQsjQ$xhR`dAR2#0KHlv|tBW zcSl!`RIW09Usq+44xj&2nZ&D1f+~}b_h6N6T&*(f@Cl=!Ly^sibSoHP#Mj~ZpUow9)?#67`9|9^KZ!lN zg$?aW@;9Dfkzcn3H7BvMVi$Mh>i(v`r4mrKq zv)Dxd-Q1KM?Jp8Yp=;PsbTh(s_G`@^(#2A>(HYuJ5i4(4{e_vGu*?tk6o_Y3eE>Y?YnR6ySJj<*Jp?oLX9Erw--#5PvST@&R!3- zDCpjOr3;}itJK@jjoaDj=4Vv*{AV?>MD{L}-i>eo!10gLV%|2vPdl5Rp!c9mg)<3S zWNi+l6vt;5j~a!HZsu%9v?<@rqsLvdaH60qC&7)^VtwRP>FXFJ{nv~+8Z~s28aX>N zNlp47+OJ40&MQaB6(2iFAIap3g;8^iK7>{eAv}zL?*QQgdyjy@ z+_-6g<7-Xy2(H02xdFnFi~(AI?Q@HbAJb*O`8(hw$ie!FQg7S)GU;PiIuhg-7D&Vn z-neYaQ{w-6dac_3HNV-3M5{6iw&qK3Cwf$M03GgR-*=6I*?rvu9Y;Xm)aHx2F>ZBd zbGG@EX&<8|F5{;PhMo?wx4KUrzSfD3XDUl^5;~FzG2uQ}#NY46f6Y_0!__*zvK7;? zXaS`~Hr*3bc_=&CY)`n2w`drL8&s{C&%*GWC{7ua_vrJm<&)BPJs(I()ILl#Kxj%n zKT+bSN5Q=@%aVYBbU?*^u&qUT5x6Vthi%)$R9oqApGrn}`SJffC*T>~gn*Ni;s~WX z5%55u*m68y6az`o0gBq@1o{%v=!CjmhshsX*??#VyHEr1{Z diff --git a/FitnessSync/backend/src/tasks/definitions.py b/FitnessSync/backend/src/tasks/definitions.py index 983e8e0..29a3557 100644 --- a/FitnessSync/backend/src/tasks/definitions.py +++ b/FitnessSync/backend/src/tasks/definitions.py @@ -24,40 +24,32 @@ def run_activity_sync_task(job_id: str, days_back: int, db_session_factory): logger.error(f"Background task failed: {e}") job_manager.fail_job(job_id, str(e)) -def run_metrics_sync_task(job_id: str, days_back: int, db_session_factory): - logger.info(f"Starting background metrics sync task {job_id}") - with db_session_factory() as session: - try: - load_and_verify_garth_session(session) - garmin_client = GarminClient() - sync_app = SyncApp(db_session=session, garmin_client=garmin_client) - sync_app.sync_health_metrics(days_back=days_back, job_id=job_id) - except Exception as e: - logger.error(f"Background task failed: {e}") - job_manager.fail_job(job_id, str(e)) - -def run_health_scan_job(job_id: str, days_back: int, db_session_factory): - """Background task wrapper for health scan""" +def run_health_sync_job(job_id: str, days_back: int, db_session_factory): + """ + Merged health sync job: Scan metadata gaps -> Sync pending details. + Replaces old metrics_sync and independent scan/pending jobs. + """ with db_session_factory() as db: try: + load_and_verify_garth_session(db) garmin_client = GarminClient() sync_app = SyncApp(db, garmin_client) - job_manager.update_job(job_id, status="running", progress=0) + # Step 1: Scan for gaps + job_manager.update_job(job_id, status="running", progress=0, message=f"Scanning health metrics gaps ({days_back} days)...") sync_app.scan_health_metrics(days_back=days_back) - job_manager.complete_job(job_id) - except Exception as e: - logger.error(f"Background task failed: {e}") - job_manager.fail_job(job_id, str(e)) - -def run_health_sync_job(job_id: str, limit: int, db_session_factory): - """Background task wrapper for health sync pending""" - with db_session_factory() as db: - try: - garmin_client = GarminClient() - sync_app = SyncApp(db, garmin_client) - sync_app.sync_pending_health_metrics(limit=limit, job_id=job_id) + # Step 2: Sync pending items + job_manager.update_job(job_id, status="running", progress=20, message="Syncing pending health details...") + + # Pass job_id for sub-progress updates + sync_app.sync_pending_health_metrics(limit=None, job_id=job_id) + + # Job completion is handled by sync_pending_health_metrics if job_id is passed? + # Checking code: yes line 390 calls complete_job. + # But wait, we passed job_id to it. + # If sub-function completes it, we shouldn't do anything else here. + # However, sync_pending_metrics assumes it OWNS the job. except Exception as e: logger.error(f"Health sync job failed: {e}") @@ -234,3 +226,19 @@ def run_segment_matching_task(job_id: str, db_session_factory, **kwargs): # Just call the job function directly, it handles everything run_segment_matching_job(job_id, db_session_factory=db_session_factory, **kwargs) +def run_bike_matching_job(job_id: str, db_session_factory): + """Background task wrapper for bike matching""" + with db_session_factory() as db: + try: + from ..services.bike_matching import run_matching_for_all + + job_manager.update_job(job_id, status="running", progress=0, message="Starting bike matching...") + + run_matching_for_all(db) + + job_manager.complete_job(job_id, result={"status": "completed"}) + + except Exception as e: + logger.error(f"Bike matching job failed: {e}") + job_manager.fail_job(job_id, str(e)) + diff --git a/FitnessSync/backend/src/utils/__pycache__/geo.cpython-313.pyc b/FitnessSync/backend/src/utils/__pycache__/geo.cpython-313.pyc index 46f9304139e6933e7b32f87c3a8e8f09eb9fc56b..795b570f722395fbe2b76a8489e8054ebfefb89b 100644 GIT binary patch delta 1314 zcmZuxO=ufO6rTN)R$AH8YNf<7s1=r|s=8Ki6UU96%1L8Ge8}%=iBf@OA<51Ld9BVW zZQ9UEkV6HzNL{-XGzUX_u%U-QdTlYJ(5r-?s3M2ldR0hhOAn=Q#**Da2KJlxvv1z} zzFGEu1a!&VHT8{BQD4&w`A}(7 zH_B#6H)vuL>+8CaEryD^S&lpL9Gu#7nB^=kXY~Btp1ok?iGdxE$PIeZ&V-}_&x&^jGrxT=CVaI5hq97A;sSU{8dF=$=jA*+mSocMAsK=DbbcI`poUE@m-Hx6Z)e_ z2{Z$3Z@6afdXCjUZn$g0ODFOkuYa;L-S}*0{Z~0&JO5OU?%H-g+;cpd?a1%fu0id# z*bR(V>i+In;7i|g2lAfiyO7IMk3LBJrW|iNI!dgzNGzeFL~Dy(u(+nTex_M&E8&(a z{A@T39w(d*C5;LAJ;IMCpCT2u$HAwH$z8`ds>^{wLL4^t93`u})5sni+a2DJRz+V|aMT-F(X+(Umt z*C9x!!Z)l9e^B}Wd{ScyjJ56mX>y8*>_&FWkCyiCwcncWBk=bNbZcErwzO3H>dluV L*k5HV^;q~{*nS>x delta 125 zcmbPe{!NGPGcPX}0}upPBxbVnPvnzeyf#t&h&X!;Qw$3$P~3ziSWt=~i=~J&ol#S0 zvlin40mk&nuLY;E*#TvX>?XGh9pB6&F_n?k6Ua}WyiBr!k!!NNlpaLhb8@VdEVmC( TiV=v5Z!t`6mD)EsPg)rOUo;){ diff --git a/FitnessSync/backend/templates/activities.html b/FitnessSync/backend/templates/activities.html index f7fac0a..680ea74 100644 --- a/FitnessSync/backend/templates/activities.html +++ b/FitnessSync/backend/templates/activities.html @@ -6,28 +6,11 @@
- - - - - + +
-
- New: 0 - Updated: 0 - Synced: 0 -
+
@@ -419,15 +402,8 @@ document.getElementById('download-selected-btn').addEventListener('click', downloadSelected); document.getElementById('redownload-selected-btn').addEventListener('click', redownloadSelected); - document.getElementById('scan-30-btn').addEventListener('click', () => scanActivities(30)); - document.getElementById('scan-all-btn').addEventListener('click', () => scanActivities(3650)); - document.getElementById('sync-10-btn').addEventListener('click', () => syncPending(10)); - document.getElementById('sync-all-btn').addEventListener('click', () => syncPending(null)); - document.getElementById('sync-all-btn').addEventListener('click', () => syncPending(null)); - document.getElementById('match-bikes-btn').addEventListener('click', triggerBikeMatching); - updateSyncStatus(); }); // ... Helpers same as before ... @@ -617,17 +593,7 @@ loadActivities(); } - async function updateSyncStatus() { - try { - const res = await fetch('/api/activities/sync/status'); - if (res.ok) { - const data = await res.json(); - document.getElementById('status-new').textContent = `New: ${data.new || 0}`; - document.getElementById('status-updated').textContent = `Updated: ${data.updated || 0}`; - document.getElementById('status-synced').textContent = `Synced: ${data.synced || 0}`; - } - } catch (e) { } - } + async function scanActivities(daysBack) { showToast("Scanning...", `Starting scan...`, "info"); @@ -655,21 +621,7 @@ } catch (e) { showToast("Error", e.message, "error"); } } - async function triggerBikeMatching() { - showToast("Matching...", "Starting bike match process...", "info"); - try { - const res = await fetch('/api/bike-setups/match-all', { method: 'POST' }); - const data = await res.json(); - if (res.ok) { - showToast("Success", data.message, "success"); - loadActivities(); - } else { - throw new Error("Failed"); - } - } catch (e) { - showToast("Error", "Matching failed", "error"); - } - } + async function fetchBikeSetups() { try { diff --git a/FitnessSync/backend/templates/activity_view.html b/FitnessSync/backend/templates/activity_view.html index 17af5f0..5cf66dc 100644 --- a/FitnessSync/backend/templates/activity_view.html +++ b/FitnessSync/backend/templates/activity_view.html @@ -3,339 +3,397 @@ {% block head %} - - + + {% endblock %} {% block content %} -
-
-

Loading...

-

- - | - - | - # -

-
-
-
- - - - - - - - - - - - + +
+
+
+

Loading Activity...

+

+ | + | + ID: +

- - - - - - - Discovery - -
-
+
+ +
+ + + + +
- -
-
-
-
- -
-
-
-
- -
Distance
-

-

+
+ +
+ +
+
+ Route Map +
-
-
-
-
-
- -
Duration
-

-

-
-
-
-
-
-
- -
Avg HR
-

-

-
-
-
-
-
-
- -
Calories
-

-

-
-
-
-
- - -

Detailed Metrics

-
- -
-
-
-
Matched Segments
- -
-
-
- - - - - - - - - - - - - - -
SegmentTimeAwardsRank
Loading segments...
+
+
+
-
- -
-
-
Heart Rate
-
-
Average: - - bpm
-
Max: - bpm
+ +
+
Matched Segments
+
+
Loading segments...
-
- -
-
-
Speed / Pace
-
-
Avg Speed: - km/h
-
Max Speed: - - km/h
-
-
-
- - -
-
-
Power
-
-
Avg Power: - W
-
Max Power: - W
-
Norm Power: - W
-
VO2 Max: -
-
-
-
- - -
-
-
Elevation
-
-
Gain: - m -
-
Loss: - m -
-
-
-
- - -
-
-
Training Effect
-
-
Aerobic: - -
-
Anaerobic: -
-
TSS: -
-
-
-
- - -
-
-
Cadence
-
-
Avg: - -
-
Max: -
-
-
-
- - -
-
-
Respiration
-
-
Avg: - - br/min
-
Max: - br/min -
-
-
-
- - -
-
-
- Bike Setup - -
-
-
No Setup
-
-
-
-
- - -
-
-
-
- Activity Streams -
-
+ +
+
+ Activity Streams +
+
- +
- +
-
-
-
-

Cycling Workout Analysis

-

Toggle metrics to customize view

+ + +
+ +
+ +
+
+
+ + +
+ +
+
Overview
+
+
+
Distance
+
-
+
+
+
Duration
+
-
+
+
+
Avg HR
+
-
+
+
+
+ + +
+
Performance Metrics
+
+ +
+

Heart Rate

+
Average- bpm
+
Max- bpm
+
+ + +
+

Speed

+
Average- km/h
+
Max- km/h
+
+ + +
+

Power

+
Average- W
+
Max- W
+
Normalized- W
+
VO2 Max-
+
+ + +
+

Elevation

+
Gain- m
+
Loss- m
+
+ + +
+

Training Effect

+
Aerobic-
+
Anaerobic-
+
TSS-
+
+ + +
+

Cadence

+
Average- rpm
+
Max- rpm
+
+ + +
+

Respiration

+
Average- br/min
+
Max- br/min
+
+ + +
+
+

Bike Setup

+
-
-
- - -
- -
-
- - -
- -
- - -
- -
- - -
- +