V1 Web PWA · Surface walkthrough

Messages

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
10sections10 minread

On this page

Context. surface customer · domain discovery · route /messages · auth signed-in · source apps/oshun/web/src/app/messages/page.tsx

Last walked. 2026-06-27 automated coverage reconciliation by Codex — MessageCenter source, shell-state previews, component tests, live browser-proxy Playwright, real BFF dispatch/read/archive/unsubscribe/replay coverage, producer notifications from Themis and Trust & Safety, profile notification preference handoffs, docs center generation, and current interaction states cross-checked.

Purpose#

The customer's in-product inbox for notifications, billing notes, support replies, content updates, incident messages, and producer-originated decisions. It lists BFF-backed messages with delivery receipts and history, supports read/archive/deep-link follow actions, adds shell-core filters and local snooze/topic-stop state, and exposes a "Manage notifications" form for channel/category unsubscribe and re-subscribe.

Current producer coverage#

  • Direct customer-message dispatchmessages-center.spec.ts seeds the real BFF through POST /v1/admin/customer-messages/dispatch for the browser's access-token subject, renders the rows through /api/messages/inbox, follows a registered deep link through the real follow proxy, marks read, archives, unsubscribes, proves the next matching dispatch is suppressed, re-subscribes, and verifies delivery is restored.
  • Customer-message BFF contractsmessages-center-bff.spec.ts exercises auth/scope gates, invalid payloads, dispatch to inbox, deep-link follow, unknown deep-link rejection, mark-read, archive, unsubscribe/re-subscribe, dedupe, cross-user isolation, bounded latest-message snapshots, quiet-hours delay/replay, and crisis-frame suppression/bypass.
  • Themis appeal decisionsoperator-metis-themis-review.spec.ts resolves a live academic-integrity appeal; the BFF publishes an in-app incident message with subject "Academic integrity appeal decided", verdict id, resolution note, authenticated appeal deep link, delivery receipts, clicked receipt, and mark-read state.
  • Trust & Safety report decisionsuser-flag-to-review.spec.ts records a moderator decision and verifies the reporter receives "Your Trust & Safety report was reviewed", follows "Open Safety Center" through /api/messages, lands on /profile/safety?reportId=<id>, and reads the clicked receipt back.
  • Profile notification controlsprofile-notifications-index.spec.ts, profile-notification-preferences.spec.ts, and profile-channel-bindings.spec.ts cover the broader delivery-settings, quiet-hours, channel roster, and binding surfaces that /messages links to conceptually but does not embed.

Entry points#

  • Direct URL / bookmark/messages; auth required via shell middleware.
  • Shell notification / message affordances — shell header and notification surfaces deep-link here or to message follow targets.
  • Push / email "Read in inbox" deep links — registered links resolve through /api/messages/<messageId>/follow?url=<encodedUrl> and then route to the BFF-returned internal target.
  • Profile-adjacent navigation — the route intentionally mounts ShellLayout active="profile"; notification delivery settings live under /profile/notifications and /profile?path=notifications.
  • Shell state preview?shellState=<mode> replaces live content with ShellSurfaceStatePreview for loading, empty, offline, degraded, retry, partial-response, partial-outage, and error modes.

Layout regions#

page.tsx mounts ShellLayout with active="profile" and breadcrumbs Messages, then calls resolveShellRouteSurfaceContent({ surface: 'messages' }) around <MessageCenter /> and <InProductHelp />.

Inside MessageCenter:

  • Header — eyebrow "Inbox", H1 "Messages", and <unreadCount> unread of <totalCount>. once loaded.
  • Summary metrics (data-testid="message-center-summary") — Visible, Unread, Issues, and History from buildConsumerShellMessageCenter.
  • Status row — loading copy, load-error alert, and action-error alert.
  • Filters (data-testid="message-filters") — Kind, Channel, Status, Search, Show snoozed, and Show unsubscribed topics.
  • Message list (data-testid="message-list") — grouped shell-core message rows when any messages match the current filters.
  • Empty cards — "You have no messages in your inbox." for true empty inbox; "No messages match the current filters." for filter-empty results.
  • Manage notifications — channel/category selects plus Unsubscribe and Re-subscribe buttons.
  • InProductHelp — shell-wide help affordance; the /messages pathname maps to "About messages" content in InProductHelp.test.tsx.

States#

  • Loadingload.kind === 'loading' shows "Loading your messages...". Covered by MessageCenter.test.tsx with a held inbox request.
  • Idle (pre-refresh internal state) — initial client state is { kind: 'idle' }; no visible idle card is rendered before refresh() starts. Covered by MessageCenter.test.tsx render setup and source audit.
  • Loaded with messages — non-empty snapshots render summary metrics, filters, grouped message rows, unread count, delivery receipts, and history. Covered by messages-center.spec.ts, MessageCenter.test.tsx, and messages-center-bff.spec.ts.
  • Loaded empty — zero-message snapshots render "You have no messages in your inbox." and no list. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Filtered empty — loaded inbox with active filters but no matching rows renders "No messages match the current filters." Covered by filter coverage in messages-center.spec.ts and MessageCenter.test.tsx.
  • Load error — non-2xx /api/messages/inbox response renders a role="alert" message such as "Failed to load messages (HTTP 503)." Covered by messages-center.spec.ts.
  • Action busy — during mark-read/archive/follow/unsubscribe/resubscribe the row and form buttons receive disabled={action.kind === 'busy'}. Covered by source audit and success/error interaction tests.
  • Action error — failed mutations render a secondary role="alert" above the list, e.g. "Mark-read failed (HTTP 500)." Covered by messages-center.spec.ts.
  • Unread highlight — unread rows have data-unread="true", an accent left border, and an unread meta chip; read rows have data-unread="false" and no Mark read button. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Snoozed — Snooze applies a local override, hides the row by default, adds "Snoozed until ..." history, and restores it when Show snoozed is checked. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Unsubscribed topic — Stop topic hides optional categories by default, adds local history/unsubscribed labeling, and restores rows when Show unsubscribed topics is checked. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Required topic — billing, privacy, support, incident, and system category topics are marked required in CATEGORY_TOPICS; Stop topic is disabled for required topics. Covered by source audit and shell-core model behavior.
  • Shell state preview?shellState= swaps route content for ShellSurfaceStatePreview, including messages partial-response BFF trace data. Covered by shell-surface-states.spec.ts and ShellSurfaceStatePreview.test.tsx.
  • Standalone PWA / offline shell — this route relies on shared shell PWA and offline behavior rather than a messages-specific branch. Shell-state, PWA, and offline route specs cover the shared behavior.

Interactions#

Inbox header and summary#

  • Unread summary — read-only text <unreadCount> unread of <totalCount>. from the BFF snapshot. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Summary metrics — read-only Visible / Unread / Issues / History metrics derived from buildConsumerShellMessageCenter. Covered by the live browser-proxy messages spec.

Filters#

  • Kind select (data-testid="message-filter-kind") — All, System, Support, Transactional, Education, Marketing. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Channel select (data-testid="message-filter-channel") — All, In-app, Push, Email, SMS, Webhook; customer voice maps to shell-core webhook. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Status select (data-testid="message-filter-status") — All, Active, Read, Snoozed. Covered over live dispatched rows in messages-center.spec.ts.
  • Search input (data-testid="message-filter-search") — filters by message content while retaining the loaded BFF snapshot. Covered over live dispatched rows in messages-center.spec.ts.
  • Show snoozed — includes locally snoozed rows. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Show unsubscribed topics — includes locally stopped-topic rows. Covered by messages-center.spec.ts and MessageCenter.test.tsx.

Message row#

  • Subject + meta — read-only topic label, kind, status, unread, delivery issue, and unsubscribed chips. Covered by messages-center.spec.ts.
  • Body — read-only message.body. Covered by route/component tests.
  • Deep links — normal left-click fetches the follow proxy, records a clicked receipt for a registered link, then routes to the returned destination; modifier clicks keep native anchor behavior. Covered by messages-center.spec.ts, messages-center-bff.spec.ts, operator-metis-themis-review.spec.ts, and user-flag-to-review.spec.ts.
  • Mark readPOST /api/messages/mark-read with { messageId }, refreshes inbox, clears unread state, and removes the button. Covered by messages-center.spec.ts, MessageCenter.test.tsx, and messages-center-bff.spec.ts.
  • ArchivePOST /api/messages/archive with { messageId }, refreshes inbox, and removes the row. Covered by messages-center.spec.ts, MessageCenter.test.tsx, and messages-center-bff.spec.ts.
  • Snooze — local override only; hides the row until Show snoozed is enabled and appends snooze history. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Stop topic — optional-topic unsubscribe from the row calls /api/messages/unsubscribe, hides the row until Show unsubscribed topics is enabled, and appends unsubscribe history. Covered by messages-center.spec.ts and MessageCenter.test.tsx.
  • Delivery receipts <details> — expands queued/sent/delivered/opened/ failed receipt timestamps in ISO format. Covered by messages-center.spec.ts.
  • History list — shows created, receipt-derived, read, snoozed, and unsubscribed history events. Covered by messages-center.spec.ts and MessageCenter.test.tsx.

Manage notifications card#

  • Channel select — options: in-app, email, push, sms, voice. Covered by messages-center.spec.ts and component tests.
  • Category select — options: onboarding, milestone, reengagement, billing, privacy, support, incident, content-update, social, system. Covered by messages-center.spec.ts.
  • UnsubscribePOST /api/messages/unsubscribe with channel, category, and reason: 'user-opt-out'; updates topic state locally, shows no success toast, and does not force an inbox refresh. Covered by messages-center.spec.ts, MessageCenter.test.tsx, and messages-center-bff.spec.ts.
  • Re-subscribePOST /api/messages/resubscribe with channel and category; clears suppression locally and allows later dispatches for that channel/category. Covered by messages-center.spec.ts, MessageCenter.test.tsx, and messages-center-bff.spec.ts.

In-product help#

  • InProductHelp — route-specific help content appears below the inbox. Covered by InProductHelp.test.tsx.

Data & contracts#

  • Reads:
    • Browser route: GET /api/messages/inbox.
    • BFF route: GET /v1/customer/messages/inbox, returning { snapshot: CustomerMessageInboxSnapshot }.
    • Default BFF snapshot returns newest 25 latestMessages while preserving totalCount and unreadCount; explicit limit returns a smaller newest subset.
  • Writes:
    • POST /api/messages/mark-read -> /v1/customer/messages/mark-read.
    • POST /api/messages/archive -> /v1/customer/messages/archive.
    • POST /api/messages/unsubscribe -> /v1/customer/messages/unsubscribe.
    • POST /api/messages/resubscribe -> /v1/customer/messages/resubscribe.
    • GET /api/messages/<messageId>/follow?url=<encodedUrl> -> /v1/customer/messages/:messageId/follow.
    • Producer/admin: POST /v1/admin/customer-messages/dispatch and POST /v1/admin/customer-messages/replay-due.
  • Realtime: None in the page. Refresh is request-driven after successful mark-read/archive mutations; no WS/SSE.
  • Caching: inbox fetch uses cache: 'no-store'; BFF snapshots are per-recipient and auth scoped.
  • Auth/role check: page calls same-origin API routes with session cookies; BFF customer routes require a valid bearer; admin dispatch/replay requires messaging admin scope.
  • Contracts:
    • CustomerMessage, CustomerMessageCategory, CustomerMessageChannel, CustomerMessageInboxSnapshot from @oshun/contracts.
    • Shell-core projection via buildConsumerShellMessageCenter.

Automated coverage#

  • apps/oshun/web/e2e/messages-center.spec.ts covers live BFF producer-to-browser rendering through /api/messages, summary metrics, search/status filters, delivery receipts, follow proxy, clicked receipts, mark-read, archive, unsubscribe suppression, re-subscribe restoration, and component-level route/error/empty/filter/snooze/topic-stop cases.
  • apps/oshun/web/e2e/messages-center-bff.spec.ts covers the real BFF customer-message engine over HTTP: auth/scope gates, validation, dispatch, inbox snapshots, follow, unknown links, mark-read, archive, unsubscribe, resubscribe, quiet-hours delay/replay, crisis-frame suppression/bypass, dedupe, privacy isolation, and bounded snapshots.
  • apps/oshun/web/src/components/__tests__/MessageCenter.test.tsx covers local render/mutation behavior for loading, empty, populated, mark-read, archive, follow hrefs, unsubscribe/resubscribe, kind/channel filters, history, snooze, and row topic unsubscribe.
  • apps/oshun/web/e2e/shell-surface-states.spec.ts and apps/oshun/web/src/components/__tests__/ShellSurfaceStatePreview.test.tsx cover route shell-state previews for /messages, including partial-response BFF trace output.
  • apps/oshun/web/e2e/profile-notifications-index.spec.ts, profile-notification-preferences.spec.ts, and profile-channel-bindings.spec.ts cover the notification settings and channel-binding surfaces that remain outside /messages.
  • apps/oshun/web/e2e/operator-metis-themis-review.spec.ts and apps/oshun/web/e2e/user-flag-to-review.spec.ts cover real incident producers that deliver rows into this inbox and follow their links back through /api/messages.

Cross-references#

Open questions / known gaps#

  • Push-token enrollment, digest cadence, quiet-hours, and channel bindings live under Profile notification surfaces, not inside /messages; covered by profile notification specs listed above.
  • Successful unsubscribe intentionally has no toast and no automatic inbox refresh; it updates topic state locally and leaves error alerts absent.
  • Back-pressure semantics are bounded by the BFF: newest 25 rows by default, with full aggregate counts preserved and explicit lower limit supported.
  • /messages remains profile-adjacent in the shell (active="profile"); it is not an Activity tab surface in current source.