Skip to content

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_SUMMARY
  • MessageType.DOCTOR_FEEDBACK_NOTE
  • NotificationType.PATIENT_FEEDBACK
  • NotificationType.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 a PATIENT_FEEDBACK_SUMMARY message, and pushes PATIENT_FEEDBACK to 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 pushes DOCTOR_FEEDBACK to the patient.

Private helper getOrCreateFeedbackConversation(patientId, tenantId) resolves the right conversation:

  • 0 doctors → returns null (skip)
  • 1 doctor → ONE_ON_ONE containing both
  • 2+ doctors → GROUP titled "{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 on feedbackType. Internal PainBadge and ExertionBadge components handle the color thresholds (red ≥ 7 pain, orange ≥ 9 exertion).
  • components/patient/PatientSessionDetailModal.tsx (~200 lines) — patient-side read-only modal opened from a DOCTOR_FEEDBACK_NOTE card's "View Full Session" button. Fetches /workouts/patient/completed and 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> for PATIENT_FEEDBACK_SUMMARY / DOCTOR_FEEDBACK_NOTE, falls through to the default text bubble otherwise.
  • components/messaging/ConversationView.tsx — accepts a new onViewSession?: (workoutId: number) => void prop and forwards it to each MessageBubble.
  • lib/api/messaging.ts — adds PATIENT_FEEDBACK_SUMMARY / DOCTOR_FEEDBACK_NOTE to the MessageType union and metadata?: Record<string, any> | null to the Message interface.

Modified pages

  • pages/patient/messages.tsx — owns viewingSessionId state. handleViewSession(workoutId) opens <PatientSessionDetailModal>.
  • pages/doctor/messages.tsx — owns sessionModalData state. handleViewSession(workoutId) calls httpClient.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. Reads workoutId from useLocalSearchParams, 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-notes and 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 on feedbackType.

Modified screens

  • app/conversation/[id].tsx — adds a handleViewSession(workoutId) callback passed down to MessageBubble. The callback inspects user.userType:
    • DOCTOR → routes to /(doctor)/(session-viewer)/view?workoutId=...
    • PATIENT → routes to /(tabs)/history

Modified components and services

  • components/messaging/MessageBubble.tsx — dispatches PATIENT_FEEDBACK_SUMMARY / DOCTOR_FEEDBACK_NOTE to <FeedbackMessageBubble>.
  • services/messagingService.ts — adds the two new message types and metadata?: Record<string, any> | null to the Message interface. (Also unrelated: this PR refactors the file to use the shared httpClient singleton instead of inline fetch.)
  • services/notificationService.ts — adds initializeNotificationListeners(navigate) which routes notification taps based on data.type. PATIENT_FEEDBACK and DOCTOR_FEEDBACK taps navigate to /(tabs)/messages. Handles both warm taps and cold-start taps via getLastNotificationResponseAsync. Also configures setNotificationHandler at 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