feat: enhance dashboard with PRs, adherence, activity, progression chart, and muscle heatmap
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 13s

Add 3 new stat cards (Last Workout, Personal Records, Adherence Rate),
recent activity table, progression timeline chart, and muscle group
recency heatmap to the dashboard. Remove Total Volume card.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:44:21 -05:00
parent c5a7728818
commit df8d5c65fb
8 changed files with 465 additions and 7 deletions

View File

@@ -4,6 +4,7 @@
{% block head_extra %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
{% endblock %}
{% block content %}
@@ -28,6 +29,15 @@
{% include "partials/volume_chart.html" %}
</article>
<!-- Recent Activity -->
{% include "partials/recent_activity.html" %}
<!-- Progression Timeline -->
{% include "partials/progression_chart.html" %}
<!-- Muscle Group Heatmap -->
{% include "partials/muscle_heatmap.html" %}
<!-- Exercise Progress Links -->
<article>
<header><h3>Per-Exercise Progress</h3></header>
@@ -45,5 +55,9 @@
<!-- Export -->
{% include "partials/export_form.html" %}
{% endif %}
<!-- Import -->
{% include "partials/import_form.html" %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
<article>
<header><h3>Muscle Group Heatmap</h3></header>
{% if muscle_recency %}
<div class="muscle-heatmap">
{% for mg in muscle_recency %}
<div class="muscle-heatmap-cell {% if mg.days_ago is none %}recency-never{% elif mg.days_ago <= 3 %}recency-fresh{% elif mg.days_ago <= 7 %}recency-ok{% elif mg.days_ago <= 14 %}recency-stale{% else %}recency-overdue{% endif %}">
<strong>{{ mg.muscle_group }}</strong>
<br>
{% if mg.days_ago is none %}
<small>Never worked</small>
{% elif mg.days_ago == 0 %}
<small>Today</small>
{% elif mg.days_ago == 1 %}
<small>1 day ago</small>
{% else %}
<small>{{ mg.days_ago }} days ago</small>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p>No exercises found in the library.</p>
{% endif %}
</article>

View File

@@ -0,0 +1,90 @@
<article>
<header><h3>Progression Timeline</h3></header>
<div id="progression-container">
<canvas id="progression-chart" style="max-height:350px;"></canvas>
<div style="margin-top:0.5rem;">
<label for="progression-filter" style="display:inline; margin-right:0.5rem;">Exercise:</label>
<select id="progression-filter" style="display:inline-block; width:auto;">
<option value="all">All Exercises</option>
</select>
</div>
</div>
<p id="progression-empty" style="display:none;">No progression data yet.</p>
</article>
<script>
(function() {
var timeline = {{ progression_timeline_json|safe }};
var exerciseData = timeline.exercises || {};
var names = Object.keys(exerciseData);
if (names.length === 0) {
document.getElementById('progression-container').style.display = 'none';
document.getElementById('progression-empty').style.display = 'block';
return;
}
var colors = [
'rgba(99, 102, 241, 1)',
'rgba(34, 197, 94, 1)',
'rgba(234, 179, 8, 1)',
'rgba(249, 115, 22, 1)',
'rgba(239, 68, 68, 1)',
'rgba(168, 85, 247, 1)',
'rgba(20, 184, 166, 1)',
'rgba(236, 72, 153, 1)',
];
var datasets = [];
var filterSelect = document.getElementById('progression-filter');
names.forEach(function(name, i) {
var d = exerciseData[name];
datasets.push({
label: name,
data: d.dates.map(function(dt, j) {
return {x: dt, y: d.weights[j]};
}),
borderColor: colors[i % colors.length],
backgroundColor: colors[i % colors.length].replace('1)', '0.2)'),
tension: 0.3,
pointRadius: 3,
hidden: false,
});
var opt = document.createElement('option');
opt.value = i;
opt.textContent = name;
filterSelect.appendChild(opt);
});
var chart = new Chart(document.getElementById('progression-chart'), {
type: 'line',
data: {datasets: datasets},
options: {
responsive: true,
plugins: {
legend: {labels: {color: '#ccc'}},
},
scales: {
x: {
type: 'time',
time: {unit: 'week'},
ticks: {color: '#ccc'},
},
y: {
beginAtZero: false,
title: {display: true, text: 'Weight (lbs)', color: '#ccc'},
ticks: {color: '#ccc'},
},
},
},
});
filterSelect.addEventListener('change', function() {
var val = this.value;
chart.data.datasets.forEach(function(ds, i) {
ds.hidden = (val !== 'all' && i !== parseInt(val));
});
chart.update();
});
})();
</script>

View File

@@ -0,0 +1,29 @@
<article>
<header><h3>Recent Activity</h3></header>
{% if recent_activity %}
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>Date</th>
<th>Workout</th>
<th>Volume (lbs)</th>
<th>Sets</th>
</tr>
</thead>
<tbody>
{% for session in recent_activity %}
<tr>
<td>{{ session.date.strftime('%b %d, %Y') }}</td>
<td>{{ session.workout_day_name }}</td>
<td>{{ "{:,}".format(session.total_volume) }}</td>
<td>{{ session.total_sets }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p>No workout sessions logged yet.</p>
{% endif %}
</article>

View File

@@ -4,12 +4,6 @@
{{ stats.total_sessions }}
</p>
</article>
<article>
<header><h4>Total Volume</h4></header>
<p style="font-size:2rem; font-weight:700;">
{{ "{:,}".format(stats.total_volume) }} lbs
</p>
</article>
<article>
<header><h4>Total Sets</h4></header>
<p style="font-size:2rem; font-weight:700;">
@@ -22,3 +16,38 @@
{{ stats.current_streak }} week{{ "s" if stats.current_streak != 1 }}
</p>
</article>
</div>
<div class="grid">
<article>
<header><h4>Last Workout</h4></header>
<p style="font-size:2rem; font-weight:700;">
{% if stats.last_workout_date %}
{{ stats.last_workout_date.strftime('%b %d') }}
{% else %}
Never
{% endif %}
</p>
</article>
<article>
<header><h4>Personal Records</h4></header>
<p style="font-size:2rem; font-weight:700;">
{{ personal_records|length }} PR{{ "s" if personal_records|length != 1 }}
</p>
{% if personal_records %}
<details>
<summary>View records</summary>
<ul style="font-size:0.85rem; margin-top:0.5rem;">
{% for pr in personal_records[:10] %}
<li>{{ pr.exercise_name }}: {{ pr.weight_display }}</li>
{% endfor %}
</ul>
</details>
{% endif %}
</article>
<article>
<header><h4>Adherence</h4></header>
<p style="font-size:2rem; font-weight:700;">
{{ adherence.rate }}%
</p>
<small>{{ adherence.completed }}/{{ adherence.expected }} sessions ({{ adherence.weeks }}wk)</small>
</article>