App Entrance Tracking — Detailed Changes (2026-04-08)
Summary
Added a full-stack "App Entrance Tracking" system across OktaPT-API (14 files), OktaPT-FE (8 files), and Okta-Mobile (5 files). When a patient opens the app without completing a workout, the system creates an inferred workout after a 2-hour delay window, giving therapists engagement visibility.
Data Flow
┌─────────────────┐ ┌─────────────────┐
│ Web Dashboard │ │ Mobile Dashboard │
│ (OktaPT-FE) │ │ (Okta-Mobile) │
└────────┬─────────┘ └────────┬─────────┘
│ POST /v2/app-entrances/log │
└────────────┬──────────────────────┘
▼
┌────────────────────────┐
│ appEntrances/ │
│ controller.ts │
│ (check flag, compute │
│ localDate, upsert │
│ AppEntrance) │
└────────────┬───────────┘
│ enqueueAppEntranceFinalization()
▼
┌────────────────────────┐
│ BullMQ Queue │
│ "app-entrance- │
│ finalization" │
│ (2h delay) │
└────────────┬───────────┘
│ after delay
▼
┌────────────────────────┐
│ Worker (worker.ts) │
│ → processAppEntrance │
│ (jobs/process- │
│ AppEntrance.ts) │
└────────────┬───────────┘
│
┌───────┴────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Same-day real │ │ No real │
│ workout? │ │ workout │
│ → CANCELED │ │ → Create │
└──────────────┘ │ mirror │
│ workout │
└──────┬───────┘
│
┌─────────┴──────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Streak │ │ RTM Billing │
│ Rebuild │ │ Recalculate │
└──────────────┘ └──────────────┘
Status Lifecycle State Machine
┌──────────────────────────┐
│ PENDING │
│ (created on app open) │
└──┬────┬────┬────┬────────┘
│ │ │ │
real workout ────┘ │ │ └──── feature disabled
completed │ │ at finalization
│ │ │ │
▼ │ │ ▼
┌──────────────────────┐ │ │ ┌─────────────────────┐
│ CANCELED_BY_REAL_ │ │ │ │ SKIPPED_FEATURE_ │
│ WORKOUT │ │ │ │ DISABLED │
└──────────────────────┘ │ │ └─────────────────────┘
│ │
no source ───────┘ └──── source workout found,
workout no same-day real workout
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ SKIPPED_NO_SOURCE_ │ │ FINALIZED_TO_INFERRED_ │
│ WORKOUT │ │ WORKOUT │
└──────────────────────┘ └──────────────────────────┘
OktaPT-API Changes (14 files)
New Files
| File |
Purpose |
prisma/migrations/20260409011908_add_app_entrance_tracking/migration.sql |
Creates AppEntranceStatus enum, AppEntrance table, adds completedViaAppEntrance and mirroredFromWorkoutId to Workout |
src/routes/appEntrances/controller.ts |
logEntrance handler — feature flag check, timezone lookup, localDate computation, upsert-or-extend logic, same-day workout check, queue enqueue |
src/routes/appEntrances/index.ts |
Express router: POST /log with authentication middleware |
src/queue/appEntranceQueue.ts |
BullMQ queue setup, enqueueAppEntranceFinalization() with dedup (deterministic ID) and re-queue (timestamped ID) modes |
src/jobs/processAppEntrance.ts |
Finalization logic: pre-checks, feature flag re-check, source workout lookup, serialized transaction with FOR UPDATE row lock, mirror workout creation with exercise copy, post-transaction streak rebuild and RTM recalc |
src/services/streakSimulation.ts |
simulateStreaks() — replays full workout history to compute streak state from scratch. Groups workouts by local calendar day, walks chronologically computing daily/weekly streaks with rest-day logic |
Modified Files
| File |
Change |
prisma/schema.prisma |
Added AppEntranceStatus enum, AppEntrance model, completedViaAppEntrance/mirroredFromWorkoutId on Workout, relations on User and Tenant |
src/middleware/prisma.ts |
Added AppEntrance to TENANT_MODELS set for tenant isolation middleware |
src/routes/index.ts |
Registered /app-entrances route |
src/routes/doctor/controller.ts |
getWorkoutSessions now includes completedViaAppEntrance in response |
src/routes/legacy.ts |
Workout completion handler cancels PENDING entrances via updateMany |
src/routes/streaks/controllers.ts |
Added rebuildStreakForPatient() using simulateStreaks for full streak replay |
src/jobs/notificationJobs.ts |
Added 15-minute repair sweep cron for stale PENDING entrances |
src/worker.ts |
Registered app-entrance-finalization worker (concurrency 3) |
OktaPT-FE Changes (8 files)
| File |
Change |
pages/patient/dashboard.tsx |
Fires POST /v2/app-entrances/log on mount (behind feature flag). Changed useEffect to await fetchFeatures() before refreshDashboardData() so hasFeature() is populated. |
pages/patient/past-workouts.tsx |
"App Visit" badge (emerald green) on workouts with completedViaAppEntrance |
components/doctor/dashboardV2/WorkoutSessionsTable.tsx |
"App Entrance" badge (emerald green) on sessions with completedViaAppEntrance — both compact and expanded views |
lib/stores/doctorDashboard.ts |
Added completedViaAppEntrance to WorkoutSessionV2 interface |
lib/types/exercise.ts |
Added completedViaAppEntrance to PatientWorkout interface |
pages/admin/feature-flags.tsx |
Added appEntranceTracking to KNOWN_FEATURE_FLAGS |
public/locales/en/common.json |
Added pastWorkouts.workoutDetails.appVisit and doctorDashboardV2.appEntrance |
public/locales/ru/common.json |
Russian translations for the above |
Okta-Mobile Changes (5 files)
| File |
Change |
components/patient/PatientDashboard.tsx |
Fires POST /v2/app-entrances/log once per mount via useEffect + useRef guard. Uses useTenantFeatures hook. |
app/(tabs)/history.tsx |
"App Visit" badge (emerald green with phone icon) on workouts with completedViaAppEntrance |
types/workout.ts |
Added completedViaAppEntrance to Workout interface |
locales/en.json |
Added history.appVisit |
locales/ru.json |
Russian translation for the above |
Design Decisions
- 2-hour delay window: Gives patients time to complete a real workout after opening the app. Window extends on re-entry.
- Row-level locking:
SELECT ... FOR UPDATE in the finalization transaction prevents two concurrent workers from both creating a mirror workout for the same entrance.
- Full streak rebuild (not incremental): Finalization may fire after the local day boundary, making incremental streak update unsafe. Uses
simulateStreaks to replay full history.
- No celebrations or workout generation: Mirror workouts don't trigger
CelebrationService or queueWorkoutGeneration to avoid noise.
- Fire-and-forget client calls: The
POST /v2/app-entrances/log call never fails the dashboard load — errors are silently caught.
- Repair sweep: 15-minute cron compensates for lost BullMQ jobs (Redis restart, worker crash).