Fix Doris Barbell band logging and nav links
This commit is contained in:
@@ -61,7 +61,7 @@ class RoutineCreate(BaseModel):
|
|||||||
|
|
||||||
class WorkoutSetCreate(BaseModel):
|
class WorkoutSetCreate(BaseModel):
|
||||||
reps: int = Field(ge=1)
|
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)
|
rpe: float | None = Field(default=None, ge=0)
|
||||||
rest_seconds: int | None = Field(default=None, ge=0)
|
rest_seconds: int | None = Field(default=None, ge=0)
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ def routine_log_sheet(request: Request, routine_id: str):
|
|||||||
context={
|
context={
|
||||||
'title': f"Log {routine['name']}",
|
'title': f"Log {routine['name']}",
|
||||||
'routine': routine,
|
'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),
|
'exercise_options': _exercise_options_for_routine(request, routine),
|
||||||
'routine_action_label': _routine_action_label(routine['name']),
|
'routine_action_label': _routine_action_label(routine['name']),
|
||||||
'default_performed_at': _default_performed_at_value(),
|
'default_performed_at': _default_performed_at_value(),
|
||||||
@@ -261,24 +261,29 @@ def _get_routine_or_404(request: Request, routine_id: str) -> dict:
|
|||||||
return routine
|
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 = []
|
sheet = []
|
||||||
for exercise_index, exercise in enumerate(routine.get('exercises', [])):
|
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)
|
target_sets = max(int(exercise.get('target_sets', 1) or 1), 1)
|
||||||
default_reps = _default_reps_value(exercise.get('target_reps'))
|
default_reps = _default_reps_value(exercise.get('target_reps'))
|
||||||
|
is_band = equipment_by_name.get(exercise_name.strip().lower()) == 'band'
|
||||||
sheet.append(
|
sheet.append(
|
||||||
{
|
{
|
||||||
'exercise_index': exercise_index,
|
'exercise_index': exercise_index,
|
||||||
'exercise_name': exercise['exercise_name'],
|
'exercise_name': exercise_name,
|
||||||
'target_sets': target_sets,
|
'target_sets': target_sets,
|
||||||
'target_reps': exercise.get('target_reps'),
|
'target_reps': exercise.get('target_reps'),
|
||||||
'default_reps': default_reps,
|
'default_reps': default_reps,
|
||||||
'notes': exercise.get('notes'),
|
'notes': exercise.get('notes'),
|
||||||
|
'is_band': is_band,
|
||||||
'sets': [
|
'sets': [
|
||||||
{
|
{
|
||||||
'exercise_index': exercise_index,
|
'exercise_index': exercise_index,
|
||||||
'index': set_index,
|
'index': set_index,
|
||||||
'default_reps': default_reps,
|
'default_reps': default_reps,
|
||||||
|
'is_band': is_band,
|
||||||
}
|
}
|
||||||
for set_index in range(target_sets)
|
for set_index in range(target_sets)
|
||||||
],
|
],
|
||||||
@@ -305,6 +310,17 @@ def _exercise_options_for_routine(request: Request, routine: dict) -> list[str]:
|
|||||||
return ordered
|
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:
|
def _default_reps_value(target_reps: str | None) -> str:
|
||||||
if target_reps is None:
|
if target_reps is None:
|
||||||
return ''
|
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]:
|
async def _parse_structured_routine_form(request: Request, routine: dict, exercise_count: int) -> list[dict]:
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
routine_exercises = routine.get('exercises', [])
|
routine_exercises = routine.get('exercises', [])
|
||||||
|
equipment_by_name = _exercise_equipment_lookup(request)
|
||||||
exercises = []
|
exercises = []
|
||||||
for exercise_index in range(exercise_count):
|
for exercise_index in range(exercise_count):
|
||||||
exercise_name = str(form.get(f'exercise_{exercise_index}_name', '')).strip()
|
exercise_name = str(form.get(f'exercise_{exercise_index}_name', '')).strip()
|
||||||
if not exercise_name:
|
if not exercise_name:
|
||||||
continue
|
continue
|
||||||
|
is_band_exercise = equipment_by_name.get(exercise_name.lower()) == 'band'
|
||||||
try:
|
try:
|
||||||
set_count = int(str(form.get(f'exercise_{exercise_index}_set_count', '0')).strip())
|
set_count = int(str(form.get(f'exercise_{exercise_index}_set_count', '0')).strip())
|
||||||
except ValueError as exc:
|
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()
|
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()
|
weight_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_weight_lbs', '')).strip()
|
||||||
if not reps_raw and not weight_raw:
|
if not reps_raw and not weight_raw:
|
||||||
continue
|
if is_band_exercise and default_reps_raw.isdigit():
|
||||||
if not reps_raw and weight_raw and default_reps_raw.isdigit():
|
|
||||||
reps_raw = default_reps_raw
|
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(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f'Exercise {exercise_name} set {set_index + 1} needs both reps and weight.',
|
detail=f'Exercise {exercise_name} set {set_index + 1} needs both reps and weight.',
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
reps = int(reps_raw)
|
reps = int(reps_raw)
|
||||||
weight_lbs = float(weight_raw)
|
weight_lbs = None if not weight_raw else float(weight_raw)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|||||||
@@ -289,7 +289,9 @@ class JSONStore:
|
|||||||
normalized_sets = []
|
normalized_sets = []
|
||||||
for item in exercise['sets']:
|
for item in exercise['sets']:
|
||||||
set_count += 1
|
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))
|
normalized_sets.append(dict(item))
|
||||||
exercises.append({
|
exercises.append({
|
||||||
'exercise_name': exercise['exercise_name'],
|
'exercise_name': exercise['exercise_name'],
|
||||||
@@ -340,10 +342,13 @@ class JSONStore:
|
|||||||
stats['last_performed_at'] = workout['performed_at']
|
stats['last_performed_at'] = workout['performed_at']
|
||||||
for item in exercise['sets']:
|
for item in exercise['sets']:
|
||||||
stats['set_count'] += 1
|
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'] = max(
|
||||||
stats['best_set_estimated_1rm'],
|
stats['best_set_estimated_1rm'],
|
||||||
_estimated_1rm(item['weight_lbs'], item['reps']),
|
_estimated_1rm(weight_lbs, item['reps']),
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
for item in progression.values():
|
for item in progression.values():
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<span class="pill badge-hotfuzz">For the Greater Good</span>
|
<span class="pill badge-hotfuzz">For the Greater Good</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="art-stack">
|
||||||
<div class="hot-fuzz-art compact-hot-fuzz-art" aria-hidden="true">
|
<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">
|
<svg viewBox="0 0 320 140" class="poster-illustration compact-hero-mark" role="presentation">
|
||||||
<defs>
|
<defs>
|
||||||
@@ -60,6 +61,11 @@
|
|||||||
<text x="160" y="118" class="poster-callout">TRAINING DOSSIER</text>
|
<text x="160" y="118" class="poster-callout">TRAINING DOSSIER</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<nav class="case-nav" aria-label="Doris Barbell sections">
|
<nav class="case-nav" aria-label="Doris Barbell sections">
|
||||||
<a class="case-nav-link active" href="/#body-metrics">Body metrics</a>
|
<a class="case-nav-link active" href="/#body-metrics">Body metrics</a>
|
||||||
@@ -69,6 +75,43 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="page-shell compact-page-shell">
|
<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 %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -66,6 +66,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted">Target reps: {{ exercise.target_reps or 'enter manually' }}</p>
|
<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">
|
<div class="grid cols-2 compact-grid">
|
||||||
{% for set in exercise.sets %}
|
{% for set in exercise.sets %}
|
||||||
<div class="evidence-slab compact-slab">
|
<div class="evidence-slab compact-slab">
|
||||||
|
|||||||
@@ -386,7 +386,8 @@ def test_homepage_renders_core_sections_and_forms(tmp_path: Path) -> None:
|
|||||||
assert 'Add exercise template' in body
|
assert 'Add exercise template' in body
|
||||||
assert 'Current routine board' in body
|
assert 'Current routine board' in body
|
||||||
assert 'Monday: Chest' 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 'V1 focus' not in body
|
||||||
assert 'Next likely build' 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:
|
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
|
||||||
state_path = tmp_path / 'state.json'
|
state_path = tmp_path / 'state.json'
|
||||||
state_path.write_text(
|
state_path.write_text(
|
||||||
|
|||||||
@@ -2019,7 +2019,7 @@ def top_nav(active:str)->str:
|
|||||||
('Services','services.html','services'),
|
('Services','services.html','services'),
|
||||||
('Kitchen','http://10.5.30.7:8092','kitchen'),
|
('Kitchen','http://10.5.30.7:8092','kitchen'),
|
||||||
('Schoolhouse','https://schoolhouse.paccoco.com','schoolhouse'),
|
('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'),
|
('Donetick','https://donetick.paccoco.com','donetick'),
|
||||||
('Paperless','https://paperless.paccoco.com','paperless'),
|
('Paperless','https://paperless.paccoco.com','paperless'),
|
||||||
('n8n','https://n8n.paccoco.com','n8n'),
|
('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.'),
|
('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 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 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=[]
|
cards=[]
|
||||||
active_key='dashboard' if active=='home' else active
|
active_key='dashboard' if active=='home' else active
|
||||||
|
|||||||
@@ -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/">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:8787/services.html">Services</a>
|
||||||
<a class="nav-link" href="https://schoolhouse.paccoco.com/">Schoolhouse</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="casefile-title-row">
|
<div class="casefile-title-row">
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<strong>Doris Schoolhouse</strong>
|
<strong>Doris Schoolhouse</strong>
|
||||||
<small>Assignments, recordings, and archive intake.</small>
|
<small>Assignments, recordings, and archive intake.</small>
|
||||||
</a>
|
</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>
|
<span class="family-app-kicker">Training desk</span>
|
||||||
<strong>Doris Barbell</strong>
|
<strong>Doris Barbell</strong>
|
||||||
<small>Weights, routines, workouts, and progression snapshots.</small>
|
<small>Weights, routines, workouts, and progression snapshots.</small>
|
||||||
|
|||||||
@@ -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/">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: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.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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="casefile-title-row">
|
<div class="casefile-title-row">
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<strong>Doris Kitchen</strong>
|
<strong>Doris Kitchen</strong>
|
||||||
<small>Meal discovery, import repair, and planner triage.</small>
|
<small>Meal discovery, import repair, and planner triage.</small>
|
||||||
</a>
|
</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>
|
<span class="family-app-kicker">Training desk</span>
|
||||||
<strong>Doris Barbell</strong>
|
<strong>Doris Barbell</strong>
|
||||||
<small>Workout logs, body metrics, and progression dossiers.</small>
|
<small>Workout logs, body metrics, and progression dossiers.</small>
|
||||||
|
|||||||
Reference in New Issue
Block a user