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.

Commit
0dd99e6f812d9b1eb5ae6ae4e04952949f039150
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/page_kit.ml
index be3ca2c3..a9de3881 100644..100644
@@ -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
index f5324a5c..c7a0756c 100644..100644
@@ -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
index 71512a36..6d16d154 100644..100644
@@ -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
index bd2e686b..34447fe6 100644..100644
@@ -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
index efc72aee..49f89142 100644..100644
@@ -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
index 162c64ce..068a67a4 100644..100644
@@ -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
index 641a4df7..5d8e3a5e 100644..100644
@@ -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
index 4f5131c8..718b9196 100644..100644
@@ -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
index 5accc4aa..d0e17cdd 100644..100644
@@ -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
index 5948a653..75d35848 100644..100644
@@ -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 () ->