From 20b91ef511495116ffcba82143d71ae9b3f5ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 5 Apr 2026 17:47:48 +0200 Subject: [PATCH] Fix Wahoo import bugs, add Import All, update spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugs fixed: - FIT parser returns timestamps as Date objects, not strings — convert to ISO 8601 before passing to generateGpx (caused str.replace crash) - FIT parser already converts semicircles to degrees — remove redundant conversion that produced near-zero coordinates - Wahoo CDN URLs are pre-signed S3 URLs — remove Bearer auth header from download requests (caused 400 "Unsupported Authorization Type") - Filter out third-party workouts (fitness_app_id >= 1000) since Wahoo does not share their data via the API UX improvements: - Individual imports use fetcher (no page refresh), button shows "Importing..." inline - "Import all" button imports all unimported workouts on the page sequentially with progress indicator - Add @vitejs/plugin-basic-ssl for local HTTPS dev (opt-in via HTTPS=1) Also adds unit test for FIT-to-GPX conversion with real fixture file, and updates wahoo-import spec to reflect all behaviors. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../providers/__fixtures__/wahoo-ride.fit | Bin 0 -> 34734 bytes .../app/lib/sync/providers/wahoo.test.ts | 63 +++++++ apps/journal/app/lib/sync/providers/wahoo.ts | 21 ++- .../app/routes/sync.import.$provider.tsx | 171 +++++++++++++----- apps/journal/package.json | 1 + apps/journal/vite.config.ts | 1 + openspec/specs/wahoo-import/spec.md | 40 +++- packages/i18n/src/locales/de.ts | 3 + packages/i18n/src/locales/en.ts | 3 + pnpm-lock.yaml | 13 ++ 10 files changed, 258 insertions(+), 58 deletions(-) create mode 100644 apps/journal/app/lib/sync/providers/__fixtures__/wahoo-ride.fit create mode 100644 apps/journal/app/lib/sync/providers/wahoo.test.ts diff --git a/apps/journal/app/lib/sync/providers/__fixtures__/wahoo-ride.fit b/apps/journal/app/lib/sync/providers/__fixtures__/wahoo-ride.fit new file mode 100644 index 0000000000000000000000000000000000000000..94e96ef0fafeb46c8da9545a9b8a50792e76b8d4 GIT binary patch literal 34734 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 OZ->2lWmlD0`~Lu3eFd}t literal 0 HcmV?d00001 diff --git a/apps/journal/app/lib/sync/providers/wahoo.test.ts b/apps/journal/app/lib/sync/providers/wahoo.test.ts new file mode 100644 index 0000000..412ff05 --- /dev/null +++ b/apps/journal/app/lib/sync/providers/wahoo.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, it, expect } from "vitest"; +import { wahooProvider } from "./wahoo"; + +const fixturePath = resolve(__dirname, "__fixtures__/wahoo-ride.fit"); +const fitBuffer = readFileSync(fixturePath); + +describe("wahooProvider.convertToGpx", () => { + it("converts a FIT file to valid GPX with correct coordinates", async () => { + const gpx = await wahooProvider.convertToGpx(Buffer.from(fitBuffer)); + + expect(gpx).not.toBeNull(); + expect(gpx).toContain(' { + const gpx = await wahooProvider.convertToGpx(Buffer.from(fitBuffer)); + expect(gpx).not.toBeNull(); + + const latMatches = gpx!.matchAll(/lat="([^"]+)"/g); + const lonMatches = gpx!.matchAll(/lon="([^"]+)"/g); + + const lats = [...latMatches].map((m) => parseFloat(m[1]!)); + const lons = [...lonMatches].map((m) => parseFloat(m[1]!)); + + expect(lats.length).toBeGreaterThan(10); + expect(lons.length).toBeGreaterThan(10); + + for (const lat of lats) { + expect(lat).toBeGreaterThan(-90); + expect(lat).toBeLessThan(90); + // Should be real-world coords, not near-zero from double-conversion + expect(Math.abs(lat)).toBeGreaterThan(1); + } + for (const lon of lons) { + expect(lon).toBeGreaterThan(-180); + expect(lon).toBeLessThan(180); + } + }); + + it("includes ISO 8601 timestamps (not Date objects)", async () => { + const gpx = await wahooProvider.convertToGpx(Buffer.from(fitBuffer)); + expect(gpx).not.toBeNull(); + + const timeMatches = gpx!.matchAll(/