[OCaml] High Intensity Training Online
refactor Remove relocated presenter sources
Complete the presenter extraction by removing the former web-library source files.
lib/web/pages.ml
@@ -1,2057 +0,0 @@
1
Removed:
open Dream_html
2
Removed:
3
Removed:
type page = Dream_html.node
4
Removed:
5
Removed:
let tag = Dream_html.std_tag
6
Removed:
let void = Dream_html.void_tag
7
Removed:
let class_ = Dream_html.string_attr "class"
8
Removed:
let name = Dream_html.string_attr "name"
9
Removed:
let value = Dream_html.string_attr "value"
10
Removed:
let rel = Dream_html.string_attr "rel"
11
Removed:
let type_ = Dream_html.string_attr "type"
12
Removed:
let step = Dream_html.string_attr "step"
13
Removed:
let id = Dream_html.string_attr "id"
14
Removed:
let tabindex = Dream_html.string_attr "tabindex"
15
Removed:
let required = Dream_html.attr "required"
16
Removed:
let href path = Dream_html.path_attr (Dream_html.uri_attr "href") path
17
Removed:
let src path = Dream_html.path_attr (Dream_html.uri_attr "src") path
18
Removed:
let action path = Dream_html.path_attr (Dream_html.uri_attr "action") path
19
Removed:
let post_form = Dream_html.string_attr "method" "post"
20
Removed:
let flash_key = "hito.flash"
21
Removed:
22
Removed:
(* The shell. [viewer] and [request] are present on authenticated pages, which
23
Removed:
then show a logout control and the username on the history link. Auth pages
24
Removed:
omit both. *)
25
Removed:
let theme_session_key = "hito.theme"
26
Removed:
27
Removed:
let html_page ?viewer ?request ?(active = "") ?(logging = false)
28
Removed:
?(show_nav = false) title content =
29
Removed:
let spa_client = true in
30
Removed:
let theme =
31
Removed:
match request with
32
Removed:
| Some request -> (
33
Removed:
match Dream.session_field request theme_session_key with
34
Removed:
| Some "dark" -> "dark"
35
Removed:
| _ -> "light")
36
Removed:
| None -> "light"
37
Removed:
in
38
Removed:
let nav_link page path label =
39
Removed:
let attrs = [ href path ] in
40
Removed:
let attrs =
41
Removed:
if spa_client then Dream_html.attr "data-hito-app-link" :: attrs
42
Removed:
else attrs
43
Removed:
in
44
Removed:
let attrs =
45
Removed:
if String.equal page active then
46
Removed:
Dream_html.string_attr "aria-current" "page" :: attrs
47
Removed:
else attrs
48
Removed:
in
49
Removed:
tag "a" attrs [ txt "%s" label ]
50
Removed:
in
51
Removed:
(* Three always-available actions. The middle slot points to the routine
52
Removed:
normally and to the workout in progress while logging, so a running
53
Removed:
workout stays one tap away without adding a fourth destination. The logbook
54
Removed:
tab always reads "Logbook". The username lives on its own profile control. *)
55
Removed:
let navigation_links () =
56
Removed:
[
57
Removed:
nav_link "home" Routes.home "Home";
58
Removed:
(if logging then nav_link "workout" Routes.workout "Current workout"
59
Removed:
else nav_link "routine" Routes.routine "Routine");
60
Removed:
nav_link "logbook" Routes.logbook "Logbook";
61
Removed:
]
62
Removed:
in
63
Removed:
(* The profile control names the signed-in trainee and opens a drop-down of
64
Removed:
account actions: Profile, Settings, and Sign out. It sits in the masthead,
65
Removed:
and on mobile it is the top-bar profile control the bottom nav has no room
66
Removed:
for. Settings points at the profile page too — account settings live there
67
Removed:
and there is no separate settings page. A <details> menu needs no client
68
Removed:
script, so it works before the SPA enhancement loads. The Sign-out form is
69
Removed:
nested inside and keeps its CSRF token. *)
70
Removed:
let profile_area =
71
Removed:
match (viewer, request) with
72
Removed:
| Some (viewer : View_model.Viewer.t), Some request ->
73
Removed:
let menu_link page path label =
74
Removed:
let attrs = [ class_ "profile-menu-item"; href path ] in
75
Removed:
let attrs = Dream_html.string_attr "role" "menuitem" :: attrs in
76
Removed:
let attrs =
77
Removed:
if spa_client then Dream_html.attr "data-hito-app-link" :: attrs
78
Removed:
else attrs
79
Removed:
in
80
Removed:
let attrs =
81
Removed:
if String.equal page active then
82
Removed:
Dream_html.string_attr "aria-current" "page" :: attrs
83
Removed:
else attrs
84
Removed:
in
85
Removed:
tag "a" attrs [ txt "%s" label ]
86
Removed:
in
87
Removed:
let summary_attrs =
88
Removed:
if String.equal "profile" active || String.equal "settings" active
89
Removed:
then
90
Removed:
[ class_ "profile"; Dream_html.string_attr "aria-current" "page" ]
91
Removed:
else [ class_ "profile" ]
92
Removed:
in
93
Removed:
[
94
Removed:
tag "details"
95
Removed:
[ class_ "profile-menu" ]
96
Removed:
[
97
Removed:
tag "summary" summary_attrs
98
Removed:
[ txt "%s" viewer.View_model.Viewer.username ];
99
Removed:
tag "div"
100
Removed:
[
101
Removed:
class_ "profile-menu-drawer";
102
Removed:
Dream_html.string_attr "role" "menu";
103
Removed:
Dream_html.string_attr "aria-label" "Profile menu";
104
Removed:
]
105
Removed:
[
106
Removed:
menu_link "profile" Routes.profile "Profile";
107
Removed:
menu_link "settings" Routes.settings "Settings";
108
Removed:
menu_link "app-feedback" Routes.app_feedback "Feedback";
109
Removed:
tag "form"
110
Removed:
[
111
Removed:
action Routes.logout;
112
Removed:
post_form;
113
Removed:
class_ "logout";
114
Removed:
Dream_html.attr "data-hito-app-form";
115
Removed:
]
116
Removed:
[
117
Removed:
Dream_html.csrf_tag request;
118
Removed:
void "input"
119
Removed:
[
120
Removed:
type_ "submit";
121
Removed:
class_ "profile-menu-item";
122
Removed:
Dream_html.string_attr "role" "menuitem";
123
Removed:
value "Sign out";
124
Removed:
];
125
Removed:
];
126
Removed:
];
127
Removed:
];
128
Removed:
]
129
Removed:
| _ -> []
130
Removed:
in
131
Removed:
let account_area = [] in
132
Removed:
let primary_nav =
133
Removed:
match (viewer, request) with
134
Removed:
| Some (_ : View_model.Viewer.t), Some _ ->
135
Removed:
[ tag "nav" [ class_ "primary-nav" ] (navigation_links ()) ]
136
Removed:
| _ when show_nav ->
137
Removed:
[ tag "nav" [ class_ "primary-nav" ] (navigation_links ()) ]
138
Removed:
| _ -> []
139
Removed:
in
140
Removed:
let bottom_nav =
141
Removed:
match (viewer, request) with
142
Removed:
| Some (_ : View_model.Viewer.t), Some _ ->
143
Removed:
[
144
Removed:
tag "nav"
145
Removed:
[
146
Removed:
class_ "bottom-nav";
147
Removed:
Dream_html.string_attr "aria-label" "Mobile navigation";
148
Removed:
]
149
Removed:
(navigation_links ());
150
Removed:
]
151
Removed:
| _ when show_nav ->
152
Removed:
[
153
Removed:
tag "nav"
154
Removed:
[
155
Removed:
class_ "bottom-nav";
156
Removed:
Dream_html.string_attr "aria-label" "Mobile navigation";
157
Removed:
]
158
Removed:
(navigation_links ());
159
Removed:
]
160
Removed:
| _ -> []
161
Removed:
in
162
Removed:
let flash_notice =
163
Removed:
match request with
164
Removed:
| Some request -> (
165
Removed:
match Dream.session_field request flash_key with
166
Removed:
| Some message ->
167
Removed:
[
168
Removed:
tag "div"
169
Removed:
[
170
Removed:
class_ "toast";
171
Removed:
Dream_html.attr "data-hito-toast";
172
Removed:
Dream_html.string_attr "role" "status";
173
Removed:
Dream_html.string_attr "aria-live" "polite";
174
Removed:
]
175
Removed:
[ txt "%s" message ];
176
Removed:
]
177
Removed:
| None -> [])
178
Removed:
| None -> []
179
Removed:
in
180
Removed:
tag "html"
181
Removed:
[
182
Removed:
Dream_html.string_attr "lang" "en";
183
Removed:
Dream_html.string_attr "data-theme" "%s" theme;
184
Removed:
]
185
Removed:
[
186
Removed:
tag "head" []
187
Removed:
[
188
Removed:
tag "title" [] [ txt "Hito — %s" title ];
189
Removed:
void "meta" [ Dream_html.string_attr "charset" "utf-8" ];
190
Removed:
void "meta"
191
Removed:
[
192
Removed:
Dream_html.string_attr "name" "viewport";
193
Removed:
Dream_html.string_attr "content"
194
Removed:
"width=device-width,initial-scale=1";
195
Removed:
];
196
Removed:
void "link" [ rel "stylesheet"; href Routes.stylesheet ];
197
Removed:
];
198
Removed:
tag "body"
199
Removed:
[ Dream_html.string_attr "class" "hito-app" ]
200
Removed:
(flash_notice
201
Removed:
@ [
202
Removed:
tag "a"
203
Removed:
[
204
Removed:
class_ "skip-link";
205
Removed:
Dream_html.string_attr "href" "#main-content";
206
Removed:
]
207
Removed:
[ txt "Skip to main content" ];
208
Removed:
tag "div"
209
Removed:
([
210
Removed:
Dream_html.string_attr "class" "app-shell page-%s" active;
211
Removed:
Dream_html.string_attr "data-hito-page-title" "Hito — %s" title;
212
Removed:
]
213
Removed:
@
214
Removed:
if spa_client then [ Dream_html.attr "data-hito-app-shell" ]
215
Removed:
else [])
216
Removed:
([
217
Removed:
tag "header"
218
Removed:
[ class_ "masthead" ]
219
Removed:
([
220
Removed:
tag "a"
221
Removed:
([ class_ "brand"; href Routes.home ]
222
Removed:
@
223
Removed:
if spa_client then
224
Removed:
[ Dream_html.attr "data-hito-app-link" ]
225
Removed:
else [])
226
Removed:
[ txt "hito" ];
227
Removed:
]
228
Removed:
@ primary_nav @ profile_area @ account_area);
229
Removed:
tag "main"
230
Removed:
[ id "main-content"; tabindex "-1" ]
231
Removed:
[
232
Removed:
tag "div"
233
Removed:
([ class_ "page-surface" ]
234
Removed:
@
235
Removed:
if spa_client then
236
Removed:
[ Dream_html.attr "data-hito-app-content" ]
237
Removed:
else [])
238
Removed:
content;
239
Removed:
];
240
Removed:
]
241
Removed:
@ bottom_nav);
242
Removed:
]
243
Removed:
@
244
Removed:
if spa_client then
245
Removed:
[
246
Removed:
tag "script"
247
Removed:
[ src Routes.workout_client; Dream_html.attr "defer" ]
248
Removed:
[];
249
Removed:
]
250
Removed:
else []);
251
Removed:
]
252
Removed:
253
Removed:
let problem ~title ~detail =
254
Removed:
html_page title
255
Removed:
[
256
Removed:
tag "p" [ class_ "eyebrow" ] [ txt "Attention required" ];
257
Removed:
tag "h1" [] [ txt "%s" title ];
258
Removed:
tag "p" [ class_ "warn" ] [ txt "%s" detail ];
259
Removed:
]
260
Removed:
261
Removed:
(* A branded error page for the common HTTP failures. It states only the class
262
Removed:
of failure, never a server-supplied string, so nothing internal leaks. The
263
Removed:
navigation stays present — top on desktop, bottom on mobile — so a lost
264
Removed:
visitor is one tap from a known destination. *)
265
Removed:
let error_page ?viewer ?request ~status () =
266
Removed:
let title, detail =
267
Removed:
match status with
268
Removed:
| 404 ->
269
Removed:
("Page not found", "That page does not exist. Use the navigation below.")
270
Removed:
| 400 -> ("Bad request", "That request could not be understood.")
271
Removed:
| 403 -> ("Forbidden", "You do not have access to that page.")
272
Removed:
| s when s >= 500 ->
273
Removed:
("Something went wrong", "The server hit a problem. Try again shortly.")
274
Removed:
| _ -> ("Something went wrong", "That request could not be completed.")
275
Removed:
in
276
Removed:
html_page ?viewer ?request ~show_nav:true title
277
Removed:
[
278
Removed:
tag "p" [ class_ "eyebrow" ] [ txt "%d" status ];
279
Removed:
tag "h1" [] [ txt "%s" title ];
280
Removed:
tag "p" [ class_ "warn" ] [ txt "%s" detail ];
281
Removed:
]
282
Removed:
283
Removed:
(* --- authentication --- *)
284
Removed:
285
Removed:
let auth_error = function
286
Removed:
| None -> []
287
Removed:
| Some message ->
288
Removed:
[
289
Removed:
tag "p"
290
Removed:
[ class_ "warn"; Dream_html.string_attr "role" "alert" ]
291
Removed:
[ txt "%s" message ];
292
Removed:
]
293
Removed:
294
Removed:
let credentials_form request ~submit ~action_path =
295
Removed:
tag "form"
296
Removed:
[ action action_path; post_form; Dream_html.attr "data-hito-app-form" ]
297
Removed:
[
298
Removed:
Dream_html.csrf_tag request;
299
Removed:
tag "div"
300
Removed:
[ class_ "field" ]
301
Removed:
[
302
Removed:
tag "label"
303
Removed:
[ Dream_html.string_attr "for" "username" ]
304
Removed:
[ txt "Username" ];
305
Removed:
void "input"
306
Removed:
[
307
Removed:
type_ "text";
308
Removed:
name "username";
309
Removed:
Dream_html.string_attr "id" "username";
310
Removed:
Dream_html.string_attr "autocomplete" "username";
311
Removed:
required;
312
Removed:
];
313
Removed:
];
314
Removed:
tag "div"
315
Removed:
[ class_ "field" ]
316
Removed:
[
317
Removed:
tag "label"
318
Removed:
[ Dream_html.string_attr "for" "password" ]
319
Removed:
[ txt "Password" ];
320
Removed:
void "input"
321
Removed:
[
322
Removed:
type_ "password";
323
Removed:
name "password";
324
Removed:
Dream_html.string_attr "id" "password";
325
Removed:
Dream_html.string_attr "autocomplete" "current-password";
326
Removed:
required;
327
Removed:
];
328
Removed:
];
329
Removed:
void "input" [ type_ "submit"; value submit ];
330
Removed:
]
331
Removed:
332
Removed:
let login request ?error ?(registration_open = false) () =
333
Removed:
let register_prompt =
334
Removed:
if registration_open then
335
Removed:
[
336
Removed:
tag "p" []
337
Removed:
[
338
Removed:
txt "No account yet? ";
339
Removed:
tag "a"
340
Removed:
[ href Routes.register; Dream_html.attr "data-hito-app-link" ]
341
Removed:
[ txt "Register" ];
342
Removed:
];
343
Removed:
]
344
Removed:
else []
345
Removed:
in
346
Removed:
html_page ~active:"auth" "Sign in"
347
Removed:
([
348
Removed:
tag "h1" [] [ txt "Sign in" ];
349
Removed:
credentials_form request ~submit:"Sign in" ~action_path:Routes.login;
350
Removed:
]
351
Removed:
@ register_prompt @ auth_error error)
352
Removed:
353
Removed:
let register request ?error () =
354
Removed:
html_page ~active:"auth" "Register"
355
Removed:
([
356
Removed:
tag "h1" [] [ txt "Create an account" ];
357
Removed:
credentials_form request ~submit:"Register" ~action_path:Routes.register;
358
Removed:
tag "p" []
359
Removed:
[
360
Removed:
txt "Already registered? ";
361
Removed:
tag "a"
362
Removed:
[ href Routes.login; Dream_html.attr "data-hito-app-link" ]
363
Removed:
[ txt "Sign in" ];
364
Removed:
];
365
Removed:
]
366
Removed:
@ auth_error error)
367
Removed:
368
Removed:
(* --- application pages --- *)
369
Removed:
370
Removed:
let error_id input_id = input_id ^ "-error"
371
Removed:
372
Removed:
let extension_select ?(selected = "") ?(invalid = false) ~input_id () =
373
Removed:
let error_attrs =
374
Removed:
if invalid then
375
Removed:
[
376
Removed:
Dream_html.string_attr "aria-invalid" "true";
377
Removed:
Dream_html.string_attr "aria-describedby" "%s" (error_id input_id);
378
Removed:
]
379
Removed:
else []
380
Removed:
in
381
Removed:
let option (code : string) label =
382
Removed:
let attrs = [ Dream_html.string_attr "value" "%s" code ] in
383
Removed:
let attrs =
384
Removed:
if String.equal code selected then Dream_html.attr "selected" :: attrs
385
Removed:
else attrs
386
Removed:
in
387
Removed:
tag "option" attrs [ txt "%s" label ]
388
Removed:
in
389
Removed:
tag "select"
390
Removed:
([ name "extension"; Dream_html.string_attr "id" "%s" input_id ]
391
Removed:
@ error_attrs)
392
Removed:
[
393
Removed:
option "" "to positive failure";
394
Removed:
option "forced" "then forced reps";
395
Removed:
option "negatives" "then negatives";
396
Removed:
option "rest-pause" "then rest-pause";
397
Removed:
option "static" "then a static hold";
398
Removed:
]
399
Removed:
400
Removed:
let choose_routine request ~logging ~viewer
401
Removed:
~(routines : View_model.Routine_choice.t list) =
402
Removed:
html_page ~viewer ~request ~active:"home" ~logging "Choose a routine"
403
Removed:
[
404
Removed:
tag "h1" [] [ txt "Routines" ];
405
Removed:
tag "div" []
406
Removed:
(List.map
407
Removed:
(fun (choice : View_model.Routine_choice.t) ->
408
Removed:
tag "form"
409
Removed:
[
410
Removed:
action Routes.select_routine choice.id;
411
Removed:
post_form;
412
Removed:
Dream_html.attr "data-hito-app-form";
413
Removed:
]
414
Removed:
[
415
Removed:
Dream_html.csrf_tag request;
416
Removed:
tag "fieldset" []
417
Removed:
[
418
Removed:
tag "legend" [] [ txt "%s" choice.name ];
419
Removed:
tag "p" []
420
Removed:
[ txt "%d workouts in the cycle." choice.workout_count ];
421
Removed:
void "input" [ type_ "submit"; value "Use this routine" ];
422
Removed:
];
423
Removed:
])
424
Removed:
routines);
425
Removed:
]
426
Removed:
427
Removed:
let begin_form request ~routine_id ~override label =
428
Removed:
tag "form"
429
Removed:
[ action Routes.workout; post_form; Dream_html.attr "data-hito-app-form" ]
430
Removed:
[
431
Removed:
Dream_html.csrf_tag request;
432
Removed:
void "input"
433
Removed:
[
434
Removed:
type_ "hidden";
435
Removed:
name "routine";
436
Removed:
Dream_html.string_attr "value" "%s" routine_id;
437
Removed:
];
438
Removed:
void "input"
439
Removed:
[
440
Removed:
type_ "hidden";
441
Removed:
name "override";
442
Removed:
Dream_html.string_attr "value" "%s" (string_of_bool override);
443
Removed:
];
444
Removed:
void "input" [ type_ "submit"; value label ];
445
Removed:
]
446
Removed:
447
Removed:
(* Beginning under override needs an explicit acknowledgement, so its submit is
448
Removed:
gated behind a confirmation modal. Without script the form submits at once —
449
Removed:
the doctrine's deviation stays possible. The modal only makes it deliberate.
450
Removed:
The client reuses the shared confirm machinery: [data-hito-confirm-cancel]
451
Removed:
opens the dialog, [data-hito-confirm-form] is the form its accept submits. *)
452
Removed:
let override_begin request ~routine_id =
453
Removed:
[
454
Removed:
tag "form"
455
Removed:
[
456
Removed:
action Routes.workout;
457
Removed:
post_form;
458
Removed:
Dream_html.attr "data-hito-app-form";
459
Removed:
Dream_html.attr "data-hito-confirm-form";
460
Removed:
]
461
Removed:
[
462
Removed:
Dream_html.csrf_tag request;
463
Removed:
void "input"
464
Removed:
[
465
Removed:
type_ "hidden";
466
Removed:
name "routine";
467
Removed:
Dream_html.string_attr "value" "%s" routine_id;
468
Removed:
];
469
Removed:
void "input"
470
Removed:
[
471
Removed:
type_ "hidden";
472
Removed:
name "override";
473
Removed:
Dream_html.string_attr "value" "true";
474
Removed:
];
475
Removed:
void "input"
476
Removed:
[
477
Removed:
type_ "submit";
478
Removed:
value "Begin under override";
479
Removed:
Dream_html.attr "data-hito-confirm-cancel";
480
Removed:
];
481
Removed:
];
482
Removed:
tag "dialog"
483
Removed:
[ class_ "confirm-dialog"; Dream_html.attr "data-hito-confirm-modal" ]
484
Removed:
[
485
Removed:
tag "h2" [] [ txt "Begin before recovery is complete?" ];
486
Removed:
tag "p" []
487
Removed:
[
488
Removed:
txt
489
Removed:
"Heavy Duty grows muscle during recovery, not in the gym. \
490
Removed:
Training early can cut into it. This is recorded as an \
491
Removed:
override.";
492
Removed:
];
493
Removed:
tag "div"
494
Removed:
[ class_ "button-group" ]
495
Removed:
[
496
Removed:
tag "button"
497
Removed:
[
498
Removed:
type_ "button";
499
Removed:
class_ "secondary";
500
Removed:
Dream_html.attr "data-hito-confirm-dismiss";
501
Removed:
]
502
Removed:
[ txt "Wait to recover" ];
503
Removed:
tag "button"
504
Removed:
[ type_ "button"; Dream_html.attr "data-hito-confirm-accept" ]
505
Removed:
[ txt "Begin under override" ];
506
Removed:
];
507
Removed:
];
508
Removed:
]
509
Removed:
510
Removed:
let home request ~viewer ~home:(vm : View_model.Home.t) =
511
Removed:
let status, gate =
512
Removed:
match vm.gate with
513
Removed:
| View_model.Home.Ready ->
514
Removed:
( "Recovery is complete.",
515
Removed:
[
516
Removed:
begin_form request ~routine_id:vm.routine_id ~override:false
517
Removed:
"Begin next workout";
518
Removed:
] )
519
Removed:
| View_model.Home.Recovering { status } ->
520
Removed:
( status,
521
Removed:
[
522
Removed:
tag "div"
523
Removed:
[ class_ "warn" ]
524
Removed:
([
525
Removed:
tag "p" []
526
Removed:
[
527
Removed:
txt "Heavy Duty requires recovery before the next workout.";
528
Removed:
];
529
Removed:
]
530
Removed:
@ override_begin request ~routine_id:vm.routine_id);
531
Removed:
] )
532
Removed:
in
533
Removed:
html_page ~viewer ~request ~active:"home" "Home"
534
Removed:
([
535
Removed:
tag "h1" [] [ txt "%s" vm.routine_name ];
536
Removed:
tag "p" [ class_ "ledger-meta" ] [ txt "Next: %s" vm.next_workout ];
537
Removed:
tag "p" [ class_ "ledger-meta" ] [ txt "%s" status ];
538
Removed:
]
539
Removed:
@ gate)
540
Removed:
541
Removed:
(* Home while a workout is in progress. Rather than the recovery gate or the
542
Removed:
routine chooser, Home shows a single card: the workout is under way and one
543
Removed:
tap returns to it. The middle navigation action already reads "Current
544
Removed:
workout" here, and this card is the matching Home affordance. *)
545
Removed:
let workout_in_progress request ~viewer ~workout_name =
546
Removed:
html_page ~viewer ~request ~active:"home" ~logging:true "Home"
547
Removed:
[
548
Removed:
tag "a"
549
Removed:
[
550
Removed:
class_ "in-progress-card";
551
Removed:
href Routes.workout;
552
Removed:
Dream_html.attr "data-hito-app-link";
553
Removed:
]
554
Removed:
[
555
Removed:
tag "p" [ class_ "eyebrow" ] [ txt "Workout in progress" ];
556
Removed:
tag "h1" [] [ txt "%s" workout_name ];
557
Removed:
tag "p" [] [ txt "Return to the current workout to keep logging." ];
558
Removed:
];
559
Removed:
]
560
Removed:
561
Removed:
let routine request ?(logging = false) ~viewer (vm : View_model.Routine.t) =
562
Removed:
html_page ~viewer ~request ~active:"routine" ~logging "Routine"
563
Removed:
[
564
Removed:
tag "h1" [] [ txt "%s" vm.name ];
565
Removed:
tag "div"
566
Removed:
[ class_ "routine-accordions" ]
567
Removed:
(List.map
568
Removed:
(fun (workout : View_model.Routine.workout) ->
569
Removed:
tag "details"
570
Removed:
[ class_ "routine-accordion" ]
571
Removed:
[
572
Removed:
tag "summary" [] [ txt "%s" workout.name ];
573
Removed:
tag "div"
574
Removed:
[ class_ "routine-description" ]
575
Removed:
[
576
Removed:
tag "ul" []
577
Removed:
(List.map
578
Removed:
(fun (exercise : View_model.Routine.exercise) ->
579
Removed:
tag "li" []
580
Removed:
[
581
Removed:
tag "span"
582
Removed:
[ class_ "routine-exercise" ]
583
Removed:
[ txt "%s" exercise.name ];
584
Removed:
tag "span"
585
Removed:
[ class_ "routine-reps" ]
586
Removed:
[ txt "%s" exercise.reps ];
587
Removed:
])
588
Removed:
workout.exercises);
589
Removed:
];
590
Removed:
])
591
Removed:
vm.workouts);
592
Removed:
]
593
Removed:
594
Removed:
let error_for ~input_id (field : string) (errors : (string * string) list) =
595
Removed:
match List.assoc_opt field errors with
596
Removed:
| None -> []
597
Removed:
| Some error ->
598
Removed:
[
599
Removed:
tag "p"
600
Removed:
[
601
Removed:
Dream_html.string_attr "id" "%s" (error_id input_id);
602
Removed:
class_ "warn";
603
Removed:
Dream_html.string_attr "role" "alert";
604
Removed:
]
605
Removed:
[ txt "%s" (Decode.errors_to_text [ (field, error) ]) ];
606
Removed:
]
607
Removed:
608
Removed:
let input_row ?value:v ?(invalid = false) ?placeholder ?(hide_label = false)
609
Removed:
~input_id (field : string) label =
610
Removed:
let error_attrs =
611
Removed:
if invalid then
612
Removed:
[
613
Removed:
Dream_html.string_attr "aria-invalid" "true";
614
Removed:
Dream_html.string_attr "aria-describedby" "%s" (error_id input_id);
615
Removed:
]
616
Removed:
else []
617
Removed:
in
618
Removed:
let placeholder_attr =
619
Removed:
match placeholder with
620
Removed:
| None -> []
621
Removed:
| Some p -> [ Dream_html.string_attr "placeholder" "%s" p ]
622
Removed:
in
623
Removed:
tag "div"
624
Removed:
[ class_ "field" ]
625
Removed:
[
626
Removed:
tag "label"
627
Removed:
[
628
Removed:
Dream_html.string_attr "for" "%s" input_id;
629
Removed:
class_ (if hide_label then "sr-only" else "");
630
Removed:
]
631
Removed:
[ txt "%s" label ];
632
Removed:
void "input"
633
Removed:
([
634
Removed:
type_ "number";
635
Removed:
Dream_html.string_attr "name" "%s" field;
636
Removed:
Dream_html.string_attr "id" "%s" input_id;
637
Removed:
step "0.5";
638
Removed:
required;
639
Removed:
]
640
Removed:
@ placeholder_attr @ error_attrs
641
Removed:
@
642
Removed:
match v with
643
Removed:
| None -> []
644
Removed:
| Some v -> [ Dream_html.string_attr "value" "%s" v ]);
645
Removed:
]
646
Removed:
647
Removed:
(* A movement's fields as cells of the fieldset grid: the exercise name, the
648
Removed:
load input, and the reps input. The cells are direct participants of the one
649
Removed:
[.logging-grid] the fieldset wraps them in — a [display: contents] wrapper
650
Removed:
groups a movement without opening a nested grid — so every movement's name,
651
Removed:
load, and reps columns line up across the whole fieldset. The name reads as a
652
Removed:
heading, the inputs as labelled fields. *)
653
Removed:
let movement_row ~(errors : (string * string) list) ~name ~load_id ~load_field
654
Removed:
~reps_id ~reps_field ?load_value ?reps_value ~reps_label
655
Removed:
?(hide_labels = false) () =
656
Removed:
tag "div"
657
Removed:
[ class_ "movement" ]
658
Removed:
[
659
Removed:
tag "p" [ class_ "exercise-name" ] [ txt "%s" name ];
660
Removed:
input_row ?value:load_value ~placeholder:"kg" ~hide_label:hide_labels
661
Removed:
~invalid:(List.mem_assoc load_field errors)
662
Removed:
~input_id:load_id load_field "Load (kg)";
663
Removed:
input_row ?value:reps_value ~placeholder:"reps" ~hide_label:hide_labels
664
Removed:
~invalid:(List.mem_assoc reps_field errors)
665
Removed:
~input_id:reps_id reps_field reps_label;
666
Removed:
]
667
Removed:
668
Removed:
(* A shared heading row for a multi-exercise group: empty name cell, then the
669
Removed:
Load and Reps column headings. Rendered once as cells of the fieldset grid so
670
Removed:
the labels are not repeated above every input. *)
671
Removed:
let exercise_fields_head ~reps_label () =
672
Removed:
tag "div"
673
Removed:
[ class_ "movement movement-head" ]
674
Removed:
[
675
Removed:
tag "span" [ class_ "exercise-name" ] [ txt "" ];
676
Removed:
tag "span" [ class_ "col-heading" ] [ txt "Load (kg)" ];
677
Removed:
tag "span" [ class_ "col-heading" ] [ txt "%s" reps_label ];
678
Removed:
]
679
Removed:
680
Removed:
(* [movements] carries the record/correction field spec the controller built
681
Removed:
from the slot's prescription: one row for a single exercise, two for a
682
Removed:
pre-exhaust pair, each already pre-filled when correcting. [submit] names the
683
Removed:
action. Load and reps of each movement are laid out side by side. *)
684
Removed:
let form_for_stimulus request ~action_path ~slot
685
Removed:
~(movements : View_model.Workout.movement list) ~selected_extension
686
Removed:
?(enhanced = false) ?(submit = "Record") ~(errors : (string * string) list)
687
Removed:
() =
688
Removed:
let field_id field = Printf.sprintf "slot-%d-%s" slot field in
689
Removed:
let selected = selected_extension in
690
Removed:
(* A single-exercise slot shows one row with a visible "Reps to failure"
691
Removed:
label. A pre-exhaust pair shows a shared column heading and hides the
692
Removed:
per-row labels. *)
693
Removed:
let hide_labels = List.length movements > 1 in
694
Removed:
let head =
695
Removed:
match movements with
696
Removed:
| first :: _ :: _ ->
697
Removed:
[
698
Removed:
exercise_fields_head ~reps_label:first.View_model.Workout.reps_label
699
Removed:
();
700
Removed:
]
701
Removed:
| _ -> []
702
Removed:
in
703
Removed:
let movement_of (m : View_model.Workout.movement) =
704
Removed:
movement_row ~errors ~name:m.name ~load_id:(field_id m.load_field)
705
Removed:
~load_field:m.load_field ~reps_id:(field_id m.reps_field)
706
Removed:
~reps_field:m.reps_field ?load_value:m.load_value ?reps_value:m.reps_value
707
Removed:
~reps_label:m.reps_label ~hide_labels ()
708
Removed:
in
709
Removed:
let fields = head @ List.map movement_of movements in
710
Removed:
(* The error rows follow the fields present, so a single-exercise slot reports
711
Removed:
load/reps and a pre-exhaust slot reports the iso/comp fields. *)
712
Removed:
let field_names =
713
Removed:
List.concat_map
714
Removed:
(fun (m : View_model.Workout.movement) -> [ m.load_field; m.reps_field ])
715
Removed:
movements
716
Removed:
@ [ "extension" ]
717
Removed:
in
718
Removed:
tag "form"
719
Removed:
([ action_path; post_form ]
720
Removed:
@
721
Removed:
if enhanced then [ Dream_html.attr "data-hito-workout-form" ]
722
Removed:
else [ Dream_html.attr "data-hito-app-form" ])
723
Removed:
[
724
Removed:
Dream_html.csrf_tag request;
725
Removed:
tag "fieldset"
726
Removed:
[ class_ "logging-fieldset" ]
727
Removed:
(tag "div"
728
Removed:
[ class_ "logging-grid" ]
729
Removed:
(fields
730
Removed:
@ [
731
Removed:
tag "div"
732
Removed:
[ class_ "field field-ending" ]
733
Removed:
[
734
Removed:
tag "label"
735
Removed:
[
736
Removed:
Dream_html.string_attr "for" "%s" (field_id "extension");
737
Removed:
]
738
Removed:
[ txt "Ending" ];
739
Removed:
extension_select ~selected
740
Removed:
~invalid:(List.mem_assoc "extension" errors)
741
Removed:
~input_id:(field_id "extension") ();
742
Removed:
];
743
Removed:
])
744
Removed:
:: List.concat_map
745
Removed:
(fun field -> error_for ~input_id:(field_id field) field errors)
746
Removed:
field_names
747
Removed:
@ [
748
Removed:
void "input"
749
Removed:
[ type_ "submit"; Dream_html.string_attr "value" "%s" submit ];
750
Removed:
]);
751
Removed:
]
752
Removed:
753
Removed:
let workout request ~viewer ?(errors = []) ?editing ?logging ~record_id
754
Removed:
~active_slot (vm : View_model.Workout.t) =
755
Removed:
let enhanced = Option.is_none record_id in
756
Removed:
(* A live workout view is itself the workout in progress, so the middle nav
757
Removed:
action reads "Current workout". A saved-record view defers to the handler,
758
Removed:
which knows whether a separate workout is in progress. *)
759
Removed:
let logging = Option.value logging ~default:enhanced in
760
Removed:
let slot_count = List.length vm.slots in
761
Removed:
let filled_slots =
762
Removed:
List.length
763
Removed:
(List.filter
764
Removed:
(fun (s : View_model.Workout.slot) ->
765
Removed:
Option.is_some s.recorded_summary)
766
Removed:
vm.slots)
767
Removed:
in
768
Removed:
let record_action slot =
769
Removed:
match record_id with
770
Removed:
| None -> action Routes.workout_slot slot
771
Removed:
| Some record_id -> action Routes.record_slot record_id slot
772
Removed:
in
773
Removed:
let edit_action slot =
774
Removed:
match record_id with
775
Removed:
| None -> action Routes.workout_slot_edit slot
776
Removed:
| Some record_id -> action Routes.record_slot_edit record_id slot
777
Removed:
in
778
Removed:
(* The early-workout notice. The override was already confirmed in a modal, so
779
Removed:
on the workout screen this is informational only: a toast rather than a
780
Removed:
component in the main content. It is a polite live region. The client shows
781
Removed:
it briefly then dismisses it. Without script it stays visible as a quiet
782
Removed:
fixed notice, so a no-JS visitor is still told. *)
783
Removed:
let override_note =
784
Removed:
if vm.overridden then
785
Removed:
[
786
Removed:
tag "div"
787
Removed:
[
788
Removed:
class_ "toast";
789
Removed:
Dream_html.attr "data-hito-toast";
790
Removed:
Dream_html.string_attr "role" "status";
791
Removed:
Dream_html.string_attr "aria-live" "polite";
792
Removed:
]
793
Removed:
[ txt "Begun before recovery finished." ];
794
Removed:
]
795
Removed:
else []
796
Removed:
in
797
Removed:
(* The exercise selector: a dropdown naming every prescription slot. Choosing
798
Removed:
an option opens that exercise below. The selector is a GET form so it works
799
Removed:
without script — submitting navigates to the slot via a [?slot=] query, the
800
Removed:
same contract the panel already reads. The client enhances it: it navigates
801
Removed:
the moment the selection changes, so the submit button is a no-JS fallback.
802
Removed:
A recorded slot is marked done in its label. *)
803
Removed:
let select_action_base =
804
Removed:
match record_id with None -> "/workout" | Some id -> "/logbook/" ^ id
805
Removed:
in
806
Removed:
let exercise_group =
807
Removed:
let option_for (slot : View_model.Workout.slot) =
808
Removed:
let done_ = Option.is_some slot.recorded_summary in
809
Removed:
let label =
810
Removed:
if done_ then Printf.sprintf "%d. %s (done)" (slot.index + 1) slot.label
811
Removed:
else Printf.sprintf "%d. %s" (slot.index + 1) slot.label
812
Removed:
in
813
Removed:
let attrs = [ Dream_html.string_attr "value" "%d" slot.index ] in
814
Removed:
let attrs =
815
Removed:
if slot.index = active_slot then Dream_html.attr "selected" :: attrs
816
Removed:
else attrs
817
Removed:
in
818
Removed:
tag "option" attrs [ txt "%s" label ]
819
Removed:
in
820
Removed:
let form_attrs =
821
Removed:
[
822
Removed:
Dream_html.string_attr "method" "get";
823
Removed:
Dream_html.string_attr "action" "%s" select_action_base;
824
Removed:
class_ "exercise-select";
825
Removed:
]
826
Removed:
in
827
Removed:
let form_attrs =
828
Removed:
if enhanced then Dream_html.attr "data-hito-exercise-form" :: form_attrs
829
Removed:
else form_attrs
830
Removed:
in
831
Removed:
tag "form" form_attrs
832
Removed:
[
833
Removed:
tag "label"
834
Removed:
[ Dream_html.string_attr "for" "exercise-choice"; class_ "sr-only" ]
835
Removed:
[ txt "Exercise" ];
836
Removed:
tag "select"
837
Removed:
[
838
Removed:
name "slot";
839
Removed:
id "exercise-choice";
840
Removed:
Dream_html.attr "data-hito-exercise-select";
841
Removed:
]
842
Removed:
(List.map option_for vm.slots);
843
Removed:
void "input"
844
Removed:
[
845
Removed:
type_ "submit"; class_ "secondary exercise-select-go"; value "Open";
846
Removed:
];
847
Removed:
]
848
Removed:
in
849
Removed:
(* The panel for the active slot. It shows one exercise at a time: a recorded
850
Removed:
slot renders its read-only summary and a pre-filled correction form (which
851
Removed:
replaces the slot rather than adding volume). An outstanding slot renders
852
Removed:
the record form. *)
853
Removed:
let active_panel =
854
Removed:
match List.nth_opt vm.slots active_slot with
855
Removed:
| None ->
856
Removed:
[
857
Removed:
tag "p"
858
Removed:
[ class_ "warn" ]
859
Removed:
[ txt "That exercise is not part of this workout." ];
860
Removed:
]
861
Removed:
| Some (slot : View_model.Workout.slot) ->
862
Removed:
let errors = if editing = Some active_slot then errors else [] in
863
Removed:
let heading = tag "h2" [] [ txt "%s" slot.label ] in
864
Removed:
let body =
865
Removed:
match slot.recorded_summary with
866
Removed:
| Some summary ->
867
Removed:
[
868
Removed:
tag "p" [ class_ "eyebrow" ] [ txt "Recorded" ];
869
Removed:
tag "p" [ class_ "done" ] [ txt "%s" summary ];
870
Removed:
form_for_stimulus request ~action_path:(edit_action active_slot)
871
Removed:
~slot:active_slot ~movements:slot.movements
872
Removed:
~selected_extension:slot.selected_extension ~enhanced
873
Removed:
~submit:"Save correction" ~errors ();
874
Removed:
]
875
Removed:
| None ->
876
Removed:
[
877
Removed:
form_for_stimulus request
878
Removed:
~action_path:(record_action active_slot)
879
Removed:
~slot:active_slot ~movements:slot.movements
880
Removed:
~selected_extension:slot.selected_extension ~enhanced ~errors
881
Removed:
();
882
Removed:
]
883
Removed:
in
884
Removed:
[ tag "section" [ class_ "slot-panel" ] (heading :: body) ]
885
Removed:
in
886
Removed:
let complete_note =
887
Removed:
if vm.all_recorded then
888
Removed:
[
889
Removed:
tag "p"
890
Removed:
[ class_ "done" ]
891
Removed:
[ txt "Everything prescribed has been recorded." ];
892
Removed:
]
893
Removed:
else []
894
Removed:
in
895
Removed:
(* A sticky timer bar for the workout in progress. It carries the workout's
896
Removed:
start time as an epoch, and the client ticks the elapsed time from it.
897
Removed:
Reading the true start on every render means an SPA content swap never
898
Removed:
resets the count. Only the live workout shows it. *)
899
Removed:
let timer_bar =
900
Removed:
if enhanced then
901
Removed:
let started = vm.started_at_unix in
902
Removed:
[
903
Removed:
tag "div"
904
Removed:
[
905
Removed:
class_ "workout-timer";
906
Removed:
Dream_html.attr "data-hito-workout-timer";
907
Removed:
Dream_html.string_attr "data-started" "%d" started;
908
Removed:
]
909
Removed:
[
910
Removed:
tag "span" [ class_ "workout-timer-label" ] [ txt "Elapsed" ];
911
Removed:
tag "span"
912
Removed:
[
913
Removed:
class_ "workout-timer-value";
914
Removed:
Dream_html.string_attr "aria-live" "off";
915
Removed:
Dream_html.attr "data-hito-workout-timer-value";
916
Removed:
]
917
Removed:
[ txt "0:00" ];
918
Removed:
];
919
Removed:
]
920
Removed:
else []
921
Removed:
in
922
Removed:
let finish_section =
923
Removed:
match record_id with
924
Removed:
| Some _ -> []
925
Removed:
| None ->
926
Removed:
[
927
Removed:
tag "div"
928
Removed:
[ class_ "button-group" ]
929
Removed:
[
930
Removed:
tag "form"
931
Removed:
[
932
Removed:
action Routes.finish_workout;
933
Removed:
post_form;
934
Removed:
Dream_html.attr "data-hito-app-form";
935
Removed:
]
936
Removed:
[
937
Removed:
Dream_html.csrf_tag request;
938
Removed:
void "input" [ type_ "submit"; value "Finish workout" ];
939
Removed:
];
940
Removed:
(* Cancel is a native POST form. Without script, it submits
941
Removed:
immediately. The client only opens the confirm dialog. Its
942
Removed:
acceptance submits this form as a full document navigation so
943
Removed:
the cancelled workout cannot leave a stale app shell behind. *)
944
Removed:
tag "form"
945
Removed:
[
946
Removed:
action Routes.cancel_workout;
947
Removed:
post_form;
948
Removed:
Dream_html.attr "data-hito-confirm-form";
949
Removed:
Dream_html.attr "data-hito-cancel-form";
950
Removed:
]
951
Removed:
[
952
Removed:
Dream_html.csrf_tag request;
953
Removed:
void "input"
954
Removed:
[
955
Removed:
type_ "submit";
956
Removed:
class_ "secondary";
957
Removed:
value "Cancel logging";
958
Removed:
Dream_html.attr "data-hito-confirm-cancel";
959
Removed:
];
960
Removed:
];
961
Removed:
];
962
Removed:
(* The confirmation modal. Native dialog for built-in focus trapping
963
Removed:
and Escape-to-close. The client opens it. "Cancel workout" submits
964
Removed:
the cancel form, "Keep logging" closes it. *)
965
Removed:
tag "dialog"
966
Removed:
[
967
Removed:
class_ "confirm-dialog"; Dream_html.attr "data-hito-confirm-modal";
968
Removed:
]
969
Removed:
[
970
Removed:
tag "h2" [] [ txt "Cancel this workout?" ];
971
Removed:
tag "p" []
972
Removed:
[
973
Removed:
txt
974
Removed:
"The workout is discarded and leaves no record, then you \
975
Removed:
return Home.";
976
Removed:
];
977
Removed:
tag "div"
978
Removed:
[ class_ "button-group" ]
979
Removed:
[
980
Removed:
tag "button"
981
Removed:
[
982
Removed:
type_ "button";
983
Removed:
class_ "secondary";
984
Removed:
Dream_html.attr "data-hito-confirm-dismiss";
985
Removed:
]
986
Removed:
[ txt "Keep logging" ];
987
Removed:
tag "button"
988
Removed:
[
989
Removed:
type_ "button"; Dream_html.attr "data-hito-confirm-accept";
990
Removed:
]
991
Removed:
[ txt "Cancel workout" ];
992
Removed:
];
993
Removed:
];
994
Removed:
]
995
Removed:
in
996
Removed:
html_page ~viewer ~request ~active:"workout" ~logging vm.name
997
Removed:
[
998
Removed:
tag "div"
999
Removed:
(if enhanced then
1000
Removed:
[
1001
Removed:
Dream_html.attr "data-hito-workout";
1002
Removed:
Dream_html.attr "data-hito-workout-content";
1003
Removed:
]
1004
Removed:
else [])
1005
Removed:
(timer_bar @ override_note
1006
Removed:
@ [
1007
Removed:
tag "h1" [] [ txt "%s" vm.name ];
1008
Removed:
tag "p"
1009
Removed:
[ class_ "ledger-meta" ]
1010
Removed:
[ txt "%d of %d recorded." filled_slots slot_count ];
1011
Removed:
exercise_group;
1012
Removed:
]
1013
Removed:
@ active_panel @ complete_note @ finish_section
1014
Removed:
@
1015
Removed:
if enhanced then
1016
Removed:
[
1017
Removed:
tag "p"
1018
Removed:
[
1019
Removed:
Dream_html.string_attr "aria-live" "polite";
1020
Removed:
Dream_html.attr "data-hito-workout-status";
1021
Removed:
]
1022
Removed:
[];
1023
Removed:
]
1024
Removed:
else []);
1025
Removed:
]
1026
Removed:
1027
Removed:
(* The subjective feedback form. Radio groups report sleep, appetite,
1028
Removed:
readiness, motivation, and difficulty against a personal baseline. Flags
1029
Removed:
report pain, injury, and insufficient preparation. A field left blank reports
1030
Removed:
nothing, so the trainee submits only what they mean to. Feedback is
1031
Removed:
standalone — recorded from the Logbook at any time. *)
1032
Removed:
type feedback_flow = { step : int; answers : (string * string) list }
1033
Removed:
1034
Removed:
let feedback_factors =
1035
Removed:
[
1036
Removed:
("sleep", "Sleep");
1037
Removed:
("appetite", "Appetite");
1038
Removed:
("readiness", "Readiness");
1039
Removed:
("motivation", "Motivation");
1040
Removed:
("difficulty", "Perceived difficulty");
1041
Removed:
]
1042
Removed:
1043
Removed:
(* Each subjective metric uses five unselected radio buttons styled as inline
1044
Removed:
buttons. The server shows one metric at a time. *)
1045
Removed:
let feedback_level_buttons field label ~selected =
1046
Removed:
let choice code text =
1047
Removed:
tag "label"
1048
Removed:
[ class_ "feedback-choice"; Dream_html.attr "data-hito-feedback-choice" ]
1049
Removed:
[
1050
Removed:
void "input"
1051
Removed:
([
1052
Removed:
type_ "radio";
1053
Removed:
Dream_html.string_attr "name" "choice";
1054
Removed:
Dream_html.string_attr "value" "%s" code;
1055
Removed:
Dream_html.string_attr "id" "%s-%s" field code;
1056
Removed:
]
1057
Removed:
@
1058
Removed:
if String.equal code selected then [ Dream_html.attr "checked" ]
1059
Removed:
else []);
1060
Removed:
tag "span" [] [ txt "%s" text ];
1061
Removed:
]
1062
Removed:
in
1063
Removed:
tag "div"
1064
Removed:
[ class_ "feedback-group" ]
1065
Removed:
[
1066
Removed:
tag "p" [ class_ "feedback-group-label" ] [ txt "%s" label ];
1067
Removed:
tag "div"
1068
Removed:
[
1069
Removed:
class_ "feedback-buttons";
1070
Removed:
Dream_html.string_attr "role" "group";
1071
Removed:
Dream_html.string_attr "aria-label" "%s" label;
1072
Removed:
]
1073
Removed:
[
1074
Removed:
choice "1" "1 — very poor";
1075
Removed:
choice "2" "2";
1076
Removed:
choice "3" "3";
1077
Removed:
choice "4" "4";
1078
Removed:
choice "5" "5 — very good";
1079
Removed:
];
1080
Removed:
]
1081
Removed:
1082
Removed:
let feedback_flag field label =
1083
Removed:
tag "label"
1084
Removed:
[ class_ "feedback-flag" ]
1085
Removed:
[
1086
Removed:
void "input"
1087
Removed:
[
1088
Removed:
type_ "checkbox";
1089
Removed:
Dream_html.string_attr "name" "%s" field;
1090
Removed:
value "true";
1091
Removed:
];
1092
Removed:
txt " %s" label;
1093
Removed:
]
1094
Removed:
1095
Removed:
let feedback_progress step =
1096
Removed:
let completed = min 5 (max 0 step) in
1097
Removed:
tag "div"
1098
Removed:
[ class_ "feedback-progress" ]
1099
Removed:
[
1100
Removed:
tag "progress"
1101
Removed:
[
1102
Removed:
Dream_html.string_attr "value" "%s" (string_of_int completed);
1103
Removed:
Dream_html.string_attr "max" "5";
1104
Removed:
Dream_html.string_attr "aria-label" "Feedback progress";
1105
Removed:
]
1106
Removed:
[ txt "%d of 5 factors" completed ];
1107
Removed:
tag "p" [ class_ "ledger-meta" ] [ txt "%d of 5 factors" completed ];
1108
Removed:
]
1109
Removed:
1110
Removed:
let feedback_action_button ~action ~label ?(secondary = false) () =
1111
Removed:
tag "button"
1112
Removed:
([
1113
Removed:
type_ "submit";
1114
Removed:
name "action";
1115
Removed:
value action;
1116
Removed:
Dream_html.attr "data-hito-feedback-action";
1117
Removed:
]
1118
Removed:
@ if secondary then [ class_ "secondary" ] else [])
1119
Removed:
[ txt "%s" label ]
1120
Removed:
1121
Removed:
let feedback_flow_form request flow =
1122
Removed:
let step = min 5 (max 0 flow.step) in
1123
Removed:
let fields =
1124
Removed:
[
1125
Removed:
void "input"
1126
Removed:
[
1127
Removed:
type_ "hidden";
1128
Removed:
name "step";
1129
Removed:
Dream_html.string_attr "value" "%s" (string_of_int step);
1130
Removed:
];
1131
Removed:
void "input"
1132
Removed:
[
1133
Removed:
type_ "hidden";
1134
Removed:
name "action";
1135
Removed:
Dream_html.string_attr "value" "";
1136
Removed:
Dream_html.attr "data-hito-feedback-action-value";
1137
Removed:
];
1138
Removed:
Dream_html.csrf_tag request;
1139
Removed:
]
1140
Removed:
in
1141
Removed:
let body =
1142
Removed:
if step < 5 then
1143
Removed:
let field, label = List.nth feedback_factors step in
1144
Removed:
fields
1145
Removed:
@ [
1146
Removed:
tag "h3" [] [ txt "%s" label ];
1147
Removed:
feedback_level_buttons field label
1148
Removed:
~selected:
1149
Removed:
(Option.value (List.assoc_opt field flow.answers) ~default:"");
1150
Removed:
feedback_progress (step + 1);
1151
Removed:
tag "div"
1152
Removed:
[ class_ "feedback-actions" ]
1153
Removed:
((if step > 0 then
1154
Removed:
let _, previous_label = List.nth feedback_factors (step - 1) in
1155
Removed:
[
1156
Removed:
feedback_action_button ~action:"back"
1157
Removed:
~label:(Printf.sprintf "Back to %s" previous_label)
1158
Removed:
~secondary:true ();
1159
Removed:
]
1160
Removed:
else [])
1161
Removed:
@ [
1162
Removed:
feedback_action_button ~action:"skip" ~label:"Skip this factor"
1163
Removed:
~secondary:true ();
1164
Removed:
]);
1165
Removed:
]
1166
Removed:
else
1167
Removed:
fields
1168
Removed:
@ [
1169
Removed:
tag "h3" [] [ txt "Anything else?" ];
1170
Removed:
tag "p" []
1171
Removed:
[ txt "Add an optional note about pain, injury, or preparation." ];
1172
Removed:
tag "div"
1173
Removed:
[ class_ "field" ]
1174
Removed:
[
1175
Removed:
feedback_flag "pain" "Pain";
1176
Removed:
feedback_flag "injury" "Injury";
1177
Removed:
feedback_flag "preparation" "Preparation was insufficient";
1178
Removed:
];
1179
Removed:
feedback_progress 5;
1180
Removed:
tag "div"
1181
Removed:
[ class_ "feedback-actions" ]
1182
Removed:
[
1183
Removed:
feedback_action_button ~action:"back"
1184
Removed:
~label:"Back to Perceived difficulty" ~secondary:true ();
1185
Removed:
feedback_action_button ~action:"save" ~label:"Submit feedback" ();
1186
Removed:
];
1187
Removed:
]
1188
Removed:
in
1189
Removed:
tag "form"
1190
Removed:
[
1191
Removed:
action Routes.feedback;
1192
Removed:
post_form;
1193
Removed:
class_ "feedback-form";
1194
Removed:
Dream_html.attr "data-hito-feedback-form";
1195
Removed:
Dream_html.attr "data-hito-app-form";
1196
Removed:
]
1197
Removed:
[ tag "fieldset" [] body ]
1198
Removed:
1199
Removed:
let feedback_modal request ~flow ~open_ =
1200
Removed:
tag "dialog"
1201
Removed:
([ class_ "feedback-modal"; Dream_html.attr "data-hito-feedback-modal" ]
1202
Removed:
@ if open_ then [ Dream_html.attr "open" ] else [])
1203
Removed:
[
1204
Removed:
tag "div"
1205
Removed:
[ class_ "feedback-modal-content" ]
1206
Removed:
[
1207
Removed:
tag "div"
1208
Removed:
[ class_ "feedback-modal-heading" ]
1209
Removed:
[
1210
Removed:
tag "h2" [] [ txt "How are you feeling?" ];
1211
Removed:
(* Close cancels the flow: it submits the cancel form, so the
1212
Removed:
partial progress is discarded rather than kept for later. *)
1213
Removed:
tag "form"
1214
Removed:
[
1215
Removed:
action Routes.cancel_feedback;
1216
Removed:
post_form;
1217
Removed:
class_ "feedback-close-form";
1218
Removed:
Dream_html.attr "data-hito-app-form";
1219
Removed:
]
1220
Removed:
[
1221
Removed:
Dream_html.csrf_tag request;
1222
Removed:
tag "button"
1223
Removed:
[
1224
Removed:
type_ "submit";
1225
Removed:
class_ "feedback-modal-close";
1226
Removed:
Dream_html.string_attr "aria-label" "Close feedback";
1227
Removed:
]
1228
Removed:
[ txt "×" ];
1229
Removed:
];
1230
Removed:
];
1231
Removed:
feedback_flow_form request flow;
1232
Removed:
];
1233
Removed:
]
1234
Removed:
1235
Removed:
(* A small inline-SVG line chart of leveled feedback over time, one polyline per
1236
Removed:
factor. No script and no dependency: the logbook records, and this only draws
1237
Removed:
what it already holds. Shown once at least two reports carry a leveled
1238
Removed:
factor, since a single point is not a trend. *)
1239
Removed:
let feedback_graph (reports : View_model.Feedback.report list) =
1240
Removed:
let chronological =
1241
Removed:
List.sort
1242
Removed:
(fun (a : View_model.Feedback.report) (b : View_model.Feedback.report) ->
1243
Removed:
Int.compare a.at_unix b.at_unix)
1244
Removed:
reports
1245
Removed:
in
1246
Removed:
let count = List.length chronological in
1247
Removed:
let score_of field (report : View_model.Feedback.report) =
1248
Removed:
match List.assoc_opt field report.factor_scores with
1249
Removed:
| Some s -> s
1250
Removed:
| None -> None
1251
Removed:
in
1252
Removed:
let series =
1253
Removed:
List.map
1254
Removed:
(fun (field, label) ->
1255
Removed:
( label,
1256
Removed:
List.mapi (fun i report -> (i, score_of field report)) chronological
1257
Removed:
))
1258
Removed:
feedback_factors
1259
Removed:
in
1260
Removed:
(* A factor charts only if it holds at least two scored points. *)
1261
Removed:
let plottable (_, points) =
1262
Removed:
List.length (List.filter (fun (_, s) -> Option.is_some s) points) >= 2
1263
Removed:
in
1264
Removed:
let series = List.filter plottable series in
1265
Removed:
if count < 2 || series = [] then []
1266
Removed:
else
1267
Removed:
let width = 480 and height = 180 in
1268
Removed:
let pad_left = 28 and pad_right = 12 and pad_top = 12 and pad_bottom = 24 in
1269
Removed:
let plot_w = width - pad_left - pad_right in
1270
Removed:
let plot_h = height - pad_top - pad_bottom in
1271
Removed:
let x_of i =
1272
Removed:
if count = 1 then pad_left + (plot_w / 2)
1273
Removed:
else pad_left + (i * plot_w / (count - 1))
1274
Removed:
in
1275
Removed:
(* Score 1..5 maps low-to-high, so 5 sits at the top. *)
1276
Removed:
let y_of score = pad_top + ((5 - score) * plot_h / 4) in
1277
Removed:
let polyline label points =
1278
Removed:
let coords =
1279
Removed:
List.filter_map
1280
Removed:
(fun (i, s) ->
1281
Removed:
Option.map (fun s -> Printf.sprintf "%d,%d" (x_of i) (y_of s)) s)
1282
Removed:
points
1283
Removed:
in
1284
Removed:
tag "polyline"
1285
Removed:
[
1286
Removed:
class_ "feedback-graph-line";
1287
Removed:
Dream_html.string_attr "points" "%s" (String.concat " " coords);
1288
Removed:
Dream_html.string_attr "fill" "none";
1289
Removed:
Dream_html.string_attr "data-factor" "%s" label;
1290
Removed:
]
1291
Removed:
[]
1292
Removed:
in
1293
Removed:
let axis =
1294
Removed:
List.map
1295
Removed:
(fun score ->
1296
Removed:
let y = y_of score in
1297
Removed:
tag "text"
1298
Removed:
[
1299
Removed:
class_ "feedback-graph-tick";
1300
Removed:
Dream_html.string_attr "x" "%d" (pad_left - 6);
1301
Removed:
Dream_html.string_attr "y" "%d" (y + 3);
1302
Removed:
Dream_html.string_attr "text-anchor" "end";
1303
Removed:
]
1304
Removed:
[ txt "%d" score ])
1305
Removed:
[ 1; 2; 3; 4; 5 ]
1306
Removed:
in
1307
Removed:
let legend =
1308
Removed:
tag "ul"
1309
Removed:
[ class_ "feedback-graph-legend" ]
1310
Removed:
(List.map
1311
Removed:
(fun (label, _) ->
1312
Removed:
tag "li"
1313
Removed:
[ Dream_html.string_attr "data-factor" "%s" label ]
1314
Removed:
[ txt "%s" label ])
1315
Removed:
series)
1316
Removed:
in
1317
Removed:
[
1318
Removed:
tag "figure"
1319
Removed:
[ class_ "feedback-graph" ]
1320
Removed:
[
1321
Removed:
tag "figcaption" [] [ txt "Feedback over time" ];
1322
Removed:
tag "svg"
1323
Removed:
[
1324
Removed:
class_ "feedback-graph-svg";
1325
Removed:
Dream_html.string_attr "viewBox" "0 0 %d %d" width height;
1326
Removed:
Dream_html.string_attr "role" "img";
1327
Removed:
Dream_html.string_attr "aria-label"
1328
Removed:
"Subjective feedback scores over time, one line per factor";
1329
Removed:
]
1330
Removed:
(axis
1331
Removed:
@ List.map (fun (label, points) -> polyline label points) series);
1332
Removed:
legend;
1333
Removed:
];
1334
Removed:
]
1335
Removed:
1336
Removed:
let feedback_list (reports : View_model.Feedback.report list) =
1337
Removed:
if reports = [] then []
1338
Removed:
else
1339
Removed:
[
1340
Removed:
tag "ul"
1341
Removed:
[ class_ "feedback-list" ]
1342
Removed:
(List.map
1343
Removed:
(fun (report : View_model.Feedback.report) ->
1344
Removed:
let signals = String.concat ", " report.signals in
1345
Removed:
tag "li" []
1346
Removed:
[ txt "%s" (if signals = "" then "No signals" else signals) ])
1347
Removed:
reports);
1348
Removed:
]
1349
Removed:
1350
Removed:
let logbook request ?(logging = false) ~viewer ?(feedback = [])
1351
Removed:
?(suggest_feedback = false) ?feedback_flow ?(feedback_open = false) records
1352
Removed:
=
1353
Removed:
let feedback_flow =
1354
Removed:
Option.value feedback_flow ~default:{ step = 0; answers = [] }
1355
Removed:
in
1356
Removed:
html_page ~viewer ~request ~active:"logbook" ~logging "Logbook"
1357
Removed:
[
1358
Removed:
tag "h1" [] [ txt "Logbook" ];
1359
Removed:
(if records = [] then tag "p" [] [ txt "Nothing logged yet." ]
1360
Removed:
else
1361
Removed:
tag "ul" []
1362
Removed:
(List.map
1363
Removed:
(fun (entry : View_model.Logbook_entry.t) ->
1364
Removed:
tag "li" []
1365
Removed:
[
1366
Removed:
tag "a"
1367
Removed:
[
1368
Removed:
href Routes.record entry.id;
1369
Removed:
Dream_html.attr "data-hito-app-link";
1370
Removed:
]
1371
Removed:
[
1372
Removed:
txt "%s — %d stimuli" entry.workout_name
1373
Removed:
entry.stimuli_count;
1374
Removed:
];
1375
Removed:
tag "p"
1376
Removed:
[ class_ "done" ]
1377
Removed:
[
1378
Removed:
txt "%s"
1379
Removed:
(if entry.complete then "complete" else "incomplete");
1380
Removed:
];
1381
Removed:
])
1382
Removed:
records));
1383
Removed:
tag "section"
1384
Removed:
[ class_ "feedback-section" ]
1385
Removed:
([
1386
Removed:
tag "h2" [] [ txt "How are you feeling?" ];
1387
Removed:
(if suggest_feedback then
1388
Removed:
tag "p"
1389
Removed:
[ class_ "eyebrow" ]
1390
Removed:
[ txt "Workout saved — add feedback while it is fresh." ]
1391
Removed:
else txt "");
1392
Removed:
tag "p" []
1393
Removed:
[
1394
Removed:
txt
1395
Removed:
"Report each factor in turn. Skip any factor or the full flow.";
1396
Removed:
];
1397
Removed:
tag "a"
1398
Removed:
[
1399
Removed:
class_ "feedback-toggle";
1400
Removed:
Dream_html.string_attr "href" "/logbook?feedback=start";
1401
Removed:
Dream_html.attr "data-hito-feedback-open";
1402
Removed:
]
1403
Removed:
[ txt "Record feedback" ];
1404
Removed:
feedback_modal request ~flow:feedback_flow ~open_:feedback_open;
1405
Removed:
]
1406
Removed:
@ feedback_graph feedback @ feedback_list feedback);
1407
Removed:
]
1408
Removed:
1409
Removed:
(* The authenticated application feedback page. It keeps writing and reviewing
1410
Removed:
feedback in separate tabs, while both tabs remain plain links for no-script
1411
Removed:
use and progressive enhancement. *)
1412
Removed:
let app_feedback_tab ~selected ~tab ~label =
1413
Removed:
let path =
1414
Removed:
match tab with
1415
Removed:
| `Write -> "/app-feedback?tab=write"
1416
Removed:
| `Submitted -> "/app-feedback?tab=submitted"
1417
Removed:
in
1418
Removed:
let attrs =
1419
Removed:
[
1420
Removed:
class_ "feedback-tab";
1421
Removed:
Dream_html.string_attr "href" "%s" path;
1422
Removed:
Dream_html.attr "data-hito-app-link";
1423
Removed:
]
1424
Removed:
in
1425
Removed:
let attrs =
1426
Removed:
if selected = tab then Dream_html.string_attr "aria-current" "page" :: attrs
1427
Removed:
else attrs
1428
Removed:
in
1429
Removed:
tag "a" attrs [ txt "%s" label ]
1430
Removed:
1431
Removed:
let app_feedback_form request ?error () =
1432
Removed:
let error_block =
1433
Removed:
match error with
1434
Removed:
| None -> []
1435
Removed:
| Some message ->
1436
Removed:
[
1437
Removed:
tag "p"
1438
Removed:
[ class_ "warn"; Dream_html.string_attr "role" "alert" ]
1439
Removed:
[ txt "%s" message ];
1440
Removed:
]
1441
Removed:
in
1442
Removed:
tag "form"
1443
Removed:
[
1444
Removed:
action Routes.submit_app_feedback;
1445
Removed:
post_form;
1446
Removed:
class_ "app-feedback-form";
1447
Removed:
Dream_html.attr "data-hito-app-form";
1448
Removed:
]
1449
Removed:
(Dream_html.csrf_tag request
1450
Removed:
:: tag "label"
1451
Removed:
[ Dream_html.string_attr "for" "app-feedback-message" ]
1452
Removed:
[ txt "Your feedback" ]
1453
Removed:
:: tag "textarea"
1454
Removed:
[
1455
Removed:
name "message";
1456
Removed:
id "app-feedback-message";
1457
Removed:
Dream_html.string_attr "rows" "8";
1458
Removed:
required;
1459
Removed:
Dream_html.string_attr "placeholder"
1460
Removed:
"Tell us what works well or what needs attention.";
1461
Removed:
]
1462
Removed:
[]
1463
Removed:
:: error_block
1464
Removed:
@ [ void "input" [ type_ "submit"; value "Submit feedback" ] ])
1465
Removed:
1466
Removed:
let app_feedback_vote_form request (report : View_model.App_feedback.t) =
1467
Removed:
if report.viewer_owns then
1468
Removed:
tag "p" [ class_ "app-feedback-own" ] [ txt "Your feedback" ]
1469
Removed:
else
1470
Removed:
let label =
1471
Removed:
if report.viewer_upvoted then "Upvoted"
1472
Removed:
else Printf.sprintf "Upvote (%d)" report.upvotes
1473
Removed:
in
1474
Removed:
let attrs =
1475
Removed:
[
1476
Removed:
action Routes.upvote_app_feedback report.id;
1477
Removed:
post_form;
1478
Removed:
class_ "app-feedback-vote";
1479
Removed:
Dream_html.attr "data-hito-app-form";
1480
Removed:
]
1481
Removed:
in
1482
Removed:
let attrs =
1483
Removed:
if report.viewer_upvoted then Dream_html.attr "disabled" :: attrs
1484
Removed:
else attrs
1485
Removed:
in
1486
Removed:
tag "form" attrs
1487
Removed:
[
1488
Removed:
Dream_html.csrf_tag request;
1489
Removed:
void "input"
1490
Removed:
[ type_ "submit"; Dream_html.string_attr "value" "%s" label ];
1491
Removed:
]
1492
Removed:
1493
Removed:
let app_feedback_edit_form request (report : View_model.App_feedback.t) =
1494
Removed:
tag "form"
1495
Removed:
[
1496
Removed:
action Routes.edit_app_feedback report.id;
1497
Removed:
post_form;
1498
Removed:
class_ "app-feedback-edit-form";
1499
Removed:
Dream_html.attr "data-hito-app-form";
1500
Removed:
]
1501
Removed:
[
1502
Removed:
Dream_html.csrf_tag request;
1503
Removed:
tag "label"
1504
Removed:
[ Dream_html.string_attr "for" "app-feedback-edit-%s" report.id ]
1505
Removed:
[ txt "Edit feedback" ];
1506
Removed:
tag "textarea"
1507
Removed:
[
1508
Removed:
name "message";
1509
Removed:
Dream_html.string_attr "id" "app-feedback-edit-%s" report.id;
1510
Removed:
Dream_html.string_attr "rows" "4";
1511
Removed:
required;
1512
Removed:
]
1513
Removed:
[ txt "%s" report.message ];
1514
Removed:
void "input" [ type_ "submit"; value "Save edit" ];
1515
Removed:
]
1516
Removed:
1517
Removed:
let app_feedback_remove_form request (report : View_model.App_feedback.t) =
1518
Removed:
tag "form"
1519
Removed:
[
1520
Removed:
action Routes.remove_app_feedback report.id;
1521
Removed:
post_form;
1522
Removed:
class_ "app-feedback-remove-form";
1523
Removed:
Dream_html.attr "data-hito-app-form";
1524
Removed:
]
1525
Removed:
[
1526
Removed:
Dream_html.csrf_tag request;
1527
Removed:
void "input" [ type_ "submit"; value "Remove feedback" ];
1528
Removed:
]
1529
Removed:
1530
Removed:
let app_feedback_actions request (report : View_model.App_feedback.t) =
1531
Removed:
if report.viewer_owns then
1532
Removed:
tag "div"
1533
Removed:
[ class_ "app-feedback-actions" ]
1534
Removed:
[
1535
Removed:
app_feedback_edit_form request report;
1536
Removed:
app_feedback_remove_form request report;
1537
Removed:
]
1538
Removed:
else
1539
Removed:
tag "div"
1540
Removed:
[ class_ "app-feedback-actions" ]
1541
Removed:
[ app_feedback_vote_form request report ]
1542
Removed:
1543
Removed:
let app_feedback request ?(logging = false) ~viewer ?(tab = `Write) ?error
1544
Removed:
reports =
1545
Removed:
let panel =
1546
Removed:
match tab with
1547
Removed:
| `Write ->
1548
Removed:
[
1549
Removed:
tag "h2" [] [ txt "Write feedback" ];
1550
Removed:
tag "p" []
1551
Removed:
[ txt "Tell us about your experience using the application." ];
1552
Removed:
app_feedback_form request ?error ();
1553
Removed:
]
1554
Removed:
| `Submitted ->
1555
Removed:
[
1556
Removed:
tag "h2" [] [ txt "Submitted feedback" ];
1557
Removed:
(if reports = [] then
1558
Removed:
tag "p" []
1559
Removed:
[ txt "You have not submitted any application feedback." ]
1560
Removed:
else
1561
Removed:
tag "ul"
1562
Removed:
[ class_ "app-feedback-list" ]
1563
Removed:
(List.map
1564
Removed:
(fun (report : View_model.App_feedback.t) ->
1565
Removed:
tag "li" []
1566
Removed:
[
1567
Removed:
tag "p"
1568
Removed:
[ class_ "app-feedback-author" ]
1569
Removed:
[
1570
Removed:
txt "%s — %d contributions" report.author
1571
Removed:
report.contributions;
1572
Removed:
];
1573
Removed:
tag "time"
1574
Removed:
[
1575
Removed:
class_ "app-feedback-time";
1576
Removed:
Dream_html.string_attr "datetime" "%s"
1577
Removed:
report.submitted_at;
1578
Removed:
]
1579
Removed:
[ txt "%s" report.submitted_at ];
1580
Removed:
tag "p"
1581
Removed:
[ class_ "app-feedback-message" ]
1582
Removed:
[ txt "%s" report.message ];
1583
Removed:
tag "p"
1584
Removed:
[ class_ "app-feedback-upvotes" ]
1585
Removed:
[ txt "Upvotes: %d" report.upvotes ];
1586
Removed:
app_feedback_actions request report;
1587
Removed:
])
1588
Removed:
reports));
1589
Removed:
]
1590
Removed:
in
1591
Removed:
html_page ~viewer ~request ~active:"app-feedback" ~logging "App feedback"
1592
Removed:
[
1593
Removed:
tag "div" []
1594
Removed:
([
1595
Removed:
tag "h1" [] [ txt "App feedback" ];
1596
Removed:
tag "p" [] [ txt "Help us improve hito by sharing your experience." ];
1597
Removed:
tag "nav"
1598
Removed:
[
1599
Removed:
class_ "feedback-tabs";
1600
Removed:
Dream_html.string_attr "aria-label" "App feedback tabs";
1601
Removed:
]
1602
Removed:
[
1603
Removed:
app_feedback_tab ~selected:tab ~tab:`Write
1604
Removed:
~label:"Write feedback";
1605
Removed:
app_feedback_tab ~selected:tab ~tab:`Submitted
1606
Removed:
~label:"Submitted feedback";
1607
Removed:
];
1608
Removed:
]
1609
Removed:
@ panel);
1610
Removed:
]
1611
Removed:
1612
Removed:
(* The account profile page. It names the signed-in trainee and offers two
1613
Removed:
independent forms: rename the account, and change the password. Each form
1614
Removed:
reports its own error inline. [notice] confirms a successful change. Both are
1615
Removed:
plain authenticated app-forms, so they work with or without script. *)
1616
Removed:
let profile request ?(logging = false) ~viewer ?username_error ?password_error
1617
Removed:
?notice () =
1618
Removed:
let notice_block =
1619
Removed:
match notice with
1620
Removed:
| Some message -> [ tag "p" [ class_ "notice" ] [ txt "%s" message ] ]
1621
Removed:
| None -> []
1622
Removed:
in
1623
Removed:
let error_block = function
1624
Removed:
| Some message ->
1625
Removed:
[
1626
Removed:
tag "p"
1627
Removed:
[ class_ "warn"; Dream_html.string_attr "role" "alert" ]
1628
Removed:
[ txt "%s" message ];
1629
Removed:
]
1630
Removed:
| None -> []
1631
Removed:
in
1632
Removed:
html_page ~viewer ~request ~active:"profile" ~logging "Profile"
1633
Removed:
([ tag "h1" [] [ txt "Profile" ] ]
1634
Removed:
@ notice_block
1635
Removed:
@ [
1636
Removed:
(* Username: show the current value and a button that opens the edit
1637
Removed:
modal. The modal holds the rename form. A no-script client, or an
1638
Removed:
error re-render, opens the dialog through the [open] attribute so the
1639
Removed:
form stays reachable and its inline error shows. *)
1640
Removed:
tag "section"
1641
Removed:
[ class_ "profile-section" ]
1642
Removed:
[
1643
Removed:
tag "h2" [] [ txt "Username" ];
1644
Removed:
tag "div"
1645
Removed:
[ class_ "profile-value" ]
1646
Removed:
[
1647
Removed:
tag "span" [ class_ "profile-value-label" ] [ txt "Username" ];
1648
Removed:
tag "span"
1649
Removed:
[ class_ "profile-value-text" ]
1650
Removed:
[ txt "%s" viewer.View_model.Viewer.username ];
1651
Removed:
tag "button"
1652
Removed:
[
1653
Removed:
type_ "button";
1654
Removed:
class_ "profile-edit";
1655
Removed:
Dream_html.string_attr "data-hito-dialog-open"
1656
Removed:
"username-dialog";
1657
Removed:
]
1658
Removed:
[ txt "Change username" ];
1659
Removed:
];
1660
Removed:
tag "dialog"
1661
Removed:
([
1662
Removed:
Dream_html.string_attr "id" "username-dialog";
1663
Removed:
class_ "profile-dialog";
1664
Removed:
]
1665
Removed:
@
1666
Removed:
if Option.is_some username_error then [ Dream_html.attr "open" ]
1667
Removed:
else [])
1668
Removed:
[
1669
Removed:
tag "div"
1670
Removed:
[ class_ "profile-dialog-content" ]
1671
Removed:
([
1672
Removed:
tag "h3" [] [ txt "Change username" ];
1673
Removed:
tag "form"
1674
Removed:
[
1675
Removed:
action Routes.profile_username;
1676
Removed:
post_form;
1677
Removed:
class_ "profile-form";
1678
Removed:
Dream_html.attr "data-hito-app-form";
1679
Removed:
]
1680
Removed:
[
1681
Removed:
Dream_html.csrf_tag request;
1682
Removed:
tag "div"
1683
Removed:
[ class_ "field" ]
1684
Removed:
[
1685
Removed:
tag "label"
1686
Removed:
[ Dream_html.string_attr "for" "username" ]
1687
Removed:
[ txt "Username" ];
1688
Removed:
void "input"
1689
Removed:
[
1690
Removed:
type_ "text";
1691
Removed:
name "username";
1692
Removed:
Dream_html.string_attr "id" "username";
1693
Removed:
Dream_html.string_attr "value" "%s"
1694
Removed:
viewer.View_model.Viewer.username;
1695
Removed:
required;
1696
Removed:
];
1697
Removed:
];
1698
Removed:
tag "div"
1699
Removed:
[ class_ "profile-dialog-actions" ]
1700
Removed:
[
1701
Removed:
tag "button"
1702
Removed:
[
1703
Removed:
type_ "button";
1704
Removed:
class_ "secondary";
1705
Removed:
Dream_html.attr "data-hito-dialog-close";
1706
Removed:
]
1707
Removed:
[ txt "Cancel" ];
1708
Removed:
void "input"
1709
Removed:
[ type_ "submit"; value "Change username" ];
1710
Removed:
];
1711
Removed:
];
1712
Removed:
]
1713
Removed:
@ error_block username_error);
1714
Removed:
];
1715
Removed:
];
1716
Removed:
(* Password: a button opens the change-password modal. *)
1717
Removed:
tag "section"
1718
Removed:
[ class_ "profile-section" ]
1719
Removed:
[
1720
Removed:
tag "h2" [] [ txt "Password" ];
1721
Removed:
tag "div"
1722
Removed:
[ class_ "profile-value" ]
1723
Removed:
[
1724
Removed:
tag "span" [ class_ "profile-value-label" ] [ txt "Password" ];
1725
Removed:
tag "span" [ class_ "profile-value-text" ] [ txt "••••••••" ];
1726
Removed:
tag "button"
1727
Removed:
[
1728
Removed:
type_ "button";
1729
Removed:
class_ "profile-edit";
1730
Removed:
Dream_html.string_attr "data-hito-dialog-open"
1731
Removed:
"password-dialog";
1732
Removed:
]
1733
Removed:
[ txt "Change password" ];
1734
Removed:
];
1735
Removed:
tag "dialog"
1736
Removed:
([
1737
Removed:
Dream_html.string_attr "id" "password-dialog";
1738
Removed:
class_ "profile-dialog";
1739
Removed:
]
1740
Removed:
@
1741
Removed:
if Option.is_some password_error then [ Dream_html.attr "open" ]
1742
Removed:
else [])
1743
Removed:
[
1744
Removed:
tag "div"
1745
Removed:
[ class_ "profile-dialog-content" ]
1746
Removed:
([
1747
Removed:
tag "h3" [] [ txt "Change password" ];
1748
Removed:
tag "form"
1749
Removed:
[
1750
Removed:
action Routes.profile_password;
1751
Removed:
post_form;
1752
Removed:
class_ "profile-form";
1753
Removed:
Dream_html.attr "data-hito-app-form";
1754
Removed:
]
1755
Removed:
[
1756
Removed:
Dream_html.csrf_tag request;
1757
Removed:
tag "div"
1758
Removed:
[ class_ "field" ]
1759
Removed:
[
1760
Removed:
tag "label"
1761
Removed:
[ Dream_html.string_attr "for" "current" ]
1762
Removed:
[ txt "Current password" ];
1763
Removed:
void "input"
1764
Removed:
[
1765
Removed:
type_ "password";
1766
Removed:
name "current";
1767
Removed:
Dream_html.string_attr "id" "current";
1768
Removed:
required;
1769
Removed:
];
1770
Removed:
];
1771
Removed:
tag "div"
1772
Removed:
[ class_ "field" ]
1773
Removed:
[
1774
Removed:
tag "label"
1775
Removed:
[ Dream_html.string_attr "for" "next" ]
1776
Removed:
[ txt "New password" ];
1777
Removed:
void "input"
1778
Removed:
[
1779
Removed:
type_ "password";
1780
Removed:
name "next";
1781
Removed:
Dream_html.string_attr "id" "next";
1782
Removed:
required;
1783
Removed:
];
1784
Removed:
];
1785
Removed:
tag "div"
1786
Removed:
[ class_ "profile-dialog-actions" ]
1787
Removed:
[
1788
Removed:
tag "button"
1789
Removed:
[
1790
Removed:
type_ "button";
1791
Removed:
class_ "secondary";
1792
Removed:
Dream_html.attr "data-hito-dialog-close";
1793
Removed:
]
1794
Removed:
[ txt "Cancel" ];
1795
Removed:
void "input"
1796
Removed:
[ type_ "submit"; value "Change password" ];
1797
Removed:
];
1798
Removed:
];
1799
Removed:
]
1800
Removed:
@ error_block password_error);
1801
Removed:
];
1802
Removed:
];
1803
Removed:
(* Import: a multipart form uploads a native Hevy CSV export. The file
1804
Removed:
field is [csv], the wall-clock offset is [utc_offset_seconds], and an
1805
Removed:
optional checkbox overrides duplicate-source rejection. The form is a
1806
Removed:
plain multipart POST with its CSRF token, so it needs no script. *)
1807
Removed:
tag "section"
1808
Removed:
[ class_ "profile-section" ]
1809
Removed:
[
1810
Removed:
tag "h2" [] [ txt "Import" ];
1811
Removed:
tag "form"
1812
Removed:
[
1813
Removed:
action Routes.import_upload;
1814
Removed:
post_form;
1815
Removed:
Dream_html.string_attr "enctype" "multipart/form-data";
1816
Removed:
class_ "profile-form";
1817
Removed:
]
1818
Removed:
[
1819
Removed:
Dream_html.csrf_tag request;
1820
Removed:
tag "div"
1821
Removed:
[ class_ "field" ]
1822
Removed:
[
1823
Removed:
tag "label"
1824
Removed:
[ Dream_html.string_attr "for" "csv" ]
1825
Removed:
[ txt "Hevy CSV export" ];
1826
Removed:
void "input"
1827
Removed:
[
1828
Removed:
type_ "file";
1829
Removed:
name "csv";
1830
Removed:
Dream_html.string_attr "id" "csv";
1831
Removed:
Dream_html.string_attr "accept" ".csv,text/csv";
1832
Removed:
required;
1833
Removed:
];
1834
Removed:
];
1835
Removed:
tag "div"
1836
Removed:
[ class_ "field" ]
1837
Removed:
[
1838
Removed:
tag "label"
1839
Removed:
[ Dream_html.string_attr "for" "utc_offset_seconds" ]
1840
Removed:
[ txt "UTC offset in seconds" ];
1841
Removed:
void "input"
1842
Removed:
[
1843
Removed:
type_ "number";
1844
Removed:
name "utc_offset_seconds";
1845
Removed:
Dream_html.string_attr "id" "utc_offset_seconds";
1846
Removed:
Dream_html.string_attr "value" "0";
1847
Removed:
required;
1848
Removed:
];
1849
Removed:
];
1850
Removed:
tag "div"
1851
Removed:
[ class_ "field" ]
1852
Removed:
[
1853
Removed:
void "input"
1854
Removed:
[
1855
Removed:
type_ "checkbox";
1856
Removed:
name "allow_duplicate";
1857
Removed:
Dream_html.string_attr "id" "allow_duplicate";
1858
Removed:
value "true";
1859
Removed:
];
1860
Removed:
tag "label"
1861
Removed:
[ Dream_html.string_attr "for" "allow_duplicate" ]
1862
Removed:
[ txt "Import even if this export was imported before" ];
1863
Removed:
];
1864
Removed:
void "input" [ type_ "submit"; value "Upload import" ];
1865
Removed:
];
1866
Removed:
];
1867
Removed:
])
1868
Removed:
1869
Removed:
(* The settings page. It currently stores the user's light or dark theme choice
1870
Removed:
in the session. *)
1871
Removed:
let settings request ?(logging = false) ~viewer ~theme () =
1872
Removed:
let next_theme, label =
1873
Removed:
match theme with
1874
Removed:
| "dark" -> ("light", "Use light theme")
1875
Removed:
| _ -> ("dark", "Use dark theme")
1876
Removed:
in
1877
Removed:
html_page ~viewer ~request ~active:"settings" ~logging "Settings"
1878
Removed:
[
1879
Removed:
tag "h1" [] [ txt "Settings" ];
1880
Removed:
tag "section"
1881
Removed:
[ class_ "settings-section" ]
1882
Removed:
[
1883
Removed:
tag "h2" [] [ txt "Theme" ];
1884
Removed:
tag "p" [] [ txt "Choose a light or dark theme for this session." ];
1885
Removed:
tag "form"
1886
Removed:
[ action Routes.settings_theme; post_form; class_ "settings-form" ]
1887
Removed:
[
1888
Removed:
Dream_html.csrf_tag request;
1889
Removed:
void "input"
1890
Removed:
[
1891
Removed:
type_ "hidden";
1892
Removed:
name "theme";
1893
Removed:
Dream_html.string_attr "value" "%s" next_theme;
1894
Removed:
];
1895
Removed:
void "input"
1896
Removed:
[ type_ "submit"; Dream_html.string_attr "value" "%s" label ];
1897
Removed:
];
1898
Removed:
];
1899
Removed:
]
1900
Removed:
1901
Removed:
(* The review of a provisional import batch. It renders the parse warnings, then
1902
Removed:
each workout with its sets, blockers, and forms. Every form is a plain POST
1903
Removed:
carrying its CSRF token, so cleaning and promotion need no script. The page
1904
Removed:
names only the view model: the controller has already phrased set types,
1905
Removed:
blockers, and warnings, so no core entity crosses the boundary here. *)
1906
Removed:
let import_review request ~viewer (vm : View_model.Import_review.t) =
1907
Removed:
let warnings_block =
1908
Removed:
match vm.View_model.Import_review.warnings with
1909
Removed:
| [] -> []
1910
Removed:
| warnings ->
1911
Removed:
[
1912
Removed:
tag "section"
1913
Removed:
[ class_ "import-warnings" ]
1914
Removed:
[
1915
Removed:
tag "h2" [] [ txt "Dropped rows" ];
1916
Removed:
tag "ul" []
1917
Removed:
(List.map
1918
Removed:
(fun (w : View_model.Import_warning.t) ->
1919
Removed:
tag "li" []
1920
Removed:
[
1921
Removed:
txt "Row %d: %s" w.View_model.Import_warning.row
1922
Removed:
w.View_model.Import_warning.detail;
1923
Removed:
])
1924
Removed:
warnings);
1925
Removed:
];
1926
Removed:
]
1927
Removed:
in
1928
Removed:
(* One row per set. A mapped set shows the catalog name it resolves to. *)
1929
Removed:
let set_row (s : View_model.Import_set.t) =
1930
Removed:
let mapping =
1931
Removed:
match s.View_model.Import_set.mapping with
1932
Removed:
| Some name -> Printf.sprintf " → %s" name
1933
Removed:
| None -> " → unmapped"
1934
Removed:
in
1935
Removed:
tag "li" []
1936
Removed:
[
1937
Removed:
txt "Row %d: %s (%s) %s kg × %s reps%s" s.View_model.Import_set.row
1938
Removed:
s.View_model.Import_set.exercise_name s.View_model.Import_set.set_type
1939
Removed:
s.View_model.Import_set.weight s.View_model.Import_set.reps mapping;
1940
Removed:
]
1941
Removed:
in
1942
Removed:
(* One mapping form per distinct source name in the workout. The source name
1943
Removed:
is fixed by a hidden field; the trainee supplies the catalog exercise id. *)
1944
Removed:
let mapping_form batch_id workout_id source_name =
1945
Removed:
tag "form"
1946
Removed:
[
1947
Removed:
action Routes.import_map batch_id workout_id;
1948
Removed:
post_form;
1949
Removed:
class_ "import-map-form";
1950
Removed:
]
1951
Removed:
[
1952
Removed:
Dream_html.csrf_tag request;
1953
Removed:
void "input"
1954
Removed:
[
1955
Removed:
type_ "hidden";
1956
Removed:
name "source_name";
1957
Removed:
Dream_html.string_attr "value" "%s" source_name;
1958
Removed:
];
1959
Removed:
tag "div"
1960
Removed:
[ class_ "field" ]
1961
Removed:
[
1962
Removed:
tag "label" [] [ txt "Map %s to exercise id" source_name ];
1963
Removed:
void "input" [ type_ "text"; name "exercise_id"; required ];
1964
Removed:
];
1965
Removed:
void "input" [ type_ "submit"; value "Map exercise" ];
1966
Removed:
]
1967
Removed:
in
1968
Removed:
let confirm_form batch_id workout_id acknowledgement =
1969
Removed:
tag "form"
1970
Removed:
[
1971
Removed:
action Routes.import_confirm batch_id workout_id;
1972
Removed:
post_form;
1973
Removed:
class_ "import-confirm-form";
1974
Removed:
]
1975
Removed:
[
1976
Removed:
Dream_html.csrf_tag request;
1977
Removed:
tag "div"
1978
Removed:
[ class_ "field" ]
1979
Removed:
[
1980
Removed:
tag "label" [] [ txt "Confirm normal sets" ];
1981
Removed:
void "input"
1982
Removed:
([ type_ "text"; name "acknowledgement"; required ]
1983
Removed:
@
1984
Removed:
match acknowledgement with
1985
Removed:
| Some ack -> [ Dream_html.string_attr "value" "%s" ack ]
1986
Removed:
| None -> []);
1987
Removed:
];
1988
Removed:
void "input" [ type_ "submit"; value "Confirm normal sets" ];
1989
Removed:
]
1990
Removed:
in
1991
Removed:
let promote_workout_form batch_id workout_id =
1992
Removed:
tag "form"
1993
Removed:
[
1994
Removed:
action Routes.import_promote_workout batch_id workout_id;
1995
Removed:
post_form;
1996
Removed:
class_ "import-promote-workout-form";
1997
Removed:
]
1998
Removed:
[
1999
Removed:
Dream_html.csrf_tag request;
2000
Removed:
void "input" [ type_ "submit"; value "Promote workout" ];
2001
Removed:
]
2002
Removed:
in
2003
Removed:
let blockers_block (w : View_model.Import_workout.t) =
2004
Removed:
match w.View_model.Import_workout.blockers with
2005
Removed:
| [] -> [ tag "p" [ class_ "import-ready" ] [ txt "Ready to promote." ] ]
2006
Removed:
| blockers ->
2007
Removed:
[
2008
Removed:
tag "p" [] [ txt "Blocked:" ];
2009
Removed:
tag "ul"
2010
Removed:
[ class_ "import-blockers" ]
2011
Removed:
(List.map (fun b -> tag "li" [] [ txt "%s" b ]) blockers);
2012
Removed:
]
2013
Removed:
in
2014
Removed:
let workout_section (w : View_model.Import_workout.t) =
2015
Removed:
let batch_id = vm.View_model.Import_review.batch_id in
2016
Removed:
let workout_id = w.View_model.Import_workout.id in
2017
Removed:
(* Every distinct source name, in first-appearance order. *)
2018
Removed:
let source_names =
2019
Removed:
List.fold_left
2020
Removed:
(fun acc (s : View_model.Import_set.t) ->
2021
Removed:
let n = s.View_model.Import_set.exercise_name in
2022
Removed:
if List.mem n acc then acc else acc @ [ n ])
2023
Removed:
[] w.View_model.Import_workout.sets
2024
Removed:
in
2025
Removed:
tag "section"
2026
Removed:
[ class_ "import-workout" ]
2027
Removed:
([
2028
Removed:
tag "h2" [] [ txt "%s" w.View_model.Import_workout.title ];
2029
Removed:
tag "ul"
2030
Removed:
[ class_ "import-sets" ]
2031
Removed:
(List.map set_row w.View_model.Import_workout.sets);
2032
Removed:
]
2033
Removed:
@ blockers_block w
2034
Removed:
@ List.map (mapping_form batch_id workout_id) source_names
2035
Removed:
@ [
2036
Removed:
confirm_form batch_id workout_id
2037
Removed:
w.View_model.Import_workout.acknowledgement;
2038
Removed:
promote_workout_form batch_id workout_id;
2039
Removed:
])
2040
Removed:
in
2041
Removed:
let batch_promote_form =
2042
Removed:
tag "form"
2043
Removed:
[
2044
Removed:
action Routes.import_promote_batch vm.View_model.Import_review.batch_id;
2045
Removed:
post_form;
2046
Removed:
class_ "import-promote-batch-form";
2047
Removed:
]
2048
Removed:
[
2049
Removed:
Dream_html.csrf_tag request;
2050
Removed:
void "input" [ type_ "submit"; value "Promote whole batch" ];
2051
Removed:
]
2052
Removed:
in
2053
Removed:
html_page ~viewer ~request ~active:"profile" "Import review"
2054
Removed:
([ tag "h1" [] [ txt "Import review" ] ]
2055
Removed:
@ warnings_block
2056
Removed:
@ List.map workout_section vm.View_model.Import_review.workouts
2057
Removed:
@ [ batch_promote_form ])
lib/web/pages.mli
@@ -1,124 +0,0 @@
1
Removed:
type page = Dream_html.node
2
Removed:
3
Removed:
val login :
4
Removed:
Dream.request -> ?error:string -> ?registration_open:bool -> unit -> page
5
Removed:
(** [registration_open] shows the "Register" link. Defaults to [false]. *)
6
Removed:
7
Removed:
val register : Dream.request -> ?error:string -> unit -> page
8
Removed:
9
Removed:
val choose_routine :
10
Removed:
Dream.request ->
11
Removed:
logging:bool ->
12
Removed:
viewer:View_model.Viewer.t ->
13
Removed:
routines:View_model.Routine_choice.t list ->
14
Removed:
page
15
Removed:
16
Removed:
val home :
17
Removed:
Dream.request -> viewer:View_model.Viewer.t -> home:View_model.Home.t -> page
18
Removed:
19
Removed:
val workout_in_progress :
20
Removed:
Dream.request -> viewer:View_model.Viewer.t -> workout_name:string -> page
21
Removed:
(** The Home card shown while a workout is in progress. Links to the current
22
Removed:
workout so it stays one tap away. *)
23
Removed:
24
Removed:
val routine :
25
Removed:
Dream.request ->
26
Removed:
?logging:bool ->
27
Removed:
viewer:View_model.Viewer.t ->
28
Removed:
View_model.Routine.t ->
29
Removed:
page
30
Removed:
31
Removed:
val workout :
32
Removed:
Dream.request ->
33
Removed:
viewer:View_model.Viewer.t ->
34
Removed:
?errors:(string * string) list ->
35
Removed:
?editing:int ->
36
Removed:
?logging:bool ->
37
Removed:
record_id:string option ->
38
Removed:
active_slot:int ->
39
Removed:
View_model.Workout.t ->
40
Removed:
page
41
Removed:
(** Renders one prescribed slot at a time under a dropdown that selects the
42
Removed:
exercise. Authenticated pages carry generic progressive-enhancement markers.
43
Removed:
Current workouts additionally mark slot links and forms, while saved-record
44
Removed:
edits use the generic authenticated form path. [active_slot] is the slot the
45
Removed:
view opens on. A handler clamps a requested slot and falls back to the first
46
Removed:
outstanding slot. [record_id] is [None] for the workout in progress and
47
Removed:
[Some id] for a saved history record. [logging] sets the middle navigation
48
Removed:
action. It defaults to a live workout view (true when [record_id] is
49
Removed:
[None]). *)
50
Removed:
51
Removed:
type feedback_flow = { step : int; answers : (string * string) list }
52
Removed:
53
Removed:
val feedback_factors : (string * string) list
54
Removed:
(** Ordered factor codes and labels used by the sequential feedback flow. *)
55
Removed:
56
Removed:
val logbook :
57
Removed:
Dream.request ->
58
Removed:
?logging:bool ->
59
Removed:
viewer:View_model.Viewer.t ->
60
Removed:
?feedback:View_model.Feedback.report list ->
61
Removed:
?suggest_feedback:bool ->
62
Removed:
?feedback_flow:feedback_flow ->
63
Removed:
?feedback_open:bool ->
64
Removed:
View_model.Logbook_entry.t list ->
65
Removed:
page
66
Removed:
(** The logbook: recorded workouts and a modal sequential subjective feedback
67
Removed:
flow. [feedback] lists past reports, most recent first. [suggest_feedback]
68
Removed:
shows a prompt inviting feedback, set after a workout finishes. *)
69
Removed:
70
Removed:
val profile :
71
Removed:
Dream.request ->
72
Removed:
?logging:bool ->
73
Removed:
viewer:View_model.Viewer.t ->
74
Removed:
?username_error:string ->
75
Removed:
?password_error:string ->
76
Removed:
?notice:string ->
77
Removed:
unit ->
78
Removed:
page
79
Removed:
(** The account profile page: rename the account and change the password, each
80
Removed:
an independent form. [username_error] and [password_error] report a refused
81
Removed:
change inline. [notice] confirms a successful one. *)
82
Removed:
83
Removed:
val settings :
84
Removed:
Dream.request ->
85
Removed:
?logging:bool ->
86
Removed:
viewer:View_model.Viewer.t ->
87
Removed:
theme:string ->
88
Removed:
unit ->
89
Removed:
page
90
Removed:
(** The session-scoped theme settings page. *)
91
Removed:
92
Removed:
val import_review :
93
Removed:
Dream.request ->
94
Removed:
viewer:View_model.Viewer.t ->
95
Removed:
View_model.Import_review.t ->
96
Removed:
page
97
Removed:
(** The review of a provisional Hevy import batch. Lists the rows dropped on
98
Removed:
parse, then each workout with its sets, blockers, and acknowledgement state.
99
Removed:
Renders a mapping form per distinct source name, a normal-set confirmation
100
Removed:
form, a per-workout promote form, and a batch promote form. Basic accessible
101
Removed:
HTML: it names no core entity, only the view model. *)
102
Removed:
103
Removed:
val problem : title:string -> detail:string -> page
104
Removed:
105
Removed:
val error_page :
106
Removed:
?viewer:View_model.Viewer.t ->
107
Removed:
?request:Dream.request ->
108
Removed:
status:int ->
109
Removed:
unit ->
110
Removed:
page
111
Removed:
(** A branded page for an HTTP failure. It names only the class of failure, so
112
Removed:
no server-supplied string leaks, and keeps the navigation affordances: top
113
Removed:
on desktop, bottom on mobile. *)
114
Removed:
115
Removed:
val app_feedback :
116
Removed:
Dream.request ->
117
Removed:
?logging:bool ->
118
Removed:
viewer:View_model.Viewer.t ->
119
Removed:
?tab:[ `Write | `Submitted ] ->
120
Removed:
?error:string ->
121
Removed:
View_model.App_feedback.t list ->
122
Removed:
page
123
Removed:
(** The authenticated application feedback page. One tab accepts a freeform
124
Removed:
message. The other lists the trainee's submitted messages. *)
lib/web/routes.ml
@@ -1,39 +0,0 @@
1
Removed:
(* The path values carry format types that are inferred and checked at each use.
2
Removed:
Writing an interface for them would add noise without adding safety. *)
3
Removed:
4
Removed:
let%path home = "/"
5
Removed:
let%path register = "/register"
6
Removed:
let%path login = "/login"
7
Removed:
let%path logout = "/logout"
8
Removed:
let%path routines = "/routines"
9
Removed:
let%path select_routine = "/routines/%s/select"
10
Removed:
let%path routine = "/routine"
11
Removed:
let%path workout = "/workout"
12
Removed:
let%path workout_slot = "/workout/slots/%d"
13
Removed:
let%path workout_slot_edit = "/workout/slots/%d/edit"
14
Removed:
let%path finish_workout = "/workout/finish"
15
Removed:
let%path cancel_workout = "/workout/cancel"
16
Removed:
let%path logbook = "/logbook"
17
Removed:
let%path profile = "/profile"
18
Removed:
let%path settings = "/settings"
19
Removed:
let%path settings_theme = "/settings/theme"
20
Removed:
let%path profile_username = "/profile/username"
21
Removed:
let%path profile_password = "/profile/password"
22
Removed:
let%path import_upload = "/profile/import"
23
Removed:
let%path import_review = "/profile/import/%s"
24
Removed:
let%path import_map = "/profile/import/%s/workouts/%s/map"
25
Removed:
let%path import_confirm = "/profile/import/%s/workouts/%s/confirm"
26
Removed:
let%path import_promote_workout = "/profile/import/%s/workouts/%s/promote"
27
Removed:
let%path import_promote_batch = "/profile/import/%s/promote"
28
Removed:
let%path app_feedback = "/app-feedback"
29
Removed:
let%path submit_app_feedback = "/app-feedback/submit"
30
Removed:
let%path upvote_app_feedback = "/app-feedback/%s/upvote"
31
Removed:
let%path edit_app_feedback = "/app-feedback/%s/edit"
32
Removed:
let%path remove_app_feedback = "/app-feedback/%s/remove"
33
Removed:
let%path feedback = "/feedback"
34
Removed:
let%path cancel_feedback = "/feedback/cancel"
35
Removed:
let%path record = "/logbook/%s"
36
Removed:
let%path record_slot = "/logbook/%s/slots/%d"
37
Removed:
let%path record_slot_edit = "/logbook/%s/slots/%d/edit"
38
Removed:
let%path stylesheet = "/assets/hito.css"
39
Removed:
let%path workout_client = "/assets/workout-client.js"
lib/web/view_model.ml
@@ -1,183 +0,0 @@
1
Removed:
(** View models: the simple data structures the presenter renders.
2
Removed:
3
Removed:
The Clean Architecture rule for crossing a boundary is that the data passed
4
Removed:
is a plain structure, never an entity or a database row. These DTOs are that
5
Removed:
structure. A handler (the controller) maps a repository record or a core
6
Removed:
entity into a view model, and the presenter ({!Pages}) renders the view
7
Removed:
model without ever naming an inner-circle type.
8
Removed:
9
Removed:
Every page now crosses the boundary as a DTO. No page names a core entity.
10
Removed:
Add a page DTO here before the presenter renders it. *)
11
Removed:
12
Removed:
module Viewer = struct
13
Removed:
type t = { username : string }
14
Removed:
(** The signed-in trainee, as the shell needs them: a name to show, nothing
15
Removed:
more. Replaces passing the [Trainee.t] entity into the presenter. *)
16
Removed:
end
17
Removed:
18
Removed:
module App_feedback = struct
19
Removed:
type t = {
20
Removed:
id : string; (** Opaque feedback identity, already stringified. *)
21
Removed:
author : string;
22
Removed:
contributions : int;
23
Removed:
submitted_at : string; (** Pre-formatted "YYYY-MM-DD HH:MM" in UTC. *)
24
Removed:
message : string;
25
Removed:
upvotes : int;
26
Removed:
viewer_upvoted : bool;
27
Removed:
viewer_owns : bool;
28
Removed:
}
29
Removed:
(** One application-feedback entry, ready to render. Carries no timestamp
30
Removed:
type, no opaque id, no repository record — only what the view shows. *)
31
Removed:
end
32
Removed:
33
Removed:
module Feedback = struct
34
Removed:
type report = {
35
Removed:
at_unix : int; (** Report time in Unix seconds, for chronological order. *)
36
Removed:
signals : string list; (** Each signal already described, for the list. *)
37
Removed:
factor_scores : (string * int option) list;
38
Removed:
(** Score per leveled factor field, aligned with the factor order the
39
Removed:
presenter graphs. [None] where the report gave that factor no level.
40
Removed:
Flags such as pain carry no score and are absent here. *)
41
Removed:
}
42
Removed:
(** One subjective-feedback report, ready to render. Carries no
43
Removed:
[Evidence.Feedback] variant and no timestamp type. *)
44
Removed:
end
45
Removed:
46
Removed:
module Routine = struct
47
Removed:
type exercise = { name : string; reps : string }
48
Removed:
(** One prescribed exercise line, its name and rep band already phrased. *)
49
Removed:
50
Removed:
type workout = { name : string; exercises : exercise list }
51
Removed:
52
Removed:
type t = { name : string; workouts : workout list }
53
Removed:
(** A routine as the accordion descriptions render it: names, numeric rep
54
Removed:
bands, and workout grouping only. No [Prescription] entity. *)
55
Removed:
end
56
Removed:
57
Removed:
module Routine_choice = struct
58
Removed:
type t = { id : string; name : string; workout_count : int }
59
Removed:
(** One selectable routine on the chooser: its id for the select form, its
60
Removed:
name, and how many workouts the cycle holds. *)
61
Removed:
end
62
Removed:
63
Removed:
module Home = struct
64
Removed:
type gate =
65
Removed:
| Ready
66
Removed:
| Recovering of { status : string }
67
Removed:
(** Whether the next workout may begin, and the recovery status line to
68
Removed:
show while it may not. *)
69
Removed:
70
Removed:
type t = {
71
Removed:
routine_id : string; (** For the begin/override forms. *)
72
Removed:
routine_name : string;
73
Removed:
next_workout : string;
74
Removed:
gate : gate;
75
Removed:
}
76
Removed:
(** The overview while no workout is in progress. The recovery policy has
77
Removed:
already decided [gate] in the use case. The page only renders it. *)
78
Removed:
end
79
Removed:
80
Removed:
module Workout = struct
81
Removed:
type movement = {
82
Removed:
name : string; (** The exercise name shown for this input row. *)
83
Removed:
load_field : string; (** Form field name for the load input. *)
84
Removed:
reps_field : string; (** Form field name for the reps input. *)
85
Removed:
reps_label : string;
86
Removed:
load_value : string option;
87
Removed:
(** Pre-fill when correcting a recorded slot. *)
88
Removed:
reps_value : string option;
89
Removed:
}
90
Removed:
(** One input row of a slot's record/correction form. The controller emits one
91
Removed:
movement for a single-exercise slot and two for a pre-exhaust pair, so the
92
Removed:
presenter renders whatever movements it is given and never inspects the
93
Removed:
prescription's [delivery] shape itself. *)
94
Removed:
95
Removed:
type slot = {
96
Removed:
index : int;
97
Removed:
label : string; (** Exercise label for the selector option and heading. *)
98
Removed:
movements : movement list;
99
Removed:
recorded_summary : string option;
100
Removed:
(** [Some description] once the slot is filled. [None] while
101
Removed:
outstanding. Drives the recorded/outstanding branch without an
102
Removed:
entity. *)
103
Removed:
selected_extension : string;
104
Removed:
(** The ending code to pre-select when correcting a recorded slot ("",
105
Removed:
"forced", "negatives", "rest-pause", "static"). "" for an
106
Removed:
outstanding slot. *)
107
Removed:
}
108
Removed:
109
Removed:
type t = {
110
Removed:
name : string; (** The workout (prescription) name, for the page title. *)
111
Removed:
slots : slot list;
112
Removed:
active_slot : int;
113
Removed:
started_at_unix : int; (** Workout start, for the elapsed timer. *)
114
Removed:
overridden : bool; (** Begun before recovery finished. *)
115
Removed:
all_recorded : bool; (** Every prescribed slot has a record. *)
116
Removed:
}
117
Removed:
(** A workout view: the slot the trainee is on, the record/correction state of
118
Removed:
each slot, and the timer and override facts. Carries no Evidence or
119
Removed:
Prescription entity. Extension/ending selection stays in the presenter
120
Removed:
form, since its option set is fixed UI vocabulary, not domain data. *)
121
Removed:
end
122
Removed:
123
Removed:
module Logbook_entry = struct
124
Removed:
type t = {
125
Removed:
id : string; (** Record id for the link. *)
126
Removed:
workout_name : string;
127
Removed:
stimuli_count : int;
128
Removed:
complete : bool;
129
Removed:
}
130
Removed:
(** One saved workout as the logbook list shows it: a link, a name, how many
131
Removed:
stimuli it holds, and whether it is complete. No Repository record and no
132
Removed:
Evidence.Workout entity. *)
133
Removed:
end
134
Removed:
135
Removed:
module Import_warning = struct
136
Removed:
type t = {
137
Removed:
row : int; (** The source row that the adapter dropped. *)
138
Removed:
detail : string; (** Why the row was dropped, already phrased. *)
139
Removed:
}
140
Removed:
(** One dropped import row, ready to render. The controller phrases the
141
Removed:
reason, so no adapter warning variant crosses the boundary. *)
142
Removed:
end
143
Removed:
144
Removed:
module Import_set = struct
145
Removed:
type t = {
146
Removed:
row : int; (** The source row, for provenance. *)
147
Removed:
exercise_name : string; (** The export's exercise name, before mapping. *)
148
Removed:
set_type : string; (** The set type, already phrased. *)
149
Removed:
weight : string; (** The load, already formatted. *)
150
Removed:
reps : string; (** The rep count, already formatted. *)
151
Removed:
mapping : string option;
152
Removed:
(** The catalog exercise name this set maps to, when confirmed. *)
153
Removed:
}
154
Removed:
(** One provisional import set, ready to render. Carries only strings, no
155
Removed:
source-set entity. *)
156
Removed:
end
157
Removed:
158
Removed:
module Import_workout = struct
159
Removed:
type t = {
160
Removed:
id : string; (** The workout id, for the per-workout forms. *)
161
Removed:
title : string;
162
Removed:
sets : Import_set.t list;
163
Removed:
blockers : string list;
164
Removed:
(** Why the workout cannot promote yet, each already phrased. Empty when
165
Removed:
the workout can promote. *)
166
Removed:
acknowledgement : string option;
167
Removed:
(** The retained normal-set acknowledgement, when present. *)
168
Removed:
revision : int; (** The workout revision, for provenance display. *)
169
Removed:
}
170
Removed:
(** One provisional import workout, ready to render: its sets, its blockers,
171
Removed:
and its acknowledgement state. No Workout_import entity. *)
172
Removed:
end
173
Removed:
174
Removed:
module Import_review = struct
175
Removed:
type t = {
176
Removed:
batch_id : string;
177
Removed:
warnings : Import_warning.t list;
178
Removed:
workouts : Import_workout.t list;
179
Removed:
}
180
Removed:
(** A provisional import batch under review: the rows dropped on parse and the
181
Removed:
workouts awaiting cleaning and promotion. No batch entity crosses the
182
Removed:
boundary. *)
183
Removed:
end