Deepen three architectural seams: FIT consolidation, host election extraction, injectable db
- Extract shared fitToGpx into connected-services/fit.ts (FIT is a Garmin open standard used by Wahoo, Coros, Garmin — not provider-specific) - Add importActivity() to sync/imports.server.ts so providers call one function instead of createActivity + recordImport separately; eliminates direct dependency on activities.server.ts from provider adapters - Update wahoo importer + webhook to use both shared helpers - Extract useHostElection(yjs) hook from useRouting so host election is independently testable without mounting the full routing stack - Add setDb() to journal db.ts for module-level injection in unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
48d97be8f1
commit
e387e1f798
8 changed files with 104 additions and 122 deletions
38
apps/journal/app/lib/connected-services/fit.ts
Normal file
38
apps/journal/app/lib/connected-services/fit.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Shared FIT file → GPX converter for provider importers.
|
||||
//
|
||||
// FIT is an open standard (Garmin/ANT+) used by Wahoo, Garmin, Coros, and
|
||||
// others. This module is shared across all providers that produce FIT files
|
||||
// so the conversion logic lives in one place.
|
||||
|
||||
import FitParser from "fit-file-parser";
|
||||
import { generateGpx } from "@trails-cool/gpx";
|
||||
|
||||
export async function fitToGpx(buffer: Buffer, name: string): Promise<string | null> {
|
||||
const parsed = await new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
const parser = new FitParser({ force: true });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
parser.parse(buffer as any, (error: unknown, data: any) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data ?? {});
|
||||
});
|
||||
});
|
||||
|
||||
const records = (parsed.records ?? []) as Array<{
|
||||
position_lat?: number;
|
||||
position_long?: number;
|
||||
altitude?: number;
|
||||
timestamp?: string | Date;
|
||||
}>;
|
||||
|
||||
const trackPoints = records
|
||||
.filter((r) => r.position_lat != null && r.position_long != null)
|
||||
.map((r) => ({
|
||||
lat: r.position_lat!,
|
||||
lon: r.position_long!,
|
||||
ele: r.altitude,
|
||||
time: r.timestamp instanceof Date ? r.timestamp.toISOString() : r.timestamp,
|
||||
}));
|
||||
|
||||
if (trackPoints.length < 2) return null;
|
||||
return generateGpx({ name, tracks: [trackPoints] });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue