From 03f9194903cf82a4bda41891a81212ffe9550983 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 f077dfde..8565eecc 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 62422678..7cc62c02 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 3e69ce55..617b09f7 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 f1141036..4f28e78f 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}