[OCaml] High Intensity Training Online
refactor align workout evidence vocabulary
Keep prescriptions, scheduled forecasts, recorded workouts, stimuli, and observations distinct in presenter names. This keeps the plan and record boundary clear. No Heavy Duty training rule changes.
Changed files
lib/presenter/page_kit.ml
@@ -43,3 +43,83 @@
43
43
tag "nav"
44
44
[ class_ "page-tabs"; Dream_html.string_attr "aria-label" "%s" aria_label ]
45
45
(List.map tab_link tabs)
46
Added:
47
Added:
(* Render the common comparison table structure. The caller supplies grouped
48
Added:
bodies, so recorded and planned workouts keep their different values. *)
49
Added:
let workout_evidence_table ?(upcoming = false) ?(caption_class = "sr-only")
50
Added:
~caption bodies =
51
Added:
let classes =
52
Added:
if upcoming then "workout-evidence-table upcoming-workout-table"
53
Added:
else "workout-evidence-table"
54
Added:
in
55
Added:
let column label =
56
Added:
tag "th"
57
Added:
[ Dream_html.string_attr "scope" "col" ]
58
Added:
[ Dream_html.txt "%s" label ]
59
Added:
in
60
Added:
let headers =
61
Added:
[ column "Exercise"; column "Load (kg)" ]
62
Added:
@
63
Added:
if upcoming then [ column "Reps" ]
64
Added:
else [ column "Δ"; column "Reps"; column "Δ" ]
65
Added:
in
66
Added:
tag "table"
67
Added:
[ class_ "%s" classes ]
68
Added:
([
69
Added:
tag "caption" [ class_ "%s" caption_class ] caption;
70
Added:
tag "thead" [] [ tag "tr" [] headers ];
71
Added:
]
72
Added:
@ bodies)
73
Added:
74
Added:
(* Render a prescribed workout before it has a log. Both Home and Logbook use
75
Added:
this component, so the same upcoming workout shows the same evidence. *)
76
Added:
let upcoming_workout_card (planned : View_model.Upcoming_workout.t) =
77
Added:
let format_load load = Printf.sprintf "%g" load in
78
Added:
let format_reps reps = string_of_int reps in
79
Added:
let rows_of_group (group : View_model.Upcoming_workout.group) =
80
Added:
List.map
81
Added:
(fun (exercise : View_model.Upcoming_workout.exercise) ->
82
Added:
tag "tr" []
83
Added:
[
84
Added:
tag "th"
85
Added:
[
86
Added:
Dream_html.string_attr "scope" "row"; class_ "evidence-exercise";
87
Added:
]
88
Added:
[ Dream_html.txt "%s" exercise.name ];
89
Added:
tag "td"
90
Added:
[ class_ "observation-latest" ]
91
Added:
[
92
Added:
Dream_html.txt "%s"
93
Added:
(Option.fold ~none:"—" ~some:format_load exercise.latest_load);
94
Added:
];
95
Added:
tag "td"
96
Added:
[ class_ "observation-latest" ]
97
Added:
[
98
Added:
Dream_html.txt "%s"
99
Added:
(Option.fold ~none:"—" ~some:format_reps exercise.latest_reps);
100
Added:
];
101
Added:
])
102
Added:
group.exercises
103
Added:
in
104
Added:
let caption =
105
Added:
[
106
Added:
tag "span"
107
Added:
[ class_ "workout-evidence-caption-name" ]
108
Added:
[ tag "strong" [] [ Dream_html.txt "%s" planned.name ] ];
109
Added:
tag "span"
110
Added:
[ class_ "workout-evidence-caption-status upcoming" ]
111
Added:
[ Dream_html.txt "Planned on %s" planned.scheduled_on ];
112
Added:
]
113
Added:
in
114
Added:
tag "article"
115
Added:
[ class_ "selected-upcoming-workout" ]
116
Added:
[
117
Added:
workout_evidence_table ~upcoming:true
118
Added:
~caption_class:"upcoming-workout-caption" ~caption
119
Added:
(List.map
120
Added:
(fun group -> tag "tbody" [] (rows_of_group group))
121
Added:
planned.groups);
122
Added:
tag "p"
123
Added:
[ class_ "latest-observation-note" ]
124
Added:
[ Dream_html.txt "Latest performance is the record to beat." ];
125
Added:
]
lib/presenter/pages.mli
@@ -62,10 +62,10 @@
62
62
?workout_started:int ->
63
63
viewer:View_model.Viewer.t ->
64
64
?calendar:View_model.Calendar.t ->
65
Removed:
?selected_day:View_model.Selected_day.t ->
65
Added:
?selected_date:View_model.Selected_date.t ->
66
66
?suggest_feedback:bool ->
67
67
?feedback_open:bool ->
68
Removed:
?delete_record:View_model.Logbook_entry.t ->
68
Added:
?delete_record:View_model.Recorded_workout.t ->
69
69
?tab:string ->
70
70
?progress_charts:View_model.Progress_page.chart list ->
71
71
unit ->
lib/presenter/pages_logbook.ml
@@ -84,7 +84,7 @@
84
84
[ tag "h2" [] [ txt "How are you feeling?" ]; feedback_form request ];
85
85
]
86
86
87
Removed:
let delete_record_dialog request (record : View_model.Logbook_entry.t) =
87
Added:
let delete_record_dialog request (record : View_model.Recorded_workout.t) =
88
88
tag "dialog"
89
89
[ class_ "confirm-dialog"; Dream_html.attr "open" ]
90
90
[
@@ -134,10 +134,10 @@
134
134
let delta_value ~previous ~current =
135
135
let delta = current -. previous in
136
136
let class_name, value =
137
Removed:
if Float.equal delta 0. then ("performance-delta", "—")
137
Added:
if Float.equal delta 0. then ("comparison-delta", "—")
138
138
else if delta > 0. then
139
Removed:
("performance-delta delta-up", Printf.sprintf "%+g" delta)
140
Removed:
else ("performance-delta delta-down", Printf.sprintf "%+g" delta)
139
Added:
("comparison-delta delta-up", Printf.sprintf "%+g" delta)
140
Added:
else ("comparison-delta delta-down", Printf.sprintf "%+g" delta)
141
141
in
142
142
(class_name, value)
143
143
@@ -148,21 +148,21 @@
148
148
let class_name, value = delta_value ~previous ~current in
149
149
tag "td" [ class_ "%s" class_name ] [ txt "%s" value ]
150
150
151
Removed:
let comparison_rows (row : View_model.Logbook_row.t) =
151
Added:
let comparison_rows (row : View_model.Exercise_comparison.t) =
152
152
let exercise_cell =
153
153
tag "th"
154
Removed:
[ Dream_html.string_attr "scope" "row"; class_ "performance-exercise" ]
154
Added:
[ Dream_html.string_attr "scope" "row"; class_ "evidence-exercise" ]
155
155
[ txt "%s" row.exercise_name ]
156
156
in
157
157
[
158
158
tag "tr" []
159
159
[
160
160
exercise_cell;
161
Removed:
value_cell "performance-current" (format_load row.current_load);
161
Added:
value_cell "observation-current" (format_load row.current_load);
162
162
delta_cell
163
163
~previous:(Option.value ~default:row.current_load row.previous_load)
164
164
~current:row.current_load;
165
Removed:
value_cell "performance-current" (format_reps row.current_reps);
165
Added:
value_cell "observation-current" (format_reps row.current_reps);
166
166
delta_cell
167
167
~previous:
168
168
(float_of_int
@@ -171,84 +171,25 @@
171
171
];
172
172
]
173
173
174
Removed:
let performance_table groups =
175
Removed:
tag "table"
176
Removed:
[ class_ "logbook-performance-table" ]
177
Removed:
([
178
Removed:
tag "caption" [ class_ "sr-only" ] [ txt "Workout performance" ];
179
Removed:
tag "thead" []
180
Removed:
[
181
Removed:
tag "tr" []
182
Removed:
[
183
Removed:
tag "th"
184
Removed:
[ Dream_html.string_attr "scope" "col" ]
185
Removed:
[ txt "Exercise" ];
186
Removed:
tag "th"
187
Removed:
[ Dream_html.string_attr "scope" "col" ]
188
Removed:
[ txt "Load (kg)" ];
189
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Δ" ];
190
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Reps" ];
191
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Δ" ];
192
Removed:
];
193
Removed:
];
194
Removed:
]
195
Removed:
@ List.map
196
Removed:
(fun (group : View_model.Logbook_group.t) ->
197
Removed:
tag "tbody"
198
Removed:
[ class_ "logbook-stimulus-group" ]
199
Removed:
(List.concat_map comparison_rows group.rows))
200
Removed:
groups)
201
Removed:
202
Removed:
let planned_table (groups : View_model.Selected_day.Planned.group list) =
203
Removed:
let rows_of_group (group : View_model.Selected_day.Planned.group) =
204
Removed:
List.map
205
Removed:
(fun (exercise : View_model.Selected_day.Planned.exercise) ->
206
Removed:
tag "tr" []
207
Removed:
[
208
Removed:
tag "th"
209
Removed:
[
210
Removed:
Dream_html.string_attr "scope" "row";
211
Removed:
class_ "performance-exercise";
212
Removed:
]
213
Removed:
[ txt "%s" exercise.name ];
214
Removed:
tag "td"
215
Removed:
[ class_ "performance-previous" ]
216
Removed:
[
217
Removed:
txt "%s"
218
Removed:
(Option.fold ~none:"—" ~some:format_load exercise.latest_load);
219
Removed:
];
220
Removed:
tag "td" [ class_ "performance-previous" ] [ txt "—" ];
221
Removed:
tag "td"
222
Removed:
[ class_ "performance-previous" ]
223
Removed:
[
224
Removed:
txt "%s"
225
Removed:
(Option.fold ~none:"—" ~some:format_reps exercise.latest_reps);
226
Removed:
];
227
Removed:
tag "td" [ class_ "performance-previous" ] [ txt "—" ];
228
Removed:
])
229
Removed:
group.exercises
174
Added:
let workout_evidence_table (entry : View_model.Recorded_workout.t) groups =
175
Added:
let caption =
176
Added:
[
177
Added:
tag "span"
178
Added:
[ class_ "workout-evidence-caption-name" ]
179
Added:
[ tag "strong" [] [ txt "%s" entry.workout_name ] ];
180
Added:
tag "span"
181
Added:
[ class_ "workout-evidence-caption-status" ]
182
Added:
[ txt "Performed on %s" entry.performed_on ];
183
Added:
]
230
184
in
231
Removed:
tag "table"
232
Removed:
[ class_ "logbook-performance-table logbook-planned-table" ]
233
Removed:
([
234
Removed:
tag "caption" [ class_ "sr-only" ] [ txt "Latest performance" ];
235
Removed:
tag "thead" []
236
Removed:
[
237
Removed:
tag "tr" []
238
Removed:
[
239
Removed:
tag "th"
240
Removed:
[ Dream_html.string_attr "scope" "col" ]
241
Removed:
[ txt "Exercise" ];
242
Removed:
tag "th"
243
Removed:
[ Dream_html.string_attr "scope" "col" ]
244
Removed:
[ txt "Load (kg)" ];
245
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Δ" ];
246
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Reps" ];
247
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [ txt "Δ" ];
248
Removed:
];
249
Removed:
];
250
Removed:
]
251
Removed:
@ List.map (fun group -> tag "tbody" [] (rows_of_group group)) groups)
185
Added:
Page_kit.workout_evidence_table ~caption_class:"workout-evidence-caption"
186
Added:
~caption
187
Added:
(List.map
188
Added:
(fun (group : View_model.Stimulus_group.t) ->
189
Added:
tag "tbody"
190
Added:
[ class_ "logbook-stimulus-group" ]
191
Added:
(List.concat_map comparison_rows group.rows))
192
Added:
groups)
252
193
253
194
let calendar_month_name month =
254
195
List.nth
@@ -268,7 +209,7 @@
268
209
]
269
210
(month - 1)
270
211
271
Removed:
let workout_metadata (entry : View_model.Logbook_entry.t) =
212
Added:
let workout_metadata (entry : View_model.Recorded_workout.t) =
272
213
let duration_item =
273
214
match
274
215
duration_text ~seconds:entry.duration_seconds
@@ -283,11 +224,11 @@
283
224
(Option.map
284
225
(fun next ->
285
226
[ tag "dt" [] [ txt "Next planned" ]; tag "dd" [] [ txt "%s" next ] ])
286
Removed:
entry.next_planned)
227
Added:
entry.next_scheduled)
287
228
|> List.concat
288
229
in
289
230
tag "dl"
290
Removed:
[ class_ "performance-metadata" ]
231
Added:
[ class_ "workout-metadata" ]
291
232
(duration_item
292
233
@ [
293
234
tag "dt" [] [ txt "Performed" ];
@@ -295,13 +236,13 @@
295
236
]
296
237
@ next_item)
297
238
298
Removed:
let selected_workout_card (entry : View_model.Logbook_entry.t) =
239
Added:
let selected_workout_card (entry : View_model.Recorded_workout.t) =
299
240
let performance =
300
241
if entry.groups = [] then
301
242
[
302
243
tag "p" [ class_ "logbook-no-progress" ] [ txt "No exercises recorded" ];
303
244
]
304
Removed:
else [ performance_table entry.groups ]
245
Added:
else [ workout_evidence_table entry entry.groups ]
305
246
in
306
247
let actions =
307
248
if entry.imported then []
@@ -326,48 +267,32 @@
326
267
];
327
268
]
328
269
in
329
Removed:
let header =
330
Removed:
tag "header"
331
Removed:
[ class_ "selected-workout-header" ]
332
Removed:
([ tag "h3" [] [ txt "%s" entry.workout_name ] ]
333
Removed:
@
334
Removed:
if entry.imported then
335
Removed:
[ tag "span" [ class_ "logbook-import-pill" ] [ txt "Imported" ] ]
336
Removed:
else [])
270
Added:
let imported =
271
Added:
if entry.imported then
272
Added:
[ tag "span" [ class_ "logbook-import-pill" ] [ txt "Imported" ] ]
273
Added:
else []
337
274
in
338
275
tag "article"
339
276
[ class_ "selected-workout" ]
340
Removed:
[
341
Removed:
header;
342
Removed:
workout_metadata entry;
343
Removed:
tag "div" [ class_ "selected-workout-panel" ] (performance @ actions);
344
Removed:
]
277
Added:
(imported
278
Added:
@ [
279
Added:
workout_metadata entry;
280
Added:
tag "div" [ class_ "selected-workout-panel" ] (performance @ actions);
281
Added:
])
345
282
346
Removed:
let selected_planned_card (planned : View_model.Selected_day.Planned.t) =
347
Removed:
tag "article"
348
Removed:
[ class_ "selected-planned-workout" ]
349
Removed:
[
350
Removed:
tag "h3" [] [ txt "Planned: %s" planned.name ];
351
Removed:
tag "p" [] [ txt "This workout follows your routine recovery." ];
352
Removed:
planned_table planned.groups;
353
Removed:
tag "p"
354
Removed:
[ class_ "planned-record-note" ]
355
Removed:
[ txt "Latest performance is the record to beat." ];
356
Removed:
]
283
Added:
let selected_upcoming_card = upcoming_workout_card
357
284
358
Removed:
let selected_day_panel (selected : View_model.Selected_day.t) =
285
Added:
let selected_date_panel (selected : View_model.Selected_date.t) =
359
286
let completed = List.map selected_workout_card selected.completed in
360
287
let planned =
361
Removed:
Option.to_list (Option.map selected_planned_card selected.planned)
288
Added:
Option.to_list (Option.map selected_upcoming_card selected.upcoming)
362
289
in
363
290
let content =
364
291
if completed = [] && planned = [] then
365
292
[ tag "p" [] [ txt "No workout was recorded or planned on this day." ] ]
366
293
else completed @ planned
367
294
in
368
Removed:
tag "section"
369
Removed:
[ class_ "selected-day" ]
370
Removed:
(tag "h2" [] [ txt "%s" selected.date ] :: content)
295
Added:
tag "section" [ class_ "selected-day" ] content
371
296
372
297
(* The month calendar above the selected-day panel. Days that carry a workout
373
298
are highlighted; today is marked. A header row holds a previous arrow, the
@@ -379,13 +304,13 @@
379
304
let classes =
380
305
"calendar-day"
381
306
^ (if d.workout then " calendar-workout" else "")
382
Removed:
^ (if Option.is_some d.planned_workout then " calendar-planned"
307
Added:
^ (if Option.is_some d.scheduled_workout then " calendar-planned"
383
308
else "")
384
309
^ (if d.selected then " calendar-selected" else "")
385
310
^ if d.today then " calendar-today" else ""
386
311
in
387
312
let aria_label =
388
Removed:
match (d.workout, d.planned_workout, d.today) with
313
Added:
match (d.workout, d.scheduled_workout, d.today) with
389
314
| true, Some name, true ->
390
315
Some
391
316
(Printf.sprintf "%d: completed, planned %s, today"
@@ -409,7 +334,7 @@
409
334
let title =
410
335
Option.map
411
336
(fun name -> Printf.sprintf "Planned: %s" name)
412
Removed:
d.planned_workout
337
Added:
d.scheduled_workout
413
338
in
414
339
let href =
415
340
Printf.sprintf "/logbook?year=%d&month=%d&day=%d" cal.selected_year
@@ -572,7 +497,7 @@
572
497
]
573
498
| charts -> List.map progress_chart_section charts
574
499
575
Removed:
let logbook request ?workout_started ~viewer ?calendar ?selected_day
500
Added:
let logbook request ?workout_started ~viewer ?calendar ?selected_date
576
501
?(suggest_feedback = false) ?(feedback_open = false) ?delete_record
577
502
?(tab = "calendar") ?progress_charts () =
578
503
let active_tab =
@@ -583,8 +508,8 @@
583
508
| "calendar" ->
584
509
(match calendar with Some cal -> [ calendar_grid cal ] | None -> [])
585
510
@ [
586
Removed:
(match selected_day with
587
Removed:
| Some selected -> selected_day_panel selected
511
Added:
(match selected_date with
512
Added:
| Some selected -> selected_date_panel selected
588
513
| None ->
589
514
tag "section"
590
515
[ class_ "selected-day empty-selected-day" ]
lib/presenter/pages_routine.ml
@@ -167,27 +167,7 @@
167
167
html_page ~viewer ~request ~active:"home" "Home"
168
168
([
169
169
tag "h1" [] [ txt "%s" vm.routine_name ];
170
Removed:
tag "p" [] [ txt "Next: %s" vm.next_workout ];
171
Removed:
(if vm.next_exercises = [] then txt ""
172
Removed:
else
173
Removed:
tag "ul"
174
Removed:
[ class_ "next-workout-preview" ]
175
Removed:
(List.map
176
Removed:
(fun (line : View_model.Home.exercise_line) ->
177
Removed:
tag "li" []
178
Removed:
(tag "span"
179
Removed:
[ class_ "next-workout-exercise" ]
180
Removed:
[ txt "%s" line.name ]
181
Removed:
::
182
Removed:
(match line.best with
183
Removed:
| Some best ->
184
Removed:
[
185
Removed:
tag "span"
186
Removed:
[ class_ "next-workout-best" ]
187
Removed:
[ txt "%s" best ];
188
Removed:
]
189
Removed:
| None -> [])))
190
Removed:
vm.next_exercises));
170
Added:
upcoming_workout_card vm.next_workout;
191
171
tag "p" [] [ txt "%s" status ];
192
172
]
193
173
@ gate
lib/presenter/view_model.ml
@@ -56,6 +56,20 @@
56
56
name, and how many workouts the cycle holds. *)
57
57
end
58
58
59
Added:
module Upcoming_workout = struct
60
Added:
type exercise = {
61
Added:
name : string;
62
Added:
latest_load : float option;
63
Added:
latest_reps : int option;
64
Added:
}
65
Added:
66
Added:
type group = { exercises : exercise list }
67
Added:
68
Added:
type t = { name : string; scheduled_on : string; groups : group list }
69
Added:
(** A prescribed workout that has no logging record yet. Latest performance is
70
Added:
evidence for the next attempt, not a repetition target. *)
71
Added:
end
72
Added:
59
73
module Home = struct
60
74
type gate =
61
75
| Ready
@@ -63,25 +77,10 @@
63
77
(** Whether the next workout may begin, and the recovery status line to
64
78
show while it may not. *)
65
79
66
Removed:
type exercise_line = {
67
Removed:
name : string; (** The prescribed exercise, phrased. *)
68
Removed:
best : string option;
69
Removed:
(** The exercise's current best weight × rep combination, the record to
70
Removed:
beat, pre-formatted (for example ["60 kg × 12 reps"]). [None] when
71
Removed:
the exercise has no earlier observation. An observed fact, not a
72
Removed:
target — the set still ends at failure. *)
73
Removed:
}
74
Removed:
(** One line of the next-workout preview: an exercise and the weight × reps to
75
Removed:
beat, per the Heavy Duty progress chart. *)
76
Removed:
77
80
type t = {
78
81
routine_id : string; (** For the begin/override forms. *)
79
82
routine_name : string;
80
Removed:
next_workout : string;
81
Removed:
next_exercises : exercise_line list;
82
Removed:
(** The prescribed exercises of the upcoming workout, in order, each
83
Removed:
with its current best weight × reps. Shown on the card to preview
84
Removed:
what is coming and the record to beat. *)
83
Added:
next_workout : Upcoming_workout.t;
85
84
gate : gate;
86
85
}
87
86
(** The overview while no workout is in progress. The recovery policy has
@@ -145,7 +144,7 @@
145
144
form, since its option set is fixed UI vocabulary, not domain data. *)
146
145
end
147
146
148
Removed:
module Logbook_row = struct
147
Added:
module Exercise_comparison = struct
149
148
type t = {
150
149
exercise_name : string;
151
150
current_load : float;
@@ -156,12 +155,12 @@
156
155
(** One observed exercise effort and its optional previous performance. *)
157
156
end
158
157
159
Removed:
module Logbook_group = struct
160
Removed:
type t = { rows : Logbook_row.t list }
158
Added:
module Stimulus_group = struct
159
Added:
type t = { rows : Exercise_comparison.t list }
161
160
(** The exercises delivered by one stimulus, in performance order. *)
162
161
end
163
162
164
Removed:
module Logbook_entry = struct
163
Added:
module Recorded_workout = struct
165
164
type t = {
166
165
id : string; (** Record id for edit and delete links. *)
167
166
imported : bool;
@@ -172,6 +171,7 @@
172
171
at_unix : int;
173
172
(** Start time in Unix seconds, for ordering and selection. *)
174
173
workout_name : string;
174
Added:
performed_on : string;
175
175
duration_previous_seconds : int option;
176
176
(** The most recent earlier workout with the same prescription name,
177
177
strictly before this one, in seconds, when both finished. [None] for
@@ -181,10 +181,10 @@
181
181
previous duration so the presenter can derive a signed delta. *)
182
182
performed_ago : string;
183
183
(** Time since the workout started, already phrased for the view. *)
184
Removed:
next_planned : string option;
184
Added:
next_scheduled : string option;
185
185
(** Time until the next planned occurrence of this workout, already
186
186
phrased for the view. *)
187
Removed:
groups : Logbook_group.t list;
187
Added:
groups : Stimulus_group.t list;
188
188
(** Exercise rows grouped by the stimulus that delivered them. *)
189
189
}
190
190
(** One recorded workout for the selected-day detail panel. It carries the
@@ -192,22 +192,10 @@
192
192
*)
193
193
end
194
194
195
Removed:
module Selected_day = struct
196
Removed:
module Planned = struct
197
Removed:
type exercise = {
198
Removed:
name : string;
199
Removed:
latest_load : float option;
200
Removed:
latest_reps : int option;
201
Removed:
}
202
Removed:
203
Removed:
type group = { exercises : exercise list }
204
Removed:
type t = { name : string; groups : group list }
205
Removed:
end
206
Removed:
195
Added:
module Selected_date = struct
207
196
type t = {
208
Removed:
date : string;
209
Removed:
completed : Logbook_entry.t list;
210
Removed:
planned : Planned.t option;
197
Added:
completed : Recorded_workout.t list;
198
Added:
upcoming : Upcoming_workout.t option;
211
199
}
212
200
(** The selected calendar day: recorded workouts and the planned workout, if
213
201
the routine schedules one. *)
@@ -217,11 +205,11 @@
217
205
type day = {
218
206
day_of_month : int;
219
207
workout : bool;
220
Removed:
planned_workout : string option;
208
Added:
scheduled_workout : string option;
221
209
today : bool;
222
210
selected : bool;
223
211
}
224
Removed:
(** One dated cell. [workout] marks a completed workout. [planned_workout]
212
Added:
(** One dated cell. [workout] marks a completed workout. [scheduled_workout]
225
213
names the next workout planned for that day, when one exists. *)
226
214
227
215
type cell =
lib/web/assets/hito.css
@@ -559,74 +559,92 @@
559
559
560
560
/* The Logbook tables align current, previous, and change rows. Each tbody is
561
561
one delivered stimulus, so a pre-exhaust pair remains a visible group. */
562
Removed:
.logbook-performance-table {
562
Added:
.workout-evidence-table {
563
563
width: 100%;
564
564
margin: 0;
565
565
border-collapse: collapse;
566
566
font-family: var(--mono);
567
567
}
568
Removed:
.logbook-performance-table thead {
568
Added:
.upcoming-workout-caption,
569
Added:
.workout-evidence-caption {
570
Added:
margin-bottom: 0.45rem;
569
571
color: var(--ink);
572
Added:
font: 400 var(--font-size-lead)/1.35 var(--sans);
573
Added:
letter-spacing: -0.03em;
574
Added:
text-align: left;
575
Added:
}
576
Added:
.workout-evidence-caption-name,
577
Added:
.workout-evidence-caption-status {
578
Added:
display: block;
579
Added:
}
580
Added:
.workout-evidence-caption-name {
581
Added:
font-weight: 700;
582
Added:
}
583
Added:
.workout-evidence-caption-status.upcoming {
584
Added:
font-style: italic;
585
Added:
}
586
Added:
.workout-evidence-table thead {
587
Added:
color: var(--ink);
570
588
font-family: var(--sans);
571
589
font-size: inherit;
572
590
}
573
Removed:
.logbook-performance-table thead th {
591
Added:
.workout-evidence-table thead th {
574
592
font-weight: 700;
575
593
}
576
Removed:
.logbook-performance-table th,
577
Removed:
.logbook-performance-table td {
594
Added:
.workout-evidence-table th,
595
Added:
.workout-evidence-table td {
578
596
padding: 0.45rem 0.35rem;
579
597
text-align: right;
580
598
vertical-align: middle;
581
599
}
582
Removed:
.logbook-performance-table th:first-child {
600
Added:
.workout-evidence-table th:first-child {
583
601
text-align: left;
584
602
}
585
Removed:
.logbook-performance-table tbody + tbody {
603
Added:
.workout-evidence-table tbody + tbody {
586
604
border-top: 1px solid var(--rule-strong);
587
605
}
588
Removed:
.logbook-planned-table tbody {
606
Added:
.upcoming-workout-table tbody {
589
607
border-top: 1px solid var(--rule-strong);
590
608
}
591
Removed:
.logbook-performance-table .performance-exercise {
609
Added:
.workout-evidence-table .evidence-exercise {
592
610
width: 40%;
593
611
color: var(--ink);
594
612
font-family: var(--sans);
595
613
font-weight: 400;
596
614
text-align: left;
597
615
}
598
Removed:
.logbook-performance-table .performance-current,
599
Removed:
.logbook-performance-table .performance-previous {
616
Added:
.workout-evidence-table .observation-current,
617
Added:
.workout-evidence-table .observation-previous {
600
618
font-family: var(--mono);
601
619
font-weight: 400;
602
620
}
603
Removed:
.logbook-performance-table .performance-current {
621
Added:
.workout-evidence-table .observation-current {
604
622
color: var(--ink);
605
623
}
606
Removed:
.logbook-performance-table .performance-previous {
624
Added:
.workout-evidence-table .observation-previous {
607
625
color: var(--muted-ink);
608
626
}
609
Removed:
.performance-delta {
627
Added:
.comparison-delta {
610
628
font-weight: 700;
611
629
}
612
Removed:
.performance-delta.delta-up { color: var(--evergreen); }
613
Removed:
.performance-delta.delta-down { color: var(--blood-dark); }
614
Removed:
.logbook-planned-table .performance-previous {
630
Added:
.comparison-delta.delta-up { color: var(--evergreen); }
631
Added:
.comparison-delta.delta-down { color: var(--blood-dark); }
632
Added:
.upcoming-workout-table .observation-latest {
615
633
color: var(--ink);
616
634
font-style: italic;
617
635
}
618
Removed:
.performance-metadata {
636
Added:
.workout-metadata {
619
637
display: grid;
620
638
grid-template-columns: auto 1fr;
621
639
gap: 0.25rem 0.75rem;
622
640
margin: 0.65rem 0 0;
623
641
}
624
Removed:
.performance-metadata dt {
642
Added:
.workout-metadata dt {
625
643
color: var(--ink);
626
644
font-family: var(--sans);
627
645
font-weight: 700;
628
646
}
629
Removed:
.performance-metadata dd {
647
Added:
.workout-metadata dd {
630
648
margin: 0;
631
649
color: var(--ink);
632
650
font-family: var(--mono);
@@ -779,12 +797,8 @@
779
797
padding: 1rem 0;
780
798
border-top: 1px solid var(--rule-strong);
781
799
}
782
Removed:
.selected-day > h2 {
783
Removed:
margin: 0 0 0.75rem;
784
Removed:
font-size: var(--font-size-lead);
785
Removed:
}
786
800
.selected-workout,
787
Removed:
.selected-planned-workout {
801
Added:
.selected-upcoming-workout {
788
802
margin-top: 0.75rem;
789
803
padding: 0.85rem 0;
790
804
border-bottom: 1px solid var(--rule);
@@ -795,7 +809,7 @@
795
809
gap: 0.75rem;
796
810
}
797
811
.selected-workout-header h3,
798
Removed:
.selected-planned-workout h3 {
812
Added:
.selected-upcoming-workout h3 {
799
813
margin: 0;
800
814
font-size: var(--font-size-lead);
801
815
}
lib/web/handlers_logbook.ml
@@ -34,7 +34,7 @@
34
34
35
35
type planned_group = { exercises : Exercise.t list }
36
36
37
Removed:
type planned_workout = {
37
Added:
type scheduled_workout = {
38
38
at_unix : int;
39
39
workout_name : string;
40
40
groups : planned_group list;
@@ -64,7 +64,7 @@
64
64
(* Predict only the next routine positions after the latest finished native
65
65
workout. A late workout stays the next workout rather than advancing the
66
66
routine silently. *)
67
Removed:
let planned_workouts ~now ~log ~routine =
67
Added:
let scheduled_workouts ~now ~log ~routine =
68
68
match latest_finished_workout log with
69
69
| None -> []
70
70
| Some (last, ended_at) ->
@@ -95,9 +95,9 @@
95
95
96
96
(* Build the calendar for [year]/[month], highlighting completed and planned
97
97
days. [today] marks the current day when it falls in the shown month. *)
98
Removed:
let calendar_view ~now ~year ~month ~selected_day
99
Removed:
~(planned : planned_workout list)
100
Removed:
(entries : View_model.Logbook_entry.t list) : View_model.Calendar.t =
98
Added:
let calendar_view ~now ~year ~month ~selected_date
99
Added:
~(scheduled : scheduled_workout list)
100
Added:
(entries : View_model.Recorded_workout.t list) : View_model.Calendar.t =
101
101
let now_tm = Unix.gmtime (float_of_int now) in
102
102
let this_year = now_tm.Unix.tm_year + 1900 in
103
103
let today =
@@ -107,7 +107,7 @@
107
107
in
108
108
let workout_days =
109
109
List.filter_map
110
Removed:
(fun (e : View_model.Logbook_entry.t) ->
110
Added:
(fun (e : View_model.Recorded_workout.t) ->
111
111
let tm = Unix.gmtime (float_of_int e.at_unix) in
112
112
if tm.Unix.tm_year + 1900 = year && tm.Unix.tm_mon + 1 = month then
113
113
Some tm.Unix.tm_mday
@@ -117,7 +117,7 @@
117
117
let has_workout d = List.mem d workout_days in
118
118
let planned_name d =
119
119
List.find_map
120
Removed:
(fun (p : planned_workout) ->
120
Added:
(fun (p : scheduled_workout) ->
121
121
let tm = Unix.gmtime (float_of_int p.at_unix) in
122
122
if
123
123
tm.Unix.tm_year + 1900 = year
@@ -125,7 +125,7 @@
125
125
&& tm.Unix.tm_mday = d
126
126
then Some p.workout_name
127
127
else None)
128
Removed:
planned
128
Added:
scheduled
129
129
in
130
130
let total = Calendar.days_in_month ~year ~month in
131
131
let lead = Calendar.monday_first_weekday ~year ~month ~day:1 in
@@ -137,9 +137,9 @@
137
137
{
138
138
day_of_month = d;
139
139
workout = has_workout d;
140
Removed:
planned_workout = planned_name d;
140
Added:
scheduled_workout = planned_name d;
141
141
today = d = today;
142
Removed:
selected = Some d = selected_day;
142
Added:
selected = Some d = selected_date;
143
143
})
144
144
in
145
145
let trailing = (7 - (List.length cells mod 7)) mod 7 in
@@ -187,7 +187,8 @@
187
187
|> Option.join
188
188
|> Option.map Recovery.duration_to_seconds
189
189
190
Removed:
let entry_view log (record : Repository.record) : View_model.Logbook_entry.t =
190
Added:
let entry_view log (record : Repository.record) :
191
Added:
View_model.Recorded_workout.t =
191
192
let workout = record.Repository.workout in
192
193
let started =
193
194
Recovery.timestamp_to_unix_seconds (Evidence.Workout.started_at workout)
@@ -203,7 +204,8 @@
203
204
match comparison_for exercise comparisons with
204
205
| None ->
205
206
{
206
Removed:
View_model.Logbook_row.exercise_name = Exercise.name exercise;
207
Added:
View_model.Exercise_comparison.exercise_name =
208
Added:
Exercise.name exercise;
207
209
current_load = Evidence.Stimulus.Effort.load effort;
208
210
current_reps = Evidence.Stimulus.Effort.reps effort;
209
211
previous_load = None;
@@ -211,7 +213,8 @@
211
213
}
212
214
| Some comparison ->
213
215
{
214
Removed:
View_model.Logbook_row.exercise_name = Exercise.name exercise;
216
Added:
View_model.Exercise_comparison.exercise_name =
217
Added:
Exercise.name exercise;
215
218
current_load = Evidence.Stimulus.Effort.load effort;
216
219
current_reps = Evidence.Stimulus.Effort.reps effort;
217
220
previous_load =
@@ -225,7 +228,7 @@
225
228
Evidence.Workout.performed workout
226
229
|> List.map (fun (_slot, stimulus) ->
227
230
{
228
Removed:
View_model.Logbook_group.rows =
231
Added:
View_model.Stimulus_group.rows =
229
232
List.map
230
233
(row_of_effort comparisons)
231
234
(Evidence.Stimulus.efforts stimulus);
@@ -240,12 +243,13 @@
240
243
at_unix = started;
241
244
workout_name =
242
245
Prescription.Workout.name (Evidence.Workout.prescription workout);
246
Added:
performed_on = utc_long_date started;
243
247
duration_previous_seconds;
244
248
duration_seconds =
245
249
Option.map Recovery.duration_to_seconds
246
250
(Evidence.Workout.duration workout);
247
251
performed_ago = "";
248
Removed:
next_planned = None;
252
Added:
next_scheduled = None;
249
253
groups;
250
254
}
251
255
@@ -254,7 +258,7 @@
254
258
imported history the service supplies. The service fetches history once per
255
259
distinct exercise in the record. *)
256
260
let imported_entry_view t trainee (record : Repository.imported_record) :
257
Removed:
View_model.Logbook_entry.t Lwt.t =
261
Added:
View_model.Recorded_workout.t Lwt.t =
258
262
let started =
259
263
Recovery.timestamp_to_unix_seconds record.Repository.started_at
260
264
in
@@ -289,7 +293,7 @@
289
293
Option.map Evidence.Observation.effort previous
290
294
in
291
295
{
292
Removed:
View_model.Logbook_row.exercise_name =
296
Added:
View_model.Exercise_comparison.exercise_name =
293
297
Exercise.name (Evidence.Observation.exercise o);
294
298
current_load = Evidence.Stimulus.Effort.load effort;
295
299
current_reps = Evidence.Stimulus.Effort.reps effort;
@@ -305,13 +309,14 @@
305
309
imported = true;
306
310
at_unix = started;
307
311
workout_name = record.Repository.title;
312
Added:
performed_on = utc_long_date started;
308
313
duration_previous_seconds = None;
309
314
duration_seconds = Some (ended - started);
310
315
performed_ago = "";
311
Removed:
next_planned = None;
312
Removed:
groups = [ { View_model.Logbook_group.rows } ];
316
Added:
next_scheduled = None;
317
Added:
groups = [ { View_model.Stimulus_group.rows } ];
313
318
}
314
Removed:
: View_model.Logbook_entry.t)
319
Added:
: View_model.Recorded_workout.t)
315
320
316
321
let index t trainee request =
317
322
let viewer = viewer_of trainee in
@@ -336,29 +341,29 @@
336
341
let recorded_entries =
337
342
List.map (entry_view log) records @ imported_entries
338
343
|> List.stable_sort (fun a b ->
339
Removed:
Int.compare b.View_model.Logbook_entry.at_unix
340
Removed:
a.View_model.Logbook_entry.at_unix)
344
Added:
Int.compare b.View_model.Recorded_workout.at_unix
345
Added:
a.View_model.Recorded_workout.at_unix)
341
346
in
342
347
let now = Recovery.timestamp_to_unix_seconds (t.now ()) in
343
348
let now_tm = Unix.gmtime (float_of_int now) in
344
349
let clamp lo hi v = max lo (min hi v) in
345
350
let year = query_int "year" (now_tm.Unix.tm_year + 1900) in
346
351
let month = clamp 1 12 (query_int "month" (now_tm.Unix.tm_mon + 1)) in
347
Removed:
let selected_day_number =
352
Added:
let selected_date_number =
348
353
match query_int "day" 0 with
349
354
| day when day >= 1 && day <= Calendar.days_in_month ~year ~month ->
350
355
Some day
351
356
| _ -> None
352
357
in
353
Removed:
let same_day (e : View_model.Logbook_entry.t) day =
358
Added:
let same_day (e : View_model.Recorded_workout.t) day =
354
359
let tm = Unix.gmtime (float_of_int e.at_unix) in
355
360
tm.Unix.tm_year + 1900 = year
356
361
&& tm.Unix.tm_mon + 1 = month
357
362
&& tm.Unix.tm_mday = day
358
363
in
359
Removed:
let planned =
364
Added:
let scheduled =
360
365
match active_routine with
361
Removed:
| Some (_, routine) -> planned_workouts ~now ~log ~routine
366
Added:
| Some (_, routine) -> scheduled_workouts ~now ~log ~routine
362
367
| None -> []
363
368
in
364
369
let plural count singular plural = if count = 1 then singular else plural in
@@ -379,23 +384,23 @@
379
384
let elapsed = max 0 (now - at) in
380
385
if elapsed < 60 then "Just now" else elapsed_words elapsed "ago"
381
386
in
382
Removed:
let next_planned_for name =
387
Added:
let next_scheduled_for name =
383
388
List.find_opt
384
Removed:
(fun (planned : planned_workout) ->
389
Added:
(fun (planned : scheduled_workout) ->
385
390
String.equal planned.workout_name name)
386
Removed:
planned
391
Added:
scheduled
387
392
in
388
393
let enriched_entries =
389
394
List.map
390
Removed:
(fun (entry : View_model.Logbook_entry.t) ->
395
Added:
(fun (entry : View_model.Recorded_workout.t) ->
391
396
{
392
397
entry with
393
398
performed_ago = performed_ago ~at:entry.at_unix;
394
Removed:
next_planned =
399
Added:
next_scheduled =
395
400
Option.map
396
Removed:
(fun (planned : planned_workout) ->
401
Added:
(fun (planned : scheduled_workout) ->
397
402
elapsed_words (max 0 (planned.at_unix - now)) "from now")
398
Removed:
(next_planned_for entry.workout_name);
403
Added:
(next_scheduled_for entry.workout_name);
399
404
})
400
405
recorded_entries
401
406
in
@@ -403,21 +408,21 @@
403
408
let selected_completed =
404
409
Option.map
405
410
(fun day -> List.filter (fun entry -> same_day entry day) all_records)
406
Removed:
selected_day_number
411
Added:
selected_date_number
407
412
|> Option.value ~default:[]
408
413
in
409
414
let planned_for_day day =
410
415
List.find_opt
411
Removed:
(fun (planned : planned_workout) ->
416
Added:
(fun (planned : scheduled_workout) ->
412
417
let tm = Unix.gmtime (float_of_int planned.at_unix) in
413
418
tm.Unix.tm_year + 1900 = year
414
419
&& tm.Unix.tm_mon + 1 = month
415
420
&& tm.Unix.tm_mday = day)
416
Removed:
planned
421
Added:
scheduled
417
422
in
418
Removed:
let selected_planned = Option.bind selected_day_number planned_for_day in
419
Removed:
let selected_planned_lwt =
420
Removed:
match selected_planned with
423
Added:
let selected_upcoming = Option.bind selected_date_number planned_for_day in
424
Added:
let selected_upcoming_lwt =
425
Added:
match selected_upcoming with
421
426
| None -> Lwt.return_none
422
427
| Some planned ->
423
428
Lwt_list.map_s
@@ -431,36 +436,35 @@
431
436
| None -> (None, None)
432
437
| Some (load, reps) -> (Some load, Some reps)
433
438
in
434
Removed:
View_model.Selected_day.Planned.
439
Added:
View_model.Upcoming_workout.
435
440
{ name = Exercise.name exercise; latest_load; latest_reps })
436
441
group.exercises
437
Removed:
>|= fun exercises -> View_model.Selected_day.Planned.{ exercises })
442
Added:
>|= fun exercises -> View_model.Upcoming_workout.{ exercises })
438
443
planned.groups
439
444
>|= fun groups ->
440
445
Some
441
Removed:
View_model.Selected_day.Planned.
442
Removed:
{ name = planned.workout_name; groups }
446
Added:
View_model.Upcoming_workout.
447
Added:
{
448
Added:
name = planned.workout_name;
449
Added:
scheduled_on = utc_long_date planned.at_unix;
450
Added:
groups;
451
Added:
}
443
452
in
444
Removed:
selected_planned_lwt >>= fun selected_planned ->
445
Removed:
let selected_day =
453
Added:
selected_upcoming_lwt >>= fun selected_upcoming ->
454
Added:
let selected_date =
446
455
Option.map
447
Removed:
(fun day ->
448
Removed:
View_model.Selected_day.
449
Removed:
{
450
Removed:
date =
451
Removed:
utc_long_date (Calendar.unix_seconds_of_date ~year ~month ~day);
452
Removed:
completed = selected_completed;
453
Removed:
planned = selected_planned;
454
Removed:
})
455
Removed:
selected_day_number
456
Added:
(fun _ ->
457
Added:
View_model.Selected_date.
458
Added:
{ completed = selected_completed; upcoming = selected_upcoming })
459
Added:
selected_date_number
456
460
in
457
461
let delete_record =
458
462
match Dream.query request "delete" with
459
463
| Some id ->
460
464
List.find_opt
461
465
(fun entry ->
462
Removed:
String.equal entry.View_model.Logbook_entry.id id
463
Removed:
&& not entry.View_model.Logbook_entry.imported)
466
Added:
String.equal entry.View_model.Recorded_workout.id id
467
Added:
&& not entry.View_model.Recorded_workout.imported)
464
468
all_records
465
469
| None -> None
466
470
in
@@ -470,8 +474,8 @@
470
474
| _ -> false
471
475
in
472
476
let calendar =
473
Removed:
calendar_view ~now ~year ~month ~selected_day:selected_day_number ~planned
474
Removed:
all_records
477
Added:
calendar_view ~now ~year ~month ~selected_date:selected_date_number
478
Added:
~scheduled all_records
475
479
in
476
480
let progress_charts_lwt =
477
481
if String.equal tab "progress" then
@@ -507,7 +511,7 @@
507
511
html
508
512
(Pages.logbook request
509
513
?workout_started:(workout_started in_progress)
510
Removed:
~viewer ~calendar ?selected_day
514
Added:
~viewer ~calendar ?selected_date
511
515
~suggest_feedback:
512
516
(match Dream.query request "prompt" with
513
517
| Some "feedback" -> true
lib/web/handlers_overview.ml
@@ -45,51 +45,47 @@
45
45
(Prescription.Routine.workouts r);
46
46
}
47
47
48
Removed:
(* The most recent effort for [observations], pre-formatted as the record to
49
Removed:
beat. History is oldest first, so the best current combination — the one to
50
Removed:
beat next — is the last. An observed fact, not a target. *)
51
Removed:
let best_of observations =
48
Added:
(* The most recent observed effort for an exercise. History is oldest first,
49
Added:
so the last value is the evidence available for the next workout. *)
50
Added:
let latest_performance observations =
52
51
match List.rev observations with
53
52
| latest :: _ ->
54
Removed:
let e = Evidence.Observation.effort latest in
53
Added:
let effort = Evidence.Observation.effort latest in
55
54
Some
56
Removed:
(Printf.sprintf "%g kg × %d reps"
57
Removed:
(Evidence.Stimulus.Effort.load e)
58
Removed:
(Evidence.Stimulus.Effort.reps e))
55
Added:
( Evidence.Stimulus.Effort.load effort,
56
Added:
Evidence.Stimulus.Effort.reps effort )
59
57
| [] -> None
60
58
61
Removed:
(* The exercises of one prescribed stimulus, in order. A pre-exhaust pair
62
Removed:
yields two, one per underlying exercise, so every exercise shows its own
63
Removed:
weight × reps to beat. *)
64
Removed:
let stimulus_exercises prescription =
65
Removed:
match Prescription.Stimulus.delivery prescription with
66
Removed:
| Prescription.Stimulus.Single exercise -> [ exercise ]
67
Removed:
| Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
68
Removed:
[ isolation; compound ]
69
Removed:
70
Removed:
(* Build the preview lines for the next workout, each with its current best
71
Removed:
from the merged native and imported history. *)
72
Removed:
let next_exercise_lines t trainee next :
73
Removed:
View_model.Home.exercise_line list Lwt.t =
74
Removed:
let exercises =
75
Removed:
List.concat_map stimulus_exercises (Prescription.Workout.stimuli next)
76
Removed:
in
59
Added:
(* Build the next planned workout with the latest observed performance for
60
Added:
each exercise. Groups preserve a pre-exhaust pair. *)
61
Added:
let scheduled_workout_view t trainee next ~scheduled_on :
62
Added:
View_model.Upcoming_workout.t Lwt.t =
77
63
Lwt_list.map_s
78
Removed:
(fun exercise ->
79
Removed:
Service.exercise_history t.service trainee exercise >|= fun history ->
80
Removed:
{
81
Removed:
View_model.Home.name = Exercise.name exercise;
82
Removed:
best = best_of history;
83
Removed:
})
84
Removed:
exercises
64
Added:
(fun stimulus ->
65
Added:
Lwt_list.map_s
66
Added:
(fun exercise ->
67
Added:
Service.exercise_history t.service trainee exercise
68
Added:
>|= fun history ->
69
Added:
let latest_load, latest_reps =
70
Added:
match latest_performance history with
71
Added:
| None -> (None, None)
72
Added:
| Some (load, reps) -> (Some load, Some reps)
73
Added:
in
74
Added:
View_model.Upcoming_workout.
75
Added:
{ name = Exercise.name exercise; latest_load; latest_reps })
76
Added:
(Prescription.Stimulus.exercises stimulus)
77
Added:
>|= fun exercises -> View_model.Upcoming_workout.{ exercises })
78
Added:
(Prescription.Workout.stimuli next)
79
Added:
>|= fun groups ->
80
Added:
View_model.Upcoming_workout.
81
Added:
{ name = Prescription.Workout.name next; scheduled_on; groups }
85
82
86
Removed:
let home_view ~(routine : Repository.routine_id) ~routine_name ~next
87
Removed:
~readiness ~next_exercises : View_model.Home.t =
83
Added:
let home_view ~(routine : Repository.routine_id) ~routine_name ~next_workout
84
Added:
~readiness : View_model.Home.t =
88
85
{
89
86
routine_id = (routine :> string);
90
87
routine_name;
91
Removed:
next_workout = Prescription.Workout.name next;
92
Removed:
next_exercises;
88
Added:
next_workout;
93
89
gate =
94
90
(match readiness with
95
91
| Recovery.Ready -> View_model.Home.Ready
@@ -127,13 +123,24 @@
127
123
>>= fun readiness ->
128
124
match (next, readiness) with
129
125
| Ok next, Ok readiness ->
130
Removed:
next_exercise_lines t trainee.Trainee.id next
131
Removed:
>|= fun next_exercises ->
126
Added:
let now = Recovery.timestamp_to_unix_seconds (t.now ()) in
127
Added:
let scheduled_on =
128
Added:
match readiness with
129
Added:
| Recovery.Ready -> now
130
Added:
| Recovery.Recovering { rested; recommended } ->
131
Added:
now
132
Added:
+ max 0
133
Added:
(Recovery.duration_to_seconds recommended
134
Added:
- Recovery.duration_to_seconds rested)
135
Added:
in
136
Added:
scheduled_workout_view t trainee.Trainee.id next
137
Added:
~scheduled_on:(utc_long_date scheduled_on)
138
Added:
>|= fun next_workout ->
132
139
Pages.home request ~viewer
133
140
~home:
134
141
(home_view ~routine
135
142
~routine_name:(Prescription.Routine.name selected)
136
Removed:
~next ~readiness ~next_exercises)
143
Added:
~next_workout ~readiness)
137
144
| Error error, _ | _, Error error ->
138
145
Lwt.return
139
146
(Pages.problem ~title:"Routine unavailable"
test/web/logbook.ml
@@ -53,14 +53,19 @@
53
53
"calendar names the next workout" true
54
54
(Option.is_some (index_from ~needle:"13: planned Day 2" page 0));
55
55
Alcotest.(check bool)
56
Removed:
"selected date uses the long ordinal format" true
57
Removed:
(Option.is_some (index_from ~needle:"13th January 1970" page 0));
56
Added:
"planned workout uses the selected date" true
57
Added:
(Option.is_some
58
Added:
(index_from ~needle:"Planned on 13th January 1970" page 0));
58
59
Alcotest.(check bool)
59
60
"selected planned workout is displayed" true
60
Removed:
(Option.is_some (index_from ~needle:"Planned: Day 2" page 0));
61
Added:
(Option.is_some (index_from ~needle:"Day 2" page 0));
61
62
Alcotest.(check bool)
62
Removed:
"planned workout uses the latest-performance table" true
63
Removed:
(Option.is_some (index_from ~needle:"Latest performance" page 0));
63
Added:
"planned workout dates itself in the table caption" true
64
Added:
(Option.is_some
65
Added:
(index_from ~needle:"Planned on 13th January 1970" page 0));
66
Added:
Alcotest.(check bool)
67
Added:
"planned workout omits change columns" true
68
Added:
(Option.is_none (index_from ~needle:"Δ" page 0));
64
69
Alcotest.(check bool)
65
70
"the old entry list is absent" true
66
71
(Option.is_none (index_from ~needle:"logbook-entry" page 0)) );
test/web/overview.ml
@@ -104,8 +104,13 @@
104
104
"the Day 1 preview lists Laterals" true
105
105
(Option.is_some (index_from ~needle:"Laterals" page 0));
106
106
Alcotest.(check bool)
107
Removed:
"Laterals shows its best weight and reps" true
108
Removed:
(Option.is_some (index_from ~needle:"14 kg × 10 reps" page 0)) );
107
Added:
"Laterals names the planned workout in the table caption" true
108
Added:
(Option.is_some (index_from ~needle:"Day 1" page 0));
109
Added:
Alcotest.(check bool)
110
Added:
"Laterals shows its best load and reps" true
111
Added:
(Option.is_some (index_from ~needle:"Load (kg)" page 0)
112
Added:
&& Option.is_some (index_from ~needle:">14<" page 0)
113
Added:
&& Option.is_some (index_from ~needle:">10<" page 0)) );
109
114
( "the routine page is served once a routine is selected",
110
115
`Quick,
111
116
fun () ->