Archive wahoo-route-update
Fold the wahoo-route-update delta into openspec/specs/wahoo-route-push/spec.md (POST→PUT logic with 404 fallback, stable external_id, push-status UI) and move the change directory to openspec/changes/archive/. Task 4.3 (Playwright E2E) skipped — contract is fully covered by unit/integration tests in wahoo.test.ts and pushes.server.test.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a62b249aed
commit
6f6010cb51
6 changed files with 48 additions and 31 deletions
|
|
@ -26,50 +26,62 @@ The Journal SHALL show a "Send to Wahoo" button on the route detail page when al
|
|||
- **THEN** the "Send to Wahoo" button is not rendered
|
||||
|
||||
### Requirement: Server-side route push pipeline
|
||||
The Journal SHALL convert the current route version to a FIT Course file and POST it to `https://api.wahooligan.com/v1/routes` with the user's stored Wahoo access token, recording the outcome in `sync_pushes`.
|
||||
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: Successful push
|
||||
- **WHEN** the route owner triggers the push action for a route version that has not been pushed before
|
||||
- **THEN** the server reads the route's GPX, runs `gpxToFitCourse`, base64-encodes the result, and POSTs it to `/v1/routes` with the required fields (`external_id`, `provider_updated_at`, `name`, `workout_type_family_id`, `start_lat`, `start_lng`, `distance`, `ascent`)
|
||||
- **AND** on a 2xx response a `sync_pushes` row is inserted with `pushed_at = now()`, `remote_id` set from Wahoo's response, and `error = null`
|
||||
#### Scenario: First push of a route
|
||||
- **WHEN** the route owner triggers the push action and no `sync_pushes` row exists for `(user, route, wahoo)`
|
||||
- **THEN** the server reads the route's GPX, runs `gpxToFitCourse`, encodes the result as `data:application/vnd.fit;base64,<base64>`, and POSTs it to `/v1/routes`
|
||||
- **AND** on a 2xx response, a `sync_pushes` row is inserted with `pushed_at = now()`, `remote_id` set from Wahoo's response, `last_pushed_version` set to the local route version, and `error = 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_pushes` row exists for `(user, route, wahoo)` with non-null `remote_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_pushes` row is updated in place: `pushed_at = now()`, `last_pushed_version` advanced 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_pushes` row is updated with the new `remote_id` from the POST response
|
||||
|
||||
#### Scenario: Wahoo returns an error
|
||||
- **WHEN** Wahoo responds with a non-2xx status
|
||||
- **THEN** a `sync_pushes` row is inserted with `pushed_at = null`, `remote_id = null`, and `error` populated with the response body or status
|
||||
- **WHEN** Wahoo responds with a non-2xx status that is not the 404 fallback
|
||||
- **THEN** the `sync_pushes` row is created or updated in place with `error` populated and `pushed_at` unchanged
|
||||
- **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_pushes` row is created
|
||||
- **AND** no `sync_pushes` row is created or modified
|
||||
|
||||
### Requirement: Push idempotency per route version
|
||||
Each combination of `(user_id, route_id, route_version, provider)` SHALL be pushed at most once successfully. Re-clicking the button for the same version SHALL be a no-op surfacing the existing remote id.
|
||||
### 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 version
|
||||
- **WHEN** the route owner clicks "Send to Wahoo" on a version that already has a `sync_pushes` row with `pushed_at` set
|
||||
#### Scenario: Re-push of an already-pushed unchanged route
|
||||
- **WHEN** the route owner clicks "Send to Wahoo" and the existing `sync_pushes` row has `last_pushed_version` equal to the current route version and a non-null `pushed_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 existing row
|
||||
- **AND** the UI shows "Already on Wahoo" with the timestamp from the row
|
||||
|
||||
#### Scenario: Push of a new version after editing
|
||||
- **WHEN** the route owner edits the route, creating a new version, and clicks "Send to Wahoo"
|
||||
- **THEN** the new version has no `sync_pushes` row yet, so the push proceeds normally
|
||||
- **AND** a new `sync_pushes` row is inserted for the new version
|
||||
#### 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_pushes` row exists for `(user, route, wahoo)` after the operation completes
|
||||
|
||||
#### Scenario: Retry after a failed push
|
||||
- **WHEN** a previous push attempt for this version failed (`pushed_at` null, `error` populated)
|
||||
- **THEN** clicking "Send to Wahoo" again retries the push
|
||||
- **AND** the existing `sync_pushes` row is updated in place (no duplicate row)
|
||||
- **WHEN** the existing `sync_pushes` row has `pushed_at = null` and `error` populated
|
||||
- **THEN** clicking "Send to Wahoo" retries — POST if `remote_id` is null, PUT if `remote_id` is non-null
|
||||
- **AND** the existing row is updated in place (no duplicate row)
|
||||
|
||||
### Requirement: Stable external_id per route version
|
||||
The `external_id` field sent to Wahoo SHALL be deterministic per `(route_id, route_version)` so that retries and accidental races do not create duplicate routes on Wahoo's side.
|
||||
### 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 a route version
|
||||
- **THEN** `external_id` is set to `route:<route_id>:v<route_version>`
|
||||
- **AND** identical calls for the same version produce the same `external_id`
|
||||
- **WHEN** the server constructs the Wahoo payload for any version of a route
|
||||
- **THEN** `external_id` is set to `route:<route_id>` (no version suffix)
|
||||
- **AND** subsequent pushes of the same route — including PUTs after edits — send the same `external_id`
|
||||
|
||||
### Requirement: Re-auth flow when `routes_write` scope is missing
|
||||
The Journal SHALL detect when a connected Wahoo account lacks the `routes_write` scope before calling Wahoo, redirect the user through OAuth to grant it, and resume the push automatically after the user returns.
|
||||
|
|
@ -108,13 +120,18 @@ The system SHALL provide a `gpxToFitCourse` function in the `@trails-cool/fit` p
|
|||
- **THEN** `gpxToFitCourse` throws an error and produces no output
|
||||
|
||||
### Requirement: Push status surfaced on the route detail page
|
||||
The route detail page SHALL show whether the current route version has been pushed to Wahoo, with timestamp.
|
||||
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: Already-pushed route shows status
|
||||
- **WHEN** the route owner views a route version with a successful `sync_pushes` row
|
||||
#### Scenario: Local version matches Wahoo's
|
||||
- **WHEN** the route owner views a route whose `sync_pushes` row has `last_pushed_version` equal to the current local version and a non-null `pushed_at`
|
||||
- **THEN** the page shows "Sent to Wahoo on <date>" 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_version` is 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 version whose latest `sync_pushes` row has `error` set and `pushed_at` null
|
||||
- **WHEN** the route owner views a route whose `sync_pushes` row has `error` set and `pushed_at` null
|
||||
- **THEN** the page shows "Last attempt failed: <short error>" with a "Send to Wahoo" button still active
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue