fix: persist and display Volume PRs in workout history
All checks were successful
CI / update (push) Successful in 2m13s

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.
This commit is contained in:
2026-03-24 20:31:17 +01:00
parent f9f8761c7b
commit 81bb3a2428
4 changed files with 22 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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({

View File

@@ -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;

View File

@@ -771,6 +771,7 @@
<span class="pr-type">
{#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}
</span>