[OCaml] High Intensity Training Online
feat show each next exercise's best weight and reps on Home
Previously the home preview listed only the upcoming workout's exercise names. Now each line shows the exercise's current best weight × rep combination beside it, the record to beat, following the Heavy Duty progress chart (HD1 rule 11: record the weight and reps for each exercise; even a one-rep increase is significant). The best is the most recent effort — the set you must beat next — drawn from the merged native and imported history, so an imported effort also sets the bar. It is shown as an observed fact, not a target: the set still ends at failure, and the rep band only calibrates load. A pre-exhaust pair shows a line per underlying exercise.
Changed files
lib/presenter/pages_routine.ml
@@ -144,7 +144,20 @@
144
144
tag "ul"
145
145
[ class_ "next-workout-preview" ]
146
146
(List.map
147
Removed:
(fun name -> tag "li" [] [ txt "%s" name ])
147
Added:
(fun (line : View_model.Home.exercise_line) ->
148
Added:
tag "li" []
149
Added:
(tag "span"
150
Added:
[ class_ "next-workout-exercise" ]
151
Added:
[ txt "%s" line.name ]
152
Added:
::
153
Added:
(match line.best with
154
Added:
| Some best ->
155
Added:
[
156
Added:
tag "span"
157
Added:
[ class_ "next-workout-best" ]
158
Added:
[ txt "%s" best ];
159
Added:
]
160
Added:
| None -> [])))
148
161
vm.next_exercises));
149
162
tag "p" [] [ txt "%s" status ];
150
163
]
lib/presenter/view_model.ml
@@ -63,13 +63,25 @@
63
63
(** Whether the next workout may begin, and the recovery status line to
64
64
show while it may not. *)
65
65
66
Added:
type exercise_line = {
67
Added:
name : string; (** The prescribed exercise, phrased. *)
68
Added:
best : string option;
69
Added:
(** The exercise's current best weight × rep combination, the record to
70
Added:
beat, pre-formatted (for example ["60 kg × 12 reps"]). [None] when
71
Added:
the exercise has no earlier observation. An observed fact, not a
72
Added:
target — the set still ends at failure. *)
73
Added:
}
74
Added:
(** One line of the next-workout preview: an exercise and the weight × reps to
75
Added:
beat, per the Heavy Duty progress chart. *)
76
Added:
66
77
type t = {
67
78
routine_id : string; (** For the begin/override forms. *)
68
79
routine_name : string;
69
80
next_workout : string;
70
Removed:
next_exercises : string list;
71
Removed:
(** The prescribed exercises of the upcoming workout, phrased, in order.
72
Removed:
Shown on the card to preview what is coming. *)
81
Added:
next_exercises : exercise_line list;
82
Added:
(** The prescribed exercises of the upcoming workout, in order, each
83
Added:
with its current best weight × reps. Shown on the card to preview
84
Added:
what is coming and the record to beat. *)
73
85
gate : gate;
74
86
}
75
87
(** The overview while no workout is in progress. The recovery policy has
lib/web/assets/hito.css
@@ -375,7 +375,21 @@
375
375
padding-left: 1.1rem;
376
376
color: var(--ink);
377
377
}
378
Removed:
.next-workout-preview li { padding: 0.2rem 0; }
378
Added:
.next-workout-preview li {
379
Added:
display: flex;
380
Added:
align-items: baseline;
381
Added:
gap: 0.6rem;
382
Added:
padding: 0.2rem 0;
383
Added:
}
384
Added:
.next-workout-exercise { flex: 1 1 auto; }
385
Added:
/* The current best weight × reps to beat: an observed fact, monospaced and
386
Added:
quiet, aligned to the right of each exercise. */
387
Added:
.next-workout-best {
388
Added:
flex: 0 0 auto;
389
Added:
font-family: var(--mono);
390
Added:
color: var(--muted-ink);
391
Added:
white-space: nowrap;
392
Added:
}
379
393
380
394
fieldset {
381
395
margin: 1rem 0 0;
lib/web/handlers_overview.ml
@@ -45,14 +45,51 @@
45
45
(Prescription.Routine.workouts r);
46
46
}
47
47
48
Added:
(* The most recent effort for [observations], pre-formatted as the record to
49
Added:
beat. History is oldest first, so the best current combination — the one to
50
Added:
beat next — is the last. An observed fact, not a target. *)
51
Added:
let best_of observations =
52
Added:
match List.rev observations with
53
Added:
| latest :: _ ->
54
Added:
let e = Evidence.Observation.effort latest in
55
Added:
Some
56
Added:
(Printf.sprintf "%g kg × %d reps"
57
Added:
(Evidence.Stimulus.Effort.load e)
58
Added:
(Evidence.Stimulus.Effort.reps e))
59
Added:
| [] -> None
60
Added:
61
Added:
(* The exercises of one prescribed stimulus, in order. A pre-exhaust pair
62
Added:
yields two, one per underlying exercise, so every exercise shows its own
63
Added:
weight × reps to beat. *)
64
Added:
let stimulus_exercises prescription =
65
Added:
match Prescription.Stimulus.delivery prescription with
66
Added:
| Prescription.Stimulus.Single exercise -> [ exercise ]
67
Added:
| Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
68
Added:
[ isolation; compound ]
69
Added:
70
Added:
(* Build the preview lines for the next workout, each with its current best
71
Added:
from the merged native and imported history. *)
72
Added:
let next_exercise_lines t trainee next :
73
Added:
View_model.Home.exercise_line list Lwt.t =
74
Added:
let exercises =
75
Added:
List.concat_map stimulus_exercises (Prescription.Workout.stimuli next)
76
Added:
in
77
Added:
Lwt_list.map_s
78
Added:
(fun exercise ->
79
Added:
Service.exercise_history t.service trainee exercise >|= fun history ->
80
Added:
{
81
Added:
View_model.Home.name = Exercise.name exercise;
82
Added:
best = best_of history;
83
Added:
})
84
Added:
exercises
85
Added:
48
86
let home_view ~(routine : Repository.routine_id) ~routine_name ~next
49
Removed:
~readiness : View_model.Home.t =
87
Added:
~readiness ~next_exercises : View_model.Home.t =
50
88
{
51
89
routine_id = (routine :> string);
52
90
routine_name;
53
91
next_workout = Prescription.Workout.name next;
54
Removed:
next_exercises =
55
Removed:
List.map exercise_string (Prescription.Workout.stimuli next);
92
Added:
next_exercises;
56
93
gate =
57
94
(match readiness with
58
95
| Recovery.Ready -> View_model.Home.Ready
@@ -92,17 +129,20 @@
92
129
>>= fun next ->
93
130
Service.readiness t.service trainee.Trainee.id ~routine
94
131
~now:(t.now ())
95
Removed:
>|= fun readiness ->
132
Added:
>>= fun readiness ->
96
133
match (next, readiness) with
97
134
| Ok next, Ok readiness ->
135
Added:
next_exercise_lines t trainee.Trainee.id next
136
Added:
>|= fun next_exercises ->
98
137
Pages.home request ~viewer
99
138
~home:
100
139
(home_view ~routine
101
140
~routine_name:(Prescription.Routine.name selected)
102
Removed:
~next ~readiness)
141
Added:
~next ~readiness ~next_exercises)
103
142
| Error error, _ | _, Error error ->
104
Removed:
Pages.problem ~title:"Routine unavailable"
105
Removed:
~detail:(Present.routine error)))
143
Added:
Lwt.return
144
Added:
(Pages.problem ~title:"Routine unavailable"
145
Added:
~detail:(Present.routine error))))
106
146
107
147
let home t trainee request = page t trainee request >>= html
108
148
test/web/overview.ml
@@ -49,6 +49,76 @@
49
49
Alcotest.(check bool)
50
50
"an upcoming exercise is shown" true
51
51
(Option.is_some (index_from ~needle:"Incline Presses" page 0)) );
52
Added:
( "the home preview shows each exercise's current best weight and reps",
53
Added:
`Quick,
54
Added:
fun () ->
55
Added:
(* Give the Day 1 exercise Laterals a prior effort through an import,
56
Added:
which feeds the log but never advances the routine cycle. The next
57
Added:
workout stays Day 1, and its preview must show Laterals with the
58
Added:
recorded best — the record to beat, per the Heavy Duty progress
59
Added:
chart. *)
60
Added:
let c, repo = client_with_repo () in
61
Added:
let _ = register c ~username:"lifter" ~password:"heavyduty1" in
62
Added:
let trainee = the_trainee repo ~username:"lifter" in
63
Added:
let token = Option.get (csrf_token (body (get c "/"))) in
64
Added:
let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
65
Added:
let header =
66
Added:
"title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n"
67
Added:
in
68
Added:
let source =
69
Added:
header
70
Added:
^ "Delts,\"01 Jan 2024, 10:00\",\"01 Jan 2024, 11:00\",,Side \
71
Added:
Raise,,,0,failure,14,10,,,\r\n"
72
Added:
in
73
Added:
let token = Option.get (csrf_token (body (get c "/profile"))) in
74
Added:
let _ =
75
Added:
post_multipart c "/profile/import"
76
Added:
~file:(Some ("csv", "workouts.csv", source))
77
Added:
~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ]
78
Added:
in
79
Added:
let batch =
80
Added:
List.hd
81
Added:
(Lwt_main.run
82
Added:
(Memory_repo.import_batches repo trainee.Trainee.id))
83
Added:
in
84
Added:
let bid =
85
Added:
Workout_import.batch_id_to_string
86
Added:
(Workout_import.batch_identity batch)
87
Added:
in
88
Added:
let wid =
89
Added:
Workout_import.workout_id_to_string
90
Added:
(Workout_import.workout_identity
91
Added:
(List.hd (Workout_import.batch_workouts batch)))
92
Added:
in
93
Added:
let review = "/profile/import/" ^ bid in
94
Added:
let t = Option.get (csrf_token (body (get c review))) in
95
Added:
let _ =
96
Added:
post c
97
Added:
(Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid)
98
Added:
[
99
Added:
("dream.csrf", t);
100
Added:
("source_name", "Side Raise");
101
Added:
("exercise_id", "laterals");
102
Added:
]
103
Added:
in
104
Added:
let t = Option.get (csrf_token (body (get c review))) in
105
Added:
let _ =
106
Added:
post c
107
Added:
(Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid)
108
Added:
[ ("dream.csrf", t) ]
109
Added:
in
110
Added:
let page = body (get c "/") in
111
Added:
(* The cycle has not advanced: the next workout is still Day 1. *)
112
Added:
Alcotest.(check bool)
113
Added:
"the Day 1 preview lists Laterals" true
114
Added:
(Option.is_some (index_from ~needle:"Laterals" page 0));
115
Added:
Alcotest.(check bool)
116
Added:
"Laterals shows its best weight and reps" true
117
Added:
(Option.is_some (index_from ~needle:"14 kg × 10 reps" page 0));
118
Added:
Alcotest.(check bool)
119
Added:
"the best is styled as the record to beat" true
120
Added:
(Option.is_some (index_from ~needle:"next-workout-best" page 0))
121
Added:
);
52
122
( "the routine page is served once a routine is selected",
53
123
`Quick,
54
124
fun () ->