[OCaml] High Intensity Training Online
fix correct verdict colours, simplify progress page and chart
- Swap the verdict colours: progress is green, regress oxblood; stagnation stays brass. - Progress page drops the search. It now renders one chart per exercise that has at least two merged observations; the rest are omitted, with a message when none qualify. - Chart is a plain stroked line -- no markers, no area fill -- with labelled axes: kg on the left, reps on the right, first and last dates on the x-axis. - Mobile logbook rows are a single-line flex row instead of a grid: the name flexes and truncates, and date, duration, and the three-dot menu always sit together on one line. The import pill hides below 26rem; the menu still marks the row as imported.
Changed files
lib/presenter/pages_progress.ml
@@ -2,41 +2,6 @@
2
2
open Page_kit
3
3
open Page_shell
4
4
5
Removed:
(* The exercise search: a native text input backed by a datalist, submitting a
6
Removed:
GET so the page reloads with the chosen exercise's chart. No script needed.
7
Removed:
The datalist maps a visible name to its id via a hidden convention: the
8
Removed:
input carries the id as its value once chosen from the list. To keep it
9
Removed:
no-JS, the list options use the exercise name and the form submits the name,
10
Removed:
which the handler resolves. *)
11
Removed:
let search_form ~(exercises : (string * string) list) ~selected_id =
12
Removed:
let selected_name =
13
Removed:
match selected_id with
14
Removed:
| None -> ""
15
Removed:
| Some id -> (
16
Removed:
match List.assoc_opt id exercises with Some name -> name | None -> "")
17
Removed:
in
18
Removed:
tag "form"
19
Removed:
[ action Routes.progress; class_ "progress-search" ]
20
Removed:
[
21
Removed:
tag "label" [ Dream_html.string_attr "for" "exercise" ] [ txt "Exercise" ];
22
Removed:
void "input"
23
Removed:
[
24
Removed:
id "exercise";
25
Removed:
name "exercise";
26
Removed:
Dream_html.string_attr "list" "exercise-options";
27
Removed:
Dream_html.string_attr "value" "%s" selected_name;
28
Removed:
Dream_html.string_attr "placeholder" "Search exercises…";
29
Removed:
Dream_html.string_attr "autocomplete" "off";
30
Removed:
];
31
Removed:
tag "datalist"
32
Removed:
[ id "exercise-options" ]
33
Removed:
(List.map
34
Removed:
(fun (_, name) ->
35
Removed:
tag "option" [ Dream_html.string_attr "value" "%s" name ] [])
36
Removed:
exercises);
37
Removed:
tag "button" [ type_ "submit" ] [ txt "View" ];
38
Removed:
]
39
Removed:
40
5
let legend () =
41
6
tag "div"
42
7
[ class_ "chart-legend" ]
@@ -47,31 +12,27 @@
47
12
tag "span" [ class_ "chart-legend-item reps" ] [ txt "Reps, right axis" ];
48
13
]
49
14
15
Added:
let chart_section (chart : View_model.Progress_page.chart) =
16
Added:
tag "section"
17
Added:
[ class_ "progress-chart-section" ]
18
Added:
[
19
Added:
tag "h2" [] [ txt "%s" chart.exercise_name ];
20
Added:
legend ();
21
Added:
Svg_chart.render chart;
22
Added:
]
23
Added:
50
24
let progress request ?(logging = false) ~viewer
51
25
(vm : View_model.Progress_page.t) =
52
26
html_page ~viewer ~request ~active:"logbook" ~logging "Progress"
53
Removed:
[
54
Removed:
tag "h1" [] [ txt "Progress" ];
55
Removed:
search_form ~exercises:vm.exercises ~selected_id:vm.selected_id;
56
Removed:
(match vm.chart with
57
Removed:
| Some chart ->
58
Removed:
tag "section"
59
Removed:
[ class_ "progress-chart-section" ]
60
Removed:
[
61
Removed:
tag "h2" [] [ txt "%s" chart.exercise_name ];
62
Removed:
legend ();
63
Removed:
Svg_chart.render chart.points;
64
Removed:
]
65
Removed:
| None ->
66
Removed:
if vm.insufficient then
67
Removed:
tag "p"
68
Removed:
[ class_ "eyebrow" ]
69
Removed:
[
70
Removed:
txt
71
Removed:
"Not enough history to chart yet — log or import at least \
72
Removed:
two performances of this exercise.";
73
Removed:
]
74
Removed:
else
75
Removed:
tag "p" []
76
Removed:
[ txt "Search for an exercise to see its progress chart." ]);
77
Removed:
]
27
Added:
(tag "h1" [] [ txt "Progress" ]
28
Added:
::
29
Added:
(if vm.charts = [] then
30
Added:
[
31
Added:
tag "p" []
32
Added:
[
33
Added:
txt
34
Added:
"No exercise has enough history to chart yet. Log or import at \
35
Added:
least two performances of an exercise to see its progress.";
36
Added:
];
37
Added:
]
38
Added:
else List.map chart_section vm.charts))
lib/presenter/svg_chart.ml
@@ -10,10 +10,8 @@
10
10
open Dream_html
11
11
12
12
let svg = std_tag "svg"
13
Removed:
let g = std_tag "g"
14
13
let polyline = std_tag "polyline"
15
14
let line = std_tag "line"
16
Removed:
let circle = std_tag "circle"
17
15
let text = std_tag "text"
18
16
let sattr = string_attr
19
17
let fattr = float_attr
@@ -53,29 +51,15 @@
53
51
|> List.map (fun (x, y) -> Printf.sprintf "%.1f,%.1f" x y)
54
52
|> String.concat " "
55
53
54
Added:
(* A plain stroked line: no markers, no area fill. *)
56
55
let series ~css_class coords =
57
Removed:
let markers =
58
Removed:
List.map
59
Removed:
(fun (x, y) ->
60
Removed:
circle
61
Removed:
[
62
Removed:
sattr "class" "%s" css_class;
63
Removed:
fattr "cx" x;
64
Removed:
fattr "cy" y;
65
Removed:
fattr "r" 3.;
66
Removed:
]
67
Removed:
[])
68
Removed:
coords
69
Removed:
in
70
Removed:
g []
71
Removed:
(polyline
72
Removed:
[
73
Removed:
sattr "class" "%s" css_class;
74
Removed:
sattr "fill" "none";
75
Removed:
sattr "points" "%s" (points_attr coords);
76
Removed:
]
77
Removed:
[]
78
Removed:
:: markers)
56
Added:
polyline
57
Added:
[
58
Added:
sattr "class" "%s" css_class;
59
Added:
sattr "fill" "none";
60
Added:
sattr "points" "%s" (points_attr coords);
61
Added:
]
62
Added:
[]
79
63
80
64
let axis_line ~x1 ~y1 ~x2 ~y2 =
81
65
line
@@ -100,7 +84,8 @@
100
84
101
85
(* Render the chart for a series of (weight, reps) points in time order. The
102
86
caller guarantees at least two points. *)
103
Removed:
let render (points : View_model.Progress_page.point list) =
87
Added:
let render (chart : View_model.Progress_page.chart) =
88
Added:
let points = chart.points in
104
89
let n = List.length points in
105
90
let weights =
106
91
List.map (fun (p : View_model.Progress_page.point) -> p.weight) points
@@ -129,32 +114,52 @@
129
114
sattr "class" "progress-chart";
130
115
sattr "viewBox" "0 0 %g %g" width height;
131
116
sattr "role" "img";
132
Removed:
sattr "aria-label"
133
Removed:
"Line chart of weight and reps over time for the selected exercise";
117
Added:
sattr "aria-label" "Line chart of weight and reps over time for %s"
118
Added:
chart.exercise_name;
134
119
sattr "preserveAspectRatio" "xMidYMid meet";
135
120
]
136
Removed:
[
137
Removed:
(* Axes: left (weight), right (reps), and a baseline. *)
138
Removed:
axis_line ~x1:pad_left ~y1:pad_top ~x2:pad_left ~y2:(pad_top +. plot_h);
139
Removed:
axis_line ~x1:(pad_left +. plot_w) ~y1:pad_top ~x2:(pad_left +. plot_w)
140
Removed:
~y2:(pad_top +. plot_h);
141
Removed:
axis_line ~x1:pad_left ~y1:(pad_top +. plot_h) ~x2:(pad_left +. plot_w)
142
Removed:
~y2:(pad_top +. plot_h);
143
Removed:
(* Left axis bounds (weight, kg). *)
144
Removed:
axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. 4.) ~anchor:"end"
145
Removed:
~css_class:"chart-tick weight" (Printf.sprintf "%g" w_hi);
146
Removed:
axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. plot_h) ~anchor:"end"
147
Removed:
~css_class:"chart-tick weight" (Printf.sprintf "%g" w_lo);
148
Removed:
(* Right axis bounds (reps). *)
149
Removed:
axis_label
150
Removed:
~x:(pad_left +. plot_w +. 6.)
151
Removed:
~y:(pad_top +. 4.) ~anchor:"start" ~css_class:"chart-tick reps"
152
Removed:
(Printf.sprintf "%g" r_hi);
153
Removed:
axis_label
154
Removed:
~x:(pad_left +. plot_w +. 6.)
155
Removed:
~y:(pad_top +. plot_h) ~anchor:"start" ~css_class:"chart-tick reps"
156
Removed:
(Printf.sprintf "%g" r_lo);
157
Removed:
(* The two series. *)
158
Removed:
series ~css_class:"chart-series weight" weight_coords;
159
Removed:
series ~css_class:"chart-series reps" rep_coords;
160
Removed:
]
121
Added:
(let first_date =
122
Added:
match points with p :: _ -> p.View_model.Progress_page.date | [] -> ""
123
Added:
in
124
Added:
let last_date =
125
Added:
match List.rev points with
126
Added:
| p :: _ -> p.View_model.Progress_page.date
127
Added:
| [] -> ""
128
Added:
in
129
Added:
[
130
Added:
(* Axes: left (weight), right (reps), and a baseline. *)
131
Added:
axis_line ~x1:pad_left ~y1:pad_top ~x2:pad_left ~y2:(pad_top +. plot_h);
132
Added:
axis_line ~x1:(pad_left +. plot_w) ~y1:pad_top ~x2:(pad_left +. plot_w)
133
Added:
~y2:(pad_top +. plot_h);
134
Added:
axis_line ~x1:pad_left ~y1:(pad_top +. plot_h) ~x2:(pad_left +. plot_w)
135
Added:
~y2:(pad_top +. plot_h);
136
Added:
(* Axis titles. *)
137
Added:
axis_label ~x:pad_left ~y:(pad_top -. 10.) ~anchor:"middle"
138
Added:
~css_class:"chart-axis-title weight" "kg";
139
Added:
axis_label ~x:(pad_left +. plot_w) ~y:(pad_top -. 10.) ~anchor:"middle"
140
Added:
~css_class:"chart-axis-title reps" "reps";
141
Added:
(* Left axis bounds (weight, kg). *)
142
Added:
axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. 4.) ~anchor:"end"
143
Added:
~css_class:"chart-tick weight" (Printf.sprintf "%g" w_hi);
144
Added:
axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. plot_h) ~anchor:"end"
145
Added:
~css_class:"chart-tick weight" (Printf.sprintf "%g" w_lo);
146
Added:
(* Right axis bounds (reps). *)
147
Added:
axis_label
148
Added:
~x:(pad_left +. plot_w +. 6.)
149
Added:
~y:(pad_top +. 4.) ~anchor:"start" ~css_class:"chart-tick reps"
150
Added:
(Printf.sprintf "%g" r_hi);
151
Added:
axis_label
152
Added:
~x:(pad_left +. plot_w +. 6.)
153
Added:
~y:(pad_top +. plot_h) ~anchor:"start" ~css_class:"chart-tick reps"
154
Added:
(Printf.sprintf "%g" r_lo);
155
Added:
(* X-axis: first and last dates. *)
156
Added:
axis_label ~x:pad_left
157
Added:
~y:(pad_top +. plot_h +. 20.)
158
Added:
~anchor:"start" ~css_class:"chart-tick date" first_date;
159
Added:
axis_label ~x:(pad_left +. plot_w)
160
Added:
~y:(pad_top +. plot_h +. 20.)
161
Added:
~anchor:"end" ~css_class:"chart-tick date" last_date;
162
Added:
(* The two series. *)
163
Added:
series ~css_class:"chart-series weight" weight_coords;
164
Added:
series ~css_class:"chart-series reps" rep_coords;
165
Added:
])
lib/presenter/view_model.ml
@@ -118,8 +118,8 @@
118
118
form, since its option set is fixed UI vocabulary, not domain data. *)
119
119
end
120
120
121
Removed:
(** A per-exercise progress verdict, for colour coding. Progress is oxblood,
122
Removed:
regress green, stagnation brass. *)
121
Added:
(** A per-exercise progress verdict, for colour coding. Progress is green,
122
Added:
regress oxblood, stagnation brass. *)
123
123
type verdict = Progressed | Regressed | Stagnant
124
124
125
125
module Logbook_change = struct
@@ -188,28 +188,19 @@
188
188
weight : float;
189
189
reps : int;
190
190
}
191
Removed:
(** One observation on the chart: its load and reps at a point in time. *)
191
Added:
(** One observation on a chart: its load and reps at a point in time. *)
192
192
193
193
type chart = {
194
194
exercise_name : string;
195
Removed:
points : point list; (** Oldest first. *)
195
Added:
points : point list; (** Oldest first, at least two. *)
196
196
}
197
Removed:
(** The selected exercise's merged native and imported history, ready to plot
198
Removed:
as a dual-axis line chart: weight on the left axis, reps on the right. *)
197
Added:
(** One exercise's merged native and imported history, ready to plot as a
198
Added:
dual-axis line chart: weight on the left axis, reps on the right. *)
199
199
200
Removed:
type t = {
201
Removed:
exercises : (string * string) list;
202
Removed:
(** Every catalog exercise as [(id, name)], for the search datalist. *)
203
Removed:
selected_id : string option; (** The chosen exercise id, if any. *)
204
Removed:
chart : chart option;
205
Removed:
(** [Some] once an exercise with at least two observations is selected.
206
Removed:
[None] before selection or with too little data to draw a line. *)
207
Removed:
insufficient : bool;
208
Removed:
(** True when an exercise was selected but has fewer than two
209
Removed:
observations, so no line can be drawn. *)
210
Removed:
}
211
Removed:
(** The progress page: a no-JS exercise search that reloads with the chosen
212
Removed:
exercise's chart. *)
200
Added:
type t = { charts : chart list }
201
Added:
(** The progress page: one chart per exercise that has at least two
202
Added:
observations. Exercises with too little history to draw a line are
203
Added:
omitted. *)
213
204
end
214
205
215
206
module Import_warning = struct
lib/web/assets/hito.css
@@ -552,14 +552,14 @@
552
552
white-space: nowrap;
553
553
}
554
554
.logbook-table tbody { display: block; }
555
Added:
/* One line per workout on mobile. A flex row is simpler than a grid here: the
556
Added:
name flexes and truncates, and everything else stays its natural width, so
557
Added:
date, duration, and the three-dot menu always sit together on one line. */
555
558
.logbook-table tbody > tr:not(.logbook-detail-row) {
556
Removed:
display: grid;
557
Removed:
grid-template-columns: auto minmax(0, 1fr) auto auto;
558
Removed:
grid-template-areas:
559
Removed:
"date workout duration actions"
560
Removed:
"progress progress progress progress";
561
Removed:
gap: 0.25rem 0.5rem;
559
Added:
display: flex;
562
560
align-items: center;
561
Added:
gap: 0.5rem;
562
Added:
padding: 0.6rem 0.75rem;
563
563
border: 1px solid var(--rule-strong);
564
564
border-radius: var(--radius);
565
565
background: var(--paper-raised);
@@ -567,62 +567,40 @@
567
567
overflow: visible;
568
568
}
569
569
.logbook-table td {
570
Removed:
display: block;
571
570
padding: 0;
572
571
border: none;
573
572
text-align: left;
574
573
}
575
Removed:
/* One line per workout, the same cell order and style at every width:
576
Removed:
monospaced date, sans workout name (with an import pill), monospaced
577
Removed:
duration, then the row menu. A native record's progress toggle drops to a
578
Removed:
second line on mobile; an import carries no progress cell, so its row stays a
579
Removed:
single line. */
574
Added:
/* Date: monospaced, fixed width. */
580
575
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(1) {
581
Removed:
grid-area: date;
582
Removed:
padding: 0.7rem 0 0.7rem 0.85rem;
576
Added:
flex: 0 0 auto;
583
577
font-family: var(--mono);
584
Removed:
line-height: 1.25;
585
578
white-space: nowrap;
586
579
}
580
Added:
/* Name: sans, takes the slack and truncates when the row is too narrow. The
581
Added:
import pill lives here and is hidden on a very narrow screen. */
587
582
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(2) {
588
Removed:
grid-area: workout;
583
Added:
flex: 1 1 auto;
589
584
min-width: 0;
590
Removed:
padding: 0.7rem 0;
591
585
font-family: var(--sans);
592
586
font-weight: 400;
593
Removed:
line-height: 1.25;
594
587
overflow: hidden;
595
588
text-overflow: ellipsis;
596
589
white-space: nowrap;
597
590
}
591
Added:
/* Duration: monospaced, natural width, muted. */
598
592
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(3) {
599
Removed:
grid-area: duration;
600
Removed:
padding: 0.7rem 0;
593
Added:
flex: 0 0 auto;
601
594
font-family: var(--mono);
602
595
color: var(--muted-ink);
603
596
white-space: nowrap;
604
597
}
598
Added:
/* The three-dot menu always sits beside the duration at the row's end. */
605
599
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(4) {
606
Removed:
grid-area: progress;
607
Removed:
padding: 0 0 0.75rem 0.85rem;
608
Removed:
text-align: left;
609
Removed:
font-family: var(--sans);
610
Removed:
}
611
Removed:
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(4):empty {
612
Removed:
display: none;
613
Removed:
}
614
Removed:
.logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(5) {
615
Removed:
grid-area: actions;
616
Removed:
padding: 0 0.35rem 0 0;
600
Added:
flex: 0 0 auto;
601
Added:
margin-left: auto;
617
602
text-align: right;
618
603
}
619
Removed:
620
Removed:
/* On the ledger grid this is a control, not a fixed-width table column. */
621
Removed:
.logbook-table tbody > tr:not(.logbook-detail-row) > td.logbook-actions {
622
Removed:
width: auto;
623
Removed:
justify-self: end;
624
Removed:
align-self: start;
625
Removed:
}
626
604
.logbook-duration,
627
605
.logbook-progress {
628
606
white-space: nowrap;
@@ -775,10 +753,10 @@
775
753
font: 700 var(--font-size-small)/1 var(--sans);
776
754
white-space: nowrap;
777
755
}
778
Removed:
/* Per-exercise progress verdict colours in the row menu: progress oxblood,
779
Removed:
regress green, stagnation brass. The colour lands on that line's delta. */
780
Removed:
.verdict-progress .change-delta { color: var(--oxblood-dark); }
781
Removed:
.verdict-regress .change-delta { color: var(--evergreen); }
756
Added:
/* Per-exercise progress verdict colours in the row menu: progress green,
757
Added:
regress oxblood, stagnation brass. The colour lands on that line's delta. */
758
Added:
.verdict-progress .change-delta { color: var(--evergreen); }
759
Added:
.verdict-regress .change-delta { color: var(--oxblood-dark); }
782
760
.verdict-stagnant .change-delta { color: var(--brass); }
783
761
.verdict-none .change-delta { color: var(--muted-ink); }
784
762
@@ -1371,6 +1349,9 @@
1371
1349
1372
1350
@media (max-width: 26rem) {
1373
1351
.button-group { grid-template-columns: 1fr; }
1352
Added:
/* Too narrow for the pill beside a truncated name; the row stays one line.
1353
Added:
The three-dot menu still marks the workout as imported. */
1354
Added:
.logbook-import-pill { display: none; }
1374
1355
}
1375
1356
1376
1357
@media (prefers-reduced-motion: reduce) {
lib/web/handlers_logbook.ml
@@ -346,6 +346,10 @@
346
346
name; resolve it to a catalog entry, then plot its merged native and
347
347
imported history. A chart needs at least two observations to draw a line;
348
348
fewer reports as insufficient. *)
349
Added:
(* The progress page. One dual-axis line chart per exercise that has at least
350
Added:
two observations in the merged native and imported history; exercises with
351
Added:
too little to draw a line are omitted. No search — the page shows every
352
Added:
chartable exercise. *)
349
353
let progress t trainee request =
350
354
let viewer =
351
355
{
@@ -353,71 +357,37 @@
353
357
Trainee.username_to_string trainee.Trainee.username;
354
358
}
355
359
in
356
Removed:
let exercises =
357
Removed:
List.map
358
Removed:
(fun e -> ((Exercise.id e :> string), Exercise.name e))
359
Removed:
Exercise.catalog
360
Added:
let point_of o : View_model.Progress_page.point =
361
Added:
let effort = Evidence.Observation.effort o in
362
Added:
let at =
363
Added:
Recovery.timestamp_to_unix_seconds (Evidence.Observation.performed_at o)
364
Added:
in
365
Added:
{
366
Added:
date = date_of_unix at;
367
Added:
at_unix = at;
368
Added:
weight = Evidence.Stimulus.Effort.load effort;
369
Added:
reps = Evidence.Stimulus.Effort.reps effort;
370
Added:
}
360
371
in
361
Removed:
(* The search field submits the exercise name; match it back to a catalog
362
Removed:
entry. *)
363
Removed:
let selected =
364
Removed:
match Dream.query request "exercise" with
365
Removed:
| None | Some "" -> None
366
Removed:
| Some query ->
367
Removed:
List.find_opt
368
Removed:
(fun e -> String.equal (Exercise.name e) (String.trim query))
369
Removed:
Exercise.catalog
370
Removed:
in
371
372
Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
372
373
let logging = Option.is_some in_progress in
373
Removed:
match selected with
374
Removed:
| None ->
375
Removed:
html
376
Removed:
(Pages.progress request ~logging ~viewer
377
Removed:
{
378
Removed:
View_model.Progress_page.exercises;
379
Removed:
selected_id = None;
380
Removed:
chart = None;
381
Removed:
insufficient = false;
382
Removed:
})
383
Removed:
| Some exercise ->
374
Added:
(* One history fetch per catalog exercise, kept only when it can draw. *)
375
Added:
Lwt_list.filter_map_s
376
Added:
(fun exercise ->
384
377
Service.exercise_history t.service trainee.Trainee.id exercise
385
Removed:
>>= fun history ->
386
Removed:
let points =
387
Removed:
List.map
388
Removed:
(fun o ->
389
Removed:
let effort = Evidence.Observation.effort o in
390
Removed:
let at =
391
Removed:
Recovery.timestamp_to_unix_seconds
392
Removed:
(Evidence.Observation.performed_at o)
393
Removed:
in
394
Removed:
{
395
Removed:
View_model.Progress_page.date = date_of_unix at;
396
Removed:
at_unix = at;
397
Removed:
weight = Evidence.Stimulus.Effort.load effort;
398
Removed:
reps = Evidence.Stimulus.Effort.reps effort;
399
Removed:
})
400
Removed:
history
401
Removed:
in
402
Removed:
let chart, insufficient =
403
Removed:
if List.length points >= 2 then
404
Removed:
( Some
405
Removed:
{
406
Removed:
View_model.Progress_page.exercise_name =
407
Removed:
Exercise.name exercise;
408
Removed:
points;
409
Removed:
},
410
Removed:
false )
411
Removed:
else (None, true)
412
Removed:
in
413
Removed:
html
414
Removed:
(Pages.progress request ~logging ~viewer
415
Removed:
{
416
Removed:
View_model.Progress_page.exercises;
417
Removed:
selected_id = Some (Exercise.id exercise :> string);
418
Removed:
chart;
419
Removed:
insufficient;
420
Removed:
})
378
Added:
>|= fun history ->
379
Added:
if List.length history >= 2 then
380
Added:
Some
381
Added:
{
382
Added:
View_model.Progress_page.exercise_name = Exercise.name exercise;
383
Added:
points = List.map point_of history;
384
Added:
}
385
Added:
else None)
386
Added:
Exercise.catalog
387
Added:
>>= fun charts ->
388
Added:
html
389
Added:
(Pages.progress request ~logging ~viewer
390
Added:
{ View_model.Progress_page.charts })
421
391
422
392
let routes t =
423
393
[
test/web/logbook.ml
@@ -658,14 +658,21 @@
658
658
(Printf.sprintf "/profile/import/%s/promote" bid)
659
659
[ ("dream.csrf", t) ]
660
660
in
661
Removed:
(* The search submits the exercise name; the page returns its chart. *)
662
Removed:
let page = body (get c "/progress?exercise=Squats") in
661
Added:
(* The progress page renders a chart for every chartable exercise. *)
662
Added:
let page = body (get c "/progress") in
663
663
Alcotest.(check bool)
664
664
"the SVG chart is rendered" true
665
665
(Option.is_some (index_from ~needle:"progress-chart" page 0));
666
666
Alcotest.(check bool)
667
667
"the chart names the exercise" true
668
Removed:
(Option.is_some (index_from ~needle:"Squats" page 0)) );
668
Added:
(Option.is_some (index_from ~needle:"Squats" page 0));
669
Added:
(* Axes are labelled: kg on the left, reps on the right. *)
670
Added:
Alcotest.(check bool)
671
Added:
"the weight axis is labelled kg" true
672
Added:
(Option.is_some (index_from ~needle:"chart-axis-title" page 0));
673
Added:
Alcotest.(check bool)
674
Added:
"the reps axis is labelled" true
675
Added:
(Option.is_some (index_from ~needle:">reps<" page 0)) );
669
676
( "the progress page reports too little history to chart",
670
677
`Quick,
671
678
fun () ->
@@ -718,13 +725,14 @@
718
725
(Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid)
719
726
[ ("dream.csrf", t) ]
720
727
in
721
Removed:
let page = body (get c "/progress?exercise=Squats") in
728
Added:
let page = body (get c "/progress") in
722
729
Alcotest.(check bool)
723
730
"no chart is drawn for one point" false
724
731
(Option.is_some (index_from ~needle:"progress-chart" page 0));
725
732
Alcotest.(check bool)
726
733
"an insufficient-history message is shown" true
727
Removed:
(Option.is_some (index_from ~needle:"Not enough history" page 0))
734
Added:
(Option.is_some
735
Added:
(index_from ~needle:"No exercise has enough history" page 0))
728
736
);
729
737
] );
730
738
]