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 dispatch —
messages-center.spec.tsseeds the real BFF throughPOST /v1/admin/customer-messages/dispatchfor 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 contracts —
messages-center-bff.spec.tsexercises 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 decisions —
operator-metis-themis-review.spec.tsresolves a live academic-integrity appeal; the BFF publishes an in-appincidentmessage 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 decisions —
user-flag-to-review.spec.tsrecords 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 controls —
profile-notifications-index.spec.ts,profile-notification-preferences.spec.ts, andprofile-channel-bindings.spec.tscover the broader delivery-settings, quiet-hours, channel roster, and binding surfaces that/messageslinks 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/notificationsand/profile?path=notifications. - Shell state preview —
?shellState=<mode>replaces live content withShellSurfaceStatePreviewfor 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 frombuildConsumerShellMessageCenter. - 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
/messagespathname maps to "About messages" content inInProductHelp.test.tsx.
States#
- Loading —
load.kind === 'loading'shows "Loading your messages...". Covered byMessageCenter.test.tsxwith a held inbox request. - Idle (pre-refresh internal state) — initial client state is
{ kind: 'idle' }; no visible idle card is rendered beforerefresh()starts. Covered byMessageCenter.test.tsxrender 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, andmessages-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.tsandMessageCenter.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.tsandMessageCenter.test.tsx. - Load error — non-2xx
/api/messages/inboxresponse renders arole="alert"message such as "Failed to load messages (HTTP 503)." Covered bymessages-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 bymessages-center.spec.ts. - Unread highlight — unread rows have
data-unread="true", an accent left border, and an unread meta chip; read rows havedata-unread="false"and no Mark read button. Covered bymessages-center.spec.tsandMessageCenter.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.tsandMessageCenter.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.tsandMessageCenter.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 forShellSurfaceStatePreview, including messages partial-response BFF trace data. Covered byshell-surface-states.spec.tsandShellSurfaceStatePreview.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 bymessages-center.spec.tsandMessageCenter.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 bymessages-center.spec.tsandMessageCenter.test.tsx. - Channel select (
data-testid="message-filter-channel") — All, In-app, Push, Email, SMS, Webhook; customervoicemaps to shell-corewebhook. Covered bymessages-center.spec.tsandMessageCenter.test.tsx. - Status select (
data-testid="message-filter-status") — All, Active, Read, Snoozed. Covered over live dispatched rows inmessages-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 inmessages-center.spec.ts. - Show snoozed — includes locally snoozed rows. Covered by
messages-center.spec.tsandMessageCenter.test.tsx. - Show unsubscribed topics — includes locally stopped-topic rows.
Covered by
messages-center.spec.tsandMessageCenter.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, anduser-flag-to-review.spec.ts. - Mark read —
POST /api/messages/mark-readwith{ messageId }, refreshes inbox, clears unread state, and removes the button. Covered bymessages-center.spec.ts,MessageCenter.test.tsx, andmessages-center-bff.spec.ts. - Archive —
POST /api/messages/archivewith{ messageId }, refreshes inbox, and removes the row. Covered bymessages-center.spec.ts,MessageCenter.test.tsx, andmessages-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.tsandMessageCenter.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 bymessages-center.spec.tsandMessageCenter.test.tsx. - Delivery receipts
<details>— expands queued/sent/delivered/opened/ failed receipt timestamps in ISO format. Covered bymessages-center.spec.ts. - History list — shows created, receipt-derived, read, snoozed, and
unsubscribed history events. Covered by
messages-center.spec.tsandMessageCenter.test.tsx.
Manage notifications card#
- Channel select — options:
in-app,email,push,sms,voice. Covered bymessages-center.spec.tsand component tests. - Category select — options:
onboarding,milestone,reengagement,billing,privacy,support,incident,content-update,social,system. Covered bymessages-center.spec.ts. - Unsubscribe —
POST /api/messages/unsubscribewith channel, category, andreason: 'user-opt-out'; updates topic state locally, shows no success toast, and does not force an inbox refresh. Covered bymessages-center.spec.ts,MessageCenter.test.tsx, andmessages-center-bff.spec.ts. - Re-subscribe —
POST /api/messages/resubscribewith channel and category; clears suppression locally and allows later dispatches for that channel/category. Covered bymessages-center.spec.ts,MessageCenter.test.tsx, andmessages-center-bff.spec.ts.
In-product help#
-
InProductHelp— route-specific help content appears below the inbox. Covered byInProductHelp.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
latestMessageswhile preservingtotalCountandunreadCount; explicitlimitreturns a smaller newest subset.
- Browser route:
- 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/dispatchandPOST /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,CustomerMessageInboxSnapshotfrom@oshun/contracts.- Shell-core projection via
buildConsumerShellMessageCenter.
Automated coverage#
-
apps/oshun/web/e2e/messages-center.spec.tscovers 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.tscovers 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.tsxcovers 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.tsandapps/oshun/web/src/components/__tests__/ShellSurfaceStatePreview.test.tsxcover 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, andprofile-channel-bindings.spec.tscover the notification settings and channel-binding surfaces that remain outside/messages. -
apps/oshun/web/e2e/operator-metis-themis-review.spec.tsandapps/oshun/web/e2e/user-flag-to-review.spec.tscover real incident producers that deliver rows into this inbox and follow their links back through/api/messages.
Cross-references#
- Shell:
shell/01-app-shell.md - Notifications wiring:
shell/05-notifications.md - Sibling routes:
- Component sources:
apps/oshun/web/src/components/MessageCenter.tsxapps/oshun/web/src/components/InProductHelp.tsx
- BFF and contracts:
apps/oshun/bff/src/routes/customer-message-center.tsapps/oshun/bff/src/customer/customer-message-center-store.tslibs/oshun/customer-message-center/src/message-center.tslibs/contracts/src/common/customer-message-center.ts
- Feature spec:
V1/features.md
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
limitsupported. -
/messagesremains profile-adjacent in the shell (active="profile"); it is not an Activity tab surface in current source.