Context. surface customer · domain nyx · route /nyx/tonight · auth signed-in · source apps/oshun/web/src/app/nyx/tonight/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
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.
Entry points#
- Nyx hub (
/nyx) — verify a "Tonight" link from the hub sub-nav /nyx/eventsfooter — "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
exploretab (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): leftdata.date, rightdata.observationWindow, kicker "The sky tonight", title = italicdata.tonightHeadline - Lead event card (only when
headlineexists): accent border,LEyebrow"Lead event · - "The order of the night" section:
LEyebrowthen an<ol>rule-bordered list ofrestrows; each row is90px | 1fr | auto— mono time, serif name + italic detail, and a "Log it →" accent link - Empty timeline row: when
restis 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.tsxis async; no adjacentloading.tsx, so the route blocks server-side ongetNyxTonight()(5 sbffGettimeout) - Populated (BFF) —
/v1/nyx/tonightreturns events; theaccentone leads, the rest list chronologically with "Log it →" links - Lead-only — exactly one event (the accent lead): the card renders and
restis empty, showing "No further events on the timeline tonight." - Data unavailable — when
bffGetreturnsnull,shapeTonight→tonightUnavailable():observationWindow = 'Unavailable',tonightHeadline = "Tonight's sky data is unavailable right now — check back shortly.", andevents = []. 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) —
bffGetswallows non-2xx/network and returnsnull→ unavailable state above; no retry button surfaced - Error (unrecoverable) — no
error.tsxadjacent; 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;
bffGetforwards 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 ·
Timeline list ("The order of the night")#
- Event row (per
restentry; the row itself is not a link)- Function: shows mono
time, serifname, italicdetail - Screen reader: announces time, name, detail, then the row's "Log it" link
- Function: shows mono
- "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: nowrapkeeps it on one line; verify it doesn't overflow theautocolumn
- Function: →
Footer buttons#
- "← Back to Nyx" (
LBtnghost) →/nyx - "Upcoming events" (
LBtnghost) →/nyx/events - "Plan an observation" (
LBtnprimary) →/nyx/observation(noeventId, so a freeform log)
Data & contracts#
- Reads:
getNyxTonight()(@/lib/server/nyx-depth) →bffGet<BffNyxTonight>('/v1/nyx/tonight');shapeTonightnormalizes toNyxTonightData(generatedAtIso,date,observationWindow,tonightHeadline,events: NyxTonightEvent[]where each event isid,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:
bffGetnext: { 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 - Architecture:
V1/ARCHITECTURE.md#nyx - Sibling Nyx routes:
nyx.md,nyx-events.md,nyx-observation.md - Journeys:
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
eventsis 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.observationWindowanddata.dateare surfaced verbatim from the BFF; verify they reflect the caller's actual location/lat-lng, not a server default