What We Shipped
Cross-Source Workout Deduplication
The big one today. If you connect both Garmin and Strava, the same run can show up twice — once from each source. That double-counting was silently inflating ATL/CTL/TSB numbers in training load calculations. Not great when you're making taper decisions off those charts.
Two layers of defense landed:
- Read-layer dedup (
deduplicateWorkouts()) — the training load API now detects and collapses duplicate workouts before crunching the numbers. Matches on start time, duration, and sport type across sources. - Post-insert sweep (
sweepPostInsertDuplicates()) — wired into the Garmin upsert, Strava webhook, and Strava sync routes. This catches the race condition where concurrent webhooks from both platforms insert before either sees the other. After insert, it sweeps and removes the dupe.
Also cleaned up 10 existing duplicate workouts in the database. 681 lines added, 10 unit tests covering the dedup logic. Training load numbers should be dead accurate now regardless of how many platforms you've connected.
Export Module Test Coverage
PR #578 brought 73 new tests across two previously untested export modules:
- Intervals Description Exporter — section headers, duration formatting, repeat blocks, distance-to-time conversion, pace/power/HR/ramp targets, plus edge cases like empty workouts and warmup-only sessions.
- ZWO Workout Exporter — XML structure, phase elements, power/pace-to-power/HR mapping, repeat blocks, ramp elements, XML escaping, Base64 encoding, and filename generation.
All 3,529 tests passing. These exporters had zero coverage before — now they're locked down.
Build Config Cleanup
Quick tsconfig.json fix to exclude the render-email scripts from the TypeScript build. Those scripts use Node-specific APIs that don't play nice with the Next.js build target. One-liner, zero drama.
The Takeaway
Data integrity day. Duplicate workouts were a subtle but real problem — the kind of bug where users wouldn't see an error, they'd just see wrong numbers and lose trust in the platform. Squashed it at both the read and write layers, backed by tests. That's how you ship confidence.