From 81bb3a242856fe80c67a4d068e15c79f03bc0e69 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Tue, 24 Mar 2026 20:31:17 +0100 Subject: [PATCH] fix: persist and display Volume PRs in workout history Volume PRs were calculated client-side in the workout summary but never saved to the database, so they didn't appear in history detail pages. Add bestSetVolume PR detection to both session save and recalculate endpoints, and render the new type in the history detail view. --- src/models/WorkoutSession.ts | 2 +- src/routes/api/fitness/sessions/+server.ts | 10 ++++++++++ .../api/fitness/sessions/[id]/recalculate/+server.ts | 10 ++++++++++ .../fitness/[history=fitnessHistory]/[id]/+page.svelte | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/models/WorkoutSession.ts b/src/models/WorkoutSession.ts index f077dfd..8565eec 100644 --- a/src/models/WorkoutSession.ts +++ b/src/models/WorkoutSession.ts @@ -31,7 +31,7 @@ export interface ICompletedExercise { export interface IPr { exerciseId: string; - type: string; // 'est1rm' | 'maxWeight' | 'repMax' + type: string; // 'est1rm' | 'maxWeight' | 'bestSetVolume' | 'repMax' value: number; reps?: number; } diff --git a/src/routes/api/fitness/sessions/+server.ts b/src/routes/api/fitness/sessions/+server.ts index 6242267..7cc62c0 100644 --- a/src/routes/api/fitness/sessions/+server.ts +++ b/src/routes/api/fitness/sessions/+server.ts @@ -100,8 +100,12 @@ export const POST: RequestHandler = async ({ request, locals }) => { 'exercises.exerciseId': ex.exerciseId, }).sort({ startTime: -1 }).limit(50).lean(); + const isBilateral = exercise?.bilateral ?? false; + const weightMul = isBilateral ? 2 : 1; + let prevBestWeight = 0; let prevBestEst1rm = 0; + let prevBestVolume = 0; for (const ps of prevSessions) { const pe = ps.exercises.find((e) => e.exerciseId === ex.exerciseId); if (!pe) continue; @@ -109,15 +113,18 @@ export const POST: RequestHandler = async ({ request, locals }) => { if (!s.completed || !s.weight || !s.reps) continue; prevBestWeight = Math.max(prevBestWeight, s.weight); prevBestEst1rm = Math.max(prevBestEst1rm, estimatedOneRepMax(s.weight, s.reps)); + prevBestVolume = Math.max(prevBestVolume, s.weight * s.reps * weightMul); } } let bestWeight = 0; let bestEst1rm = 0; + let bestVolume = 0; for (const s of completedSets) { if (!s.weight || !s.reps) continue; bestWeight = Math.max(bestWeight, s.weight); bestEst1rm = Math.max(bestEst1rm, estimatedOneRepMax(s.weight, s.reps)); + bestVolume = Math.max(bestVolume, s.weight * s.reps * weightMul); } if (bestWeight > prevBestWeight && prevBestWeight > 0) { @@ -126,6 +133,9 @@ export const POST: RequestHandler = async ({ request, locals }) => { if (bestEst1rm > prevBestEst1rm && prevBestEst1rm > 0) { prs.push({ exerciseId: ex.exerciseId, type: 'est1rm', value: bestEst1rm }); } + if (bestVolume > prevBestVolume && prevBestVolume > 0) { + prs.push({ exerciseId: ex.exerciseId, type: 'bestSetVolume', value: Math.round(bestVolume) }); + } } const workoutSession = new WorkoutSession({ diff --git a/src/routes/api/fitness/sessions/[id]/recalculate/+server.ts b/src/routes/api/fitness/sessions/[id]/recalculate/+server.ts index 3e69ce5..617b09f 100644 --- a/src/routes/api/fitness/sessions/[id]/recalculate/+server.ts +++ b/src/routes/api/fitness/sessions/[id]/recalculate/+server.ts @@ -76,8 +76,12 @@ export const POST: RequestHandler = async ({ params, locals }) => { startTime: { $lt: workoutSession.startTime } }).sort({ startTime: -1 }).limit(50).lean(); + const isBilateral = exercise?.bilateral ?? false; + const weightMul = isBilateral ? 2 : 1; + let prevBestWeight = 0; let prevBestEst1rm = 0; + let prevBestVolume = 0; for (const ps of prevSessions) { const pe = ps.exercises.find(e => e.exerciseId === ex.exerciseId); if (!pe) continue; @@ -85,15 +89,18 @@ export const POST: RequestHandler = async ({ params, locals }) => { if (!s.completed || !s.weight || !s.reps) continue; prevBestWeight = Math.max(prevBestWeight, s.weight); prevBestEst1rm = Math.max(prevBestEst1rm, estimatedOneRepMax(s.weight, s.reps)); + prevBestVolume = Math.max(prevBestVolume, s.weight * s.reps * weightMul); } } let bestWeight = 0; let bestEst1rm = 0; + let bestVolume = 0; for (const s of completedSets) { if (!s.weight || !s.reps) continue; bestWeight = Math.max(bestWeight, s.weight); bestEst1rm = Math.max(bestEst1rm, estimatedOneRepMax(s.weight, s.reps)); + bestVolume = Math.max(bestVolume, s.weight * s.reps * weightMul); } if (bestWeight > prevBestWeight && prevBestWeight > 0) { @@ -102,6 +109,9 @@ export const POST: RequestHandler = async ({ params, locals }) => { if (bestEst1rm > prevBestEst1rm && prevBestEst1rm > 0) { prs.push({ exerciseId: ex.exerciseId, type: 'est1rm', value: bestEst1rm }); } + if (bestVolume > prevBestVolume && prevBestVolume > 0) { + prs.push({ exerciseId: ex.exerciseId, type: 'bestSetVolume', value: Math.round(bestVolume) }); + } } workoutSession.totalVolume = totalVolume > 0 ? totalVolume : undefined; diff --git a/src/routes/fitness/[history=fitnessHistory]/[id]/+page.svelte b/src/routes/fitness/[history=fitnessHistory]/[id]/+page.svelte index f114103..4f28e78 100644 --- a/src/routes/fitness/[history=fitnessHistory]/[id]/+page.svelte +++ b/src/routes/fitness/[history=fitnessHistory]/[id]/+page.svelte @@ -771,6 +771,7 @@ {#if pr.type === 'est1rm'}Est. 1RM {:else if pr.type === 'maxWeight'}Max Weight + {:else if pr.type === 'bestSetVolume'}Best Set Volume {:else if pr.type === 'repMax'}{pr.reps}-rep max {:else}{pr.type}{/if}