Feedback Messages — Detailed Changes (2026-04-09)¶
Summary¶
Added a full-stack patient–therapist feedback loop across OktaPT-API (8 files), OktaPT-FE (8 files), and Okta-Mobile (8 files). The feature posts structured rich messages into the existing conversation thread when a patient finishes a workout with flagged feedback (high pain, high exertion, or written comments) and again when the therapist reviews the session and adds post-exercise notes. Therapist→patient delivery is debounced through a BullMQ queue with a 5-minute coalescing window so a therapist can keep editing notes without spamming the patient.
Cross-repo PRs: OktaPT-API #329, OktaPT-FE #470, Okta-Mobile #32. Gated by the feedbackNotifications per-tenant feature flag.
For the engineering reference see ../internal/feedback-messages.md. For the user-facing description see ../patient-therapist-feedback.md.
Data Flow¶
┌──────────────────────┐
│ Patient finishes │
│ workout │
│ POST /v2/workouts/ │
│ :id/complete │
└──────────┬───────────┘
│ inline (fire-and-forget dynamic import)
▼
┌──────────────────────────────────────┐
│ FeedbackNotificationService │
│ .sendPatientFeedbackToTherapist() │
│ - check feedbackNotifications flag │
│ - filter exercises (notes / pain≥7 │
│ / exertion≥9) │
│ - get-or-create conversation │
│ - idempotent insert PATIENT_FEEDBACK │
│ _SUMMARY message │
│ - push PATIENT_FEEDBACK to all docs │
└──────────┬───────────────────────────┘
▼
┌──────────────────────┐
│ Doctors see card + │
│ push notification │
└──────────┬───────────┘
│ tap "View Full Session"
▼
┌─────────────────────────────────────┐
│ Mobile: Session Viewer screen │
│ /(doctor)/(session-viewer)/view │
│ Web doctor: WorkoutDetailsModalV2 │
│ - per-exercise notes UI (mobile │
│ only — inline edit) │
└──────────┬──────────────────────────┘
│ PATCH /v2/exercises/exercises-in-workout/
│ :id/doctor-post-notes (or bulk)
▼
┌──────────────────────────────────────┐
│ doctorFeedback.ts route handler │
│ - validate (max 1000 chars) │
│ - verify ownership │
│ - update doctorNotesPostExercise[At] │
│ - enqueueFeedbackNotification( │
│ workoutId, doctorId, tenantId, │
│ 5*60*1000) │
└──────────┬───────────────────────────┘
▼
┌──────────────────────────────────────┐
│ feedbackNotificationQueue │
│ jobId: doctor-feedback-{workoutId} │
│ - if existing job in waiting/delayed/│
│ completed/failed → remove + reset │
│ - if active → return (in-flight job │
│ will pick up the latest data) │
│ - else add with 5min delay │
└──────────┬───────────────────────────┘
│ debounce window expires
▼
┌──────────────────────────────────────┐
│ worker.ts — feedbackNotificationWorker│
│ .sendTherapistFeedbackToPatient() │
│ - re-read all doctorNotesPostExercise│
│ - delete prior DOCTOR_FEEDBACK_NOTE │
│ for (workoutId, doctorId) │
│ - insert fresh DOCTOR_FEEDBACK_NOTE │
│ - push DOCTOR_FEEDBACK to patient │
└──────────┬───────────────────────────┘
▼
┌──────────────────────┐
│ Patient sees card + │
│ push notification │
└──────────────────────┘
Database Migration¶
OktaPT-API/prisma/migrations/20260409205726_add_feedback_notification_message_types/migration.sql
Adds two values each to the MessageType and NotificationType enums and adds a metadata JSONB column to Message. The new enum values are:
MessageType.PATIENT_FEEDBACK_SUMMARYMessageType.DOCTOR_FEEDBACK_NOTENotificationType.PATIENT_FEEDBACKNotificationType.DOCTOR_FEEDBACK
Message.metadata stores a discriminated union keyed on feedbackType (see internal doc for full schema). The doctorNotesPostExercise and doctorNotesPostExerciseAt columns on ExerciseInWorkout already existed (from August 2025) — this feature simply starts using them as the source of truth.
Backend Changes¶
New service — FeedbackNotificationService¶
src/services/feedbackNotificationService.ts (~383 lines)
Two public methods:
sendPatientFeedbackToTherapist(workoutId, tenantId)— synchronous, called inline at workout completion. Filters flagged exercises, idempotently inserts aPATIENT_FEEDBACK_SUMMARYmessage, and pushesPATIENT_FEEDBACKto every connected doctor.sendTherapistFeedbackToPatient(workoutId, doctorId, tenantId)— async, called by the worker after the debounce window. Bundles every saved post-exercise note for the workout, replaces any prior message from the same doctor for the same workout, and pushesDOCTOR_FEEDBACKto the patient.
Private helper getOrCreateFeedbackConversation(patientId, tenantId) resolves the right conversation:
- 0 doctors → returns
null(skip) - 1 doctor →
ONE_ON_ONEcontaining both - 2+ doctors →
GROUPtitled"{firstName}'s Doctors"containing every doctor + the patient (validates exact participant count to avoid reusing a stale group)
Both flows guard on the feedbackNotifications feature flag at the service layer, so disabling the flag stops new messages without affecting prior messages or the underlying note columns.
New queue — feedbackNotificationQueue¶
src/queue/feedbackNotificationQueue.ts
BullMQ queue named feedback-notification. The enqueueFeedbackNotification(data, delayMs) helper uses a deterministic job id doctor-feedback-{workoutId} and removes any prior job in waiting/delayed/completed/failed states before scheduling a new one — that's the debounce. If the prior job is active, it returns without scheduling (the in-flight worker will read the latest note rows at execution time, so the save is not lost).
Job options: 2 attempts, exponential backoff 5s, removeOnComplete: 200, removeOnFail: 500.
New routes — doctorFeedback.ts¶
src/routes/exercises/doctorFeedback.ts
Mounted via src/routes/exercises/index.ts (router.use("/", doctorFeedbackRouter)). Two endpoints:
| Method | Path | Purpose |
|---|---|---|
PATCH |
/exercises-in-workout/:exerciseInWorkoutId/doctor-post-notes |
Save one note |
PATCH |
/workouts/:workoutId/exercises/doctor-post-notes |
Save many notes in a single transaction |
Both validate doctorNotesPostExercise.length ≤ 1000, verify ownership (workout.doctorId === req.user.userId), update the row(s), and call enqueueFeedbackNotification(..., 300_000). The single-note endpoint has a tenant-1 ownership shortcut to preserve legacy behavior; the bulk endpoint always enforces ownership.
Worker registration¶
src/worker.ts
Worker 7 — feedback-notification queue consumer at concurrency 3. Constructs a FeedbackNotificationService directly using prismaWithoutTenant and the existing notificationService instance. The worker is registered alongside the existing 6 workers (clinical-assessment, personalized-exercise-video, exercise-canonicalization, smart-notification, personalized-exercise-injection, app-entrance-finalization).
Service singleton¶
src/services/instances.ts
Adds getFeedbackNotificationService() that lazily constructs a FeedbackNotificationService using prismaWithoutTenant and the existing getNotificationService() singleton. Reset hook (_resetForTesting) updated.
Workout completion hook¶
src/routes/legacy.ts (within POST /v2/workouts/:id/complete)
After RTM billing recalculation, a fire-and-forget dynamic import calls sendPatientFeedbackToTherapist(workout.id, workout.tenantId). Errors are logged but not thrown — feedback delivery failure must not break workout completion.
Web Frontend Changes (OktaPT-FE)¶
New components¶
components/messaging/FeedbackMessageBubble.tsx(~170 lines) — renders the structured feedback card. Branches onfeedbackType. InternalPainBadgeandExertionBadgecomponents handle the color thresholds (red ≥ 7 pain, orange ≥ 9 exertion).components/patient/PatientSessionDetailModal.tsx(~200 lines) — patient-side read-only modal opened from aDOCTOR_FEEDBACK_NOTEcard's "View Full Session" button. Fetches/workouts/patient/completedand filters client-side. Shows per-exercise sets/reps, pain/exertion badges, the patient's own notes (blue), and the therapist's notes (green).
Modified components¶
components/messaging/MessageBubble.tsx— type-dispatcher: returns<FeedbackMessageBubble>forPATIENT_FEEDBACK_SUMMARY/DOCTOR_FEEDBACK_NOTE, falls through to the default text bubble otherwise.components/messaging/ConversationView.tsx— accepts a newonViewSession?: (workoutId: number) => voidprop and forwards it to eachMessageBubble.lib/api/messaging.ts— addsPATIENT_FEEDBACK_SUMMARY/DOCTOR_FEEDBACK_NOTEto theMessageTypeunion andmetadata?: Record<string, any> | nullto theMessageinterface.
Modified pages¶
pages/patient/messages.tsx— ownsviewingSessionIdstate.handleViewSession(workoutId)opens<PatientSessionDetailModal>.pages/doctor/messages.tsx— ownssessionModalDatastate.handleViewSession(workoutId)callshttpClient.get('/v2/doctor/workout-sessions/{id}')and opens the existing<WorkoutDetailsModalV2>(no new modal — reuses the workout details modal already used elsewhere). Note editing on web is not part of this PR; therapists view the session on web but edit notes on mobile.pages/admin/feature-flags.tsx— appends{ key: "feedbackNotifications", label: "Feedback Notifications" }to the flag list.
Translations¶
public/locales/en/common.json and public/locales/ru/common.json add a new feedbackNotifications.* namespace with keys: patientFeedback, therapistNotes, pain, exertion, viewFullSession, sessionDetails, closeSessionDetails, sessionNotFound, sets, reps, yourNote, therapistNote, leftNotesOn.
Mobile Frontend Changes (Okta-Mobile)¶
New screens and route group¶
app/(doctor)/_layout.tsx(new) — registers the doctor route group inside a<RoleGuard allow="DOCTOR">. Includes screens for(clinical-assessment),(personalized-video),(exercises),(hep), and the new(session-viewer).app/(doctor)/(session-viewer)/_layout.tsx(new) — bare<Stack>for the session-viewer route group.app/(doctor)/(session-viewer)/view.tsx(new, ~520 lines) — the Session Viewer screen. ReadsworkoutIdfromuseLocalSearchParams, fetches/v2/doctor/workout-sessions/{workoutId}, and renders an exercise list with inline note editing:- Each exercise card shows actual vs planned sets/reps/duration, pain/exertion badges (red/orange thresholds match the web), patient notes (blue background), and therapist notes (green background)
- Tapping an existing therapist note enters edit mode (autoFocus
TextInput,maxLength={1000}) - Tapping "Add note for this exercise" on a blank exercise opens edit mode
- Save calls
PATCH /v2/exercises/exercises-in-workout/{id}/doctor-post-notesand updates local state — no full refetch - Cancel discards local edits
New components¶
components/messaging/FeedbackMessageBubble.tsx(~250 lines) — mobile equivalent of the web feedback card. Same metadata schemas, same dispatch onfeedbackType.
Modified screens¶
app/conversation/[id].tsx— adds ahandleViewSession(workoutId)callback passed down toMessageBubble. The callback inspectsuser.userType:DOCTOR→ routes to/(doctor)/(session-viewer)/view?workoutId=...PATIENT→ routes to/(tabs)/history
Modified components and services¶
components/messaging/MessageBubble.tsx— dispatchesPATIENT_FEEDBACK_SUMMARY/DOCTOR_FEEDBACK_NOTEto<FeedbackMessageBubble>.services/messagingService.ts— adds the two new message types andmetadata?: Record<string, any> | nullto theMessageinterface. (Also unrelated: this PR refactors the file to use the sharedhttpClientsingleton instead of inlinefetch.)services/notificationService.ts— addsinitializeNotificationListeners(navigate)which routes notification taps based ondata.type.PATIENT_FEEDBACKandDOCTOR_FEEDBACKtaps navigate to/(tabs)/messages. Handles both warm taps and cold-start taps viagetLastNotificationResponseAsync. Also configuressetNotificationHandlerat module level for foreground display.
Translations¶
locales/en.json and locales/ru.json add the same feedbackNotifications.* namespace as the web app, including patientNote, therapistNote, addNote.
Feature Flag¶
feedbackNotifications (boolean) on TenantSettings.features JSONB. Default off. The flag is checked at the service layer only — therapists can still save doctorNotesPostExercise when the flag is off, but no message is created and no push is sent. This means turning the flag on later activates the feature without losing any historical notes.
File-by-file change list¶
OktaPT-API¶
| File | Change | Description |
|---|---|---|
prisma/migrations/20260409205726_add_feedback_notification_message_types/migration.sql |
new | 4 enum values + Message.metadata column |
prisma/schema.prisma |
modified | enum updates + Message.metadata field |
src/services/feedbackNotificationService.ts |
new | Both feedback flows + conversation resolution |
src/queue/feedbackNotificationQueue.ts |
new | BullMQ queue + debounce helper |
src/routes/exercises/doctorFeedback.ts |
new | Single + bulk PATCH endpoints |
src/routes/exercises/index.ts |
modified | Mounts doctorFeedbackRouter |
src/routes/legacy.ts |
modified | Workout-completion hook into sendPatientFeedbackToTherapist |
src/services/instances.ts |
modified | getFeedbackNotificationService singleton |
src/worker.ts |
modified | Worker 7 — feedback-notification queue consumer |
OktaPT-FE¶
| File | Change | Description |
|---|---|---|
components/messaging/FeedbackMessageBubble.tsx |
new | Web feedback card |
components/patient/PatientSessionDetailModal.tsx |
new | Patient-side session detail modal |
components/messaging/MessageBubble.tsx |
modified | Type dispatcher to feedback bubble |
components/messaging/ConversationView.tsx |
modified | Forwards onViewSession prop |
pages/patient/messages.tsx |
modified | Wires session modal into messages page |
pages/doctor/messages.tsx |
modified | Opens WorkoutDetailsModalV2 from feedback card |
pages/admin/feature-flags.tsx |
modified | feedbackNotifications toggle |
lib/api/messaging.ts |
modified | MessageType union + metadata field |
public/locales/en/common.json |
modified | New feedbackNotifications.* keys |
public/locales/ru/common.json |
modified | Russian translations of the new keys |
Okta-Mobile¶
| File | Change | Description |
|---|---|---|
app/(doctor)/_layout.tsx |
new | Doctor route group with session-viewer |
app/(doctor)/(session-viewer)/_layout.tsx |
new | Stack layout for session-viewer group |
app/(doctor)/(session-viewer)/view.tsx |
new | Session Viewer screen with inline note editing |
components/messaging/FeedbackMessageBubble.tsx |
new | Mobile feedback card |
app/conversation/[id].tsx |
modified | Routes "View Full Session" by user role |
components/messaging/MessageBubble.tsx |
modified | Type dispatcher |
services/messagingService.ts |
modified | MessageType union + metadata field (also: refactor to shared httpClient) |
services/notificationService.ts |
modified | Notification tap routing for PATIENT_FEEDBACK / DOCTOR_FEEDBACK |
locales/en.json |
modified | New feedbackNotifications.* keys |
locales/ru.json |
modified | Russian translations of the new keys |