Skip to content

Mobile Doctor HEP Management

Overview

The mobile app now includes a doctor-only Manage HEP flow for quickly editing a patient's next 10 home exercise sessions while the doctor is still in the room with the patient.

This feature is designed for: - Fast manual plan edits on mobile - Bulk exercise insertion across multiple upcoming sessions - Per-session exercise customization - Lightweight AI-assisted modifications from a bottom composer - Patient-specific exercise media, including personalized exercise videos

The goal is to make plan adjustment on mobile feel native, fast, and safe, rather than forcing doctors into the heavier web editing workflow.


Gating

This feature is behind the tenant feature flag:

  • therapistMobileHepManagement

Where It Is Checked

  • Mobile UI: Okta-Mobile/components/doctor/DoctorDashboard.tsx
  • API read endpoint: OktaPT-API/src/routes/doctor/controller.ts
  • API save endpoint: OktaPT-API/src/routes/doctor/controller.ts
  • Shared access helper: OktaPT-API/src/helpers/verifyDoctorPatientAccess.ts

If the flag is disabled: - The Manage HEP button does not appear on the doctor dashboard - The doctor HEP session API returns 403

Tenant-Wide Access

HEP now uses the shared verifyDoctorPatientAccess() helper.

  • If allActivePatientsDashboard is true, doctors can manage any active accepted non-archived patient within the tenant.
  • Otherwise, the doctor must still have a direct active accepted connection to the patient.

This same rule now governs HEP reads, HEP saves, therapist AI edits, therapist AI rebalance, and patient-scoped exercise picker enrichment.


Entry Point

Doctor Dashboard

File: Okta-Mobile/components/doctor/DoctorDashboard.tsx

Each patient card in the in-clinic dashboard now supports: - Start Test as the primary action - Manage HEP as a secondary action when therapistMobileHepManagement is enabled - Record Exercise alongside Manage HEP when therapistPersonalizedExerciseVideo is also enabled

Files: - Okta-Mobile/app/(doctor)/_layout.tsx - Okta-Mobile/app/(doctor)/(hep)/_layout.tsx - Okta-Mobile/app/(doctor)/(hep)/manage.tsx

The HEP editor lives in the doctor route group:

/(doctor)/(hep)/manage

It receives: - patientId - patientName

through Expo Router params.


Screen Design

Main Screen

File: Okta-Mobile/app/(doctor)/(hep)/manage.tsx

The main screen is a single full-screen editor with: - Header: patient name + Editing next 10 sessions - Top CTA: Add exercises for each day - Scrollable list of the next 10 sessions - Persistent bottom AI composer

The screen uses modal/sheet-style subflows for: - Exercise picker - Dosage editor - Video preview - Delete scope confirmation - Save/rebalance confirmation

During save, the screen also shows an inline Saving plan... status banner below the top CTA so the save feels responsive immediately after the doctor taps Save.

Safe Areas

The HEP-specific add-exercise picker and dosage editor explicitly respect top safe areas on iPhone so header actions stay below the notch / Dynamic Island.

Files: - Okta-Mobile/components/doctor/hep/HepExercisePickerModal.tsx - Okta-Mobile/components/doctor/hep/HepDosageEditorModal.tsx


Session Window

Window Definition

The editor always works on the next 10 incomplete sessions after overdue rescheduling runs.

Server-Side Construction

Endpoint: GET /v2/doctor/patients/:patientId/hep-management?limit=10

Implementation: OktaPT-API/src/routes/doctor/controller.ts

The API: 1. Verifies doctor access to the patient 2. Triggers rescheduleOverdueWorkouts() for that patient before loading the HEP window 3. Loads incomplete workouts ordered by dateWorkoutCurrentlyScheduledFor, including any overdue workouts that were not moved 4. Marks sessions as protected if they started within the last hour 5. Attaches personalized exercise videos when available 6. Appends placeholder sessions until exactly 10 slots exist

Response Shape

{
  "sessions": [
    {
      "slotIndex": 1,
      "dayNumber": 1,
      "date": "2026-03-02T00:00:00.000Z",
      "workoutId": 123,
      "isPlaceholder": false,
      "isProtected": false,
      "focus": "",
      "exercises": [
        {
          "exerciseInWorkoutId": 456,
          "exerciseId": 12,
          "name": "Bridge",
          "exerciseType": "REPS_BASED",
          "exerciseBlock": "MAIN_SESSION",
          "sets": 3,
          "reps": 10,
          "duration": null,
          "weight": null,
          "thumbnailUrl": "...",
          "videoUrl": null,
          "photoUrl": null,
          "cloudflareStreamId": null,
          "kinescopeVideoId": "abc123",
          "personalizedVideo": {
            "id": 99,
            "cloudflareStreamId": "stream_123",
            "updatedAt": "2026-03-01T12:00:00.000Z"
          }
        }
      ]
    }
  ]
}

Protected Session Behavior

A session is treated as protected when: - timeWorkoutStart is set - isWorkoutCompleted is false - the workout started within the last hour

Protected sessions: - remain visible in the list - show a lock banner - cannot be edited - are preserved by the save API

This uses the same protection logic as bulk web plan editing.


Manual Editing Flow

Bulk Add

Doctors can tap:

  • Add exercises for each day

This opens the exercise picker, then the dosage editor, then inserts the configured exercises into every editable session in the 10-session window.

Per-Session Add

Each session card has its own:

  • Add

button that uses the same picker and dosage editor, but inserts only into that session.

Edit Existing Exercise

Tapping the non-thumbnail portion of an exercise row opens the dosage editor for that exercise.

Editable fields: - tracking type (REPS_BASED / TIME_BASED) - sets - reps or duration - weight - exercise block

Delete Existing Exercise

Trash removes an exercise from the current session.

If the same exerciseId appears in future editable sessions in the current 10-session window, the doctor is prompted to choose: - This session only - This and future sessions

If there are no future matches, only the current-session removal prompt is shown.

No Reordering in v1

Exercises are not drag-reorderable in this mobile flow.

New exercises append to the end of the session.


Exercise Picker

Component: Okta-Mobile/components/doctor/hep/HepExercisePickerModal.tsx

The picker reuses the mobile exercise-library patterns: - search - tag filters - segments - exercise cards - thumbnails

Shared components reused: - Okta-Mobile/components/exercises/ExerciseCard.tsx - Okta-Mobile/components/exercises/ExerciseListHeader.tsx - Okta-Mobile/components/exercises/TagFilterSheet.tsx - Okta-Mobile/hooks/useExercises.ts

Picker Behavior

  • Card body press: select / deselect exercise
  • Thumbnail press: open video preview if the exercise has a playable video source

Patient-Specific Media in Picker

The HEP screen owns a patient-scoped exercise catalog and refreshes it whenever the picker opens.

The picker requests exercises with patientId, so exercise results are enriched with: - personalizedVideo when that patient has a custom exercise demo

That means a doctor sees patient-specific thumbnails and playback directly in the picker, without needing to save and reload.

API Support

The exercises endpoint now accepts optional patientId:

GET /v2/exercises?lang=en&patientId=123

Implementation: OktaPT-API/src/routes/exercises/exercises.ts

When a doctor passes patientId, the backend: 1. verifies access to that patient 2. loads exercise results 3. attaches personalized video metadata for that patient's exercises


Exercise Media Behavior

Priority Rules

The HEP flow uses the same media priority as the rest of mobile:

  1. personalizedVideo.cloudflareStreamId
  2. kinescopeVideoId
  3. cloudflareStreamId
  4. videoUrl (YouTube)
  5. photoUrl

Cross-reference: - docs/personalized-exercise-videos.md - docs/exercise-media-rendering.md

Thumbnail Clickability

In the HEP flow, thumbnails are clickable anywhere a playable video source exists:

  • Current session exercise rows
  • Exercise picker cards

Non-thumbnail taps keep their original behavior: - HEP session row body = edit exercise - picker card body = select exercise

Initial Kinescope Fix

Some HEP session payloads could arrive with a thumbnail URL but without all raw video source IDs fully populated.

To keep thumbnail playback working immediately on first load, the mobile normalization layer now infers missing playable IDs from known thumbnail URL patterns: - Kinescope poster URL → infer kinescopeVideoId - Cloudflare thumbnail URL → infer cloudflareStreamId

Implementation: Okta-Mobile/lib/doctorHepUtils.ts

Preview Modal

Thumbnail playback uses:

  • Okta-Mobile/components/workout/ExerciseMediaPreviewModal.tsx
  • Okta-Mobile/components/workout/WorkoutVideoPlayer.tsx

This is a generic fullscreen preview modal for HEP media playback that supports: - personalized Cloudflare videos - Cloudflare Stream - Kinescope - YouTube


Personalized Videos in HEP

This feature fully supports patient-specific personalized exercise videos.

Where Personalized Videos Appear

If a patient has a personalized video for an exercise, that personalized media should be visible: - in the current HEP session list - in the add-exercise picker - immediately after adding the exercise to the plan - in thumbnail previews and fullscreen playback

Backend Support

The HEP session API uses:

  • attachPersonalizedVideos() from OktaPT-API/src/services/personalizedVideoService.ts

to attach patient-specific personalizedVideo objects to workout exercises.

Mobile Preservation

When an exercise is selected from the picker and added into the local HEP draft, the app preserves: - personalizedVideo - cloudflareStreamId - kinescopeVideoId - videoUrl - photoUrl - thumbnailUrl

in local state so the correct media is shown immediately, not only after save/reload.

Implementation: Okta-Mobile/lib/doctorHepUtils.ts


AI Editing

Bottom Composer

The HEP screen includes a persistent bottom composer:

  • Okta-Mobile/components/doctor/hep/HepAiComposer.tsx

Doctors can type natural language requests such as: - symptoms the patient is reporting - day-specific changes - adding or removing certain movements - intensity adjustments

Endpoint

POST /v2/ai/therapist-plan-edit

Request Shape

The mobile app sends: - patientId - currentPlan - userMessage - conversationHistory (currently empty in this mobile flow)

Apply Behavior

AI updates are applied only after the stream completes successfully.

The screen then shows: - a compact change summary - an Undo AI action that restores the previous local plan snapshot - immediate thumbnail/video hydration for AI-added exercises from the patient-scoped exercise catalog

Streaming

The mobile screen uses the shared XHR/SSE-like streaming hook:

  • Okta-Mobile/hooks/useStreamingRequest.ts

During streaming: - the keyboard is dismissed immediately - the normal session list is replaced with skeleton session cards - the skeleton session area remains vertically scrollable - workouts_count marks cards as ready as the AI completes each session

The mobile streaming hook stores both section and section_streaming events in streamingSections, which is how HEP consumes change_overview and workouts_count from the therapist-plan-edit stream.


Save Flow

Primary Save Endpoint

The HEP editor saves through:

PUT /v2/doctor/patients/:patientId/hep-management

This is a dedicated mobile persistence path for the 10-session HEP window. It avoids the heavier shared web edit-plan rewrite flow and updates sessions by workoutId when available.

Save Payload

The mobile app converts the local 10-session window into: - sessions[] - slotIndex - dayNumber - workoutId - full ISO date - focus - exercises

Empty placeholder sessions are still sent, but the backend treats them as: - delete the existing workout if workoutId is present - no-op if the slot is still a placeholder

Protected sessions are preserved by the backend even if they became protected after the screen first loaded.

When HEP performs the follow-up save after a background rebalance, the payload also includes: - notifyDoctorOnCompletion: true

focus now persists through Workout.doctorNote, so AI-generated or pre-existing session focus text survives save/reload.

Mobile adapter: Okta-Mobile/services/doctorHepService.ts


Save-Time AI Rebalance

Why It Exists

After manual editing, some sessions may end up: - below target (fewer than 3 exercises) - above target (more than 5 exercises)

The doctor can choose to let AI rebalance the plan around the changes they just made.

Trigger

After a successful manual save, the app checks only the first 5 editable session slots for rebalance issues.

If any session is outside the target range, it prompts with: - Rebalance plan? - {{count}} sessions are outside the target of 3-5 exercises per day. Let us rebalance the plan for you.

Actions: - No - Yes

Rebalance Endpoint

POST /v2/ai/therapist-plan-rebalance

Request Shape

{
  "patientId": 123,
  "currentPlan": [...],
  "lockedChanges": [
    {
      "dayNumber": 1,
      "lockedAddedExerciseIds": [12],
      "lockedModifiedExerciseIds": [44],
      "lockedRemovedExerciseIds": [87]
    }
  ],
  "rules": {
    "minExercisesPerSession": 3,
    "maxExercisesPerSession": 5
  }
}

AI Scope

Both AI edit and AI rebalance remain intentionally scoped to the first 5 session slots in the HEP window, even though the screen now displays and saves 10 sessions. Manual editing continues to work across the full 10-session list.

Locked-Change Contract

The rebalance endpoint is constrained to preserve doctor edits.

It must: - not remove locked added exercises - not re-add locked removed exercises - not modify dosage or block for locked modified exercises - return every session in the input window - enforce 3-5 exercises per workout in the response

Response Schema

Defined in:

  • OktaPT-API/lib/openai_response_formats/pt_therapist_plan_rebalance_schema.ts

UX

The rebalance flow is now a background handoff rather than an in-place blocking flow.

After the initial manual save succeeds: 1. the app immediately marks the editor clean and shows a lightweight saved acknowledgement 2. if any editable sessions are outside the 3-5 exercise target, it then prompts the doctor to rebalance 3. if the doctor chooses Yes, mobile starts the rebalance request in the background 4. the doctor is immediately routed back to the in-clinic dashboard 5. the in-clinic dashboard shows a green top toast: The rebalanced plan will be emailed to you shortly. 6. if rebalance succeeds, mobile saves the rebalanced plan again with notifyDoctorOnCompletion: true 7. the backend then sends the doctor a PLAN_UPDATE notification/email that the updated plan is ready

If the doctor chooses No: - the initial manual save stands as-is - the HEP screen shows the normal saved toast before the user leaves

If the background rebalance fails: - the original manual save still stands - the failure is logged on mobile - no follow-up success notification is sent


Local State Architecture

Types

File: Okta-Mobile/types/doctorHep.ts

Main types: - DoctorHepSession - DoctorHepExercise - DoctorHepEditLedgerEntry - DoctorHepSaveResult

Hook

File: Okta-Mobile/hooks/useDoctorHepEditor.ts

Responsibilities: - load initial 5-session window - maintain local working plan - track dirty state - track locked add/edit/remove exercise IDs for AI rebalance - apply AI updates - support AI undo - save through the doctor HEP service

Utilities

File: Okta-Mobile/lib/doctorHepUtils.ts

Responsibilities: - normalize HEP session payloads - build new exercise drafts with defaults - preserve media metadata - convert local state to API payloads - apply AI plan results to local sessions - summarize session count violations for rebalance


Components

HepSessionCard

File: Okta-Mobile/components/doctor/hep/HepSessionCard.tsx

Shows: - day label - date - exercise count - protected banner - per-exercise rows - per-session add action

HepExerciseRow

File: Okta-Mobile/components/doctor/hep/HepExerciseRow.tsx

Shows: - thumbnail - play overlay when preview is available - exercise name - dosage summary - block label - delete button

Interaction split: - thumbnail press = preview - row press = edit

HepExercisePickerModal

File: Okta-Mobile/components/doctor/hep/HepExercisePickerModal.tsx

Uses the exercise-library UI pattern for patient-aware exercise selection.

HepDosageEditorModal

File: Okta-Mobile/components/doctor/hep/HepDosageEditorModal.tsx

Edits: - type - sets - reps / duration - weight - block

HepAiComposer

File: Okta-Mobile/components/doctor/hep/HepAiComposer.tsx

Bottom AI input with: - summary card - undo action - send button


Translations

All HEP strings were added to: - Okta-Mobile/locales/en.json - Okta-Mobile/locales/ru.json

Key namespace: - hepManagement

Supporting additions were also made to: - common.yes - common.no - inClinic.manageHep

Cross-reference: - docs/mobile-translations.md


Verification

Implementation was verified with:

npm test -- --runTestsByPath src/routes/__tests__/doctor-hep-management.test.ts

in: - OktaPT-API

and:

npx tsc --noEmit

in: - Okta-Mobile

and:

npm run build

in: - OktaPT-API

This confirms the mobile HEP codepaths type-check and the backend HEP codepaths build successfully.


Key Files

Mobile

  • Okta-Mobile/app/(doctor)/(hep)/manage.tsx
  • Okta-Mobile/app/(doctor)/(hep)/_layout.tsx
  • Okta-Mobile/components/doctor/DoctorDashboard.tsx
  • Okta-Mobile/components/doctor/hep/HepSessionCard.tsx
  • Okta-Mobile/components/doctor/hep/HepExerciseRow.tsx
  • Okta-Mobile/components/doctor/hep/HepExercisePickerModal.tsx
  • Okta-Mobile/components/doctor/hep/HepDosageEditorModal.tsx
  • Okta-Mobile/components/doctor/hep/HepAiComposer.tsx
  • Okta-Mobile/components/workout/ExerciseMediaPreviewModal.tsx
  • Okta-Mobile/hooks/useDoctorHepEditor.ts
  • Okta-Mobile/hooks/useExercises.ts
  • Okta-Mobile/lib/doctorHepUtils.ts
  • Okta-Mobile/services/doctorHepService.ts
  • Okta-Mobile/services/exerciseService.ts
  • Okta-Mobile/types/doctorHep.ts
  • Okta-Mobile/types/exercise.ts
  • Okta-Mobile/locales/en.json
  • Okta-Mobile/locales/ru.json

API

  • OktaPT-API/src/routes/doctor/index.ts
  • OktaPT-API/src/routes/doctor/controller.ts
  • OktaPT-API/src/routes/__tests__/doctor-hep-management.test.ts
  • OktaPT-API/src/routes/ai/index.ts
  • OktaPT-API/src/routes/ai/controllers.ts
  • OktaPT-API/src/routes/exercises/exercises.ts
  • OktaPT-API/src/services/personalizedVideoService.ts
  • OktaPT-API/lib/openai_response_formats/pt_therapist_plan_rebalance_schema.ts
  • OktaPT-API/prisma/migrations/20260301120000_add_workout_patient_incomplete_schedule_index/migration.sql

  • docs/mobile-ui-ux-design.md
  • docs/mobile-keyboard-handling.md
  • docs/mobile-exercise-management.md
  • docs/patient-workout-management.md
  • docs/therapist-ai-plan-generation.md
  • docs/personalized-exercise-videos.md
  • docs/exercise-media-rendering.md
  • docs/ai-integration-guide.md
  • docs/feature-flags.md