[OCaml] High Intensity Training Online
feat preserve partial subjective feedback
Add a Back action to the feedback flow that steps to the previous factor and keeps every answer, so a trainee can revise an earlier report without losing the rest. Skip already keeps the other answers and ignores only the current factor. Make the modal Close control cancel the flow by posting the cancel route, so closing discards partial progress rather than hiding it. A report saves as a partial list: only the answered factors are recorded. The logbook records what was reported; it does not demand a full set.
Changed files
ARCHITECTURE.org
@@ -223,7 +223,9 @@
223
223
duplicate categories rejected. Leveled signals carry a five-point score, 1 (very
224
224
poor) to 5 (very good); these are wellness self-reports, never a measure of set
225
225
intensity. The application stores reports independently of workouts, and the web
226
Removed:
tier records them through a sequential optional flow.
226
Added:
tier records them through a sequential optional flow. The flow keeps its partial
227
Added:
answers: Back and Skip preserve them, Close cancels the flow, and a report may be
228
Added:
saved as a partial list.
227
229
228
230
** Progression — the judgment
229
231
lib/web/handlers.ml
@@ -576,12 +576,21 @@
576
576
let action =
577
577
match field "action" fields with
578
578
| Some ("next" | "Next") -> Some "next"
579
Added:
| Some ("back" | "Back") -> Some "back"
579
580
| Some ("skip" | "Skip this factor" | "Skip this step") ->
580
581
Some "skip"
581
582
| Some ("save" | "Record feedback") -> Some "save"
582
583
| _ -> None
583
584
in
584
Removed:
if step = 5 then
585
Added:
(* Back steps to the previous factor and keeps every answer, so a
586
Added:
trainee can revise an earlier report without losing the rest. *)
587
Added:
if action = Some "back" then
588
Added:
if step = 0 then redirect_to request Routes.logbook
589
Added:
else
590
Added:
let previous = Pages.{ flow with step = step - 1 } in
591
Added:
set_feedback_flow request previous >>= fun () ->
592
Added:
redirect_to request Routes.logbook
593
Added:
else if step = 5 then
585
594
if action = Some "skip" then
586
595
finish_feedback t trainee request flow []
587
596
else if action = Some "save" then
lib/web/pages.ml
@@ -1265,11 +1265,17 @@
1265
1265
feedback_progress (step + 1);
1266
1266
tag "div"
1267
1267
[ class_ "feedback-actions" ]
1268
Removed:
[
1269
Removed:
feedback_action_button ~action:"next" ~label:"Next" ();
1270
Removed:
feedback_action_button ~action:"skip" ~label:"Skip this factor"
1271
Removed:
~secondary:true ();
1272
Removed:
];
1268
Added:
((if step > 0 then
1269
Added:
[
1270
Added:
feedback_action_button ~action:"back" ~label:"Back"
1271
Added:
~secondary:true ();
1272
Added:
]
1273
Added:
else [])
1274
Added:
@ [
1275
Added:
feedback_action_button ~action:"next" ~label:"Next" ();
1276
Added:
feedback_action_button ~action:"skip" ~label:"Skip this factor"
1277
Added:
~secondary:true ();
1278
Added:
]);
1273
1279
]
1274
1280
else
1275
1281
fields
@@ -1288,6 +1294,8 @@
1288
1294
tag "div"
1289
1295
[ class_ "feedback-actions" ]
1290
1296
[
1297
Added:
feedback_action_button ~action:"back" ~label:"Back"
1298
Added:
~secondary:true ();
1291
1299
feedback_action_button ~action:"save" ~label:"Record feedback" ();
1292
1300
feedback_action_button ~action:"skip" ~label:"Skip this step"
1293
1301
~secondary:true ();
@@ -1315,14 +1323,25 @@
1315
1323
[ class_ "feedback-modal-heading" ]
1316
1324
[
1317
1325
tag "h2" [] [ txt "How are you feeling?" ];
1318
Removed:
tag "button"
1326
Added:
(* Close cancels the flow: it submits the cancel form, so the
1327
Added:
partial progress is discarded rather than kept for later. *)
1328
Added:
tag "form"
1319
1329
[
1320
Removed:
type_ "button";
1321
Removed:
class_ "feedback-modal-close";
1322
Removed:
Dream_html.attr "data-hito-feedback-dismiss";
1323
Removed:
Dream_html.string_attr "aria-label" "Close feedback";
1330
Added:
action Routes.cancel_feedback;
1331
Added:
post_form;
1332
Added:
class_ "feedback-close-form";
1333
Added:
Dream_html.attr "data-hito-app-form";
1324
1334
]
1325
Removed:
[ txt "×" ];
1335
Added:
[
1336
Added:
Dream_html.csrf_tag request;
1337
Added:
tag "button"
1338
Added:
[
1339
Added:
type_ "submit";
1340
Added:
class_ "feedback-modal-close";
1341
Added:
Dream_html.string_attr "aria-label" "Close feedback";
1342
Added:
]
1343
Added:
[ txt "×" ];
1344
Added:
];
1326
1345
];
1327
1346
feedback_flow_form request flow;
1328
1347
feedback_cancel_form request;
test/test_web.ml
@@ -1181,6 +1181,105 @@
1181
1181
Alcotest.(check bool)
1182
1182
"does not add an empty report" false
1183
1183
(contains ~substring:"No signals" logbook_page) );
1184
Added:
( "back returns to the previous factor and keeps answers",
1185
Added:
`Quick,
1186
Added:
fun () ->
1187
Added:
let c = client () in
1188
Added:
let _ = sign_in_new c in
1189
Added:
let start = body (get c "/logbook?feedback=start") in
1190
Added:
Alcotest.(check bool)
1191
Added:
"the first factor offers no back control" false
1192
Added:
(contains ~substring:"value=\"back\"" start);
1193
Added:
let token = Option.get (csrf_token start) in
1194
Added:
let _ =
1195
Added:
post c "/feedback"
1196
Added:
[
1197
Added:
("dream.csrf", token);
1198
Added:
("step", "0");
1199
Added:
("action", "next");
1200
Added:
("choice", "5");
1201
Added:
]
1202
Added:
in
1203
Added:
let appetite = body (get c "/logbook") in
1204
Added:
Alcotest.(check bool)
1205
Added:
"the second factor offers a back control" true
1206
Added:
(contains ~substring:">Appetite</h3>" appetite
1207
Added:
&& contains ~substring:"value=\"back\"" appetite);
1208
Added:
let token = Option.get (csrf_token appetite) in
1209
Added:
let back =
1210
Added:
post c "/feedback"
1211
Added:
[ ("dream.csrf", token); ("step", "1"); ("action", "back") ]
1212
Added:
in
1213
Added:
Alcotest.(check int) "back redirects" 303 (status back);
1214
Added:
let sleep_again = body (get c "/logbook") in
1215
Added:
Alcotest.(check bool)
1216
Added:
"back returns to the first factor" true
1217
Added:
(contains ~substring:">Sleep</h3>" sleep_again
1218
Added:
&& contains ~substring:"1 of 5 factors" sleep_again);
1219
Added:
(* The earlier answer survives Back. Skipping forward from here
1220
Added:
never touches it, so saving still stores the sleep signal. *)
1221
Added:
let rec skip_to_notes page =
1222
Added:
let token = Option.get (csrf_token page) in
1223
Added:
let step_of =
1224
Added:
if contains ~substring:">Sleep</h3>" page then 0
1225
Added:
else if contains ~substring:">Appetite</h3>" page then 1
1226
Added:
else if contains ~substring:">Readiness</h3>" page then 2
1227
Added:
else if contains ~substring:">Motivation</h3>" page then 3
1228
Added:
else if contains ~substring:">Perceived difficulty</h3>" page
1229
Added:
then 4
1230
Added:
else -1
1231
Added:
in
1232
Added:
if step_of < 0 then page
1233
Added:
else begin
1234
Added:
let _ =
1235
Added:
post c "/feedback"
1236
Added:
[
1237
Added:
("dream.csrf", token);
1238
Added:
("step", string_of_int step_of);
1239
Added:
("action", "skip");
1240
Added:
]
1241
Added:
in
1242
Added:
skip_to_notes (body (get c "/logbook"))
1243
Added:
end
1244
Added:
in
1245
Added:
let notes = skip_to_notes sleep_again in
1246
Added:
let token = Option.get (csrf_token notes) in
1247
Added:
let _ =
1248
Added:
post c "/feedback"
1249
Added:
[ ("dream.csrf", token); ("step", "5"); ("action", "save") ]
1250
Added:
in
1251
Added:
let logbook_page = body (get c "/logbook") in
1252
Added:
Alcotest.(check bool)
1253
Added:
"the pre-back answer persists" true
1254
Added:
(contains ~substring:"Sleep 5 (very good)" logbook_page) );
1255
Added:
( "closing the feedback modal cancels the flow",
1256
Added:
`Quick,
1257
Added:
fun () ->
1258
Added:
let c = client () in
1259
Added:
let _ = sign_in_new c in
1260
Added:
let start = body (get c "/logbook?feedback=start") in
1261
Added:
(* Answer the first factor, then close: the flow is discarded and
1262
Added:
reopening starts fresh at the first factor. *)
1263
Added:
let token = Option.get (csrf_token start) in
1264
Added:
let _ =
1265
Added:
post c "/feedback"
1266
Added:
[
1267
Added:
("dream.csrf", token);
1268
Added:
("step", "0");
1269
Added:
("action", "next");
1270
Added:
("choice", "5");
1271
Added:
]
1272
Added:
in
1273
Added:
let appetite = body (get c "/logbook") in
1274
Added:
let token = Option.get (csrf_token appetite) in
1275
Added:
(* The close control posts to the cancel route. *)
1276
Added:
let closed = post c "/feedback/cancel" [ ("dream.csrf", token) ] in
1277
Added:
Alcotest.(check int) "close redirects" 303 (status closed);
1278
Added:
let reopened = body (get c "/logbook?feedback=start") in
1279
Added:
Alcotest.(check bool)
1280
Added:
"reopening starts fresh at the first factor" true
1281
Added:
(contains ~substring:">Sleep</h3>" reopened
1282
Added:
&& contains ~substring:"1 of 5 factors" reopened) );
1184
1283
( "submitting feedback stores it and it appears on the logbook",
1185
1284
`Quick,
1186
1285
fun () ->