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

# Nyx · tonight

## Purpose

Tonight's sky over the user's observation window, in order: civil twilight,
planet rises, conjunctions, meteor peaks. The page fetches `/v1/nyx/tonight` (60
s revalidate), promotes the BFF event flagged `accent: true` to a highlighted
"lead event", and stacks the rest below in chronological order, each row
offering a "Log it →" shortcut into the field log. See
[`V1/features.md#nyx`](../../../V1/features.md#nyx).

## Entry points

- **Nyx hub (`/nyx`)** — verify a "Tonight" link from the hub sub-nav
- **`/nyx/events` footer** — "Tonight" ghost button → `/nyx/tonight`
- **`/nyx/observation`** — both the form's "Tonight" button and the accepted
  card's "Tonight" button route here
- **Direct URL / bookmark** — yes (`alternates.canonical = '/nyx/tonight'`);
  signed-in
- **Shell nav** — arrives under the `explore` tab
  (`LCustomerNav active="explore"`)

## Layout regions

`page.tsx` is async: `await getNyxTonight()`, then picks
`headline = events.find(e => e.accent) ?? events[0]` and
`rest = events.filter(e => e.id !== headline?.id)`. Centered column,
`maxWidth: 880`, `data-responsive-page`.

- **Top chrome**: `LCustomerNav` (Explore active) with "Lilith" wordmark + "⌕
  Search"
- **Masthead** (`LMasthead`): left `data.date`, right `data.observationWindow`,
  kicker "The sky tonight", title = italic `data.tonightHeadline`
- **Lead event card** (only when `headline` exists): accent border, `LEyebrow`
  "Lead event · <time>", serif `headline.name` (h2), italic `headline.detail`
- **"The order of the night"** section: `LEyebrow` then an `<ol>` rule-bordered
  list of `rest` rows; each row is `90px | 1fr | auto` — mono time, serif name +
  italic detail, and a "Log it →" accent link
- **Empty timeline row**: when `rest` is empty, one italic muted `<li>` "No
  further events on the timeline tonight."
- **Footer button row**: "← Back to Nyx", "Upcoming events", "Plan an
  observation"

## States

- [ ] **Loading** — `page.tsx` is async; no adjacent `loading.tsx`, so the route
      blocks server-side on `getNyxTonight()` (5 s `bffGet` timeout)
- [ ] **Populated (BFF)** — `/v1/nyx/tonight` returns events; the `accent` one
      leads, the rest list chronologically with "Log it →" links
- [ ] **Lead-only** — exactly one event (the accent lead): the card renders and
      `rest` is empty, showing "No further events on the timeline tonight."
- [ ] **Data unavailable** — when `bffGet` returns `null`, `shapeTonight` →
      `tonightUnavailable()`: `observationWindow = 'Unavailable'`,
      `tonightHeadline = "Tonight's sky data is unavailable right now — check     back shortly."`,
      and `events = []`. Deliberately no fabricated conjunctions: no lead card,
      empty-timeline row shows. (This is the honest empty/error state —
      astronomy claims must not be invented.)
- [ ] **Error (recoverable)** — `bffGet` swallows non-2xx/network and returns
      `null` → unavailable state above; no retry button surfaced
- [ ] **Error (unrecoverable)** — no `error.tsx` adjacent; unexpected throw
      bubbles to a higher boundary (verify)
- [ ] **Offline** — served from SW cache where present; "Log it →" links are
      plain hrefs to `/nyx/observation`
- [ ] **Gated** — middleware signed-in; `bffGet` forwards session cookie
- [ ] **Standalone PWA** — renders cleanly; verify safe-area insets on footer
      row

## Interactions

### Lead event card

- [ ] **Lead event card** (section; non-interactive)
  - Function: displays the accent event's time, name, and detail; no link
  - Screen reader: announces "Lead event · <time>", then name (h2), then detail

### Timeline list ("The order of the night")

- [ ] **Event row** (per `rest` entry; the row itself is not a link)
  - Function: shows mono `time`, serif `name`, italic `detail`
  - Screen reader: announces time, name, detail, then the row's "Log it" link
- [ ] **"Log it →"** (link, one per row)
  - Function: → `/nyx/observation?eventId=<encodeURIComponent(event.id)>` to
    pre-anchor a field log to that event
  - Touch target: small mono uppercase link (fontSize 10) — verify ≥ 44×44 px
    hit area on coarse pointers (see gaps)
  - Mobile (≤ 640 px): `whiteSpace: nowrap` keeps it on one line; verify it
    doesn't overflow the `auto` column

### Footer buttons

- [ ] **"← Back to Nyx"** (`LBtn` ghost) → `/nyx`
- [ ] **"Upcoming events"** (`LBtn` ghost) → `/nyx/events`
- [ ] **"Plan an observation"** (`LBtn` primary) → `/nyx/observation` (no
      `eventId`, so a freeform log)

## Data & contracts

- **Reads**: `getNyxTonight()` (`@/lib/server/nyx-depth`) →
  `bffGet<BffNyxTonight>('/v1/nyx/tonight')`; `shapeTonight` normalizes to
  `NyxTonightData` (`generatedAtIso`, `date`, `observationWindow`,
  `tonightHeadline`, `events: NyxTonightEvent[]` where each event is `id`,
  `time`, `name`, `detail`, `accent`). Events missing any required string field
  are dropped by the filter.
- **Writes**: none here (the "Log it →" link defers the write to
  `/nyx/observation`)
- **Realtime**: none
- **Caching**: `bffGet` `next: { revalidate: 60 }` (matches the page header's
  "60s revalidate")
- **Auth/role check**: middleware signed-in; session cookie forwarded by
  `bffGet`

## Cross-references

- Feature spec: [`V1/features.md#nyx`](../../../V1/features.md#nyx)
- Architecture: [`V1/ARCHITECTURE.md#nyx`](../../../V1/ARCHITECTURE.md#nyx)
- Sibling Nyx routes: [`nyx.md`](./nyx.md), [`nyx-events.md`](./nyx-events.md),
  [`nyx-observation.md`](./nyx-observation.md)
- Journeys:
  [`journeys/nyx-tonight-observation.md`](../../journeys/nyx-tonight-observation.md)
  (this view's "Log it →" is the entry into that journey)
- Component / data sources:
  - `apps/oshun/web/src/lib/server/nyx-depth.ts` (`getNyxTonight`,
    `shapeTonight`, `tonightUnavailable`)

## Open questions / known gaps

- [ ] The two NyxTonight states (populated, unavailable) collapse the same way
      when `events` is empty: there is no separate retry affordance for a true
      BFF failure — confirm whether a recoverable-error state with retry is
      wanted
- [ ] "Log it →" link is a 10 px mono link with no padded hit area — verify ≥
      44×44 px touch target on coarse pointers
- [ ] No telemetry events on the lead card or "Log it →" links — confirm
      intended for V1
- [ ] `data.observationWindow` and `data.date` are surfaced verbatim from the
      BFF; verify they reflect the caller's actual location/lat-lng, not a
      server default
