[OCaml] High Intensity Training Online
feat confirm workout override in a modal
Beginning before recovery is complete now opens a confirmation modal, reusing the shared confirm machinery. The override stays possible but never silent: an explicit acknowledgement is required, per Heavy Duty's rule that deviations must be deliberate. Without script the form still submits directly, so the no-JS path keeps working.
Changed files
lib/web/pages.ml
@@ -411,6 +411,69 @@
411
411
void "input" [ type_ "submit"; value label ];
412
412
]
413
413
414
Added:
(* Beginning under override needs an explicit acknowledgement, so its submit is
415
Added:
gated behind a confirmation modal. Without script the form submits at once —
416
Added:
the doctrine's deviation stays possible; the modal only makes it deliberate.
417
Added:
The client reuses the shared confirm machinery: [data-hito-confirm-cancel]
418
Added:
opens the dialog, [data-hito-confirm-form] is the form its accept submits. *)
419
Added:
let override_begin request ~routine =
420
Added:
[
421
Added:
tag "form"
422
Added:
[
423
Added:
action Routes.workout;
424
Added:
post_form;
425
Added:
Dream_html.attr "data-hito-app-form";
426
Added:
Dream_html.attr "data-hito-confirm-form";
427
Added:
]
428
Added:
[
429
Added:
Dream_html.csrf_tag request;
430
Added:
void "input"
431
Added:
[
432
Added:
type_ "hidden";
433
Added:
name "routine";
434
Added:
Dream_html.string_attr "value" "%s" (id_string routine);
435
Added:
];
436
Added:
void "input"
437
Added:
[
438
Added:
type_ "hidden";
439
Added:
name "override";
440
Added:
Dream_html.string_attr "value" "true";
441
Added:
];
442
Added:
void "input"
443
Added:
[
444
Added:
type_ "submit";
445
Added:
value "Begin under override";
446
Added:
Dream_html.attr "data-hito-confirm-cancel";
447
Added:
];
448
Added:
];
449
Added:
tag "dialog"
450
Added:
[ class_ "confirm-dialog"; Dream_html.attr "data-hito-confirm-modal" ]
451
Added:
[
452
Added:
tag "h2" [] [ txt "Begin before recovery is complete?" ];
453
Added:
tag "p" []
454
Added:
[
455
Added:
txt
456
Added:
"Heavy Duty grows muscle during recovery, not in the gym. \
457
Added:
Training early can cut into it. This is recorded as an \
458
Added:
override.";
459
Added:
];
460
Added:
tag "div"
461
Added:
[ class_ "button-group" ]
462
Added:
[
463
Added:
tag "button"
464
Added:
[
465
Added:
type_ "button";
466
Added:
class_ "secondary";
467
Added:
Dream_html.attr "data-hito-confirm-dismiss";
468
Added:
]
469
Added:
[ txt "Wait to recover" ];
470
Added:
tag "button"
471
Added:
[ type_ "button"; Dream_html.attr "data-hito-confirm-accept" ]
472
Added:
[ txt "Begin under override" ];
473
Added:
];
474
Added:
];
475
Added:
]
476
Added:
414
477
let home request ~trainee ~routine ~routine_name ~next ~readiness =
415
478
let status, gate =
416
479
match readiness with
@@ -424,14 +487,13 @@
424
487
[
425
488
tag "div"
426
489
[ class_ "warn" ]
427
Removed:
[
428
Removed:
tag "p" []
429
Removed:
[
430
Removed:
txt "Heavy Duty requires recovery before the next workout.";
431
Removed:
];
432
Removed:
begin_form request ~routine ~override:true
433
Removed:
"Begin under override";
434
Removed:
];
490
Added:
([
491
Added:
tag "p" []
492
Added:
[
493
Added:
txt "Heavy Duty requires recovery before the next workout.";
494
Added:
];
495
Added:
]
496
Added:
@ override_begin request ~routine);
435
497
] )
436
498
in
437
499
html_page ~trainee ~request ~active:"home" "Home"
test/test_web.ml
@@ -1023,6 +1023,38 @@
1023
1023
"opens the feedback disclosure on a suggestion" true
1024
1024
(contains ~substring:"<details class=\"feedback-disclosure\" open"
1025
1025
prompted) );
1026
Added:
( "beginning under override is gated behind a confirmation modal",
1027
Added:
`Quick,
1028
Added:
fun () ->
1029
Added:
let c = client () in
1030
Added:
let _ = sign_in_new c in
1031
Added:
let token = Option.get (csrf_token (body (get c "/"))) in
1032
Added:
let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
1033
Added:
let token = Option.get (csrf_token (body (get c "/"))) in
1034
Added:
let _ =
1035
Added:
post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
1036
Added:
in
1037
Added:
let token = Option.get (csrf_token (body (get c "/workout"))) in
1038
Added:
let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
1039
Added:
(* Recovery has not elapsed, so Home shows the override gate. The
1040
Added:
begin form carries the shared confirm attributes and a matching
1041
Added:
dialog, so the deviation is possible but never silent. *)
1042
Added:
let home = body (get c "/") in
1043
Added:
Alcotest.(check bool)
1044
Added:
"still requires recovery" true
1045
Added:
(contains ~substring:"requires recovery" home);
1046
Added:
Alcotest.(check bool)
1047
Added:
"gates the override submit behind the confirm machinery" true
1048
Added:
(contains ~substring:"data-hito-confirm-cancel" home);
1049
Added:
Alcotest.(check bool)
1050
Added:
"posts the override through a confirm form" true
1051
Added:
(contains ~substring:"data-hito-confirm-form" home);
1052
Added:
Alcotest.(check bool)
1053
Added:
"carries the confirmation modal" true
1054
Added:
(contains ~substring:"data-hito-confirm-modal" home);
1055
Added:
Alcotest.(check bool)
1056
Added:
"still submits the override without script" true
1057
Added:
(contains ~substring:"value=\"true\"" home) );
1026
1058
] );
1027
1059
( "web.app_feedback",
1028
1060
[