V1 Web PWA · Surface walkthrough

Nyx · Sky Almanac

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

walked + e2e-covered
9sections5 minread

On this page

Context. surface customer · domain nyx · route /nyx/sky-almanac · auth signed-in · source apps/oshun/web/src/app/nyx/sky-almanac/page.tsx

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

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

  • 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
  • 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
  • Populateddata.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 " — " (data-testid="sky-almanac-detail", data-nyx-sky-almanac-detail)
  • 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.
  • 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.
  • 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.

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

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#

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.