Skip to content

feat: recognise strength training as a sport without distance - #29

Merged
danielededo merged 3 commits into
mainfrom
feat/strength-sport
Oct 11, 2026
Merged

danielededo merged 3 commits into
mainfrom
feat/strength-sport

Conversation

@danielededo

Copy link
Copy Markdown
Owner

Step 2. Stacked on #28 (the sport catalogue), so this PR targets that branch and its diff shows only step 2. GitHub retargets it to main when #28 is merged.

Garmin records a strength session as FIT sport training, sub-sport strength_training. Until now it was stored as other and shown with the few metres of drift a wrist logs in a gym, plus a pace for it.

What changes

Catalogue (app/services/sports.py)

  • New has_distance (default True) and fit_sub (FIT sub-sport aliases) on every sport.
  • New strength entry: has_distance=False, fit_sub=("strength_training",).
  • sport_from_file("fit", sport, sub_sport): a sub-sport the catalogue lists wins over the sport. Unlisted sub-sports change nothing, so indoor_rowing under fitness_equipment is still other, and road under cycling is still cycling.
  • GET /api/sports now includes has_distance.

FIT parser

  • Reads the sub-sport from the session, or from the sport message if the session has none, as it already did for the sport.
  • Keeps the declared sport in raw_data.declared_sport beside sub_sport. TCX and GPX already kept theirs.

New scripts/backfill_sports.py

  • Re-applies the catalogue to activities already stored, using what each file declared. FIT rows stored before declared_sport existed can still be moved by sub-sport.
  • Renames an activity only if it still has the generated name (Other 2026-06-24 becomes Strength 2026-06-24). A name from the file, or one you chose, stays.
  • Has --dry-run, and is safe to re-run.

Dashboard, for a sport without distance

  • Hidden: distance, pace and climb (detail, table, records), the route panel, comparison along a distance axis, and the bar in the per-sport breakdown.
  • Kept: time, heart rate, zones and load.
  • formatPaceOrSpeed answers — for such a sport, so every place that shows pace is covered.
  • The figures are still stored as the file wrote them.

Docs: README (feature list, a backfill step under "Updating", "Adding a sport", test counts) and three new paragraphs in docs/DESIGN.md.

Verified

  • Backend: pytest 389 passed (15 new: parser, catalogue and backfill), coverage 96.8%, ruff lint and format.
  • On PostgreSQL 16, using the scratch database from the import tests:
    • A synthetic Garmin strength FIT (sport 10, sub-sport 20, no GPS) is stored as strength / Strength 2026-06-24, with declared_sport: training and sub_sport: strength_training.
    • With that row reset to other, the backfill moves it back and renames it.
    • A dry run over the 300 running and cycling activities moves nothing.
  • Frontend: vitest 252 passed (6 new in tests/strength.test.jsx), eslint and build. All 6 new tests fail when run against the components without this change.

Not covered

  • A real strength FIT file. No public one exists (the Garmin SDK samples and the fixtures of python-fitparse and fitdecode have none), so this follows Garmin's documented sport and sub-sport values, tested on files built from the format spec.
  • Sets, reps and weights: that's step 3.

What differs between sports lived in seven places: an alias table in each
of the three parsers, and the pace list, cadence unit, labels, colours and
order in the frontend. Adding a sport meant finding all of them.

app/services/sports.py now holds one entry per sport: label, pace or speed,
cadence unit, colour slot, and per-format file aliases. The parsers look
sports up there, GET /api/sports serves it, and the dashboard loads it at
startup and reads it everywhere. No behaviour change: the new lookup was
checked against the old tables for every alias. A backend test keeps the
frontend tests' copy of the catalogue in step.
Garmin records a strength session as FIT sport training, sub-sport
strength_training, so it was stored as other and shown with the few metres
of drift a wrist logs in a gym, and a pace for it.

The catalogue gains has_distance and FIT sub-sport aliases, and a strength
entry. A listed sub-sport now decides the sport. For a sport without
distance the dashboard leaves out distance, pace, climb, the route, distance
records, comparison and the distance bar, and keeps time, heart rate, zones
and load. The FIT parser keeps the declared sport, and
scripts/backfill_sports.py re-applies the catalogue to activities already
stored.
Base automatically changed from refactor/sport-catalogue to main October 11, 2026 17:34
# Conflicts:
#	README.md
#	backend/app/schemas/sport.py
#	backend/app/services/parsers/fit_parser.py
#	backend/app/services/sports.py
#	backend/tests/test_sports.py
#	docs/DESIGN.md
#	frontend/src/components/SportBreakdown.jsx
#	frontend/src/sports.js
#	frontend/src/utils/formatters.js
#	frontend/tests/sports.json
#	frontend/tests/sports.test.js
@danielededo
danielededo merged commit bd3980c into main Oct 11, 2026
5 checks passed
@danielededo
danielededo deleted the feat/strength-sport branch October 11, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant