[OCaml] High Intensity Training Online
1
(** View models: the simple data structures the presenter renders.
2
3
The Clean Architecture rule for crossing a boundary is that the data passed
4
is a plain structure, never an entity or a database row. These DTOs are that
5
structure. A handler (the controller) maps a repository record or a core
6
entity into a view model, and the presenter ({!Pages}) renders the view
7
model without ever naming an inner-circle type.
8
9
Every page now crosses the boundary as a DTO. No page names a core entity.
10
Add a page DTO here before the presenter renders it. *)
11
12
module Viewer = struct
13
type t = { username : string }
14
(** The signed-in trainee, as the shell needs them: a name to show, nothing
15
more. Replaces passing the [Trainee.t] entity into the presenter. *)
16
end
17
18
module App_feedback = struct
19
type t = {
20
id : string; (** Opaque feedback identity, already stringified. *)
21
author : string;
22
contributions : int;
23
submitted_at : string; (** Pre-formatted "YYYY-MM-DD HH:MM" in UTC. *)
24
message : string;
25
upvotes : int;
26
viewer_upvoted : bool;
27
viewer_owns : bool;
28
}
29
(** One application-feedback entry, ready to render. Carries no timestamp
30
type, no opaque id, no repository record — only what the view shows. *)
31
end
32
33
module Feedback = struct
34
type report = {
35
at_unix : int; (** Report time in Unix seconds, for chronological order. *)
36
signals : string list; (** Each signal already described, for the list. *)
37
}
38
(** One subjective-feedback report, ready to render. Carries no [Feedback]
39
variant and no timestamp type. *)
40
end
41
42
module Routine = struct
43
type exercise = { name : string }
44
(** One prescribed exercise line, its name already phrased. *)
45
46
type position =
47
| Elsewhere
48
| Next of { recovery : string option }
49
(** The workout the cycle reaches next. [recovery] is the phrased
50
recovery status while the trainee still recovers. *)
51
52
type workout = {
53
name : string;
54
exercises : exercise list;
55
rest_after : string;
56
(** The phrased rest that follows the workout. The last workout's rest
57
names the workout that starts the cycle again. *)
58
position : position;
59
}
60
61
type t = { name : string; workouts : workout list }
62
(** A routine as the timeline renders it: workouts in cycle order, each with
63
the rest that gates the next one. No rep bands, no [Prescription] entity.
64
*)
65
end
66
67
module Routine_choice = struct
68
type t = { id : string; name : string; workout_count : int }
69
(** One selectable routine on the chooser: its id for the select form, its
70
name, and how many workouts the cycle holds. *)
71
end
72
73
module Upcoming_workout = struct
74
type exercise = {
75
name : string;
76
latest_load : float option;
77
latest_reps : int option;
78
}
79
80
type group = { exercises : exercise list }
81
82
type t = {
83
routine_name : string;
84
name : string;
85
scheduled_on : string;
86
groups : group list;
87
}
88
(** A prescribed workout that has no logging record yet. Latest performance is
89
evidence for the next attempt, not a repetition target. *)
90
end
91
92
module Home = struct
93
type gate =
94
| Ready
95
| Recovering of { status : string }
96
(** Whether the next workout may begin, and the recovery status line to
97
show while it may not. *)
98
99
type t = {
100
routine_id : string; (** For the begin/override forms. *)
101
routine_name : string;
102
next_workout : Upcoming_workout.t;
103
gate : gate;
104
}
105
(** The overview while no workout is in progress. The recovery policy has
106
already decided [gate] in the use case. The page only renders it. *)
107
end
108
109
module Workout = struct
110
type movement = {
111
name : string; (** The exercise name shown for this input row. *)
112
load_field : string; (** Form field name for the load input. *)
113
reps_field : string; (** Form field name for the reps input. *)
114
reps_label : string;
115
load_value : string option;
116
(** Pre-fill when correcting a recorded slot. *)
117
reps_value : string option;
118
previous_load : string option;
119
(** The most recent prior load for this exercise, pre-formatted as the
120
numeric record to beat (for example ["12"]). [None] when the
121
exercise has no earlier observation. It is an observed fact, not a
122
target — the set still ends at failure. *)
123
previous_reps : string option;
124
(** The most recent prior reps for this exercise, pre-formatted as the
125
numeric record to beat (for example ["22"]). [None] exactly when
126
[previous_load] is [None] — the pair comes from the same
127
observation. *)
128
}
129
(** One input row of a slot's record/correction form. The controller emits one
130
movement for a single-exercise slot and two for a pre-exhaust pair, so the
131
presenter renders whatever movements it is given and never inspects the
132
prescription's [delivery] shape itself. *)
133
134
type slot = {
135
index : int;
136
label : string; (** Exercise label for the accordion summary. *)
137
movements : movement list;
138
recorded_summary : string option;
139
(** [Some description] once the slot is filled. [None] while
140
outstanding. Drives the recorded/outstanding branch without an
141
entity. *)
142
selected_extension : string;
143
(** The ending code to pre-select when correcting a recorded slot ("",
144
"forced", "negatives", "static"). *)
145
selected_extension_amount : string;
146
(** Reps for forced reps/negatives or seconds for a static hold.
147
Defaults to one for an outstanding slot. *)
148
}
149
150
type t = {
151
name : string; (** The workout (prescription) name, for the page title. *)
152
date : string;
153
(** Workout start date, pre-formatted as "day month year" in UTC. *)
154
slots : slot list;
155
active_slot : int;
156
started_at_unix : int; (** Workout start, for the elapsed timer. *)
157
overridden : bool; (** Begun before recovery finished. *)
158
all_recorded : bool; (** Every prescribed slot has a record. *)
159
}
160
(** A workout view: the slot the trainee is on, the record/correction state of
161
each slot, and the timer and override facts. Carries no Evidence or
162
Prescription entity. Extension/ending selection stays in the presenter
163
form, since its option set is fixed UI vocabulary, not domain data. *)
164
end
165
166
module Exercise_comparison = struct
167
type t = {
168
exercise_name : string;
169
current_load : float;
170
current_reps : int;
171
previous_load : float option;
172
previous_reps : int option;
173
}
174
(** One observed exercise effort and its optional previous performance. *)
175
end
176
177
module Stimulus_group = struct
178
type t = { rows : Exercise_comparison.t list }
179
(** The exercises delivered by one stimulus, in performance order. *)
180
end
181
182
module Recorded_workout = struct
183
type t = {
184
id : string; (** Record id for edit and delete links. *)
185
imported : bool;
186
(** True for a promoted third-party import. Shown read-only; its title
187
is the source title verbatim, and an oxblood outline pill, not text
188
on the title, signals its imported status. It feeds progress per
189
exercise but is never a prescribed native record. *)
190
at_unix : int;
191
(** Start time in Unix seconds, for ordering and selection. *)
192
workout_name : string;
193
performed_on : string;
194
duration_previous_seconds : int option;
195
(** The most recent earlier workout with the same prescription name,
196
strictly before this one, in seconds, when both finished. [None] for
197
a first performance of this workout or an unfinished one. *)
198
duration_seconds : int option;
199
(** This workout's own elapsed seconds, when finished. Carried with the
200
previous duration so the presenter can derive a signed delta. *)
201
performed_ago : string;
202
(** Time since the workout started, already phrased for the view. *)
203
next_scheduled : string option;
204
(** Time until the next planned occurrence of this workout, already
205
phrased for the view. *)
206
groups : Stimulus_group.t list;
207
(** Exercise rows grouped by the stimulus that delivered them. *)
208
}
209
(** One recorded workout for the selected-day detail panel. It carries the
210
exercise rows grouped by stimulus, or one read-only group for an import.
211
*)
212
end
213
214
module Selected_date = struct
215
type t = {
216
completed : Recorded_workout.t list;
217
upcoming : Upcoming_workout.t option;
218
}
219
(** The selected calendar day: recorded workouts and the planned workout, if
220
the routine schedules one. *)
221
end
222
223
module Calendar = struct
224
type day = {
225
day_of_month : int;
226
workout : bool;
227
scheduled_workout : string option;
228
today : bool;
229
selected : bool;
230
}
231
(** One dated cell. [workout] marks a completed workout. [scheduled_workout]
232
names the next workout planned for that day, when one exists. *)
233
234
type cell =
235
| Blank
236
| Day of day
237
(** [Blank] pads the leading and trailing days of the grid that fall
238
outside the month. *)
239
240
type t = {
241
selected_year : int;
242
selected_month : int; (** 1..12; the month shown. *)
243
weekday_headings : string list; (** Seven short labels, Monday first. *)
244
weeks : cell list list; (** Rows of seven cells, Monday first. *)
245
}
246
(** A single month laid out as a week grid, above the logbook. Prev/next
247
arrows and a Today link navigate months. The handler does the date
248
arithmetic; the presenter renders the grid. *)
249
end
250
251
module Progress_page = struct
252
type point = {
253
date : string; (** Pre-formatted "YYYY-MM-DD" in UTC. *)
254
at_unix : int; (** For x-axis positioning. *)
255
weight : float;
256
reps : int;
257
}
258
(** One observation on a chart: its load and reps at a point in time. *)
259
260
type chart = {
261
exercise_name : string;
262
points : point list; (** Oldest first, at least two. *)
263
}
264
(** One exercise's merged native and imported history, ready to plot as a
265
dual-axis line chart: weight on the left axis, reps on the right. *)
266
267
type t = { charts : chart list }
268
(** The progress page: one chart per exercise that has at least two
269
observations. Exercises with too little history to draw a line are
270
omitted. *)
271
end
272
273
module Import_warning = struct
274
type t = {
275
row : int; (** The source row that the adapter dropped. *)
276
detail : string; (** Why the adapter dropped the row, already phrased. *)
277
}
278
(** One dropped import row, ready to render. The controller phrases the
279
reason, so no adapter warning variant crosses the boundary. *)
280
end
281
282
module Import_set = struct
283
type t = {
284
row : int; (** The source row, for provenance. *)
285
exercise_name : string; (** The export's exercise name, before mapping. *)
286
set_type : string; (** The set type, already phrased. *)
287
weight : string; (** The load, already formatted. *)
288
reps : string; (** The rep count, already formatted. *)
289
mapping : string option;
290
(** The catalog exercise name this set maps to, when confirmed. *)
291
}
292
(** One provisional import set, ready to render. Carries only strings, no
293
source-set entity. *)
294
end
295
296
module Import_workout = struct
297
type t = {
298
id : string; (** The workout id, for the per-workout forms. *)
299
title : string;
300
sets : Import_set.t list;
301
blockers : string list;
302
(** Why the workout cannot promote yet, each already phrased. Empty when
303
the workout can promote. *)
304
acknowledgement : string option;
305
(** The retained normal-set acknowledgement, when present. *)
306
revision : int; (** The workout revision, for provenance display. *)
307
}
308
(** One provisional import workout, ready to render: its sets, its blockers,
309
and its acknowledgement state. No Workout_import entity. *)
310
end
311
312
module Import_review = struct
313
type t = {
314
batch_id : string;
315
warnings : Import_warning.t list;
316
workouts : Import_workout.t list;
317
}
318
(** A provisional import batch under review: the rows dropped on parse and the
319
workouts awaiting cleaning and promotion. No batch entity crosses the
320
boundary. *)
321
end
322