V1 Web PWA · Journey

Journey: Nyx event calendar sync + reminder

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 8 specs verdict: partial· 2026-05-29
9sections10 minread

On this page
Journey at a glance
ActorsAgenda watcherRich-event readerReminder plannerConnected-calendar memberObserver
Automation8 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Choose the event authority before choosing…"] s2["2. Export the appropriate ICS contract"] s3["3. Treat rich-detail action controls as prefe…"] s4["4. Create a durable reminder through the sepa…"] s5["5. Connect and export a provider from Profile"] s6["6. Distinguish optional in-app delivery from…"] s7["7. Record the observation through the matchin…"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 click s1 href "#1-choose-the-event-authority-before-choosing-an-action" click s2 href "#2-export-the-appropriate-ics-contract" click s3 href "#3-treat-rich-detail-action-controls-as-preference-metadata" click s4 href "#4-create-a-durable-reminder-through-the-separate-nyx-member-authority" click s5 href "#5-connect-and-export-a-provider-from-profile" click s6 href "#6-distinguish-optional-in-app-delivery-from-push-navigation" click s7 href "#7-record-the-observation-through-the-matching-surface" s7 --> v(["verdict: partial"]) click v href "../results/nyx-event-calendar-sync-reminder.html" style v stroke:#d6a531,color:#d6a531

Nyx exposes useful calendar, preference, durable reminder, delivery, and observation capabilities, but today they form several separated seams with two event registries. The dynamic /nyx/events agenda exports a whole astronomical feed. The fixed /domains/nyx/events catalog exports one browser-generated event. Rich-detail action controls write browser state and a process-local BFF mirror. A separate assistant/Nyx-adapter path owns durable member reminders; only that state feeds provider calendars and the optional reminder worker. Service-worker notification and observation coverage prove further independent contracts. This journey must preserve those boundaries until one same-event receipt joins them.

Personas#

  • Agenda watcher — scans computed astronomical events and subscribes to the full calendar feed.
  • Rich-event reader — opens an authored 2026 detail and downloads a single ICS file.
  • Reminder planner — records a cadence and channel preference, or asks the assistant to create a durable reminder.
  • Connected-calendar member — authorizes a provider in Profile and exports the member schedule.
  • Observer — records an owner-scoped observation associated with an event id.

Pre-conditions#

  • Use /nyx/events for the dynamic Nyx agenda and /events for the shared cross-domain agenda. These call /v1/events/upcoming and use computed ids.
  • Use /domains/nyx/events for the fixed rich catalog. Its detail routes are /events/<fixed-id> and /domains/nyx/events/<fixed-id>; there is no /nyx/events/<id> detail route.
  • Do not treat matching labels as matching records. march-equinox and season-march-equinox-YYYY-MM-DD, for example, belong to different authorities.
  • Use an authenticated session for action-state mirroring, durable member reminders, provider connections, exports, and BFF observation rows.
  • Configure a supported provider before expecting OAuth or export. Profile displays Google, Apple, and Outlook; Apple is fail-closed by default, and the default writer transport supports Google.
  • Enable OSHUN_REMINDER_WORKER_INTERVAL_MS with a positive value before expecting autonomous in-process reminder delivery; the worker is off by default.

Steps#

1. Choose the event authority before choosing an action#

For an agenda/subscription path, open /nyx/events. The page filters the real upcoming-events response to Nyx records. Lunar phases and seasons are computed for the current window, while featured eclipse and meteor records are source-backed templates included only when upcoming. The shared /events page uses the same response without the Nyx-only filter.

For a rich detail, open /domains/nyx/events and select a bundled record. These fixed 2026 records carry description, timing, visibility, guidance, and recommendations. They are not detail views over the dynamic agenda. Record the actual id and source in evidence; a human-readable astronomical name is insufficient.

2. Export the appropriate ICS contract#

On either agenda page, Subscribe (.ics) and every row-level Add to calendar (.ics) link resolve to /v1/nyx/events.ics. The endpoint exports the whole 365-day astronomical feed, capped at 36 records, with @oshun.app UIDs. The row label does not narrow the bytes to that event.

On a rich detail, Add to calendar calls downloadNyxEventICalFile. It downloads exactly one fixed record as <eventId>.ics, with an <eventId>@oshun-nyx UID. The browser/OS then owns import. Neither file path provides an external-client acknowledgment, and the two generators must not be compared as if their ids or UIDs represented the same event record.

3. Treat rich-detail action controls as preference metadata#

Save, Follow, Remind, cadence, and channel choices write immediately to oshun:nyx:event-actions:v1. Same-tab and cross-tab events refresh the UI. When an access token exists, the hook also PUTs the selected state to /v1/nyx/event-actions/<eventId> and hydrates the newest timestamp.

The BFF action store is an owner-scoped process-local map with no snapshot sink. It validates the request and reconstructs route labels, but it has no due-time worker, verified-recipient join, provider export call, or channel dispatcher. Selecting push, email, SMS, or external calendar is not evidence that any route was scheduled or delivered. The browser key also lacks a member partition and is not removed by normal auth cleanup.

4. Create a durable reminder through the separate Nyx member authority#

The assistant nyx.set_reminder action reaches the Nyx adapter's setEventReminder. That adapter rebuilds the dynamic upcoming-event set, requires an id in it, and writes title, type, reminder time, cadence, channels, enabled state, and owner into the durable Nyx member store. The server requires and wires this store's snapshot persistence; export and erasure include it.

No rich-detail component calls this mutation. Fixed catalog ids generally do not pass its dynamic lookup. There is no bridge that turns the saved Nyx event-action record into this durable member reminder. A durable reminder may therefore be real while the detail control still has no path to create it.

5. Connect and export a provider from Profile#

At /profile?path=calendar, inspect the schedule preview and manage Google, Apple, or Outlook connections. Google and Outlook may complete OAuth when their configuration is present. Apple currently returns the explicit unconfigured 503 boundary. Established OAuth credentials are durable and secret-free in member views; pending OAuth state is only process memory.

/v1/calendar/export builds the member schedule and reports provider outcomes. Its Nyx items come only from enabled durable member reminders whose channels permit external-calendar, inside the 14-day horizon. The rich event-action store is never read. The generated Nyx canonical URL currently uses the missing /nyx/events/<dynamic-id> route, so even a successful provider write does not prove a valid detail deep link.

6. Distinguish optional in-app delivery from push navigation#

When the worker is enabled, produceNyxEventReminders sweeps durable enabled member reminders and creates stable scheduled entries. The bridge's recipient is empty and its channel is labelled push; the dependable path is the owner- keyed in-app inbox, while an external transport lacks a recipient. It also uses the missing /nyx/events/<dynamic-id> URL.

The service-worker E2E independently injects a payload with a valid fixed rich route. A cold notification tap opens it. With an existing app window, the service worker posts OSHUN_NOTIFICATION_TAP and focuses the window, but the app bootstrap does not consume that message. The normal Web Push dispatcher does not pass the optional action data. This test is a notification contract, not proof that the durable Nyx reminder produced a navigable push.

7. Record the observation through the matching surface#

Rich detail links to /domains/nyx/observation-log?eventId=<fixed-id>. Submitting creates local oshun.nyx state first, then attempts an authenticated BFF write; success marks the local row synchronized. The server record is owner scoped and durable. The displayed Linked event value is a tag rather than a return link, and a failed local-first row has no audited replay worker.

Agenda rows instead link to /nyx/observation?eventId=<dynamic-id>, a separate direct-post form. Neither observation surface is reached by the current real reminder producer. The global oshun.nyx browser key is not member partitioned or cleared on logout, so local evidence and server evidence must be reported separately.

Post-conditions#

  • A feed download can be identified by its dynamic event ids and @oshun.app UIDs; a rich-event download can be identified by one fixed id and @oshun-nyx UID.
  • A rich-event action preference can be read from the current browser and, for an authenticated member, from the current BFF process. It is not a scheduled reminder.
  • A durable member reminder can survive through the supported snapshot store, export, and erasure, but must be reported as created by the separate adapter path.
  • A provider export can report per-provider success or failure for eligible durable schedule items. It does not acknowledge importing a downloaded ICS file and is not invoked from rich detail.
  • An enabled worker can create an in-app delivery record; a real push deep link requires recipient resolution and action data not proven here.
  • An observation BFF row can be read back for its owner. A local queued row and a non-navigable event tag do not close the calendar/reminder journey.

Failure modes#

  • Wrong event authority — a fixed detail id is supplied to the dynamic reminder adapter, or a dynamic agenda id is expected to resolve in rich detail.
  • Mislabelled export expectation — the row-level agenda link downloads the entire feed rather than one VEVENT.
  • Action-store restart — the BFF preference mirror disappears; the global browser record remains and can cross account boundaries.
  • No durable bridge — changing a rich-detail cadence/channel never creates a member reminder.
  • Provider unavailable — credentials or a writer transport are absent; Apple is unconfigured by default, and Outlook has no default export transport.
  • Broken calendar/reminder link — provider and worker items use a route that does not exist and an id the rich catalog may not know.
  • Worker disabled or recipient absent — no autonomous tick runs, or external push cannot resolve a target even though in-app delivery succeeds.
  • Warm notification tap — the existing tab focuses without navigating because the application ignores the posted tap message.
  • Observation sync failure — local queued data remains without server read-back or replay; local state can also survive account change.

These are the twelve separations the result must keep visible: agenda versus rich record; feed versus single-event ICS; action label versus dispatched channel; process mirror versus durable reminder; shared label versus shared id; durable reminder versus detail control; provider export versus detail sync; injected push versus produced push; focus versus navigation; tag versus return link; browser storage versus subject partition; distributed cases versus one receipt.

E2E coverage#

Per-view files touched#

Cross-references#

Open questions#

  • Which dynamic/fixed event mapping is authoritative for reminders, calendar links, and observations?
  • Should the detail action state be migrated into the durable member reminder or removed in favor of it?
  • Should agenda rows expose separate Download this event and Subscribe to all events controls?
  • Which provider exports are production supported, and how will provider event ids and receipts be shown?
  • Should worker delivery carry a verified push recipient and structured actionPath, and should PwaBootstrap route warm taps?
  • How will local Nyx state be subject partitioned, cleared, replayed, and reconciled with owner-scoped server rows?
  • What single trace and restart test will define completion of this journey?