diff --git a/backend/app/workers/tasks.py b/backend/app/workers/tasks.py index 22e9db8..0f67f77 100644 --- a/backend/app/workers/tasks.py +++ b/backend/app/workers/tasks.py @@ -530,6 +530,13 @@ def detect_route(activity_id: int, user_id: int): return {"status": "no_match"} +# Personal records are only computed from device-recorded FIT files (Garmin and +# other GPS watches/head units). Phone- and Strava-sourced GPX/TCX imports — +# especially older ones — suffer GPS "teleporting" that fabricates impossibly +# fast splits, so they're excluded from PRs entirely. +PR_ELIGIBLE_SOURCE_TYPES = {"fit"} + + @celery_app.task(name="compute_personal_records") def compute_personal_records(activity_id: int, user_id: int, parsed: dict): """Calculate personal records for standard distances from this activity.""" @@ -539,6 +546,10 @@ def compute_personal_records(activity_id: int, user_id: int, parsed: dict): from sqlalchemy import select from datetime import datetime, timezone + # Only FIT (watch-recorded) activities are trusted for PRs — see note above. + if parsed.get("source_type") not in PR_ELIGIBLE_SOURCE_TYPES: + return {"status": "skipped_non_watch", "activity_id": activity_id} + data_points = parsed.get("data_points", []) total_dist = parsed.get("distance_m", 0) or 0 sport = parsed.get("sport_type", "running") @@ -1188,8 +1199,9 @@ def backfill_indoor_distances(user_id: int): @celery_app.task(name="recompute_personal_records_all") def recompute_personal_records_all(user_id: int): """Wipe and rebuild all personal records from stored activity data, excluding - indoor (no-GPS) runs whose distance is unreliable. Fixes records polluted by - treadmill over-measurement and any duplicate current-record rows.""" + indoor (no-GPS) runs and non-watch (GPX/TCX) imports whose distance is + unreliable. Fixes records polluted by treadmill over-measurement, GPS + teleporting in old phone/Strava imports, and any duplicate current-record rows.""" from app.services.route_matcher import compute_best_splits, STANDARD_DISTANCES from app.core.database import SyncSessionLocal from app.models.user import Activity, ActivityDataPoint, PersonalRecord @@ -1209,6 +1221,8 @@ def recompute_personal_records_all(user_id: int): best = {} # (sport, dist_m) -> {dur, aid, at, label} for a in acts: + if a.source_type not in PR_ELIGIBLE_SOURCE_TYPES: + continue # only watch-recorded FIT files count toward PRs if a.sport_type == "running" and a.polyline is None: continue # indoor/treadmill — unreliable distance rows = db.execute(