Pass workout stats (distance, duration, startedAt) through on import
ActivityInput now accepts optional distance, duration, and startedAt. GPX-derived stats take precedence when available, but provider stats are used as fallback for workouts without GPS data. The import form and Import All now pass these fields from the Wahoo workout data so activities show correct metadata even without a FIT file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b331c2295f
commit
80291ccf6e
2 changed files with 33 additions and 27 deletions
|
|
@ -10,25 +10,28 @@ export interface ActivityInput {
|
||||||
description?: string;
|
description?: string;
|
||||||
gpx?: string;
|
gpx?: string;
|
||||||
routeId?: string;
|
routeId?: string;
|
||||||
|
distance?: number | null;
|
||||||
|
duration?: number | null;
|
||||||
|
startedAt?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createActivity(ownerId: string, input: ActivityInput) {
|
export async function createActivity(ownerId: string, input: ActivityInput) {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
|
|
||||||
let distance: number | null = null;
|
let distance: number | null = input.distance ?? null;
|
||||||
let elevationGain: number | null = null;
|
let elevationGain: number | null = null;
|
||||||
let elevationLoss: number | null = null;
|
let elevationLoss: number | null = null;
|
||||||
let startedAt: Date | null = null;
|
let startedAt: Date | null = input.startedAt ?? null;
|
||||||
|
let duration: number | null = input.duration ?? null;
|
||||||
if (input.gpx) {
|
if (input.gpx) {
|
||||||
try {
|
try {
|
||||||
const gpxData = await parseGpxAsync(input.gpx);
|
const gpxData = await parseGpxAsync(input.gpx);
|
||||||
distance = gpxData.distance || null;
|
distance = gpxData.distance || distance;
|
||||||
elevationGain = gpxData.elevation.gain;
|
elevationGain = gpxData.elevation.gain;
|
||||||
elevationLoss = gpxData.elevation.loss;
|
elevationLoss = gpxData.elevation.loss;
|
||||||
|
|
||||||
// Try to extract start time from first track point
|
if (!startedAt && gpxData.tracks[0]?.[0]?.time) {
|
||||||
if (gpxData.tracks[0]?.[0]?.time) {
|
|
||||||
startedAt = new Date(gpxData.tracks[0][0].time);
|
startedAt = new Date(gpxData.tracks[0][0].time);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -44,6 +47,7 @@ export async function createActivity(ownerId: string, input: ActivityInput) {
|
||||||
description: input.description ?? "",
|
description: input.description ?? "",
|
||||||
gpx: input.gpx,
|
gpx: input.gpx,
|
||||||
distance,
|
distance,
|
||||||
|
duration,
|
||||||
elevationGain,
|
elevationGain,
|
||||||
elevationLoss,
|
elevationLoss,
|
||||||
startedAt,
|
startedAt,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ export async function action({ params, request }: Route.ActionArgs) {
|
||||||
const workoutId = formData.get("workoutId") as string;
|
const workoutId = formData.get("workoutId") as string;
|
||||||
const workoutName = formData.get("workoutName") as string;
|
const workoutName = formData.get("workoutName") as string;
|
||||||
const fileUrl = formData.get("fileUrl") 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
|
// Refresh token if needed
|
||||||
let tokens = {
|
let tokens = {
|
||||||
|
|
@ -77,18 +80,9 @@ export async function action({ params, request }: Route.ActionArgs) {
|
||||||
await updateTokens(connection.id, tokens);
|
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;
|
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);
|
const fileBuffer = await provider.downloadFile(tokens, workout);
|
||||||
gpx = await provider.convertToGpx(fileBuffer);
|
gpx = await provider.convertToGpx(fileBuffer);
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +90,9 @@ export async function action({ params, request }: Route.ActionArgs) {
|
||||||
const activityId = await createActivity(user.id, {
|
const activityId = await createActivity(user.id, {
|
||||||
name: workoutName || `${provider.name} workout`,
|
name: workoutName || `${provider.name} workout`,
|
||||||
gpx: gpx ?? undefined,
|
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);
|
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="workoutId" value={workout.id} />
|
||||||
<input type="hidden" name="workoutName" value={workout.name} />
|
<input type="hidden" name="workoutName" value={workout.name} />
|
||||||
<input type="hidden" name="fileUrl" value={workout.fileUrl ?? ""} />
|
<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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
|
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) {
|
export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
|
||||||
const { provider, workouts, page, totalPages } = loaderData;
|
const { provider, workouts, page, totalPages } = loaderData;
|
||||||
const { t } = useTranslation("journal");
|
const { t } = useTranslation("journal");
|
||||||
|
|
@ -192,11 +203,7 @@ export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = importAllRef.current.workouts[nextIndex]!;
|
const w = importAllRef.current.workouts[nextIndex]!;
|
||||||
const formData = new FormData();
|
importAllFetcher.submit(workoutFormData(w), { method: "post" });
|
||||||
formData.set("workoutId", w.id);
|
|
||||||
formData.set("workoutName", w.name);
|
|
||||||
formData.set("fileUrl", w.fileUrl ?? "");
|
|
||||||
importAllFetcher.submit(formData, { method: "post" });
|
|
||||||
}, [importAllActive, importAllFetcher.state, importAllFetcher.data]);
|
}, [importAllActive, importAllFetcher.state, importAllFetcher.data]);
|
||||||
|
|
||||||
const startImportAll = useCallback(() => {
|
const startImportAll = useCallback(() => {
|
||||||
|
|
@ -205,12 +212,7 @@ export default function SyncImportPage({ loaderData }: Route.ComponentProps) {
|
||||||
setImportAllIndex(0);
|
setImportAllIndex(0);
|
||||||
setImportAllActive(true);
|
setImportAllActive(true);
|
||||||
|
|
||||||
const w = importableWorkouts[0]!;
|
importAllFetcher.submit(workoutFormData(importableWorkouts[0]!), { method: "post" });
|
||||||
const formData = new FormData();
|
|
||||||
formData.set("workoutId", w.id);
|
|
||||||
formData.set("workoutName", w.name);
|
|
||||||
formData.set("fileUrl", w.fileUrl ?? "");
|
|
||||||
importAllFetcher.submit(formData, { method: "post" });
|
|
||||||
}, [importableWorkouts, importAllFetcher]);
|
}, [importableWorkouts, importAllFetcher]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue