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
exploretab (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): leftNyx · 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, anddata-event-domains; sharedEventListrows expose event id/domain/start/end, date/title/summary, and per-linkdata-event-linkkinds.data-show-domain-chip="false"confirms chip suppression on this Nyx-only view.
States#
- Loading —
page.tsxis async (await getUpcomingEvents()); no adjacentloading.tsx, so the route blocks server-side on the BFF fetch (5 s timeout inbffGet) - Populated (BFF) —
/v1/events/upcomingreturns events; onlydomain === '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 atnyx-depth.ts~205-210, "return an empty list rather than fabricating astronomy"); after thedomain === 'nyx'filter the page still shows 0 events, so the masthead reads "0 upcoming" andEventListrenders its empty card - Empty — if no Nyx events survive the filter,
EventListrenders 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) —
bffGetswallows non-2xx/network errors and returnsnull;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.tsxboundary adjacent; an unexpected throw would bubble to a higher app boundary (verify) - Offline — view served from SW cache where present; the
.icssubscribe 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+· locationwhen present), thetitle(serif), and the italicsummary - Screen reader: announces date line, then title, then summary
- Mobile (≤ 640 px): date line + domain chip row wraps (
flexWrap)
- Function: shows the localized date line (
- "Plan an observation →" (link, nyx events only)
- Function: →
/nyx/observation?eventId=<id>to pre-anchor a field log - Telemetry: verify event fired
- Function: →
- "Add to calendar (.ics) →" (link, every card)
- Function: →
/v1/nyx/events.ics(BFF-served calendar feed through the same-origin Next rewrite)
- Function: →
Footer buttons#
- "← Back to Nyx" (
LBtnghost) →/nyx - "Tonight" (
LBtnghost) →/nyx/tonight - "Subscribe (.ics)" (
LBtnprimary) →/v1/nyx/events.ics— calendar subscription; BFF returns atext/calendarfeed
Data & contracts#
- Reads:
getUpcomingEvents()(@/lib/server/nyx-depth) →bffGet<BffEventsResponse>('/v1/events/upcoming'); returnsreadonly NyxUpcomingEvent[](id,title,startIso,endIso,location,summary,domain). Cross-domain feed filtered tonyxin the page. - Writes: none — read-only feed
- Realtime: none
- Caching:
bffGetsetsnext: { revalidate: 60 }(60 s ISR by default); honest empty array[]onnull(no fixture fallback) - Auth/role check: middleware enforces signed-in;
bffGetforwardsoshun-session/__sessioncookies 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#
- Feature spec:
V1/features.md#nyx - Architecture:
V1/ARCHITECTURE.md#nyx - Sibling Nyx routes:
nyx.md,nyx-tonight.md,nyx-observation.md - Journeys:
journeys/nyx-event-calendar-sync-reminder.md,journeys/nyx-to-tara-bridge.md(cross-domain links inEventListhop to/tara,/metis,/arete) - Component sources:
apps/oshun/web/src/components/nyx/EventList.tsx(EventList,LinkRow)apps/oshun/web/src/lib/server/nyx-depth.ts(getUpcomingEvents— returns[]on null/unreachable; there is noupcomingFixture)
Open questions / known gaps#
- No telemetry events are wired in
EventListor 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 byshowDomainChip={false}affecting the chip — the links themselves still render per domain; verify intended behavior on the nyx-only feed