Skip to content

v0.0.2.1 feat: pause direct booking behind a flag - #81

Merged
jamescruisepalmer merged 6 commits into
mainfrom
jamespalmerrainforestqa/direct-booking-pause
Oct 5, 2026
Merged

jamescruisepalmer merged 6 commits into
mainfrom
jamespalmerrainforestqa/direct-booking-pause

Conversation

@jamescruisepalmer

Copy link
Copy Markdown
Contributor

Summary

Pauses direct booking: nobody is routed to the Calendly scheduling page anymore, including people who clear every qualifier. Onboarding-call volume outran the time we have to run the calls.

The pause

  • script.js gains one module-level kill switch, DIRECT_BOOKING_OPEN = false. The post-profile panel choice now reads routedToBooking (qualifies && DIRECT_BOOKING_OPEN) instead of qualifies, so every profile submitter lands on the "You're on the list." panel.
  • This is a capacity limit, not a fit one, so nothing about the qualifier was loosened or deleted. Fit is still scored on every submission, and both analytics events carry a new routed_to_booking alongside the existing qualified — so we can still see exactly who we would have booked.
  • Paused, not removed: the panel markup and its Calendly URL stay in welcome.html, commented as intentionally unreachable.

Containment fixes found during review

  • The gh-pages deploy rsynced the repo root with only a handful of exclusions, so tests/ was being served live. Verified: grandeldercare.com/tests/waitlist-phone.test.cjs returns HTTP 200, no X-Robots-Tag, and robots.txt is Allow: /. A test asserting on the booking URL would have published that link on a crawlable path, defeating the noindex on welcome.html that was the pause's only real containment. The deploy now excludes tests/, and the test file asserts on the Calendly host rather than the bookable event path.
  • Nothing ran the test suite — the only workflow was the Pages deploy — so the new guard against silently reopening booking was a guard that never fired. Adds .github/workflows/test.yml.

Copy

  • The profile intro promised to "match you with the right next step" when every submitter now lands on the same panel.

Before / After

A perfect-fit candidate (iPhone, loved one lives alone, no pets) submitting the profile form:

  • Before — "Fast track getting set up as a test user of Grand" + a "Schedule 20-minute call" Calendly button.
  • After — "You're on the list." + a "Back to home" link. The Calendly link is no longer reachable from the site.

Profile intro copy:

  • Before — "A few quick questions to help us match you with the right next step."
  • After — "A few quick questions so we know who you are and where you are."

Deploy surface:

  • Before — grandeldercare.com/tests/*.cjs served live, HTTP 200, crawlable.
  • After — tests/ excluded. A simulated rsync confirms welcome.html (which is noindex) is the only deployed file still carrying the booking URL.

No other visual change: both panels already existed and are unmodified.

Test Coverage

PAUSE DIRECT BOOKING — COVERAGE

script.js :: DIRECT_BOOKING_OPEN
├── const = false (shipped value)                         *** pinned by regex + behaviour
└── flag injected = true (documented rollback)            *** NEW

script.js :: qualifies (fit scoring, logic unchanged)
├── iphone + alone + no pets        -> true               ***
├── phone_type != iphone            -> false              ***
├── lives_alone != yes              -> false              ***
├── has_pets != no                  -> false              ***
└── {} / empty answers              -> false              ***

script.js :: routedToBooking = qualifies && FLAG
├── FLAG false, qualifies true      -> false  (the pause) ***
├── FLAG false, qualifies false     -> false              ***
├── FLAG true,  qualifies true      -> true   (rollback)  *** NEW
└── FLAG true,  qualifies false     -> false (no widening)*** NEW

script.js :: analytics
├── waitlist_profile_submit_success.qualified             *** NEW
├── waitlist_profile_submit_success.routed_to_booking     *** NEW
├── profile_completed.qualified                           *** NEW
├── profile_completed.routed_to_booking                   *** NEW
├── pre-existing props not displaced                      *** NEW regression guard
└── onboarding_call_clicked now unreachable               X   [->E2E] analytics-only

script.js :: panel selection
├── routedToBooking false -> waitlistedMessage            *** NEW (object identity)
├── routedToBooking true  -> doneMessage                  *** NEW
├── if (panel): hide layout, unhide, scrollIntoView       X   [->E2E] DOM side effects
└── else: setProfileStatus fallback                       X   [->E2E] not sliceable

welcome.html :: kept-but-unreachable markup
├── [data-profile-done] present AND hidden                *** NEW
├── [data-profile-waitlisted] present AND hidden          *** NEW
├── booking link contained inside hidden panel            *** NEW
├── exactly one booking link on the page                  *** NEW
└── no other page links Calendly                          **  NEW

USER FLOWS
├── qualifier submits -> "You're on the list."            *** + [->E2E] visual
├── non-qualifier submits -> same panel                   ***
├── booking panel never paints                            **  static hidden attr
├── rollback: flip flag -> fast track restored            ***
└── submit failure -> error status, no panel              .   unchanged by diff

COVERAGE: 82% (14/17 changed paths)
QUALITY:  14 *** . 1 ** . 0 * . 3 uncovered (all [->E2E] or DOM-only)
GAPS:     3 — panel-reveal DOM side effects; missing-panel fallback status;
          onboarding_call_clicked going to zero

Tests: 56 → 71 (+15). Coverage gate: PASS (82%, target 80%).

Mutation testing during the audit found the original tests would have passed with qualified deleted from the event, and with the flag gate replaced by a literal false (which silently kills the rollback). Both are now caught behaviourally.

Pre-Landing Review

Checklist passes clean (no SQL, no auth, no migrations, no LLM boundary in this diff). Adversarial review raised 11 findings; 5 were acted on in this PR, the rest are recorded below as judgment calls.

Acted on:

  • tests/ published to the live site with the booking URL — fixed (deploy exclusion + host-only assertion).
  • No CI ran the suite — fixed (test workflow).
  • README claimed reopening was "one flag flip with no other changes"; a guard test pins the flag, so it is deliberately two steps — corrected.
  • Test-count and date claims in README/CHANGELOG were stale — corrected.
  • Profile intro promised a next step that no longer exists — reworded.

Open judgment calls, not blockers:

  • The pause is a funnel change, not a capacity control. The Calendly event is untouched, so anyone holding the direct link can still book. If the goal is zero new bookings rather than zero new site referrals, the event needs pausing or capping in Calendly.
  • Dashboards keyed on qualified as a proxy for "was offered a call" now over-report. The two populations used to be identical and are now disjoint. routed_to_booking is the correct field for "offered"; existing PostHog insights should be checked.
  • website_onboarding_call_clicked will flatline to zero for the duration of the pause. Dormant, not broken — useful as a deploy canary.
  • Three required qualifier fields now gate nothing user-visible. Kept deliberately (fit scoring still feeds outreach), but it is a conversion cost on a paid-ad-driven funnel.
  • CHANGELOG.md is published to the live site and now names the flag. Pre-existing deploy behaviour, flagged rather than changed unilaterally.

Design Review

Design Review (lite): 0 findings. The only shipped markup changes are an HTML comment and one copy edit; both panels and all CSS are unmodified. Verified the hiding mechanism is sound — welcome.css:5 .profile-done[hidden]{display:none} at specificity (0,2,0) beats .profile-done at welcome.css:35, which sets no display.

Eval Results

No prompt-related files changed — evals skipped.

Plan Completion

No plan file detected.

Documentation

Doc diff preview

  • README.md: added a ## Tests section. This branch added .github/workflows/test.yml, but nothing in the README mentioned the suite existed. The new section covers the run command, what the 71 tests cover, why tests/direct-booking-pause.test.cjs is a guard rather than a feature test (reopening booking is a deliberate two-step: flip the flag, update the guard), and the Node 22 trap where node --test tests/ resolves the directory as a module entry point and fails before running anything — CI uses the glob for the same reason.
  • README.md: tests/ added to the documented deploy exclusions. The SEO & Indexing section listed only README.md and google-apps-script/. Now carries the reason: a .cjs file served at /tests/ cannot hold a noindex the way welcome.html does, so publishing it would route crawlers straight past that protection.
  • README.md: website_onboarding_call_clicked marked dormant, not broken. Its only trigger is the Calendly button on the panel DIRECT_BOOKING_OPEN now hides, so the event drops to zero for the duration of the pause and returns on its own. A flat line there is the pause working, not instrumentation coming loose — don't alert on it until booking reopens.
  • README.md + CHANGELOG.md: analytics names disambiguated. profile_completed and waitlist_profile_submit_success are call-site names. profile_completed reaches PostHog as website_profile_completed (searching the bare name returns nothing) and waitlist_profile_submit_success is sheet-side only. Anyone charting routed_to_booking would have hit this.
  • README.md: two superseded Status entries marked. The entries for the alpha-onboarding CTA and the fit qualifier still claimed qualifiers see the scheduling panel and that "Back to home" was removed. Both are now contradicted by the pause. Resolved with supersession pointers rather than rewrites, matching how this section already handles history.
  • README.md: workflow_dispatch documented — the shipped workflow has a manual trigger the first draft of the Tests section omitted.

CHANGELOG entries were preserved verbatim apart from one additive clarifying parenthetical; nothing was deleted, reordered, or regenerated. VERSION (0.0.2.1) was already bumped and its entry covers every commit on the branch, so it was left as-is.

Documentation Debt

Both items are pre-existing, predate this branch, and were not touched by it — flagged rather than fixed to keep this PR focused:

  • ⚠️ README:289 — stale timeout. Claims the browser stops waiting after 10 seconds; script.js:721 aborts at 45000ms.
  • ⚠️ README:145 and README:336 — stale profile-page description. Still describe the profile questions as optional and including "alpha-tester interest". They became required qualifiers (full_name, zipcode, phone_type, lives_alone, has_pets) in Qualify Grand signups before showing the scheduling call link #55, and no alpha-tester field is collected. README:336 is in the present-tense ## Current Sections section, so it reads as a live claim.

Backend

No Apps Script change ships with this. routed_to_booking rides inside the existing raw_payload JSON column exactly as qualified already does, so there is no sheet migration and no redeploy dependency. Sheet-side it needs a JSON extract to chart; PostHog gets it as a first-class property.

Test plan

  • All 71 tests pass (node --test tests/*.test.cjs), 0 failures
  • End-to-end browser check: submitted the profile form as an ideal candidate with fetch stubbed (no real waitlist row created) — booking panel stayed hidden, waitlist panel shown, no console errors
  • Deploy verified by simulating the workflow's rsync: tests/ excluded, welcome.html the only deployed file carrying the booking URL
  • Confirmed live /tests/ exposure before the fix (HTTP 200, no X-Robots-Tag)

🤖 Generated with Claude Code

jamescruisepalmer and others added 6 commits October 5, 2026 09:51
Onboarding-call volume outran the time we have to run the calls, so no
profile submitter is routed to the Calendly scheduling panel anymore --
not even one who clears all three qualifiers.

This is a capacity limit, not a fit one, so nothing about the qualifier
was loosened or deleted: `qualifies` is still scored on every submission
and the panel markup stays in welcome.html. The routing now reads
`routedToBooking` (`qualifies && DIRECT_BOOKING_OPEN`), so reopening is
one flag flip back to `true` with no other edits.

Both analytics events carry `routed_to_booking` alongside the existing
`qualified`, so the people we would have booked stay visible while the
pause is on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Executes the real post-submit block with the flag injected, so the same
source runs both paused and reopened. Covers the pause today, the
one-flag-flip rollback, both analytics events (so `qualified` cannot be
quietly dropped), and the booking panel staying present-but-hidden.

Also guards the two ways the pause could be silently undone: flipping
the flag, or re-pointing the panel choice back at `qualifies`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gh-pages deploy rsynced the repo root with only a handful of
exclusions, so tests/ was being served at grandeldercare.com/tests/ --
HTTP 200, no X-Robots-Tag, and robots.txt says Allow: /. A test that
asserted on the booking URL would therefore have published that link on
a crawlable path, defeating the noindex on welcome.html that was the
pause's only real containment.

Excludes tests/ from the deploy, and drops the bookable event path from
the test file in favour of the Calendly host, so the file stays harmless
even if that exclusion is ever reverted.

Also adds a test workflow. The only workflow before this was the Pages
deploy, so nothing ran the suite -- which made the new guard against
silently reopening booking a guard that never fired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"A few quick questions to help us match you with the right next step"
was written when the qualifier routed people to two different places.
Every submitter now lands on the same panel, so the promise no longer
matches what happens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The direct-booking pause changed behavior that several README sections still
described as current, so the docs contradicted the code in ways a reader would
have acted on.

- Add a `## Tests` section. The branch added CI for a suite the README never
  mentioned, so there was no way to learn the suite existed, how to run it, or
  that `node --test tests/` fails on Node 22 and needs the glob.
- Add `tests/` to the documented deploy exclusions, with the reason: a `.cjs`
  file served at `/tests/` cannot carry a `noindex`, so publishing it would
  route crawlers past the protection on `welcome.html`.
- Note that `website_onboarding_call_clicked` is dormant, not broken. Its only
  trigger is the Calendly button on the now-unreachable panel, so it goes to
  zero for the duration of the pause.
- Disambiguate the analytics names in the Status bullet and the 0.0.2.1
  changelog entry. `profile_completed` reaches PostHog as
  `website_profile_completed`; searching the bare name finds nothing.
- Mark the two superseded Status entries that still claimed qualifiers see the
  scheduling panel and that "Back to home" was removed. Supersession pointers
  rather than rewrites, matching how this section already handles history.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamescruisepalmer
jamescruisepalmer merged commit b1e303c into main Oct 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant