---
path: /nyx
surface: customer
domain: nyx
auth: signed-in
source: apps/oshun/web/src/app/nyx/page.tsx
status: walked
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'
---

# Nyx · the sky tonight

## 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

- [ ] **Loading** — `page.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

### 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-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)

### Canon cross-link (Nisaba)

- [ ] **"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.ts` —
  `bffGet<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

- Lilith design system index: [`../10-lilith/lilith.md`](../10-lilith/lilith.md)
- Sibling Nyx routes:
  - [`domains-nyx-star-chart.md`](./domains-nyx-star-chart.md)
  - [`domains-nyx-events.md`](./domains-nyx-events.md)
  - [`domains-nyx-events-eventId.md`](./domains-nyx-events-eventId.md)
  - [`domains-nyx-moon.md`](./domains-nyx-moon.md)
  - [`domains-nyx-solar.md`](./domains-nyx-solar.md)
  - [`domains-nyx-observation-log.md`](./domains-nyx-observation-log.md)
- 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`](../../../V1/features.md#nyx)

## 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`)
