@tummycrypt/acuity-middleware 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/adapters/acuity-scraper.d.ts +8 -0
  2. package/dist/adapters/acuity-scraper.d.ts.map +1 -0
  3. package/dist/adapters/acuity-scraper.js +8 -0
  4. package/dist/adapters/acuity-scraper.js.map +1 -0
  5. package/dist/adapters/types.d.ts +8 -0
  6. package/dist/adapters/types.d.ts.map +1 -0
  7. package/dist/adapters/types.js +8 -0
  8. package/dist/adapters/types.js.map +1 -0
  9. package/dist/core/types.d.ts +10 -0
  10. package/dist/core/types.d.ts.map +1 -0
  11. package/dist/core/types.js +2 -0
  12. package/dist/core/types.js.map +1 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +18 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/acuity-wizard.d.ts +49 -0
  18. package/dist/middleware/acuity-wizard.d.ts.map +1 -0
  19. package/dist/middleware/acuity-wizard.js +265 -0
  20. package/dist/middleware/acuity-wizard.js.map +1 -0
  21. package/dist/middleware/browser-service.d.ts +53 -0
  22. package/dist/middleware/browser-service.d.ts.map +1 -0
  23. package/dist/middleware/browser-service.js +105 -0
  24. package/dist/middleware/browser-service.js.map +1 -0
  25. package/dist/middleware/errors.d.ts +58 -0
  26. package/dist/middleware/errors.d.ts.map +1 -0
  27. package/dist/middleware/errors.js +43 -0
  28. package/dist/middleware/errors.js.map +1 -0
  29. package/{src/middleware/index.ts → dist/middleware/index.d.ts} +5 -52
  30. package/dist/middleware/index.d.ts.map +1 -0
  31. package/dist/middleware/index.js +38 -0
  32. package/dist/middleware/index.js.map +1 -0
  33. package/dist/middleware/logger.d.ts +26 -0
  34. package/dist/middleware/logger.d.ts.map +1 -0
  35. package/dist/middleware/logger.js +65 -0
  36. package/dist/middleware/logger.js.map +1 -0
  37. package/dist/middleware/remote-adapter.d.ts +45 -0
  38. package/dist/middleware/remote-adapter.d.ts.map +1 -0
  39. package/dist/middleware/remote-adapter.js +178 -0
  40. package/dist/middleware/remote-adapter.js.map +1 -0
  41. package/dist/middleware/selector-health.d.ts +44 -0
  42. package/dist/middleware/selector-health.d.ts.map +1 -0
  43. package/dist/middleware/selector-health.js +144 -0
  44. package/dist/middleware/selector-health.js.map +1 -0
  45. package/dist/middleware/selectors.d.ts +108 -0
  46. package/dist/middleware/selectors.d.ts.map +1 -0
  47. package/dist/middleware/selectors.js +249 -0
  48. package/dist/middleware/selectors.js.map +1 -0
  49. package/dist/middleware/server.d.ts +34 -0
  50. package/dist/middleware/server.d.ts.map +1 -0
  51. package/dist/middleware/server.js +377 -0
  52. package/dist/middleware/server.js.map +1 -0
  53. package/dist/middleware/service-resolver.d.ts +46 -0
  54. package/dist/middleware/service-resolver.d.ts.map +1 -0
  55. package/dist/middleware/service-resolver.js +274 -0
  56. package/dist/middleware/service-resolver.js.map +1 -0
  57. package/dist/middleware/slot-parser.d.ts +29 -0
  58. package/dist/middleware/slot-parser.d.ts.map +1 -0
  59. package/dist/middleware/slot-parser.js +50 -0
  60. package/dist/middleware/slot-parser.js.map +1 -0
  61. package/dist/middleware/steps/__tests__/fixtures.d.ts +14 -0
  62. package/dist/middleware/steps/__tests__/fixtures.d.ts.map +1 -0
  63. package/dist/middleware/steps/__tests__/fixtures.js +204 -0
  64. package/dist/middleware/steps/__tests__/fixtures.js.map +1 -0
  65. package/dist/middleware/steps/bypass-payment.d.ts +54 -0
  66. package/dist/middleware/steps/bypass-payment.d.ts.map +1 -0
  67. package/dist/middleware/steps/bypass-payment.js +164 -0
  68. package/dist/middleware/steps/bypass-payment.js.map +1 -0
  69. package/dist/middleware/steps/extract-business.d.ts +93 -0
  70. package/dist/middleware/steps/extract-business.d.ts.map +1 -0
  71. package/dist/middleware/steps/extract-business.js +170 -0
  72. package/dist/middleware/steps/extract-business.js.map +1 -0
  73. package/dist/middleware/steps/extract.d.ts +41 -0
  74. package/dist/middleware/steps/extract.d.ts.map +1 -0
  75. package/dist/middleware/steps/extract.js +128 -0
  76. package/dist/middleware/steps/extract.js.map +1 -0
  77. package/dist/middleware/steps/fill-form.d.ts +45 -0
  78. package/dist/middleware/steps/fill-form.d.ts.map +1 -0
  79. package/dist/middleware/steps/fill-form.js +262 -0
  80. package/dist/middleware/steps/fill-form.js.map +1 -0
  81. package/dist/middleware/steps/index.d.ts +12 -0
  82. package/dist/middleware/steps/index.d.ts.map +1 -0
  83. package/dist/middleware/steps/index.js +12 -0
  84. package/dist/middleware/steps/index.js.map +1 -0
  85. package/dist/middleware/steps/navigate.d.ts +51 -0
  86. package/dist/middleware/steps/navigate.d.ts.map +1 -0
  87. package/dist/middleware/steps/navigate.js +391 -0
  88. package/dist/middleware/steps/navigate.js.map +1 -0
  89. package/dist/middleware/steps/read-availability.d.ts +37 -0
  90. package/dist/middleware/steps/read-availability.d.ts.map +1 -0
  91. package/dist/middleware/steps/read-availability.js +298 -0
  92. package/dist/middleware/steps/read-availability.js.map +1 -0
  93. package/dist/middleware/steps/read-slots.d.ts +33 -0
  94. package/dist/middleware/steps/read-slots.d.ts.map +1 -0
  95. package/dist/middleware/steps/read-slots.js +295 -0
  96. package/dist/middleware/steps/read-slots.js.map +1 -0
  97. package/dist/middleware/steps/read-via-url.d.ts +39 -0
  98. package/dist/middleware/steps/read-via-url.d.ts.map +1 -0
  99. package/dist/middleware/steps/read-via-url.js +141 -0
  100. package/dist/middleware/steps/read-via-url.js.map +1 -0
  101. package/dist/middleware/steps/submit.d.ts +22 -0
  102. package/dist/middleware/steps/submit.d.ts.map +1 -0
  103. package/dist/middleware/steps/submit.js +112 -0
  104. package/dist/middleware/steps/submit.js.map +1 -0
  105. package/dist/middleware/wizard-calendar.d.ts +37 -0
  106. package/dist/middleware/wizard-calendar.d.ts.map +1 -0
  107. package/dist/middleware/wizard-calendar.js +177 -0
  108. package/dist/middleware/wizard-calendar.js.map +1 -0
  109. package/dist/middleware/wizard-service.d.ts +30 -0
  110. package/dist/middleware/wizard-service.d.ts.map +1 -0
  111. package/dist/middleware/wizard-service.js +89 -0
  112. package/dist/middleware/wizard-service.js.map +1 -0
  113. package/dist/server.d.ts +6 -0
  114. package/dist/server.d.ts.map +1 -0
  115. package/{src/server.ts → dist/server.js} +1 -0
  116. package/dist/server.js.map +1 -0
  117. package/package.json +16 -4
  118. package/.github/workflows/build-paper.yml +0 -39
  119. package/.github/workflows/ci.yml +0 -37
  120. package/Dockerfile +0 -53
  121. package/docs/blog-post.mdx +0 -240
  122. package/docs/paper/IEEEtran.bst +0 -2409
  123. package/docs/paper/IEEEtran.cls +0 -6347
  124. package/docs/paper/acuity-middleware-paper.tex +0 -375
  125. package/docs/paper/balance.sty +0 -87
  126. package/docs/paper/references.bib +0 -231
  127. package/docs/paper.md +0 -400
  128. package/flake.nix +0 -32
  129. package/modal-app.py +0 -82
  130. package/src/adapters/acuity-scraper.ts +0 -543
  131. package/src/adapters/types.ts +0 -193
  132. package/src/core/types.ts +0 -325
  133. package/src/index.ts +0 -75
  134. package/src/middleware/acuity-wizard.ts +0 -456
  135. package/src/middleware/browser-service.ts +0 -183
  136. package/src/middleware/errors.ts +0 -70
  137. package/src/middleware/remote-adapter.ts +0 -246
  138. package/src/middleware/selectors.ts +0 -308
  139. package/src/middleware/server.ts +0 -372
  140. package/src/middleware/steps/bypass-payment.ts +0 -226
  141. package/src/middleware/steps/extract.ts +0 -174
  142. package/src/middleware/steps/fill-form.ts +0 -359
  143. package/src/middleware/steps/index.ts +0 -27
  144. package/src/middleware/steps/navigate.ts +0 -537
  145. package/src/middleware/steps/read-availability.ts +0 -399
  146. package/src/middleware/steps/read-slots.ts +0 -405
  147. package/src/middleware/steps/submit.ts +0 -168
  148. package/tsconfig.json +0 -25
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Selector Health Check
3
+ *
4
+ * Tiered probing of Acuity page selectors with degradation detection.
5
+ * "Degraded" means the primary selector (index 0) failed but a fallback
6
+ * (index > 0) still works — an early warning that Acuity changed their DOM.
7
+ *
8
+ * Tiers:
9
+ * depth=0: HTTP-only BUSINESS object check (~200ms, no browser)
10
+ * depth=1: Service page selectors (~3-5s, browser required)
11
+ * depth=2: + Calendar page selectors (~8-15s, clicks through wizard)
12
+ */
13
+ import { Effect } from 'effect';
14
+ import { BrowserService } from './browser-service.js';
15
+ import { Selectors } from './selectors.js';
16
+ import { fetchBusinessData } from './steps/extract-business.js';
17
+ // =============================================================================
18
+ // SELECTOR TIERS
19
+ // =============================================================================
20
+ /** Selectors available on the service selection page (no navigation needed) */
21
+ const SERVICE_PAGE_KEYS = [
22
+ 'serviceList', 'serviceName', 'serviceBookButton',
23
+ 'serviceCategory', 'servicePrice', 'serviceDuration',
24
+ ];
25
+ /** Selectors available on the calendar page (requires clicking Book) */
26
+ const CALENDAR_PAGE_KEYS = [
27
+ 'calendar', 'calendarMonth', 'calendarDay',
28
+ 'calendarPrev', 'calendarNext',
29
+ ];
30
+ // =============================================================================
31
+ // PROBE WITH DEGRADATION DETECTION
32
+ // =============================================================================
33
+ /**
34
+ * Probe a single selector key, trying ALL candidates in order.
35
+ * Detects degradation when primary (index 0) fails but a fallback works.
36
+ */
37
+ const probeSelectorWithDegradation = (page, key) => Effect.gen(function* () {
38
+ const start = Date.now();
39
+ const candidates = Selectors[key];
40
+ for (let i = 0; i < candidates.length; i++) {
41
+ const exists = yield* Effect.tryPromise({
42
+ try: () => page.$(candidates[i]).then((el) => el !== null),
43
+ catch: () => false,
44
+ }).pipe(Effect.orElseSucceed(() => false));
45
+ if (exists) {
46
+ return {
47
+ key,
48
+ status: i === 0 ? 'passed' : 'degraded',
49
+ matchedSelector: candidates[i],
50
+ matchedIndex: i,
51
+ probeMs: Date.now() - start,
52
+ };
53
+ }
54
+ }
55
+ return {
56
+ key,
57
+ status: 'failed',
58
+ matchedSelector: null,
59
+ matchedIndex: null,
60
+ probeMs: Date.now() - start,
61
+ };
62
+ });
63
+ // =============================================================================
64
+ // REPORT BUILDER
65
+ // =============================================================================
66
+ const buildReport = (selectors, pagesProbed, businessObjectAvailable, startMs) => {
67
+ const passed = selectors.filter((s) => s.status === 'passed').length;
68
+ const degraded = selectors.filter((s) => s.status === 'degraded').length;
69
+ const failed = selectors.filter((s) => s.status === 'failed').length;
70
+ const status = failed > 0 ? 'unhealthy' : degraded > 0 ? 'degraded' : 'healthy';
71
+ return {
72
+ status,
73
+ selectors,
74
+ passed,
75
+ degraded,
76
+ failed,
77
+ totalMs: Date.now() - startMs,
78
+ pagesProbed,
79
+ businessObjectAvailable,
80
+ timestamp: new Date().toISOString(),
81
+ };
82
+ };
83
+ // =============================================================================
84
+ // TIERED HEALTH CHECK
85
+ // =============================================================================
86
+ /**
87
+ * Run a selector health check at the specified depth.
88
+ *
89
+ * @param baseUrl - Acuity scheduling URL
90
+ * @param depth - 0 = HTTP-only, 1 = service page, 2 = service + calendar
91
+ */
92
+ export const selectorHealthCheck = (baseUrl, depth = 0) => Effect.gen(function* () {
93
+ const start = Date.now();
94
+ const results = [];
95
+ const pagesProbed = [];
96
+ // Tier 0: BUSINESS object check (HTTP-only, no browser)
97
+ const businessAvailable = yield* Effect.tryPromise({
98
+ try: () => fetchBusinessData(baseUrl).then((b) => b !== null),
99
+ catch: () => false,
100
+ }).pipe(Effect.orElseSucceed(() => false));
101
+ if (depth === 0) {
102
+ return buildReport(results, pagesProbed, businessAvailable, start);
103
+ }
104
+ // Tier 1: Service page selectors
105
+ const { acquirePage, config } = yield* BrowserService;
106
+ const page = yield* acquirePage.pipe(Effect.orDie);
107
+ yield* Effect.tryPromise({
108
+ try: () => page.goto(config.baseUrl, { waitUntil: 'networkidle', timeout: config.timeout }),
109
+ catch: () => null,
110
+ }).pipe(Effect.ignore);
111
+ for (const key of SERVICE_PAGE_KEYS) {
112
+ results.push(yield* probeSelectorWithDegradation(page, key));
113
+ }
114
+ pagesProbed.push('service');
115
+ if (depth < 2) {
116
+ return buildReport(results, pagesProbed, businessAvailable, start);
117
+ }
118
+ // Tier 2: Calendar page (click first service's Book)
119
+ const navigated = yield* Effect.tryPromise({
120
+ try: async () => {
121
+ const btn = await page.$(Selectors.serviceBookButton[0]);
122
+ if (btn) {
123
+ await btn.click();
124
+ await page.waitForURL(/\/calendar\//, { timeout: 10000 });
125
+ return true;
126
+ }
127
+ return false;
128
+ },
129
+ catch: () => false,
130
+ }).pipe(Effect.orElseSucceed(() => false));
131
+ if (navigated) {
132
+ // Wait for calendar to render
133
+ yield* Effect.tryPromise({
134
+ try: () => page.waitForSelector(Selectors.calendar[0], { timeout: 10000 }),
135
+ catch: () => null,
136
+ }).pipe(Effect.ignore);
137
+ for (const key of CALENDAR_PAGE_KEYS) {
138
+ results.push(yield* probeSelectorWithDegradation(page, key));
139
+ }
140
+ pagesProbed.push('calendar');
141
+ }
142
+ return buildReport(results, pagesProbed, businessAvailable, start);
143
+ });
144
+ //# sourceMappingURL=selector-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selector-health.js","sourceRoot":"","sources":["../../src/middleware/selector-health.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Acuity CSS Selector Registry
3
+ *
4
+ * Single source of truth for all CSS selectors used by the wizard middleware.
5
+ * Each selector has a primary pattern and fallback chain.
6
+ * When Acuity changes their DOM, fix this ONE file.
7
+ */
8
+ import { Effect } from 'effect';
9
+ import type { Page, ElementHandle } from 'playwright-core';
10
+ import { SelectorError } from './errors.js';
11
+ /**
12
+ * Acuity Scheduling (2026 React SPA) CSS Selector Registry
13
+ *
14
+ * Verified against live DOM: 2026-02-25
15
+ * Uses Emotion CSS-in-JS (css-* hashes are UNSTABLE — prefer semantic classes)
16
+ *
17
+ * Wizard flow:
18
+ * 1. Service page: massageithaca.as.me → <li.select-item> list with "Book" buttons
19
+ * 2. Calendar page: /schedule/<hash>/appointment/<aptId>/calendar/<calId>
20
+ * - react-calendar month grid + available-times-container
21
+ * 3. Client form: (after selecting time slot) → input fields
22
+ * 4. Payment/coupon: certificate input → Apply → verify $0
23
+ * 5. Submit → confirmation
24
+ *
25
+ * URL pattern (no query params):
26
+ * /schedule/<hash>/appointment/<appointmentTypeId>/calendar/<calendarId>
27
+ */
28
+ export declare const Selectors: {
29
+ readonly serviceList: readonly [".select-item", ".select-item-box", ".appointment-type-item"];
30
+ readonly serviceName: readonly [".appointment-type-name", ".type-name", "h3"];
31
+ readonly serviceLink: readonly [".select-item", ".select-item-box"];
32
+ readonly servicePrice: readonly [".duration-container", ".duration-container span", ".price", ".cost"];
33
+ readonly serviceDuration: readonly [".duration-container", ".duration-container span", ".duration", ".time-duration"];
34
+ readonly serviceDescription: readonly [".type-description", ".description", "p.type-description"];
35
+ readonly serviceBookButton: readonly ["button.btn", ".select-item button.btn"];
36
+ readonly serviceCategory: readonly [".select-label", ".select-label p", ".select-type .select-label"];
37
+ readonly calendar: readonly [".monthly-calendar-v2", ".react-calendar", ".monthly-calendar-react-calendar"];
38
+ readonly calendarMonth: readonly [".react-calendar__navigation__label", ".react-calendar__navigation__label__labelText"];
39
+ readonly calendarPrev: readonly [".react-calendar__navigation__prev-button"];
40
+ readonly calendarNext: readonly [".react-calendar__navigation__next-button"];
41
+ readonly calendarDay: readonly [".react-calendar__tile", ".react-calendar__month-view__days__day", "button.react-calendar__tile"];
42
+ readonly activeDay: readonly [".react-calendar__tile--active", ".activeday", ".react-calendar__tile:not(:disabled)"];
43
+ readonly timeSlotContainer: readonly [".available-times-container"];
44
+ readonly timeSlot: readonly ["button.time-selection", ".time-selection", ".time-slot", "[data-time]"];
45
+ readonly timeSlotSelected: readonly ["button.time-selection.selected-time", ".selected-time"];
46
+ readonly selectAndContinue: readonly ["li[role=\"menuitem\"]", "[data-keyboard-navigable=\"keyboard-navigable-list-item\"]", "text=Select and continue"];
47
+ readonly firstNameInput: readonly ["input[name=\"client.firstName\"]", "#client\\.firstName", "input[name=\"firstName\"]"];
48
+ readonly lastNameInput: readonly ["input[name=\"client.lastName\"]", "#client\\.lastName", "input[name=\"lastName\"]"];
49
+ readonly emailInput: readonly ["input[name=\"client.email\"]", "#client\\.email", "input[name=\"email\"]"];
50
+ readonly phoneInput: readonly ["input[name=\"client.phone\"]", "#client\\.phone", "input[name=\"phone\"]"];
51
+ readonly continueToPayment: readonly ["button.btn:has-text(\"Continue to Payment\")", "button:has-text(\"Continue to Payment\")", "button.btn[type=\"submit\"]"];
52
+ readonly checkCodeBalance: readonly ["button:has-text(\"Check Code Balance\")", "button.css-9zfkvr"];
53
+ readonly termsCheckbox: readonly ["input[type=\"checkbox\"][name*=\"field-13933959\"]", "input[id*=\"13933959\"]"];
54
+ readonly radioNoLabel: readonly ["label:has(input[type=\"radio\"][value=\"no\"])"];
55
+ readonly radioYesLabel: readonly ["label:has(input[type=\"radio\"][value=\"yes\"])"];
56
+ readonly howDidYouHearCheckbox: readonly ["input[type=\"checkbox\"][name=\"Internet search\"]", "label:has(input[type=\"checkbox\"][name=\"Internet search\"])"];
57
+ readonly medicationField: readonly ["textarea[name=\"fields[field-16606770]\"]", "#fields\\[field-16606770\\]"];
58
+ readonly couponField: readonly ["#code", "input#code", "input[id=\"code\"]"];
59
+ readonly couponTabByCode: readonly ["button:has-text(\"Check by code\")", "button.css-1jjp8vb:has-text(\"Check by code\")"];
60
+ readonly couponConfirmButton: readonly ["[role=\"dialog\"] button:has-text(\"Confirm\")", "button.css-qgmcoe", "button:has-text(\"Confirm\")"];
61
+ readonly couponCloseButton: readonly ["button:has-text(\"Close\")", "button.css-ve50y1"];
62
+ readonly couponError: readonly ["[role=\"dialog\"] p:has-text(\"weren't able to recognize\")", "[role=\"dialog\"] p:has-text(\"try entering it again\")", "p.css-7bwtx1"];
63
+ readonly couponSuccess: readonly ["[role=\"dialog\"] p:has-text(\"balance\")", "[role=\"dialog\"] [class*=\"success\"]", ".coupon-applied", ".certificate-success"];
64
+ readonly paymentCouponToggle: readonly ["button:has-text(\"Package, gift, or coupon code\")", "text=Package, gift, or coupon code"];
65
+ readonly paymentCouponInput: readonly ["input[placeholder=\"Enter code\"]", "input[placeholder*=\"code\" i]", "input[name*=\"coupon\"]"];
66
+ readonly paymentCouponApply: readonly ["button:has-text(\"Apply\")", "button:has-text(\"Redeem\")"];
67
+ readonly paymentCouponRemove: readonly ["text=REMOVE", "a:has-text(\"REMOVE\")", "button:has-text(\"REMOVE\")"];
68
+ readonly paymentTotal: readonly [".order-total", ".payment-total", ".total-amount", "text=$0.00"];
69
+ readonly paymentSubtotal: readonly ["text=Subtotal"];
70
+ readonly payAndConfirm: readonly ["button:has-text(\"Pay & Confirm\")", "button:has-text(\"PAY & CONFIRM\")", "button:has-text(\"Confirm Appointment\")"];
71
+ readonly submitButton: readonly ["button:has-text(\"Pay & Confirm\")", "button:has-text(\"PAY & CONFIRM\")", "button[type=\"submit\"].confirm", ".complete-booking", "#submit-booking", "button:has-text(\"Complete Appointment\")", "button:has-text(\"Book Now\")", "button:has-text(\"Schedule\")"];
72
+ readonly confirmationPage: readonly [".confirmation", ".booking-confirmed", ".thank-you", "#confirmation"];
73
+ readonly confirmationId: readonly [".confirmation-number", ".appointment-id", "[data-confirmation]"];
74
+ readonly confirmationService: readonly [".appointment-type", ".service-name", ".booked-service"];
75
+ readonly confirmationDatetime: readonly [".appointment-datetime", ".booked-time", ".booking-date"];
76
+ };
77
+ export type SelectorKey = keyof typeof Selectors;
78
+ export interface ResolvedSelector {
79
+ readonly selector: string;
80
+ readonly element: ElementHandle;
81
+ }
82
+ /**
83
+ * Try selectors in order, return the first match.
84
+ * Fails with SelectorError if none match.
85
+ */
86
+ export declare const resolveSelector: (page: Page, candidates: readonly string[], timeout?: number) => Effect.Effect<ResolvedSelector, SelectorError>;
87
+ /**
88
+ * Resolve a selector from the registry by key name.
89
+ */
90
+ export declare const resolve: (page: Page, key: SelectorKey, timeout?: number) => Effect.Effect<ResolvedSelector, SelectorError>;
91
+ /**
92
+ * Check if any selector in the candidates list exists on the page (non-blocking).
93
+ * Returns the matching selector string or null.
94
+ */
95
+ export declare const probeSelector: (page: Page, candidates: readonly string[]) => Effect.Effect<string | null, never>;
96
+ /**
97
+ * Probe a selector from the registry by key name.
98
+ */
99
+ export declare const probe: (page: Page, key: SelectorKey) => Effect.Effect<string | null, never>;
100
+ /**
101
+ * Validate that all critical selectors can be resolved on the current page.
102
+ * Returns a report of which selectors passed/failed.
103
+ */
104
+ export declare const healthCheck: (page: Page, keys: readonly SelectorKey[]) => Effect.Effect<{
105
+ passed: SelectorKey[];
106
+ failed: SelectorKey[];
107
+ }, never>;
108
+ //# sourceMappingURL=selectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../src/middleware/selectors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM5C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmKZ,CAAC;AAMX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,SAAS,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CAChC;AAMD;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC3B,MAAM,IAAI,EACV,YAAY,SAAS,MAAM,EAAE,EAC7B,gBAAc,KACZ,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,aAAa,CAuB7C,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,OAAO,GACnB,MAAM,IAAI,EACV,KAAK,WAAW,EAChB,UAAU,MAAM,KACd,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,aAAa,CACD,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,aAAa,GACzB,MAAM,IAAI,EACV,YAAY,SAAS,MAAM,EAAE,KAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,CAWlC,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,IAAI,EAAE,KAAK,WAAW,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,CACnD,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,WAAW,GACvB,MAAM,IAAI,EACV,MAAM,SAAS,WAAW,EAAE,KAC1B,MAAM,CAAC,MAAM,CACf;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,MAAM,EAAE,WAAW,EAAE,CAAA;CAAE,EAChD,KAAK,CAgBH,CAAC"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Acuity CSS Selector Registry
3
+ *
4
+ * Single source of truth for all CSS selectors used by the wizard middleware.
5
+ * Each selector has a primary pattern and fallback chain.
6
+ * When Acuity changes their DOM, fix this ONE file.
7
+ */
8
+ import { Effect } from 'effect';
9
+ import { SelectorError } from './errors.js';
10
+ // =============================================================================
11
+ // SELECTOR DEFINITIONS
12
+ // =============================================================================
13
+ /**
14
+ * Acuity Scheduling (2026 React SPA) CSS Selector Registry
15
+ *
16
+ * Verified against live DOM: 2026-02-25
17
+ * Uses Emotion CSS-in-JS (css-* hashes are UNSTABLE — prefer semantic classes)
18
+ *
19
+ * Wizard flow:
20
+ * 1. Service page: massageithaca.as.me → <li.select-item> list with "Book" buttons
21
+ * 2. Calendar page: /schedule/<hash>/appointment/<aptId>/calendar/<calId>
22
+ * - react-calendar month grid + available-times-container
23
+ * 3. Client form: (after selecting time slot) → input fields
24
+ * 4. Payment/coupon: certificate input → Apply → verify $0
25
+ * 5. Submit → confirmation
26
+ *
27
+ * URL pattern (no query params):
28
+ * /schedule/<hash>/appointment/<appointmentTypeId>/calendar/<calendarId>
29
+ */
30
+ export const Selectors = {
31
+ // -- Service selection page --
32
+ // Services are <li class="select-item select-item-box"> with NO <a> links
33
+ // Categories use <div class="select-type"> with <div class="select-label">
34
+ serviceList: ['.select-item', '.select-item-box', '.appointment-type-item'],
35
+ serviceName: ['.appointment-type-name', '.type-name', 'h3'],
36
+ serviceLink: ['.select-item', '.select-item-box'],
37
+ // Price & duration are combined: <span>30 minutes @ $150.00</span> inside .duration-container
38
+ servicePrice: ['.duration-container', '.duration-container span', '.price', '.cost'],
39
+ serviceDuration: ['.duration-container', '.duration-container span', '.duration', '.time-duration'],
40
+ serviceDescription: ['.type-description', '.description', 'p.type-description'],
41
+ // "Book" button inside each service item
42
+ serviceBookButton: ['button.btn', '.select-item button.btn'],
43
+ // Category labels
44
+ serviceCategory: ['.select-label', '.select-label p', '.select-type .select-label'],
45
+ // -- Calendar page (react-calendar component) --
46
+ // Wrapper: .monthly-calendar-v2 > .react-calendar.monthly-calendar-react-calendar
47
+ calendar: ['.monthly-calendar-v2', '.react-calendar', '.monthly-calendar-react-calendar'],
48
+ calendarMonth: ['.react-calendar__navigation__label', '.react-calendar__navigation__label__labelText'],
49
+ calendarPrev: ['.react-calendar__navigation__prev-button'],
50
+ calendarNext: ['.react-calendar__navigation__next-button'],
51
+ // Day tiles are buttons: <button class="react-calendar__tile react-calendar__month-view__days__day">1</button>
52
+ calendarDay: [
53
+ '.react-calendar__tile',
54
+ '.react-calendar__month-view__days__day',
55
+ 'button.react-calendar__tile',
56
+ ],
57
+ // Active/selected day: react-calendar__tile--active + custom "activeday" class
58
+ activeDay: [
59
+ '.react-calendar__tile--active',
60
+ '.activeday',
61
+ '.react-calendar__tile:not(:disabled)',
62
+ ],
63
+ // -- Time slot selection --
64
+ // Container: .available-times-container
65
+ // Slots: <button class="time-selection">10:00 AM1 spot left</button>
66
+ // Selected: <button class="time-selection selected-time">
67
+ timeSlotContainer: ['.available-times-container'],
68
+ timeSlot: ['button.time-selection', '.time-selection', '.time-slot', '[data-time]'],
69
+ timeSlotSelected: ['button.time-selection.selected-time', '.selected-time'],
70
+ // "Select and continue" is an <li role="menuitem"> NOT a button
71
+ selectAndContinue: [
72
+ 'li[role="menuitem"]',
73
+ '[data-keyboard-navigable="keyboard-navigable-list-item"]',
74
+ 'text=Select and continue',
75
+ ],
76
+ // -- Client form --
77
+ // Field names use "client." prefix: client.firstName, client.lastName, etc.
78
+ firstNameInput: ['input[name="client.firstName"]', '#client\\.firstName', 'input[name="firstName"]'],
79
+ lastNameInput: ['input[name="client.lastName"]', '#client\\.lastName', 'input[name="lastName"]'],
80
+ emailInput: ['input[name="client.email"]', '#client\\.email', 'input[name="email"]'],
81
+ phoneInput: ['input[name="client.phone"]', '#client\\.phone', 'input[name="phone"]'],
82
+ // "Continue to Payment" button on the form page
83
+ continueToPayment: [
84
+ 'button.btn:has-text("Continue to Payment")',
85
+ 'button:has-text("Continue to Payment")',
86
+ 'button.btn[type="submit"]',
87
+ ],
88
+ // "Check Code Balance" button for entering coupon codes
89
+ checkCodeBalance: [
90
+ 'button:has-text("Check Code Balance")',
91
+ 'button.css-9zfkvr',
92
+ ],
93
+ // Terms agreement checkbox (custom field)
94
+ termsCheckbox: [
95
+ 'input[type="checkbox"][name*="field-13933959"]',
96
+ 'input[id*="13933959"]',
97
+ ],
98
+ // -- Client form intake fields --
99
+ // Radio buttons have NO name or id attrs; are purely React-controlled.
100
+ // Strategy: click <label> wrapping the radio via locator().nth().
101
+ // 3 yes/no question groups, each with aria-required="true".
102
+ radioNoLabel: ['label:has(input[type="radio"][value="no"])'],
103
+ radioYesLabel: ['label:has(input[type="radio"][value="yes"])'],
104
+ // "How did you hear" multi-checkbox (REQUIRED — at least 1 must be checked)
105
+ // Names: "Internet search", "google maps", "referral from Noha Acupuncture",
106
+ // "referral from dentist", "referral from PT or other practitioner"
107
+ howDidYouHearCheckbox: [
108
+ 'input[type="checkbox"][name="Internet search"]',
109
+ 'label:has(input[type="checkbox"][name="Internet search"])',
110
+ ],
111
+ // Medication textarea
112
+ medicationField: [
113
+ 'textarea[name="fields[field-16606770]"]',
114
+ '#fields\\[field-16606770\\]',
115
+ ],
116
+ // -- Payment / coupon --
117
+ // PAYMENT IS A SEPARATE PAGE at URL .../datetime/<ISO>/payment
118
+ // Verified 2026-02-26: Square-powered (NOT Stripe).
119
+ //
120
+ // "Check Code Balance" modal on client form page is INFORMATIONAL ONLY.
121
+ // The REAL coupon entry is on the PAYMENT page:
122
+ // "Package, gift, or coupon code" expandable section
123
+ //
124
+ // Client form modal selectors (kept for reference):
125
+ couponField: ['#code', 'input#code', 'input[id="code"]'],
126
+ couponTabByCode: [
127
+ 'button:has-text("Check by code")',
128
+ 'button.css-1jjp8vb:has-text("Check by code")',
129
+ ],
130
+ couponConfirmButton: [
131
+ '[role="dialog"] button:has-text("Confirm")',
132
+ 'button.css-qgmcoe',
133
+ 'button:has-text("Confirm")',
134
+ ],
135
+ couponCloseButton: ['button:has-text("Close")', 'button.css-ve50y1'],
136
+ couponError: [
137
+ '[role="dialog"] p:has-text("weren\'t able to recognize")',
138
+ '[role="dialog"] p:has-text("try entering it again")',
139
+ 'p.css-7bwtx1',
140
+ ],
141
+ couponSuccess: [
142
+ '[role="dialog"] p:has-text("balance")',
143
+ '[role="dialog"] [class*="success"]',
144
+ '.coupon-applied',
145
+ '.certificate-success',
146
+ ],
147
+ // -- Payment page (Square checkout) --
148
+ // URL pattern: .../datetime/<ISO>/payment
149
+ // "Package, gift, or coupon code" expandable section is the coupon entry point.
150
+ paymentCouponToggle: [
151
+ 'button:has-text("Package, gift, or coupon code")',
152
+ 'text=Package, gift, or coupon code',
153
+ ],
154
+ // After expanding: input placeholder="Enter code" (React id unstable like :r9:)
155
+ // and an "Apply" button. Verified 2026-02-26.
156
+ paymentCouponInput: ['input[placeholder="Enter code"]', 'input[placeholder*="code" i]', 'input[name*="coupon"]'],
157
+ paymentCouponApply: ['button:has-text("Apply")', 'button:has-text("Redeem")'],
158
+ // After applying, the certificate shows with a "REMOVE" link
159
+ paymentCouponRemove: ['text=REMOVE', 'a:has-text("REMOVE")', 'button:has-text("REMOVE")'],
160
+ // Order summary on payment page
161
+ paymentTotal: ['.order-total', '.payment-total', '.total-amount', 'text=$0.00'],
162
+ paymentSubtotal: ['text=Subtotal'],
163
+ // Pay & Confirm button (the final submit on payment page)
164
+ payAndConfirm: [
165
+ 'button:has-text("Pay & Confirm")',
166
+ 'button:has-text("PAY & CONFIRM")',
167
+ 'button:has-text("Confirm Appointment")',
168
+ ],
169
+ // -- Checkout / submit (legacy — use payAndConfirm for payment page) --
170
+ submitButton: [
171
+ 'button:has-text("Pay & Confirm")',
172
+ 'button:has-text("PAY & CONFIRM")',
173
+ 'button[type="submit"].confirm',
174
+ '.complete-booking',
175
+ '#submit-booking',
176
+ 'button:has-text("Complete Appointment")',
177
+ 'button:has-text("Book Now")',
178
+ 'button:has-text("Schedule")',
179
+ ],
180
+ // -- Confirmation page --
181
+ confirmationPage: ['.confirmation', '.booking-confirmed', '.thank-you', '#confirmation'],
182
+ confirmationId: ['.confirmation-number', '.appointment-id', '[data-confirmation]'],
183
+ confirmationService: ['.appointment-type', '.service-name', '.booked-service'],
184
+ confirmationDatetime: ['.appointment-datetime', '.booked-time', '.booking-date'],
185
+ };
186
+ // =============================================================================
187
+ // RESOLUTION UTILITIES
188
+ // =============================================================================
189
+ /**
190
+ * Try selectors in order, return the first match.
191
+ * Fails with SelectorError if none match.
192
+ */
193
+ export const resolveSelector = (page, candidates, timeout = 3000) => Effect.gen(function* () {
194
+ for (const selector of candidates) {
195
+ const el = yield* Effect.tryPromise({
196
+ try: () => page.waitForSelector(selector, { timeout, state: 'attached' }).then((handle) => handle, () => null),
197
+ catch: () => null,
198
+ }).pipe(Effect.orElseSucceed(() => null));
199
+ if (el) {
200
+ return { selector, element: el };
201
+ }
202
+ }
203
+ return yield* Effect.fail(new SelectorError({
204
+ candidates,
205
+ message: `None of [${candidates.join(', ')}] found within ${timeout}ms`,
206
+ }));
207
+ });
208
+ /**
209
+ * Resolve a selector from the registry by key name.
210
+ */
211
+ export const resolve = (page, key, timeout) => resolveSelector(page, Selectors[key], timeout);
212
+ /**
213
+ * Check if any selector in the candidates list exists on the page (non-blocking).
214
+ * Returns the matching selector string or null.
215
+ */
216
+ export const probeSelector = (page, candidates) => Effect.gen(function* () {
217
+ for (const selector of candidates) {
218
+ const exists = yield* Effect.tryPromise({
219
+ try: () => page.$(selector).then((el) => el !== null),
220
+ catch: () => false,
221
+ }).pipe(Effect.orElseSucceed(() => false));
222
+ if (exists)
223
+ return selector;
224
+ }
225
+ return null;
226
+ });
227
+ /**
228
+ * Probe a selector from the registry by key name.
229
+ */
230
+ export const probe = (page, key) => probeSelector(page, Selectors[key]);
231
+ /**
232
+ * Validate that all critical selectors can be resolved on the current page.
233
+ * Returns a report of which selectors passed/failed.
234
+ */
235
+ export const healthCheck = (page, keys) => Effect.gen(function* () {
236
+ const passed = [];
237
+ const failed = [];
238
+ for (const key of keys) {
239
+ const found = yield* probe(page, key);
240
+ if (found) {
241
+ passed.push(key);
242
+ }
243
+ else {
244
+ failed.push(key);
245
+ }
246
+ }
247
+ return { passed, failed };
248
+ });
249
+ //# sourceMappingURL=selectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectors.js","sourceRoot":"","sources":["../../src/middleware/selectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Middleware HTTP Server
3
+ *
4
+ * Standalone Node.js HTTP server wrapping the Effect TS wizard programs.
5
+ * Designed to run inside a Docker container with Playwright + Chromium
6
+ * on Modal Labs, Fly.io, or any host.
7
+ *
8
+ * Endpoints:
9
+ * GET /health - Health check
10
+ * GET /services - List services (scraper)
11
+ * GET /services/:id - Get service by ID
12
+ * POST /availability/dates - Available dates for a service
13
+ * POST /availability/slots - Time slots for a date
14
+ * POST /availability/check - Check if a slot is available
15
+ * POST /booking/create - Create booking (standard)
16
+ * POST /booking/create-with-payment - Create booking with payment ref (coupon bypass)
17
+ *
18
+ * Environment variables:
19
+ * PORT - Server port (default: 3001)
20
+ * ACUITY_BASE_URL - Acuity scheduling URL
21
+ * ACUITY_BYPASS_COUPON - 100% coupon code
22
+ * AUTH_TOKEN - Required Bearer token for all endpoints
23
+ * PLAYWRIGHT_HEADLESS - Browser headless mode (default: true)
24
+ * PLAYWRIGHT_TIMEOUT - Page timeout in ms (default: 30000)
25
+ *
26
+ * Usage:
27
+ * node --import tsx/esm src/middleware/server.ts
28
+ * # or after build:
29
+ * node dist/middleware/server.js
30
+ */
31
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
32
+ declare const server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
33
+ export { server };
34
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/middleware/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAgYpF,QAAA,MAAM,MAAM,sEA2DV,CAAC;AAaH,OAAO,EAAE,MAAM,EAAE,CAAC"}