Add PR migration script and documentation

Co-authored-by: joaovitoriasilva <8648976+joaovitoriasilva@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-11 14:49:41 +00:00
parent 127ea01bdc
commit 87c65e1edc
2 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# Personal Records (PRs) Module
This module implements automatic tracking of personal records (PRs) for fitness activities.
## Features
### Supported Activity Types
#### 🏃 Running (activity_type: 1, 2, 3, 34)
- **fastest_1km**: Fastest time to complete 1km
- **fastest_5km**: Fastest time to complete 5km
- **fastest_10km**: Fastest time to complete 10km
- **fastest_half_marathon**: Fastest time to complete 21.097km
- **fastest_marathon**: Fastest time to complete 42.195km
- **longest_distance**: Longest running distance
- **best_average_pace**: Best average pace (lower is better)
#### 🚴 Cycling (activity_type: 4, 5, 6, 7, 27, 28, 29, 35, 36)
- **fastest_5km**: Fastest time to complete 5km
- **fastest_20km**: Fastest time to complete 20km
- **fastest_40km**: Fastest time to complete 40km
- **longest_distance**: Longest cycling distance
- **max_power**: Maximum power output (watts)
- **best_normalized_power**: Best normalized power (FTP indicator)
#### 🏊 Swimming (activity_type: 8, 9)
- **fastest_50m**: Fastest time to complete 50m
- **fastest_100m**: Fastest time to complete 100m
- **fastest_200m**: Fastest time to complete 200m
- **fastest_400m**: Fastest time to complete 400m
- **fastest_1500m**: Fastest time to complete 1500m
- **longest_distance**: Longest swimming distance
#### 🏋️ Strength Training (activity_type: 19, 20)
- Placeholder for future implementation (requires activity_sets parsing)
## How It Works
### Automatic PR Detection
PRs are automatically checked and updated whenever:
1. A new activity is uploaded (GPX, TCX, or FIT file)
2. An activity is synced from Strava
3. An activity is synced from Garmin Connect
### Distance Tolerance
To ensure accurate PR tracking, distance-based PRs use a tolerance threshold:
- **Running/Cycling**: 2% tolerance (e.g., 4.9-5.1km counts as 5km)
- **Swimming**: 5% tolerance (pool distances can vary)
This prevents false PRs from longer activities where we don't have split data.
### Database Schema
```python
class PersonalRecord:
id: int # Primary key
user_id: int # Foreign key to users table
activity_id: int # Foreign key to activities table
activity_type: int # Activity type (1=run, 4=ride, etc.)
pr_date: datetime # When the PR was set
metric: str # PR metric name (e.g., 'fastest_5km')
value: Decimal # PR value (time in seconds, distance in meters, etc.)
unit: str # Unit of measurement ('seconds', 'meters', 'watts', etc.)
```
## API Endpoints
### GET /personal_records/user/{user_id}
Get all personal records for a user.
**Response:**
```json
[
{
"id": 1,
"user_id": 123,
"activity_id": 456,
"activity_type": 1,
"pr_date": "2025-10-11T10:30:00",
"metric": "fastest_5km",
"value": "1200.50",
"unit": "seconds"
}
]
```
### POST /personal_records/user/{user_id}/recalculate
Recalculate all personal records for a user from scratch.
**Response:**
```json
{
"message": "Personal records recalculated successfully"
}
```
## Initial Data Migration
To calculate PRs for existing activities, use the migration script:
```bash
# Migrate all users
cd backend/app
python -m activities.personal_records.migration
# Migrate specific user
python -m activities.personal_records.migration 123
```
## Database Migration
Run Alembic migration to create the personal_records table:
```bash
cd backend/app
alembic upgrade head
```
## Future Enhancements
1. **Strength Training PRs**: Parse activity_sets to track 1RM for specific exercises
2. **Segment PRs**: Track best times for specific route segments (like Strava segments)
3. **Power-based PRs**: Add 1-min, 5-min, 20-min power PRs for cycling
4. **Notifications**: Alert users when they set a new PR
5. **PR History**: Track historical PRs to see progression over time
6. **Stream-based PRs**: Calculate split times from activity streams for more accurate distance PRs

View File

@@ -0,0 +1,95 @@
"""
Script to migrate existing activities and calculate personal records.
This can be run as a one-time migration or scheduled as a background task.
Usage:
python -m activities.personal_records.migration
"""
import asyncio
from sqlalchemy.orm import Session
import core.database as core_database
import core.logger as core_logger
import users.user.crud as users_crud
import activities.personal_records.utils as personal_records_utils
async def migrate_all_users_prs():
"""
Migrate all users' activities to calculate personal records.
This is a one-time migration that should be run after the PR feature is deployed.
"""
db = next(core_database.get_db())
try:
# Get all users
users = users_crud.get_all_users(db)
if not users:
core_logger.print_to_log("No users found")
return
core_logger.print_to_log(f"Starting PR migration for {len(users)} users")
for user in users:
try:
core_logger.print_to_log(f"Calculating PRs for user {user.id} ({user.username})")
await personal_records_utils.recalculate_all_user_prs(user.id, db)
core_logger.print_to_log(f"Completed PRs for user {user.id}")
except Exception as err:
core_logger.print_to_log(
f"Error calculating PRs for user {user.id}: {err}",
"error",
exc=err
)
continue
core_logger.print_to_log("PR migration completed for all users")
except Exception as err:
core_logger.print_to_log(
f"Error in migrate_all_users_prs: {err}",
"error",
exc=err
)
raise
finally:
db.close()
async def migrate_single_user_prs(user_id: int):
"""
Migrate a single user's activities to calculate personal records.
Args:
user_id: The ID of the user to migrate
"""
db = next(core_database.get_db())
try:
core_logger.print_to_log(f"Calculating PRs for user {user_id}")
await personal_records_utils.recalculate_all_user_prs(user_id, db)
core_logger.print_to_log(f"Completed PRs for user {user_id}")
except Exception as err:
core_logger.print_to_log(
f"Error calculating PRs for user {user_id}: {err}",
"error",
exc=err
)
raise
finally:
db.close()
if __name__ == "__main__":
# Run the migration
import sys
if len(sys.argv) > 1:
# Migrate specific user
user_id = int(sys.argv[1])
asyncio.run(migrate_single_user_prs(user_id))
else:
# Migrate all users
asyncio.run(migrate_all_users_prs())