Files
workout/templates/partials/exercise_history.html
T

125 lines
8.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% if offset == 0 %}
<div id="exercise-history-container"
class="w-full bg-gray-50 p-4 border-t border-gray-200 shadow-inner overflow-x-auto">
<div class="progress-heading">
<div class="progress-title">
<h4>{{ title }}</h4>
</div>
<button class="progress-action" title="Show Progress Graph"
hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=exercise_id, epoch=selected_epoch, forecast='true' if forecast_enabled else 'false', forecast_weeks=forecast_weeks, forecast_reps=forecast_target_reps, min_date=min_date, max_date=max_date) }}"
hx-target="#exercise-history-container" hx-swap="outerHTML">Graph</button>
</div>
<div class="progress-badges" aria-label="Progress achievements" hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=latest_workout_id, topset_id=latest_topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"></div>
{% if best_fit_formula and not forecast_enabled %}
<h2 class="text-xs font-semibold text-blue-200 mb-4 text-center">
{{ best_fit_formula.kg_per_week }} kg/week, {{ best_fit_formula.kg_per_month }} kg/month
</h2>
{% else %}
<div class="mb-4"></div>
{% endif %}
{% if forecast_enabled and forecast.available %}
<section class="exercise-progress" aria-label="Workout forecasts">
<div class="forecast-table-title">Upcoming sessions</div>
<p class="forecast-caption">Approximate dates, every {{ forecast.profile.cadence_days }} days. These are scenarios based on your recent routine.</p>
{% if forecast.stale %}<p class="forecast-caption">Historical projection from {{ forecast.last_date.strftime('%d %b %Y') }}. Log recent workouts to update.</p>{% endif %}
<div class="forecast-table-wrap">
<table class="forecast-table">
<thead><tr><th scope="col">Around</th><th scope="col">Top-set scenario</th><th scope="col">Est. 1RM</th><th class="forecast-desktop-range" scope="col">E1RM range</th></tr></thead>
<tbody>
{% for point in forecast.session_points %}
<tr><td>{{ point.date.strftime('%d %b %Y') }}</td>
<td><strong>{{ point.set_display }}</strong></td>
<td>{{ point.value }} kg<span class="forecast-mobile-range">{{ point.lower }}–{{ point.upper }} range</span></td>
<td class="forecast-desktop-range">{{ point.lower }}–{{ point.upper }} kg</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not forecast.session_points %}<p class="forecast-caption">No session expected inside this horizon at your recent cadence.</p>{% endif %}
<details class="forecast-method">
<summary>Why this forecast?</summary>
<div class="forecast-facts">
<span><strong>{{ forecast.observed_points }}</strong> sessions since {{ forecast.training_start.strftime('%d %b %Y') }}</span>
<span><strong>{{ forecast.training_blocks - 1 }}</strong> earlier training blocks excluded</span>
</div>
<p>At the current load: {{ forecast.profile.block_sessions }} sessions, from {{ forecast.profile.block_start_reps }} to a best of {{ forecast.profile.block_best_reps }} reps. {{ forecast.profile.block_stalls }} sessions did not improve that rep best.</p>
{% if forecast.profile.increment and forecast.profile.ceiling_evidence >= 2 %}
<p>Your recent load increases usually follow {{ forecast.profile.ceiling }} reps. The most common step is {{ '%g'|format(forecast.profile.increment) }} kg; forecasts use the recorded mix of steps. The model learns repeats, rep gains, difficult sessions and rep resets from this exercise.</p>
{% else %}<p>There is not enough repeated loading history to infer a reliable rep threshold. Repeated sets remain a possible outcome.</p>{% endif %}
{% if forecast.validation %}
{% set validation = forecast.validation.transitions %}
<p>Checked against {{ validation.count }} past next-session outcomes, each predicted using only earlier data: average error {{ validation.weight_mae }} kg load, {{ validation.reps_mae }} reps, {{ validation.e1rm_mae }} kg E1RM. Repeating the last set scored {{ forecast.validation.repeat.e1rm_mae }} kg E1RM error.</p>
{% else %}<p>Too few sessions for a useful accuracy check yet.</p>{% endif %}
<p>Ranges combine simulated outcomes with past forecast errors. They are not guaranteed limits. Each row is a representative outcome at that date; the rows are not a prescribed training plan. Longer horizons are less certain.</p>
</details>
<details class="forecast-method">
<summary>Equivalent loads at other rep counts · {{ forecast.end_date.strftime('%d %b %Y') }}</summary>
<p>These are formula equivalents of {{ forecast.projected_value }} kg E1RM, not predictions that you will choose these sets. Rep range: {{ forecast.observed_rep_min }}–{{ forecast.observed_rep_max }} in view, expanded by ±{{ forecast.rep_buffer }}.</p>
<div class="forecast-table-wrap">
<table class="forecast-table">
<thead><tr><th scope="col">Reps</th><th scope="col">Equivalent load</th><th scope="col">Load range</th></tr></thead>
<tbody>{% for top_set in forecast.top_sets %}
<tr><td>{{ top_set.repetitions }}</td><td>{{ top_set.weight_display }} kg</td><td>{{ top_set.lower_weight_display }}–{{ top_set.upper_weight_display }} kg</td></tr>
{% endfor %}</tbody>
</table>
</div>
<p>Rounded to {{ forecast.target_weight_increment_display }} kg display increments. Equivalents at rep counts far from your usual range are less reliable.</p>
</details>
</section>
{% elif forecast_enabled %}
<p class="forecast-caption">{{ forecast.reason }}</p>
{% endif %}
<table class="w-full text-left text-sm text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-100">
<tr>
<th scope="col" class="px-3 py-2">Date</th>
<th scope="col" class="px-3 py-2">Set & Achievements</th>
</tr>
</thead>
<tbody>
{% endif %}
{% for topset in topsets %}
<tr class="border-b bg-white">
<td class="px-3 py-2 text-sm text-gray-900 whitespace-nowrap">
{{ topset.start_date | strftime }}
</td>
<td class="px-3 py-2 text-sm text-gray-900">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="whitespace-nowrap">{{ topset.repetitions }} x {{ topset.weight }}kg</span>
<div hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=topset.workout_id, topset_id=topset.topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"
class="flex flex-wrap items-center gap-1">
<!-- Badges load here -->
</div>
</div>
</td>
</tr>
{% endfor %}
{% if topsets|length == limit %}
<tr id="history-load-more-{{ source_topset_id }}-{{ offset + limit }}">
<td colspan="2" class="px-3 py-3 text-center">
<button
class="text-sm text-blue-600 hover:underline font-medium px-4 py-2 border border-blue-600 rounded"
hx-get="{{ url_for('workout.get_exercise_history', person_id=person_id, exercise_id=exercise_id, limit=limit, offset=offset + limit, source_topset_id=source_topset_id, forecast='true' if forecast_enabled else 'false', forecast_weeks=forecast_weeks, forecast_reps=forecast_target_reps, epoch=selected_epoch, min_date=min_date, max_date=max_date) }}"
hx-target="#history-load-more-{{ source_topset_id }}-{{ offset + limit }}" hx-swap="outerHTML">
Load More
</button>
</td>
</tr>
{% elif topsets|length == 0 and offset == 0 %}
<tr>
<td colspan="2" class="px-3 py-4 text-center text-gray-500">
No history found.
</td>
</tr>
{% endif %}
{% if offset == 0 %}
</tbody>
</table>
</div>
{% endif %}