From c2a52076c71ae841f1f5aab6fadfa1b306601f62 Mon Sep 17 00:00:00 2001 From: Boris 'B' Kurktchiev Date: Wed, 7 Oct 2026 10:51:44 -0400 Subject: [PATCH] fix(workout): editing a warm-up's weight leaves the later warm-ups on their ramp cascadeWeight copied an edited load onto every later undone row of the same phase. That suited warm-ups when "Add warm-up set" cloned the row before it (86ac937d). Since warm-ups ramp (0863aa73, 287550df), each rung halving the gap to the work weight, editing rung 1 of 60/90/105 flattened the ramp to 65/65/65 and every later rung had to be put back by hand. A warm-up edit now stays on its own row, whole-row and one-side alike. A work-set edit still carries through the later inherited work sets and never reaches into the ramp. The phase-only warm-up test flips to the new rule; two new tests cover the built ramp and the one-side case. Co-Authored-By: Claude Opus 5.5 (1M context) --- frontend/src/lib/history.js | 11 +++++++---- frontend/src/lib/history.test.js | 30 ++++++++++++++++++++++++++++-- frontend/src/views/Workout.jsx | 5 +++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/history.js b/frontend/src/lib/history.js index 7247ac0617..30eda02888 100644 --- a/frontend/src/lib/history.js +++ b/frontend/src/lib/history.js @@ -804,18 +804,21 @@ export function streakWeeks(S) { } /** - * Cascade an explicit load edit through later inherited rows in the same phase. + * Cascade an explicit work-set load edit through the later inherited work sets. * * Missing `weightOrigin` is inherited for compatibility with existing sessions. A row or side * marked `manual` is an explicit exception, so it stays put even when it is heavier or lighter. * `side` narrows a per-side edit to one limb; without it both limbs are eligible independently. * Completed rows (and completed limbs) never get rewritten. Clearing an inherited load removes * its `w` key just like a direct edit. + * + * Only a work-set edit cascades, and only onto work sets. Warm-ups are a ramp (buildSets), each + * rung its own load, so editing one rung leaves the rungs after it where they were (setting + * warm-up 1 of a 60/90/105 ramp to 65 used to turn it into 65/65/65). */ export function cascadeWeight(rows, from, value, side) { const source = rows[from] - if (!source) return rows.slice() - const warm = isWarmupRow(source) + if (!source || isWarmupRow(source)) return rows.slice() const sides = isSideSet(source) ? (side ? [side] : ['L', 'R']) : null const next = rows.slice() const setWeight = row => { @@ -832,7 +835,7 @@ export function cascadeWeight(rows, from, value, side) { } for (let j = from + 1; j < next.length; j++) { const row = next[j] - if (isWarmupRow(row) !== warm) continue + if (isWarmupRow(row)) continue if (sides) { if (!isSideSet(row)) continue let out = row diff --git a/frontend/src/lib/history.test.js b/frontend/src/lib/history.test.js index b3f43aeaca..1ecf869692 100644 --- a/frontend/src/lib/history.test.js +++ b/frontend/src/lib/history.test.js @@ -966,6 +966,32 @@ describe('session row helpers', () => { expect(next[1].w).toBe(20) // different flag (warm-up) untouched }) + // Editing warm-up 1 of an incline bench ramp used to copy the new load onto the later + // warm-ups, so each one had to be put back by hand. Each rung keeps its own load. + it('cascadeWeight leaves the later warm-up rungs on their ramp loads', () => { + const S = { unit: 'lb', exWeights: {}, workouts: [] } + const rows = buildSets(S, { id: '0047', mode: 'reps', sets: 4, reps: 8, weight: 125, warmupSets: 3 }, { step: 5 }) + expect(rows.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125]) + const next = cascadeWeight(rows, 0, 70) // the caller writes the edited row itself + expect(next.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125]) + const later = cascadeWeight(rows, 1, 100) + expect(later.map(r => r.w)).toEqual([60, 90, 105, 125, 125, 125, 125]) + // a work-set edit still carries through the work sets and never reaches back into the ramp + expect(cascadeWeight(rows, 3, 130).map(r => r.w)).toEqual([60, 90, 105, 125, 130, 130, 130]) + }) + + it('cascadeWeight leaves later one-side warm-up rungs alone too', () => { + const rows = [ + { ...makeSideSet({ w: 10, r: 16 }), phase: 'warmup' }, + { ...makeSideSet({ w: 15, r: 16 }), phase: 'warmup' }, + makeSideSet({ w: 25, r: 16 }), + ] + const next = cascadeWeight(rows, 0, 5, 'L') + expect(next[1].sides.L.w).toBe(15) + expect(next[1].sides.R.w).toBe(15) + expect(next[2].sides.L.w).toBe(25) + }) + it('cascadeWeight deleting the weight removes the key from following undone rows only', () => { const rows = [ { w: 60, done: true }, @@ -1070,14 +1096,14 @@ describe('warm-up rows identified by phase alone', () => { expect(workSetsDone({ entries: [{ sets: [imported, work] }] })).toBe(1) }) - it('cascadeWeight keeps phase-only warm-ups in their own lane', () => { + it('cascadeWeight treats a phase-only warm-up as a warm-up: the edit stays on its row', () => { const rows = [ { w: 40, r: 10, phase: 'warmup' }, { w: 45, r: 10, phase: 'warmup' }, { w: 100, r: 5 }, ] const next = cascadeWeight(rows, 0, 50) - expect(next[1].w).toBe(50) + expect(next[1].w).toBe(45) expect(next[2].w).toBe(100) }) }) diff --git a/frontend/src/views/Workout.jsx b/frontend/src/views/Workout.jsx index 39a96e3d84..e008b3264c 100644 --- a/frontend/src/views/Workout.jsx +++ b/frontend/src/views/Workout.jsx @@ -1080,8 +1080,9 @@ function ActiveWorkout() { // hold put aside must not outrank what you just typed, or the field would read 45 and the ▶ // would still hold the 30 the row was asking for before. if (field === 'sec') delete e.sets[i].planSec - // Changing a weight cascades to following inherited sets of the same phase, so a correction - // carries through without retyping every row while explicit manual exceptions stay put. + // Changing a work set's weight cascades to the following inherited work sets, so a correction + // carries through without retyping every row while explicit manual exceptions stay put. A + // warm-up edit stays on its own row, and the later rungs keep their ramp loads. if (field === 'w') { e.sets[i].weightOrigin = WEIGHT_ORIGIN_MANUAL e.sets = cascadeWeight(e.sets, i, v)