doris-barbell: seed john split and routine board
This commit is contained in:
@@ -22,7 +22,7 @@ Tech stack: FastAPI, Jinja2, JSON state store for V1 bootstrap, Docker Compose o
|
||||
- Progress photos: out for V1.
|
||||
- Muscle-size tracking: not a priority.
|
||||
- Current height assumption for seed data: 68.5 inches unless John corrects it.
|
||||
- Exercise catalog: John will send the real exercise list later; V1 should support templates/routines once that lands.
|
||||
- Exercise catalog: John's current five-day split has now been imported as the default exercise/routine library with a default prescription of 3 sets x 10 reps; working weights are still pending.
|
||||
|
||||
## Current scaffold already started
|
||||
|
||||
@@ -45,6 +45,7 @@ Tech stack: FastAPI, Jinja2, JSON state store for V1 bootstrap, Docker Compose o
|
||||
- `GET /api/history/{user_id}/workouts`
|
||||
- `GET /api/progression/{user_id}/exercises`
|
||||
- Homepage now renders current metrics, recent workouts, exercise bests, and placeholder template/routine scaffolding.
|
||||
- Homepage now renders John's imported five-day split as the default routine board.
|
||||
- Homepage now includes mobile-friendly HTML forms for quick weight entry, body measurements, exercise templates, routine drafts, and quick workout logging.
|
||||
- Homepage now includes John-focused recent history snapshots so the app is useful before charts exist.
|
||||
- Tests currently cover health, seeded users, BMI calculation, imperial measurement logging, exercise template creation, routine creation, history ordering, structured workout logging, progression summaries, homepage render, and form-post redirects.
|
||||
@@ -86,7 +87,7 @@ Tech stack: FastAPI, Jinja2, JSON state store for V1 bootstrap, Docker Compose o
|
||||
|
||||
## Biggest open product questions still waiting on John
|
||||
|
||||
1. Exact exercise/routine list.
|
||||
1. Working weights for the imported split.
|
||||
2. Preferred auth mode for V1.
|
||||
3. Which extra non-photo metrics matter beyond weight, BMI, body fat, waist, hips, chest, and neck.
|
||||
4. Whether 68.5 inches should be treated as the current real height, or whether he wants 69 inches used instead.
|
||||
|
||||
@@ -41,11 +41,17 @@ Unless John overrides them later, V1 is being built with these defaults:
|
||||
The current scaffold already supports:
|
||||
|
||||
- seeded users for `john` and stub `manndra`
|
||||
- seeded exercise templates and John's current five-day split:
|
||||
- Monday: Chest
|
||||
- Tuesday: Back
|
||||
- Wednesday: Arms
|
||||
- Thursday: Shoulders
|
||||
- Friday: Legs
|
||||
- profile updates
|
||||
- weight logging with BMI calculation
|
||||
- imperial body measurements for waist, hips, chest, and neck
|
||||
- exercise template creation/listing
|
||||
- routine creation/listing, even before the final real workout split is known
|
||||
- routine creation/listing on top of John's imported base split
|
||||
- structured workout logging with nested exercises and sets
|
||||
- history endpoints for weight, measurements, and workouts
|
||||
- exercise progression summaries with max weight and estimated 1RM per exercise
|
||||
@@ -58,6 +64,7 @@ The current scaffold already supports:
|
||||
- basic dashboard summary plus John-facing history snapshots
|
||||
|
||||
John is currently seeded at a default height assumption of `68.5` inches until he says otherwise.
|
||||
His current imported split is stored with a default prescription of `3` sets and `10` reps for each listed movement, while working weights remain `TBD` until he fills them in.
|
||||
|
||||
## Current API surface
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
DEFAULT_STATE = {
|
||||
'users': {
|
||||
DEFAULT_USERS = {
|
||||
'john': {
|
||||
'id': 'john',
|
||||
'display_name': 'John',
|
||||
@@ -26,9 +25,133 @@ DEFAULT_STATE = {
|
||||
'goal_weight_lbs': None,
|
||||
'notes': 'Stubbed for future shared use.',
|
||||
},
|
||||
}
|
||||
|
||||
SEEDED_EXERCISE_TEMPLATES = [
|
||||
{'name': 'Bench Press', 'primary_muscle': 'chest', 'equipment': 'barbell'},
|
||||
{'name': 'Incline Bench Press', 'primary_muscle': 'upper chest', 'equipment': 'barbell'},
|
||||
{'name': 'Decline Bench Press', 'primary_muscle': 'lower chest', 'equipment': 'barbell'},
|
||||
{'name': 'Butterflies', 'primary_muscle': 'chest', 'equipment': 'machine'},
|
||||
{'name': 'Lat Pulldowns', 'primary_muscle': 'back', 'equipment': 'cable'},
|
||||
{'name': 'Bent-Over Supported Rows', 'primary_muscle': 'back', 'equipment': 'machine'},
|
||||
{'name': 'Landmine Rows', 'primary_muscle': 'back', 'equipment': 'barbell'},
|
||||
{'name': 'Seated Machine Rows', 'primary_muscle': 'back', 'equipment': 'machine'},
|
||||
{'name': 'Resistance Band Ab Holds', 'primary_muscle': 'abs', 'equipment': 'band'},
|
||||
{'name': 'Lat Pulldown Drop Set', 'primary_muscle': 'back', 'equipment': 'cable'},
|
||||
{'name': 'Bicep Curls', 'primary_muscle': 'biceps', 'equipment': 'dumbbells'},
|
||||
{'name': 'Front Tricep Extensions', 'primary_muscle': 'triceps', 'equipment': 'cable'},
|
||||
{'name': 'Overhead Tricep Extensions', 'primary_muscle': 'triceps', 'equipment': 'cable'},
|
||||
{'name': 'Incline Barbell Curls', 'primary_muscle': 'biceps', 'equipment': 'barbell'},
|
||||
{'name': 'Isolation Bicep Curls', 'primary_muscle': 'biceps', 'equipment': 'machine'},
|
||||
{'name': '7-7-7 Barbell Curl', 'primary_muscle': 'biceps', 'equipment': 'barbell'},
|
||||
{'name': 'Suitcase Carry', 'primary_muscle': 'abs', 'equipment': 'dumbbell'},
|
||||
{'name': 'Shoulder Press', 'primary_muscle': 'shoulders', 'equipment': 'machine'},
|
||||
{'name': 'Arnold Press', 'primary_muscle': 'shoulders', 'equipment': 'dumbbells'},
|
||||
{'name': 'Side Lateral Raise', 'primary_muscle': 'shoulders', 'equipment': 'dumbbells'},
|
||||
{'name': 'Front Lateral Raise', 'primary_muscle': 'shoulders', 'equipment': 'dumbbells'},
|
||||
{'name': 'Upright Row', 'primary_muscle': 'shoulders', 'equipment': 'barbell'},
|
||||
{'name': 'Shoulder Shrugs', 'primary_muscle': 'traps', 'equipment': 'dumbbells'},
|
||||
{'name': '7-7-7 Shoulder Press', 'primary_muscle': 'shoulders', 'equipment': 'machine'},
|
||||
{'name': 'Resistance Band Punch-Out Holds', 'primary_muscle': 'abs', 'equipment': 'band'},
|
||||
{'name': 'Squats', 'primary_muscle': 'legs', 'equipment': 'barbell'},
|
||||
{'name': 'Leg Extensions', 'primary_muscle': 'quads', 'equipment': 'machine'},
|
||||
{'name': 'Step Ups', 'primary_muscle': 'legs', 'equipment': 'bodyweight'},
|
||||
{'name': 'Deadlifts', 'primary_muscle': 'posterior chain', 'equipment': 'barbell'},
|
||||
{'name': 'Calf Raises', 'primary_muscle': 'calves', 'equipment': 'machine'},
|
||||
{'name': 'Resistance Band Crunches', 'primary_muscle': 'abs', 'equipment': 'band'},
|
||||
]
|
||||
|
||||
SEEDED_ROUTINES = [
|
||||
{
|
||||
'user_id': 'john',
|
||||
'name': 'Monday: Chest',
|
||||
'notes': "Imported from John's current routine. Default prescription is 3 sets of 10 reps. Weights are still TBD.",
|
||||
'exercises': [
|
||||
{'exercise_name': 'Bench Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Incline Bench Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Decline Bench Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Butterflies', 'target_sets': 3, 'target_reps': '10'},
|
||||
],
|
||||
},
|
||||
'exercise_templates': [],
|
||||
'routines': [],
|
||||
{
|
||||
'user_id': 'john',
|
||||
'name': 'Tuesday: Back',
|
||||
'notes': "Imported from John's current routine. Default prescription is 3 sets of 10 reps. Weights are still TBD.",
|
||||
'exercises': [
|
||||
{'exercise_name': 'Lat Pulldowns', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Bent-Over Supported Rows', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Landmine Rows', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Seated Machine Rows', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Resistance Band Ab Holds', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Lat Pulldown Drop Set', 'target_sets': 3, 'target_reps': '10'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'user_id': 'john',
|
||||
'name': 'Wednesday: Arms',
|
||||
'notes': "Imported from John's current routine. Default prescription is 3 sets of 10 reps. Weights are still TBD. Includes Bis and Tris focus plus suitcase carries for abs.",
|
||||
'exercises': [
|
||||
{'exercise_name': 'Bicep Curls', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Front Tricep Extensions', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Overhead Tricep Extensions', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Incline Barbell Curls', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Isolation Bicep Curls', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': '7-7-7 Barbell Curl', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Suitcase Carry', 'target_sets': 3, 'target_reps': '10'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'user_id': 'john',
|
||||
'name': 'Thursday: Shoulders',
|
||||
'notes': "Imported from John's current routine. Default prescription is 3 sets of 10 reps. Weights are still TBD. Includes resistance band punch-out ab holds.",
|
||||
'exercises': [
|
||||
{'exercise_name': 'Shoulder Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Arnold Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Side Lateral Raise', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Front Lateral Raise', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Upright Row', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Shoulder Shrugs', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': '7-7-7 Shoulder Press', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Resistance Band Punch-Out Holds', 'target_sets': 3, 'target_reps': '10'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'user_id': 'john',
|
||||
'name': 'Friday: Legs',
|
||||
'notes': "Imported from John's current routine. Default prescription is 3 sets of 10 reps. Weights are still TBD. Includes resistance band crunches for abs.",
|
||||
'exercises': [
|
||||
{'exercise_name': 'Squats', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Leg Extensions', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Step Ups', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Deadlifts', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Calf Raises', 'target_sets': 3, 'target_reps': '10'},
|
||||
{'exercise_name': 'Resistance Band Crunches', 'target_sets': 3, 'target_reps': '10'},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_STATE = {
|
||||
'users': DEFAULT_USERS,
|
||||
'exercise_templates': [
|
||||
{
|
||||
'id': f'seed-template-{index + 1:02d}',
|
||||
'name': template['name'],
|
||||
'primary_muscle': template.get('primary_muscle'),
|
||||
'equipment': template.get('equipment'),
|
||||
'notes': template.get('notes'),
|
||||
}
|
||||
for index, template in enumerate(SEEDED_EXERCISE_TEMPLATES)
|
||||
],
|
||||
'routines': [
|
||||
{
|
||||
'id': f'seed-routine-{index + 1:02d}',
|
||||
'user_id': routine['user_id'],
|
||||
'name': routine['name'],
|
||||
'notes': routine.get('notes'),
|
||||
'exercises': [dict(exercise) for exercise in routine['exercises']],
|
||||
}
|
||||
for index, routine in enumerate(SEEDED_ROUTINES)
|
||||
],
|
||||
'weight_entries': [],
|
||||
'measurement_entries': [],
|
||||
'workout_sessions': [],
|
||||
@@ -42,6 +165,11 @@ class JSONStore:
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not self.state_path.exists():
|
||||
self._write(deepcopy(DEFAULT_STATE))
|
||||
return
|
||||
state = self._read()
|
||||
seeded_state = _ensure_seeded_library(state)
|
||||
if seeded_state != state:
|
||||
self._write(seeded_state)
|
||||
|
||||
def _read(self) -> dict[str, Any]:
|
||||
return json.loads(self.state_path.read_text())
|
||||
@@ -243,11 +371,13 @@ class JSONStore:
|
||||
key=lambda item: item['performed_at'],
|
||||
reverse=True,
|
||||
)[:5]
|
||||
recent_templates = list(reversed(state['exercise_templates'][-5:]))
|
||||
routine_board = _routine_board(state['routines'])
|
||||
return {
|
||||
'users': users,
|
||||
'recent_workouts': recent_workouts,
|
||||
'exercise_templates': self.list_exercise_templates()[:5],
|
||||
'routines': self.list_routines()[:5],
|
||||
'exercise_templates': recent_templates,
|
||||
'routines': routine_board,
|
||||
'counts': {
|
||||
'exercise_templates': len(state['exercise_templates']),
|
||||
'measurement_entries': len(state['measurement_entries']),
|
||||
@@ -276,6 +406,76 @@ def _history_for_user(entries: list[dict[str, Any]], user_id: str, sort_key: str
|
||||
return sorted(matches, key=lambda item: item[sort_key], reverse=True)
|
||||
|
||||
|
||||
def _ensure_seeded_library(state: dict[str, Any]) -> dict[str, Any]:
|
||||
payload = deepcopy(state)
|
||||
payload.setdefault('users', deepcopy(DEFAULT_USERS))
|
||||
payload.setdefault('exercise_templates', [])
|
||||
payload.setdefault('routines', [])
|
||||
payload.setdefault('weight_entries', [])
|
||||
payload.setdefault('measurement_entries', [])
|
||||
payload.setdefault('workout_sessions', [])
|
||||
|
||||
existing_template_names = {
|
||||
item.get('name', '').strip().lower()
|
||||
for item in payload['exercise_templates']
|
||||
if item.get('name')
|
||||
}
|
||||
for index, template in enumerate(SEEDED_EXERCISE_TEMPLATES, start=1):
|
||||
normalized_name = template['name'].strip().lower()
|
||||
if normalized_name in existing_template_names:
|
||||
continue
|
||||
payload['exercise_templates'].append(
|
||||
{
|
||||
'id': f'seed-template-{index:02d}',
|
||||
'name': template['name'],
|
||||
'primary_muscle': template.get('primary_muscle'),
|
||||
'equipment': template.get('equipment'),
|
||||
'notes': template.get('notes'),
|
||||
}
|
||||
)
|
||||
existing_template_names.add(normalized_name)
|
||||
|
||||
existing_routines = {
|
||||
item.get('name', '').strip().lower(): item
|
||||
for item in payload['routines']
|
||||
if item.get('name')
|
||||
}
|
||||
for index, routine in enumerate(SEEDED_ROUTINES, start=1):
|
||||
normalized_name = routine['name'].strip().lower()
|
||||
existing_routine = existing_routines.get(normalized_name)
|
||||
seeded_exercises = [dict(exercise) for exercise in routine['exercises']]
|
||||
if existing_routine is not None:
|
||||
existing_routine['user_id'] = routine['user_id']
|
||||
existing_routine['notes'] = routine.get('notes')
|
||||
existing_routine['exercises'] = seeded_exercises
|
||||
continue
|
||||
new_routine = {
|
||||
'id': f'seed-routine-{index:02d}',
|
||||
'user_id': routine['user_id'],
|
||||
'name': routine['name'],
|
||||
'notes': routine.get('notes'),
|
||||
'exercises': seeded_exercises,
|
||||
}
|
||||
payload['routines'].append(new_routine)
|
||||
existing_routines[normalized_name] = new_routine
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _routine_board(routines: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
by_name = {
|
||||
routine.get('name', '').strip().lower(): routine
|
||||
for routine in routines
|
||||
if routine.get('name')
|
||||
}
|
||||
ordered = []
|
||||
for seeded_routine in SEEDED_ROUTINES:
|
||||
matched = by_name.get(seeded_routine['name'].strip().lower())
|
||||
if matched is not None:
|
||||
ordered.append(matched)
|
||||
return ordered
|
||||
|
||||
|
||||
def _trim_measurement_entry(entry: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
@@ -171,28 +171,35 @@
|
||||
</article>
|
||||
|
||||
<article class="card evidence-board">
|
||||
<div class="case-legend"><span class="section-label">Library snapshot</span><span class="pill">Scaffold</span></div>
|
||||
<h2>Template and routine scaffold</h2>
|
||||
<p class="muted">You can start building the library now, even before the real split lands tomorrow.</p>
|
||||
<div class="case-legend"><span class="section-label">Current split</span><span class="pill">Routine board</span></div>
|
||||
<h2>Current routine board</h2>
|
||||
<p class="muted">John's current five-day split is now loaded as the default routine library with a default prescription of 3 sets of 10 reps. Weights are still TBD.</p>
|
||||
<ul class="list case-list">
|
||||
<li>Exercise templates: {{ summary.counts.exercise_templates }}</li>
|
||||
<li>Routines: {{ summary.counts.routines }}</li>
|
||||
</ul>
|
||||
{% if summary.routines %}
|
||||
<div class="history-block">
|
||||
<h3>Loaded routine days</h3>
|
||||
<ul class="list">
|
||||
{% for routine in summary.routines %}
|
||||
<li>
|
||||
<strong>{{ routine.name }}</strong>
|
||||
<p class="muted">{{ routine.exercises|length }} exercises{% if routine.notes %} · {{ routine.notes }}{% endif %}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if summary.exercise_templates %}
|
||||
<p><strong>Templates</strong></p>
|
||||
<div class="history-block">
|
||||
<h3>Template snapshot</h3>
|
||||
<ul class="list">
|
||||
{% for template in summary.exercise_templates %}
|
||||
<li>{{ template.name }}{% if template.equipment %} · {{ template.equipment }}{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if summary.routines %}
|
||||
<p><strong>Routines</strong></p>
|
||||
<ul class="list">
|
||||
{% for routine in summary.routines %}
|
||||
<li>{{ routine.name }} · {{ routine.exercises|length }} exercises</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -30,14 +31,37 @@ def test_dashboard_summary_seeds_john_and_manndra(tmp_path: Path) -> None:
|
||||
|
||||
assert user_ids == ['john', 'manndra']
|
||||
assert payload['counts'] == {
|
||||
'exercise_templates': 0,
|
||||
'exercise_templates': 31,
|
||||
'measurement_entries': 0,
|
||||
'routines': 0,
|
||||
'routines': 5,
|
||||
'weight_entries': 0,
|
||||
'workout_sessions': 0,
|
||||
}
|
||||
john = next(user for user in payload['users'] if user['id'] == 'john')
|
||||
assert john['measurement_system'] == 'imperial'
|
||||
routine_names = [routine['name'] for routine in payload['routines']]
|
||||
assert routine_names == [
|
||||
'Monday: Chest',
|
||||
'Tuesday: Back',
|
||||
'Wednesday: Arms',
|
||||
'Thursday: Shoulders',
|
||||
'Friday: Legs',
|
||||
]
|
||||
assert payload['routines'][0]['exercises'][0] == {
|
||||
'exercise_name': 'Bench Press',
|
||||
'target_sets': 3,
|
||||
'target_reps': '10',
|
||||
}
|
||||
assert '3 sets of 10 reps' in payload['routines'][0]['notes']
|
||||
assert 'Weights are still TBD' in payload['routines'][0]['notes']
|
||||
template_names = [template['name'] for template in payload['exercise_templates']]
|
||||
assert template_names == [
|
||||
'Resistance Band Crunches',
|
||||
'Calf Raises',
|
||||
'Deadlifts',
|
||||
'Step Ups',
|
||||
'Leg Extensions',
|
||||
]
|
||||
|
||||
|
||||
def test_updating_profile_and_logging_weight_computes_bmi(tmp_path: Path) -> None:
|
||||
@@ -132,11 +156,14 @@ def test_can_create_and_list_exercise_templates(tmp_path: Path) -> None:
|
||||
list_response = client.get('/api/exercises/templates')
|
||||
assert list_response.status_code == 200
|
||||
payload = list_response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]['name'] == 'Bench Press'
|
||||
assert len(payload) == 32
|
||||
assert any(
|
||||
item['name'] == 'Bench Press' and item['notes'] == 'Flat bench barbell press.'
|
||||
for item in payload
|
||||
)
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
assert summary['counts']['exercise_templates'] == 1
|
||||
assert summary['counts']['exercise_templates'] == 32
|
||||
|
||||
|
||||
def test_can_create_routine_before_real_program_arrives(tmp_path: Path) -> None:
|
||||
@@ -181,11 +208,11 @@ def test_can_create_routine_before_real_program_arrives(tmp_path: Path) -> None:
|
||||
list_response = client.get('/api/routines')
|
||||
assert list_response.status_code == 200
|
||||
payload = list_response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]['name'] == 'Push Day Placeholder'
|
||||
assert len(payload) == 6
|
||||
assert any(routine['name'] == 'Push Day Placeholder' for routine in payload)
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
assert summary['counts']['routines'] == 1
|
||||
assert summary['counts']['routines'] == 6
|
||||
|
||||
|
||||
def test_history_endpoints_return_newest_first(tmp_path: Path) -> None:
|
||||
@@ -343,7 +370,106 @@ def test_homepage_renders_core_sections_and_forms(tmp_path: Path) -> None:
|
||||
assert 'Current body metrics' in body
|
||||
assert 'Log weight' in body
|
||||
assert 'Add exercise template' in body
|
||||
assert 'Build routine draft' in body
|
||||
assert 'Current routine board' in body
|
||||
assert 'Monday: Chest' in body
|
||||
|
||||
|
||||
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
|
||||
state_path = tmp_path / 'state.json'
|
||||
state_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
'users': {
|
||||
'john': {
|
||||
'id': 'john',
|
||||
'display_name': 'John',
|
||||
'is_stub': False,
|
||||
'measurement_system': 'imperial',
|
||||
'height_inches': 68.5,
|
||||
'goal_weight_lbs': None,
|
||||
'notes': 'Legacy state',
|
||||
},
|
||||
'manndra': {
|
||||
'id': 'manndra',
|
||||
'display_name': 'Manndra',
|
||||
'is_stub': True,
|
||||
'measurement_system': 'imperial',
|
||||
'height_inches': None,
|
||||
'goal_weight_lbs': None,
|
||||
'notes': 'Legacy stub',
|
||||
},
|
||||
},
|
||||
'exercise_templates': [],
|
||||
'routines': [],
|
||||
'weight_entries': [],
|
||||
'measurement_entries': [],
|
||||
'workout_sessions': [],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
client = make_client(tmp_path)
|
||||
payload = client.get('/api/dashboard/summary').json()
|
||||
|
||||
assert payload['counts']['exercise_templates'] == 31
|
||||
assert payload['counts']['routines'] == 5
|
||||
assert payload['routines'][0]['name'] == 'Monday: Chest'
|
||||
assert payload['routines'][0]['exercises'][0]['target_sets'] == 3
|
||||
assert payload['routines'][0]['exercises'][0]['target_reps'] == '10'
|
||||
|
||||
|
||||
def test_existing_seeded_routines_upgrade_prescriptions_to_three_by_ten(tmp_path: Path) -> None:
|
||||
state_path = tmp_path / 'state.json'
|
||||
state_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
'users': {
|
||||
'john': {
|
||||
'id': 'john',
|
||||
'display_name': 'John',
|
||||
'is_stub': False,
|
||||
'measurement_system': 'imperial',
|
||||
'height_inches': 68.5,
|
||||
'goal_weight_lbs': None,
|
||||
'notes': 'Legacy state',
|
||||
},
|
||||
'manndra': {
|
||||
'id': 'manndra',
|
||||
'display_name': 'Manndra',
|
||||
'is_stub': True,
|
||||
'measurement_system': 'imperial',
|
||||
'height_inches': None,
|
||||
'goal_weight_lbs': None,
|
||||
'notes': 'Legacy stub',
|
||||
},
|
||||
},
|
||||
'exercise_templates': [],
|
||||
'routines': [
|
||||
{
|
||||
'id': 'seed-routine-01',
|
||||
'user_id': 'john',
|
||||
'name': 'Monday: Chest',
|
||||
'notes': 'Imported from John\'s current routine. Set and rep targets were not provided yet.',
|
||||
'exercises': [
|
||||
{'exercise_name': 'Bench Press', 'target_sets': 1, 'target_reps': 'TBD'},
|
||||
{'exercise_name': 'Incline Bench Press', 'target_sets': 1, 'target_reps': 'TBD'},
|
||||
],
|
||||
}
|
||||
],
|
||||
'weight_entries': [],
|
||||
'measurement_entries': [],
|
||||
'workout_sessions': [],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
client = make_client(tmp_path)
|
||||
payload = client.get('/api/dashboard/summary').json()
|
||||
|
||||
monday = next(routine for routine in payload['routines'] if routine['name'] == 'Monday: Chest')
|
||||
assert monday['exercises'][0]['target_sets'] == 3
|
||||
assert monday['exercises'][0]['target_reps'] == '10'
|
||||
assert 'Weights are still TBD' in monday['notes']
|
||||
|
||||
|
||||
def test_weight_form_submission_redirects_and_updates_dashboard(tmp_path: Path) -> None:
|
||||
@@ -404,8 +530,8 @@ def test_template_and_routine_form_submissions_redirect_and_render_entries(tmp_p
|
||||
page = client.get('/')
|
||||
assert page.status_code == 200
|
||||
assert 'Leg Press' in page.text
|
||||
assert 'Leg Day Draft' in page.text
|
||||
|
||||
routines = client.get('/api/routines').json()
|
||||
assert routines[0]['exercises'][0]['exercise_name'] == 'Leg Press'
|
||||
assert routines[0]['exercises'][1]['notes'] == 'Slow negative'
|
||||
saved_routine = next(routine for routine in routines if routine['name'] == 'Leg Day Draft')
|
||||
assert saved_routine['exercises'][0]['exercise_name'] == 'Leg Press'
|
||||
assert saved_routine['exercises'][1]['notes'] == 'Slow negative'
|
||||
|
||||
Reference in New Issue
Block a user