trails/openspec/changes/wahoo-import/tasks.md
Ullrich Schäfer b6711d23d6
Add Wahoo activity sync with provider-agnostic framework
Provider-agnostic sync framework + Wahoo as first implementation:

Framework (apps/journal/app/lib/sync/):
- SyncProvider interface for OAuth2, webhooks, import, conversion
- Provider registry for settings UI iteration
- Generic sync_connections + sync_imports tables
- Token storage with auto-refresh

Wahoo provider:
- OAuth2 with workouts_read, user_read, offline_data scopes
- Webhook-based auto-import (workout_summary events)
- Manual import page with pagination
- FIT→GPX conversion via fit-file-parser
- Webhook token verification

Routes:
- /api/sync/connect/:provider — OAuth redirect
- /api/sync/callback/:provider — OAuth callback
- /api/sync/disconnect/:provider — remove connection
- /api/sync/webhook/:provider — webhook receiver
- /sync/import/:provider — manual import page

Settings: Connected Services section with per-provider connect/disconnect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:22:02 +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