(** View models: the simple data structures the presenter renders. The Clean Architecture rule for crossing a boundary is that the data passed is a plain structure, never an entity or a database row. These DTOs are that structure. A handler (the controller) maps a repository record or a core entity into a view model, and the presenter ({!Pages}) renders the view model without ever naming an inner-circle type. Every page now crosses the boundary as a DTO. No page names a core entity. Add a page DTO here before the presenter renders it. *) module Viewer = struct type t = { username : string } (** The signed-in trainee, as the shell needs them: a name to show, nothing more. Replaces passing the [Trainee.t] entity into the presenter. *) end module App_feedback = struct type t = { id : string; (** Opaque feedback identity, already stringified. *) author : string; contributions : int; submitted_at : string; (** Pre-formatted "YYYY-MM-DD HH:MM" in UTC. *) message : string; upvotes : int; viewer_upvoted : bool; viewer_owns : bool; } (** One application-feedback entry, ready to render. Carries no timestamp type, no opaque id, no repository record — only what the view shows. *) end module Feedback = struct type report = { at_unix : int; (** Report time in Unix seconds, for chronological order. *) signals : string list; (** Each signal already described, for the list. *) } (** One subjective-feedback report, ready to render. Carries no [Feedback] variant and no timestamp type. *) end module Routine = struct type exercise = { name : string } (** One prescribed exercise line, its name already phrased. *) type position = | Elsewhere | Next of { recovery : string option } (** The workout the cycle reaches next. [recovery] is the phrased recovery status while the trainee still recovers. *) type workout = { name : string; exercises : exercise list; rest_after : string; (** The phrased rest that follows the workout. The last workout's rest names the workout that starts the cycle again. *) position : position; } type t = { name : string; workouts : workout list } (** A routine as the timeline renders it: workouts in cycle order, each with the rest that gates the next one. No rep bands, no [Prescription] entity. *) end module Routine_choice = struct type t = { id : string; name : string; workout_count : int } (** One selectable routine on the chooser: its id for the select form, its name, and how many workouts the cycle holds. *) end module Upcoming_workout = struct type exercise = { name : string; latest_load : float option; latest_reps : int option; } type group = { exercises : exercise list } type t = { routine_name : string; name : string; scheduled_on : string; groups : group list; } (** A prescribed workout that has no logging record yet. Latest performance is evidence for the next attempt, not a repetition target. *) end module Home = struct type gate = | Ready | Recovering of { status : string } (** Whether the next workout may begin, and the recovery status line to show while it may not. *) type t = { routine_id : string; (** For the begin/override forms. *) routine_name : string; next_workout : Upcoming_workout.t; gate : gate; } (** The overview while no workout is in progress. The recovery policy has already decided [gate] in the use case. The page only renders it. *) end module Workout = struct type movement = { name : string; (** The exercise name shown for this input row. *) load_field : string; (** Form field name for the load input. *) reps_field : string; (** Form field name for the reps input. *) reps_label : string; load_value : string option; (** Pre-fill when correcting a recorded slot. *) reps_value : string option; previous_load : string option; (** The most recent prior load for this exercise, pre-formatted as the numeric record to beat (for example ["12"]). [None] when the exercise has no earlier observation. It is an observed fact, not a target — the set still ends at failure. *) previous_reps : string option; (** The most recent prior reps for this exercise, pre-formatted as the numeric record to beat (for example ["22"]). [None] exactly when [previous_load] is [None] — the pair comes from the same observation. *) } (** One input row of a slot's record/correction form. The controller emits one movement for a single-exercise slot and two for a pre-exhaust pair, so the presenter renders whatever movements it is given and never inspects the prescription's [delivery] shape itself. *) type slot = { index : int; label : string; (** Exercise label for the accordion summary. *) movements : movement list; recorded_summary : string option; (** [Some description] once the slot is filled. [None] while outstanding. Drives the recorded/outstanding branch without an entity. *) selected_extension : string; (** The ending code to pre-select when correcting a recorded slot ("", "forced", "negatives", "static"). *) selected_extension_amount : string; (** Reps for forced reps/negatives or seconds for a static hold. Defaults to one for an outstanding slot. *) } type t = { name : string; (** The workout (prescription) name, for the page title. *) date : string; (** Workout start date, pre-formatted as "day month year" in UTC. *) slots : slot list; active_slot : int; started_at_unix : int; (** Workout start, for the elapsed timer. *) overridden : bool; (** Begun before recovery finished. *) all_recorded : bool; (** Every prescribed slot has a record. *) } (** A workout view: the slot the trainee is on, the record/correction state of each slot, and the timer and override facts. Carries no Evidence or Prescription entity. Extension/ending selection stays in the presenter form, since its option set is fixed UI vocabulary, not domain data. *) end module Exercise_comparison = struct type t = { exercise_name : string; current_load : float; current_reps : int; previous_load : float option; previous_reps : int option; } (** One observed exercise effort and its optional previous performance. *) end module Stimulus_group = struct type t = { rows : Exercise_comparison.t list } (** The exercises delivered by one stimulus, in performance order. *) end module Recorded_workout = struct type t = { id : string; (** Record id for edit and delete links. *) imported : bool; (** True for a promoted third-party import. Shown read-only; its title is the source title verbatim, and an oxblood outline pill, not text on the title, signals its imported status. It feeds progress per exercise but is never a prescribed native record. *) at_unix : int; (** Start time in Unix seconds, for ordering and selection. *) workout_name : string; performed_on : string; duration_previous_seconds : int option; (** The most recent earlier workout with the same prescription name, strictly before this one, in seconds, when both finished. [None] for a first performance of this workout or an unfinished one. *) duration_seconds : int option; (** This workout's own elapsed seconds, when finished. Carried with the previous duration so the presenter can derive a signed delta. *) performed_ago : string; (** Time since the workout started, already phrased for the view. *) next_scheduled : string option; (** Time until the next planned occurrence of this workout, already phrased for the view. *) groups : Stimulus_group.t list; (** Exercise rows grouped by the stimulus that delivered them. *) } (** One recorded workout for the selected-day detail panel. It carries the exercise rows grouped by stimulus, or one read-only group for an import. *) end module Selected_date = struct type t = { completed : Recorded_workout.t list; upcoming : Upcoming_workout.t option; } (** The selected calendar day: recorded workouts and the planned workout, if the routine schedules one. *) end module Calendar = struct type day = { day_of_month : int; workout : bool; scheduled_workout : string option; today : bool; selected : bool; } (** One dated cell. [workout] marks a completed workout. [scheduled_workout] names the next workout planned for that day, when one exists. *) type cell = | Blank | Day of day (** [Blank] pads the leading and trailing days of the grid that fall outside the month. *) type t = { selected_year : int; selected_month : int; (** 1..12; the month shown. *) weekday_headings : string list; (** Seven short labels, Monday first. *) weeks : cell list list; (** Rows of seven cells, Monday first. *) } (** A single month laid out as a week grid, above the logbook. Prev/next arrows and a Today link navigate months. The handler does the date arithmetic; the presenter renders the grid. *) end module Progress_page = struct type point = { date : string; (** Pre-formatted "YYYY-MM-DD" in UTC. *) at_unix : int; (** For x-axis positioning. *) weight : float; reps : int; } (** One observation on a chart: its load and reps at a point in time. *) type chart = { exercise_name : string; points : point list; (** Oldest first, at least two. *) } (** One exercise's merged native and imported history, ready to plot as a dual-axis line chart: weight on the left axis, reps on the right. *) type t = { charts : chart list } (** The progress page: one chart per exercise that has at least two observations. Exercises with too little history to draw a line are omitted. *) end module Import_warning = struct type t = { row : int; (** The source row that the adapter dropped. *) detail : string; (** Why the adapter dropped the row, already phrased. *) } (** One dropped import row, ready to render. The controller phrases the reason, so no adapter warning variant crosses the boundary. *) end module Import_set = struct type t = { row : int; (** The source row, for provenance. *) exercise_name : string; (** The export's exercise name, before mapping. *) set_type : string; (** The set type, already phrased. *) weight : string; (** The load, already formatted. *) reps : string; (** The rep count, already formatted. *) mapping : string option; (** The catalog exercise name this set maps to, when confirmed. *) } (** One provisional import set, ready to render. Carries only strings, no source-set entity. *) end module Import_workout = struct type t = { id : string; (** The workout id, for the per-workout forms. *) title : string; sets : Import_set.t list; blockers : string list; (** Why the workout cannot promote yet, each already phrased. Empty when the workout can promote. *) acknowledgement : string option; (** The retained normal-set acknowledgement, when present. *) revision : int; (** The workout revision, for provenance display. *) } (** One provisional import workout, ready to render: its sets, its blockers, and its acknowledgement state. No Workout_import entity. *) end module Import_review = struct type t = { batch_id : string; warnings : Import_warning.t list; workouts : Import_workout.t list; } (** A provisional import batch under review: the rows dropped on parse and the workouts awaiting cleaning and promotion. No batch entity crosses the boundary. *) end