From 0c0bcdc4b6e86a9f4fb29d734baaa437a779ccbe Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 11 Oct 2026 11:30:30 +1100 Subject: [PATCH] Show consecutive stall count beside previous performance --- db.py | 4 ++-- routes/workout.py | 10 ++++++++-- static/css/workout.css | 9 ++++++++- templates/partials/new_set_form.html | 10 ++++++++-- tests/test_workout_view.py | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/db.py b/db.py index 97e2a08..2e17ec8 100644 --- a/db.py +++ b/db.py @@ -371,8 +371,8 @@ class DataBase(): WHERE e.exercise_id = %s AND (w.person_id = %s OR w.person_id IS NULL) ORDER BY - w.start_date DESC - LIMIT 1; + w.start_date DESC, t.topset_id DESC + LIMIT 1; """, [exercise_id, person_id], one=True) if not topset: diff --git a/routes/workout.py b/routes/workout.py index e19c942..38a7c72 100644 --- a/routes/workout.py +++ b/routes/workout.py @@ -308,8 +308,14 @@ def get_most_recent_topset_for_exercise(person_id, workout_id): if not topset: exercise = db.execute("select name from exercise where exercise_id=%s", [exercise_id], one=True) return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, exercise_id=exercise_id, exercise_name=exercise['name']) - (repetitions, weight, exercise_name) = topset - return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercise_id=exercise_id, exercise_name=exercise_name, repetitions=repetitions, weight=weight) + (repetitions, weight, exercise_name) = topset + stalled_sessions = 0 + if repetitions is not None: + recent_sets = db.get_recent_topsets_for_exercise(person_id, exercise_id, limit=1) + if recent_sets: + achievements = db.get_topset_achievements(recent_sets[0]['topset_id']) + stalled_sessions = achievements.get('stalled_sessions', 0) + return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercise_id=exercise_id, exercise_name=exercise_name, repetitions=repetitions, weight=weight, stalled_sessions=stalled_sessions) @workout_bp.route("/person//exercise//history", methods=['GET']) def get_exercise_history(person_id, exercise_id): diff --git a/static/css/workout.css b/static/css/workout.css index 773d60c..56c0b8b 100644 --- a/static/css/workout.css +++ b/static/css/workout.css @@ -64,7 +64,14 @@ .workout-secondary-details>summary,.workout-previous-details>summary{font-size:12px;color:#64748b;cursor:pointer} .workout-secondary-details .workout-breakdown{margin-top:20px} .workout-previous-details{border-top:1px solid #e8edf1;margin-top:18px;padding-top:14px} -.workout-previous-details>summary span{float:right;color:#334155;font-variant-numeric:tabular-nums} +.workout-previous-details>summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;list-style:none} +.workout-previous-details>summary::-webkit-details-marker{display:none} +.workout-previous-label{margin-right:auto} +.workout-previous-label:before{content:'›';display:inline-block;margin-right:8px} +.workout-previous-details[open] .workout-previous-label:before{transform:rotate(90deg)} +.workout-previous-value{color:#334155;font-variant-numeric:tabular-nums;white-space:nowrap} +.workout-stall-badge{padding:4px 8px;border-radius:5px;background:#fff4df;color:#8c5a10;font-size:11px;font-weight:500;white-space:nowrap} +@media(max-width:600px){.workout-previous-label{flex-basis:100%}} .workout-table .extra-content-container:not(:empty){background:#f8fafc;padding:18px 20px;border-bottom:1px solid #e1e7ec} .workout-table .extra-content-container .progress-heading{padding-right:22px} .workout-workspace .forecast-chart{height:220px} diff --git a/templates/partials/new_set_form.html b/templates/partials/new_set_form.html index 3e2b53e..bcb3136 100644 --- a/templates/partials/new_set_form.html +++ b/templates/partials/new_set_form.html @@ -72,7 +72,13 @@ {% if exercise_id %}
- Previous performance{% if repetitions %}{{ repetitions }} × {{ weight }} kg{% endif %} + + Previous performance + {% if repetitions %}{{ repetitions }} × {{ weight }} kg{% endif %} + {% if stalled_sessions|default(0) > 0 %} + Stalled {{ stalled_sessions }} {{ 'time' if stalled_sessions == 1 else 'times' }} + {% endif %} +
{% endif %} - \ No newline at end of file + diff --git a/tests/test_workout_view.py b/tests/test_workout_view.py index 664fa84..b33d50a 100644 --- a/tests/test_workout_view.py +++ b/tests/test_workout_view.py @@ -91,11 +91,31 @@ class WorkoutViewTests(unittest.TestCase): def test_previous_performance_is_loaded_on_expansion(self): self.db.get_most_recent_topset_for_exercise.return_value = (7, 93.5, 'Smith Press') + self.db.get_recent_topsets_for_exercise.return_value = [self.top_sets[0]] response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1') self.assertEqual(response.status_code, 200) self.assertIn(b'toggle once from:closest details', response.data) self.assertNotIn(b'hx-trigger=load', response.data) self.assertIn(b'Previous performance', response.data) + self.assertNotIn(b'workout-stall-badge', response.data) + + def test_previous_performance_shows_consecutive_stall_count(self): + self.db.get_most_recent_topset_for_exercise.return_value = (7, 93.5, 'Smith Press') + self.db.get_recent_topsets_for_exercise.return_value = [self.top_sets[0]] + for count, label in ((1, b'Stalled 1 time'), (3, b'Stalled 3 times')): + self.db.get_topset_achievements.return_value = {'stalled_sessions': count} + response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1') + self.assertEqual(response.status_code, 200) + self.assertIn(label, response.data) + self.assertIn('7 × 93.5 kg', response.get_data(as_text=True)) + self.db.get_topset_achievements.assert_called_with(1) + + def test_previous_performance_without_history_hides_stall_badge(self): + self.db.get_most_recent_topset_for_exercise.return_value = (None, None, 'Smith Press') + response = self.client.get(self.base + '/exercise/most_recent_topset_for_exercise?exercise_id=1') + self.assertEqual(response.status_code, 200) + self.assertNotIn(b'workout-stall-badge', response.data) + self.db.get_recent_topsets_for_exercise.assert_not_called() def test_create_edit_cancel_update_and_delete_targets(self): data = dict(exercise_id=1, repetitions=8, weight=93.5)