Workouts — code map
API (NestJS)
Base path: /organizations/:orgId
| Route | File | Method |
|---|---|---|
POST /workouts | apps/api/src/workouts/workouts.controller.ts | WorkoutsService.create |
GET /workouts | same | findAll (filters: programId, q) |
GET /workouts/:id | same | findById |
PATCH /workouts/:id | same | update (accepts ?assignmentId for lazy-fork redirect) |
DELETE /workouts/:id | same | remove (soft delete; rejects snapshots) |
PUT /workouts/:id/sections | same | setSections (accepts ?assignmentId) |
PATCH /workouts/:id/movements/:movementId/prescription | same | updateMovementPrescription (accepts ?assignmentId) |
GET /workouts/:id/movements/:movementId/comments | same | listMovementComments |
POST /workouts/:id/movements/:movementId/comments | same | createMovementComment |
Module: apps/api/src/workouts/workouts.module.ts registers the BullMQ workouts-enrichment queue, plus dependencies on MembershipsModule, UsersModule, UploadsModule, R2Module, PushNotificationsModule, and WorkoutAssignmentsModule (for forkSnapshotIfNeeded). It provides and exports WorkoutsService and SectionFidelityService.
DTOs
apps/api/src/workouts/dto/create-workout.dto.ts—CreateWorkoutDtoapps/api/src/workouts/dto/update-workout.dto.ts—UpdateWorkoutDtoapps/api/src/workouts/dto/set-sections.dto.ts—SetSectionsDto,SectionDto(incl.part, validated bysectionPartSchemain the service),MovementDtoapps/api/src/workouts/dto/update-movement-prescription.dto.ts—UpdateMovementPrescriptionDto(field-for-field whitelist ofPrescriptionSchema, incl. calories/bySex,percent_reference+reference,height,levels,group_load)apps/api/src/workouts/dto/create-exercise-comment.dto.ts—CreateExerciseCommentDto
Key service methods (apps/api/src/workouts/workouts.service.ts)
resolveEditTarget(workoutId, assignmentId?)— delegates toWorkoutAssignmentsService.forkSnapshotIfNeeded; returns the snapshot id when assignmentId is supplied.requireWorkoutBuilder(orgId)— tier gate for structured-mode writes.mapMovementToSnapshot/resolveSnapshotMovement— locate the snapshot’s mirror of a library movement after fork.validateStructuredSections— every section write:sectionInputSchema, then the part invariant (findSectionPartIssues→ 400invalid_section_parts), the group-load rule (findGroupLoadIssues→ 400invalid_prescription), shape config and progression bounds.insertSections— the one section/movement insert (create andreplaceSections): normalizedsourceMarks, validatedscores, validatedpart.toDetailResponse— coachGET /workouts/:idsection mapper (incl.part); member reads usetoWorkoutSectionResponseinapps/api/src/common/workout-section.ts.deepCopyWorkout— lives inWorkoutAssignmentsService.deepCopyWorkout(apps/api/src/workout-assignments/workout-assignments.service.ts). Copies sections (incl.source_text/source_marks/scores/part), movements, and comments. Program-release staleness compares the same columns inProgramSessionsService.contentOf.WorkoutsService.computeDisplayName(static) — title or first line of description, ≤60 chars.
Section fidelity (apps/api/src/workouts/section-fidelity.service.ts)
SectionFidelityService.fill(orgId, sections, { scoring })— loads every name of the sections’ exercises (name, aliases, the org’s override name/aliases) and runsfillSectionFidelity.fillSectionFidelity(pure) — fills omittedscores(deriveSectionScoreSlots,apps/api/src/ai/parse/derive-score-slots.ts), omittedsourceMarks(anchorMovementMarks) and omitted workout scoring (deriveScoring); returnsFidelityWarning[](missing_prescription,percent_base_mismatch,text_only_section,invented_score_slot). Called by the MCP upsert and the parse tool.
Web (Next.js)
| Route | File path |
|---|---|
| Library list | apps/web/src/app/[lang]/(protected)/dashboard/workouts/page.tsx |
| Unified freeform + translation creator | apps/web/src/app/[lang]/(protected)/dashboard/workouts/new/page.tsx — freeform by default; structured translation via ?mode=translate or ?jobId= |
| Structured builder (new) | apps/web/src/app/[lang]/(protected)/dashboard/workouts/new/builder/page.tsx |
| Workout detail | apps/web/src/app/[lang]/(protected)/dashboard/workouts/[id]/page.tsx |
| Edit | apps/web/src/app/[lang]/(protected)/dashboard/workouts/[id]/edit/page.tsx |
| Member whiteboard | apps/web/src/app/[lang]/(protected)/(member)/whiteboard/page.tsx |
Key components
apps/web/src/components/overview/workouts/workout-list-view.tsx— coach library listapps/web/src/components/overview/workouts/workout-card.tsx— list itemapps/web/src/components/overview/workouts/workout-builder.tsx— top-level builder shellapps/web/src/components/overview/workouts/workout-builder/— builder internals:use-builder-state.ts— central state machine (draft sections, movements, undo/redo);updatePart(sectionKey, change)applies a part edit across every block of the part (applyPartChange), and a section reorder re-normalizes partsuse-builder-save.ts— POST/PUT orchestrationsection-payload.ts,prescription-codec.ts,superset.ts— re-export shims; the implementation lives inlibs/shared/src/lib/workout-builder/because the API’s parse eval and unit specs run the same serializer, and an app may not import another app’s sourceshape.ts— section shape (linear/amrap/emom/…) registry helpersvolume.ts— total-volume computation for the right-rail summarykeys.ts— stable React keys across reorders;nextPartKey()for a new partcomponents/section-editor.tsx,part-fieldset.tsx(a block’s part: start a new part here / same part as the previous block / leave; part title, class-clock window, note, optional — every edit goes throughupdatePart),prescription-panel.tsx(calories as a rep kind with an optional ♀/♂ split; percent-of-reference loads, height, levels and group loads pass through unedited),builder-right-rail.tsx,builder-header.tsx,builder-action-bar.tsx,ribbon-action.tsx,shape-renderers.tsx(EMOM interval window max),sortable-section.tsx,section-type-combobox.tsx,num-input.tsx,movement-demo-dialog.tsx,member-preview-panel.tsx(the live “what members see” board in the right rail, structured mode only)
apps/web/src/components/overview/workouts/workout-builder/components/builder-text-editor.tsx— the freeform (text-only) editor: title,dir="auto"text, scoring, time cap, and the optional “Convert to structured workout” card. Rendered by the builder’s freeform mode and by the schedule sheets’ quick-create (apps/web/src/components/overview/schedule/quick-create-workout-dialog.tsx, a height-bounded scrolling dialog whose untitled placeholder is the name the API saves:dailyWorkoutTitle(session day, class type))apps/web/src/components/overview/workouts/paste-workout/— AI paste-to-structure flow; documented in workout-parseapps/web/src/components/overview/workouts/workout-execution-view.tsx— read-only display for non-edit contexts; renders the workout throughVerbatimWorkoutapps/web/src/components/overview/workouts/workout-mode.ts—effectiveWorkoutMode+formatWorkoutSectionsText(the flat-text workout peek; a part opens with its own heading block — title, clock, optional, note)apps/web/src/components/overview/workouts/exercise-autocomplete.tsx,exercise-library-browser.tsx,exercise-video-dialog.tsx— exercise pickersapps/web/src/components/member/workouts/workout-detail-drawer.tsx— member’s read view of the snapshot (Plan tab: the day brief —workout.description— above the parts)apps/web/src/components/shared/verbatim/— the coach-facing preview of the member app’s whiteboard, used by the workout page, the builder’s right rail and the paste review:verbatim-workout.tsx(the coach’s text line for line, movement marks →inline-demo-card.tsx, score lines in the org accent),from-builder-sections.ts(builderSectionState[]→ the same input the stored workout gives it). Mirrors the app’ssrc/components/workout/verbatim-sections.tsx; the member web views keepshared/prescription/whiteboard-section.tsxapps/web/src/components/shared/workout-parts/—section-parts.tsx(SectionParts: groups a workout’s blocks bygroupSectionsIntoPartsand puts each part underpart-header.tsx’sPartHeader— title, class clock “0:00–11:00”, optional badge, part note; blocks outside a part render with no wrapper). Every section renderer goes through it: the member drawer,workout-execution-view.tsx,verbatim-workout.tsx(so the paste preview and the builder’s member preview too),shared/workout-section.tsx(feed / session drawer / program page / program-session sheet card), the course player and the paste preview’s whiteboard listapps/web/src/components/shared/prescription/superset-groups.ts—groupBySupersetfor the whiteboard, prescribed and card renderers; a group whose movement carriesgroup_loadshows that load (and its progression) on the superset header instead of the movement row
DB (Drizzle, PostgreSQL)
All schema in libs/db/src/lib/schema/workouts.ts:
| Table | Purpose |
|---|---|
workouts | Library and snapshot rows. is_snapshot discriminates. |
workout_sections | Ordered sections per workout; carries shape + config JSONB for AMRAP/EMOM/etc., the verbatim source_text/source_marks, the scores slots members log for that part, and the part JSONB grouping consecutive blocks into the coach’s day parts. |
workout_movements | Ordered movements per section; carries prescription JSONB + label + superset_group. |
Comments: exercise_comments in libs/db/src/lib/schema/comments.ts.
Attachments: attachments in libs/db/src/lib/schema/attachments.ts (polymorphic ownerType='exercise_comment').
Shared
| File | Exports |
|---|---|
libs/shared/src/lib/schemas/workout.schema.ts | workoutResponseSchema, workoutSummaryResponseSchema, workoutSectionSchema, workoutMovementSchema, setSectionsInputSchema, sectionScoreSlotSchema/sectionScoreSlotsSchema/sectionScoreScoringValues (per-section score slots), scoring/section-type/mode enum values |
libs/shared/src/lib/schemas/prescription.ts | PrescriptionSchema + sub-schemas (reps, load) |
libs/shared/src/lib/schemas/section-shape.ts | Section container shape registry (linear / amrap / emom / for_time / tabata / rep_scheme / rounds / intervals) — TODO: verify exact filename casing |
libs/shared/src/lib/source-marks.ts | Verbatim whiteboard: sourceMarkSchema/SourceMark, detectScoreMarks, reanchorMovementMarks (follow mentions across a coach’s edit), normalizeSourceMarks (the stored invariant), anchorMovementMarks (link unmarked text: each movement claims its earliest free mention of any of its names, case/plural/joiner-insensitive, never on a score line or inside another movement’s longer name), sectionSourceFromDraft (ParseDraft → per-section text + movement mark positions). Dependency-free apart from the zod schema — mobile copies it like whiteboard-row.ts |
libs/shared/src/lib/verbatim-lines.ts | The coach’s text as a renderer lays it out: splitVerbatimLines (lines → marked/unmarked runs), leadingHeading (an opening “WARM UP” is the section’s title, not its first line), toParagraphs, isConnectorLine, synthesizeSectionSource/resolveSectionSource (a section with no sourceText rebuilt into the same shape — height rides on the movement line, each scaling level follows as its own line). Mirrored byte-for-byte by the app at src/lib/verbatim-lines.ts |
libs/shared/src/lib/whiteboard-row.ts | The member “poster” formatter: formatWhiteboardLeading (reps / calories / time / distance, sex-labelled counts), formatWhiteboardLoad (incl. percent_reference → ”@ 75% of Back Squat 3RM”), formatWhiteboardHeight, formatWhiteboardLevels, formatWhiteboardProgression, formatWhiteboardDetail. Mirrored byte-for-byte by the app at src/lib/whiteboard-row.ts; the coach’s dense line is apps/web/src/components/shared/prescription/format-prescription.ts (formatPrescription, formatPrescriptionLevels, formatSectionHeader incl. the EMOM window “EMOM 3 × 3:00–3:30”) |
libs/shared/src/lib/video-embed.ts | parseVideoEmbed — a demo link → youtube / vimeo / file, or nothing embeddable. Mirrored by the app (which adds its own WebView document builder) |
libs/shared/src/lib/workout-builder/ | Pure builder state types (SectionState incl. part, MovementState, PrescriptionState, TimerConfig) and conversion: section-payload.ts (buildSectionsPayload, deriveScoring — canonical serialization/scoring shared by builder save, parse commit, and golden eval), prescription-codec.ts (flat PrescriptionState ⇄ canonical Prescription JSONB; height, levels and percent references are carried whole), parts.ts (applyPartChange — start / join previous / leave / edit a part across all its blocks; normalizeSectionParts — restores the contiguous, identical-copies invariant the API enforces), superset.ts (superset grouping + derived labels), draft-to-builder.ts (ParseDraft → SectionState[]) |
libs/shared/src/lib/constants/platform-tiers.ts | tierHasFeature, basic_workouts, workout_builder feature constants |
Tests
| Type | File |
|---|---|
| Builder state unit | apps/web/src/components/overview/workouts/workout-builder/use-builder-state.int.spec.tsx |
| Builder save unit | apps/web/src/components/overview/workouts/workout-builder/use-builder-save.unit.spec.ts |
| Prescription codec | apps/web/src/components/overview/workouts/workout-builder/prescription-codec.unit.spec.ts |
| Section shape | apps/web/src/components/overview/workouts/workout-builder/shape.unit.spec.ts |
| Superset | apps/web/src/components/overview/workouts/workout-builder/superset.unit.spec.ts |
| Volume | apps/web/src/components/overview/workouts/workout-builder/volume.unit.spec.ts |
| Builder keys | apps/web/src/components/overview/workouts/workout-builder/keys.unit.spec.ts |
| Builder derive | apps/web/src/components/overview/workouts/workout-builder-derive.unit.spec.ts |
| Member detail drawer | apps/web/src/components/member/workouts/workout-detail-drawer.int.spec.tsx |
| Builder parts (start / join / leave / edit, invariant property) | libs/shared/src/lib/workout-builder/parts.unit.spec.ts |
| Builder part editing → save payload, EMOM interval window | apps/web/src/components/overview/workouts/workout-builder.int.spec.tsx |
| Member formatters (calories, height, levels, % of reference) | libs/shared/src/lib/whiteboard-row.unit.spec.ts, apps/web/src/components/shared/prescription/format-prescription.unit.spec.ts |
| Comments panel | apps/web/src/components/messages/workout-comments-panel.int.spec.tsx |
| E2E builder | apps/web/e2e/specs/workout-builder.spec.ts |
| E2E member workout | apps/web/e2e/specs/member-workout.spec.ts |
| E2E comments | apps/web/e2e/specs/workout-comments.spec.ts |
| E2E lazy-fork | apps/web/e2e/specs/lazy-fork.spec.ts |
| API section writes, parts, group loads, quick edit | apps/api/src/workouts/workouts.service.unit.spec.ts |
| API quick-edit DTO whitelist | apps/api/src/workouts/dto/update-movement-prescription.dto.unit.spec.ts |
| API section fidelity fill + warnings | apps/api/src/workouts/section-fidelity.service.unit.spec.ts |
| API verbatim text + parts through every read, release and fork (Postgres) | apps/api/src/workouts/workout-source-text.int.spec.ts |
| API member section serializer | apps/api/src/common/workout-section.unit.spec.ts |
| API program-release staleness | apps/api/src/workout-assignments/program-sessions.int.spec.ts |
| Source marks (incl. anchoring) | libs/shared/src/lib/source-marks.unit.spec.ts |