---
path: /nyx/sky-almanac
surface: customer
domain: nyx
auth: signed-in
source: apps/oshun/web/src/app/nyx/sky-almanac/page.tsx
status: walked + e2e-covered
last_walked:
  '2026-06-29 real-infra Playwright chromium — signed-in shell route, live BFF
  default + ?lat=51.5&lon=0 contracts, section/row DOM anchors, mobile
  standalone viewport, shared axe fixture. Evidence:
  WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md#198-2026-06-29-nyx-sky-almanac-live-bff-coverage'
---

# Nyx · Sky Almanac

## Purpose

A power-user "almanac for your sky" depth page: solar-noon altitude,
sunrise/sunset bearings, the Moon's phase and distance, chart angles, and the
night's true darkness window. All astronomy is computed upstream
(`@oshun/domain-nyx` → `buildNyxSkyAlmanacPage` over the in-repo ephemeris) and
served by the BFF at `GET /v1/nyx/sky-almanac`; this page only fetches the
view-model server-side and lays out its titled label/value/detail sections.

## Entry points

- **Direct URL / bookmark** — `/nyx/sky-almanac`
- **Nyx hub depth nav** — power-user complement to the consumer Nyx surfaces
  (`/nyx`, `/nyx/tonight`, `/nyx/observation`, `/nyx/events`)
- **Shell nav** — `LCustomerNav active="explore"` (the page mounts the customer
  nav with the Explore tab active)

## Layout regions

- **Header**: `LWebShell` top = `LCustomerNav active="explore"`
- **Main**: centered responsive column (`data-responsive-page`, `maxWidth: 880`,
  padding `32px 56px 80px`, `data-nyx-sky-almanac-page`,
  `data-route="/nyx/sky-almanac"`) holding `SkyAlmanacSections` (root wrapper
  `data-testid="sky-almanac"`, `data-nyx-sky-almanac`, `data-available`,
  `data-generated-at`, `data-section-count`, `data-row-count`)
  - **`<h1>`** — `data.title` (defaults to "Sky Almanac")
  - **Subtitle** —
    `<p data-testid="sky-almanac-subtitle" data-nyx-sky-almanac-subtitle>` (only
    when non-empty)
  - **Sections** — one `<section aria-label="<title>">` per almanac section,
    each also carries `data-nyx-sky-almanac-section`, `data-section-key`,
    `data-section-title`, and `data-row-count`. Each section is an `<h2>` + a
    `<dl>` whose rows are
    `<div data-testid="sky-almanac-row" data-nyx-sky-almanac-row data-row-key data-row-label data-has-detail>`
    holding
    `<dt>label</dt><dd>value<span data-testid="sky-almanac-detail" data-nyx-sky-almanac-detail> — detail</span></dd>`
    (the `detail` span renders only when a row has a detail; the expected
    sections are Sun / Moon / Chart angles / Darkness)
- **Aside / Footer**: _None._

## States

- [x] **Loading** — N/A in-view: the page is an async server component
      (`await getNyxSkyAlmanac()`); the route-level Suspense/skeleton applies
      while the server renders, not an in-component spinner
- [x] **Empty** — N/A: a configured backend always returns at least the Sun /
      Moon sections; an empty `sections[]` simply renders the `<h1>` + subtitle
      with no `<dl>` rows
- [x] **Populated** — `data.available === true`: real-infra Playwright asserts
      the live BFF contract and browser DOM agree on section count, row count,
      section titles, row labels, detail presence, generated-at shape, and the
      visible subtitle; rows with a `detail` append " — <detail>"
      (`data-testid="sky-almanac-detail"`, `data-nyx-sky-almanac-detail`)
- [x] **Error / unavailable (backend down)** — `data.available === false`:
      `shapeSkyAlmanac` falls back to `skyAlmanacUnavailable()` — title "Sky
      Almanac", subtitle "Unavailable", **zero rows**, and a `role="status"`
      note (`data-testid="sky-almanac-unavailable"`) "Sky almanac data is
      unavailable right now — check back shortly." **No fabricated astronomical
      values are shown** (a falsifiable claim must never be invented). Note
      `shapeSkyAlmanac` also returns the unavailable state whenever the BFF
      payload lacks an array `sections` field
      (`!remote || !Array.isArray(...)`); covered by `nyx-depth.test.ts` and
      `SkyAlmanacSections.test.tsx`
- [ ] **Offline** — server fetch fails → unavailable state (same as backend
      down); the SW may serve a cached prior render where applicable. Browser
      forcing this branch requires an isolated BFF-failure harness because the
      fetch happens inside the Next server component, not in the browser network
      stack.
- [x] **Signed-in shell context** — the route is walked through
      `primeAuthenticatedShellPage` / `openAuthenticatedShellPath`; Playwright
      asserts it does not land on `/welcome` or `/login` and that `LCustomerNav`
      marks Explore active. The domain-stubs BFF endpoint itself currently has
      no route pre-handler, so do not treat the BFF route as an authorization
      boundary.
- [x] **Standalone PWA** — mobile standalone launch is mocked with
      `(display-mode: standalone)`, the responsive column renders without
      horizontal overflow, and the section/row structure remains visible

## Interactions

This is a read-only computed-data page — no forms, buttons, or mutations.

- [x] **Section navigation** — sections are plain landmarks (`aria-label`);
      Playwright asserts the Sun / Moon / Chart angles / Darkness sections are
      present with their `<h2>` headings and row lists, so screen-reader users
      can jump between them via headings and landmarks
- [x] **`LCustomerNav`** — shell navigation (Explore active); this page asserts
      the active Explore link and the shared shell behavior is covered in
      [`../../shell/01-app-shell.md`](../../shell/01-app-shell.md)

## Data & contracts

- **Reads (server)**: `getNyxSkyAlmanac()` → `bffGet('/v1/nyx/sky-almanac')` →
  normalized `NyxSkyAlmanacData`
  (`{ title, subtitle, generatedAtIso, available, sections: [{ title, rows: [{ label, value, detail? }] }] }`);
  the shaper filters out malformed sections/rows and stamps `available: false`
  when the BFF is unreachable
- **Reads (BFF)**: `GET /v1/nyx/sky-almanac` computes the default observer;
  `GET /v1/nyx/sky-almanac?lat=51.5&lon=0` computes an explicit observer and
  returns subtitle `51.50°, 0.00°E`
- **Writes**: _None._
- **Realtime**: _None._
- **Caching**: SSR per request (async server component); no client fetch
- **Auth/role check**: page is exercised as a signed-in customer shell route;
  the current domain-stubs BFF route is not guarded by a pre-handler
- **Contracts**: `NyxSkyAlmanacData` / `NyxAlmanacSection` / `NyxAlmanacRow` in
  `apps/oshun/web/src/lib/server/nyx-depth.ts`; computation in
  `@oshun/domain-nyx` (`buildNyxSkyAlmanacPage`)

## E2E coverage

The Playwright route spec is the authoritative browser proof for this
walkthrough: it runs against the dev Next/BFF stack, verifies the live almanac
BFF default and coordinate-specific contracts, opens the signed-in customer
route, and checks section landmarks, row/detail DOM anchors, generated-at shape,
Explore nav context, standalone mobile containment, and no horizontal overflow.
Component and server-shaper Vitest coverage back the same read-only and
unavailable-state contracts at narrower layers.

- `apps/oshun/web/e2e/nyx-sky-almanac.spec.ts` — live BFF contract checks for
  default and explicit `lat/lon` observer payloads; signed-in route rendering
  from the live BFF contract; section/row/detail observability; Explore nav
  active state; mobile standalone launch; and no horizontal overflow.
- `apps/oshun/web/src/app/nyx/sky-almanac/SkyAlmanacSections.test.tsx` —
  component rendering, empty/unavailable states, and row/detail structure.
- `apps/oshun/web/src/lib/server/nyx-depth.test.ts` — server-side BFF shaping
  and unavailable fallback behavior.

## Cross-references

- Sibling Nyx surfaces: [`nyx.md`](./nyx.md),
  [`nyx-tonight.md`](./nyx-tonight.md),
  [`nyx-observation.md`](./nyx-observation.md),
  [`nyx-events.md`](./nyx-events.md),
  [`domains-nyx-solar.md`](./domains-nyx-solar.md),
  [`domains-nyx-moon.md`](./domains-nyx-moon.md),
  [`domains-nyx-star-chart.md`](./domains-nyx-star-chart.md)
- Studio downstream:
  [`../../studio/generation/studio-generation-nyx-3d.md`](../../studio/generation/studio-generation-nyx-3d.md)
- Journeys: `journeys/nyx-tonight-observation.md`,
  `journeys/nyx-event-calendar-sync-reminder.md`
- Component sources:
  `apps/oshun/web/src/app/nyx/sky-almanac/SkyAlmanacSections.tsx`,
  `apps/oshun/web/src/lib/server/nyx-depth.ts`

## Known downstream boundaries

- [ ] The BFF accepts explicit `lat`/`lon` query parameters and otherwise uses
      the default observer, but the page currently calls `/v1/nyx/sky-almanac`
      without passing a session, profile, or URL observer. Decide whether
      `/nyx/sky-almanac` should let users choose or persist their observing
      site, and whether the subtitle should explain the default site.
- [ ] Browser-driving the backend-down state needs an isolated failure harness
      for server-component BFF fetches; unit coverage proves the fallback today.
