EndurexAI now auto-merges planned endurance workouts with the Strava activities that fulfill them. When a ride, run, or swim syncs from Strava, the backend looks for a planned workout on the same day, in the same sport, with a duration that lines up. If it finds one, it links them. The calendar then shows a single card with the actual performance values on top and the planned targets below, instead of two cards sitting side by side.
Before and after
Before this change, a planned workout and the Strava activity that satisfied it lived as two separate items on the calendar. You read the planned numbers from one card, then jumped your eye over to the activity card to compare. Anyone who has done this on a Monday morning after a Sunday long ride knows the friction.
After this change, the same situation produces one card. The header shows the activity name. The first row holds actual TSS, actual duration, actual distance, and average heart rate. Below the actuals, a smaller “vs plan” row shows the planned TSS and the planned duration. The card keeps the performed treatment from the earlier release: green border, sunken shadow, grayscale fill that clears on hover. Clicking the card jumps straight into the activity detail.
The merged card
Reading the merged card top to bottom: the top row is what the body did, the bottom row is what the plan asked for. The planned description survives, so any notes you left for yourself stay visible. If the planned workout had no description, that line is omitted.
Putting actual values in the dominant position is deliberate. The plan is a hypothesis; the activity is the data. After the fact, the data carries more weight, and the planned targets become reference points rather than the headline.
How the match is decided
The matcher looks at three things. First, the activity’s date has to equal a planned event’s date. Second, the activity’s sport has to map to the workout’s sport. Strava’s Ride, Virtual Ride, Gravel Ride, Mountain Bike Ride, E-Bike Ride, and Handcycle all resolve to bike. Run, Virtual Run, and Trail Run resolve to run. Swim resolves to swim. Third, the planned duration has to sit between half and twice the actual duration.
If exactly one planned workout survives the three checks, it is linked. If several do, the matcher picks the one with the highest compliance index, computed from the actual power trace against the planned target. The reasoning is that compliance already encodes how well the actual session matches a specific target, so it is the right signal to break a tie between several candidates on the same day.
When the activity has no power data, compliance cannot be computed. In that case the matcher falls back to the smallest difference between planned and actual duration. Heart-rate-only runs and pool swims get a sensible link instead of an empty result.
The matcher never overwrites a link you made yourself. A manual merge always wins.
Forward and inverse direction
Two paths cover the two arrival orders. The forward path runs when an activity arrives from Strava and a planned workout is already on the calendar. The inverse path runs when you add a planned workout after the activity has already synced.
The second path is what makes retroactive planning work. If you decide on Tuesday that yesterday’s surprise long ride should sit in your schedule, you can drop a planned event onto Monday, and it picks up the existing activity the moment you save. The backend listens for new endurance events and, when one is created without an existing link, runs the same matching logic in reverse, looking for an unlinked activity that fits.
What stays unmatched on purpose
Activities that fall well outside the planned duration window are left alone. A thirty-minute spin on a day with a planned two-hour endurance ride does not get merged. The two are not the same training in any meaningful sense, and treating them as one would corrupt the comparison between plan and execution.
The same is true at the other end of the range. A four-hour ride on a day with a planned one-hour recovery spin stays separate. Activities outside the half-to-double window appear as their own performed cards on the calendar. The planned workout, if it stays unfulfilled, sits next to them and can still be marked as done so its TSS counts toward the day.
A narrower window would catch more cases but would also start merging sessions that should not be merged. A wider window would shrink the duplicate-card problem further but would erode the integrity of the plan-versus-actual comparison. Half to double sits in a place where almost every honest substitution falls inside and almost every clearly different session falls outside.
Where it lives
The merged card lives on the calendar. No new settings, no toggles, nothing to enable. New activities that sync after the deploy are matched automatically.
Historical activities that synced before the deploy can be matched in bulk through a management command:
manage.py backfill_activity_event_matches --dry-run --user-id=<id>
The dry-run flag prints proposed links without writing anything. Drop the flag to commit. You can also scope by --since=YYYY-MM-DD to bound the window.
Closing
The shape of the calendar has changed slightly. The behavior of the underlying training plan has not. cl, al, and form continue to track the same TSS values they did before. The merged card is a presentation change on top of data that was already there, and the matcher is a small backend service that turns a manual step into an automatic one.
Comments(0)
Sign in to join the conversation.