V1 Web PWA · Surface walkthrough

Nyx · the sky tonight

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

walked
8sections5 minread

On this page

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 in DomainCardGrid
  • Nyx perspective rail on / — "Tonight's sky" link
  • Domain switcher (/switcher or 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", LDisplay headline (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 chip accent
  • Main, right column (1fr): LEyebrow "Tonight, in order" then data.events rendered 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 that LRule, then LEyebrow "From Nisaba" whose data.canonCrossLink quote is itself a <Link href={canonCrossLinkHref}>; and — when data.taraGroundingCue is set — a "Ground with Tara" section with a <Link> to taraGroundingCue.href

States#

  • Loadingpage.tsx is async; verify whether loading.tsx ships a skeleton or the route blocks on getNyx()
  • Populated (BFF)getNyx() fetches /v1/nyx/tonight and mapNyxTonightToNyxData renders the computed events (sunset, twilight, moon) and headline
  • Unavailable / empty sky — when /v1/nyx/tonight is unreachable (no card.events), getNyx() returns nyxUnavailable(), 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 returns nyxUnavailable(), so a boundary is only hit on an unexpected throw — verify error.tsx/global-error.tsx coverage
  • 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 honor prefers-reduced-motion
  • High contrast / dark theme — chart panel uses fixed dark background: '#0e0d12' regardless of theme; cream paper around it stays light

Interactions#

  • 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-log or 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, with aria-label Open ${row.name} at ${row.time}: ${row.detail}
    • Keyboard: focusable (real anchor); Enter activates
    • Screen reader: announces the aria-label (name, time, detail)
  • "From Nisaba" quote — the italic data.canonCrossLink line 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.taraGroundingCue is set; a <Link href={data.taraGroundingCue.href}> (aria-label "Ground tonight's observation with a Tara sit") carrying taraGroundingCue.label

Data & contracts#

  • Reads: getNyx() from apps/oshun/web/src/lib/lilith-data/nyx.tsbffGet<NyxTonightResponse>('/v1/nyx/tonight', { revalidateSeconds: 300 }), mapped via mapNyxTonightToNyxData; returns NyxData. On an unreachable feed it returns nyxUnavailable() (an honest empty sky). There is no getNyxFixture().
  • Writes: none — read-only hub
  • Realtime: none in this view
  • Caching: server-component bffGet with revalidateSeconds: 300
  • Auth/role check: middleware enforces signed-in

Cross-references#

Open questions / known gaps#

  • Confirm whether the LSubNav tabs (Tonight / Planets / Meteors / Observations) are routed or decorative — code shows static items
  • Confirm chip semantics (Auto-orient, Compass, Show ecliptic, Mute light pollution) — LChip may be button or span; check LChip export
  • 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/tonight or only renders the fixed decorative asterism
  • Document loading skeleton (no loading.tsx adjacent to page.tsx)