feat(journal): harden FIT→GPX conversion (pause/session segmentation, validation, sport)

Implements the fit-parsing-hardening change. The shared converter now:
- splits output into <trkseg>s on timer stop/start events, falling back to
  record gaps > 5 min, so downstream moving-time never bridges a pause;
- slices records into per-session windows (multisport → one activity, one
  segment per session), single-session behavior unchanged;
- validates records: finite/in-range coordinates required, timestamp
  required, non-finite altitude dropped (point kept), prefers
  enhanced_altitude;
- returns { gpx, sport }, mapping FIT session sport/sub-sport to a Journal
  SportType (first session wins), consumed by the Wahoo importer + webhook
  and Garmin importer as a fallback when the provider sends no type.

Tests drive the converter via mocked fit-file-parser output (segmentation,
validation, sport mapping, no-GPS) per the chosen fixtures approach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-07-17 00:03:25 +02:00
parent 1473e2f291
commit 6eea6af673
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
7 changed files with 402 additions and 41 deletions

View file

@ -1,17 +1,17 @@
## 1. Fixtures first
- [ ] 1.1 Collect fixture FIT files under `apps/journal/app/lib/connected-services/__fixtures__/`: a real Wahoo export with a pause, a multisport (multi-session) file, an indoor no-GPS file, and a synthetic file with corrupt records (out-of-range coords, missing timestamps) — reference Endurain's `backend/tests/.../test_utils_fit.py` case list
- [ ] 1.2 Verify what `fit-file-parser` exposes for `events` and `sessions` on each fixture; note device gaps in a code comment
- [x] 1.1 Collect fixture FIT files — adapted: per the chosen approach, cases are driven by mocked `fit-file-parser` output in `fit.test.ts` (pause, multisession, indoor no-GPS, corrupt coords/missing-timestamp) rather than binary files. Real device binaries can be added later without changing the converter.
- [x] 1.2 Parser `events`/`sessions` shapes captured as the `FitEvent`/`FitSession` types + code comments in `fit.ts`, and exercised by the mocked-parser tests.
## 2. Converter
- [ ] 2.1 Segmentation: split on timer stop/start event pairs, fallback on record gaps > 5 min (named constants)
- [ ] 2.2 Session slicing: records windowed per session, one-or-more segments per session, single-session unchanged
- [ ] 2.3 Validation gates: finite/in-range coordinates, timestamp required, non-finite altitude → undefined; prefer `enhanced_altitude`/`enhanced_speed`
- [ ] 2.4 Return `{ gpx, sport }` with the FIT→SportType mapping table; update call sites (Wahoo importer + webhook, Garmin importer) — provider-explicit type wins, FIT sport is fallback
- [ ] 2.5 Unit tests over the fixtures: segment counts, pause exclusion from moving time (via `movingTime` on parsed output), corrupt-record drops, sport mapping, no-GPS returns null gpx
- [x] 2.1 Segmentation: split on timer stop/start event pairs, fallback on record gaps > 5 min (named constants)
- [x] 2.2 Session slicing: records windowed per session, one-or-more segments per session, single-session unchanged
- [x] 2.3 Validation gates: finite/in-range coordinates, timestamp required, non-finite altitude → undefined; prefer `enhanced_altitude` (speed isn't emitted to GPX so its preference is moot)
- [x] 2.4 Return `{ gpx, sport }` with the FIT→SportType mapping table; update call sites (Wahoo importer + webhook, Garmin importer) — provider-explicit type wins, FIT sport is fallback
- [x] 2.5 Unit tests over the fixtures: segment counts (pause becomes a segment boundary, which is what keeps moving time honest), corrupt-record drops, enhanced-altitude preference, sport mapping, no-GPS returns null gpx
## 3. Verification
- [ ] 3.1 Re-run the Wahoo importer test suite; confirm existing single-session imports produce identical stats
- [ ] 3.2 Run `pnpm typecheck && pnpm lint && pnpm test`
- [x] 3.1 Re-run the Wahoo importer test suite; confirm existing single-session imports produce identical stats (14 pass; mock updated to the `{gpx, sport}` shape)
- [x] 3.2 Run `pnpm typecheck && pnpm lint && pnpm test` — all green