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;
|
||||
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;
|
||||
let 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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue