doris-barbell: seed john split and routine board
Some checks failed
secret-guardrails / artifact-secret-scan (push) Has been cancelled
secret-guardrails / gitleaks (push) Has been cancelled

This commit is contained in:
Fizzlepoof
2026-05-24 02:59:14 +00:00
parent 9e0f66c2b6
commit a4c5ee7285
5 changed files with 397 additions and 56 deletions

View File

@@ -22,7 +22,7 @@ Tech stack: FastAPI, Jinja2, JSON state store for V1 bootstrap, Docker Compose o
- Progress photos: out for V1. - Progress photos: out for V1.
- Muscle-size tracking: not a priority. - Muscle-size tracking: not a priority.
- Current height assumption for seed data: 68.5 inches unless John corrects it. - 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 ## 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/history/{user_id}/workouts`
- `GET /api/progression/{user_id}/exercises` - `GET /api/progression/{user_id}/exercises`
- Homepage now renders current metrics, recent workouts, exercise bests, and placeholder template/routine scaffolding. - 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 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. - 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. - 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 ## 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. 2. Preferred auth mode for V1.
3. Which extra non-photo metrics matter beyond weight, BMI, body fat, waist, hips, chest, and neck. 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. 4. Whether 68.5 inches should be treated as the current real height, or whether he wants 69 inches used instead.

View File

@@ -41,11 +41,17 @@ Unless John overrides them later, V1 is being built with these defaults:
The current scaffold already supports: The current scaffold already supports:
- seeded users for `john` and stub `manndra` - 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 - profile updates
- weight logging with BMI calculation - weight logging with BMI calculation
- imperial body measurements for waist, hips, chest, and neck - imperial body measurements for waist, hips, chest, and neck
- exercise template creation/listing - 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 - structured workout logging with nested exercises and sets
- history endpoints for weight, measurements, and workouts - history endpoints for weight, measurements, and workouts
- exercise progression summaries with max weight and estimated 1RM per exercise - 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 - 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. 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 ## Current API surface

View File

@@ -6,29 +6,152 @@ from pathlib import Path
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
DEFAULT_STATE = { DEFAULT_USERS = {
'users': { 'john': {
'john': { 'id': 'john',
'id': 'john', 'display_name': 'John',
'display_name': 'John', 'is_stub': False,
'is_stub': False, 'measurement_system': 'imperial',
'measurement_system': 'imperial', 'height_inches': 68.5,
'height_inches': 68.5, 'goal_weight_lbs': None,
'goal_weight_lbs': None, 'notes': 'Seeded with John default height assumption from current intake.',
'notes': 'Seeded with John default height assumption from current intake.',
},
'manndra': {
'id': 'manndra',
'display_name': 'Manndra',
'is_stub': True,
'measurement_system': 'imperial',
'height_inches': None,
'goal_weight_lbs': None,
'notes': 'Stubbed for future shared use.',
},
}, },
'exercise_templates': [], 'manndra': {
'routines': [], 'id': 'manndra',
'display_name': 'Manndra',
'is_stub': True,
'measurement_system': 'imperial',
'height_inches': None,
'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'},
],
},
{
'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': [], 'weight_entries': [],
'measurement_entries': [], 'measurement_entries': [],
'workout_sessions': [], 'workout_sessions': [],
@@ -42,6 +165,11 @@ class JSONStore:
self.state_dir.mkdir(parents=True, exist_ok=True) self.state_dir.mkdir(parents=True, exist_ok=True)
if not self.state_path.exists(): if not self.state_path.exists():
self._write(deepcopy(DEFAULT_STATE)) 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]: def _read(self) -> dict[str, Any]:
return json.loads(self.state_path.read_text()) return json.loads(self.state_path.read_text())
@@ -243,11 +371,13 @@ class JSONStore:
key=lambda item: item['performed_at'], key=lambda item: item['performed_at'],
reverse=True, reverse=True,
)[:5] )[:5]
recent_templates = list(reversed(state['exercise_templates'][-5:]))
routine_board = _routine_board(state['routines'])
return { return {
'users': users, 'users': users,
'recent_workouts': recent_workouts, 'recent_workouts': recent_workouts,
'exercise_templates': self.list_exercise_templates()[:5], 'exercise_templates': recent_templates,
'routines': self.list_routines()[:5], 'routines': routine_board,
'counts': { 'counts': {
'exercise_templates': len(state['exercise_templates']), 'exercise_templates': len(state['exercise_templates']),
'measurement_entries': len(state['measurement_entries']), '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) 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: def _trim_measurement_entry(entry: dict[str, Any] | None) -> dict[str, Any] | None:
if entry is None: if entry is None:
return None return None

View File

@@ -171,28 +171,35 @@
</article> </article>
<article class="card evidence-board"> <article class="card evidence-board">
<div class="case-legend"><span class="section-label">Library snapshot</span><span class="pill">Scaffold</span></div> <div class="case-legend"><span class="section-label">Current split</span><span class="pill">Routine board</span></div>
<h2>Template and routine scaffold</h2> <h2>Current routine board</h2>
<p class="muted">You can start building the library now, even before the real split lands tomorrow.</p> <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"> <ul class="list case-list">
<li>Exercise templates: {{ summary.counts.exercise_templates }}</li> <li>Exercise templates: {{ summary.counts.exercise_templates }}</li>
<li>Routines: {{ summary.counts.routines }}</li> <li>Routines: {{ summary.counts.routines }}</li>
</ul> </ul>
{% if summary.exercise_templates %}
<p><strong>Templates</strong></p>
<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 %} {% if summary.routines %}
<p><strong>Routines</strong></p> <div class="history-block">
<ul class="list"> <h3>Loaded routine days</h3>
{% for routine in summary.routines %} <ul class="list">
<li>{{ routine.name }} · {{ routine.exercises|length }} exercises</li> {% for routine in summary.routines %}
{% endfor %} <li>
</ul> <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 %}
<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>
</div>
{% endif %} {% endif %}
</article> </article>
</section> </section>

View File

@@ -1,3 +1,4 @@
import json
from pathlib import Path from pathlib import Path
from fastapi.testclient import TestClient 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 user_ids == ['john', 'manndra']
assert payload['counts'] == { assert payload['counts'] == {
'exercise_templates': 0, 'exercise_templates': 31,
'measurement_entries': 0, 'measurement_entries': 0,
'routines': 0, 'routines': 5,
'weight_entries': 0, 'weight_entries': 0,
'workout_sessions': 0, 'workout_sessions': 0,
} }
john = next(user for user in payload['users'] if user['id'] == 'john') john = next(user for user in payload['users'] if user['id'] == 'john')
assert john['measurement_system'] == 'imperial' 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: 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') list_response = client.get('/api/exercises/templates')
assert list_response.status_code == 200 assert list_response.status_code == 200
payload = list_response.json() payload = list_response.json()
assert len(payload) == 1 assert len(payload) == 32
assert payload[0]['name'] == 'Bench Press' assert any(
item['name'] == 'Bench Press' and item['notes'] == 'Flat bench barbell press.'
for item in payload
)
summary = client.get('/api/dashboard/summary').json() 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: 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') list_response = client.get('/api/routines')
assert list_response.status_code == 200 assert list_response.status_code == 200
payload = list_response.json() payload = list_response.json()
assert len(payload) == 1 assert len(payload) == 6
assert payload[0]['name'] == 'Push Day Placeholder' assert any(routine['name'] == 'Push Day Placeholder' for routine in payload)
summary = client.get('/api/dashboard/summary').json() 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: 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 'Current body metrics' in body
assert 'Log weight' in body assert 'Log weight' in body
assert 'Add exercise template' 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: 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('/') page = client.get('/')
assert page.status_code == 200 assert page.status_code == 200
assert 'Leg Press' in page.text assert 'Leg Press' in page.text
assert 'Leg Day Draft' in page.text
routines = client.get('/api/routines').json() routines = client.get('/api/routines').json()
assert routines[0]['exercises'][0]['exercise_name'] == 'Leg Press' saved_routine = next(routine for routine in routines if routine['name'] == 'Leg Day Draft')
assert routines[0]['exercises'][1]['notes'] == 'Slow negative' assert saved_routine['exercises'][0]['exercise_name'] == 'Leg Press'
assert saved_routine['exercises'][1]['notes'] == 'Slow negative'