The dashboard shows coaches what the trainee will see. There are two mechanisms in the repo. One is in active use. The other is plumbing whose consumer is missing in the current code.

What is in use: hand built previews

These components draw a phone frame and render a simplified copy of an app screen with React and inline styles. They share:
  • ClientAppPhoneFrame in modules/shared/components/ClientAppPhoneFrame.tsx. A bezel with a scrolling inner area. Props: width (default 280), height (default 560), theme (default APP_DARK).
  • APP_DARK and APP_LIGHT in modules/shared/lib/mobile-preview-theme.ts. The trainee app’s palette as plain colour values.
  • HomeSkins.tsx for the home screen themes (classic, glass, poster, bento).
These previews paint the app’s colours, not the dashboard’s, so they are exempt from the theme check. They are copies. When a screen changes in the mobile repo, the matching preview has to be updated by hand.

The live preview bundle

The second mechanism runs the real trainee app. The mobile repo can export the Expo app for the web (react-native-web). The web app serves that bundle and is meant to embed it in an iframe.

What exists in the frontend

No component in apps/saas currently renders the preview iframe or requests a preview token. A search for TRAINEE_PREVIEW_MESSAGE, traineePreviewUrl() and preview-token finds only the definitions. The earlier design put the iframe in an “App preview” tab on the trainee card. That card page was replaced by the trainee board, whose tabs (TRAINEE_TABS) do not include a preview. The backend endpoint POST /v1/web/clients/:id/preview-token still exists.
Treat the rest of this section as the contract to follow if the tab is rebuilt.

Why the rewrite exists

public/ does not resolve a directory to its index.html. Without the rewrite, /trainee-preview falls through to the [organizationSlug] dynamic route, which treats trainee-preview as a studio slug. The bundle must be served at the directory root. Expo Router strips the configured base path and matches what remains. Pointing the iframe at /trainee-preview/index.html loads the files but leaves the router with /index.html, which matches no route. The comment on TRAINEE_PREVIEW_BASE_PATH in next.config.ts says the same.

The message contract

modules/shared/lib/trainee-preview.ts:
The intended handshake: Rules the parent must follow:
  • Send the token with postMessage. Never put it in the iframe URL.
  • Before replying, check that event.source is the iframe’s contentWindow and that event.origin equals traineePreviewOrigin().
  • isPreviewReadyMessage(event.data) checks the message type.
The constants mirror a file in the mobile repo. The two repos share no package, so a change on one side must be made on the other.

Keeping the bundle current

The bundle is built from the mobile repo and written into apps/saas/public/trainee-preview. Nothing in the frontend builds it. If the mobile app changes and the export is not rerun, the preview shows an old app. Two deployment details:
  • output: "standalone" does not copy public/. The Dockerfile and the dev.yml workflow both copy apps/saas/public into the release, which is how the bundle reaches production. It has to be on disk at build time.
  • The .gitignore line meant to ignore the bundle is damaged. It reads apps/saas/public/trainee-preview/packages/database/prisma/generated/, two ignore rules joined into one path. As written it ignores neither the preview bundle nor the old Prisma output.

Choosing between the two

For a new editor, build a hand written preview with ClientAppPhoneFrame and the APP_DARK palette. That is what every current screen does, and it updates as the coach types. The live bundle shows a real trainee’s real data and cannot drift from the app, but it only shows saved state and needs the token flow above.