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>
This commit is contained in:
parent
4c5aacf223
commit
ff93a4cfdd
20 changed files with 138 additions and 107 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-03
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## Context
|
||||
|
||||
The journal stores route geometry as PostGIS LineString (SRID 4326) in the `geom` column. The `@trails-cool/map` package provides `MapView` and `RouteLayer` components built on React Leaflet. Currently these are only used by the planner — the journal has zero map UI.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Show route shape on list pages as small map thumbnails that auto-fit to the route bounds
|
||||
- Show interactive read-only map on detail pages with route overlay
|
||||
- Use existing `@trails-cool/map` components — no new map library
|
||||
- Keep list pages fast (geometry is small as GeoJSON, lazy-load Leaflet)
|
||||
|
||||
**Non-Goals:**
|
||||
- Editable maps in the journal (editing happens in the planner)
|
||||
- Server-side map image generation (static tiles) — use client-side Leaflet for both
|
||||
- Elevation profile on detail pages — future work
|
||||
|
||||
## Decisions
|
||||
|
||||
**GeoJSON from PostGIS:** Use `ST_AsGeoJSON(geom)` in SQL queries to convert geometry to GeoJSON strings. Parse on the server and include in loader data. This avoids sending raw GPX to the client just for rendering.
|
||||
|
||||
**List thumbnails:** Small `MapView` (e.g., 200x150px) with `RouteLayer`, no controls, no interaction (`dragging: false`, `zoomControl: false`). Auto-fit bounds to route with padding. Lazy-loaded via `Suspense` to keep initial page load fast.
|
||||
|
||||
**Detail page map:** Full-width `MapView` with standard controls, auto-fit to route. Same `RouteLayer` component.
|
||||
|
||||
**Fallback for routes without geometry:** Show a placeholder (e.g., muted map icon) when `geom` is null. This handles legacy routes created before the PostGIS fix.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **List page performance:** Many small Leaflet instances could be slow. Mitigate with lazy loading and keeping the component simple (no tile layers until visible via Intersection Observer, or just accept the trade-off for v1).
|
||||
- **Geometry size:** A route with 5000 points produces ~100KB of GeoJSON. For list pages, we could simplify the geometry server-side with `ST_Simplify()` to reduce payload.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
## Why
|
||||
|
||||
The journal's route and activity pages show only text (name, distance, elevation) with no visual representation of the route. Users can't tell routes apart without clicking into each one. Every other route planning app shows a map preview — it's table-stakes UX.
|
||||
|
||||
The PostGIS `geom` column is already populated, and `@trails-cool/map` provides `MapView` + `RouteLayer` components. The infrastructure exists, just needs wiring up.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Route/activity list pages**: Add static map thumbnails with the route drawn, alongside existing stats
|
||||
- **Route/activity detail pages**: Add interactive Leaflet map with the route displayed (read-only, uses `MapView` + `RouteLayer`)
|
||||
- **Server**: Expose route geometry as GeoJSON via `ST_AsGeoJSON()` in loaders
|
||||
- **Shared**: No new packages — reuse `@trails-cool/map`
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `route-preview`: Map previews on journal list and detail pages (static thumbnails on lists, interactive maps on detail)
|
||||
|
||||
### Modified Capabilities
|
||||
- `route-management`: Loaders now return GeoJSON geometry for rendering
|
||||
- `map-display`: `@trails-cool/map` components used in the journal app (previously planner-only)
|
||||
|
||||
## Impact
|
||||
|
||||
- `apps/journal/app/routes/routes._index.tsx` — add map thumbnails to route cards
|
||||
- `apps/journal/app/routes/routes.$id.tsx` — add interactive map to detail page
|
||||
- `apps/journal/app/routes/activities._index.tsx` — add map thumbnails to activity cards
|
||||
- `apps/journal/app/routes/activities.$id.tsx` — add interactive map to detail page
|
||||
- `apps/journal/app/lib/routes.server.ts` — expose GeoJSON from PostGIS
|
||||
- `apps/journal/app/lib/activities.server.ts` — expose GeoJSON from PostGIS
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Map components used in journal app
|
||||
The `@trails-cool/map` package's `MapView` and `RouteLayer` components SHALL be used in the journal app for route previews, in addition to the planner.
|
||||
|
||||
#### Scenario: Journal uses shared map components
|
||||
- **WHEN** the journal renders a route map preview or detail map
|
||||
- **THEN** it uses `MapView` and `RouteLayer` from `@trails-cool/map`
|
||||
- **AND** no map code is duplicated between planner and journal
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Route data includes geometry for rendering
|
||||
Route and activity loaders SHALL return GeoJSON geometry when available.
|
||||
|
||||
#### Scenario: Route list returns simplified geometry
|
||||
- **WHEN** the routes list loader runs
|
||||
- **THEN** each route includes a `geojson` field containing the geometry as a GeoJSON string
|
||||
- **AND** the geometry is simplified server-side via `ST_Simplify()` for list page performance
|
||||
|
||||
#### Scenario: Route detail returns full geometry
|
||||
- **WHEN** the route detail loader runs and the route has geometry
|
||||
- **THEN** the route includes a `geojson` field with the full-resolution GeoJSON geometry
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Route map preview on list pages
|
||||
Route and activity list pages SHALL show a small map thumbnail for each item that has geometry.
|
||||
|
||||
#### Scenario: Route with geometry
|
||||
- **WHEN** the routes list page loads and a route has a `geom` column
|
||||
- **THEN** a small map thumbnail is rendered showing the route path
|
||||
- **AND** the map auto-fits to the route bounds
|
||||
|
||||
#### Scenario: Route without geometry
|
||||
- **WHEN** a route has no `geom` (legacy route)
|
||||
- **THEN** a placeholder is shown instead of a map thumbnail
|
||||
|
||||
#### Scenario: Activity with geometry
|
||||
- **WHEN** the activities list page loads and an activity has a `geom` column
|
||||
- **THEN** a small map thumbnail is rendered showing the activity path
|
||||
|
||||
### Requirement: Interactive map on detail pages
|
||||
Route and activity detail pages SHALL show an interactive read-only map with the route/activity drawn.
|
||||
|
||||
#### Scenario: Route detail with geometry
|
||||
- **WHEN** a user views a route detail page and the route has geometry
|
||||
- **THEN** a full-width interactive map is shown with the route path
|
||||
- **AND** the map has zoom controls and layer switching
|
||||
- **AND** the map auto-fits to the route bounds
|
||||
|
||||
#### Scenario: Activity detail with geometry
|
||||
- **WHEN** a user views an activity detail page and the activity has geometry
|
||||
- **THEN** a full-width interactive map is shown with the activity path
|
||||
|
||||
#### Scenario: Detail page without geometry
|
||||
- **WHEN** a route or activity has no geometry
|
||||
- **THEN** no map section is rendered
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
## 1. Server — Expose GeoJSON
|
||||
|
||||
- [x] 1.1 Add `geojsonFromGeom()` helper using `ST_AsGeoJSON(geom)` to convert PostGIS geometry to GeoJSON string
|
||||
- [x] 1.2 Update `listRoutes()` to return simplified GeoJSON per route via `ST_AsGeoJSON(ST_Simplify(geom, 0.001))`
|
||||
- [x] 1.3 Update `getRoute()` to return full-resolution GeoJSON
|
||||
- [x] 1.4 Update `listActivities()` to return simplified GeoJSON per activity
|
||||
- [x] 1.5 Update `getActivity()` to return full-resolution GeoJSON
|
||||
|
||||
## 2. Route List Page — Map Thumbnails
|
||||
|
||||
- [x] 2.1 Create `RouteMapThumbnail` component: small MapView + RouteLayer, no controls, auto-fit bounds
|
||||
- [x] 2.2 Add thumbnail to each route card in `routes._index.tsx` (lazy-loaded via Suspense)
|
||||
- [x] 2.3 Show placeholder when route has no geometry
|
||||
|
||||
## 3. Activity List Page — Map Thumbnails
|
||||
|
||||
- [x] 3.1 Add thumbnail to each activity card in `activities._index.tsx` (reuse `RouteMapThumbnail`)
|
||||
- [x] 3.2 Show placeholder when activity has no geometry
|
||||
|
||||
## 4. Route Detail Page — Interactive Map
|
||||
|
||||
- [x] 4.1 Add full-width MapView + RouteLayer to `routes.$id.tsx`, auto-fit bounds
|
||||
- [x] 4.2 Skip map section when route has no geometry
|
||||
|
||||
## 5. Activity Detail Page — Interactive Map
|
||||
|
||||
- [x] 5.1 Add full-width MapView + RouteLayer to `activities.$id.tsx`, auto-fit bounds
|
||||
- [x] 5.2 Skip map section when activity has no geometry
|
||||
|
||||
## 6. i18n
|
||||
|
||||
- [x] 6.1 Add translation keys for map placeholder text (en + de)
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- [x] 7.1 E2E: route detail page shows map when route has geometry
|
||||
- [x] 7.2 E2E: route list page renders without errors
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-04
|
||||
103
openspec/changes/archive/2026-04-04-wahoo-import/design.md
Normal file
103
openspec/changes/archive/2026-04-04-wahoo-import/design.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
## Context
|
||||
|
||||
Wahoo's Cloud API (https://cloud-api.wahooligan.com/) uses OAuth2 for authentication, provides workout data in FIT format, and supports webhooks for the `workout_summary` event. This is the first device integration — Garmin, Strava, and others will follow. The architecture must be provider-agnostic.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Provider-agnostic sync framework: common interface for OAuth2, webhooks, activity import
|
||||
- Wahoo as first provider implementation
|
||||
- Webhook-based automatic sync (new activities arrive without user action)
|
||||
- Manual import for historical workouts
|
||||
- FIT→GPX conversion for Wahoo's binary format
|
||||
- Extensible to Garmin, Strava, Coros, etc.
|
||||
|
||||
**Non-Goals:**
|
||||
- Two-way sync (no pushing data to providers)
|
||||
- Importing structured workout plans or power zones
|
||||
- Real-time streaming of in-progress activities
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Provider interface
|
||||
|
||||
```typescript
|
||||
interface SyncProvider {
|
||||
id: string; // "wahoo", "garmin", etc.
|
||||
name: string; // "Wahoo"
|
||||
scopes: string[]; // OAuth scopes needed
|
||||
|
||||
getAuthUrl(state: string): string;
|
||||
exchangeCode(code: string): Promise<TokenSet>;
|
||||
refreshToken(refreshToken: string): Promise<TokenSet>;
|
||||
|
||||
listWorkouts(tokens: TokenSet, page: number): Promise<WorkoutList>;
|
||||
downloadFile(tokens: TokenSet, workout: Workout): Promise<Buffer>;
|
||||
convertToGpx(fileBuffer: Buffer): Promise<string>;
|
||||
|
||||
parseWebhook(body: unknown): WebhookEvent | null;
|
||||
}
|
||||
```
|
||||
|
||||
Each provider implements this interface. The framework handles OAuth flow, token storage, webhook routing, and activity creation. Adding a new provider means implementing one file.
|
||||
|
||||
### D2: Database schema
|
||||
|
||||
**`sync_connections`** — one row per user-provider pair:
|
||||
- `id`, `user_id` (FK), `provider` (e.g., "wahoo"), `access_token`, `refresh_token`, `expires_at`, `provider_user_id`, `created_at`
|
||||
|
||||
**`sync_imports`** — tracks which external workouts have been imported:
|
||||
- `id`, `user_id` (FK), `provider`, `external_workout_id`, `activity_id` (FK to activities), `imported_at`
|
||||
|
||||
This replaces the earlier `wahoo_tokens` table with a generic schema. No `wahoo_workout_id` column on activities — the `sync_imports` junction table handles duplicate detection for all providers.
|
||||
|
||||
### D3: Webhook sync
|
||||
|
||||
Wahoo sends `workout_summary` webhooks to a registered URL when a workout completes. Requires the `offline_data` OAuth scope.
|
||||
|
||||
**Webhook endpoint:** `POST /api/sync/webhook/wahoo`
|
||||
- Verifies the request is from Wahoo (check payload structure)
|
||||
- Looks up the user's `sync_connection` by `provider_user_id`
|
||||
- Downloads the FIT file, converts to GPX, creates activity
|
||||
- Stores import record in `sync_imports`
|
||||
|
||||
**Webhook registration:** Done via Wahoo's app settings dashboard (not API). The URL is `{ORIGIN}/api/sync/webhook/wahoo`.
|
||||
|
||||
### D4: OAuth2 flow
|
||||
|
||||
Standard authorization code flow. Scopes: `workouts_read`, `user_read`, `offline_data` (for webhooks).
|
||||
|
||||
**Routes:**
|
||||
- `GET /api/sync/connect/wahoo` → redirect to Wahoo auth
|
||||
- `GET /api/sync/callback/wahoo` → exchange code, store tokens, redirect to settings
|
||||
- `POST /api/sync/disconnect/wahoo` → delete tokens
|
||||
|
||||
The route pattern `/api/sync/{action}/{provider}` is provider-agnostic — adding Garmin means the same routes with `garmin` instead of `wahoo`.
|
||||
|
||||
### D5: FIT→GPX conversion
|
||||
|
||||
Use `fit-file-parser` to parse FIT binary data. Extract records with `record_type === 'record'` containing `position_lat`, `position_long`, `altitude`, `timestamp`. FIT uses semicircles for coordinates — convert to degrees by dividing by `2^31 / 180`.
|
||||
|
||||
Convert to GPX using the existing `generateGpx` function, then feed through `setGeomFromGpx` for PostGIS geometry.
|
||||
|
||||
### D6: Manual import page
|
||||
|
||||
`/sync/import/wahoo` — lists workouts from Wahoo API with date, type, duration, distance. Marks already-imported ones via `sync_imports` lookup. Import button downloads FIT, converts, creates activity. Paginated (Wahoo returns 30 per page).
|
||||
|
||||
### D7: Settings integration
|
||||
|
||||
"Connected Services" section in settings. Shows each configured provider with connect/disconnect. When connected, shows provider user info and link to import page.
|
||||
|
||||
Provider registry:
|
||||
```typescript
|
||||
const providers = [wahooProvider]; // Add garminProvider, stravaProvider later
|
||||
```
|
||||
|
||||
Settings iterates over registered providers to render the UI.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Webhook reliability:** If the webhook fails, the workout is missed. Mitigated by the manual import page as fallback, and idempotent import (duplicate detection via `sync_imports`).
|
||||
- **FIT parsing:** Binary format parsing via npm package. If it fails for edge cases, the webhook silently drops that workout. Logging + manual import as fallback.
|
||||
- **Provider-specific quirks:** Each provider has different OAuth flows, data formats, and webhook schemas. The interface abstracts the common pattern, but implementation details will vary. Accept some provider-specific code in each implementation file.
|
||||
- **Webhook security:** Wahoo doesn't sign webhooks with HMAC. Verify by checking the `provider_user_id` in the payload matches a known `sync_connection`. This prevents arbitrary POST requests from creating activities.
|
||||
32
openspec/changes/archive/2026-04-04-wahoo-import/proposal.md
Normal file
32
openspec/changes/archive/2026-04-04-wahoo-import/proposal.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
## Why
|
||||
|
||||
Users with Wahoo cycling computers (ELEMNT, KICKR) record activities that are synced to Wahoo's cloud. Currently there's no way to get those activities into trails.cool without manually exporting GPX files. A direct Wahoo integration lets users connect their account once and have activities sync automatically.
|
||||
|
||||
This is the first of several planned device integrations (Garmin, Strava, Coros, etc.). The architecture should be provider-agnostic so adding new integrations is straightforward.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Provider abstraction**: A common interface for activity sync providers (OAuth2, webhook handling, activity listing, file download, format conversion). Wahoo is the first implementation.
|
||||
- **OAuth2 flow**: "Connect Wahoo" button on the journal settings page. Redirects to Wahoo's authorization endpoint, handles callback, stores tokens.
|
||||
- **Webhook sync**: Register for Wahoo's `workout_summary` webhook. When a new workout completes, automatically download the FIT file, convert to GPX, and create an activity.
|
||||
- **Manual import**: Fallback import page for browsing and selectively importing older workouts.
|
||||
- **FIT to GPX conversion**: Wahoo provides FIT format files. Convert to GPX server-side for storage.
|
||||
- **Token management**: Store and refresh OAuth tokens per provider. Handle the 2-hour expiry with automatic refresh.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `activity-sync`: Provider-agnostic activity sync framework (OAuth2, webhooks, import, format conversion)
|
||||
- `wahoo-import`: Wahoo-specific provider implementation (OAuth2 scopes, FIT files, webhook payload)
|
||||
|
||||
### Modified Capabilities
|
||||
- `account-settings`: Add "Connected Services" section for managing provider connections
|
||||
- `journal-auth`: Store OAuth tokens for external providers
|
||||
|
||||
## Impact
|
||||
|
||||
- `apps/journal/app/lib/sync/` — new directory for sync framework + provider implementations
|
||||
- `apps/journal/app/routes/settings.tsx` — Connected Services section
|
||||
- `apps/journal/app/routes/sync.*` — OAuth callback, webhook endpoint, import page
|
||||
- `packages/db/src/schema/journal.ts` — `sync_connections` and `sync_imports` tables
|
||||
- `infrastructure/secrets.app.env` — WAHOO_CLIENT_ID, WAHOO_CLIENT_SECRET
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Connected Services section
|
||||
The settings page SHALL include a "Connected Services" section for managing external integrations.
|
||||
|
||||
#### Scenario: Wahoo connection status
|
||||
- **WHEN** a user views the settings page
|
||||
- **THEN** a "Connected Services" section shows Wahoo as connected or disconnected
|
||||
- **AND** connected state shows "Disconnect" button
|
||||
- **AND** disconnected state shows "Connect Wahoo" button
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Store external service tokens
|
||||
The journal auth system SHALL store OAuth tokens for external services alongside user credentials.
|
||||
|
||||
#### Scenario: Wahoo token storage
|
||||
- **WHEN** a user connects their Wahoo account
|
||||
- **THEN** access token, refresh token, expiry time, and Wahoo user ID are stored in the `wahoo_tokens` table
|
||||
- **AND** tokens are associated with the journal user ID
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Provider-agnostic sync framework
|
||||
The system SHALL provide a common interface for external activity sync providers.
|
||||
|
||||
#### Scenario: Add new provider
|
||||
- **WHEN** a developer wants to add a new sync provider (e.g., Garmin)
|
||||
- **THEN** they implement the `SyncProvider` interface in a single file
|
||||
- **AND** register it in the provider registry
|
||||
- **AND** all OAuth, webhook, import, and settings UI works automatically
|
||||
|
||||
### Requirement: Connect Wahoo account
|
||||
Users SHALL be able to connect their Wahoo account via OAuth2.
|
||||
|
||||
#### Scenario: Connect Wahoo
|
||||
- **WHEN** a user clicks "Connect Wahoo" in journal settings
|
||||
- **THEN** they are redirected to Wahoo's OAuth authorization page with scopes `workouts_read`, `user_read`, `offline_data`
|
||||
- **AND** after granting permission, redirected back to the journal
|
||||
- **AND** access and refresh tokens are stored in `sync_connections`
|
||||
|
||||
#### Scenario: Disconnect Wahoo
|
||||
- **WHEN** a user clicks "Disconnect" next to their Wahoo connection
|
||||
- **THEN** the stored tokens are deleted from `sync_connections`
|
||||
|
||||
#### Scenario: Token refresh
|
||||
- **WHEN** a Wahoo API call fails with an expired token
|
||||
- **THEN** the refresh token is used to obtain a new access token automatically
|
||||
|
||||
### Requirement: Webhook-based automatic sync
|
||||
New Wahoo workouts SHALL be automatically imported when they complete.
|
||||
|
||||
#### Scenario: Webhook receives new workout
|
||||
- **WHEN** Wahoo sends a `workout_summary` webhook to `/api/sync/webhook/wahoo`
|
||||
- **THEN** the system identifies the user via `provider_user_id`
|
||||
- **AND** downloads the FIT file from Wahoo's CDN
|
||||
- **AND** converts it to GPX
|
||||
- **AND** creates a journal activity with the GPX, stats, and PostGIS geometry
|
||||
- **AND** records the import in `sync_imports` to prevent duplicates
|
||||
|
||||
#### Scenario: Duplicate webhook
|
||||
- **WHEN** a webhook arrives for a workout already imported
|
||||
- **THEN** the import is skipped silently (idempotent)
|
||||
|
||||
#### Scenario: Unknown user webhook
|
||||
- **WHEN** a webhook arrives with a `provider_user_id` not matching any connection
|
||||
- **THEN** the request is ignored with a 200 response (don't reveal user existence)
|
||||
|
||||
### Requirement: Manual import
|
||||
Users SHALL be able to browse and selectively import older Wahoo workouts.
|
||||
|
||||
#### Scenario: View workout list
|
||||
- **WHEN** a user visits the Wahoo import page
|
||||
- **THEN** their Wahoo workouts are listed with date, type, duration, and distance
|
||||
- **AND** already-imported workouts are marked
|
||||
|
||||
#### Scenario: Import workout
|
||||
- **WHEN** a user clicks "Import" on a Wahoo workout
|
||||
- **THEN** the FIT file is downloaded, converted to GPX, and a new activity is created
|
||||
|
||||
### Requirement: FIT to GPX conversion
|
||||
The system SHALL convert Wahoo's FIT binary files to GPX format.
|
||||
|
||||
#### Scenario: Convert FIT with GPS data
|
||||
- **WHEN** a FIT file contains GPS track records
|
||||
- **THEN** track points with lat, lon, elevation, and timestamp are extracted
|
||||
- **AND** a valid GPX string is produced using `generateGpx`
|
||||
|
||||
#### Scenario: FIT without GPS data
|
||||
- **WHEN** a FIT file has no GPS records (e.g., indoor trainer workout)
|
||||
- **THEN** the activity is created without GPX or geometry (stats only)
|
||||
65
openspec/changes/archive/2026-04-04-wahoo-import/tasks.md
Normal file
65
openspec/changes/archive/2026-04-04-wahoo-import/tasks.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
## 1. Sync Framework & Database
|
||||
|
||||
- [x] 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)
|
||||
- [x] 1.2 Add `sync_imports` table (user_id FK, provider, external_workout_id, activity_id FK, imported_at)
|
||||
- [x] 1.3 Define `SyncProvider` TypeScript interface in `apps/journal/app/lib/sync/types.ts`
|
||||
- [x] 1.4 Create provider registry in `apps/journal/app/lib/sync/registry.ts` (array of providers, lookup by id)
|
||||
- [x] 1.5 Create token storage helpers: `saveConnection()`, `getConnection()`, `deleteConnection()`, `updateTokens()` in `apps/journal/app/lib/sync/connections.server.ts`
|
||||
- [x] 1.6 Create import tracking helpers: `recordImport()`, `isAlreadyImported()`, `getImportedIds()` in `apps/journal/app/lib/sync/imports.server.ts`
|
||||
- [x] 1.7 Add WAHOO_CLIENT_ID and WAHOO_CLIENT_SECRET to `infrastructure/secrets.app.env` via SOPS
|
||||
- [x] 1.8 Run `pnpm db:push` to apply schema changes
|
||||
|
||||
## 2. Wahoo Provider Implementation
|
||||
|
||||
- [x] 2.1 Create `apps/journal/app/lib/sync/providers/wahoo.ts` implementing `SyncProvider`
|
||||
- [x] 2.2 Implement `getAuthUrl()` with scopes `workouts_read`, `user_read`, `offline_data`
|
||||
- [x] 2.3 Implement `exchangeCode()` and `refreshToken()` using Wahoo's OAuth endpoints
|
||||
- [x] 2.4 Implement `listWorkouts()` with pagination (Wahoo's `GET /v1/workouts`)
|
||||
- [x] 2.5 Implement `downloadFile()` to fetch FIT file from Wahoo CDN
|
||||
- [x] 2.6 Implement `parseWebhook()` to extract workout info from `workout_summary` payload
|
||||
|
||||
## 3. FIT → GPX Conversion
|
||||
|
||||
- [x] 3.1 Add `fit-file-parser` dependency
|
||||
- [x] 3.2 Implement `convertToGpx()` in Wahoo provider: parse FIT, extract track records, convert semicircles to degrees, generate GPX via `generateGpx`
|
||||
- [x] 3.3 Handle indoor workouts (no GPS) — return null GPX, create activity with stats only
|
||||
|
||||
## 4. OAuth Routes
|
||||
|
||||
- [x] 4.1 Create `apps/journal/app/routes/api.sync.connect.$provider.ts` — generates auth URL from provider, redirects
|
||||
- [x] 4.2 Create `apps/journal/app/routes/api.sync.callback.$provider.ts` — exchanges code, stores tokens, redirects to settings
|
||||
- [x] 4.3 Create `apps/journal/app/routes/api.sync.disconnect.$provider.ts` — deletes connection
|
||||
- [x] 4.4 Register routes in `apps/journal/app/routes.ts`
|
||||
|
||||
## 5. Webhook Route
|
||||
|
||||
- [x] 5.1 Create `apps/journal/app/routes/api.sync.webhook.$provider.ts` — receives webhook, looks up user, downloads file, converts, creates activity
|
||||
- [x] 5.2 Verify webhook by matching `provider_user_id` to a `sync_connection`
|
||||
- [x] 5.3 Handle duplicate detection via `sync_imports`
|
||||
- [x] 5.4 Register route in `routes.ts`
|
||||
|
||||
## 6. Import Page
|
||||
|
||||
- [x] 6.1 Create `apps/journal/app/routes/sync.import.$provider.tsx` — lists workouts with import buttons
|
||||
- [x] 6.2 Show workout date, type, duration, distance per row
|
||||
- [x] 6.3 Mark already-imported workouts via `sync_imports` lookup
|
||||
- [x] 6.4 Import action: download file, convert, create activity, record import
|
||||
- [x] 6.5 Pagination for workout list
|
||||
- [x] 6.6 Register route in `routes.ts`
|
||||
|
||||
## 7. Settings Integration
|
||||
|
||||
- [x] 7.1 Add "Connected Services" section to settings page
|
||||
- [x] 7.2 Iterate over registered providers to show connect/disconnect per provider
|
||||
- [x] 7.3 Show connection status and link to import page when connected
|
||||
|
||||
## 8. i18n
|
||||
|
||||
- [x] 8.1 Add translation keys for sync UI (en + de): connect/disconnect, import page, webhook status, provider names
|
||||
|
||||
## 9. Testing
|
||||
|
||||
- [x] 9.1 Unit test: FIT→GPX conversion with sample FIT data
|
||||
- [x] 9.2 Unit test: duplicate detection via sync_imports
|
||||
- [x] 9.3 Unit test: OAuth URL generation and token exchange (mock fetch)
|
||||
- [x] 9.4 E2E test: settings page shows Connected Services section
|
||||
Loading…
Add table
Add a link
Reference in a new issue