---
status: reconciled-partial
coverage_depth: deep-for-separated-agenda-export-action-reminder-provider-and-observation-seams
last_walked: 2026-05-29
last_reconciled: 2026-07-21
specs:
  - apps/oshun/web/e2e/nyx-event-pages.spec.ts
  - apps/oshun/web/e2e/nyx-event-ics.spec.ts
  - apps/oshun/web/e2e/nyx-event-actions-bff.spec.ts
  - apps/oshun/web/e2e/nyx-event-actions-real-sync.spec.ts
  - apps/oshun/web/e2e/profile-calendar-sync.spec.ts
  - apps/oshun/web/e2e/calendar-connectors-bff.spec.ts
  - apps/oshun/web/e2e/nyx-observations-bff.spec.ts
  - apps/oshun/web/e2e/nyx-service-worker-notification.spec.ts
source: WALKTHROUGH/journeys/nyx-event-calendar-sync-reminder.md
---

# Journey result: Nyx calendar/reminder — real exports and state, no joined reminder receipt

- **Walked**: 2026-05-29 at `3678298cd3`. The retained result cited an ephemeral
  `/tmp/claude-oshun-lt-2-walkthrough/walk.mjs`, confirmed that `/nyx/events`
  and `/events` rendered, saw a shared event list and ICS affordance, and
  reported no page errors or persistent request failures. It retained no
  downloaded bytes, event identity, authenticated member, preference write,
  provider connection, scheduled reminder, delivered notification, tap
  destination, observation, trace, or external receipt.
- **Reconciled**: 2026-07-21 against both event authorities, both ICS
  generators, the browser and BFF action stores, the durable Nyx member store,
  provider OAuth and export routes, the opt-in reminder worker, delivery and Web
  Push code, the shipped service worker, both observation paths, focused unit
  suites, and all eight named Playwright files. The re-run below is local
  repository evidence, not a production provider, real device, restart,
  scheduler-clock, or external-calendar exercise.
- **Verdict**: **partial / deep for the separated shipped seams** — the dynamic
  agenda has a real whole-feed ICS export; the fixed rich catalog has a real
  browser-generated single-event ICS file; event-action preferences synchronize
  to a process-local mirror; a separate durable member-reminder record can feed
  provider export and an optional in-app worker; the service worker can route a
  supplied deep-link payload; and observations have an owner-scoped BFF record.
  No shipped path joins one rich-detail action to the durable reminder, provider
  write, real push payload, event navigation, and linked observation.
- **Current authority**:
  [`WALKTHROUGH/journeys/nyx-event-calendar-sync-reminder.md`](../journeys/nyx-event-calendar-sync-reminder.md)
  now names the distinct event ids and state owners instead of presenting them
  as one reminder lifecycle.

## Result at a glance

| Interaction             | Proof · limit                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Historical route walk   | **Proof:** both agenda routes rendered a shared list and ICS affordance. **Limit:** it neither downloaded the feed nor crossed an authenticated or external boundary.                                                                                                                                                                                                                                                                                            |
| Dynamic agenda          | **Proof:** `/nyx/events` filters the real `/v1/events/upcoming` response to Nyx and `/events` shows the shared feed. The BFF builds lunar phases, season markers, and source-backed featured eclipse/meteor entries rather than fabricating a fallback. **Limit:** these generated ids are not the fixed rich-catalog ids.                                                                                                                                       |
| Agenda ICS              | **Proof:** `/v1/nyx/events.ics` returns a 365-day, at-most-36-event calendar with `@oshun.app` UIDs, timestamps, summaries, descriptions, categories, and folded lines. **Limit:** every row's **Add to calendar** and the page subscription control target this same full feed; a row click is not a single-event export.                                                                                                                                       |
| Rich event catalog      | **Proof:** `/domains/nyx/events`, `/events/[eventId]`, and `/domains/nyx/events/[eventId]` render substantial fixed 2026 records. **Limit:** the catalog is a separate authored authority; fixed ids such as `march-equinox` do not match dynamic ids such as `season-march-equinox-YYYY-MM-DD`.                                                                                                                                                                 |
| Rich event ICS          | **Proof:** **Add to calendar** generates one browser download with a deterministic filename and `<eventId>@oshun-nyx` UID. **Limit:** there is no provider import acknowledgment, and this generator is unrelated to the agenda feed's bytes and ids.                                                                                                                                                                                                            |
| Event-action controls   | **Proof:** Save, Follow, Remind, cadence, and channels persist immediately in `oshun:nyx:event-actions:v1`; authenticated clients also PUT to owner-scoped BFF routes and hydrate the newer timestamp. **Limit:** the BFF store is only process memory, route labels are reconstructed metadata, and neither side calls a scheduler or provider.                                                                                                                 |
| Durable member reminder | **Proof:** the Nyx adapter validates a dynamic upcoming id and writes owner-scoped reminder state through the snapshot-backed member store; that record participates in export and erasure. **Limit:** the rich detail never calls this adapter, and its fixed ids generally fail the dynamic lookup.                                                                                                                                                            |
| Provider calendar       | **Proof:** Profile exposes Google, Apple, and Outlook connection controls; Google/Outlook can OAuth when configured; credentials are snapshot-backed; calendar preview/export reads domain schedule items and reports per-provider outcomes. **Limit:** the Profile browser spec mocks its BFF boundary, Apple is fail-closed by default, the default transport only writes Google, and the Nyx schedule reads durable member reminders—not detail action state. |
| Reminder worker         | **Proof:** when explicitly enabled, the BFF sweeps known users, converts enabled durable member reminders into stable scheduled reminders, and can deliver an in-app inbox item. **Limit:** the worker is disabled by default; the bridge supplies no external recipient and emits a nonexistent `/nyx/events/<id>` URL.                                                                                                                                         |
| Service-worker tap      | **Proof:** the actual `sw.js` preserves a manually supplied `actionPath`; a cold tap opens it, while an existing-window tap posts a message and focuses the tab. **Limit:** production delivery does not pass action data into Web Push, and `PwaBootstrap` does not handle `OSHUN_NOTIFICATION_TAP`, so this is not producer-to-event-navigation proof.                                                                                                         |
| Observation handoff     | **Proof:** rich detail links to the local-first observation log and the BFF stores owner-scoped observations durably; agenda rows use a separate direct-post form. **Limit:** the log shows `eventId` as a tag, not a return link; queued local failures have no replay path; and browser Nyx state is not subject partitioned.                                                                                                                                  |
| Named automation        | **Proof:** eight files contain 35 focused browser/API cases over the page, ICS, action-state, Profile/admin connectors, observation, and service-worker contracts. **Limit:** they are distributed contracts with mocked or injected boundaries, not one event-action-to-observation receipt.                                                                                                                                                                    |

## Evidence map

Solid arrows are real handoffs. Dashed arrows are missing bridges or identity
breaks. The diagram deliberately keeps the rich-detail preference, durable
member reminder, provider export, and injected notification on separate lanes.

```mermaid
flowchart TB
    J[Nyx calendar and reminder] --> A[Dynamic agenda]
    J --> R[Fixed rich catalog]
    A --> F[Whole-feed ICS<br/>dynamic ids · @oshun.app]
    R --> I[Single-event ICS<br/>fixed ids · @oshun-nyx]
    R --> P[Browser action preference<br/>process-local BFF mirror]
    D[Assistant / Nyx adapter] --> M[Durable member reminder<br/>dynamic event id]
    M --> C[Profile calendar export]
    M --> W[Opt-in reminder worker<br/>in-app schedule]
    W -. no actionPath payload .-> S[Service worker<br/>injected tap contract]
    P -. no durable-reminder bridge .-> M
    R --> O[Local-first observation log<br/>owner-scoped BFF row]
    S -. existing tab only focuses .-> O
```

## Proven observations

### The old pass proved repaired agenda routes, not calendar synchronization

- `/nyx/events` and `/events` do share `EventList`. The former filters the
  server result to `domain === 'nyx'`; the latter renders the cross-domain list.
  `getUpcomingEvents` requests `/v1/events/upcoming` and returns an empty state
  on failure rather than manufacturing browser fixtures.
- The BFF response is generated from the astronomy library: principal lunar
  phases and season markers are solved for the current window, while fixed
  eclipse and meteor templates are included only when their dates are in it. The
  event ids therefore encode occurrence and date.
- The historical walker saw a calendar label but did not establish what it
  exported. Source shows that every event row appends the identical
  `/v1/nyx/events.ics` link. The top-level subscription control uses that same
  href. A row-level label does not select the row.

### There are two real ICS generators with different records and identities

- The agenda endpoint is unauthenticated and emits the complete generated
  astronomical window. Every VEVENT has a `DTSTAMP`, UTC start, optional end,
  escaped descriptive fields, category, and `<dynamic-id>@oshun.app` UID. The
  response is private-cacheable for five minutes.
- The rich catalog is bundled into `NYX_WEB_EVENT_RECORDS` and has fixed 2026
  records such as `jupiter-venus-conjunction`, `march-equinox`,
  `perseid-meteor-shower-peak`, and `total-solar-eclipse-2026`. Its detail
  workspace calls a browser-only helper that writes one VEVENT, uses
  `<fixed-id>@oshun-nyx`, and downloads `<fixed-id>.ics`.
- Equivalent astronomical labels do not imply equivalent primary keys. The
  dynamic authority uses ids such as `season-march-equinox-YYYY-MM-DD` and
  `perseid-meteor-shower-peak-2026`. No reconciliation table maps those to the
  fixed detail ids. A past fixed event can still accept a UI preference because
  the action store validates shape, not current astronomical availability.

### Detail actions persist preference metadata, not scheduled work

- The detail hook owns one browser-global localStorage record. Mutations write
  synchronously, notify the same tab through a custom event, and rely on the
  storage event across tabs. With an access token, the hook also fire-and-
  forgets an owner-scoped PUT and later merges local and remote values by
  `updatedAt`.
- The BFF route correctly requires a Nyx-domain bearer, validates event ids,
  reminder times, cadence and channels, and caps the owner bucket. Its store is
  composed of `Map` objects and has no snapshot sink, startup binding, provider
  call, due-time index, or dispatcher. Restart loses this mirror.
- `mobile-push`, email, SMS, and `calendar-sync` rows are reconstructed from the
  chosen channel strings. They make the UI state legible; no consumer resolves
  them into verified recipients or performs delivery.
- Normal auth cleanup does not remove `oshun:nyx:event-actions:v1`. On a shared
  browser, member A's values can remain visible to member B and, after a later
  mutation, be mirrored into B's server bucket. The owner-scoped BFF route does
  not repair a browser key that lacks a subject partition.

### The real durable reminder belongs to another authority

- `in-process-adapter.ts` implements `setEventReminder` by rebuilding upcoming
  events, finding the supplied dynamic id, and then calling
  `nyxMemberStateStore.addReminder`. The stored row owns title, type, due time,
  cadence, channels, enabled state, and member id.
- The server requires that member store's durability and wires it to the
  supported snapshot database. Its records participate in customer export and
  erasure. This is materially stronger than the event-action mirror.
- The assistant's `nyx.set_reminder` action can reach the adapter. No rich
  event-detail component calls it, and a fixed catalog id generally is not in
  the adapter's dynamic event set. Similar field names do not form a bridge.

### Provider export is real, but it reads the durable reminder lane

- Profile's Calendar section lists Google, Apple, and Outlook, previews the
  domain schedule, starts OAuth, lists/removes connections, and calls the
  separate calendar export route. Google and Outlook OAuth are available with
  configuration. Apple deliberately returns
  `503 apple_calendar_oauth_unconfigured` in the default runtime.
- Established OAuth connections, including refresh credentials, are stored
  behind a snapshot sink and exposed through secret-free views. Pending OAuth
  state is ephemeral. The default transport factory implements Google writes;
  other established providers need an injected/configured transport and
  otherwise report provider-unavailable outcomes.
- `buildUserDomainCalendarItems` includes only enabled durable Nyx reminders
  whose channels are absent or include `external-calendar`, and only inside its
  14-day window. It never reads `event-action-state-store.ts`.
- Both the calendar item and reminder bridge emit
  `https://oshun.app/nyx/events/<dynamic-id>`. The web app has no
  `/nyx/events/[eventId]` route; the rich routes are `/events/[eventId]` and
  `/domains/nyx/events/[eventId]`, and those expect fixed ids anyway. The
  exported URL is therefore not a working event detail.
- `profile-calendar-sync.spec.ts` is useful UI/wire-shape coverage but fulfills
  all calendar BFF routes itself. The real two-way Fastify unit flow uses an
  injected provider transport. `calendar-connectors-bff.spec.ts` exercises the
  separate admin connector runtime, not member Profile OAuth.

### The worker and notification test do not close the delivery loop

- `produceNyxEventReminders` reads enabled durable member reminders and gives
  each one a stable `nyx-event:<user>:<reminder>` session id. The generic
  planner schedules around `reminderTime`. The server starts the autonomous
  worker only when `OSHUN_REMINDER_WORKER_INTERVAL_MS` parses to a positive
  value; it is intentionally off by default.
- The bridge labels the channel `push` but supplies an empty recipient. Its
  reliable outcome is the owner-keyed in-app inbox path; an external push lacks
  a target. The delivery cycle can still report the person reached when in-app
  succeeds, so that aggregate result must not be read as provider
  acknowledgment.
- The two service-worker cases load the shipped script in a VM and manually
  inject a payload containing a valid rich route. That proves `actionPath`
  survives `showNotification`, cold taps call `openWindow`, and warm taps post a
  message before focusing a window.
- The general Web Push transport supports optional data, but the delivery
  dispatcher calls it with title and body only. `PwaBootstrap` handles sync and
  update messages, not `OSHUN_NOTIFICATION_TAP`. Thus the tested payload is not
  produced by the durable reminder lane, and a tap with an existing app window
  does not navigate that window to the event.

### Observation storage is real but remains a separate finish

- Rich detail sends **Log observation** to
  `/domains/nyx/observation-log?eventId=<fixed-id>`. The log immediately creates
  a local Zustand row with `syncState: queued`, then attempts the authenticated
  BFF POST. Success marks it synchronized; failure leaves it queued, with no
  replay worker found in the audited path.
- The BFF observation endpoint is owner scoped and durable through Postgres or
  the supported snapshot fallback. Its named API cases cover authorization,
  validation, defaults, and owner isolation. The UI renders **Linked event** as
  a tag, not an anchor back to the source detail.
- Agenda event rows instead link to `/nyx/observation?eventId=<dynamic-id>`, a
  separate direct-post form. Neither path is the destination of a real
  reminder-produced notification.
- The persisted `oshun.nyx` Zustand key is browser global and is not cleared by
  normal auth cleanup. Observations, equipment, and sky preferences can remain
  visible across accounts on a shared browser even though server rows are
  correctly owner scoped.

## Boundaries and gaps

- **An agenda row is not the rich event record.** Dynamic ephemeris ids and
  fixed detail ids have no reconciliation layer.
- **A row-level calendar label is not a per-event export.** Every agenda row
  downloads the same whole feed.
- **A delivery-route label is not a dispatched channel.** The detail state
  reconstructs labels without recipient resolution or transport calls.
- **A process-local mirror is not the durable reminder authority.** The action
  BFF map and snapshot-backed member reminder are different stores.
- **A shared label is not a shared event identity.** Equinox and Perseid names
  do not make their fixed and dynamic ids interchangeable.
- **A durable member reminder is not created by the detail controls.** The only
  audited creation path is the separate Nyx adapter/assistant action.
- **A provider export is not event-detail calendar sync.** Profile exports the
  durable schedule; detail downloads a file and never calls that route.
- **An injected push payload is not producer-to-service-worker proof.** The VM
  test supplies fields that the delivery call does not send.
- **Focusing an existing tab is not navigating to the event.** The posted tap
  message has no application handler.
- **A linked-event tag is not a return link.** Observation read-back preserves
  the id but does not make it navigable.
- **Browser-local state is not subject-partitioned state.** Both Nyx storage
  keys outlive normal account cleanup without a user namespace.
- **Thirty-five cases are not one reminder-to-observation receipt.** The named
  suites prove independent page, API, export, connector, and worker-adjacent
  contracts with mocks and injected payloads at important boundaries.

## Re-run evidence

- `python3 -m pytest tools/docs_center/tests/test_generator.py -q` finished with
  **53 passed**. The Result 44 assertion pins registry position 44, front
  matter, section order, one evidence map, the 15 + 2 + 4 + 1 + 4 + 3 + 4 + 2
  named case counts, both ICS authorities, both reminder stores, provider
  source, worker opt-in, notification-tap gap, broken canonical URLs, and
  shared-browser state boundaries.
- Seventeen focused unit/integration files finished with **147 passed** over
  astronomical events and feed ICS, fixed page models, action
  state/store/routes, durable member state, provider connection/export, the real
  two-way Fastify calendar flow, reminder bridge, observation UI, Web Push
  payload delivery, and PWA message handling. The three selected domain-stub
  route cases passed; its other 91 cases were intentionally filtered from this
  focused run.
- The eight named Playwright files ran serially with one Chromium worker. The
  reused BFF passed **33 of 35** before the two seeded-admin assertions
  correctly exposed its missing `OSHUN_CALENDAR_CONNECTORS` environment; the
  whole three-case connector file then passed against an isolated correctly
  seeded BFF. Every named case therefore has a passing run, while remaining
  distributed evidence: Profile is mocked, the service-worker payload is
  injected, and no case performs one detail action → durable reminder →
  provider/worker receipt → tap → observation chain.
- Docs generation produced **3,116 files / 2,787 reader pages**; freshness
  accepted all 3,116. Integrity reported **0 structural broken links / 0 dead
  source paths** across 3,485 entities, with 112 pre-existing imported-content
  warnings surfaced as nonblocking.
- The focused Docs Center reader case passed at desktop and Pixel 7 dimensions
  (**2 passed**). It checks partial status, substantive prose, all twelve
  boundary statements, Mermaid containment and pairwise node geometry, expand-
  control clearance, heading anchors, journey navigation, console/page errors,
  and horizontal overflow. Full-page screenshots at 1,440 px and Pixel 7
  dimensions were also reviewed without a clipping or hierarchy defect.

## Source trail

- Dynamic agenda and feed:
  `apps/oshun/web/src/app/{nyx/events,events}/page.tsx`,
  `apps/oshun/web/src/components/nyx/EventList.tsx`,
  `apps/oshun/web/src/lib/server/nyx-depth.ts`,
  `apps/oshun/bff/src/routes/domain-stubs.ts`, and
  `libs/oshun/domain-nyx/src/upcoming-events.ts`.
- Fixed catalog and per-event ICS:
  `apps/oshun/web/src/components/domains/nyx/nyxEventPageModels.ts` and
  `apps/oshun/web/src/components/domains/nyx/NyxEventDetailWorkspace.tsx`.
- Detail action state:
  `apps/oshun/web/src/components/domains/nyx/useNyxEventActionState.ts`,
  `apps/oshun/bff/src/nyx/event-action-state-store.ts`, and
  `apps/oshun/bff/src/routes/nyx.ts`.
- Durable reminder authority: `apps/oshun/bff/src/nyx/in-process-adapter.ts`,
  `apps/oshun/bff/src/nyx/nyx-member-stores.ts`, and
  `apps/oshun/bff/src/server.ts`.
- Provider calendar:
  `apps/oshun/web/src/components/profile/CalendarSyncSection.tsx`,
  `apps/oshun/bff/src/routes/calendar-connect.ts`,
  `apps/oshun/bff/src/routes/calendar-events.ts`,
  `apps/oshun/bff/src/calendar/calendar-oauth-connection-store.ts`, and
  `apps/oshun/bff/src/calendar/domain-calendar-events.ts`.
- Delivery and notification:
  `apps/oshun/bff/src/reminders/nyx-reminder-bridge.ts`,
  `libs/oshun/messaging-channels/src/delivery.ts`,
  `libs/oshun/messaging-channels/src/web-push-transport.ts`,
  `apps/oshun/web/public/sw.js`, and
  `apps/oshun/web/src/components/PwaBootstrap.tsx`.
- Observation and browser cleanup:
  `apps/oshun/web/src/components/domains/nyx/NyxObservationLog.tsx`,
  `apps/oshun/web/src/lib/stores/domain-stores.ts`, and
  `apps/oshun/web/src/lib/auth-context.tsx`.

## Cross-references

- [`nyx-tonight-observation.md`](./nyx-tonight-observation.md) covers the
  dynamic sky/observation surface and its own identity boundaries.
- [`messages-quiet-hours-and-channel-binding.md`](./messages-quiet-hours-and-channel-binding.md)
  separates verified recipient binding from in-app and provider delivery.
- [`multi-device-workspace-handoff.md`](./multi-device-workspace-handoff.md)
  covers browser-local state and device-continuity limits.
- [`account-deletion-and-dsar.md`](./account-deletion-and-dsar.md) covers the
  difference between durable export/erasure participation and unmanaged local
  browser keys.

## Open questions

- Which event registry should be canonical, and how should fixed rich records
  map to computed astronomical occurrences without silently changing identity?
- Should **Remind** create the durable member reminder through an authenticated
  mutation, and should action preferences then be removed or become a view of
  that record?
- Should agenda rows offer one-event ICS bytes, reserve the current link for a
  clearly labelled subscription, or provide both?
- Which provider transports are supported in production, and what per-event
  provider receipt should the UI expose after an explicit export?
- Should the reminder bridge resolve a verified push recipient, emit a valid
  rich-detail URL and `actionPath`, and route warm notification taps in the app?
- Should Nyx localStorage/Zustand records be user partitioned and cleared on
  logout, with queued observation replay and conflict handling?
- What single trace id and same-event assertions should gate a future detail →
  reminder → delivery → event open → observation journey?
