Fix Doris Barbell band logging and nav links
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-28 21:06:46 +00:00
parent dd4134b1ce
commit ba958f7ef6
9 changed files with 165 additions and 43 deletions

View File

@@ -61,7 +61,7 @@ class RoutineCreate(BaseModel):
class WorkoutSetCreate(BaseModel):
reps: int = Field(ge=1)
weight_lbs: float = Field(ge=0)
weight_lbs: float | None = Field(default=None, ge=0)
rpe: float | None = Field(default=None, ge=0)
rest_seconds: int | None = Field(default=None, ge=0)
notes: str | None = None

View File

@@ -132,7 +132,7 @@ def routine_log_sheet(request: Request, routine_id: str):
context={
'title': f"Log {routine['name']}",
'routine': routine,
'routine_sheet': _build_routine_log_sheet(routine),
'routine_sheet': _build_routine_log_sheet(request, routine),
'exercise_options': _exercise_options_for_routine(request, routine),
'routine_action_label': _routine_action_label(routine['name']),
'default_performed_at': _default_performed_at_value(),
@@ -261,24 +261,29 @@ def _get_routine_or_404(request: Request, routine_id: str) -> dict:
return routine
def _build_routine_log_sheet(routine: dict) -> list[dict]:
def _build_routine_log_sheet(request: Request, routine: dict) -> list[dict]:
equipment_by_name = _exercise_equipment_lookup(request)
sheet = []
for exercise_index, exercise in enumerate(routine.get('exercises', [])):
exercise_name = exercise['exercise_name']
target_sets = max(int(exercise.get('target_sets', 1) or 1), 1)
default_reps = _default_reps_value(exercise.get('target_reps'))
is_band = equipment_by_name.get(exercise_name.strip().lower()) == 'band'
sheet.append(
{
'exercise_index': exercise_index,
'exercise_name': exercise['exercise_name'],
'exercise_name': exercise_name,
'target_sets': target_sets,
'target_reps': exercise.get('target_reps'),
'default_reps': default_reps,
'notes': exercise.get('notes'),
'is_band': is_band,
'sets': [
{
'exercise_index': exercise_index,
'index': set_index,
'default_reps': default_reps,
'is_band': is_band,
}
for set_index in range(target_sets)
],
@@ -305,6 +310,17 @@ def _exercise_options_for_routine(request: Request, routine: dict) -> list[str]:
return ordered
def _exercise_equipment_lookup(request: Request) -> dict[str, str]:
lookup: dict[str, str] = {}
for template in request.app.state.store.list_exercise_templates():
name = str(template.get('name', '')).strip().lower()
equipment = str(template.get('equipment', '')).strip().lower()
if not name or not equipment:
continue
lookup[name] = equipment
return lookup
def _default_reps_value(target_reps: str | None) -> str:
if target_reps is None:
return ''
@@ -332,11 +348,13 @@ def _routine_action_label(routine_name: str) -> str:
async def _parse_structured_routine_form(request: Request, routine: dict, exercise_count: int) -> list[dict]:
form = await request.form()
routine_exercises = routine.get('exercises', [])
equipment_by_name = _exercise_equipment_lookup(request)
exercises = []
for exercise_index in range(exercise_count):
exercise_name = str(form.get(f'exercise_{exercise_index}_name', '')).strip()
if not exercise_name:
continue
is_band_exercise = equipment_by_name.get(exercise_name.lower()) == 'band'
try:
set_count = int(str(form.get(f'exercise_{exercise_index}_set_count', '0')).strip())
except ValueError as exc:
@@ -350,17 +368,25 @@ async def _parse_structured_routine_form(request: Request, routine: dict, exerci
reps_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_reps', '')).strip()
weight_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_weight_lbs', '')).strip()
if not reps_raw and not weight_raw:
continue
if not reps_raw and weight_raw and default_reps_raw.isdigit():
if is_band_exercise and default_reps_raw.isdigit():
reps_raw = default_reps_raw
if not reps_raw or not weight_raw:
else:
continue
if not reps_raw and default_reps_raw.isdigit():
reps_raw = default_reps_raw
if not reps_raw:
raise HTTPException(
status_code=400,
detail=f'Exercise {exercise_name} set {set_index + 1} needs reps.',
)
if not weight_raw and not is_band_exercise:
raise HTTPException(
status_code=400,
detail=f'Exercise {exercise_name} set {set_index + 1} needs both reps and weight.',
)
try:
reps = int(reps_raw)
weight_lbs = float(weight_raw)
weight_lbs = None if not weight_raw else float(weight_raw)
except ValueError as exc:
raise HTTPException(
status_code=400,

View File

@@ -289,7 +289,9 @@ class JSONStore:
normalized_sets = []
for item in exercise['sets']:
set_count += 1
total_volume_lbs += item['reps'] * item['weight_lbs']
weight_lbs = item.get('weight_lbs')
if weight_lbs is not None:
total_volume_lbs += item['reps'] * weight_lbs
normalized_sets.append(dict(item))
exercises.append({
'exercise_name': exercise['exercise_name'],
@@ -340,10 +342,13 @@ class JSONStore:
stats['last_performed_at'] = workout['performed_at']
for item in exercise['sets']:
stats['set_count'] += 1
stats['max_weight_lbs'] = max(stats['max_weight_lbs'], item['weight_lbs'])
weight_lbs = item.get('weight_lbs')
if weight_lbs is None:
continue
stats['max_weight_lbs'] = max(stats['max_weight_lbs'], weight_lbs)
stats['best_set_estimated_1rm'] = max(
stats['best_set_estimated_1rm'],
_estimated_1rm(item['weight_lbs'], item['reps']),
_estimated_1rm(weight_lbs, item['reps']),
)
results = []
for item in progression.values():

View File

@@ -34,6 +34,7 @@
<span class="pill badge-hotfuzz">For the Greater Good</span>
</div>
</div>
<div class="art-stack">
<div class="hot-fuzz-art compact-hot-fuzz-art" aria-hidden="true">
<svg viewBox="0 0 320 140" class="poster-illustration compact-hero-mark" role="presentation">
<defs>
@@ -60,6 +61,11 @@
<text x="160" y="118" class="poster-callout">TRAINING DOSSIER</text>
</svg>
</div>
<figure class="photo-evidence-card hero-photo-card supporting-photo-card">
<img src="/static/images/hotfuzz-training-dossier.jpg" alt="Gym training photo with cable machine station, bench, and weight stacks." loading="eager">
<figcaption>Training still · source logged in docs</figcaption>
</figure>
</div>
</div>
<nav class="case-nav" aria-label="Doris Barbell sections">
<a class="case-nav-link active" href="/#body-metrics">Body metrics</a>
@@ -69,6 +75,43 @@
</nav>
</header>
<main class="page-shell compact-page-shell">
<section class="family-portal evidence-board casefile-directory">
<div class="section-heading">
<div>
<p class="section-label">Singular front door</p>
<h2>Doris family directory</h2>
</div>
<span class="pill">Casefile directory</span>
</div>
<p class="muted case-legend">One shell, separate runtimes. Jump between training, household ops, and coursework without losing the same caseboard language.</p>
<div class="family-directory-grid">
<a class="family-app-card current-desk" href="https://gym.paccoco.com/">
<span class="family-app-kicker">Current desk</span>
<strong>Doris Barbell</strong>
<small>Workout logs, body metrics, and progression dossiers.</small>
</a>
<a class="family-app-card" href="http://10.5.30.7:8787/">
<span class="family-app-kicker">Operator portal</span>
<strong>Doris Dashboard</strong>
<small>Canonical front door for alerts, briefings, and launch control.</small>
</a>
<a class="family-app-card" href="http://10.5.30.7:8787/services.html">
<span class="family-app-kicker">Switchboard</span>
<strong>Services Directory</strong>
<small>Dense portal card wall for the broader stack.</small>
</a>
<a class="family-app-card" href="http://10.5.30.7:8092/">
<span class="family-app-kicker">Pantry desk</span>
<strong>Doris Kitchen</strong>
<small>Recipe triage, search warrants, and pantry planning.</small>
</a>
<a class="family-app-card" href="https://schoolhouse.paccoco.com/">
<span class="family-app-kicker">Records desk</span>
<strong>Doris Schoolhouse</strong>
<small>Assignments, recordings, and archive intake.</small>
</a>
</div>
</section>
{% block content %}{% endblock %}
</main>
</body>

View File

@@ -66,6 +66,9 @@
</select>
</label>
<p class="muted">Target reps: {{ exercise.target_reps or 'enter manually' }}</p>
{% if exercise.is_band %}
<p class="muted">Resistance-band exercise: leave weight blank to log sets and default reps only.</p>
{% endif %}
<div class="grid cols-2 compact-grid">
{% for set in exercise.sets %}
<div class="evidence-slab compact-slab">

View File

@@ -386,7 +386,8 @@ def test_homepage_renders_core_sections_and_forms(tmp_path: Path) -> None:
assert 'Add exercise template' in body
assert 'Current routine board' in body
assert 'Monday: Chest' in body
assert 'Doris family directory' not in body
assert 'Doris family directory' in body
assert 'Singular front door' in body
assert 'V1 focus' not in body
assert 'Next likely build' not in body
@@ -505,6 +506,50 @@ def test_seeded_routine_log_uses_numeric_target_reps_when_reps_left_blank(tmp_pa
]
def test_seeded_routine_log_allows_band_exercises_without_weight(tmp_path: Path) -> None:
client = make_client(tmp_path)
summary = client.get('/api/dashboard/summary').json()
shoulders = next(routine for routine in summary['routines'] if routine['name'] == 'Thursday: Shoulders')
response = client.post(
'/workouts/routine-log',
data={
'user_id': 'john',
'routine_id': shoulders['id'],
'performed_at': '2026-05-28T12:05:00',
'routine_name': shoulders['name'],
'notes': 'Band work without tracked weight.',
'exercise_count': '1',
'exercise_0_name': 'Resistance Band Punch-Out Training',
'exercise_0_set_count': '3',
'exercise_0_set_0_reps': '',
'exercise_0_set_0_weight_lbs': '',
'exercise_0_set_1_reps': '',
'exercise_0_set_1_weight_lbs': '',
'exercise_0_set_2_reps': '',
'exercise_0_set_2_weight_lbs': '',
},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers['location'] == '/'
workout_history = client.get('/api/history/john/workouts')
assert workout_history.status_code == 200
payload = workout_history.json()
assert payload[0]['routine_name'] == 'Thursday: Shoulders'
assert payload[0]['set_count'] == 3
assert payload[0]['total_volume_lbs'] == 0
assert payload[0]['exercises'][0]['exercise_name'] == 'Resistance Band Punch-Out Training'
assert payload[0]['exercises'][0]['sets'] == [
{'reps': 10, 'weight_lbs': None},
{'reps': 10, 'weight_lbs': None},
{'reps': 10, 'weight_lbs': None},
]
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
state_path = tmp_path / 'state.json'
state_path.write_text(

View File

@@ -2019,7 +2019,7 @@ def top_nav(active:str)->str:
('Services','services.html','services'),
('Kitchen','http://10.5.30.7:8092','kitchen'),
('Schoolhouse','https://schoolhouse.paccoco.com','schoolhouse'),
('Barbell','http://10.5.30.6:8093','barbell'),
('Barbell','https://gym.paccoco.com','barbell'),
('Donetick','https://donetick.paccoco.com','donetick'),
('Paperless','https://paperless.paccoco.com','paperless'),
('n8n','https://n8n.paccoco.com','n8n'),
@@ -2051,7 +2051,7 @@ def render_family_directory(active:str)->str:
('Services Directory','services.html','services','Switchboard','Dense launch wall for the wider stack once you know where you need to go.'),
('Doris Kitchen','http://10.5.30.7:8092','kitchen','Pantry desk','Recipe leads, import repair, and meal-planning evidence review.'),
('Doris Schoolhouse','https://schoolhouse.paccoco.com','schoolhouse','Records desk','Assignments, recordings, D2L sync, and archive intake.'),
('Doris Barbell','http://10.5.30.6:8093','barbell','Training desk','Body metrics, routines, workouts, and progression dossiers.'),
('Doris Barbell','https://gym.paccoco.com','barbell','Training desk','Body metrics, routines, workouts, and progression dossiers.'),
]
cards=[]
active_key='dashboard' if active=='home' else active

View File

@@ -21,7 +21,7 @@
<a class="nav-link" href="http://10.5.30.7:8787/">Dashboard</a>
<a class="nav-link" href="http://10.5.30.7:8787/services.html">Services</a>
<a class="nav-link" href="https://schoolhouse.paccoco.com/">Schoolhouse</a>
<a class="nav-link" href="http://10.5.30.6:8093/">Barbell</a>
<a class="nav-link" href="https://gym.paccoco.com/">Barbell</a>
</nav>
</div>
<div class="casefile-title-row">
@@ -137,7 +137,7 @@
<strong>Doris Schoolhouse</strong>
<small>Assignments, recordings, and archive intake.</small>
</a>
<a class="family-app-card" href="http://10.5.30.6:8093/">
<a class="family-app-card" href="https://gym.paccoco.com/">
<span class="family-app-kicker">Training desk</span>
<strong>Doris Barbell</strong>
<small>Weights, routines, workouts, and progression snapshots.</small>

View File

@@ -21,7 +21,7 @@
<a class="nav-link" href="http://10.5.30.7:8787/">Dashboard</a>
<a class="nav-link" href="http://10.5.30.7:8787/services.html">Services</a>
<a class="nav-link" href="http://10.5.30.7:8092/">Kitchen</a>
<a class="nav-link" href="http://10.5.30.6:8093/">Barbell</a>
<a class="nav-link" href="https://gym.paccoco.com/">Barbell</a>
</nav>
</div>
<div class="casefile-title-row">
@@ -134,7 +134,7 @@
<strong>Doris Kitchen</strong>
<small>Meal discovery, import repair, and planner triage.</small>
</a>
<a class="family-app-card" href="http://10.5.30.6:8093/">
<a class="family-app-card" href="https://gym.paccoco.com/">
<span class="family-app-kicker">Training desk</span>
<strong>Doris Barbell</strong>
<small>Workout logs, body metrics, and progression dossiers.</small>