trails/openspec/changes/archive/2026-04-04-wahoo-import/tasks.md
Ullrich Schäfer ff93a4cfdd
Archive wahoo-import and journal-route-previews, sync specs
Archive completed changes and sync their delta specs to main:
- wahoo-import: new wahoo-import spec, updated journal-auth and account-settings
- journal-route-previews: new route-preview spec, updated map-display and route-management

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:33:16 +01:00

3.6 KiB

1. Sync Framework & Database

  • 1.1 Add sync_connections table to packages/db/src/schema/journal.ts (user_id FK, provider, access_token, refresh_token, expires_at, provider_user_id, created_at)
  • 1.2 Add sync_imports table (user_id FK, provider, external_workout_id, activity_id FK, imported_at)
  • 1.3 Define SyncProvider TypeScript interface in apps/journal/app/lib/sync/types.ts
  • 1.4 Create provider registry in apps/journal/app/lib/sync/registry.ts (array of providers, lookup by id)
  • 1.5 Create token storage helpers: saveConnection(), getConnection(), deleteConnection(), updateTokens() in apps/journal/app/lib/sync/connections.server.ts
  • 1.6 Create import tracking helpers: recordImport(), isAlreadyImported(), getImportedIds() in apps/journal/app/lib/sync/imports.server.ts
  • 1.7 Add WAHOO_CLIENT_ID and WAHOO_CLIENT_SECRET to infrastructure/secrets.app.env via SOPS
  • 1.8 Run pnpm db:push to apply schema changes

2. Wahoo Provider Implementation

  • 2.1 Create apps/journal/app/lib/sync/providers/wahoo.ts implementing SyncProvider
  • 2.2 Implement getAuthUrl() with scopes workouts_read, user_read, offline_data
  • 2.3 Implement exchangeCode() and refreshToken() using Wahoo's OAuth endpoints
  • 2.4 Implement listWorkouts() with pagination (Wahoo's GET /v1/workouts)
  • 2.5 Implement downloadFile() to fetch FIT file from Wahoo CDN
  • 2.6 Implement parseWebhook() to extract workout info from workout_summary payload

3. FIT → GPX Conversion

  • 3.1 Add fit-file-parser dependency
  • 3.2 Implement convertToGpx() in Wahoo provider: parse FIT, extract track records, convert semicircles to degrees, generate GPX via generateGpx
  • 3.3 Handle indoor workouts (no GPS) — return null GPX, create activity with stats only

4. OAuth Routes

  • 4.1 Create apps/journal/app/routes/api.sync.connect.$provider.ts — generates auth URL from provider, redirects
  • 4.2 Create apps/journal/app/routes/api.sync.callback.$provider.ts — exchanges code, stores tokens, redirects to settings
  • 4.3 Create apps/journal/app/routes/api.sync.disconnect.$provider.ts — deletes connection
  • 4.4 Register routes in apps/journal/app/routes.ts

5. Webhook Route

  • 5.1 Create apps/journal/app/routes/api.sync.webhook.$provider.ts — receives webhook, looks up user, downloads file, converts, creates activity
  • 5.2 Verify webhook by matching provider_user_id to a sync_connection
  • 5.3 Handle duplicate detection via sync_imports
  • 5.4 Register route in routes.ts

6. Import Page

  • 6.1 Create apps/journal/app/routes/sync.import.$provider.tsx — lists workouts with import buttons
  • 6.2 Show workout date, type, duration, distance per row
  • 6.3 Mark already-imported workouts via sync_imports lookup
  • 6.4 Import action: download file, convert, create activity, record import
  • 6.5 Pagination for workout list
  • 6.6 Register route in routes.ts

7. Settings Integration

  • 7.1 Add "Connected Services" section to settings page
  • 7.2 Iterate over registered providers to show connect/disconnect per provider
  • 7.3 Show connection status and link to import page when connected

8. i18n

  • 8.1 Add translation keys for sync UI (en + de): connect/disconnect, import page, webhook status, provider names

9. Testing

  • 9.1 Unit test: FIT→GPX conversion with sample FIT data
  • 9.2 Unit test: duplicate detection via sync_imports
  • 9.3 Unit test: OAuth URL generation and token exchange (mock fetch)
  • 9.4 E2E test: settings page shows Connected Services section