V1 Web PWA · Surface walkthrough

Profile · Notifications

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

walked
9sections7 minread

On this page

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

Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md' '2026-06-25 focused deepening — added and verified the read-only summary → /profile?path=notifications delivery-editor handoff with real BFF data in profile-notifications-index; route doc reconciled with shipped quiet-hours editor location.' '2026-06-25 focused mobile deepening — channel grid changed from fixed 2-col to auto-fit minmax columns and verified at 390px viewport with no horizontal overflow in profile-notifications-index.' '2026-06-27 focused route-state deepening — real-BFF channel/category enabled, disabled, address, cadence, subscribed/off variants, footer href/navigation, anonymous redirect, and 44px footer tap targets verified in profile-notifications-index.' '2026-06-29 focused PWA/offline deepening — /profile/notifications promoted to an exact service-worker shell navigation and verified with standalone mobile launch plus real service-worker cached-document replay in profile-notifications-index.' '2026-06-29 focused footer telemetry deepening — all four footer actions and the rendered footer action container verified to expose no telemetry data hooks in profile-notifications-index.' '2026-06-30 focused real-projection deepening — /v1/profile/notifications moved from a static guarded fixture to an authenticated profile/preferences projection; profile-notifications-index now plants the real access token, asserts the live BFF summary, flips the real push preference through /v1/preferences, verifies the route readback, and covers the fail-closed empty copy with an invalid account bearer.' '2026-06-30 focused Arete weekly-review deepening — /v1/profile/notifications now projects an arete-weekly-review category from the real Arete domain reminder preference; profile-notifications-index patches /v1/notifications/preferences/domains/arete, reloads this page, and verifies the weekly category turns off while general reminders remain on.

Purpose#

The "how Lilith reaches you" surface — "the quiet defaults." Renders delivery channels (with their configured addresses and on/off state) and notification categories (each with a cadence and subscription state), sourced from /v1/profile/notifications. That BFF route is now an authenticated projection of the customer profile store, notification-preferences store, and Telegram link store rather than a static fixture. The page header states OSHUN never sends marketing, and channel/category states render as static reads. The mutable delivery surface lives in the Profile settings panel at /profile?path=notifications; this page now provides the explicit "Change delivery settings" handoff into that editor.

Entry points#

  • Cross-domain footer button from /profile/data ("Notifications")
  • Cross-domain footer button from /profile/telegram ("Notifications")
  • "Back to profile" / sub-route navigation from /profile
  • Direct URL / bookmark — yes; auth required (proxy.ts no-session → /welcome)

Layout regions#

Async server component in <LWebShell top={<LCustomerNav active="library" />}>, maxWidth: 880. Two <section>s below the masthead.

  • Header: LCustomerNav (active="library")
  • Masthead: LMasthead — left Profile · notifications, right dynamic ${enabledChannelCount} channel(s) on, kicker "How OSHUN reaches you", italic title "The quiet defaults."
  • Intro: LEyebrow "How this opens" + "OSHUN never sends marketing…"
  • Channels section: LEyebrow "Channels" + an auto-fit responsive grid of channel cards (label, on/off badge, address-or-"not configured"); desktop can show two columns, phone widths collapse to one column; empty readback renders [data-profile-notifications-empty="channels"]
  • Categories section: LEyebrow "Categories" + an ordered list of category cards (cadence caption, label, subscribed/off badge, sub-description); empty readback renders [data-profile-notifications-empty="categories"]
  • Footer / actions: four LBtns — "← Back to profile" (ghost), "Persona voice" (ghost), "Change delivery settings" (primary, /profile?path=notifications), "Data rights" (ghost)

States#

  • Loading — single bffGet blocks server render; no client skeleton
  • Empty (both)bffGet null → fallback { channels: [], categories: [] }; the masthead right reads "0 channel(s) on"; route root exposes data-profile-notifications-read-state="empty", channel/category counts of 0, and dedicated empty copy for both sections. E2E reaches this by preserving the web session but replacing the forwarded oshun-access bearer with an invalid token so the live BFF rejects the account-scoped read.
  • Populated (short) — a handful of channels/categories in their grids are rendered from the real authenticated /v1/profile/notifications projection.
  • Populated (long) — many categories: plain ordered list, no pagination
  • Channel enabled vs disabled — enabled card: L.paper bg, italic label, accent "● on"; disabled: transparent bg, normal label, muted "○ off". E2E flips notifications.pushEnabled through real PATCH /v1/preferences, then reloads /profile/notifications and sees the Push row switch to disabled with "not configured".
  • Channel address present vs missing — shows channel.address or italic "not configured"
  • Category subscribed vs off — accent "● subscribed" vs muted "○ off"; cadence caption is one of daily | weekly | monthly | on-event
    • E2E: profile-notifications-index.spec.ts asserts all four real BFF channels (email, push, sms, telegram) and all five categories (gentle-reminders, arete-weekly-review, coach-letters, crisis-checkins, product-changes) with their enabled/off attributes, visible badges, addresses / not configured, and cadence labels.
  • Error (recoverable) — N/A: bffGet returns null on failure → empty fallback (quiet empty, no retry UI)
  • Offline — the real service worker treats /profile/notifications as an exact shell-navigation document, warms the BFF-backed HTML while online, and replays the same channel/category summary while navigator.onLine === false; the generic offline fallback card/marker is absent
  • Gated / anonymousproxy.ts redirect to /welcome before render; E2E clears cookies and verifies /welcome?redirect=%2Fprofile%2Fnotifications.
  • Standalone PWA — 390 px standalone launch verifies display-mode, real-BFF channel/category rows, footer hrefs, 44 px footer links, and no horizontal overflow

Interactions#

The channel and category cards are read-only — no toggle inputs exist in the JSX; on/off and subscribed/off are rendered as text/badge state only. The interactive elements are footer navigation buttons, including the primary handoff to the mutable notification-preferences editor.

  • ← Back to profile (LBtn ghost, href /profile)
    • Function: navigates to the account hub
    • Screen reader: announces button text; rendered as <a>
  • Persona voice (LBtn ghost, href /profile/persona)
    • Function: navigates to the voice catalogue
  • Change delivery settings (LBtn primary, href /profile?path=notifications)
    • Function: navigates to ProfileSettingsPanel with the Notifications section open; that editor owns quiet-hours, digest cadence, and per-domain modality controls
    • Test hook: wrapper [data-profile-notifications-edit-link]
  • Data rights (LBtn ghost, href /profile/data)
    • Function: navigates to the DSAR / data-rights surface
    • Keyboard: in footer tab order; Enter activates
    • Touch target: route-local footer action style sets 44 px height/min-height; E2E verifies every footer link measures ≥ 44 px.
    • Mobile (≤ 640 px): footer row wraps (flexWrap: 'wrap')
    • E2E: profile-notifications-index.spec.ts verifies the footer hrefs, then clicks Back to profile, Persona voice, and Data rights from the real route and waits for each sibling route marker. It also asserts the footer container and all four footer links expose no data-telemetry-event, data-analytics-event, or data-event hooks in the normal browser route and standalone PWA launch.

Data & contracts#

  • Reads: bffGet('/v1/profile/notifications')NotificationsResponse { generatedAt, userId, channels: [{ id, label, enabled, address: string|null }], categories: [{ id, label, sub, enabled, cadence: 'daily'|'weekly'|'monthly'|'on-event' }] }
    • email address comes from the authenticated profile record.
    • push enabled/address comes from preferences.notifications.pushEnabled.
    • telegram enabled/address comes from telegramUserStateStore when linked.
    • categories summarize the notification-preferences store; the arete-weekly-review category is backed by the Arete domain reminder kind and delivery modalities, while product-change marketing remains off by design.
  • Writes: none from this page. The write surface is the client-side notification editor at /profile?path=notifications, backed by GET/PATCH /v1/notifications/preferences and PATCH /v1/notifications/preferences/domains/:domainId
  • Realtime: none
  • Caching: authenticated bffGet forwards oshun-access as a bearer and uses cache: 'no-store'; /profile/notifications is an exact shell navigation in public/sw.js, so an authenticated online navigation writes the rendered document into the static SW cache and offline navigation replays that cached summary rather than the generic fallback
  • Auth/role check: proxy.ts protected-by-default; no-session → /welcome
  • Metadata: title: 'Profile · notifications', description re channels / cadence / crisis check-ins honoring explicit asks, alternates.canonical: '/profile/notifications'

Cross-references#

Open questions / known gaps#

  • The view is intentionally read-only; the user changes quiet-hours, digest cadence, and per-domain modality preferences in /profile?path=notifications, now reachable through "Change delivery settings" and covered by profile-notifications-index.
  • Quiet-hours UI lives in /profile?path=notifications, not this static index page; profile-notifications-index proves the handoff and reveals the two time inputs, while profile-notification-preferences and notifications-preferences-api cover the editor and BFF persistence.
  • Empty channels/categories render on-brand empty copy in both sections and preserve the delivery-settings handoff.
  • No telemetry on footer navigation: profile-notifications-index asserts the footer action container, all four footer links, and footer descendants expose no data-telemetry-event, data-analytics-event, or data-event hooks.
  • Channel grid collapse on ≤ 640 px — changed to repeat(auto-fit, minmax(min(240px, 100%), 1fr)) and covered by profile-notifications-index at a 390 px viewport with no horizontal overflow.
  • Arete weekly-review reminder category appears in the account notification read index and follows the real Arete domain reminder preference; the actual Sunday push/in-app scheduler remains covered as a separate Arete journey gap.

E2E coverage#

  • apps/oshun/web/e2e/profile-notifications-index.spec.ts — real browser + real dev BFF. Plants the real access token cookie for the server-rendered BFF read, loads /profile/notifications, asserts the authenticated BFF summary and email address, follows "Change delivery settings" to /profile?path=notifications and reveals the quiet-hours time inputs, flips the real profile push preference through /v1/preferences and verifies Push disables on reload, patches the real Arete domain reminder preference through /v1/notifications/preferences/domains/arete and verifies the arete-weekly-review category turns off on reload, covers the invalid-bearer fail-closed empty copy, asserts every BFF-backed channel/category state variant, verifies footer hrefs, absence of footer telemetry data hooks, sibling-route navigation, anonymous redirect preservation, 44 px footer tap targets, then verifies the channel cards stack at a 390 px viewport without horizontal overflow. The spec also verifies standalone display-mode launch at 390 px, repeats the footer no-telemetry assertion there, and proves real-service-worker cached-document replay while offline with the same BFF-backed rows visible and no generic offline fallback marker.
  • Related editor coverage: apps/oshun/web/e2e/profile-notification-preferences.spec.ts and apps/oshun/web/e2e/notifications-preferences-api.spec.ts.
  • BFF coverage: apps/oshun/bff/src/__tests__/notifications-preferences-route.test.ts verifies /v1/profile/notifications is authenticated and projects profile email plus notification preferences instead of serving a fixture.