V1 Web PWA · Walk result

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

A dated evidence record for V1 Web PWA: observed behavior, current source reconciliation, automation evidence, and explicit proof limits.

verdict: partial· 2026-05-29
8sections14 minread1table

On this page
  • 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 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.

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#

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?