[OCaml] High Intensity Training Online
fix Label planned workouts and show version
Show the routine and workout name in planned captions. Show the copyright owner and the deployed commit date in the shared footer.
Changed files
ARCHITECTURE.org
@@ -597,7 +597,7 @@
597
597
| =HITO_DB= | Caqti database URI | =sqlite3:hito.sqlite= |
598
598
| =HITO_SECRET= | Signs session cookies and CSRF tokens | random per run |
599
599
| =HITO_PORT= | TCP port | =8080= |
600
Removed:
| =HITO_VERSION= | Deployment identifier | =development= |
600
Added:
| =HITO_VERSION= | Deployed commit date in YYYYMMDD format | =development= |
601
601
| =HITO_SEED= | Enable or disable startup seeding | =1= in development, =0= otherwise |
602
602
| =HITO_SEED_PASSWORD= | Password for an explicit deployment seed | none — refuses to start |
603
603
| =HITO_SEED_CSV= | Hevy export for an explicit deployment seed | =workout_data.csv= |
README.org
@@ -46,7 +46,7 @@
46
46
| ~HITO_DB~ | The Caqti URI. Default: ~sqlite3:local_db/hito.sqlite~. |
47
47
| ~HITO_SECRET~ | Signs sessions and CSRF tokens. Set it in production. |
48
48
| ~HITO_PORT~ | The TCP port. Default: ~8080~. The server binds loopback. |
49
Removed:
| ~HITO_VERSION~ | The deployment identifier in the footer. |
49
Added:
| ~HITO_VERSION~ | The deployed commit date in YYYYMMDD format. |
50
50
| ~HITO_SEED~ | ~1~ seeds an account at startup. Development defaults to ~1~. Deployment defaults to ~0~. |
51
51
| ~HITO_SEED_PASSWORD~ | Password for the deployment seed when ~HITO_SEED=1~. |
52
52
| ~HITO_SEED_CSV~ | The Hevy export for the deployment seed. |
bin/main.ml
@@ -10,8 +10,9 @@
10
10
unset, the server generates a random secret for this run only,
11
11
so sessions do not survive a restart. Set it in production.
12
12
- HITO_PORT TCP port. Default: 8080
13
Removed:
- HITO_VERSION Deployment commit identifier shown in the page footer. Defaults
14
Removed:
to "development" outside a deployment.
13
Added:
- HITO_VERSION Deployment version date shown in the page footer. Deployments
14
Added:
set YYYYMMDD from the deployed commit date. Defaults to
15
Added:
"development" outside a deployment.
15
16
- HITO_SEED Set to 1 to seed an account on startup. In development,
16
17
the default is 1 and the account is [test] with password
17
18
[test]. In a deployment, the default is 0.
@@ -78,8 +79,8 @@
78
79
exit 1
79
80
| Ok repo ->
80
81
(* Development seeds are automatic unless HITO_SEED=0 disables them.
81
Removed:
Deployments default to no seed because HITO_VERSION carries the commit
82
Removed:
identifier. The deployment seed still requires an explicit password. *)
82
Added:
Deployments default to no seed because HITO_VERSION is not
83
Added:
"development". The deployment seed still requires an explicit password. *)
83
84
(match (version, seed_mode) with
84
85
| "development", "1" -> Lwt_main.run (Seed.development_user repo)
85
86
| _, "1" ->
deploy/post-receive
@@ -79,8 +79,10 @@
79
79
# Write per-release provenance and append one durable history line.
80
80
record_metadata() {
81
81
local sha="$1" rel="$2" ts="$3" build_secs="$4"
82
Removed:
local subject
82
Added:
local subject version
83
83
subject="$(git --git-dir="${BARE}" log -1 --format=%s "${sha}")"
84
Added:
version="$(git --git-dir="${BARE}" log -1 --format=%cd \
85
Added:
--date=format:%Y%m%d "${sha}")"
84
86
85
87
jq -n \
86
88
--arg commit "${sha}" \
@@ -97,7 +99,7 @@
97
99
printf '%s\t%s\t%ss\t%s\t%s\n' \
98
100
"${ts}" "${sha:0:12}" "${build_secs}" "${DEPLOY_BRANCH}" "${subject}" \
99
101
>>"${SHARED}/deploy-history.tsv"
100
Removed:
printf 'HITO_VERSION=%s\n' "${sha:0:12}" >"${SHARED}/deploy.env"
102
Added:
printf 'HITO_VERSION=%s\n' "${version}" >"${SHARED}/deploy.env"
101
103
}
102
104
103
105
# Point current at ${rel} in a single rename so no request sees a half state.
deploy/setup.sh
@@ -144,7 +144,7 @@
144
144
# Runs entirely as the service user; root only orchestrates.
145
145
build_and_flip() {
146
146
local start="${SECONDS}"
147
Removed:
local ts rel stage sha subject build_secs prev
147
Added:
local ts rel stage sha subject version build_secs prev
148
148
ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
149
149
rel="${RELEASES}/${ts}"
150
150
prev="$(readlink -f "${SRV}/current" 2>/dev/null || true)"
@@ -172,6 +172,8 @@
172
172
ln -sfn "${DB}" "${rel}/hito.sqlite"
173
173
174
174
subject="$(git --git-dir="${BARE}" log -1 --format=%s "${sha}")"
175
Added:
version="$(git --git-dir="${BARE}" log -1 --format=%cd \
176
Added:
--date=format:%Y%m%d "${sha}")"
175
177
jq -n \
176
178
--arg commit "${sha}" \
177
179
--arg subject "${subject}" \
@@ -186,7 +188,7 @@
186
188
printf '%s\t%s\t%ss\t%s\t%s\n' \
187
189
"${ts}" "${sha:0:12}" "${build_secs}" "master" "${subject}" \
188
190
>>"${SHARED}/deploy-history.tsv"
189
Removed:
printf 'HITO_VERSION=%s\n' "${sha:0:12}" >"${SHARED}/deploy.env"
191
Added:
printf 'HITO_VERSION=%s\n' "${version}" >"${SHARED}/deploy.env"
190
192
191
193
chown -R "${SVC_USER}:${SVC_GROUP}" "${rel}"
192
194
chown "${SVC_USER}:${SVC_GROUP}" "${SHARED}/deploy-history.tsv" "${SHARED}/deploy.env"
lib/presenter/page_kit.ml
@@ -104,7 +104,7 @@
104
104
[
105
105
tag "strong"
106
106
[ class_ "workout-evidence-caption-name" ]
107
Removed:
[ Dream_html.txt "%s" planned.name ];
107
Added:
[ Dream_html.txt "%s, %s" planned.routine_name planned.name ];
108
108
tag "span"
109
109
[ class_ "workout-evidence-caption-status upcoming" ]
110
110
[ Dream_html.txt "Planned on %s" planned.scheduled_on ];
lib/presenter/page_shell.ml
@@ -10,18 +10,18 @@
10
10
| Some version when version <> "" -> version
11
11
| _ -> "development"
12
12
13
Removed:
let copyright_year () =
14
Removed:
let now = Unix.gmtime (Unix.gettimeofday ()) in
15
Removed:
now.Unix.tm_year + 1900
16
Removed:
17
13
let footer () =
18
14
tag "footer"
19
15
[ class_ "site-footer" ]
20
16
[
21
Removed:
txt "© %d Marius Peter · version " (copyright_year ());
22
Removed:
tag "code"
23
Removed:
[ class_ "deployment-version" ]
24
Removed:
[ txt "%s" (deployment_version ()) ];
17
Added:
tag "div" [] [ txt "© 2026 Marius PETER. All rights reserved." ];
18
Added:
tag "div" []
19
Added:
[
20
Added:
txt "Version: ";
21
Added:
tag "code"
22
Added:
[ class_ "deployment-version" ]
23
Added:
[ txt "%s" (deployment_version ()) ];
24
Added:
];
25
25
]
26
26
27
27
let html_page ?viewer ?request ?(active = "") ?(show_nav = false)
lib/presenter/view_model.ml
@@ -65,7 +65,12 @@
65
65
66
66
type group = { exercises : exercise list }
67
67
68
Removed:
type t = { name : string; scheduled_on : string; groups : group list }
68
Added:
type t = {
69
Added:
routine_name : string;
70
Added:
name : string;
71
Added:
scheduled_on : string;
72
Added:
groups : group list;
73
Added:
}
69
74
(** A prescribed workout that has no logging record yet. Latest performance is
70
75
evidence for the next attempt, not a repetition target. *)
71
76
end
lib/web/handlers_logbook.ml
@@ -36,6 +36,7 @@
36
36
37
37
type scheduled_workout = {
38
38
at_unix : int;
39
Added:
routine_name : string;
39
40
workout_name : string;
40
41
groups : planned_group list;
41
42
}
@@ -84,6 +85,7 @@
84
85
else
85
86
{
86
87
at_unix = planned_at;
88
Added:
routine_name = Prescription.Routine.name routine;
87
89
workout_name = Prescription.Workout.name next;
88
90
groups = groups_of_prescription next;
89
91
}
@@ -445,6 +447,7 @@
445
447
Some
446
448
View_model.Upcoming_workout.
447
449
{
450
Added:
routine_name = planned.routine_name;
448
451
name = planned.workout_name;
449
452
scheduled_on = utc_long_date planned.at_unix;
450
453
groups;
lib/web/handlers_overview.ml
@@ -58,7 +58,7 @@
58
58
59
59
(* Build the next planned workout with the latest observed performance for
60
60
each exercise. Groups preserve a pre-exhaust pair. *)
61
Removed:
let scheduled_workout_view t trainee next ~scheduled_on :
61
Added:
let scheduled_workout_view t trainee next ~routine_name ~scheduled_on :
62
62
View_model.Upcoming_workout.t Lwt.t =
63
63
Lwt_list.map_s
64
64
(fun stimulus ->
@@ -78,7 +78,12 @@
78
78
(Prescription.Workout.stimuli next)
79
79
>|= fun groups ->
80
80
View_model.Upcoming_workout.
81
Removed:
{ name = Prescription.Workout.name next; scheduled_on; groups }
81
Added:
{
82
Added:
routine_name;
83
Added:
name = Prescription.Workout.name next;
84
Added:
scheduled_on;
85
Added:
groups;
86
Added:
}
82
87
83
88
let home_view ~(routine : Repository.routine_id) ~routine_name ~next_workout
84
89
~readiness : View_model.Home.t =
@@ -134,6 +139,7 @@
134
139
- Recovery.duration_to_seconds rested)
135
140
in
136
141
scheduled_workout_view t trainee.Trainee.id next
142
Added:
~routine_name:(Prescription.Routine.name selected)
137
143
~scheduled_on:(utc_long_date scheduled_on)
138
144
>|= fun next_workout ->
139
145
Pages.home request ~viewer
test/web/auth.ml
@@ -17,6 +17,19 @@
17
17
fun () ->
18
18
let c = client () in
19
19
Alcotest.(check int) "GET /login" 200 (status (get c "/login")) );
20
Added:
( "the sign-in page identifies the copyright owner and version",
21
Added:
`Quick,
22
Added:
fun () ->
23
Added:
let c = client () in
24
Added:
let page = body (get c "/login") in
25
Added:
Alcotest.(check bool)
26
Added:
"copyright notice is visible" true
27
Added:
(Option.is_some
28
Added:
(index_from ~needle:"© 2026 Marius PETER. All rights reserved."
29
Added:
page 0));
30
Added:
Alcotest.(check bool)
31
Added:
"version is visible" true
32
Added:
(Option.is_some (index_from ~needle:"Version: " page 0)) );
20
33
( "every response carries the browser hardening headers",
21
34
`Quick,
22
35
fun () ->
test/web/logbook.ml
@@ -60,9 +60,10 @@
60
60
"selected planned workout is displayed" true
61
61
(Option.is_some (index_from ~needle:"Day 2" page 0));
62
62
Alcotest.(check bool)
63
Removed:
"planned workout dates itself in the table caption" true
63
Added:
"planned workout names its routine and day in the table caption"
64
Added:
true
64
65
(Option.is_some
65
Removed:
(index_from ~needle:"Planned on 13th January 1970" page 0));
66
Added:
(index_from ~needle:"Ideal Routine, Day 2" page 0));
66
67
Alcotest.(check bool)
67
68
"planned workout omits change columns" true
68
69
(Option.is_none (index_from ~needle:"Δ" page 0));
test/web/overview.ml
@@ -116,8 +116,11 @@
116
116
"the Day 1 preview lists Laterals" true
117
117
(Option.is_some (index_from ~needle:"Laterals" page 0));
118
118
Alcotest.(check bool)
119
Removed:
"Laterals names the planned workout in the table caption" true
120
Removed:
(Option.is_some (index_from ~needle:"Day 1" page 0));
119
Added:
"Laterals names the routine and planned workout in the table \
120
Added:
caption"
121
Added:
true
122
Added:
(Option.is_some
123
Added:
(index_from ~needle:"Ideal Routine, Day 1" page 0));
121
124
Alcotest.(check bool)
122
125
"Laterals shows its best load and reps" true
123
126
(Option.is_some (index_from ~needle:"Load (kg)" page 0)