Context. surface customer · domain nyx · route /nyx · auth signed-in · source apps/oshun/web/src/app/nyx/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; content re-verified 2026-06-03 against current source
Purpose#
The Nyx domain hub — "the sky tonight, from your window — events in order, with
a chart." Headlines tonight's notable event, paints a SVG sky chart, lists every
event in chronological order, and pulls a Nisaba canon excerpt that pairs with
the headline. Server-rendered via getNyx() — a real BFF fetch of
/v1/nyx/tonight — into the Lilith NyxRoom composition
(apps/oshun/web/src/components/lilith/rooms.tsx). When the feed is unreachable
getNyx() returns nyxUnavailable(), an honest empty sky (no events), never a
fabricated conjunction.
Entry points#
- Lilith index (
/lilith) — card 10 "Nyx · Tonight's chart · events in order · canon cross-link" - Domain card on
/— Nyx domain card inDomainCardGrid - Nyx perspective rail on
/— "Tonight's sky" link - Domain switcher (
/switcheror shell chord) — Nyx slot - Direct URL / bookmark — yes (signed-in)
- Deep link from notification — Nyx event reminder push (cadence configured
in
EventCalendarOverlay)
Layout regions#
page.tsx awaits getNyx() and renders <NyxRoom data>. NyxRoom mounts
inside LWebShell with the customer nav (active="explore") and a Lilith
LSubNav of tonight | planets | meteors | log.
- Top chrome:
LCustomerNav(explore) +LSubNav(tabs: Tonight, Planets, Meteors, Observations) - Eyebrow strip: "Nyx · the sky tonight" /
data.date(e.g.6 May · 21:14 to 04:42) /data.location(e.g.40.7 N · 74.0 W · waxing 84%) - Main, left column (2fr):
LEyebrow"Tonight's chart",LDisplayheadline (data.tonightHeadline), the SVG sky panel (16:10 aspect, ink background) showing 90 procedural stars, a dashed Leo asterism, the moon, and Jupiter; below it a row of four chips (Auto-orient · Compass · Show ecliptic · Mute light pollution) with the first chipaccent - Main, right column (1fr):
LEyebrow"Tonight, in order" thendata.eventsrendered as a list of<Link>rows (each to/domains/nyx/events/${row.id}, time-and-name, e.g.21:14 Jupiter rises,21:42 Conjunction begins); below thatLRule, thenLEyebrow"From Nisaba" whosedata.canonCrossLinkquote is itself a<Link href={canonCrossLinkHref}>; and — whendata.taraGroundingCueis set — a "Ground with Tara" section with a<Link>totaraGroundingCue.href
States#
- Loading —
page.tsxis async; verify whetherloading.tsxships a skeleton or the route blocks ongetNyx() - Populated (BFF) —
getNyx()fetches/v1/nyx/tonightandmapNyxTonightToNyxDatarenders the computed events (sunset, twilight, moon) and headline - Unavailable / empty sky — when
/v1/nyx/tonightis unreachable (nocard.events),getNyx()returnsnyxUnavailable(), an honest empty sky with no events — the list renders zero rows rather than a fabricated conjunction - Error (recoverable) —
getNyx()does not throw on an unreachable feed; it returnsnyxUnavailable(), so a boundary is only hit on an unexpected throw — verifyerror.tsx/global-error.tsxcoverage - Offline — view served from SW cache (Lilith design system pages are
static SVG/HTML, no runtime fetch beyond
getNyx) - Standalone PWA — chart SVG renders cleanly in standalone window; safe-area insets respected
- Reduced motion — no animations declared in
NyxRoom; verify chip hover transitions honorprefers-reduced-motion - High contrast / dark theme — chart panel uses fixed dark
background: '#0e0d12'regardless of theme; cream paper around it stays light
Interactions#
Sub-nav (LSubNav)#
- Tonight tab (active) — non-interactive marker for current view
- Planets tab — verify whether tab is wired to a route or is a visual
placeholder in
LSubNav - Meteors tab — verify routing target
- Observations tab — verify whether it routes to
/domains/nyx/observation-logor is decorative
Sky chart panel#
- SVG chart (decorative —
aria-hidden="true") — Leo asterism plus moon + Jupiter rendered statically; no pan, zoom, or hover - Auto-orient chip (
LChip accent) — verify whether the chip is interactive or is a label-only marker - Compass chip — same; verify hit target ≥ 44×44 px on coarse pointers
- Show ecliptic chip — same
- Mute light pollution chip — same
Tonight, in order#
- Each event row (one per
data.events)- Function: every row is a
<Link href={/domains/nyx/events/${row.id}}>— a real link, focusable, witharia-labelOpen ${row.name} at ${row.time}: ${row.detail} - Keyboard: focusable (real anchor); Enter activates
- Screen reader: announces the
aria-label(name, time, detail)
- Function: every row is a
Canon cross-link (Nisaba)#
- "From Nisaba" quote — the italic
data.canonCrossLinkline is a<Link href={data.canonCrossLinkHref}>(aria-label "Read the Nisaba canon cross-link paired with tonight's headline event") — a real link, not typography only
Ground with Tara#
- "Ground with Tara" link — rendered only when
data.taraGroundingCueis set; a<Link href={data.taraGroundingCue.href}>(aria-label "Ground tonight's observation with a Tara sit") carryingtaraGroundingCue.label
Data & contracts#
- Reads:
getNyx()fromapps/oshun/web/src/lib/lilith-data/nyx.ts—bffGet<NyxTonightResponse>('/v1/nyx/tonight', { revalidateSeconds: 300 }), mapped viamapNyxTonightToNyxData; returnsNyxData. On an unreachable feed it returnsnyxUnavailable()(an honest empty sky). There is nogetNyxFixture(). - Writes: none — read-only hub
- Realtime: none in this view
- Caching: server-component
bffGetwithrevalidateSeconds: 300 - Auth/role check: middleware enforces signed-in
Cross-references#
- Lilith design system index:
../10-lilith/lilith.md - Sibling Nyx routes:
- Component source:
apps/oshun/web/src/components/lilith/rooms.tsx(NyxRoom, lines ~805–1110) - Data layer:
apps/oshun/web/src/lib/lilith-data/nyx.ts - Feature spec:
V1/features.md#nyx
Open questions / known gaps#
- Confirm whether the
LSubNavtabs (Tonight / Planets / Meteors / Observations) are routed or decorative — code shows staticitems - Confirm chip semantics (
Auto-orient,Compass,Show ecliptic,Mute light pollution) —LChipmay be button or span; checkLChipexport - The "From Nisaba" quote is a real
<Link href={data.canonCrossLinkHref}>(resolved); confirm the canon target it points at - Confirm whether the SVG chart consumes per-user lat/lng from
/v1/nyx/tonightor only renders the fixed decorative asterism - Document loading skeleton (no
loading.tsxadjacent topage.tsx)