Skip to content

Exercise Library

Overview

The exercise library is the central exercise catalog for the Okta-Health platform. Two user roles interact with it differently:

  • Therapists manage the library: browse, create, edit, favorite, and select exercises when building patient workout plans.
  • Patients see exercises within their assigned workouts, watch demo videos, and provide feedback (pain, exertion, notes) after each exercise.

Written for both human developers and AI assistants working in this codebase.


Therapist Experience

Exercise Library Page

File: OktaPT-FE/pages/doctor/exercise-library.tsx

  • Header: "Exercise Library" title and subtitle
  • Three tabs: All Exercises (full catalog), My Private (exercises the doctor created as non-public), Favorites (starred exercises for quick access)
  • View toggle: Grid (card layout via VirtuosoGrid) vs List (sortable table via ExerciseTable)
  • Search bar: Real-time text search by exercise name
  • Filter button: Opens TagFilterDrawer with advanced filters (category, difficulty, muscle group, primary mover, creator, media type, tags). Shows badge with active filter count
  • Tag filter: Supports AND mode ("all selected tags must match") and OR mode ("any selected tag matches"). Active tags shown as removable chips below the toolbar
  • Create New button: Opens the CreateExerciseForm dialog

Exercise Card (Grid View)

File: OktaPT-FE/components/exerciseLibrary/ExerciseGrid.tsx

What each card displays in the grid:

  • Thumbnail: Priority fallback — Kinescope poster > Cloudflare thumbnail > YouTube thumbnail > uploaded photo > placeholder icon. Play icon overlay if video exists
  • Exercise name: Truncated with ellipsis if long (truncate class)
  • Creator info: UserCircleIcon + name (or "System" for platform exercises)
  • Tags: Up to 2 colored TagChip components, "+N more" indicator for additional tags
  • Favorite star: Appears on hover (desktop) / always visible if favorited. Click to toggle without page reload
  • Hover effect: Subtle scale (1.02) + shadow elevation

Exercise Detail Dialog

File: OktaPT-FE/components/exerciseLibrary/ExerciseDetailDialog.tsx

What doctors see when clicking an exercise:

  • Header: Exercise name, difficulty badge (color-coded via getDifficultyColor()), action buttons:
  • Star icon: toggle favorite
  • Pencil icon: edit (only if createdById === userId)
  • Trash icon: delete (only if createdById === userId)
  • Left column: Media display
  • If both video + photo: Tab.Group interface to switch between them
  • If one: Full display of available media
  • Supports Cloudflare Stream (@cloudflare/stream-react), Kinescope iframe, YouTube iframe
  • Right column: Exercise metadata
  • Tags displayed as colored TagChip components
  • Description text (or "No description available")
  • "Created by" credit line
  • Lazy loading: If exercise data is incomplete (e.g. AI-generated), fetches full data via GET /v2/exercises/exercise/:id

Exercise Selector (Workout Creation)

File: OktaPT-FE/components/ExerciseSelector/ExerciseLibraryDialog.tsx

How therapists select exercises when building a workout:

  1. Therapist clicks "Add Exercise" in workout builder → modal opens
  2. Search + filter bar at top (search input, tag filter button, folder dropdown)
  3. Folder dropdown: All Exercises / My Exercises / Favorites
  4. "Show Favorites First" checkbox: Reorders with favorites sorted to top
  5. Therapist clicks an exercise card → exercise added to workout with default parameters (sets/reps) → modal closes
  6. "Create New" button (bottom, dashed border): Opens create form inline, allowing immediate creation and selection

Creating and Editing Exercises

Create/Edit Form

File: OktaPT-FE/components/CreateExerciseForm.tsx

Two-column layout: Left = basic info + tags, Right = media uploads + external URLs

Form Fields
Field Required Input Type Validation Notes
Name Yes Text input required, aria-required="true" Pre-populated on edit
Description No Textarea (4 rows) None Free-text exercise instructions
Category Yes Dropdown Auto-populated from GET /exercise-categories First category auto-selected if empty
Public No Checkbox None Hidden for PHYSIOTHERAPISTRU, OTERA, OKTA brands via NEXT_PUBLIC_BRAND_NAME
Favorite No Checkbox None Shown for both new and existing exercises
Tags No Tag picker modal Via TagAssignmentModal Shows selected count and removable TagChip components
Media Upload

Two types of media, each with a two-phase upload process:

Video upload (accept="video/*"): 1. New exercises: File stored as local preview (object URL via URL.createObjectURL), uploaded AFTER exercise creation via signed S3 URL 2. Existing exercises: Uploaded immediately on file selection via handleMediaUpload() 3. Backend generates signed S3 upload URL → frontend uploads via XMLHttpRequest with progress tracking (0-100%) 4. After S3 upload: webhook token triggers Cloudflare Stream processing via POST /exercises/upload-complete/:token, storing cloudflareStreamId

Photo upload (accept="image/*"): - Same two-phase process as video - Direct S3 storage, no Cloudflare processing - uploadedPhotoKey stored in database

External URLs (separate from file uploads): - Optional type="url" fields for YouTube video URL and external photo URL - Stored as videoUrl and photoUrl respectively

Delete media: Per-type delete button → DELETE /exercises/{id}/media/{type}

Create vs Edit Flow
  • Create: POST /exercises → upload pending media → save tags via PUT /exercise/:id/tags → callback
  • Edit: PATCH /exercises/{id} → toggle favorite if changed → upload media → save tags → callback
  • Tag saving always replaces all tags with current selection (replaceExerciseTags)
  • Media upload errors are non-blocking — exercise saves even if upload fails
  • Upload Warning Modal: If user cancels during active upload, shows progress and offers "Wait" or "Cancel Upload"

Patient Experience

Exercise Preview (Before Workout)

File: OktaPT-FE/components/patient/ExerciseThumbnailRow.tsx

Patients see their upcoming exercises in a thumbnail row:

  • 84x84px thumbnails with play icon overlay if video exists
  • Exercise name + block tag (color-coded: Warm-up orange, Main blue, Supplemental purple, Cool-down green)
  • Sets/reps specification (e.g., "3x12 reps" or "3x30s" for time-based)
  • Tappable to preview video before starting (disabled if no video source)
  • Thumbnail priority: Personalized Video > Kinescope > Cloudflare > YouTube > photoUrl > placeholder

Exercise Execution (During Workout — Mobile)

Full-screen video playback with HUD overlay (see mobile-workout-flow.md):

  • Autoplay video when exercise becomes active
  • Doctor prescriptions overlay (if set by therapist):
  • Pain tolerance level (0-10, color-coded green/yellow/red)
  • Physical exertion level (0-10, color-coded blue/purple/orange)
  • Doctor's pre-exercise notes
  • Timer: Elapsed time in MM:SS
  • Info icon: Opens bottom sheet with full exercise details, description, and doctor guidelines
  • "Finish Exercise" button: Advances to next exercise

Patient Feedback (After Each Exercise — Mobile)

Two-stage feedback modal:

Stage 1 — Sets Completed: Grid of buttons (1 set, 2 sets, ... up to planned count). Auto-advances to Stage 2.

Stage 2 — How Did It Feel?: - Pain Level (0-10): 10-button grid, color-coded (0-3 green, 4-6 yellow, 7-10 red). Shows doctor's target if set - Exertion Level (0-10): Same layout, color-coded (0-3 blue, 4-6 purple, 7-10 orange) - Notes (optional): Free-text field ("How did the exercise feel?") - "Complete Exercise" button saves all feedback

Exercise Card in Web Workout View

File: OktaPT-FE/components/WorkoutExerciseCard.tsx

Shows exercises in a queue:

  • Numbered badge (gray=upcoming, brand-color=active, green=completed)
  • 96x96px thumbnail + name + sets/reps + description
  • Action buttons when active: "View Demo" (opens video), "Do Exercise with/without Recording"

Data Model

Exercise

Source: OktaPT-API/prisma/schema.prisma:398-448

model Exercise {
  id                      Int                    @id @default(autoincrement())
  name                    String
  exerciseNameEnglish     String?
  description             String?
  descriptionEnglish      String?
  videoUrl                String?                // YouTube URL
  photoUrl                String?                // External photo URL
  difficultyLevel         DifficultyLevel?
  targetMuscleGroup       String?
  primaryMover            String?
  secondaryMuscle         String?
  tertiaryMuscle          String?
  equipment               String[]
  createdAt               DateTime               @default(now())
  updatedAt               DateTime               @updatedAt
  categoryId              ExerciseCategory
  isActive                Boolean                @default(true)       // Soft-delete flag
  isPublic                Boolean                @default(false)      // Shared in tenant
  createdById             Int?
  uploadedVideoKey        String?                // S3 key
  uploadedPhotoKey        String?                // S3 key
  cloudflareStreamId      String?                // From S3→Cloudflare pipeline
  kinescopeVideoId        String?                // Legacy video provider
  isAIExercise            Boolean                @default(false)      // AI-generated placeholder
  tenantId                Int                    @default(1)          // Multi-tenant scope

  // Canonicalization fields
  nameNormalized          String?
  canonicalExerciseId     Int?
  mergedAt                DateTime?

  // Relations
  createdBy               User?                  @relation(...)
  workouts                ExerciseInWorkout[]
  templateExercises       TemplateExercise[]
  favoritedBy             UserFavoriteExercise[]
  tags                    ExerciseTag[]
  personalizedVideos      PersonalizedExerciseVideo[]
}

Key enums:

  • ExerciseCategory (12 values): UPPER_BODY, LOWER_BODY, CORE, CARDIO, STRETCHING, BALANCE, MOBILITY, REHABILITATION, MIDSECTION, FULL_BODY, OTHER, NOT_SPECIFIED
  • DifficultyLevel (10 values): BEGINNER, NOVICE, INTERMEDIATE, ADVANCED, EXPERT, MASTER, GRAND_MASTER, LEGENDARY, OTHER, NOT_SPECIFIED

Canonicalization Fields

nameNormalized, canonicalExerciseId, mergedAt — see exercise-canonicalization.md

Model Key Fields Purpose
UserFavoriteExercise doctorId, exerciseId (@@unique) Per-doctor favorites list
ExerciseInWorkout workoutId, exerciseId, planned/actual sets/reps/weight/duration, patient feedback, doctor notes Actual exercise instance in a patient workout
TemplateExercise templateId, exerciseId, planned params, exerciseBlock, orderInTemplate Exercise placed in a reusable workout template
CompletedExerciseSet exerciseInWorkoutId, setNumber, reps, weight, duration, poseData Per-set tracking data within a workout exercise
PersonalizedExerciseVideo patientId, exerciseId, doctorId, cloudflareStreamId, status Therapist-recorded per-patient exercise demos

Tag System

Source: OktaPT-API/prisma/schema.prisma:450-498

model Tag {
  id           Int              @id @default(autoincrement())
  key          String           // Language-agnostic identifier (e.g., "lower_body", "no_equipment")
  type         TagType
  color        String?          // Hex color for UI display (e.g., "#3B82F6")
  isGlobal     Boolean          @default(false)
  tenantId     Int?             // Null for global tags, specific ID for tenant-custom tags
  isActive     Boolean          @default(true)
  translations TagTranslation[]
  exercises    ExerciseTag[]

  @@unique([key, tenantId])
}

Tag Types

TagType Purpose Examples
REGION Body region Shoulders, Arms, Back, Legs, Chest
MUSCLE_GROUP Specific muscle Quads, Hamstrings, Glutes
MOVEMENT_PATTERN Movement type Push, Pull, Squat, Lunge, Rotation
EQUIPMENT Equipment required No Equipment, Dumbbells, Resistance Band
OBJECTIVE Training goal Strength, Flexibility, Balance, Endurance
SPECIALTY Clinical specialty Post ACL, Post Knee Surgery, Shoulder Rehab
POSITION Starting position Standing, On Ground, Seated, Lying Down
DIFFICULTY_CONTEXT Difficulty qualifier Beginner Friendly, Advanced Technique
CUSTOM Tenant-specific Any custom tags created by a tenant

Multi-Language Support

TagTranslation model provides one translation per language per tag (ISO 639-1 codes: "en", "es", "ru", "de"). The lang query param on tag endpoints controls which translation is returned. Falls back to the key field if no translation exists for the requested language.

model TagTranslation {
  tagId       Int
  language    String    // ISO 639-1
  name        String    // Translated tag name
  description String?

  @@unique([tagId, language])
}

Global vs Custom Tags

  • isGlobal: true + tenantId: null = system-wide tags available to all tenants
  • isGlobal: false + tenantId: N = tenant-specific custom tags
  • @@unique([key, tenantId]) prevents duplicate tag keys within the same tenant

Tag Filtering (AND/OR)

From OktaPT-FE/lib/utils/exercise.ts:86-98:

if (filters.tags && filters.tags.length > 0 && exercise.tags) {
  const exerciseTagKeys = exercise.tags.map((tag) => tag.key);
  if (filters.tagMode === "all") {
    // All selected tags must be present (AND)
    matchesTags = filters.tags.every((tagKey) =>
      exerciseTagKeys.includes(tagKey)
    );
  } else {
    // At least one selected tag must be present (OR)
    matchesTags = filters.tags.some((tagKey) =>
      exerciseTagKeys.includes(tagKey)
    );
  }
}

API Endpoints

Exercise CRUD

Method Path Auth Description
GET /v2/exercises Required List exercises (query: q, lang, limit, offset)
POST /v2/exercises Required Create exercise (triggers auto-canonicalization)
GET /v2/exercises/exercise/:exerciseId None Get single exercise
PATCH /exercises/:exerciseId Required Update exercise (creator only)
DELETE /exercises/:exerciseId Required Soft-delete exercise (creator only)

Media

Method Path Auth Description
POST /exercises/:id/media/upload-url Required Generate signed S3 upload URL (creator only)
POST /exercises/upload-complete/:token None (JWT token in path) Trigger Cloudflare Stream processing after S3 upload
DELETE /exercises/:id/media/:type Required Delete exercise media (creator only)
GET /exercises/:id/media Required Get signed download URLs for exercise media

Favorites

Method Path Auth Description
GET /exercises/favorites Required List user's favorite exercises
POST /exercises/:id/favorite Required Add exercise to favorites
DELETE /exercises/:id/favorite Required Remove exercise from favorites

Tags

Method Path Auth Description
GET /v2/exercises/tags Required Get all tags (query: type, lang, includeCustom)
GET /v2/exercises/tags/:key Required Get specific tag by key
POST /v2/exercises/tags Required Create custom tenant-specific tag
GET /v2/exercises/tag-types Required Get all available tag type enum values

Exercise Tags

Method Path Auth Description
POST /v2/exercise/:exerciseId/tags Required Add tags to exercise
DELETE /v2/exercise/:exerciseId/tags/:tagId Required Remove a tag from exercise
GET /v2/exercise/:exerciseId/tags Required Get all tags for an exercise
PUT /v2/exercise/:exerciseId/tags Required Replace all tags for exercise

Patient Feedback

Method Path Auth Description
PATCH /v2/exercises-in-workout/:id/patient-feedback Required Individual exercise feedback (pain 0-10, exertion 0-10, notes)
PATCH /v2/workouts/:workoutId/exercises/patient-feedback Required Bulk patient feedback per workout

Doctor Notes

Method Path Auth Description
PATCH /v2/exercises-in-workout/:id/doctor-post-notes Required Individual post-exercise notes (max 1000 chars)
PATCH /v2/workouts/:workoutId/exercises/doctor-post-notes Required Bulk doctor post-notes per workout

Security

Authentication

  • All exercise routes (except GET /v2/exercises/exercise/:exerciseId) require JWT via authentication middleware
  • Token sourced from Authorization: Bearer header, fallback to accessToken cookie
  • JWT validated against JWT_SECRET + active refresh token verified in database
  • requireDoctor middleware checks userType === "DOCTOR" for therapist-only actions

Source: OktaPT-API/src/middleware/auth.ts

Authorization & Ownership

Operation Who Enforcement
Browse/search Any authenticated user buildExerciseWhere("PUBLIC_OR_OWNER")
Create Authenticated doctor requireDoctor, createdById auto-set from JWT
Edit Creator only createdById check → 403
Delete Creator only createdById check → 403
Upload/delete media Creator only createdById check → 403
Add/remove tags Any authenticated user Exercise must be visible per scope
Patient feedback Assigned patient only workout.patientId === req.user.userId check
Doctor notes Assigned doctor only workout.doctorId === req.user.userId check

Tenant Isolation

  • runWithTenant() sets AsyncLocalStorage per-request context from JWT tenantId
  • Domain-based fallback for unauthenticated requests (lookup Tenant by hostname)
  • Prisma middleware auto-injects tenantId into all queries
  • Cross-ref exercise-scope-service.md

Source: OktaPT-API/src/middleware/tenant.ts

Input Validation

Field Rule Source
patientRecordedPainLevel 0-10 integer patientFeedback.ts
patientRecordedPhysicalExertion 0-10 integer patientFeedback.ts
doctorNotesPostExercise Required string, max 1000 chars doctorFeedback.ts
Tag type Must be valid TagType enum tags.ts
Search limit Capped at 100 exerciseService.ts

Media Security

  • Private S3 bucket (ACL: "private")
  • Signed upload/download URLs (1-hour expiration via getSignedUrl(..., { expiresIn: 3600 }))
  • Videos processed through Cloudflare Stream (not served from S3 directly)
  • S3 key format: exercise-media/{exerciseId}/{type}-{timestamp}{ext}
  • Frontend file type enforcement via accept="video/*" and accept="image/*" attributes

Search and Filtering

searchExercises() in OktaPT-API/src/services/exerciseService.ts:

  • Prisma contains with mode: "insensitive" (case-insensitive)
  • Offset pagination via skip/take
  • Results capped at 100 per request
  • Scope: buildExerciseWhere("PUBLIC_OR_OWNER") — only public exercises and exercises created by the requesting user

Frontend Filtering

filterExercises() in OktaPT-FE/lib/utils/exercise.ts:

Dimension Logic
Name Case-insensitive includes
Category Case-insensitive includes on categoryId
Difficulty Case-insensitive includes on difficultyLevel
Created by Match on firstName + lastName
Media video (has videoUrl), photo (has photoUrl), none (neither), all (any)
Tags AND mode: all selected tags must be present. OR mode: at least one selected tag

Exercise Visibility

  • PUBLIC_OR_OWNER — public exercises + exercises created by the user (default for library browsing)
  • PUBLIC_ONLY — only public exercises
  • PUBLIC_OR_HAS_VIDEO — public exercises + any exercise with a video source

Cross-ref exercise-scope-service.md


Exercise Blocks and Types

Exercise Types

Source: OktaPT-API/prisma/schema.prisma:602-605

Type Measurement Tracked Fields
REPS_BASED Repetitions plannedSets/actualSets, plannedReps/actualReps, plannedWeight/actualWeight
TIME_BASED Duration (seconds) plannedSets/actualSets, plannedDuration/actualDuration

Exercise Blocks

Source: OktaPT-API/prisma/schema.prisma:607-612

Block Order Purpose
WARM_UP 0 Preparation exercises
MAIN_SESSION 1 Core workout exercises
SUPPLEMENTAL_WORK 2 Accessory exercises
COOL_DOWN 3 Recovery exercises

Block ordering enforced by validateExerciseReorder() in OktaPT-FE/lib/utils/exerciseBlockValidation.ts — prevents drag-and-drop moves that would violate block sequence (e.g., placing a Cool-down before a Warm-up).


Exercise-to-Workout Pipeline

Three-level chain:

Exercise (catalog entry)
  → TemplateExercise (in reusable WorkoutTemplate)
    → ExerciseInWorkout (in actual patient Workout)
  • Exercise: Shared library entry, reusable across templates and workouts
  • TemplateExercise: Exercise placed in a WorkoutTemplate with planned params (sets/reps/weight/duration), exerciseBlock, doctor pre-notes, and prescribed tolerance levels
  • ExerciseInWorkout: Actual instance in a patient workout — tracks planned vs actual values, patient feedback, doctor post-notes, per-set data via CompletedExerciseSet

Prescription & Feedback Fields

Group Fields Scale
Doctor Prescription prescribedPainToleranceLevel, prescribedPhysicalExertionLevel, doctorNotesPreExercise 0-10
Patient Feedback patientRecordedPainLevel, patientRecordedPhysicalExertion, patientNotes 0-10
Doctor Post-Notes doctorNotesPostExercise, doctorNotesPostExerciseAt max 1000 chars

Cross-ref patient-workout-management.md


Document Covers
exercise-canonicalization.md AI-powered duplicate detection and merge pipeline
exercise-media-rendering.md Video/thumbnail priority fallback across providers
exercise-scope-service.md Visibility filtering (buildExerciseWhere, applyExerciseScope)
personalized-exercise-videos.md Therapist-recorded per-patient exercise demos
patient-workout-management.md Workout creation, editing, and plan management
mobile-workout-flow.md Mobile workout execution and exercise feedback

Key Files

File Purpose
OktaPT-API/prisma/schema.prisma Exercise, Tag, ExerciseTag, ExerciseInWorkout, TemplateExercise, CompletedExerciseSet models
OktaPT-API/src/services/exerciseService.ts searchExercises(), createExerciseShared()
OktaPT-API/src/services/exerciseScopeService.ts buildExerciseWhere() visibility helpers
OktaPT-API/src/routes/exercises/exercises.ts V2 exercise list and create routes
OktaPT-API/src/routes/exercises/tags.ts Tag CRUD routes
OktaPT-API/src/routes/exercises/exerciseTags.ts Exercise-tag association routes
OktaPT-API/src/routes/exercises/getExercise.ts Single exercise GET (public)
OktaPT-API/src/routes/exercises/patientFeedback.ts Patient feedback routes (individual + bulk)
OktaPT-API/src/routes/exercises/doctorFeedback.ts Doctor post-exercise notes routes
OktaPT-API/src/routes/legacy.ts Exercise CRUD, media upload/delete, favorites
OktaPT-API/src/middleware/auth.ts authentication, requireDoctor middleware
OktaPT-API/src/middleware/tenant.ts tenantMiddleware — per-request tenant context
OktaPT-FE/pages/doctor/exercise-library.tsx Exercise library page (tabs, filters, views)
OktaPT-FE/components/CreateExerciseForm.tsx Create/edit form with media upload
OktaPT-FE/components/exerciseLibrary/ExerciseGrid.tsx Grid view with VirtuosoGrid
OktaPT-FE/components/exerciseLibrary/ExerciseDetailDialog.tsx Exercise detail dialog
OktaPT-FE/components/ExerciseSelector/ExerciseLibraryDialog.tsx Exercise selector for workout creation
OktaPT-FE/components/patient/ExerciseThumbnailRow.tsx Patient exercise preview thumbnails
OktaPT-FE/components/WorkoutExerciseCard.tsx Web workout exercise card
OktaPT-FE/lib/utils/exercise.ts filterExercises(), thumbnail/video utilities
OktaPT-FE/lib/utils/exerciseBlockValidation.ts validateExerciseReorder(), BLOCK_ORDER