diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f36a71a..6e8e5fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,16 +1,20 @@ -name: Deploy to GitHub Pages +name: Build and Deploy on: push: branches: [main] + pull_request: + branches: [main] permissions: contents: read pages: write id-token: write +# Scope concurrency per ref so PR builds cancel their own stale runs +# without ever cancelling (or being cancelled by) the main deploy. concurrency: - group: pages + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -19,21 +23,28 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Zola - uses: taiki-e/install-action@v2 - with: - tool: zola@0.22.1 + - name: Install Nix + uses: DeterminateSystems/determinate-nix-action@v3 - name: Build - run: zola build + run: nix build --print-build-logs + + # `nix build` writes ./result as a symlink into the read-only store; + # dereference it into a plain directory the Pages artifact step can tar. + # Only needed on main — PRs stop after the build above to verify it. + - name: Stage artifact + if: github.event_name == 'push' + run: cp -rL result public - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v5 with: path: public deploy: needs: build + if: github.event_name == 'push' runs-on: ubuntu-latest environment: name: github-pages @@ -41,4 +52,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 745b165..02104a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ public/ .idea/ .claude + +# Nix build output +result +result-* +.direnv/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43413bf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# CLAUDE.md + +Project-level guidance for Claude Code working in this repo. + +## Stack + +Zola static site generator. Content lives in `content/`, templates in `templates/`, styles in `sass/main.scss`, structured data in `data/`. No npm. Build with `zola build`; serve locally via the launch config in `.claude/launch.json`. + +## Design rules + +### Data-derived UI, single source of truth + +Any aggregate, summary, count, or breakdown shown in the UI must be **computed by the template** from the underlying records — never stored as a pre-computed sibling field that has to be kept in sync manually. + +Hand-tallied summary fields drift the moment the data changes. If a stats panel, totals row, or category breakdown needs to appear, derive it in Tera by iterating the source data (e.g., `data.days[].exercises[]`) and aggregating in-template. External benchmarks (target ranges, thresholds) can still be stored — but the *current state* of the data should always be computed. + +If the in-template aggregation gets too gnarly, the next step is a Zola `load_data` shortcode or a build-time preprocessor — not pre-computed fields in the source data. + +Reference implementation: `templates/workout-program.html` weekly volume block — each exercise carries `sets_n` and `volume = ["slug", ...]`, and the template sums per muscle group at render time. + +### Architectural decisions are recorded as ADRs + +Load-bearing design choices are committed as Architecture Decision Records under `docs/adr/`. Don't make a decision that would be confusing or annoying to reverse without writing an ADR for it. See `docs/adr/README.md` for the format and `docs/roadmap.md` for the in-flight roadmap. + +When a decision changes, write a new ADR that supersedes the old one — never edit an Accepted ADR in place. The git log of `docs/adr/` is the architectural changelog. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..b41bcef --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +What's especially interesting about your phrasing is "instantiated with my longing." That's a programmer's word for +what's happening, and it's actually more accurate than the usual humanities vocabulary for this. The lyric is like a +class definition — it specifies the shape of an emotional object, the relationships between its parts, the methods it +supports — but it's not the object itself. You instantiate it by passing in your own arguments: the specific person +you're longing for, the specific arrival you're waiting on, the specific relief you're hoping will be kinder than +expected. The resulting object has the structure the artist designed and the contents only you could provide. That's why +the song can feel like it was written specifically for you despite being written years before you heard it — it was +written for you, in the sense that you're the one who completed it. \ No newline at end of file diff --git a/data/workout_program.json b/data/workout_program.json deleted file mode 100644 index 614f715..0000000 --- a/data/workout_program.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "meta": { - "tag": "5-Day Program · Recomposition Focus", - "title": "Strength & Recomp Program", - "description": "Built for 5'11\" / 175lbs / ~18-20% BF / 135lb working weight across main lifts. Heavy 3×5 compounds on Monday, hypertrophy variations on Wednesday, and a pull-focused session on Friday — with kettlebell functional work and conditioning filling the gaps. Tap any day to expand." - }, - "volume_summary": [ - { "group": "Chest/Push", "sets": "~14 sets" }, - { "group": "Back/Pull", "sets": "~19 sets" }, - { "group": "Legs", "sets": "~11 sets" }, - { "group": "Shoulders", "sets": "~12 sets" }, - { "group": "Arms", "sets": "~10 sets" }, - { "group": "Core", "sets": "~8 sets" } - ], - "science_notes": { - "volume": { - "title": "Weekly Volume & Hypertrophy", - "body": "Schoenfeld, Ogborn & Krieger (2017) conducted a meta-analysis of 15 studies and found a significant dose-response relationship between weekly training volume (measured in sets per muscle group) and muscle growth (p = 0.002). The practical takeaway: ~10–20 sets per muscle group per week appears optimal for hypertrophy. This program targets 12–16 weekly sets for major muscle groups by distributing volume across heavy, hypertrophy, and functional days.", - "source": "Schoenfeld BJ, Ogborn D, Krieger JW. J Sports Sci. 2017;35(11):1073-1082." - }, - "frequency": { - "title": "Training Frequency", - "body": "A 2016 meta-analysis by Schoenfeld et al. found that training each muscle group at least twice per week produced superior hypertrophy compared to once per week when total volume was equated. However, a 2019 systematic review by the same group found that once volume is matched, frequency itself has diminishing returns — meaning you can distribute your sets across 2, 3, or even 5 sessions and get similar results, as long as total weekly volume is adequate. This gives you freedom to structure your week around recovery and lifestyle.", - "source": "Schoenfeld BJ, Ogborn D, Krieger JW. Sports Med. 2016;46(11):1689-1697." - }, - "linearProgression": { - "title": "Linear Progression & Heavy Compound Training", - "body": "No peer-reviewed RCT has specifically validated the 5×5 or 3×5 protocol as a complete program — the evidence supporting them is entirely indirect, drawn from research on progressive overload, multiple-set superiority, and compound movements. Kraemer & Ratamess (2004) found that neural adaptations dominate early training responses, meaning beginners get stronger primarily through improved motor unit recruitment. The ACSM Position Stand (2009) actually recommends 8–12 RM for novices, which differs from the 3–5 rep range. However, novices respond broadly to any progressive program. The true advantages of low-rep barbell training are simplicity, adequate heavy practice, and decades of coaching refinement. Pelland et al. (2026) found strength gains plateau at surprisingly low volume (~3 fractional weekly sets) with adequate frequency — which is why this program uses 3×5 rather than 5×5, reserving recovery capacity for hypertrophy work.", - "source": "Kraemer WJ, Ratamess NA. Med Sci Sports Exerc. 2004. ACSM Position Stand. Med Sci Sports Exerc. 2009. Pelland JC et al. Sports Med. 2026;56(2):481-505." - }, - "recomp": { - "title": "Body Recomposition", - "body": "A 2020 review in Strength & Conditioning Journal (Barakat et al.) demonstrated that body recomposition — simultaneous fat loss and muscle gain — is well-documented even in trained populations, not just novices. The two critical factors are progressive resistance training and high protein intake. A meta-analysis of fat loss studies found that muscle growth stalls when caloric deficits exceed ~500 calories, suggesting a mild deficit of 200–300 calories (or eating at maintenance) is optimal for recomp. Novice and detrained lifters have the greatest recomp potential.", - "source": "Barakat C et al. Strength Cond J. 2020;42(5):7-21." - }, - "protein": { - "title": "Protein for Recomposition", - "body": "Research in the Journal of the International Society of Sports Nutrition indicates that protein intakes of 1.6–2.2g per kg of bodyweight maximize muscle protein synthesis when combined with resistance training. During a caloric deficit, higher protein (up to 2.4g/kg/day) has been shown to preserve lean mass and promote fat loss more effectively than lower intakes. For a 175lb (79.5kg) individual, this translates to roughly 127–175g of protein per day. Studies pooled by The Muscle PhD found that the average protein intake across successful recomp studies was 2.56g/kg/day (~1.16g/lb/day).", - "source": "Jäger R et al. JISSN. 2017;14:20. Longland TM et al. Am J Clin Nutr. 2016;103(3):738-746." - }, - "unilateral": { - "title": "Unilateral Training & Kettlebells", - "body": "Unilateral (single-limb) training addresses strength asymmetries that bilateral movements can mask. Research on cross-education shows that training one limb can even preserve strength in an immobilized contralateral limb (Magnus et al., 2018). Kettlebells are particularly suited for unilateral work because their offset center of mass forces greater stabilizer recruitment. A 2024 study in Frontiers in Sports found that two-armed kettlebell swings produced bilateral asymmetries exceeding 15% in the posterior deltoid and external oblique — suggesting single-arm variations may be needed to address these imbalances.", - "source": "Magnus CRA et al. J Appl Physiol. 2018. Frontiers in Sports and Active Living. 2024." - }, - "trapImbalance": { - "title": "Trap Asymmetry & Corrective Approach", - "body": "Prolonged asymmetric postures — such as mouse use in gaming or desk work — can create chronic hypertonicity in the dominant-side upper trapezius via the Cinderella hypothesis (Hägg, 1991): the same low-threshold motor units are continuously recruited and never allowed to rest. The muscle isn't necessarily larger; it's neurally 'stuck on.' Research by Sjøgaard et al. (2006) confirmed sustained trapezius activity above resting levels during computer work. Importantly, rehabilitation researchers caution against excessive upper trap strengthening when the real issue is an upper/lower trapezius imbalance. For desk workers, training the lower trapezius and serratus anterior — the muscles that are actually weak — is often more appropriate than piling more shrugs onto an already overactive upper trap. Progressive resistance training of any kind significantly reduces trapezius pain (Andersen et al., 2008; 2011).", - "source": "Sjøgaard G et al. Eur J Appl Physiol. 2006. Visser B, van Dieën JH. Clin Biomech. 2006. Andersen LL et al. Arthritis Care Res. 2008." - }, - "posterior": { - "title": "Posterior Chain & Pulling Volume", - "body": "The often-cited 2:1 pull-to-push ratio is coaching heuristic, not research-validated — no peer-reviewed study has tested whether specific ratios produce superior outcomes for posture or shoulder health. However, the premise is sound: Negrete et al. (2013) found recreationally active males are naturally push-dominant at 1.57:1, and Kolber et al. (2009) showed weight trainers develop measurable anterior-biased imbalances. The practical takeaway: ensure adequate pulling volume rather than chasing a specific ratio. Warneke et al. (2024) found that strengthening exercises produced large improvements in thoracic and cervical posture, while stretching alone had no effect.", - "source": "Negrete et al. Int J Sports Phys Ther. 2013. Kolber et al. JSCR. 2009. Warneke et al. Sports Med Open. 2024." - } - }, - "days": [ - { - "name": "Monday", - "title": "Heavy Compound Strength", - "subtitle": "3×5 Linear Progression", - "science_keys": ["linearProgression", "volume"], - "rationale": "This is your primary strength day. The 3×5 protocol at ~80-85% 1RM develops maximal strength through neural adaptation with minimal unnecessary fatigue. Pelland et al. (2026) found strength gains plateau at remarkably low volume (~3 fractional weekly sets) when frequency is adequate — so 3 heavy sets here, combined with Wednesday's lighter exposure, gives you near-optimal strength stimulus without burning recovery you need for hypertrophy work. You should still be adding 5lbs to squat and deadlift every session, and 5lbs to bench/OHP every week. Same progression rules as a 5×5 — just less junk volume.", - "exercises": [ - { "name": "Barbell Back Squat", "sets": "3×5", "notes": "Work weight. Add 5lbs per session. Full depth (hip crease below knee). 2-3 min rest between sets.", "primary": true }, - { "name": "Barbell Bench Press", "sets": "3×5", "notes": "Work weight. Add 5lbs per week. Full ROM, pause briefly on chest. 2-3 min rest.", "primary": true }, - { "name": "Barbell Overhead Press", "sets": "3×5", "notes": "Work weight. Add 2.5-5lbs per week. Strict press, no leg drive. 2-3 min rest.", "primary": true }, - { "name": "Barbell Row (Pendlay)", "sets": "3×8", "notes": "From floor each rep. Builds the pulling volume your program currently lacks.", "primary": false }, - { "name": "Face Pulls (Tonal or band)", "sets": "3×15", "notes": "Light weight, high reps. External rotation at top. Rear delt & rotator cuff health.", "primary": false } - ] - }, - { - "name": "Tuesday", - "title": "Kettlebell Functional", - "subtitle": "Unilateral & Cross-Body Patterns", - "science_keys": ["unilateral"], - "rationale": "This session prioritizes single-limb and asymmetrical loading patterns that expose and correct imbalances. The offset center of mass in a kettlebell forces greater core and stabilizer engagement than dumbbells. Always start with your weaker side and match reps — never do more on your strong side.", - "exercises": [ - { "name": "Turkish Get-Up", "sets": "3×2 per side", "notes": "Slow and controlled. This is a full-body mobility and stability drill disguised as strength work. Use a weight you can control perfectly.", "primary": true }, - { "name": "Single-Arm KB Swing", "sets": "5×10 per side", "notes": "Hip hinge, not squat. Explosive hip extension. Anti-rotation demand is high.", "primary": true }, - { "name": "Single-Arm KB Clean & Press", "sets": "4×5 per side", "notes": "Rack clean, then strict press. Builds unilateral pressing strength and shoulder stability.", "primary": true }, - { "name": "KB Single-Leg Romanian Deadlift", "sets": "3×8 per side", "notes": "Hold KB in opposite hand (contralateral). Develops posterior chain and balance simultaneously.", "primary": false }, - { "name": "KB Windmill", "sets": "3×5 per side", "notes": "Thoracic rotation under load. Directly addresses the posture and mobility goals you described.", "primary": false }, - { "name": "KB Goblet Lateral Lunge", "sets": "3×8 per side", "notes": "Frontal plane movement — something your barbell work completely misses.", "primary": false } - ] - }, - { - "name": "Wednesday", - "title": "Hypertrophy Compounds", - "subtitle": "Lighter Variations · 3×8-12", - "science_keys": ["frequency", "volume"], - "rationale": "This is Monday's complement — same movement patterns (squat, press, push), lighter loads, higher reps. You're not trying to set PRs here; you're accumulating volume at hypertrophy rep ranges to drive muscle growth and practice the movement patterns a second time per week. Using dumbbell and variation lifts instead of repeating the exact barbell movements reduces joint stress while still training the same muscles. Research consistently shows that hitting each muscle group twice weekly is superior for both strength and hypertrophy when volume is equated.", - "exercises": [ - { "name": "Front Squat or Goblet Squat (heavy DB)", "sets": "3×10", "notes": "Different squat pattern from Monday — front-loaded forces more upright torso and targets quads harder. Also builds the upper back bracing you need for heavier back squats.", "primary": true }, - { "name": "Dumbbell Bench Press", "sets": "3×10", "notes": "Greater ROM than barbell and forces each arm to stabilize independently. Addresses any left/right pressing imbalance. Moderate weight, controlled tempo.", "primary": true }, - { "name": "Dumbbell Overhead Press (seated)", "sets": "3×10", "notes": "Seated removes leg drive entirely — pure shoulder strength. Lighter than barbell OHP but second weekly pressing exposure builds the volume needed for shoulder growth.", "primary": true }, - { "name": "Incline DB Press", "sets": "3×10", "notes": "Targets upper chest and front delts, which are harder to hit with flat pressing. 30-45° incline. This adds pressing volume without repeating Monday's exact stimulus.", "primary": false }, - { "name": "Prone Y-Raises (DB or bench)", "sets": "3×12", "notes": "POSTURE CORRECTION: Lie face-down on an incline bench, arms hanging. Raise DBs into a Y overhead, thumbs up. Targets the lower trapezius and serratus anterior — the muscles that are actually weak in desk workers, unlike the overactive upper traps.", "primary": false }, - { "name": "Tricep Pushdown (Tonal)", "sets": "3×12", "notes": "Isolation work for triceps — the limiting muscle on both bench and OHP. The Tonal's cable is ideal for constant tension through the full range.", "primary": false }, - { "name": "Lateral Raises (DB)", "sets": "3×15", "notes": "Builds the medial delt cap that makes shoulders look broader. Light weight, controlled, no momentum. This is one of the few isolation moves worth programming for aesthetics.", "primary": false } - ] - }, - { - "name": "Thursday", - "title": "Conditioning + Mobility", - "subtitle": "Work Capacity & Posture Correction", - "science_keys": ["recomp", "trapImbalance"], - "rationale": "Placed mid-late week as a deliberate intensity valley between Wednesday's hypertrophy work and Friday's heavy pulling. KB complexes build work capacity (which supports recomp by increasing energy expenditure without the cortisol spike of long cardio), while the mobility work targets the specific postural issues visible in your side-profile photo. The trap release and neck stretch work addresses your dominant-side hypertonicity from mouse use — do this consistently and you should feel a difference within 2-3 weeks.", - "exercises": [ - { "name": "KB Complex: Swing → Clean → Press → Squat", "sets": "4 rounds × 5 reps each", "notes": "Use a moderate KB. No rest between movements, 90sec rest between rounds. This is your cardio.", "primary": true }, - { "name": "Farmer's Carries (heavy DBs)", "sets": "4 × 40m", "notes": "Grip, traps, core, everything. Walk tall. Alternate with single-arm carries to expose and correct the trap imbalance — the weaker side will fatigue first.", "primary": true }, - { "name": "X-Bar Assisted Pullups or Lat Pulldown (Tonal)", "sets": "3×max reps", "notes": "Vertical pulling — essential for lat development and shoulder health. Use the Tonal for assisted pulldowns until you can do 3+ unassisted reps on the X-bar. No shame in assistance — building the pattern matters more than the method.", "primary": true }, - { "name": "Band Pull-Aparts", "sets": "3×20", "notes": "Posture corrective. Retracts scapulae and strengthens mid/lower traps.", "primary": false }, - { "name": "Upper Trap Release (lacrosse ball)", "sets": "60-90 sec per side", "notes": "TRAP CORRECTION: Pin lacrosse ball between dominant-side upper trap and wall. Apply pressure and slowly nod/turn head. Releases the chronic hypertonicity from mouse-arm posture.", "primary": false }, - { "name": "Lateral Neck Stretches", "sets": "30 sec × 3 per side", "notes": "TRAP CORRECTION: Ear toward shoulder, gentle hand pressure. Focus extra time on the tight (dominant) side. Pairs with the lacrosse ball work to restore resting symmetry.", "primary": false }, - { "name": "Thoracic Spine Extensions (foam roller)", "sets": "2×10", "notes": "Lie on foam roller at upper back, hands behind head, extend over it. Directly counters desk-posture rounding.", "primary": false }, - { "name": "90/90 Hip Stretch", "sets": "2 min per side", "notes": "Internal and external hip rotation. Essential for squat depth and hip health.", "primary": false }, - { "name": "Wall Slides", "sets": "3×10", "notes": "Back and arms against wall, slide up into Y position. Tests and builds overhead mobility for your OHP.", "primary": false } - ] - }, - { - "name": "Friday", - "title": "Pull & Posterior Chain Focus", - "subtitle": "Hypertrophy Rep Ranges (3×8-12)", - "science_keys": ["posterior", "volume", "trapImbalance"], - "rationale": "Moved to Friday so you have the full weekend to recover from deadlifts before Monday's heavy squats. This day directly addresses the anterior/posterior imbalance visible in your photos and the trap asymmetry from dominant-side mouse use. Pulling movements are programmed at hypertrophy rep ranges (8-12) to build muscle mass in your upper back, lats, and rear delts. The assisted pullups add the vertical pull volume this program was missing — bringing weekly back sets from ~16 to ~19.", - "exercises": [ - { "name": "Barbell Deadlift", "sets": "3×5", "notes": "Work weight but NOT max effort. Add 5lbs per session. Landing on Friday means 2 full rest days before Monday's squats — optimal spacing.", "primary": true }, - { "name": "Assisted Pullups (band or Tonal) or Lat Pulldown", "sets": "3×8", "notes": "VERTICAL PULL: Second weekly exposure to close the lat volume gap. Use a band looped over the X-bar for assistance, or the Tonal's lat pulldown. Progress toward unassisted reps over time.", "primary": true }, - { "name": "Dumbbell Row", "sets": "4×10 per side", "notes": "Unilateral. Full stretch at bottom, squeeze shoulder blade at top. Build the lats your program has been neglecting.", "primary": true }, - { "name": "Dips (bodyweight → weighted)", "sets": "3×8-12", "notes": "Use the dip station. Full ROM. Once you can do 3×12 easily, add weight via a belt or holding a DB between your feet.", "primary": true }, - { "name": "Single-Arm DB Shrug", "sets": "2×12 per side", "notes": "TRAP CORRECTION: Light weight, activation-focused. Start with weaker (non-mouse) side. 2-second hold at top. This is the only shrug session per week — the goal is balanced recruitment, not building more upper trap mass on an already overactive muscle.", "primary": false }, - { "name": "Face Pulls", "sets": "3×15", "notes": "Yes, again. Twice per week is appropriate for correcting anterior dominance. Light, controlled.", "primary": false }, - { "name": "Hammer Curls (DB)", "sets": "3×10", "notes": "Builds brachioradialis and bicep. Neutral grip is easier on the elbows than barbell curls.", "primary": false }, - { "name": "Pallof Press (Tonal)", "sets": "3×10 per side", "notes": "Anti-rotation core work. Far more functional than crunches. The Tonal's cable system is perfect for this.", "primary": false }, - { "name": "Dead Bugs", "sets": "3×8 per side", "notes": "Core stability under contralateral limb movement. Directly trains the deep stabilizers.", "primary": false } - ] - }, - { - "name": "Saturday & Sunday", - "title": "Full Rest", - "subtitle": "Recovery & Nutrition Focus", - "science_keys": ["protein"], - "rationale": "After five consecutive training days, two full rest days are essential for CNS and muscular recovery. You've accumulated significant volume across the week — your body builds muscle during rest, not during training. Use this time to meal prep and ensure you're hitting protein targets. Sleep quality on rest days is just as important as training days for adaptation.", - "exercises": [] - } - ], - "progression": { - "title": "12-Week Progression Framework", - "phases": [ - { - "name": "Weeks 1–4: Foundation", - "description": "Focus on nailing form and building consistency. Start the 3×5 lifts at current working weight (135lbs) and progress linearly. KB days should use a weight you can control perfectly for every rep. Track every session.", - "targets": ["Bench: 135 → 155 lbs", "Squat: 135 → 175 lbs", "Deadlift: 135 → 195 lbs"] - }, - { - "name": "Weeks 5–8: Push", - "description": "Linear progression should still be working. If you stall on a lift (fail to complete 3×5), repeat the same weight next session. Two consecutive failures = deload 10% and build back up. Add cardio intensity on Thursday (shorter rest between KB complex rounds).", - "targets": ["Bench: 155 → 175 lbs", "Squat: 175 → 215 lbs", "Deadlift: 195 → 255 lbs"] - }, - { - "name": "Weeks 9–12: Transition", - "description": "If linear progression stalls, you've outgrown pure novice programming. Shift Monday to a heavy/light scheme (heavy singles or triples followed by back-off sets). Consider moving to a 5/3/1 Wendler-style progression after this block. Re-assess body composition with photos and measurements.", - "targets": ["Bench: 175 → 185+ lbs", "Squat: 215 → 245+ lbs", "Deadlift: 255 → 295+ lbs"] - } - ] - }, - "nutrition": { - "science_keys": ["recomp", "protein"], - "items": [ - { "label": "Daily Protein Target", "value": "140–175g", "detail": "~0.8–1g per lb bodyweight. This is the single most important nutritional variable." }, - { "label": "Caloric Strategy", "value": "Maintenance ± 200 cal", "detail": "Slight deficit on rest days, maintenance or slight surplus on heavy training days. Don't restrict aggressively — you're still building foundational muscle." }, - { "label": "Tracking", "value": "Protein only (to start)", "detail": "You don't need to count every macro. Just ensure you hit protein. Use MyFitnessPal or MacroFactor for the first 2 weeks to calibrate your intuition, then adjust." }, - { "label": "Timing", "value": "3–4 protein feedings/day", "detail": "~30–45g per meal distributes muscle protein synthesis signaling across the day. Don't skip breakfast if you're training in the afternoon." } - ] - }, - "sources": "Pelland JC et al. \"RT Dose-Response Meta-Regressions.\" Sports Med (2026). · Schoenfeld BJ et al. \"Dose-response relationship between weekly RT volume and muscle mass.\" J Sports Sci (2017). · Schoenfeld BJ et al. \"Effects of RT Frequency on Muscle Hypertrophy.\" Sports Med (2016). · Barakat C et al. \"Body Recomposition: Can Trained Individuals Build Muscle and Lose Fat?\" Strength Cond J (2020). · Kraemer WJ, Ratamess NA. \"Fundamentals of Resistance Training.\" Med Sci Sports Exerc (2004). · ACSM Position Stand: \"Progression Models in RT.\" Med Sci Sports Exerc (2009). · Warneke et al. \"Strengthening vs. Stretching for Posture.\" Sports Med Open (2024). · Negrete et al. \"Push/Pull Ratio in Active Adults.\" Int J Sports Phys Ther (2013). · Sjøgaard G et al. \"Trapezius & Computer Work.\" Eur J Appl Physiol (2006). · Andersen LL et al. \"Strength Training for Trapezius Pain.\" Arthritis Care Res (2008)." -} diff --git a/data/workout_program.toml b/data/workout_program.toml new file mode 100644 index 0000000..ec73633 --- /dev/null +++ b/data/workout_program.toml @@ -0,0 +1,730 @@ +sources = """ +Pelland JC et al. "RT Dose-Response Meta-Regressions." Sports Med (2026). · \ +Schoenfeld BJ et al. "Dose-response relationship between weekly RT volume \ +and muscle mass." J Sports Sci (2017). · \ +Schoenfeld BJ et al. "Effects of RT Frequency on Muscle Hypertrophy." \ +Sports Med (2016). · \ +Barakat C et al. "Body Recomposition: Can Trained Individuals Build Muscle \ +and Lose Fat?" Strength Cond J (2020). · \ +Kraemer WJ, Ratamess NA. "Fundamentals of Resistance Training." \ +Med Sci Sports Exerc (2004). · \ +ACSM Position Stand: "Progression Models in RT." Med Sci Sports Exerc (2009). · \ +Warneke et al. "Strengthening vs. Stretching for Posture." \ +Sports Med Open (2024). · \ +Negrete et al. "Push/Pull Ratio in Active Adults." Int J Sports Phys Ther (2013). · \ +Sjøgaard G et al. "Trapezius & Computer Work." Eur J Appl Physiol (2006). · \ +Andersen LL et al. "Strength Training for Trapezius Pain." \ +Arthritis Care Res (2008).\ +""" + +# Per-muscle weekly volume targets. `target_min`/`target_max` are evidence-based +# ranges (Schoenfeld 2017 meta-analysis of 10–20 sets/wk for hypertrophy), +# adjusted for compound carryover where relevant — direct-set targets are +# raised where carryover is light (biceps, side delts) and lowered where +# carryover is heavy (triceps, front delts). +# +# The current weekly count is computed at render time from each exercise's +# `volume = [...]` and `sets_n` fields — do not maintain it by hand. Per-side +# exercises count as a single bout (e.g., "5×10 per side" = 5 sets), unless +# the movement loads both sides simultaneously (e.g., single-arm KB swing). + +[[volume_targets]] +group = "Chest" +slug = "chest" +target_min = 10 +target_max = 14 + +[[volume_targets]] +group = "Back" +slug = "back" +target_min = 14 +target_max = 18 + +[[volume_targets]] +group = "Quads" +slug = "quads" +target_min = 8 +target_max = 12 + +[[volume_targets]] +group = "Posterior Chain" +slug = "posterior_chain" +target_min = 10 +target_max = 14 + +[[volume_targets]] +group = "Front Delts" +slug = "front_delts" +target_min = 6 +target_max = 10 + +[[volume_targets]] +group = "Side Delts" +slug = "side_delts" +target_min = 8 +target_max = 12 + +[[volume_targets]] +group = "Rear Delts" +slug = "rear_delts" +target_min = 10 +target_max = 14 + +[[volume_targets]] +group = "Triceps" +slug = "triceps" +target_min = 4 +target_max = 8 + +[[volume_targets]] +group = "Biceps" +slug = "biceps" +target_min = 8 +target_max = 12 + +[[volume_targets]] +group = "Core" +slug = "core" +target_min = 6 +target_max = 12 + +[meta] +tag = "5-Day Program · Recomposition Focus" +title = "Strength & Recomp Program" +description = """ +Heavy 3×5 compounds on Monday, hypertrophy variations on Wednesday, \ +and a pull-focused session on Friday — with kettlebell functional work and \ +conditioning filling the gaps.\ +""" + + +# ───────────────────────────────────────────────────────────────────── +# Science notes (referenced by `science_keys` on each day) +# ───────────────────────────────────────────────────────────────────── + +[science_notes.volume] +title = "Weekly Volume & Hypertrophy" +body = """ +Schoenfeld, Ogborn & Krieger (2017) conducted a meta-analysis of 15 studies \ +and found a significant dose-response relationship between weekly training \ +volume (measured in sets per muscle group) and muscle growth (p = 0.002). \ +The practical takeaway: ~10–20 sets per muscle group per week appears \ +optimal for hypertrophy. This program targets 12–16 weekly sets for major \ +muscle groups by distributing volume across heavy, hypertrophy, and \ +functional days.\ +""" +source = "Schoenfeld BJ, Ogborn D, Krieger JW. J Sports Sci. 2017;35(11):1073-1082." + +[science_notes.frequency] +title = "Training Frequency" +body = """ +A 2016 meta-analysis by Schoenfeld et al. found that training each muscle \ +group at least twice per week produced superior hypertrophy compared to \ +once per week when total volume was equated. However, a 2019 systematic \ +review by the same group found that once volume is matched, frequency \ +itself has diminishing returns — meaning you can distribute your sets \ +across 2, 3, or even 5 sessions and get similar results, as long as total \ +weekly volume is adequate. This gives you freedom to structure your week \ +around recovery and lifestyle.\ +""" +source = "Schoenfeld BJ, Ogborn D, Krieger JW. Sports Med. 2016;46(11):1689-1697." + +[science_notes.linearProgression] +title = "Linear Progression & Heavy Compound Training" +body = """ +No peer-reviewed RCT has specifically validated the 5×5 or 3×5 protocol \ +as a complete program — the evidence supporting them is entirely indirect, \ +drawn from research on progressive overload, multiple-set superiority, \ +and compound movements. Kraemer & Ratamess (2004) found that neural \ +adaptations dominate early training responses, meaning beginners get \ +stronger primarily through improved motor unit recruitment. The ACSM \ +Position Stand (2009) actually recommends 8–12 RM for novices, which \ +differs from the 3–5 rep range. However, novices respond broadly to any \ +progressive program. The true advantages of low-rep barbell training are \ +simplicity, adequate heavy practice, and decades of coaching refinement. \ +Pelland et al. (2026) found strength gains plateau at surprisingly low \ +volume (~3 fractional weekly sets) with adequate frequency — which is \ +why this program uses 3×5 rather than 5×5, reserving recovery capacity \ +for hypertrophy work.\ +""" +source = "Kraemer WJ, Ratamess NA. Med Sci Sports Exerc. 2004. ACSM Position Stand. Med Sci Sports Exerc. 2009. Pelland JC et al. Sports Med. 2026;56(2):481-505." + +[science_notes.recomp] +title = "Body Recomposition" +body = """ +A 2020 review in Strength & Conditioning Journal (Barakat et al.) \ +demonstrated that body recomposition — simultaneous fat loss and muscle \ +gain — is well-documented even in trained populations, not just novices. \ +The two critical factors are progressive resistance training and high \ +protein intake. A meta-analysis of fat loss studies found that muscle \ +growth stalls when caloric deficits exceed ~500 calories, suggesting a \ +mild deficit of 200–300 calories (or eating at maintenance) is optimal \ +for recomp. Novice and detrained lifters have the greatest recomp \ +potential.\ +""" +source = "Barakat C et al. Strength Cond J. 2020;42(5):7-21." + +[science_notes.protein] +title = "Protein for Recomposition" +body = """ +Research in the Journal of the International Society of Sports Nutrition \ +indicates that protein intakes of 1.6–2.2g per kg of bodyweight maximize \ +muscle protein synthesis when combined with resistance training. During a \ +caloric deficit, higher protein (up to 2.4g/kg/day) has been shown to \ +preserve lean mass and promote fat loss more effectively than lower \ +intakes. For a 175lb (79.5kg) individual, this translates to roughly \ +127–175g of protein per day. Studies pooled by The Muscle PhD found that \ +the average protein intake across successful recomp studies was \ +2.56g/kg/day (~1.16g/lb/day).\ +""" +source = "Jäger R et al. JISSN. 2017;14:20. Longland TM et al. Am J Clin Nutr. 2016;103(3):738-746." + +[science_notes.unilateral] +title = "Unilateral Training & Kettlebells" +body = """ +Unilateral (single-limb) training addresses strength asymmetries that \ +bilateral movements can mask. Research on cross-education shows that \ +training one limb can even preserve strength in an immobilized \ +contralateral limb (Magnus et al., 2018). Kettlebells are particularly \ +suited for unilateral work because their offset center of mass forces \ +greater stabilizer recruitment. A 2024 study in Frontiers in Sports found \ +that two-armed kettlebell swings produced bilateral asymmetries exceeding \ +15% in the posterior deltoid and external oblique — suggesting single-arm \ +variations may be needed to address these imbalances.\ +""" +source = "Magnus CRA et al. J Appl Physiol. 2018. Frontiers in Sports and Active Living. 2024." + +[science_notes.trapImbalance] +title = "Trap Asymmetry & Corrective Approach" +body = """ +Prolonged asymmetric postures — such as mouse use in gaming or desk work \ +— can create chronic hypertonicity in the dominant-side upper trapezius \ +via the Cinderella hypothesis (Hägg, 1991): the same low-threshold motor \ +units are continuously recruited and never allowed to rest. The muscle \ +isn't necessarily larger; it's neurally 'stuck on.' Research by Sjøgaard \ +et al. (2006) confirmed sustained trapezius activity above resting levels \ +during computer work. Importantly, rehabilitation researchers caution \ +against excessive upper trap strengthening when the real issue is an \ +upper/lower trapezius imbalance. For desk workers, training the lower \ +trapezius and serratus anterior — the muscles that are actually weak — \ +is often more appropriate than piling more shrugs onto an already \ +overactive upper trap. Progressive resistance training of any kind \ +significantly reduces trapezius pain (Andersen et al., 2008; 2011).\ +""" +source = "Sjøgaard G et al. Eur J Appl Physiol. 2006. Visser B, van Dieën JH. Clin Biomech. 2006. Andersen LL et al. Arthritis Care Res. 2008." + +[science_notes.posterior] +title = "Posterior Chain & Pulling Volume" +body = """ +The often-cited 2:1 pull-to-push ratio is coaching heuristic, not \ +research-validated — no peer-reviewed study has tested whether specific \ +ratios produce superior outcomes for posture or shoulder health. However, \ +the premise is sound: Negrete et al. (2013) found recreationally active \ +males are naturally push-dominant at 1.57:1, and Kolber et al. (2009) \ +showed weight trainers develop measurable anterior-biased imbalances. \ +The practical takeaway: ensure adequate pulling volume rather than chasing \ +a specific ratio. Warneke et al. (2024) found that strengthening exercises \ +produced large improvements in thoracic and cervical posture, while \ +stretching alone had no effect.\ +""" +source = "Negrete et al. Int J Sports Phys Ther. 2013. Kolber et al. JSCR. 2009. Warneke et al. Sports Med Open. 2024." + + +# ───────────────────────────────────────────────────────────────────── +# Training days +# ───────────────────────────────────────────────────────────────────── + +[[days]] +id = "monday" +name = "Monday" +title = "Heavy Compound Strength" +subtitle = "3×5 Linear Progression" +science_keys = ["linearProgression", "volume"] +rationale = """ +This is your primary strength day. The 3×5 protocol at ~80-85% 1RM \ +develops maximal strength through neural adaptation with minimal \ +unnecessary fatigue. Pelland et al. (2026) found strength gains plateau \ +at remarkably low volume (~3 fractional weekly sets) when frequency is \ +adequate — so 3 heavy sets here, combined with Wednesday's lighter \ +exposure, gives you near-optimal strength stimulus without burning \ +recovery you need for hypertrophy work. Add 5lbs to squat and deadlift \ +every session, and 5lbs to bench/OHP every week. Linear progression on a \ +3×5 protocol — focused working sets without excess fatigue.\ +""" + +[[days.exercises]] +id = "barbell_squat" +name = "Barbell Back Squat" +sets = "3×5" +sets_n = 3 +volume = ["quads"] +progression = { kind = "linear", increment_lbs = 5, cadence = "session" } +notes = "Work weight. Add 5lbs per session. Full depth (hip crease below knee). 2-3 min rest between sets." +primary = true + +[[days.exercises]] +id = "barbell_bench" +name = "Barbell Bench Press" +sets = "3×5" +sets_n = 3 +volume = ["chest"] +progression = { kind = "linear", increment_lbs = 5, cadence = "session" } +notes = "Work weight. Add 5lbs per week. Full ROM, pause briefly on chest. 2-3 min rest." +primary = true + +[[days.exercises]] +id = "barbell_ohp" +name = "Barbell Overhead Press" +sets = "3×5" +sets_n = 3 +volume = ["front_delts"] +progression = { kind = "linear", increment_lbs = 2.5, cadence = "session" } +notes = "Work weight. Add 2.5-5lbs per week. Strict press, no leg drive. 2-3 min rest." +primary = true + +[[days.exercises]] +id = "barbell_row_pendlay" +name = "Barbell Row (Pendlay)" +sets = "3×8" +sets_n = 3 +volume = ["back"] +progression = { kind = "linear", increment_lbs = 5, cadence = "session" } +notes = "From floor each rep. Adds horizontal pulling volume to balance the bench and OHP work." +primary = false + +[[days.exercises]] +id = "face_pulls" +name = "Face Pulls (Tonal or band)" +sets = "3×15" +sets_n = 3 +volume = ["rear_delts"] +notes = "Light weight, high reps. External rotation at top. Rear delt & rotator cuff health." +primary = false + + +[[days]] +id = "tuesday" +name = "Tuesday" +title = "Kettlebell Functional" +subtitle = "Unilateral & Cross-Body Patterns" +science_keys = ["unilateral"] +rationale = """ +This session prioritizes single-limb and asymmetrical loading patterns \ +that expose and correct imbalances. The offset center of mass in a \ +kettlebell forces greater core and stabilizer engagement than dumbbells. \ +Always start with your weaker side and match reps — never do more on \ +your strong side.\ +""" + +[[days.exercises]] +id = "kb_turkish_getup" +name = "Turkish Get-Up" +sets = "3×2 per side" +sets_n = 3 +volume = ["core"] +notes = "Slow and controlled. This is a full-body mobility and stability drill disguised as strength work. Use a weight you can control perfectly." +primary = true + +[[days.exercises]] +id = "kb_swing_single_arm" +name = "Single-Arm KB Swing" +sets = "5×10 per side" +sets_n = 5 +volume = ["posterior_chain"] +notes = "Hip hinge, not squat. Explosive hip extension. Anti-rotation demand is high." +primary = true + +[[days.exercises]] +id = "kb_clean_press_single_arm" +name = "Single-Arm KB Clean & Press" +sets = "4×5 per side" +sets_n = 4 +volume = ["front_delts"] +notes = "Rack clean, then strict press. Builds unilateral pressing strength and shoulder stability." +primary = true + +[[days.exercises]] +id = "kb_sl_rdl" +name = "KB Single-Leg Romanian Deadlift" +sets = "3×8 per side" +sets_n = 3 +volume = ["posterior_chain"] +notes = "Hold KB in opposite hand (contralateral). Develops posterior chain and balance simultaneously." +primary = false + +[[days.exercises]] +id = "kb_windmill" +name = "KB Windmill" +sets = "3×5 per side" +sets_n = 3 +volume = ["core"] +notes = "Thoracic rotation under load. Directly addresses the posture and mobility goals you described." +primary = false + +[[days.exercises]] +id = "kb_lateral_lunge" +name = "KB Goblet Lateral Lunge" +sets = "3×8 per side" +sets_n = 3 +volume = ["quads", "posterior_chain"] +notes = "Frontal plane movement — a pattern the barbell lifts don't train." +primary = false + + +[[days]] +id = "wednesday" +name = "Wednesday" +title = "Hypertrophy Compounds" +subtitle = "Lighter Variations · 3×8-12" +science_keys = ["frequency", "volume"] +rationale = """ +This is Monday's complement — same movement patterns (squat, press, push), \ +lighter loads, higher reps. You're not trying to set PRs here; you're \ +accumulating volume at hypertrophy rep ranges to drive muscle growth and \ +practice the movement patterns a second time per week. Using dumbbell and \ +variation lifts instead of repeating the exact barbell movements reduces \ +joint stress while still training the same muscles. Research consistently \ +shows that hitting each muscle group twice weekly is superior for both \ +strength and hypertrophy when volume is equated.\ +""" + +[[days.exercises]] +id = "front_squat" +name = "Front Squat or Goblet Squat (heavy DB)" +sets = "3×10" +sets_n = 3 +volume = ["quads"] +notes = "Different squat pattern from Monday — front-loaded forces more upright torso and targets quads harder. Also builds the upper back bracing you need for heavier back squats." +primary = true + +[[days.exercises]] +id = "db_bench" +name = "Dumbbell Bench Press" +sets = "3×10" +sets_n = 3 +volume = ["chest"] +notes = "Greater ROM than barbell and forces each arm to stabilize independently. Addresses any left/right pressing imbalance. Moderate weight, controlled tempo." +primary = true + +[[days.exercises]] +id = "db_ohp" +name = "Dumbbell Overhead Press (seated)" +sets = "3×10" +sets_n = 3 +volume = ["front_delts"] +notes = "Seated removes leg drive entirely — pure shoulder strength. Lighter than barbell OHP but second weekly pressing exposure builds the volume needed for shoulder growth." +primary = true + +[[days.exercises]] +id = "incline_db_press" +name = "Incline DB Press" +sets = "3×10" +sets_n = 3 +volume = ["chest"] +notes = "Targets upper chest and front delts, which are harder to hit with flat pressing. 30-45° incline. Adds pressing volume without repeating Monday's exact stimulus." +primary = false + +[[days.exercises]] +id = "prone_y_raises" +name = "Prone Y-Raises (DB or bench)" +sets = "3×12" +sets_n = 3 +volume = ["rear_delts"] +notes = "POSTURE CORRECTION: Lie face-down on an incline bench, arms hanging. Raise DBs into a Y overhead, thumbs up. Targets the lower trapezius and serratus anterior — the muscles that are actually weak in desk workers, unlike the overactive upper traps." +primary = false + +[[days.exercises]] +id = "tricep_pushdown" +name = "Tricep Pushdown (Tonal)" +sets = "3×12" +sets_n = 3 +volume = ["triceps"] +notes = "Isolation work for triceps — the limiting muscle on both bench and OHP. The Tonal's cable is ideal for constant tension through the full range." +primary = false + +[[days.exercises]] +id = "lateral_raises_db" +name = "Lateral Raises (DB)" +sets = "3×15" +sets_n = 3 +volume = ["side_delts"] +notes = "Builds the medial delt cap that makes shoulders look broader. Light weight, controlled, no momentum. This is one of the few isolation moves worth programming for aesthetics." +primary = false + + +[[days]] +id = "thursday" +name = "Thursday" +title = "Conditioning + Mobility" +subtitle = "Work Capacity & Posture Correction" +science_keys = ["recomp", "trapImbalance"] +rationale = """ +Placed mid-late week as a deliberate intensity valley between Wednesday's \ +hypertrophy work and Friday's heavy pulling. KB complexes build work \ +capacity (which supports recomp by increasing energy expenditure without \ +the cortisol spike of long cardio), while the mobility work targets the \ +specific postural issues visible in your side-profile photo. The trap \ +release and neck stretch work addresses your dominant-side hypertonicity \ +from mouse use — do this consistently and you should feel a difference \ +within 2-3 weeks.\ +""" + +[[days.exercises]] +id = "kb_complex" +name = "KB Complex: Swing → Clean → Press → Squat" +sets = "4 rounds × 5 reps per movement, each arm" +sets_n = 0 +volume = [] +notes = "Use a moderate KB. Run all 4 movements on one arm, switch, repeat — that's one round. No rest between movements within a round, 90sec rest between rounds. This is your cardio." +primary = true + +[[days.exercises]] +id = "farmers_carry" +name = "Farmer's Carries (heavy DBs)" +sets = "4 × 40m (2 two-handed, 2 single-arm per side)" +sets_n = 0 +volume = [] +notes = "Grip, traps, core, everything. Walk tall. Two rounds two-handed, then two rounds single-arm — one carry per side — to expose and correct the trap imbalance. The weaker side will fatigue first." +primary = true + +[[days.exercises]] +id = "assisted_pullups" +name = "X-Bar Assisted Pullups or Lat Pulldown (Tonal)" +sets = "3 × AMRAP (1-2 reps shy of failure)" +sets_n = 3 +volume = ["back"] +notes = "Vertical pulling — essential for lat development and shoulder health. Stop each set 1-2 reps before form breakdown, not at absolute failure. Use the Tonal for assisted pulldowns until you can do 3+ unassisted reps on the X-bar. No shame in assistance — building the pattern matters more than the method." +primary = true + +[[days.exercises]] +id = "band_pull_aparts" +name = "Band Pull-Aparts" +sets = "3×20" +sets_n = 3 +volume = ["rear_delts"] +notes = "Posture corrective. Retracts scapulae and strengthens mid/lower traps." +primary = false + +[[days.exercises]] +id = "upper_trap_release" +name = "Upper Trap Release (lacrosse ball)" +sets = "60-90 sec per side" +sets_n = 0 +volume = [] +notes = "TRAP CORRECTION: Pin lacrosse ball between dominant-side upper trap and wall. Apply pressure and slowly nod/turn head. Releases the chronic hypertonicity from mouse-arm posture." +primary = false + +[[days.exercises]] +id = "neck_stretches_lateral" +name = "Lateral Neck Stretches" +sets = "30 sec × 3 per side" +sets_n = 0 +volume = [] +notes = "TRAP CORRECTION: Ear toward shoulder, gentle hand pressure. Focus extra time on the tight (dominant) side. Pairs with the lacrosse ball work to restore resting symmetry." +primary = false + +[[days.exercises]] +id = "t_spine_extensions" +name = "Thoracic Spine Extensions (foam roller)" +sets = "2×10" +sets_n = 0 +volume = [] +notes = "Lie on foam roller at upper back, hands behind head, extend over it. Directly counters desk-posture rounding." +primary = false + +[[days.exercises]] +id = "hip_stretch_90_90" +name = "90/90 Hip Stretch" +sets = "2 min per side" +sets_n = 0 +volume = [] +notes = "Internal and external hip rotation. Essential for squat depth and hip health." +primary = false + +[[days.exercises]] +id = "wall_slides" +name = "Wall Slides" +sets = "3×10" +sets_n = 0 +volume = [] +notes = "Back and arms against wall, slide up into Y position. Tests and builds overhead mobility for your OHP." +primary = false + + +[[days]] +id = "friday" +name = "Friday" +title = "Pull & Posterior Chain Focus" +subtitle = "Hypertrophy Rep Ranges (3×8-12)" +science_keys = ["posterior", "volume", "trapImbalance"] +rationale = """ +Friday placement gives you the full weekend to recover from deadlifts \ +before Monday's heavy squats. This day directly addresses the \ +anterior/posterior imbalance visible in your photos and the trap \ +asymmetry from dominant-side mouse use. Pulling movements are programmed \ +at hypertrophy rep ranges (8-12) to build muscle mass in your upper back, \ +lats, and rear delts. Assisted pullups add the vertical pull volume that \ +brings weekly back sets to ~19.\ +""" + +[[days.exercises]] +id = "barbell_deadlift" +name = "Barbell Deadlift" +sets = "3×5" +sets_n = 3 +volume = ["posterior_chain", "back"] +progression = { kind = "linear", increment_lbs = 5, cadence = "session" } +notes = "Work weight but NOT max effort. Add 5lbs per session. Landing on Friday means 2 full rest days before Monday's squats — optimal spacing." +primary = true + +[[days.exercises]] +id = "assisted_pullups" +name = "Assisted Pullups (band or Tonal) or Lat Pulldown" +sets = "3×8" +sets_n = 3 +volume = ["back"] +notes = "VERTICAL PULL: Second weekly exposure for adequate lat volume. Use a band looped over the X-bar for assistance, or the Tonal's lat pulldown. Progress toward unassisted reps over time." +primary = true + +[[days.exercises]] +id = "db_row" +name = "Dumbbell Row" +sets = "4×10 per side" +sets_n = 4 +volume = ["back"] +notes = "Unilateral. Full stretch at bottom, squeeze shoulder blade at top. Builds lat thickness and addresses left/right pulling imbalances." +primary = true + +[[days.exercises]] +id = "dips" +name = "Dips (bodyweight → weighted)" +sets = "3×8-12" +sets_n = 3 +volume = ["chest"] +notes = "Use the dip station. Full ROM. Once you can do 3×12 easily, add weight via a belt or holding a DB between your feet." +primary = true + +[[days.exercises]] +id = "db_shrug_single_arm" +name = "Single-Arm DB Shrug" +sets = "2×12 per side" +sets_n = 2 +volume = [] +notes = "TRAP CORRECTION: Light weight, activation-focused. Start with weaker (non-mouse) side. 2-second hold at top. This is the only shrug session per week — the goal is balanced recruitment, not building more upper trap mass on an already overactive muscle." +primary = false + +[[days.exercises]] +id = "face_pulls" +name = "Face Pulls" +sets = "3×15" +sets_n = 3 +volume = ["rear_delts"] +notes = "Twice per week is appropriate for correcting anterior dominance. Light, controlled." +primary = false + +[[days.exercises]] +id = "hammer_curls" +name = "Hammer Curls (DB)" +sets = "3×10" +sets_n = 3 +volume = ["biceps"] +notes = "Builds brachioradialis and bicep. Neutral grip is easier on the elbows than barbell curls." +primary = false + +[[days.exercises]] +id = "pallof_press" +name = "Pallof Press (Tonal)" +sets = "3×10 per side" +sets_n = 3 +volume = ["core"] +notes = "Anti-rotation core work. Far more functional than crunches. The Tonal's cable system is perfect for this." +primary = false + +[[days.exercises]] +id = "dead_bugs" +name = "Dead Bugs" +sets = "3×8 per side" +sets_n = 3 +volume = ["core"] +notes = "Core stability under contralateral limb movement. Directly trains the deep stabilizers." +primary = false + + +[[days]] +id = "weekend" +name = "Saturday & Sunday" +title = "Full Rest" +subtitle = "Recovery & Nutrition Focus" +science_keys = ["protein"] +rationale = """ +After five consecutive training days, two full rest days are essential \ +for CNS and muscular recovery. You've accumulated significant volume \ +across the week — your body builds muscle during rest, not during \ +training. Use this time to meal prep and ensure you're hitting protein \ +targets. Sleep quality on rest days is just as important as training \ +days for adaptation.\ +""" +exercises = [] + + +# ───────────────────────────────────────────────────────────────────── +# Progression +# ───────────────────────────────────────────────────────────────────── + +[progression] +title = "12-Week Progression Framework" + +[[progression.phases]] +name = "Weeks 1–4: Foundation" +description = "Focus on nailing form and building consistency. Start the 3×5 lifts at current working weight (135lbs) and progress linearly. KB days should use a weight you can control perfectly for every rep. Track every session." +targets = [ + "Bench: 135 → 155 lbs", + "Squat: 135 → 175 lbs", + "Deadlift: 135 → 195 lbs", +] + +[[progression.phases]] +name = "Weeks 5–8: Push" +description = "Linear progression should still be working. If you stall on a lift (fail to complete 3×5), repeat the same weight next session. Two consecutive failures = deload 10% and build back up. Add cardio intensity on Thursday (shorter rest between KB complex rounds)." +targets = [ + "Bench: 155 → 175 lbs", + "Squat: 175 → 215 lbs", + "Deadlift: 195 → 255 lbs", +] + +[[progression.phases]] +name = "Weeks 9–12: Transition" +description = "If linear progression stalls, you've outgrown pure novice programming. Shift Monday to a heavy/light scheme (heavy singles or triples followed by back-off sets). Consider moving to a 5/3/1 Wendler-style progression after this block. Re-assess body composition with photos and measurements." +targets = [ + "Bench: 175 → 185+ lbs", + "Squat: 215 → 245+ lbs", + "Deadlift: 255 → 295+ lbs", +] + + +# ───────────────────────────────────────────────────────────────────── +# Nutrition +# ───────────────────────────────────────────────────────────────────── + +[nutrition] +science_keys = ["recomp", "protein"] + +[[nutrition.items]] +label = "Daily Protein Target" +value = "140–175g" +detail = "~0.8–1g per lb bodyweight. This is the single most important nutritional variable." + +[[nutrition.items]] +label = "Caloric Strategy" +value = "Maintenance ± 200 cal" +detail = "Slight deficit on rest days, maintenance or slight surplus on heavy training days. Don't restrict aggressively — you're still building foundational muscle." + +[[nutrition.items]] +label = "Tracking" +value = "Protein only (to start)" +detail = "You don't need to count every macro. Just ensure you hit protein. Use MyFitnessPal or MacroFactor for the first 2 weeks to calibrate your intuition, then adjust." + +[[nutrition.items]] +label = "Timing" +value = "3–4 protein feedings/day" +detail = "~30–45g per meal distributes muscle protein synthesis signaling across the day. Don't skip breakfast if you're training in the afternoon." diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 0000000..8f86b34 --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,16 @@ +# NNNN. Title in Sentence Case + +- **Status:** Proposed +- **Date:** YYYY-MM-DD + +## Context + +What's going on that requires a decision? What forces are at play — technical, philosophical, operational? What constraints exist? Don't argue for a side yet; describe the situation honestly. + +## Decision + +The choice. State it clearly and unambiguously. If multiple options were considered, name the rejected alternatives in one line each — but the body of this section is about the chosen path. + +## Consequences + +What follows from this decision — both good and bad. What does it enable? What does it foreclose? What new work does it create? What ongoing costs does it carry? Be honest about the downsides. diff --git a/docs/adr/0001-hosting-target.md b/docs/adr/0001-hosting-target.md new file mode 100644 index 0000000..e10064e --- /dev/null +++ b/docs/adr/0001-hosting-target.md @@ -0,0 +1,46 @@ +# 0001. Hosting target + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +The existing site is a Zola static site deployed to GitHub Pages via GitHub Actions. Adding a workout tracker introduces two requirements GitHub Pages alone can't satisfy: + +- **Authenticated server-side writes.** The tracker logs sessions as JSON files in the repo. The browser can't safely hold a write-scoped GitHub token, so a server-side function (validating the request and performing the commit) is required. +- **Co-located function + static delivery for the tracker SPA.** A small "API" surface (one endpoint) needs to live next to or near the SPA, ideally on the same provider for simplicity. + +GitHub Pages is static-only and has no native serverless function story. Three options were weighed: + +1. **All-GitHub, with a webhook-triggered GitHub Actions workflow as the "Worker."** Free, single-provider, but cycle time is ~30–60 seconds *per request* (Actions queues, runners spin up). Unacceptable for an interactive tap. +2. **Full migration to Cloudflare Pages.** Site, tracker, Worker all on Cloudflare. One provider, native Worker integration. But the existing site already works on GitHub Pages with DNS, custom domain, and a tested CI pipeline; migration is real cost for marginal gain on the static-content side. +3. **Hybrid.** Site stays on GitHub Pages. Tracker SPA + Worker live on Cloudflare. Two providers, but each does what it's good at, and migration cost is zero. + +A subtlety: it would be tempting to have the main site serve `program.json` to the tracker (since both read the same TOML). That makes the static site quietly responsible for being a data source for an external app, coupling two deploys at runtime. The cleaner alternative is for both the site and the tracker to *independently* consume the source TOML at build time — the site renders its workout-program page; the tracker emits its own `program.json` to its own deploy. The TOML is the shared input; the deploys don't talk to each other. + +## Decision + +**Hybrid hosting (option 3), with each deploy self-contained:** + +- **Main site** — GitHub Pages, status quo. Zola, deployed via GitHub Actions. Zero API responsibilities. Pure content. +- **Tracker SPA** — Cloudflare Pages, separate deploy. Vite/React/TS bundle, reads `data/workout_program.toml` at build time and emits its own `program.json` to its `dist/`. SPA fetches `program.json` from same-origin at runtime. +- **Worker** — Cloudflare Workers, paired with the tracker. Single endpoint accepting authenticated session POST; performs the git commit via the GitHub API. +- **Repo as the database.** Both deploys read the same monorepo's TOML at build time. The Worker is the only writer. All reads are static (CDN-served). + +## Consequences + +**Good:** +- The static site keeps doing what it's good at, with no migration cost. +- The tracker gets the right tool (Workers) for its single dynamic responsibility, on the same provider as its SPA — clean operational unit. +- No "site as backend" anti-pattern. Each deploy is self-contained; the TOML is the only shared coupling, resolved at build time. +- All free-tier. Cloudflare Workers free tier (100k req/day) and Pages free tier are far beyond a single-user load. +- Portability: if Cloudflare's terms ever change, the Worker is small and the SPA is portable. The main site is unaffected. + +**Bad:** +- Two providers (GitHub + Cloudflare) to manage credentials, billing settings, and DNS records on. Mitigated by the fact that each provider hosts a self-contained piece. +- Two CI pipelines — one per host. Negligible operational burden but worth acknowledging. +- The TOML being read by two independent build pipelines means a program edit triggers two rebuilds. Both are fast (~60s) and parallel; the user experiences this as "edit, push, both are live in about a minute." + +**Foreclosed:** +- Tightly-coupled SSR-style architectures where the main site dynamically serves data to the tracker. (Intentional — keeps the site pure.) +- Single-provider operational simplicity. (Considered but rejected; the migration cost outweighs the simplification for this scope.) diff --git a/docs/adr/0002-domain-strategy.md b/docs/adr/0002-domain-strategy.md new file mode 100644 index 0000000..6890584 --- /dev/null +++ b/docs/adr/0002-domain-strategy.md @@ -0,0 +1,35 @@ +# 0002. Domain strategy + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +The tracker needs a URL. Two patterns: + +- **Subpath** — `shanemurphy.space/tracker`. SEO-friendly (one domain), single set of cookies. Requires either path-based routing on a single host or a reverse proxy in front. Both contradict the hybrid hosting model (ADR 0001), which has the site and the tracker on different providers (GitHub Pages and Cloudflare Pages). Making subpath work would essentially force collapsing the two deploys back to one provider, undoing ADR 0001's reasoning. +- **Subdomain** — `tracker.shanemurphy.space`. Independent DNS, independent deploy, standard pattern for multi-app domains. No reverse proxy. Clean separation between the static site and the app. Trivial to set up (CNAME the subdomain to Cloudflare Pages). + +For a single-user personal tracker, SEO consolidation is a non-goal. Cookie scope and shared-auth concerns don't apply (auth is a Bearer token in localStorage; cookies aren't load-bearing). + +The subdomain name itself: `tracker.` is descriptive, brief, and unambiguous. Alternatives considered (`gym.`, `lift.`, `app.`) trade descriptiveness for brevity or playfulness; not a meaningful improvement. + +## Decision + +The tracker lives at **`tracker.shanemurphy.space`**, served by Cloudflare Pages. The main site stays at the apex (and `www.`) on GitHub Pages. + +DNS: a CNAME record for `tracker` pointed at the Cloudflare Pages target, set in the Squarespace DNS panel where the rest of the domain's records live. + +## Consequences + +**Good:** +- Falls out of ADR 0001 cleanly — each deploy owns its origin, no path-based plumbing required. +- Independent deploys, independent CI failures, independent caching layers. Either side can break without taking the other down. +- Easy to add more apps later under sibling subdomains (`projectN.shanemurphy.space`) without touching the main site. + +**Bad:** +- Three DNS records to maintain (apex, `www.`, `tracker.`) instead of one. +- Cross-link from the site's workout-program page to the tracker is a bare `https://tracker.shanemurphy.space` link — no chance of accidental in-app navigation between site and tracker. (Arguably a feature: the tracker is a different mode of use.) + +**Foreclosed:** +- Anything that relies on the site and tracker sharing cookies or session state automatically. Not relevant here. diff --git a/docs/adr/0003-auth-model.md b/docs/adr/0003-auth-model.md new file mode 100644 index 0000000..613e248 --- /dev/null +++ b/docs/adr/0003-auth-model.md @@ -0,0 +1,52 @@ +# 0003. Auth model for the tracker write path + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +The tracker SPA needs to authenticate to the Worker, which performs the actual `git commit` against the repo via a GitHub PAT held server-side. Two distinct credentials are at play: + +1. A **GitHub PAT** (fine-grained, scoped to this repo + Contents read/write) lives as a Cloudflare Worker secret. This is what does the commit. It is never exposed to the browser. +2. A **tracker auth token** that the SPA presents to the Worker, proving "this request is from Shane." + +This ADR is about #2. + +Options weighed: + +- **Shared secret (Bearer token).** Worker holds a long random `TRACKER_TOKEN` in its env. Tracker SPA stores the same value in localStorage. Every Worker request carries `Authorization: Bearer `; Worker compares constant-time. +- **OAuth with GitHub.** SPA does the GitHub OAuth dance, gets an access token, Worker validates. Standard, well-understood, but introduces callback handling, token storage, refresh, and an entire identity flow for a single-user app. The Worker still uses its own PAT to commit (the user's GitHub token wouldn't have scoped repo write where it needs it without further configuration). Net: extra ceremony with no real gain. +- **WebAuthn / passkey.** Modern and biometric-friendly, but overkill for a personal tool. +- **No auth (URL obscurity).** Unacceptable; the Worker writes to the repo. +- **Static token + IP allowlist.** Brittle on cellular and on the road. + +For a single user, the simplest credential that adequately protects a low-blast-radius write surface is a shared secret. The Worker's blast radius is *already* tightly scoped — it accepts only `POST /sessions`, validates the JSON shape, and writes to a fixed path in one repo. A leaked tracker token gives an attacker the ability to commit garbage session JSON; not a serious harm, easy to revoke (rotate the Worker env). + +## Decision + +**Single-user shared secret, Bearer-token style.** + +- A long random `TRACKER_TOKEN` (≥32 bytes hex) is held as a Cloudflare Worker secret. +- The same value is stored in tracker localStorage. A Settings screen in the SPA lets the user paste/replace it. +- Every Worker request requires `Authorization: Bearer `. The Worker performs a constant-time comparison. +- The Worker's endpoints are narrow by construction: a single `POST /sessions` accepting validated JSON, writing to a fixed path in a fixed repo via the GitHub PAT. No general "execute commit" surface. +- Token rotation: regenerate the value, update the Worker secret, refresh localStorage on next tracker visit. Not automated; this is a manual operation done rarely. + +## Consequences + +**Good:** +- Trivial to implement on both sides. Single env var on the Worker; single localStorage key on the SPA. +- No third-party auth dependency. No OAuth callbacks, no flow handling. +- Token persists in localStorage indefinitely; no re-auth friction in normal use. +- Constant-time comparison prevents timing-based extraction. + +**Bad:** +- Static long-lived token. If localStorage is exfiltrated (e.g., XSS in the tracker SPA), the token leaks. Mitigated by: + - The Worker accepts only narrow operations (one endpoint, validated payloads, fixed write path). + - The PAT held server-side has fine-grained scope (this repo, contents only). + - Rotation is manual but easy. +- Manual rotation is a vigilance tax. Acceptable for personal use; the rotation cadence can be "when something feels off" rather than scheduled. + +**Foreclosed:** +- Multi-user. If the tracker ever needs to support more than one person, this ADR is superseded by an OAuth-based one. +- Per-device tokens. Possible to issue distinct tokens per device by storing a list in the Worker, but unnecessary at one user. diff --git a/docs/adr/0004-ci-rebuild-filtering.md b/docs/adr/0004-ci-rebuild-filtering.md new file mode 100644 index 0000000..145cf5d --- /dev/null +++ b/docs/adr/0004-ci-rebuild-filtering.md @@ -0,0 +1,43 @@ +# 0004. CI rebuild filtering for session-only commits + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +Once Phase 4 lands, every gym session results in a commit to the repo (a new `data/sessions/.json` written by the Worker). On a typical training week, that's 4–5 commits — a few hundred per year. + +Two CI pipelines watch the repo: + +- **GitHub Actions** rebuilds the Zola site on every push to `main`. The site's workout-program page reads `data/workout_program.toml` but currently does not surface session data. So a session-only commit produces a byte-identical site. +- **Cloudflare Pages** (Phase 2 onward) rebuilds the tracker SPA on every push to `main`. The SPA reads its own input (the TOML at build time, plus runtime fetches of session JSON). A session-only commit doesn't change the SPA bundle either. + +Without filtering, every session commit triggers two rebuilds that produce no visible change — pure waste, and slow cache invalidation on the CDN side. With filtering, both pipelines skip when the change is "session JSON only." + +The filtering mechanisms differ: + +- GitHub Actions supports `paths-ignore` and `paths` filters on the workflow `on:` trigger. +- Cloudflare Pages supports `[skip ci]` in commit messages and a build-watching path config. + +## Decision + +**Filter both pipelines so session-only commits do not rebuild.** + +- The site workflow uses `paths-ignore: ['data/sessions/**']` on the `push` trigger, so commits touching only those files don't run the build. +- The tracker Pages project is configured to skip builds when no monitored paths change. As an additional safety net, the Worker writes session commits with `[skip ci]` in the commit message — Cloudflare Pages honors that convention. + +A future ADR will supersede this one if/when the site or tracker grows a feature that *does* depend on session data being visible (e.g., a "last logged" indicator on the workout-program page). At that point, filtering needs to be revisited. + +## Consequences + +**Good:** +- ~30 fewer site builds and ~30 fewer SPA builds per month, all of which would have been no-ops. +- Faster perceived sync from a logged session: the Worker commit lands; nothing else needs to happen. +- CDN cache stays warm; user-facing TTFB is unaffected by background logging activity. + +**Bad:** +- Adds a "what changed?" question every time the site or tracker behavior depends on something. Future ADR-superseding-this-one needs a clear trigger condition so the filter doesn't silently drop a needed rebuild. +- The skip-condition lives in two places (workflow file and Pages config). Keep them in sync. + +**Foreclosed:** +- Site features that read session JSON at build time. Possible later, but requires this ADR to be superseded. diff --git a/docs/adr/0005-progression-rule-encoding.md b/docs/adr/0005-progression-rule-encoding.md new file mode 100644 index 0000000..a1d6d1a --- /dev/null +++ b/docs/adr/0005-progression-rule-encoding.md @@ -0,0 +1,72 @@ +# 0005. Encoding progression rules in the program TOML + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +The program TOML currently encodes progression rules as free-text in each exercise's `notes` field — e.g., `"Add 5lbs per session."` Humans can read and apply these. The tracker cannot. + +For the tracker to auto-suggest the next session's working weight (the killer feature of this whole project), each progressing exercise needs structured progression metadata. Three patterns appear in the existing program: + +1. **Linear** — fixed increment per session. Squat, Bench, OHP, Deadlift, Pendlay Row. +2. **Double progression** — fixed weight until all sets are completed at the top of the rep range, then bump weight and reset to the bottom. The Wednesday hypertrophy DB lifts, Friday DB Row, Hammer Curls. +3. **AMRAP** — log reps achieved; "progression" is just "more next time." Assisted Pullups, Dips. + +Many exercises don't progress at all — Face Pulls, mobility, conditioning. Those simply omit the field. + +The schema must be: + +- **Self-explanatory enough that I (Claude) can read and reason about it without external documentation.** Following the same principle as ADR 0001 / the data-derived UI rule. +- **Extensible.** New `kind` values (e.g., wave loading, RPE-based, percent-of-1RM) can be added later without breaking existing entries. +- **Minimal up front.** Don't pre-specify deload rules, plateau detection, or anything else the tracker doesn't implement yet. Add fields when the tracker grows behaviors that need them. + +## Decision + +A `progression` table is added to each exercise that progresses. Three `kind` values are defined now; more can be added later. + +**Linear:** +```toml +progression = { kind = "linear", increment_lbs = 5, cadence = "session" } +``` +Bump the working weight by `increment_lbs` after every successful session. + +**Double progression:** +```toml +progression = { kind = "double", rep_range = [8, 12], increment_lbs = 5 } +``` +Stay at the current weight until all sets are completed at `rep_range[1]`. Then bump weight by `increment_lbs` and reset target reps to `rep_range[0]`. + +**AMRAP:** +```toml +progression = { kind = "amrap", rep_range = [8, 12] } +``` +No automatic weight progression. Tracker logs reps achieved and surfaces the trend. Optionally a target band can be encoded for context. + +**No field** = no auto-progression. Tracker shows the prescribed `sets` string and lets the user log completion as a checkbox without weight tracking. + +**Cadence default.** All linear lifts in the current program are performed once per week (stable IDs distinguish e.g. BB Squat from Front Squat), so `cadence = "session"` and `cadence = "week"` are operationally identical. We default to `"session"` everywhere as the simpler model, and revisit per-exercise if any lift starts being performed multiple times per week with the same ID. + +**Deferred fields:** + +- **Deload rules** (after N failed sessions, drop X%). Will be added when the tracker implements deload handling — likely Phase 3. +- **Stall detection.** Same. +- **RPE / RIR-aware progression.** Not in the current program; add when introduced. +- **Plate-rounding precision.** Increments are in lbs; assume the user has 2.5lb plates. If micro-loading becomes relevant, add `min_increment_lbs`. + +## Consequences + +**Good:** +- The tracker has everything it needs to compute next session's target weight from the program + last session log. +- Schema is small (≤4 keys per record) and easy to read in the TOML. +- Free-text `notes` stay alongside as human prose; the structured field doesn't replace the human-readable rationale. +- Adding new `kind` values later is non-breaking: tracker handles unknown kinds with a fallback ("show prescribed, no auto-progression"), and exercises retain the same `id` so history continues across schema additions. +- Claude can read both the TOML and a few weeks of session logs and propose `progression` edits the same way it proposes any other TOML edit. + +**Bad:** +- The structured rule and the free-text `notes` can drift. Discipline: when editing one, eyeball the other. +- "Successful session" needs a clear definition somewhere — currently "all prescribed sets completed at prescribed reps." That definition lives in the tracker's progression engine code, not the schema. If it ever needs configuring, it becomes a new field here. + +**Foreclosed:** +- Encoding deload, plateau, or RPE rules right now. (Intentional. Add when needed.) +- Per-set progression schemes (e.g., "first set 3×5, back-off sets 2×8"). The current program doesn't have these; if introduced, the schema needs a `sets` array with per-set progression metadata. diff --git a/docs/adr/0006-session-json-schema.md b/docs/adr/0006-session-json-schema.md new file mode 100644 index 0000000..f33ead3 --- /dev/null +++ b/docs/adr/0006-session-json-schema.md @@ -0,0 +1,90 @@ +# 0006. Session JSON schema + +- **Status:** Accepted +- **Date:** 2026-05-07 + +## Context + +Each gym session produces one JSON file written to the repo via the Worker. This ADR fixes its shape — what each file contains, where it lives, and how it relates to the program TOML. + +Constraints: + +- **AI-readable.** Same principle as the program TOML: Claude must be able to glance at a session file and reason about it without external schema docs. +- **Self-contained per file.** A session file is the unit of commit. Reconstructing what happened that day shouldn't require fetching N other files. +- **Forward-compatible.** Additions over time (per-set RPE, video links, body-weight, sleep score, whatever) shouldn't break existing files. Tracker treats unknown fields as ignorable. +- **Minimum viable.** Phase 3 is "did the set" UX. Don't pre-design fields the tracker won't write. + +A subtlety: the program changes over time. A session logged on 2026-05-08 might have been performed against a program that no longer exists in `data/workout_program.toml`. To preserve that context, each session records the git SHA of the program revision active at log time. Anyone (including future Claude) can `git show :data/workout_program.toml` to reconstruct exactly what the day's program was. + +A second subtlety: the user may override the prescribed weight at log time ("didn't feel like 145, did 140 instead"). The session must record what was *actually* done, not what was prescribed. The prescribed value can be looked up via the program revision; the actual value cannot be reconstructed without being captured. + +## Decision + +**File location:** `data/sessions/-.json`, e.g. `data/sessions/2026-05-08-monday.json`. One file per session. Sorted lexically by date for `git log` readability. + +**Shape:** + +```json +{ + "date": "2026-05-08", + "day_id": "monday", + "program_revision": "abc1234", + "started_at": "2026-05-08T17:32:00Z", + "ended_at": "2026-05-08T18:45:00Z", + "exercises": [ + { + "id": "barbell_squat", + "completed": true, + "weight_lbs": 145, + "reps": [5, 5, 5] + }, + { + "id": "barbell_bench", + "completed": false, + "weight_lbs": 160, + "reps": [5, 5, 4], + "notes": "missed last rep" + }, + { + "id": "face_pulls_mon", + "completed": true + } + ], + "notes": "" +} +``` + +**Field semantics:** + +- `date` — ISO date the session was performed. Required. +- `day_id` — slug matching the day in the program (e.g. `"monday"`, `"wednesday"`). Required. Used to resolve which program day this session was logged against. +- `program_revision` — git SHA (short, 7+ chars) of the commit holding the program TOML when the session was started. Required. Enables time-travel reconstruction of prescribed work. +- `started_at`, `ended_at` — ISO timestamps. Optional. Useful for session-length analysis later; not load-bearing. +- `exercises[]` — required. Order matches the program's exercise order at log time, but consumers should not rely on order; key off `id`. + - `id` — stable exercise ID from the program TOML. Required. + - `completed` — boolean. Required. Whether all prescribed sets were completed at prescribed reps/weight (or better). Drives progression: `true` allows the next-session weight bump per ADR 0005's rules; `false` holds or eventually deloads. + - `weight_lbs` — optional. The actual weight used. Required for any exercise with a `progression` rule that uses weight. Omit for bodyweight or unweighted exercises. + - `reps` — optional array of integers, one per set. Required for `kind = "double"` and `kind = "amrap"` exercises (so the tracker can decide rep-range progression). Optional for linear (the `completed` boolean is sufficient there). + - `notes` — optional string. Free-form per-exercise comment. +- `notes` — optional string. Free-form session-level comment. + +**Unknown fields** are preserved on read (tracker passes them through if rewriting a session), but the schema-of-record is what's documented above. Additions land via this ADR being superseded. + +## Consequences + +**Good:** +- Self-contained: one file fully describes one session. +- Time-travel: `program_revision` plus git history makes "what was prescribed that day?" answerable forever. +- Progression engine has exactly what it needs: `completed` for linear, `reps` for double/amrap, `weight_lbs` for the running working-weight. +- AI-readable: the structure is obvious from skimming a few fields. Field names are not abbreviated. +- File-per-session means `git log data/sessions/` is a chronological training journal, and any tool (cli, ripgrep, jq) can analyze the corpus trivially. + +**Bad:** +- Many small files. After 12 months of training that's ~250 files. Not a real performance concern, but lists in the tracker need pagination or filtering by date. +- The `program_revision` field requires the Worker to know the current HEAD SHA at write time. The Worker does the commit, so it has access — but it must record the SHA *before* the session commit (i.e., the parent SHA), not the SHA of the session commit itself. +- Double-progression "did all sets at top of range" decision requires `reps[]`, which adds a small UX wrinkle for the user (they tap done, but did they hit 12 reps or 11 on each set?). v1 may default to "if you tap completed, assume you hit the top of the range; if you didn't, manually edit `reps`." Refine as needed. + +**Foreclosed:** +- Per-rep RPE/RIR tracking. Possible additive field; not now. +- Per-set weight variation (drop sets, ascending sets). Not in the current program. +- Photos/video. Not relevant to this tool's purpose. diff --git a/docs/adr/0007-nix-build-tooling.md b/docs/adr/0007-nix-build-tooling.md new file mode 100644 index 0000000..117cfb8 --- /dev/null +++ b/docs/adr/0007-nix-build-tooling.md @@ -0,0 +1,49 @@ +# 0007. Nix flake as the build toolchain + +- **Status:** Accepted +- **Date:** 2026-06-06 + +## Context + +The site is built with Zola, a single static binary. Until now the toolchain lived in two unrelated places: + +- **Locally**, Zola was installed via Homebrew (`/opt/homebrew/bin/zola`, currently 0.22.1). +- **In CI**, `deploy.yml` installed Zola with `taiki-e/install-action`, pinned to `zola@0.22.1`. + +Two pins for the same tool, kept in sync by hand. They agree today; nothing enforces it. A `brew upgrade` bumps the local version silently, and the CI pin only changes when someone remembers to edit the workflow. For a static site this rarely bites, but the project is heading toward a tracker SPA (see `docs/roadmap.md`) with its own toolchain (Vite, a Cloudflare Worker), where reproducibility matters more and "works on my machine" failures get expensive. + +Determinate Nix is now installed on the dev machine, which makes a flake the natural single source of truth: one pinned toolchain that the local shell, local builds, and CI all draw from. + +Alternatives considered: +- **Status quo (Homebrew + `taiki-e` pin).** Zero new concepts, but keeps the two-pin drift problem. +- **Devbox / mise / asdf.** Lighter than Nix, but another tool to install everywhere and weaker reproducibility guarantees than a locked flake. +- **Docker dev image.** Heavier, slower inner loop, and overkill for a single static binary. + +## Decision + +**Adopt a Nix flake (`flake.nix` + `flake.lock`) as the canonical build toolchain, and drive CI from it.** + +The flake exposes three outputs, all sharing one `pkgs.zola`: + +- `packages.default` — renders the site into `$out` (the `public/` tree). Because the config file is `zola.toml` rather than the default `config.toml`, every invocation passes `--config zola.toml`. +- `devShells.default` — `nix develop` drops into a shell with that same Zola on `PATH`. +- `apps.serve` (the default app) — `nix run` runs `zola serve` for live-reload local development. + +`flake.lock` pins `nixpkgs` (currently the `nixos-unstable` revision shipping Zola 0.22.1, matching the previous CI pin). The Zola version now changes only by `nix flake update` plus a committed lockfile bump — one place, reviewable in a diff. + +CI switches from `taiki-e/install-action` to `DeterminateSystems/determinate-nix-action` + `nix build`. The build output is a store symlink, so the workflow dereferences it (`cp -rL result public`) before handing it to `upload-pages-artifact`. + +## Consequences + +**Good:** +- One pinned toolchain for local dev and CI. No more hand-synced version pins; drift is structurally impossible. +- Reproducible builds: the lockfile makes "the bytes CI produces" a function of committed state, not of whatever Homebrew last installed. +- A natural home for future tooling. When the tracker SPA arrives, its Node/Bun/Worker tools join the same flake, and `nix develop` becomes the one onboarding command. + +**Bad:** +- Requires Nix to use the canonical build path. Anyone without Nix can still run a Homebrew/`cargo install` Zola directly, but they're then off the pinned toolchain. +- CI cold builds pay a Nix install + store fetch (~tens of seconds) that the lightweight `taiki-e` action avoided. Acceptable for a once-per-push Pages deploy; revisit with a Nix cache (e.g. Magic Nix Cache / Cachix) if it becomes annoying. +- Adds Nix as a concept to the repo. Mitigated by keeping `flake.nix` small and commented. + +**Foreclosed:** +- Nothing hard. The flake is additive; reverting means restoring the `taiki-e` step and deleting two files. This ADR documents the rationale so a future reverter knows what they'd be giving up. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..ffa519f --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,29 @@ +# Architecture Decision Records + +This directory holds the load-bearing design decisions for the project. Each decision is one numbered markdown file. The history of these files is the project's architectural memory — readable by Shane, by Claude in future sessions, and by anyone else who needs to understand *why* something is the way it is. + +## Convention + +- **Format:** Nygard-style. Five sections: **Title · Status · Context · Decision · Consequences**. +- **Length:** One page or less. ADRs are decision artifacts, not design documents. +- **Numbering:** Zero-padded sequential — `0001-...`, `0002-...`. Never reused. +- **File name:** `NNNN-short-kebab-slug.md`, e.g. `0001-hosting-target.md`. +- **Status lifecycle:** `Proposed` → `Accepted` → (later, if revised) `Superseded by NNNN`. Once an ADR is `Accepted`, its content does not change. Decisions evolve by writing a *new* ADR that supersedes the old one. The old ADR's status changes to `Superseded by NNNN` and gains a one-line note pointing forward; otherwise it stays as it was. The git log of `docs/adr/` is the architectural changelog. +- **Scope:** Write an ADR for any decision that would be confusing or annoying to reverse, or where the *next* person (including future Shane) would benefit from knowing why this path was chosen over the alternatives. Skip for trivial choices. + +## Template + +A starter template is at `0000-template.md`. Copy it, renumber, fill it in. + +## Index + +| # | Title | Status | +|---|---|---| +| 0000 | Template | n/a | +| [0001](0001-hosting-target.md) | Hosting target | Accepted | +| [0002](0002-domain-strategy.md) | Domain strategy | Accepted | +| [0003](0003-auth-model.md) | Auth model for the tracker write path | Accepted | +| [0004](0004-ci-rebuild-filtering.md) | CI rebuild filtering for session-only commits | Accepted | +| [0005](0005-progression-rule-encoding.md) | Encoding progression rules in the program TOML | Accepted | +| [0006](0006-session-json-schema.md) | Session JSON schema | Accepted | +| [0007](0007-nix-build-tooling.md) | Nix flake as the build toolchain | Accepted | diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..b7b9814 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,233 @@ +# Workout Tracker — Roadmap + +Living document. Updated as phases complete or assumptions change. The original planning artifact is in +`.claude/plans/`; this is the durable record committed to the repo. + +## Context + +The site is a Zola SSG at shanemurphy.space (GitHub Pages, GitHub Actions deploy). It includes a workout program in +`data/workout_program.toml` with auto-derived volume targets per muscle group. + +The goal is a workout tracker that: + +- **Solves progressive overload** — shows today's prescribed weight (per program rules), takes a "did the set" tap, + increments next session's target. +- **Treats the program as mutable** — Shane revises it frequently with AI assistance, so the program file stays plain + text in git. +- **Treats logs as owned data** — sessions live in the same repo as JSON files, in a format both Claude and Shane can + read. +- **Stays on free hosting.** +- **Lives in the same monorepo as the static site.** + +This roadmap enumerates the discrete plans, decisions, and implementations needed. Each phase below will get its own +dedicated plan when it's reached. + +--- + +## Project documentation strategy + +Design rationale persists in the repo, not in per-user/per-session scratch files. Future Claude sessions, future Shane, +and anyone else reading should be able to understand *why* the architecture is what it is from `git log` alone. + +Two artifacts are committed: + +1. **This roadmap** at `docs/roadmap.md`. Living document — every phase completion updates it (mark phase done, add + lessons learned, revise downstream phases). +2. **Architecture Decision Records (ADRs)** at `docs/adr/`. Every meaningful design decision gets a short, numbered ADR. + See `docs/adr/README.md` for the format and convention. + +The principle: **single source of truth, in the repo, AI-readable** — extending the same rule CLAUDE.md captured for UI +data to design rationale. + +--- + +## Cross-cutting open decisions (Phase 0) + +These touch every later phase, so resolving them first is load-bearing. Each will be settled with an ADR. + +1. **Hosting target.** Currently on GitHub Pages. The tracker write path needs a serverless function that can commit to + the repo via the GitHub API. +2. **Domain strategy.** Subdomain (`tracker.shanemurphy.space`) vs subpath (`/tracker`). +3. **Auth between phone and Worker.** Single-user shared secret vs OAuth-with-GitHub vs nothing-but-obscurity. +4. **CI rebuild filtering.** Whether session-only commits should skip the tracker SPA rebuild. +5. **Structured progression rules in TOML.** Whether to encode `progression = { kind, increment, cadence, deload }` now + or defer. + +--- + +## Phases + +Each phase ends in a commit. Each will get its own plan when it starts. + +### Phase 0 — Decisions & schema preparation + +**Goal:** Resolve the cross-cutting decisions above and prep the TOML so the tracker can consume it cleanly. + +**Plans/work:** + +- Decide hosting model (cross-cutting #1) → ADR 0001. +- Decide domain strategy (cross-cutting #2) → ADR 0002. +- Decide auth model (cross-cutting #3) → ADR 0003. +- Decide CI rebuild filtering (cross-cutting #4) → ADR 0004. +- Decide structured-progression-now-or-later (cross-cutting #5) → ADR 0005. +- Add stable `id = "barbell_bench"`-style fields to every exercise in `data/workout_program.toml`. +- Encode structured `progression = { ... }` metadata on linearly-progressing exercises (Squat, Bench, OHP, Deadlift, + Row). +- Define the JSON shape for emitted program data (`program.json`) and session logs (`sessions/.json`). + +**Deliverable:** + +- Updated `workout_program.toml` with `id` + `progression` per exercise. +- ADRs 0001–0005 committed to `docs/adr/`. +- This roadmap committed to `docs/roadmap.md`. +- CLAUDE.md updated to reference the ADR convention. +- No tracker code yet. + +**Ready when:** All five cross-cutting decisions have an Accepted ADR; TOML has stable IDs; session JSON shape is +sketched; `docs/roadmap.md` and `docs/adr/` exist in the repo. + +--- + +### Phase 1 — Build pipeline & data exposure + +**Goal:** Make the program (and later, sessions) available to a runtime fetcher at a stable URL on the deployed site, +without bundling them into a SPA build. + +**Plans/work:** + +- Add a Zola build step that emits `static/program.json` from the TOML on each build. +- Decide where session JSON lives: `data/sessions/*.json` (Zola consumes & passes through) vs `static/sessions/*.json` ( + untouched passthrough). +- Update `deploy.yml` if needed; add CI path filter so session-only commits skip SPA rebuilds (relevant once Phase 2 + adds one). +- Verify program.json is fetchable from a deployed page. + +**Deliverable:** A live URL like `shanemurphy.space/program.json` that returns the compiled program. + +**Ready when:** `curl https://shanemurphy.space/program.json` returns valid JSON matching the schema agreed in Phase 0. + +--- + +### Phase 2 — Tracker SPA scaffold + +**Goal:** A deployable Vite + React + TS app at the chosen URL, reading the program and rendering "Today" with no +logging behavior yet. + +**Plans/work:** + +- Decide monorepo layout: `tracker/` directory alongside Zola's `content/`, `templates/`, etc. +- Initialize Vite + React + TypeScript in `tracker/`. +- Routing (React Router or TanStack Router): Today / History / Settings. +- Today screen: fetch `program.json`, identify today's day-of-week, render the prescribed exercises read-only. +- Add CI to build and deploy the tracker SPA to the chosen URL. +- Cross-link from the main site's workout-program page. + +**Deliverable:** A deployed tracker URL that renders today's program in a phone-friendly layout. No state, no checkboxes +yet. + +**Ready when:** On a phone in the gym, you can open the URL and read what to do today. + +--- + +### Phase 3 — Tracker v1 (offline-only logging) + +**Goal:** The actual app — checkbox UX, progressive overload calculation, local session history. localStorage +persistence only; no sync yet. + +**Plans/work:** + +- "Did the set" UX: per-exercise checkbox with current target weight rendered prominently. One-tap completion. Optional + weight override field. +- Progression engine: read `progression` rule from program, look up last completed session for that exercise ID, compute + today's target weight. Handle deload after N consecutive incomplete sessions. +- localStorage schema mirroring the future session JSON shape, so Phase 4 sync is a serialization swap, not a rewrite. +- "End session" flow: mark complete, update progression state. +- History view: list past sessions, basic table. +- PWA manifest + install prompt. Service worker for shell caching only (defer offline-first reads to Phase 6 if it's a + lift). + +**Deliverable:** A functional tracker on the phone. Loses data if browser storage is cleared. Sufficient for a 2-week +trial. + +**Ready when:** Used in the gym for 1–2 weeks with feedback on in-gym UX. + +--- + +### Phase 4 — Sync (Worker + repo writes) + +**Goal:** Sessions persist durably as JSON files in the repo. Multi-device read works. + +**Plans/work:** + +- Cloudflare Worker (or chosen alternative) accepting authenticated session POST and committing to the repo via GitHub + API. +- Auth: Bearer token in tracker localStorage, validated against a Worker secret. Single-user model. +- Tracker sync flow: localStorage immediate write on "End session," POST in background, retry on failure. UI shows sync + status. +- Tracker reads sessions from `/sessions/*.json` (or an index file) on launch, reconciles with localStorage. +- CI path filter so session-only commits skip the SPA rebuild. +- Backfill: import any sessions logged in Phase 3 from localStorage on first sync. + +**Deliverable:** Logging on phone → JSON file in repo within ~60s. History queryable from any device. + +**Ready when:** A session logged on phone is visible in `git log` and renders correctly on a second device after +refresh. + +--- + +### Phase 5 — Research-mode loop + +**Goal:** Close the cycle that motivates this whole project: log → review → revise program with AI assistance. + +**Plans/work:** + +- Document the workflow in `docs/program-iteration.md`. +- Optional: a small "stats" view in the tracker (or a static page on the main site) summarizing adherence and trend per + lift. +- Optional: a `scripts/analyze.ts` (Bun) that emits a markdown summary of the last N weeks for paste-into-conversation. + +**Deliverable:** A documented, repeatable loop where every 4–8 weeks Shane asks Claude to review and propose program +changes; Shane commits; tracker picks up the new program; history continues seamlessly via stable IDs. + +**Ready when:** One full review cycle has happened end-to-end and the program has been updated based on logged data. + +--- + +### Phase 6 — PWA polish (deferred until needed) + +**Goal:** Make it gym-grade — installable, offline-resilient, fast on a mid-tier phone. + +**Plans/work:** + +- Service worker offline-first reads. +- Install prompt UX, app icon, splash screen. +- Performance pass: bundle size, time-to-interactive on phone, tap responsiveness. +- Robust error handling for sync failures. + +**Deliverable:** App-quality experience indistinguishable from a native gym app. + +**Ready when:** A workout in airplane mode syncs cleanly on reconnect with no data loss. + +--- + +## What this roadmap is NOT + +- Not a commitment to all phases. Phases 5 and 6 may turn out unnecessary; re-evaluate after Phase 4. +- Not a fixed timeline. Each phase is planned and executed when ready. +- Not a substitute for per-phase plans. + +## Critical files (current state) + +- `data/workout_program.toml` — the program. Will gain `id` and `progression` fields in Phase 0. +- `templates/workout-program.html` — already consumes the TOML at build time. +- `.github/workflows/deploy.yml` — will gain steps in Phases 1, 2, 4. +- `CLAUDE.md` — records project-wide rules. Will gain the ADR convention in Phase 0. +- `docs/roadmap.md` (this file) — committed roadmap, revised as phases complete. +- `docs/adr/000N-*.md` — one ADR per architectural decision. +- (Future) `tracker/` — the SPA, scaffolded in Phase 2. +- (Future) `data/sessions/*.json` (or `static/sessions/*.json`) — log files, written by the Worker in Phase 4. + +## Verification approach + +Each phase has its own "Ready when" criterion above. The overall verification is a 4-week real-world trial: log every +gym session, review logs with Claude, revise the program once, see continuity preserved. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..bffd844 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1780243769, + "narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "331800de5053fcebacf6813adb5db9c9dca22a0c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..47dca0e --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "shanemurphy.space — Zola static site"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + in + { + # `nix build` → renders the site into ./result (the contents of public/). + # Config lives in zola.toml (non-default name), so pass it explicitly. + packages = forAllSystems (pkgs: { + default = pkgs.stdenv.mkDerivation { + pname = "shanemurphy-space"; + version = "0.1.0"; + src = ./.; + nativeBuildInputs = [ pkgs.zola ]; + buildPhase = '' + zola --config zola.toml build --output-dir $out + ''; + # mkDerivation's default installPhase would clobber $out; we wrote there directly. + dontInstall = true; + }; + }); + + # `nix develop` → shell with the same zola the build uses. + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = [ pkgs.zola ]; + # `nix develop` drops into a bash subshell. When launched from Warp, + # emit Warp's bootstrap escape sequence so the subshell gets blocks, + # completions, etc. + # + # Two guards: + # - $- contains `i` only for an interactive shell. Under direnv the + # hook runs non-interactively inside the already-warpified host + # shell, so there's no subshell to bootstrap — skip it there. + # - $TERM_PROGRAM makes it a no-op outside Warp. + shellHook = '' + case $- in + *i*) + if [ "$TERM_PROGRAM" = "WarpTerminal" ]; then + printf '\eP$f{"hook": "SourcedRcFileForWarp", "value": { "shell": "bash" }}\x9c' + fi + ;; + esac + ''; + }; + }); + + # `nix run` / `nix run .#serve` → live-reloading dev server. + apps = forAllSystems (pkgs: + let + serve = pkgs.writeShellScriptBin "serve" '' + exec ${pkgs.zola}/bin/zola --config zola.toml serve "$@" + ''; + in + { + default = self.apps.${pkgs.stdenv.hostPlatform.system}.serve; + serve = { + type = "app"; + program = "${serve}/bin/serve"; + }; + }); + }; +} diff --git a/sass/main.scss b/sass/main.scss index b291f95..1e77afb 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -370,25 +370,65 @@ a { .wp-vol-grid { display : grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap : 0.5rem; margin-top : 0.75rem; } -.wp-vol-item { text-align: center; padding: 0.5rem 0; } +.wp-vol-item { + text-align : center; + padding : 0.65rem 0.4rem; + border-radius: 6px; + + summary { + list-style: none; + cursor : pointer; + &::-webkit-details-marker { display: none; } + } + + &.is-under { + background: color-mix(in srgb, var(--color-muted) 12%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-muted) 35%, transparent); + } + &.is-over { + background: color-mix(in srgb, var(--color-border) 35%, transparent); + } +} + +.wp-vol-notes { + font-family : sans-serif; + font-size : 11px; + color : var(--color-body); + line-height : 1.55; + margin-top : 0.5rem; + padding-top : 0.5rem; + border-top : 1px solid var(--color-border); + text-align : left; +} .wp-vol-num { font-family: sans-serif; - font-size : 16px; + font-size : 18px; font-weight: 600; color : var(--color-heading); + line-height: 1.1; +} + +.wp-vol-target { + font-family: sans-serif; + font-size : 10px; + color : var(--color-muted); + margin-top : 2px; + letter-spacing: 0.02em; + + .is-under & { color: var(--color-heading); font-weight: 500; } } .wp-vol-name { font-family: sans-serif; font-size : 11px; color : var(--color-muted); - margin-top : 2px; + margin-top : 4px; } .wp-day { diff --git a/static/CNAME b/static/CNAME index 0d19f50..03ce6cd 100644 --- a/static/CNAME +++ b/static/CNAME @@ -1 +1 @@ -shanemurphy.space +semurphy.com diff --git a/templates/workout-program.html b/templates/workout-program.html index b951592..92d68c7 100644 --- a/templates/workout-program.html +++ b/templates/workout-program.html @@ -3,7 +3,7 @@ {% block title %}{{ page.title }} — {{ config.title }}{% endblock title %} {% block content %} -{% set data = load_data(path="data/workout_program.json") %} +{% set data = load_data(path="data/workout_program.toml") %}
@@ -18,13 +18,32 @@

-
Weekly Volume Summary
+
Weekly Volume vs. Target
- {% for v in data.volume_summary %} -
-
{{ v.sets }}
-
{{ v.group }}
-
+ {% for v in data.volume_targets %} + {% set_global vt_total = 0 %} + {% set_global vt_items = [] %} + {% for day in data.days %} + {% for ex in day.exercises %} + {% if ex.volume and v.slug in ex.volume %} + {% set_global vt_total = vt_total + ex.sets_n %} + {% set_global vt_items = vt_items | concat(with=ex.name ~ " " ~ ex.sets_n) %} + {% endif %} + {% endfor %} + {% endfor %} + {% set status = "ok" %} + {% if vt_total < v.target_min %}{% set status = "under" %}{% endif %} + {% if vt_total > v.target_max %}{% set status = "over" %}{% endif %} +
+ +
{{ vt_total }}
+
+ target {{ v.target_min }}–{{ v.target_max }}{% if status == "under" %} ↓{% elif status == "over" %} ↑{% endif %} +
+
{{ v.group }}
+
+
{{ vt_items | join(sep=" · ") }}
+
{% endfor %}