From eb776336ea9b2cdcd82a1133b2b9ef23d4ce3005 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 18:23:37 +0000 Subject: [PATCH 1/3] GymLog: tier PR border by overall (red) / phase (yellow) / rep (green) --- src/App.css | 48 +++++++++++++++++++--------- src/App.jsx | 1 + src/tabs/GymLog.jsx | 77 ++++++++++++++++++++++++++------------------- 3 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/App.css b/src/App.css index 07f3c35..9355b0a 100644 --- a/src/App.css +++ b/src/App.css @@ -22,6 +22,8 @@ --acc-amber-bg: linear-gradient(135deg, #4a3a2a 0%, #5a4020 100%); --acc-blue-bg: linear-gradient(135deg, #2a3a4a 0%, #203050 100%); --acc-green-bg: #1e3a2e; + --acc-green-bg-grad: linear-gradient(135deg, #1e3a2e 0%, #194d2e 100%); + --acc-red-bg: linear-gradient(135deg, #4a2a32 0%, #5a2030 100%); --acc-blue-flat: #2a3a4a; /* Catppuccin Mocha accents (vibrant on dark). */ --c-blue: #89b4fa; @@ -69,6 +71,8 @@ --acc-amber-bg: linear-gradient(135deg, #fef3d4 0%, #fce5a4 100%); --acc-blue-bg: linear-gradient(135deg, #dde7ff 0%, #c4d4ff 100%); --acc-green-bg: #dff2e1; + --acc-green-bg-grad: linear-gradient(135deg, #dff2e1 0%, #c5ecd0 100%); + --acc-red-bg: linear-gradient(135deg, #fbd5dd 0%, #f7b8c5 100%); --acc-blue-flat: #dde7ff; /* Catppuccin Latte accents (saturated, darker — readable on light bg). */ --c-blue: #1e66f5; @@ -424,14 +428,21 @@ body:has(textarea:focus) .navbar { .pr-info > * { flex-shrink: 0; } .pr-info .last-value { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } -.pr-info.new-weight-pr { +/* Tiered PR styling — red = all-time, yellow = current phase, green = same + weight more reps. */ +.pr-info.new-overall-pr { + background: var(--acc-red-bg); + border-color: var(--c-red); +} + +.pr-info.new-phase-pr { background: var(--acc-amber-bg); border-color: var(--c-yellow); } .pr-info.new-rep-pr { - background: var(--acc-blue-bg); - border-color: var(--c-blue); + background: var(--acc-green-bg-grad); + border-color: var(--c-green); } .pr-label { @@ -444,10 +455,9 @@ body:has(textarea:focus) .navbar { letter-spacing: 0.3px; } -.pr-info.new-weight-pr .pr-label, -.pr-info.new-rep-pr .pr-label { - background: var(--c-green); -} +.pr-info.new-overall-pr .pr-label { background: var(--c-red); } +.pr-info.new-phase-pr .pr-label { background: var(--c-yellow); } +.pr-info.new-rep-pr .pr-label { background: var(--c-green); } .pr-value { color: var(--c-yellow); @@ -510,24 +520,34 @@ body:has(textarea:focus) .navbar { color: var(--text); } -/* Weight PR: gold styling (overrides committed) */ -.set-row.weight-pr { +/* Overall (all-time) PR: red styling (overrides committed) */ +.set-row.overall-pr { + background: var(--acc-red-bg); + border-color: var(--c-red); +} + +.set-row.overall-pr .set-label { + color: var(--c-red); +} + +/* Phase PR: gold styling (overrides committed) */ +.set-row.phase-pr { background: var(--acc-amber-bg); border-color: var(--c-yellow); } -.set-row.weight-pr .set-label { +.set-row.phase-pr .set-label { color: var(--c-yellow); } -/* Rep PR: blue styling (overrides committed) */ +/* Rep PR (same weight, more reps): green styling (overrides committed) */ .set-row.rep-pr { - background: var(--acc-blue-bg); - border-color: var(--c-blue); + background: var(--acc-green-bg-grad); + border-color: var(--c-green); } .set-row.rep-pr .set-label { - color: var(--c-blue); + color: var(--c-green); } .set-label { diff --git a/src/App.jsx b/src/App.jsx index c218fad..fd4cf03 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -223,6 +223,7 @@ export default function App() { activeRoutineId={activeRoutineId} exerciseNotes={exerciseNotes} setExerciseNotes={setExerciseNotes} + phases={phases} /> )} {tab === 'stats' && ( diff --git a/src/tabs/GymLog.jsx b/src/tabs/GymLog.jsx index 72f26f0..30e161f 100644 --- a/src/tabs/GymLog.jsx +++ b/src/tabs/GymLog.jsx @@ -36,7 +36,7 @@ const defaultGetWorkoutFromTemplate = (templateKey, template, exercises, exercis } } -export default function GymLog({ workouts, setWorkouts, exercises, routines, activeRoutineId, exerciseNotes, setExerciseNotes }) { +export default function GymLog({ workouts, setWorkouts, exercises, routines, activeRoutineId, exerciseNotes, setExerciseNotes, phases }) { const [date, setDate] = useState(todayKey()) // Persist current exercise index keyed by date so reopening today's session // lands back where the user left off (PWA reload, app switch, etc.). @@ -212,30 +212,38 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act return null } + // Returns { overall, phase } PRs prior to `date`. `phase` is scoped to the + // current (open) phase if one exists; otherwise it matches `overall` so any + // weight beat counts as a phase PR too. const getExercisePR = (exerciseName, exerciseId) => { - let maxWeight = 0 - let maxRepsAtMaxWeight = 0 const config = getExerciseConfig(exerciseName, exerciseId) + const curPhase = (phases || []).find(p => !p.end) + const phaseStart = curPhase?.start || null + const acc = { overall: { maxWeight: 0, maxRepsAtMaxWeight: 0 }, phase: { maxWeight: 0, maxRepsAtMaxWeight: 0 } } + const fold = (bucket, w, r) => { + if (w > bucket.maxWeight) { + bucket.maxWeight = w + bucket.maxRepsAtMaxWeight = r + } else if (Math.abs(w - bucket.maxWeight) < 0.1 && r > bucket.maxRepsAtMaxWeight) { + bucket.maxRepsAtMaxWeight = r + } + } Object.entries(workouts).forEach(([d, w]) => { if (d >= date || !w.committed) return const ex = findExerciseInWorkout(w, exerciseId, exerciseName) - if (ex) { - ex.workSets?.forEach(set => { - if (set.committed === false) return - const weight = toKg(set.weight, set.unit || config.unit, config.kgPerUnit) - const reps = parseInt(set.reps) || 0 - if (weight > 0 && reps > 0) { - if (weight > maxWeight) { - maxWeight = weight - maxRepsAtMaxWeight = reps - } else if (Math.abs(weight - maxWeight) < 0.1 && reps > maxRepsAtMaxWeight) { - maxRepsAtMaxWeight = reps - } - } - }) - } + if (!ex) return + const inPhase = !phaseStart || d >= phaseStart + ex.workSets?.forEach(set => { + if (set.committed === false) return + const weight = toKg(set.weight, set.unit || config.unit, config.kgPerUnit) + const reps = parseInt(set.reps) || 0 + if (weight <= 0 || reps <= 0) return + fold(acc.overall, weight, reps) + if (inPhase) fold(acc.phase, weight, reps) + }) }) - return { maxWeight: Math.round(maxWeight * 10) / 10, maxRepsAtMaxWeight } + const round = (b) => ({ maxWeight: Math.round(b.maxWeight * 10) / 10, maxRepsAtMaxWeight: b.maxRepsAtMaxWeight }) + return { overall: round(acc.overall), phase: round(acc.phase) } } const lastExerciseValues = currentExercise ? getLastExerciseValues(currentExercise.name, currentExercise.id) : { warmupSets: [], workSets: [] } @@ -447,13 +455,10 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act // Only the single best work set today gets the PR flag, and only when it // actually beats prior history (avoids tagging every set tied at same load). const isThisBest = type === 'work' && bestThis && bestThis.idx === idx - const prStatus = { - isWeightPR: !!(isThisBest && isWeightPR), - isRepPR: !!(isThisBest && isRepPR), - } + const tier = isThisBest ? prTier : null return ( -
+
@@ -469,7 +474,7 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act
toggleSetCommitted(type, idx)}> - {label}{prStatus.isWeightPR && '⭐'}{prStatus.isRepPR && '✓'} + {label}{(tier === 'overall' || tier === 'phase') && '⭐'}{tier === 'rep' && '✓'}
@@ -560,13 +565,21 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act ) } - const pr = currentExercise ? getExercisePR(currentExercise.name, currentExercise.id) : { maxWeight: 0, maxRepsAtMaxWeight: 0 } + const pr = currentExercise ? getExercisePR(currentExercise.name, currentExercise.id) : { overall: { maxWeight: 0, maxRepsAtMaxWeight: 0 }, phase: { maxWeight: 0, maxRepsAtMaxWeight: 0 } } const unit = routineTemplate?.unit || 'kg' const kgPerUnit = routineTemplate?.kgPerUnit const bestThis = currentExercise ? todayBestSet(currentExercise.workSets, { unit, kgPerUnit }) : null - const isWeightPR = !!(bestThis && bestThis.weightKg > pr.maxWeight) - const isRepPR = !!(bestThis && !isWeightPR && bestThis.weightKg === pr.maxWeight && bestThis.reps > pr.maxRepsAtMaxWeight) - const isAnyPR = isWeightPR || isRepPR + // Tier: 'overall' beats all-time, 'phase' beats phase only, 'rep' matches a + // prior best weight with more reps. Overall implies phase but we surface the + // strongest tier only. + let prTier = null + if (bestThis) { + if (bestThis.weightKg > pr.overall.maxWeight) prTier = 'overall' + else if (bestThis.weightKg > pr.phase.maxWeight) prTier = 'phase' + else if (bestThis.weightKg === pr.overall.maxWeight && bestThis.reps > pr.overall.maxRepsAtMaxWeight) prTier = 'rep' + else if (bestThis.weightKg === pr.phase.maxWeight && bestThis.reps > pr.phase.maxRepsAtMaxWeight) prTier = 'rep' + } + const isAnyPR = prTier !== null const lastDataKg = lastData ? toKg(lastData.weight, unit, kgPerUnit) : 0 const anyCommitted = currentExercise?.warmupSets.some(s => s.committed === true) || currentExercise?.workSets.some(s => s.committed === true) const allWorkDone = currentExercise?.workSets.length > 0 && currentExercise?.workSets.every(s => s.committed === true) @@ -620,13 +633,13 @@ export default function GymLog({ workouts, setWorkouts, exercises, routines, act <> {/* Top bar always rendered so the layout stays put. Falls back to an em-dash when no PR / last data exists. */} -
+
{isAnyPR ? 'NEW PR' : 'PR'} {isAnyPR ? `${bestThis.weightKg}kg×${bestThis.reps}` - : pr.maxWeight > 0 - ? `${pr.maxWeight}kg×${pr.maxRepsAtMaxWeight}` + : pr.overall.maxWeight > 0 + ? `${pr.overall.maxWeight}kg×${pr.overall.maxRepsAtMaxWeight}` : '—'} {lastData && !isAnyPR && ( From 02fabd8cf184c7d4b6fd2bd55efafabc92878076 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 18:40:42 +0000 Subject: [PATCH 2/3] =?UTF-8?q?GymStats=20history:=20tier-color=20?= =?UTF-8?q?=E2=AD=90=20PR=20markers=20(red/yellow/green)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 6 ++++ src/tabs/GymStats.jsx | 66 ++++++++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/App.css b/src/App.css index 9355b0a..9abddb4 100644 --- a/src/App.css +++ b/src/App.css @@ -1938,6 +1938,9 @@ body:has(textarea:focus) .navbar { .sess-head .date { font-size: 13px; font-weight: 500; color: var(--text); } .sess-head .meta { font-size: 11px; color: var(--text-sub); display: flex; gap: 8px; align-items: center; } .sess-head .meta .pr { color: var(--c-yellow); } +.sess-head .meta .pr.t-overall { color: var(--c-red); } +.sess-head .meta .pr.t-phase { color: var(--c-yellow); } +.sess-head .meta .pr.t-rep { color: var(--c-green); } .sess-head .chev { font-size: 11px; color: var(--text-overlay); } .sess-body { padding: 0 12px 12px; border-top: 1px solid var(--bg-surface1); } .sess-rest { font-size: 11px; color: var(--text-sub); padding: 8px 0; } @@ -1945,6 +1948,9 @@ body:has(textarea:focus) .navbar { .sess-ex:last-child { border-bottom: none; } .sess-ex .nm { font-size: 12px; font-weight: 500; color: var(--text); margin-bottom: 3px; display: flex; justify-content: space-between; } .sess-ex .nm .pr { color: var(--c-yellow); font-size: 10px; } +.sess-ex .nm .pr.t-overall { color: var(--c-red); } +.sess-ex .nm .pr.t-phase { color: var(--c-yellow); } +.sess-ex .nm .pr.t-rep { color: var(--c-green); } .sess-ex .sets { font-size: 11px; color: var(--text-sub); font-variant-numeric: tabular-nums; } .sess-ex .sets .work { color: var(--text); } .sess-ex .sets .warm { color: var(--text-overlay); } diff --git a/src/tabs/GymStats.jsx b/src/tabs/GymStats.jsx index fc4febe..337a82c 100644 --- a/src/tabs/GymStats.jsx +++ b/src/tabs/GymStats.jsx @@ -196,32 +196,54 @@ function buildExerciseIndex(exercises, workouts, phases, statsFilter) { return idx } +// Walks every committed workout chronologically, tracks per-exercise running +// bests both all-time and per-phase, and tags each PR moment by tier: +// overall — beats all-time top weight +// phase — beats the containing phase's top weight (but not all-time) +// rep — matches a prior top weight with more reps +// Only PRs whose date falls in the filtered range are returned. function detectPRsInRange(exercises, workouts, phases, statsFilter) { const filtered = filterByPhase(workouts, phases, statsFilter) - const sorted = Object.entries(filtered).sort(([a], [b]) => a.localeCompare(b)) - const best = {} + const inRange = new Set(Object.keys(filtered)) + const phaseFor = (d) => { + const p = (phases || []).find(p => d >= (p.start || '') && (!p.end || d <= p.end)) + return p ? p.id : '__none__' + } + const allDates = Object.keys(workouts).filter(d => workouts[d]?.committed).sort() + const overall = {} + const phaseBest = {} const prs = [] - sorted.forEach(([date, w]) => { + allDates.forEach(date => { + const w = workouts[date] if (isRestWorkout(w)) return + const pid = phaseFor(date) + if (!phaseBest[pid]) phaseBest[pid] = {} w.exercises?.forEach(ex => { const cfg = getExerciseConfig(exercises, ex.name) - let topW = 0, topR = 0, e1RM = 0 + let topW = 0, topR = 0 ex.workSets?.forEach(s => { if (s.committed === false) return const wt = toKg(s.weight, cfg.unit, cfg.kgPerUnit) const r = parseInt(s.reps) || 0 - if (wt > 0 && r > 0) { - if (wt > topW) { topW = wt; topR = r } - const e = wt * (1 + r / 30) - if (e > e1RM) e1RM = e - } + if (wt <= 0 || r <= 0) return + if (wt > topW || (Math.abs(wt - topW) < 0.01 && r > topR)) { topW = wt; topR = r } }) - if (e1RM === 0) return - const cur = best[ex.name] || 0 - if (e1RM > cur + 0.1) { - if (cur > 0) prs.push({ date, name: ex.name, weight: topW, reps: topR, e1RM }) - best[ex.name] = e1RM - } else { best[ex.name] = Math.max(cur, e1RM) } + if (topW === 0) return + const ob = overall[ex.name] || { weight: 0, reps: 0 } + const pb = phaseBest[pid][ex.name] || { weight: 0, reps: 0 } + let tier = null + if (topW > ob.weight + 0.01) tier = 'overall' + else if (topW > pb.weight + 0.01) tier = 'phase' + else if (Math.abs(topW - ob.weight) < 0.01 && topR > ob.reps) tier = 'rep' + else if (Math.abs(topW - pb.weight) < 0.01 && topR > pb.reps) tier = 'rep' + // Update running bests after classification. + if (topW > ob.weight) overall[ex.name] = { weight: topW, reps: topR } + else if (Math.abs(topW - ob.weight) < 0.01 && topR > ob.reps) overall[ex.name] = { weight: ob.weight, reps: topR } + else overall[ex.name] = ob + if (topW > pb.weight) phaseBest[pid][ex.name] = { weight: topW, reps: topR } + else if (Math.abs(topW - pb.weight) < 0.01 && topR > pb.reps) phaseBest[pid][ex.name] = { weight: pb.weight, reps: topR } + else phaseBest[pid][ex.name] = pb + if (tier && inRange.has(date)) prs.push({ date, name: ex.name, weight: topW, reps: topR, tier }) }) }) return prs @@ -288,7 +310,9 @@ export default function GymStats({ workouts, phases, exercises, routines, active if (!histGroups[wk]) histGroups[wk] = [] histGroups[wk].push([d, w]) }) - const prSet = new Set(); prs.forEach(p => prSet.add(p.date + '|' + p.name)) + const prMap = {}; prs.forEach(p => { prMap[p.date + '|' + p.name] = p.tier }) + // Rank used to pick the dominant tier in a session header (red > yellow > green). + const tierRank = { overall: 3, phase: 2, rep: 1 } return (
@@ -500,7 +524,9 @@ export default function GymStats({ workouts, phases, exercises, routines, active const isRest = isRestWorkout(w) const { volume, sets } = isRest ? { volume: 0, sets: 0 } : getSessionVolume(exercises, w) const open = openHistDates.has(d) - const exPRs = isRest ? 0 : (w.exercises || []).filter(ex => prSet.has(d + '|' + ex.name)).length + const exPRTiers = isRest ? [] : (w.exercises || []).map(ex => prMap[d + '|' + ex.name]).filter(Boolean) + const exPRs = exPRTiers.length + const headTier = exPRTiers.reduce((best, t) => tierRank[t] > tierRank[best] ? t : best, exPRTiers[0] || 'rep') return (
{ @@ -514,7 +540,7 @@ export default function GymStats({ workouts, phases, exercises, routines, active
{isRest ? rehab ✓ : <>{fmtKg(volume)}{sets} sets} - {exPRs > 0 && {exPRs}⭐} + {exPRs > 0 && {exPRs}⭐} {open ? '▴' : '▾'}
@@ -526,13 +552,13 @@ export default function GymStats({ workouts, phases, exercises, routines, active const cfg = getExerciseConfig(exercises, ex.name) const work = (ex.workSets || []).filter(s => s.committed !== false && parseFloat(s.weight) > 0) const warm = (ex.warmupSets || []).filter(s => parseFloat(s.weight) > 0) - const isPR = prSet.has(d + '|' + ex.name) + const prTier = prMap[d + '|' + ex.name] if (!work.length && !warm.length) return null return (
{ex.name} - {isPR && ⭐ PR} + {prTier && ⭐ PR}
{work.map((s, si) => ( From 212af819f3e1e0d64401e473cdab24fe2b2d6346 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 13:05:22 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Revert=20"GymStats=20history:=20tier-color?= =?UTF-8?q?=20=E2=AD=90=20PR=20markers=20(red/yellow/green)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 02fabd8cf184c7d4b6fd2bd55efafabc92878076. --- src/App.css | 6 ---- src/tabs/GymStats.jsx | 66 +++++++++++++------------------------------ 2 files changed, 20 insertions(+), 52 deletions(-) diff --git a/src/App.css b/src/App.css index 9abddb4..9355b0a 100644 --- a/src/App.css +++ b/src/App.css @@ -1938,9 +1938,6 @@ body:has(textarea:focus) .navbar { .sess-head .date { font-size: 13px; font-weight: 500; color: var(--text); } .sess-head .meta { font-size: 11px; color: var(--text-sub); display: flex; gap: 8px; align-items: center; } .sess-head .meta .pr { color: var(--c-yellow); } -.sess-head .meta .pr.t-overall { color: var(--c-red); } -.sess-head .meta .pr.t-phase { color: var(--c-yellow); } -.sess-head .meta .pr.t-rep { color: var(--c-green); } .sess-head .chev { font-size: 11px; color: var(--text-overlay); } .sess-body { padding: 0 12px 12px; border-top: 1px solid var(--bg-surface1); } .sess-rest { font-size: 11px; color: var(--text-sub); padding: 8px 0; } @@ -1948,9 +1945,6 @@ body:has(textarea:focus) .navbar { .sess-ex:last-child { border-bottom: none; } .sess-ex .nm { font-size: 12px; font-weight: 500; color: var(--text); margin-bottom: 3px; display: flex; justify-content: space-between; } .sess-ex .nm .pr { color: var(--c-yellow); font-size: 10px; } -.sess-ex .nm .pr.t-overall { color: var(--c-red); } -.sess-ex .nm .pr.t-phase { color: var(--c-yellow); } -.sess-ex .nm .pr.t-rep { color: var(--c-green); } .sess-ex .sets { font-size: 11px; color: var(--text-sub); font-variant-numeric: tabular-nums; } .sess-ex .sets .work { color: var(--text); } .sess-ex .sets .warm { color: var(--text-overlay); } diff --git a/src/tabs/GymStats.jsx b/src/tabs/GymStats.jsx index 337a82c..fc4febe 100644 --- a/src/tabs/GymStats.jsx +++ b/src/tabs/GymStats.jsx @@ -196,54 +196,32 @@ function buildExerciseIndex(exercises, workouts, phases, statsFilter) { return idx } -// Walks every committed workout chronologically, tracks per-exercise running -// bests both all-time and per-phase, and tags each PR moment by tier: -// overall — beats all-time top weight -// phase — beats the containing phase's top weight (but not all-time) -// rep — matches a prior top weight with more reps -// Only PRs whose date falls in the filtered range are returned. function detectPRsInRange(exercises, workouts, phases, statsFilter) { const filtered = filterByPhase(workouts, phases, statsFilter) - const inRange = new Set(Object.keys(filtered)) - const phaseFor = (d) => { - const p = (phases || []).find(p => d >= (p.start || '') && (!p.end || d <= p.end)) - return p ? p.id : '__none__' - } - const allDates = Object.keys(workouts).filter(d => workouts[d]?.committed).sort() - const overall = {} - const phaseBest = {} + const sorted = Object.entries(filtered).sort(([a], [b]) => a.localeCompare(b)) + const best = {} const prs = [] - allDates.forEach(date => { - const w = workouts[date] + sorted.forEach(([date, w]) => { if (isRestWorkout(w)) return - const pid = phaseFor(date) - if (!phaseBest[pid]) phaseBest[pid] = {} w.exercises?.forEach(ex => { const cfg = getExerciseConfig(exercises, ex.name) - let topW = 0, topR = 0 + let topW = 0, topR = 0, e1RM = 0 ex.workSets?.forEach(s => { if (s.committed === false) return const wt = toKg(s.weight, cfg.unit, cfg.kgPerUnit) const r = parseInt(s.reps) || 0 - if (wt <= 0 || r <= 0) return - if (wt > topW || (Math.abs(wt - topW) < 0.01 && r > topR)) { topW = wt; topR = r } + if (wt > 0 && r > 0) { + if (wt > topW) { topW = wt; topR = r } + const e = wt * (1 + r / 30) + if (e > e1RM) e1RM = e + } }) - if (topW === 0) return - const ob = overall[ex.name] || { weight: 0, reps: 0 } - const pb = phaseBest[pid][ex.name] || { weight: 0, reps: 0 } - let tier = null - if (topW > ob.weight + 0.01) tier = 'overall' - else if (topW > pb.weight + 0.01) tier = 'phase' - else if (Math.abs(topW - ob.weight) < 0.01 && topR > ob.reps) tier = 'rep' - else if (Math.abs(topW - pb.weight) < 0.01 && topR > pb.reps) tier = 'rep' - // Update running bests after classification. - if (topW > ob.weight) overall[ex.name] = { weight: topW, reps: topR } - else if (Math.abs(topW - ob.weight) < 0.01 && topR > ob.reps) overall[ex.name] = { weight: ob.weight, reps: topR } - else overall[ex.name] = ob - if (topW > pb.weight) phaseBest[pid][ex.name] = { weight: topW, reps: topR } - else if (Math.abs(topW - pb.weight) < 0.01 && topR > pb.reps) phaseBest[pid][ex.name] = { weight: pb.weight, reps: topR } - else phaseBest[pid][ex.name] = pb - if (tier && inRange.has(date)) prs.push({ date, name: ex.name, weight: topW, reps: topR, tier }) + if (e1RM === 0) return + const cur = best[ex.name] || 0 + if (e1RM > cur + 0.1) { + if (cur > 0) prs.push({ date, name: ex.name, weight: topW, reps: topR, e1RM }) + best[ex.name] = e1RM + } else { best[ex.name] = Math.max(cur, e1RM) } }) }) return prs @@ -310,9 +288,7 @@ export default function GymStats({ workouts, phases, exercises, routines, active if (!histGroups[wk]) histGroups[wk] = [] histGroups[wk].push([d, w]) }) - const prMap = {}; prs.forEach(p => { prMap[p.date + '|' + p.name] = p.tier }) - // Rank used to pick the dominant tier in a session header (red > yellow > green). - const tierRank = { overall: 3, phase: 2, rep: 1 } + const prSet = new Set(); prs.forEach(p => prSet.add(p.date + '|' + p.name)) return (
@@ -524,9 +500,7 @@ export default function GymStats({ workouts, phases, exercises, routines, active const isRest = isRestWorkout(w) const { volume, sets } = isRest ? { volume: 0, sets: 0 } : getSessionVolume(exercises, w) const open = openHistDates.has(d) - const exPRTiers = isRest ? [] : (w.exercises || []).map(ex => prMap[d + '|' + ex.name]).filter(Boolean) - const exPRs = exPRTiers.length - const headTier = exPRTiers.reduce((best, t) => tierRank[t] > tierRank[best] ? t : best, exPRTiers[0] || 'rep') + const exPRs = isRest ? 0 : (w.exercises || []).filter(ex => prSet.has(d + '|' + ex.name)).length return (
{ @@ -540,7 +514,7 @@ export default function GymStats({ workouts, phases, exercises, routines, active
{isRest ? rehab ✓ : <>{fmtKg(volume)}{sets} sets} - {exPRs > 0 && {exPRs}⭐} + {exPRs > 0 && {exPRs}⭐} {open ? '▴' : '▾'}
@@ -552,13 +526,13 @@ export default function GymStats({ workouts, phases, exercises, routines, active const cfg = getExerciseConfig(exercises, ex.name) const work = (ex.workSets || []).filter(s => s.committed !== false && parseFloat(s.weight) > 0) const warm = (ex.warmupSets || []).filter(s => parseFloat(s.weight) > 0) - const prTier = prMap[d + '|' + ex.name] + const isPR = prSet.has(d + '|' + ex.name) if (!work.length && !warm.length) return null return (
{ex.name} - {prTier && ⭐ PR} + {isPR && ⭐ PR}
{work.map((s, si) => (