- wahoo-production-cutover: ops checklist for moving the Wahoo Cloud API integration from sandbox to production tier (new app registration, logo upload, forced reauthorization). - wahoo-route-update: switch the route push pipeline from POST-per- version to POST-then-PUT so re-pushing an edited route updates the existing Wahoo route instead of creating a duplicate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.1 KiB
MODIFIED Requirements
Requirement: Server-side route push pipeline
The Journal SHALL convert the current route version to a FIT Course file and send it to Wahoo, recording the outcome in sync_pushes. When no successful push exists for the (user_id, route_id, provider='wahoo') tuple, the request is POST https://api.wahooligan.com/v1/routes. When a successful push exists with a non-null remote_id, the request is PUT https://api.wahooligan.com/v1/routes/<remote_id> so subsequent pushes update the existing Wahoo route in place rather than creating duplicates. If a PUT returns 404 (the user deleted the Wahoo route on their side), the server SHALL fall back to POST and overwrite remote_id with the response. The body shape is identical for POST and PUT and matches the existing field set (external_id, provider_updated_at, name, workout_type_family_id, start_lat, start_lng, distance, ascent, route[file] data URI).
Scenario: First push of a route
- WHEN the route owner triggers the push action and no
sync_pushesrow exists for(user, route, wahoo) - THEN the server reads the route's GPX, runs
gpxToFitCourse, encodes the result asdata:application/vnd.fit;base64,<base64>, and POSTs it to/v1/routes - AND on a 2xx response, a
sync_pushesrow is inserted withpushed_at = now(),remote_idset from Wahoo's response,last_pushed_versionset to the local route version, anderror = null - AND the user is shown a "Sent to Wahoo" confirmation
Scenario: Re-push after editing the route
- WHEN the route owner edits a previously-pushed route, then triggers the push action
- AND a
sync_pushesrow exists for(user, route, wahoo)with non-nullremote_id - THEN the server PUTs the new FIT and metadata to
/v1/routes/<remote_id>(not POST) - AND on a 2xx response, the existing
sync_pushesrow is updated in place:pushed_at = now(),last_pushed_versionadvanced to the new local version,error = null - AND the Wahoo route id remains the same (no duplicate route created on Wahoo's side)
Scenario: PUT against a deleted Wahoo route falls back to POST
- WHEN the server PUTs to
/v1/routes/<remote_id>and Wahoo returns 404 - THEN the server retries the request as POST
/v1/routes - AND the existing
sync_pushesrow is updated with the newremote_idfrom the POST response
Scenario: Wahoo returns an error
- WHEN Wahoo responds with a non-2xx status that is not the 404 fallback
- THEN the
sync_pushesrow is created or updated in place witherrorpopulated andpushed_atunchanged - AND the user is shown a generic "Sending to Wahoo failed — try again later" message
- AND no exception leaks to the browser
Scenario: Route file omits records
- WHEN the route's GPX has no track points (zero-segment route)
- THEN the server returns HTTP 422 to the client and does not call Wahoo
- AND no
sync_pushesrow is created or modified
Requirement: Push idempotency per route
Each (user_id, route_id, provider) SHALL map to at most one sync_pushes row. Re-clicking the button when the local version equals last_pushed_version and the row's pushed_at is non-null SHALL be a no-op surfacing the existing remote id; clicking after editing (local version > last_pushed_version) SHALL trigger an update via PUT.
Scenario: Re-push of an already-pushed unchanged route
- WHEN the route owner clicks "Send to Wahoo" and the existing
sync_pushesrow haslast_pushed_versionequal to the current route version and a non-nullpushed_at - THEN the server skips the Wahoo call and returns the existing
remote_id - AND the UI shows "Already on Wahoo" with the timestamp from the row
Scenario: Push after editing the route (new version)
- WHEN the route owner edits the route (incrementing the local version) and clicks "Send to Wahoo"
- THEN the server takes the PUT path against the existing
remote_id - AND exactly one
sync_pushesrow exists for(user, route, wahoo)after the operation completes
Scenario: Retry after a failed push
- WHEN the existing
sync_pushesrow haspushed_at = nullanderrorpopulated - THEN clicking "Send to Wahoo" retries — POST if
remote_idis null, PUT ifremote_idis non-null - AND the existing row is updated in place (no duplicate row)
Requirement: Stable external_id per route
The external_id field sent to Wahoo SHALL be deterministic per (route_id) so that the value identifies the logical route, not a specific edit of it. This makes the source-of-truth identifier stable across re-pushes.
Scenario: External id is deterministic
- WHEN the server constructs the Wahoo payload for any version of a route
- THEN
external_idis set toroute:<route_id>(no version suffix) - AND subsequent pushes of the same route — including PUTs after edits — send the same
external_id
Requirement: Push status surfaced on the route detail page
The route detail page SHALL show whether the route has been pushed to Wahoo and whether the local version is newer than what Wahoo currently has.
Scenario: Local version matches Wahoo's
- WHEN the route owner views a route whose
sync_pushesrow haslast_pushed_versionequal to the current local version and a non-nullpushed_at - THEN the page shows "Sent to Wahoo on " near the action buttons
- AND the "Send to Wahoo" button is replaced with disabled "Already on Wahoo" text
Scenario: Local version is newer than Wahoo's
- WHEN the route owner views a route whose
last_pushed_versionis less than the current local version - THEN the page shows "On Wahoo (v<last_pushed_version>) — local version is newer"
- AND the action button reads "Send updated version" and triggers the same push action (which will PUT)
Scenario: Push failed previously shows retry affordance
- WHEN the route owner views a route whose
sync_pushesrow haserrorset andpushed_atnull - THEN the page shows "Last attempt failed: " with a "Send to Wahoo" button still active