Change evidence report

Response-owned public schedule explorer

PR 2003 replaces two diverged public widget controllers with one shared schedule explorer. The controller distinguishes the requested week and filter context from the last settled response, so BookingCalendar cannot evaluate stale sessions during a week transition. Exhausted walks now report the actual localized final searched date, including filter-aware copy, while API errors remain separate. Schedule and BookingCalendar retain independent render trees, public props, analytics names, custom-element tags, and LocationMap integration.

Range
c5ebdd11a (current merge-base with main; packages/widgets unchanged from captured base e57f97835)…93ea6fee4 (current implementation commit; packages/widgets byte-equivalent to captured implementation 3d9d99177)
Source state
fix/1948-schedule-controller rebased cleanly onto c5ebdd11a; implementation commit 93ea6fee4 is followed by the PR evidence commits
Generated

Reviewer brief

Outcome and scope

Week walking now advances only after the requested response settles

Both widgets consume one request owner that keys tenant, week, server filters, and reload generation. Search state separately owns filter and Group or Private category context. Exhaustion renders only when its request key is the settled visible response, and its through date is the final bucket Sunday. Schedule now uses the same WidgetAccountMenu as BookingCalendar without merging their visual presentations.

What this evidence proves

The screenshots and transition observations use real Storybook widget render trees with sanitized controlled schedule responses. The baseline and proposed exhausted captures use a 1024 by 768 viewport, the same calendar date, and eight empty future week responses. The pending-response scenario deliberately held the 2026-08-31 request, observed no prior-week session and no premature exhaustion, then settled it empty and observed the controller request 2026-09-07 and land on that response. Tests establish checkout and integration contracts separately. No production data was used.

Claim map

Evidence coverage

Each material changed behavior or state maps to direct evidence or an explicit gap.

Behavior or claimEvidenceStatus
BookingCalendar never evaluates sessions owned by a different weekHeld-response browser transition, focused stale-frame regression, and request-key controller excerptCovered
Exhausted copy uses the actual final localized searched dateMatched baseline and proposed Schedule captures, eight observed future bucket requests, and focused tests in both widgetsCovered
Filter and category changes cannot relabel stale exhaustionFilter-context request model, filter-aware browser capture, and focused filter, clear-filter, and Group or Private category transition testsCovered
API failures remain distinct from empty and exhausted statesLive 503 Storybook scenario and focused Schedule failure assertionCovered
Both widgets retain their public and embedded contractsLocationMap and custom-element registration tests plus complete caller reviewCovered
Schedule and BookingCalendar use the shared account menu behaviorShared WidgetAccountMenu call sites, existing header behavior tests, and guest headers visible in the proposed capturesCovered

Observed change

Review evidence

Evidence is grouped by the reviewer question each item addresses.

Schedule exhausted state

Screenshot

An unbounded claim becomes the actual settled final date

Both surfaces walked the same eight future week buckets from 2026-08-31 through 2026-10-19. The merge-base said only that no upcoming sessions exist. The proposed widget names the final searched bucket Sunday, October 25.

Current merge-base c5ebdd11a (widget tree unchanged from captured base): `No upcoming sessions found.` after eight empty future buckets.Before
Public schedule widget showing the unbounded message No upcoming sessions found
Current implementation 93ea6fee4: `No sessions found through Sun, Oct 25.` for the same eight responses.After
Public schedule widget showing No sessions found through Sun, Oct 25

Filtered exhausted state

Screenshot

A filtered walk describes only matching sessions

Selecting the Sauna session-type filter first rendered the ordinary filter empty state. After the bounded walk settled, the copy retained filter context and named the same response-owned final date.

Current implementation 93ea6fee4: `No sessions matching your filters were found through Sun, Oct 25.`After
Public schedule widget with one active filter showing a filter-aware searched-through date

BookingCalendar response ownership

Screenshot

The walker continues only after the requested week settles

The browser loaded a prior-day session in week 2026-08-24, held the 2026-08-31 response, and observed neither the prior session nor premature exhaustion. Settling that bucket empty triggered 2026-09-07, whose session became the visible result shown here.

Current implementation 93ea6fee4 after the owned response sequence 2026-08-24, 2026-08-31, and 2026-09-07.After
Booking calendar on September 7 showing the Next Settled Session returned by that week's owned response

Controller ownership boundary

Code

Exhaustion must match both filter context and settled request identity

The request key includes tenant, week, every server filter, and reload generation. A separate context key adds the selected Group or Private category. Exhausted presentation requires matching context, matching request, the settled response owner, and no active load.

Shared controller at current implementation commit 93ea6fee4.After
packages/widgets/src/components/schedule-explorer-controller.ts:199-276
const requestKey = useMemo(
  () => JSON.stringify([
    apiUrl, brandId, weekKey, filterSessionType, filterClassId,
    filterInstructorId, filterLocationId, scheduleReloadToken,
  ]),
  [apiUrl, brandId, weekKey, filterSessionType, filterClassId,
   filterInstructorId, filterLocationId, scheduleReloadToken],
);
const filterContextKey = useMemo(
  () => JSON.stringify([
    apiUrl, brandId, filterSessionType, filterClassId,
    filterInstructorId, filterLocationId, chosenCategory,
  ]),
  [apiUrl, brandId, filterSessionType, filterClassId,
   filterInstructorId, filterLocationId, chosenCategory],
);
const responseOwnsRequest = loadedRequestKey === requestKey;
const nextSearch: NextAvailableState =
  search.status === "searching" && search.contextKey === filterContextKey
    ? "searching"
    : search.status === "exhausted" &&
        search.contextKey === filterContextKey &&
        search.requestKey === requestKey &&
        responseOwnsRequest &&
        !loading
      ? "exhausted"
      : "idle";

Observed state transitions

Table

Each asynchronous state has one truthful presentation

Live browser scenarios used the same public Schedule and BookingCalendar story surfaces exercised by customers.

ScenarioObserved response sequenceVisible result
Unfiltered exhausted walkEight settled future week bucketsNo sessions found through Sun, Oct 25.
Filtered exhausted walkFilter request plus eight settled future bucketsNo sessions matching your filters were found through Sun, Oct 25.
Unsettled BookingCalendar transition2026-08-31 held, then empty; 2026-09-07 returned a sessionNo stale prior-week session or premature exhaustion; landed on Next Settled Session
Schedule API failure503 responseRetry error only; no empty or searched-through claim

Confidence

Validation

Pass

Focused schedule and booking behavior

121 BookingCalendar and Schedule tests passed with 542 assertions, including stale request ownership, exact final dates, filter and category invalidation, failure separation, boundary refresh, and checkout recovery behavior

Pass

Public integration contracts

4 LocationMap and custom-element registration tests passed with 22 assertions

Pass

Widget type and production build

The @sauna-crm/widgets typecheck exited 0 and the production Vite build completed successfully

Pass

Focused style checks

Biome checked all changed controller, widget, auth-type, and focused test files without diagnostics; git diff --check passed

Pass

Real browser state transitions

Storybook exercised unfiltered and filtered exhaustion, a held BookingCalendar week transition, continued walking after an empty settled response, shared guest headers, and a 503 failure boundary

Pass

Independent exact-head review

The final reviewer reported no findings after checking request and filter identity, walker settlement, boundary refresh, checkout recovery guards, account-menu parity, public contracts, and focused regression coverage

Limits

Known issues, risks, and gaps

The shared owner removes divergence but remains a broad asynchronous boundary

Checkout and recovery ordering moved with the controller The extraction preserves the existing guards and tests, but it centralizes a broad state machine. Existing recovery suites remain the primary protection for redirect, magic-link, waiver, and return-to-list ordering.

Localized horizon omits the year by existing convention The through date uses the established formatSessionDate contract: weekday, abbreviated month, and day. Very long searches crossing a year are currently impossible under the eight-bucket cap.

Captures use controlled schedule responses Controlled responses make asynchronous ownership reproducible without customer data. The images prove widget rendering; tests and code review establish broader recovery and public-contract behavior.