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.

Commit
7c9f7012ed507ebdaf80ab816c2e899571ae8616
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/web/pages.ml
index bea82225..1c50e51a 100644..100644
@@ -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
index a815b4ce..bc866f11 100644..100644
@@ -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 [