Merge pull request #177 from trails-cool/fix/wahoo-import-pass-stats

Pass workout stats through on import
This commit is contained in:
Ullrich Schäfer 2026-04-05 19:06:37 +01:00 committed by GitHub
commit c6949e9490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 27 deletions

View file

@ -10,25 +10,28 @@ export interface ActivityInput {
description?: string;
gpx?: string;
routeId?: string;
distance?: number | null;
duration?: number | null;
startedAt?: Date | null;
}
export async function createActivity(ownerId: string, input: ActivityInput) {
const db = getDb();
const id = randomUUID();
let distance: number | null = null;
let distance: number | null = input.distance ?? null;
let elevationGain: number | null = null;
let elevationLoss: number | null = null;
let startedAt: Date | null = null;
let startedAt: Date | null = input.startedAt ?? null;
const duration: number | null = input.duration ?? null;
if (input.gpx) {
try {
const gpxData = await parseGpxAsync(input.gpx);
distance = gpxData.distance || null;
distance = gpxData.distance || distance;
elevationGain = gpxData.elevation.gain;
elevationLoss = gpxData.elevation.loss;
// Try to extract start time from first track point
if (gpxData.tracks[0]?.[0]?.time) {
if (!startedAt && gpxData.tracks[0]?.[0]?.time) {
startedAt = new Date(gpxData.tracks[0][0].time);
}
} catch {
@ -44,6 +47,7 @@ export async function createActivity(ownerId: string, input: ActivityInput) {
description: input.description ?? "",
gpx: input.gpx,
distance,
duration,
elevationGain,
elevationLoss,
startedAt,

View file

@ -65,6 +65,9 @@ export async function action({ params, request }: Route.ActionArgs) {
const workoutId = formData.get("workoutId") as string;
const workoutName = formData.get("workoutName") as string;
const fileUrl = formData.get("fileUrl") as string;
const startedAt = formData.get("startedAt") as string;
const distance = formData.get("distance") as string;
const duration = formData.get("duration") as string;
// Refresh token if needed
let tokens = {
@ -77,18 +80,9 @@ export async function action({ params, request }: Route.ActionArgs) {
await updateTokens(connection.id, tokens);
}
const workout = {
id: workoutId,
name: workoutName,
type: "",
startedAt: "",
duration: null,
distance: null,
fileUrl: fileUrl || undefined,
};
let gpx: string | null = null;
if (workout.fileUrl) {
if (fileUrl) {
const workout = { id: workoutId, name: workoutName, type: "", startedAt: "", duration: null, distance: null, fileUrl };
const fileBuffer = await provider.downloadFile(tokens, workout);
gpx = await provider.convertToGpx(fileBuffer);
}
@ -96,6 +90,9 @@ export async function action({ params, request }: Route.ActionArgs) {
const activityId = await createActivity(user.id, {
name: workoutName || `${provider.name} workout`,
gpx: gpx ?? undefined,
distance: distance ? parseFloat(distance) : null,
duration: duration ? parseInt(duration) : null,
startedAt: startedAt ? new Date(startedAt) : null,
});
await recordImport(user.id, provider.id, workoutId, activityId);
@ -151,6 +148,9 @@ function WorkoutRow({
<input type="hidden" name="workoutId" value={workout.id} />
<input type="hidden" name="workoutName" value={workout.name} />
<input type="hidden" name="fileUrl" value={workout.fileUrl ?? ""} />
<input type="hidden" name="startedAt" value={workout.startedAt ?? ""} />
<input type="hidden" name="distance" value={workout.distance ?? ""} />
<input type="hidden" name="duration" value={workout.duration ?? ""} />
<button
type="submit"
className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
@ -163,6 +163,17 @@ function WorkoutRow({
);
}
function workoutFormData(w: { id: string; name: string; fileUrl?: string; startedAt: string; distance: number | null; duration: number | null }) {
const fd = new FormData();
fd.set("workoutId", w.id);
fd.set("workoutName", w.name);
fd.set("fileUrl", w.fileUrl ?? "");
fd.set("startedAt", w.startedAt ?? "");
fd.set("distance", w.distance != null ? String(w.distance) : "");
fd.set("duration", w.duration != null ? String(w.duration) : "");
return fd;
}
export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
const { provider, workouts, page, totalPages } = loaderData;
const { t } = useTranslation("journal");
@ -192,11 +203,7 @@ export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
}
const w = importAllRef.current.workouts[nextIndex]!;
const formData = new FormData();
formData.set("workoutId", w.id);
formData.set("workoutName", w.name);
formData.set("fileUrl", w.fileUrl ?? "");
importAllFetcher.submit(formData, { method: "post" });
importAllFetcher.submit(workoutFormData(w), { method: "post" });
}, [importAllActive, importAllFetcher.state, importAllFetcher.data]);
const startImportAll = useCallback(() => {
@ -205,12 +212,7 @@ export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
setImportAllIndex(0);
setImportAllActive(true);
const w = importableWorkouts[0]!;
const formData = new FormData();
formData.set("workoutId", w.id);
formData.set("workoutName", w.name);
formData.set("fileUrl", w.fileUrl ?? "");
importAllFetcher.submit(formData, { method: "post" });
importAllFetcher.submit(workoutFormData(importableWorkouts[0]!), { method: "post" });
}, [importableWorkouts, importAllFetcher]);
return (