@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.
- package/dist/adapters/acuity-scraper.d.ts +8 -0
- package/dist/adapters/acuity-scraper.d.ts.map +1 -0
- package/dist/adapters/acuity-scraper.js +8 -0
- package/dist/adapters/acuity-scraper.js.map +1 -0
- package/dist/adapters/types.d.ts +8 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +8 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/acuity-wizard.d.ts +49 -0
- package/dist/middleware/acuity-wizard.d.ts.map +1 -0
- package/dist/middleware/acuity-wizard.js +265 -0
- package/dist/middleware/acuity-wizard.js.map +1 -0
- package/dist/middleware/browser-service.d.ts +53 -0
- package/dist/middleware/browser-service.d.ts.map +1 -0
- package/dist/middleware/browser-service.js +105 -0
- package/dist/middleware/browser-service.js.map +1 -0
- package/dist/middleware/errors.d.ts +58 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +43 -0
- package/dist/middleware/errors.js.map +1 -0
- package/{src/middleware/index.ts → dist/middleware/index.d.ts} +5 -52
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +38 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +26 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +65 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/remote-adapter.d.ts +45 -0
- package/dist/middleware/remote-adapter.d.ts.map +1 -0
- package/dist/middleware/remote-adapter.js +178 -0
- package/dist/middleware/remote-adapter.js.map +1 -0
- package/dist/middleware/selector-health.d.ts +44 -0
- package/dist/middleware/selector-health.d.ts.map +1 -0
- package/dist/middleware/selector-health.js +144 -0
- package/dist/middleware/selector-health.js.map +1 -0
- package/dist/middleware/selectors.d.ts +108 -0
- package/dist/middleware/selectors.d.ts.map +1 -0
- package/dist/middleware/selectors.js +249 -0
- package/dist/middleware/selectors.js.map +1 -0
- package/dist/middleware/server.d.ts +34 -0
- package/dist/middleware/server.d.ts.map +1 -0
- package/dist/middleware/server.js +377 -0
- package/dist/middleware/server.js.map +1 -0
- package/dist/middleware/service-resolver.d.ts +46 -0
- package/dist/middleware/service-resolver.d.ts.map +1 -0
- package/dist/middleware/service-resolver.js +274 -0
- package/dist/middleware/service-resolver.js.map +1 -0
- package/dist/middleware/slot-parser.d.ts +29 -0
- package/dist/middleware/slot-parser.d.ts.map +1 -0
- package/dist/middleware/slot-parser.js +50 -0
- package/dist/middleware/slot-parser.js.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts +14 -0
- package/dist/middleware/steps/__tests__/fixtures.d.ts.map +1 -0
- package/dist/middleware/steps/__tests__/fixtures.js +204 -0
- package/dist/middleware/steps/__tests__/fixtures.js.map +1 -0
- package/dist/middleware/steps/bypass-payment.d.ts +54 -0
- package/dist/middleware/steps/bypass-payment.d.ts.map +1 -0
- package/dist/middleware/steps/bypass-payment.js +164 -0
- package/dist/middleware/steps/bypass-payment.js.map +1 -0
- package/dist/middleware/steps/extract-business.d.ts +93 -0
- package/dist/middleware/steps/extract-business.d.ts.map +1 -0
- package/dist/middleware/steps/extract-business.js +170 -0
- package/dist/middleware/steps/extract-business.js.map +1 -0
- package/dist/middleware/steps/extract.d.ts +41 -0
- package/dist/middleware/steps/extract.d.ts.map +1 -0
- package/dist/middleware/steps/extract.js +128 -0
- package/dist/middleware/steps/extract.js.map +1 -0
- package/dist/middleware/steps/fill-form.d.ts +45 -0
- package/dist/middleware/steps/fill-form.d.ts.map +1 -0
- package/dist/middleware/steps/fill-form.js +262 -0
- package/dist/middleware/steps/fill-form.js.map +1 -0
- package/dist/middleware/steps/index.d.ts +12 -0
- package/dist/middleware/steps/index.d.ts.map +1 -0
- package/dist/middleware/steps/index.js +12 -0
- package/dist/middleware/steps/index.js.map +1 -0
- package/dist/middleware/steps/navigate.d.ts +51 -0
- package/dist/middleware/steps/navigate.d.ts.map +1 -0
- package/dist/middleware/steps/navigate.js +391 -0
- package/dist/middleware/steps/navigate.js.map +1 -0
- package/dist/middleware/steps/read-availability.d.ts +37 -0
- package/dist/middleware/steps/read-availability.d.ts.map +1 -0
- package/dist/middleware/steps/read-availability.js +298 -0
- package/dist/middleware/steps/read-availability.js.map +1 -0
- package/dist/middleware/steps/read-slots.d.ts +33 -0
- package/dist/middleware/steps/read-slots.d.ts.map +1 -0
- package/dist/middleware/steps/read-slots.js +295 -0
- package/dist/middleware/steps/read-slots.js.map +1 -0
- package/dist/middleware/steps/read-via-url.d.ts +39 -0
- package/dist/middleware/steps/read-via-url.d.ts.map +1 -0
- package/dist/middleware/steps/read-via-url.js +141 -0
- package/dist/middleware/steps/read-via-url.js.map +1 -0
- package/dist/middleware/steps/submit.d.ts +22 -0
- package/dist/middleware/steps/submit.d.ts.map +1 -0
- package/dist/middleware/steps/submit.js +112 -0
- package/dist/middleware/steps/submit.js.map +1 -0
- package/dist/middleware/wizard-calendar.d.ts +37 -0
- package/dist/middleware/wizard-calendar.d.ts.map +1 -0
- package/dist/middleware/wizard-calendar.js +177 -0
- package/dist/middleware/wizard-calendar.js.map +1 -0
- package/dist/middleware/wizard-service.d.ts +30 -0
- package/dist/middleware/wizard-service.d.ts.map +1 -0
- package/dist/middleware/wizard-service.js +89 -0
- package/dist/middleware/wizard-service.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/{src/server.ts → dist/server.js} +1 -0
- package/dist/server.js.map +1 -0
- package/package.json +16 -4
- package/.github/workflows/build-paper.yml +0 -39
- package/.github/workflows/ci.yml +0 -37
- package/Dockerfile +0 -53
- package/docs/blog-post.mdx +0 -240
- package/docs/paper/IEEEtran.bst +0 -2409
- package/docs/paper/IEEEtran.cls +0 -6347
- package/docs/paper/acuity-middleware-paper.tex +0 -375
- package/docs/paper/balance.sty +0 -87
- package/docs/paper/references.bib +0 -231
- package/docs/paper.md +0 -400
- package/flake.nix +0 -32
- package/modal-app.py +0 -82
- package/src/adapters/acuity-scraper.ts +0 -543
- package/src/adapters/types.ts +0 -193
- package/src/core/types.ts +0 -325
- package/src/index.ts +0 -75
- package/src/middleware/acuity-wizard.ts +0 -456
- package/src/middleware/browser-service.ts +0 -183
- package/src/middleware/errors.ts +0 -70
- package/src/middleware/remote-adapter.ts +0 -246
- package/src/middleware/selectors.ts +0 -308
- package/src/middleware/server.ts +0 -372
- package/src/middleware/steps/bypass-payment.ts +0 -226
- package/src/middleware/steps/extract.ts +0 -174
- package/src/middleware/steps/fill-form.ts +0 -359
- package/src/middleware/steps/index.ts +0 -27
- package/src/middleware/steps/navigate.ts +0 -537
- package/src/middleware/steps/read-availability.ts +0 -399
- package/src/middleware/steps/read-slots.ts +0 -405
- package/src/middleware/steps/submit.ts +0 -168
- 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"}
|