V1 Web PWA · Surface walkthrough

Nyx · agenda (upcoming events)

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

walked + e2e-covered
8sections5 minread

On this page

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

Last walked. 2026-07-03 real-infra Playwright chromium — live /v1/events/upcoming contract, signed-in customer shell, Nyx-only filter, domain-chip suppression, corrected /v1/nyx/events.ics BFF-proxied card/footer links, mobile standalone sibling route coverage, and shared axe scan. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md#246-2026-07-03-events-agenda-calendar-feed-coverage

Purpose#

One chronological pane of what the sky (and the school) holds ahead. The page calls the cross-domain BFF feed /v1/events/upcoming, then filters it down to domain === 'nyx' so this view shows only Nyx events (conjunctions, meteor peaks). A .ics subscription link lets a user put them next to their other appointments. See V1/features.md#nyx.

Entry points#

  • Nyx hub (/nyx) — verify a link to the agenda from the hub sub-nav
  • /nyx/tonight — "Upcoming events" ghost button at the foot of the timeline links here
  • Direct URL / bookmark — yes (alternates.canonical = '/nyx/events'); signed-in
  • Deep link from notification — Nyx event reminder push (see journeys/nyx-event-calendar-sync-reminder.md)
  • Shell nav — arrives under the explore tab (LCustomerNav active="explore")

Layout regions#

page.tsx is an async server component: await getUpcomingEvents().filter(event.domain === 'nyx') → renders inside LWebShell with LCustomerNav active="explore". Centered column, maxWidth: 880, data-responsive-page.

  • Top chrome: LCustomerNav (Today / Explore / Activity / Library; Explore active) with the "Lilith" wordmark and a "⌕ Search" affordance
  • Masthead (LMasthead): left Nyx · agenda, right ${events.length} upcoming, kicker "What the sky holds, ahead", title "The next few nights."
  • "How this opens" note (LEyebrow + italic serif copy): "Conjunctions and meteor peaks for your observation window. Subscribe to the calendar to put them next to your other appointments."
  • Main: <EventList events={events} showDomainChip={false} /> — a grid of event cards (domain chip suppressed since the feed is already Nyx-only)
  • Footer button row: three LBtns — "← Back to Nyx", "Tonight", "Subscribe (.ics)"
  • Automation anchors: root exposes data-nyx-events-page, data-route, data-event-count, and data-event-domains; shared EventList rows expose event id/domain/start/end, date/title/summary, and per-link data-event-link kinds. data-show-domain-chip="false" confirms chip suppression on this Nyx-only view.

States#

  • Loadingpage.tsx is async (await getUpcomingEvents()); no adjacent loading.tsx, so the route blocks server-side on the BFF fetch (5 s timeout in bffGet)
  • Populated (BFF)/v1/events/upcoming returns events; only domain === 'nyx' rows survive the filter and render as cards
  • Unavailable (BFF unreachable) — when the BFF is unreachable, getUpcomingEvents() returns an empty array [] (it never fabricates astronomy — see the comment at nyx-depth.ts ~205-210, "return an empty list rather than fabricating astronomy"); after the domain === 'nyx' filter the page still shows 0 events, so the masthead reads "0 upcoming" and EventList renders its empty card
  • Empty — if no Nyx events survive the filter, EventList renders its own empty card: "No upcoming events on the feed yet. The next conjunctions and class slots will appear here as the schedule fills."
  • Error (recoverable)bffGet swallows non-2xx/network errors and returns null; getUpcomingEvents() then returns an empty array [], so the page shows the honest empty state (0 events) rather than fabricated data — but a transient outage is indistinguishable from a genuinely empty feed; verify whether that should be surfaced as a retryable error
  • Error (unrecoverable) — no error.tsx boundary adjacent; an unexpected throw would bubble to a higher app boundary (verify)
  • Offline — view served from SW cache where present; the .ics subscribe link and back/forward buttons are plain hrefs (no client fetch)
  • Gated — middleware enforces signed-in; the BFF resolves the caller by forwarded session cookie; an unauthenticated request yields no events, so the page shows the honest empty state (0 events), never fabricated data
  • Standalone PWA — renders cleanly in standalone window; verify safe-area insets on the footer button row

Interactions#

Event card (per NyxUpcomingEvent, rendered by EventList)#

  • Event card (article; not itself a link)
    • Function: shows the localized date line (weekday, month day, hh:mm + · location when present), the title (serif), and the italic summary
    • Screen reader: announces date line, then title, then summary
    • Mobile (≤ 640 px): date line + domain chip row wraps (flexWrap)
  • "Plan an observation →" (link, nyx events only)
    • Function: → /nyx/observation?eventId=<id> to pre-anchor a field log
    • Telemetry: verify event fired
  • "Add to calendar (.ics) →" (link, every card)
    • Function: → /v1/nyx/events.ics (BFF-served calendar feed through the same-origin Next rewrite)
  • "← Back to Nyx" (LBtn ghost) → /nyx
  • "Tonight" (LBtn ghost) → /nyx/tonight
  • "Subscribe (.ics)" (LBtn primary) → /v1/nyx/events.ics — calendar subscription; BFF returns a text/calendar feed

Data & contracts#

  • Reads: getUpcomingEvents() (@/lib/server/nyx-depth) → bffGet<BffEventsResponse>('/v1/events/upcoming'); returns readonly NyxUpcomingEvent[] (id, title, startIso, endIso, location, summary, domain). Cross-domain feed filtered to nyx in the page.
  • Writes: none — read-only feed
  • Realtime: none
  • Caching: bffGet sets next: { revalidate: 60 } (60 s ISR by default); honest empty array [] on null (no fixture fallback)
  • Auth/role check: middleware enforces signed-in; bffGet forwards oshun-session / __session cookies so the BFF resolves the caller
  • Calendar: /v1/nyx/events.ics — BFF calendar endpoint, served same-origin by the Next /v1/:path* rewrite

Cross-references#

Open questions / known gaps#

  • No telemetry events are wired in EventList or the page — confirm whether this is intentional for V1
  • On a BFF outage getUpcomingEvents() returns an empty array [] (the honest empty state — never a fabricated agenda), which is indistinguishable from a genuinely empty feed; decide whether a transient outage should be surfaced as a retryable error instead of "0 upcoming"
  • EventList's cross-domain links (/tara, /metis, /arete) are suppressed here only by showDomainChip={false} affecting the chip — the links themselves still render per domain; verify intended behavior on the nyx-only feed