Add badges to sets showing stats ie weight/rep increase or how many weeks stalled

This commit is contained in:
Peter Stockings
2026-01-30 22:42:06 +11:00
parent eada1a829b
commit e156dd30cc
5 changed files with 138 additions and 3 deletions

89
db.py
View File

@@ -466,6 +466,95 @@ class DataBase():
return result[0]['earliest_date'], result[0]['latest_date']
def get_topset_achievements(self, topset_id):
# 1. Fetch current topset details
current = self.execute("""
SELECT
t.weight, t.repetitions, t.exercise_id, w.person_id, w.start_date, w.workout_id,
ROUND((100 * t.weight::NUMERIC::INTEGER) / (101.3 - 2.67123 * t.repetitions), 0)::NUMERIC::INTEGER AS estimated_1rm
FROM topset t
JOIN workout w ON t.workout_id = w.workout_id
WHERE t.topset_id = %s
""", [topset_id], one=True)
if not current:
return {}
person_id = current['person_id']
exercise_id = current['exercise_id']
current_date = current['start_date']
current_weight = current['weight']
current_reps = current['repetitions']
current_e1rm = current['estimated_1rm']
# 2. Fetch "Last Time" (previous workout's best set for this exercise)
last_set = self.execute("""
SELECT t.weight, t.repetitions
FROM topset t
JOIN workout w ON t.workout_id = w.workout_id
WHERE w.person_id = %s AND t.exercise_id = %s AND w.start_date < %s
ORDER BY w.start_date DESC, (100 * t.weight::NUMERIC::INTEGER) / (101.3 - 2.67123 * t.repetitions) DESC
LIMIT 1
""", [person_id, exercise_id, current_date], one=True)
# 3. Fetch All-Time Bests (strictly before current workout)
best_stats = self.execute("""
SELECT
MAX(t.weight) as max_weight,
MAX(ROUND((100 * t.weight::NUMERIC::INTEGER) / (101.3 - 2.67123 * t.repetitions), 0)) as max_e1rm,
MAX(t.repetitions) FILTER (WHERE t.weight >= %s) as max_reps_at_weight
FROM topset t
JOIN workout w ON t.workout_id = w.workout_id
WHERE w.person_id = %s AND t.exercise_id = %s AND w.start_date < %s
""", [current_weight, person_id, exercise_id, current_date], one=True)
achievements = {
'is_pr_weight': False,
'is_pr_e1rm': False,
'is_pr_reps': False,
'weight_increase': 0,
'rep_increase': 0,
'stalled_sessions': 0
}
# Calculate PRs
if best_stats:
if best_stats['max_weight'] and current_weight > best_stats['max_weight']:
achievements['is_pr_weight'] = True
if best_stats['max_e1rm'] and current_e1rm > best_stats['max_e1rm']:
achievements['is_pr_e1rm'] = True
if best_stats['max_reps_at_weight'] and current_reps > best_stats['max_reps_at_weight']:
achievements['is_pr_reps'] = True
# Calculate Stalled Sessions
# Count consecutive previous workouts for this exercise where weight and reps were identical to current
previous_sets = self.execute("""
SELECT t.weight, t.repetitions
FROM topset t
JOIN workout w ON t.workout_id = w.workout_id
WHERE w.person_id = %s AND t.exercise_id = %s AND w.start_date < %s
ORDER BY w.start_date DESC
""", [person_id, exercise_id, current_date])
stalled_count = 0
for s in previous_sets:
if s['weight'] == current_weight and s['repetitions'] == current_reps:
stalled_count += 1
else:
break
if stalled_count >= 1: # If it's the same as at least the previous session
achievements['stalled_sessions'] = stalled_count
# Calculate Increases vs Last Time
if last_set:
if current_weight > last_set['weight']:
achievements['weight_increase'] = current_weight - last_set['weight']
elif current_weight == last_set['weight'] and current_reps > last_set['repetitions']:
achievements['rep_increase'] = current_reps - last_set['repetitions']
return achievements

View File

@@ -179,6 +179,12 @@ def get_workout_start_date(person_id, workout_id):
start_date = workout.get('start_date') if workout else None
return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=start_date)
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>/achievements", methods=['GET'])
@validate_topset
def get_topset_achievements(person_id, workout_id, topset_id):
achievements = db.get_topset_achievements(topset_id)
return render_template('partials/achievement_badges.html', achievements=achievements)
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>", methods=['GET'])
@validate_topset
def get_topset(person_id, workout_id, topset_id):

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
{% if achievements %}
{% if achievements.is_pr_weight or achievements.is_pr_e1rm or achievements.is_pr_reps %}
<span
class="inline-flex items-center rounded-full bg-gradient-to-r from-yellow-100 to-amber-200 px-2.5 py-0.5 text-xs font-bold text-amber-900 shadow-sm ring-1 ring-inset ring-amber-500/30 whitespace-nowrap"
title="Personal Record">
<svg class="mr-1 h-3 w-3 text-amber-600" fill="currentColor" viewBox="0 0 20 20">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
PR
</span>
{% endif %}
{% if achievements.weight_increase > 0 %}
<span
class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-bold text-green-800 shadow-sm ring-1 ring-inset ring-green-500/30 whitespace-nowrap"
title="Weight increase vs last time">
+{{ achievements.weight_increase }}kg
</span>
{% elif achievements.rep_increase > 0 %}
<span
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-bold text-blue-800 shadow-sm ring-1 ring-inset ring-blue-500/30 whitespace-nowrap"
title="Rep increase at same weight vs last time">
+{{ achievements.rep_increase }} reps
</span>
{% endif %}
{% if achievements.stalled_sessions >= 1 %}
<span
class="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-600 shadow-sm ring-1 ring-inset ring-slate-400/20 whitespace-nowrap"
title="Weight and reps matched for {{ achievements.stalled_sessions + 1 }} sessions total">
Stalled ({{ achievements.stalled_sessions + 1 }}x)
</span>
{% endif %}
{% endif %}

View File

@@ -31,9 +31,14 @@
</div>
{% endif %}
</td>
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
<td class="p-4 text-sm font-semibold text-gray-900">
{% if is_edit|default(false, true) == false %}
{{ repetitions }} x {{ weight }}kg
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="whitespace-nowrap">{{ repetitions }} x {{ weight }}kg</span>
<div hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=workout_id, topset_id=topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML" class="flex flex-wrap items-center gap-1">
</div>
</div>
{% else %}
<div class="flex items-center flex-col sm:flex-row">
<input type="number"