@rubytech/create-maxy-code 0.1.352 → 0.1.353
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/package.json +1 -1
- package/payload/platform/package-lock.json +76 -1
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +41 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +236 -47
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +27 -9
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js +102 -29
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +17 -6
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +28 -10
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/package.json +13 -3
- package/payload/server/public/assets/{AdminLoginScreens-lt72mYrz.js → AdminLoginScreens-CzdOQKyO.js} +1 -1
- package/payload/server/public/assets/{AdminShell-B-GeoG3j.js → AdminShell-CC-CuN6Y.js} +1 -1
- package/payload/server/public/assets/{Checkbox-DoGMXVpF.js → Checkbox-sQBc9xSy.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-CfG1EYyP.js → OperatorConversations-CA_2c3sp.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-DPVKGsRS.css +1 -0
- package/payload/server/public/assets/{admin-DzLxv-nz.js → admin-qW6jM6l0.js} +1 -1
- package/payload/server/public/assets/{browser-Cyi6v8BS.js → browser-BsW6KaxZ.js} +1 -1
- package/payload/server/public/assets/calendar-BPAiN1cL.js +1 -0
- package/payload/server/public/assets/chat-DVD9yoVL.js +1 -0
- package/payload/server/public/assets/{data-CrJZjlQ1.js → data-D_cR_NCZ.js} +1 -1
- package/payload/server/public/assets/{graph-CutaO0w1.js → graph-CmpAy2WP.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CH0sWvcA.js → graph-labels-C0SLq_VP.js} +1 -1
- package/payload/server/public/assets/{operator-MASl7IqX.js → operator-CrjtJcEY.js} +1 -1
- package/payload/server/public/assets/page-Cf3fVDSo.js +32 -0
- package/payload/server/public/assets/{public-77Ubicd7.js → public-BkNtvSYp.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +125 -84
- package/payload/server/public/assets/OperatorConversations-C8G6Gfgu.css +0 -1
- package/payload/server/public/assets/calendar-CA3_Dm8j.js +0 -1
- package/payload/server/public/assets/chat-B9aNK1FG.js +0 -1
- package/payload/server/public/assets/page-CZy33qUH.js +0 -30
package/package.json
CHANGED
|
@@ -93,6 +93,10 @@
|
|
|
93
93
|
"resolved": "plugins/cloudflare/mcp",
|
|
94
94
|
"link": true
|
|
95
95
|
},
|
|
96
|
+
"node_modules/@maxy/connector": {
|
|
97
|
+
"resolved": "plugins/connector/mcp",
|
|
98
|
+
"link": true
|
|
99
|
+
},
|
|
96
100
|
"node_modules/@maxy/contacts": {
|
|
97
101
|
"resolved": "plugins/contacts/mcp",
|
|
98
102
|
"link": true
|
|
@@ -105,6 +109,10 @@
|
|
|
105
109
|
"resolved": "plugins/graph-viewer/mcp",
|
|
106
110
|
"link": true
|
|
107
111
|
},
|
|
112
|
+
"node_modules/@maxy/joblogic": {
|
|
113
|
+
"resolved": "plugins/joblogic/mcp",
|
|
114
|
+
"link": true
|
|
115
|
+
},
|
|
108
116
|
"node_modules/@maxy/memory": {
|
|
109
117
|
"resolved": "plugins/memory/mcp",
|
|
110
118
|
"link": true
|
|
@@ -117,6 +125,10 @@
|
|
|
117
125
|
"resolved": "plugins/outlook/mcp",
|
|
118
126
|
"link": true
|
|
119
127
|
},
|
|
128
|
+
"node_modules/@maxy/quickbooks": {
|
|
129
|
+
"resolved": "plugins/quickbooks/mcp",
|
|
130
|
+
"link": true
|
|
131
|
+
},
|
|
120
132
|
"node_modules/@maxy/replicate": {
|
|
121
133
|
"resolved": "plugins/replicate/mcp",
|
|
122
134
|
"link": true
|
|
@@ -125,6 +137,10 @@
|
|
|
125
137
|
"resolved": "plugins/scheduling/mcp",
|
|
126
138
|
"link": true
|
|
127
139
|
},
|
|
140
|
+
"node_modules/@maxy/telegram-channel": {
|
|
141
|
+
"resolved": "services/telegram-channel",
|
|
142
|
+
"link": true
|
|
143
|
+
},
|
|
128
144
|
"node_modules/@maxy/url-get": {
|
|
129
145
|
"resolved": "plugins/url-get/mcp",
|
|
130
146
|
"link": true
|
|
@@ -3494,6 +3510,15 @@
|
|
|
3494
3510
|
"node": ">=14.17"
|
|
3495
3511
|
}
|
|
3496
3512
|
},
|
|
3513
|
+
"node_modules/undici": {
|
|
3514
|
+
"version": "6.27.0",
|
|
3515
|
+
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
|
3516
|
+
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
|
3517
|
+
"license": "MIT",
|
|
3518
|
+
"engines": {
|
|
3519
|
+
"node": ">=18.17"
|
|
3520
|
+
}
|
|
3521
|
+
},
|
|
3497
3522
|
"node_modules/undici-types": {
|
|
3498
3523
|
"version": "6.21.0",
|
|
3499
3524
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
@@ -3856,6 +3881,19 @@
|
|
|
3856
3881
|
"typescript": "^5.7.0"
|
|
3857
3882
|
}
|
|
3858
3883
|
},
|
|
3884
|
+
"plugins/connector/mcp": {
|
|
3885
|
+
"name": "@maxy/connector",
|
|
3886
|
+
"version": "0.1.0",
|
|
3887
|
+
"dependencies": {
|
|
3888
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
3889
|
+
"undici": "^6.27.0",
|
|
3890
|
+
"zod": "^3.24.0"
|
|
3891
|
+
},
|
|
3892
|
+
"devDependencies": {
|
|
3893
|
+
"@types/node": "^22.0.0",
|
|
3894
|
+
"typescript": "^5.7.0"
|
|
3895
|
+
}
|
|
3896
|
+
},
|
|
3859
3897
|
"plugins/contacts/mcp": {
|
|
3860
3898
|
"name": "@maxy/contacts",
|
|
3861
3899
|
"version": "0.1.0",
|
|
@@ -3865,7 +3903,8 @@
|
|
|
3865
3903
|
},
|
|
3866
3904
|
"devDependencies": {
|
|
3867
3905
|
"@types/node": "^22.0.0",
|
|
3868
|
-
"typescript": "^5.7.0"
|
|
3906
|
+
"typescript": "^5.7.0",
|
|
3907
|
+
"vitest": "^4.1.2"
|
|
3869
3908
|
}
|
|
3870
3909
|
},
|
|
3871
3910
|
"plugins/email/mcp": {
|
|
@@ -3901,6 +3940,18 @@
|
|
|
3901
3940
|
"vitest": "^4.1.2"
|
|
3902
3941
|
}
|
|
3903
3942
|
},
|
|
3943
|
+
"plugins/joblogic/mcp": {
|
|
3944
|
+
"name": "@maxy/joblogic",
|
|
3945
|
+
"version": "0.1.0",
|
|
3946
|
+
"dependencies": {
|
|
3947
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
3948
|
+
"zod": "^3.24.0"
|
|
3949
|
+
},
|
|
3950
|
+
"devDependencies": {
|
|
3951
|
+
"@types/node": "^22.0.0",
|
|
3952
|
+
"typescript": "^5.7.0"
|
|
3953
|
+
}
|
|
3954
|
+
},
|
|
3904
3955
|
"plugins/memory/mcp": {
|
|
3905
3956
|
"name": "@maxy/memory",
|
|
3906
3957
|
"version": "0.1.0",
|
|
@@ -3927,6 +3978,18 @@
|
|
|
3927
3978
|
"typescript": "^5.7.0"
|
|
3928
3979
|
}
|
|
3929
3980
|
},
|
|
3981
|
+
"plugins/quickbooks/mcp": {
|
|
3982
|
+
"name": "@maxy/quickbooks",
|
|
3983
|
+
"version": "0.1.0",
|
|
3984
|
+
"dependencies": {
|
|
3985
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
3986
|
+
"zod": "^3.24.0"
|
|
3987
|
+
},
|
|
3988
|
+
"devDependencies": {
|
|
3989
|
+
"@types/node": "^22.0.0",
|
|
3990
|
+
"typescript": "^5.7.0"
|
|
3991
|
+
}
|
|
3992
|
+
},
|
|
3930
3993
|
"plugins/replicate/mcp": {
|
|
3931
3994
|
"name": "@maxy/replicate",
|
|
3932
3995
|
"version": "0.1.0",
|
|
@@ -4065,6 +4128,18 @@
|
|
|
4065
4128
|
"vitest": "^4.1.2"
|
|
4066
4129
|
}
|
|
4067
4130
|
},
|
|
4131
|
+
"services/telegram-channel": {
|
|
4132
|
+
"name": "@maxy/telegram-channel",
|
|
4133
|
+
"version": "0.1.0",
|
|
4134
|
+
"dependencies": {
|
|
4135
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
4136
|
+
},
|
|
4137
|
+
"devDependencies": {
|
|
4138
|
+
"@types/node": "^22.0.0",
|
|
4139
|
+
"typescript": "^5.7.0",
|
|
4140
|
+
"vitest": "^4.1.2"
|
|
4141
|
+
}
|
|
4142
|
+
},
|
|
4068
4143
|
"services/webchat-channel": {
|
|
4069
4144
|
"name": "@maxy/webchat-channel",
|
|
4070
4145
|
"version": "0.1.0",
|
|
@@ -34,7 +34,7 @@ The public page shows open slots computed as the weekly window minus busy `:Meet
|
|
|
34
34
|
<accountDir>/calendar-availability.json
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Add the `apiBase` (the brand tunnel origin that serves free/busy, `https://<brand-tunnel-domain>`), the `bookingDomain` (the custom domain the page will answer on), and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability is changed later; there is no separate editor.
|
|
37
|
+
Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Also collect `daysAhead` (the booking horizon — a positive integer count of days the page lets visitors book into, e.g. `14`) and `allowMultiSlot` (a boolean: may a visitor combine consecutive open slots into one longer back-to-back booking? default `false`). Add the `apiBase` (the brand tunnel origin that serves free/busy, `https://<brand-tunnel-domain>`), the `bookingDomain` (the custom domain the page will answer on), and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability — including the horizon and the multi-slot choice — is changed later; there is no separate editor.
|
|
38
38
|
|
|
39
39
|
## Assemble the canonical tree
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ Copy the skill template into the one canonical Pages source path `site-deploy` e
|
|
|
44
44
|
<accountDir>/pages/<project>/
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`.
|
|
47
|
+
The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`. The substitutions that make it concrete are: in `public/index.html`, set the `<meta name="api-base">` content to the `apiBase` value (the booking page fetches free/busy cross-origin from the brand tunnel), set `<meta name="days-ahead">` content to `daysAhead`, and set `<meta name="allow-multi-slot">` content to `allowMultiSlot` as the literal `true` or `false`; and fill `wrangler.toml`'s `__PROJECT_NAME__`, `__D1_DATABASE_NAME__`, and `__D1_DATABASE_ID__` from the project name and the database created below. Leave no `__...__` placeholder in the assembled tree.
|
|
48
48
|
|
|
49
49
|
## Provision the D1 bookings table
|
|
50
50
|
|
package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css
CHANGED
|
@@ -41,6 +41,44 @@ body {
|
|
|
41
41
|
.bk-day { margin-bottom: 20px; }
|
|
42
42
|
.bk-day-title { font-size: 0.95rem; margin: 0 0 8px; color: var(--bk-ink); }
|
|
43
43
|
.bk-day-slots { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
44
|
+
.bk-day-empty { margin: 0; font-size: 0.85rem; color: var(--bk-muted); }
|
|
45
|
+
|
|
46
|
+
/* Five-day window pager. */
|
|
47
|
+
.bk-nav {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
margin-bottom: 16px;
|
|
53
|
+
}
|
|
54
|
+
.bk-nav-label { font-size: 0.9rem; color: var(--bk-muted); }
|
|
55
|
+
.bk-arrow {
|
|
56
|
+
border: 1px solid var(--bk-line);
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: var(--bk-ink);
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
width: 36px;
|
|
61
|
+
height: 36px;
|
|
62
|
+
font-size: 1.1rem;
|
|
63
|
+
line-height: 1;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
.bk-arrow:hover:not(:disabled) { border-color: var(--bk-accent); color: var(--bk-accent); }
|
|
67
|
+
.bk-arrow:disabled { opacity: 0.35; cursor: default; }
|
|
68
|
+
|
|
69
|
+
/* Extend / shorten a back-to-back multi-slot booking (operator-enabled). */
|
|
70
|
+
.bk-extend { display: flex; gap: 8px; margin: 0 0 4px; }
|
|
71
|
+
.bk-extend-btn {
|
|
72
|
+
border: 1px solid var(--bk-accent);
|
|
73
|
+
background: transparent;
|
|
74
|
+
color: var(--bk-accent);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 6px 12px;
|
|
77
|
+
font-size: 0.85rem;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
.bk-extend-btn:hover:not(:disabled) { background: var(--bk-accent); color: var(--bk-accent-ink); }
|
|
81
|
+
.bk-extend-btn:disabled { opacity: 0.4; cursor: default; }
|
|
44
82
|
|
|
45
83
|
.bk-slot {
|
|
46
84
|
border: 1px solid var(--bk-accent);
|
|
@@ -54,6 +92,9 @@ body {
|
|
|
54
92
|
.bk-slot:hover { background: var(--bk-accent); color: var(--bk-accent-ink); }
|
|
55
93
|
|
|
56
94
|
.bk-form { display: flex; flex-direction: column; gap: 14px; }
|
|
95
|
+
/* The form's display:flex would otherwise beat the UA [hidden] rule and show
|
|
96
|
+
the form before a slot is picked; this keeps `hidden` authoritative. */
|
|
97
|
+
.bk-form[hidden] { display: none; }
|
|
57
98
|
.bk-chosen { font-weight: 600; margin: 0 0 4px; }
|
|
58
99
|
.bk-label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--bk-muted); }
|
|
59
100
|
.bk-input {
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
// Public booking page (calendar-site skill). Dependency-free.
|
|
2
2
|
// 1. Fetch open slots from the brand tunnel free/busy endpoint.
|
|
3
|
-
// 2.
|
|
3
|
+
// 2. Page the horizon five days at a time, let the visitor pick a slot
|
|
4
|
+
// (and, when the operator allows it, extend to back-to-back slots).
|
|
4
5
|
// 3. POST the booking to the same-origin Pages Function (/api/book → D1).
|
|
5
6
|
(function () {
|
|
6
7
|
'use strict'
|
|
7
8
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
var WINDOW_DAYS = 5
|
|
10
|
+
|
|
11
|
+
function metaContent(name) {
|
|
12
|
+
var el = document.querySelector('meta[name="' + name + '"]')
|
|
13
|
+
return el ? el.getAttribute('content') : null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Operator-set booking horizon. The skill rewrites the meta at assemble time;
|
|
17
|
+
// fall back to 14 only when it is absent or left unsubstituted, so pages
|
|
18
|
+
// assembled before this change behave exactly as they did.
|
|
19
|
+
var parsedDaysAhead = parseInt(metaContent('days-ahead'), 10)
|
|
20
|
+
var daysAhead = isFinite(parsedDaysAhead) && parsedDaysAhead > 0 ? parsedDaysAhead : 14
|
|
21
|
+
|
|
22
|
+
// Operator choice: may a visitor combine consecutive open slots into one
|
|
23
|
+
// longer booking? Anything other than the literal "true" is off.
|
|
24
|
+
var allowMultiSlot = metaContent('allow-multi-slot') === 'true'
|
|
25
|
+
|
|
26
|
+
var apiBase = metaContent('api-base') || ''
|
|
11
27
|
|
|
12
28
|
var statusEl = document.getElementById('bk-status')
|
|
13
29
|
var slotsEl = document.getElementById('bk-slots')
|
|
@@ -18,7 +34,12 @@
|
|
|
18
34
|
var durationEl = document.getElementById('bk-duration')
|
|
19
35
|
var backBtn = document.getElementById('bk-back')
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
// The slots that share the chosen block's day, the index of the first slot
|
|
38
|
+
// in that block, and how many back-to-back slots it spans. count stays 1
|
|
39
|
+
// unless the operator enabled multi-slot bookings.
|
|
40
|
+
var block = null
|
|
41
|
+
// Extend/shorten controls, built lazily once and reused.
|
|
42
|
+
var multiControls = null
|
|
22
43
|
|
|
23
44
|
function setStatus(msg) {
|
|
24
45
|
statusEl.textContent = msg || ''
|
|
@@ -27,9 +48,57 @@
|
|
|
27
48
|
function fmtDayHeading(d) {
|
|
28
49
|
return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
|
|
29
50
|
}
|
|
51
|
+
function fmtShortDay(d) {
|
|
52
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
53
|
+
}
|
|
30
54
|
function fmtTime(d) {
|
|
31
55
|
return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
32
56
|
}
|
|
57
|
+
function dayKey(d) {
|
|
58
|
+
return d.toDateString()
|
|
59
|
+
}
|
|
60
|
+
function startOfLocalDay(d) {
|
|
61
|
+
var x = new Date(d)
|
|
62
|
+
x.setHours(0, 0, 0, 0)
|
|
63
|
+
return x
|
|
64
|
+
}
|
|
65
|
+
function clear(el) {
|
|
66
|
+
while (el.firstChild) el.removeChild(el.firstChild)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- horizon state, set once per free/busy load -------------------------
|
|
70
|
+
var allDays = [] // local-midnight Date per calendar day in the horizon
|
|
71
|
+
var slotsByDay = {} // dayKey -> ordered slot array
|
|
72
|
+
var windowStart = 0 // index into allDays, advances in WINDOW_DAYS steps
|
|
73
|
+
|
|
74
|
+
// The first and last slot of the chosen block — the single source of the
|
|
75
|
+
// block's index arithmetic, shared by the formatters and the submit payload.
|
|
76
|
+
function firstSlot() {
|
|
77
|
+
return block.daySlots[block.startIdx]
|
|
78
|
+
}
|
|
79
|
+
function lastSlot() {
|
|
80
|
+
return block.daySlots[block.startIdx + block.count - 1]
|
|
81
|
+
}
|
|
82
|
+
function blockStart() {
|
|
83
|
+
return new Date(firstSlot().start)
|
|
84
|
+
}
|
|
85
|
+
function blockEnd() {
|
|
86
|
+
return new Date(lastSlot().end)
|
|
87
|
+
}
|
|
88
|
+
// The slot immediately after the current block, if it is open and back-to-back.
|
|
89
|
+
function nextContiguousSlot() {
|
|
90
|
+
var next = block.daySlots[block.startIdx + block.count]
|
|
91
|
+
if (!next) return null
|
|
92
|
+
return next.start === lastSlot().end ? next : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fmtChosen() {
|
|
96
|
+
var start = blockStart()
|
|
97
|
+
if (block.count === 1) {
|
|
98
|
+
return fmtDayHeading(start) + ' at ' + fmtTime(start)
|
|
99
|
+
}
|
|
100
|
+
return fmtDayHeading(start) + ', ' + fmtTime(start) + ' – ' + fmtTime(blockEnd())
|
|
101
|
+
}
|
|
33
102
|
|
|
34
103
|
function renderSlots(data) {
|
|
35
104
|
var slots = (data && data.slots) || []
|
|
@@ -37,69 +106,184 @@
|
|
|
37
106
|
durationEl.textContent = data.durationMins + ' minute meeting'
|
|
38
107
|
}
|
|
39
108
|
if (slots.length === 0) {
|
|
40
|
-
setStatus('No open times in the next
|
|
109
|
+
setStatus('No open times in the next ' + daysAhead + ' day' + (daysAhead === 1 ? '' : 's') + '. Please check back later.')
|
|
41
110
|
return
|
|
42
111
|
}
|
|
43
112
|
setStatus('')
|
|
44
113
|
|
|
45
|
-
|
|
46
|
-
|
|
114
|
+
// The full calendar-day list for the horizon, today first. setDate keeps
|
|
115
|
+
// local midnight across DST, unlike adding fixed millisecond spans.
|
|
116
|
+
var today = startOfLocalDay(new Date())
|
|
117
|
+
allDays = []
|
|
118
|
+
for (var i = 0; i < daysAhead; i++) {
|
|
119
|
+
var d = new Date(today)
|
|
120
|
+
d.setDate(today.getDate() + i)
|
|
121
|
+
allDays.push(d)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
slotsByDay = {}
|
|
47
125
|
slots.forEach(function (s) {
|
|
48
|
-
var
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
126
|
+
var k = dayKey(new Date(s.start))
|
|
127
|
+
if (!slotsByDay[k]) slotsByDay[k] = []
|
|
128
|
+
slotsByDay[k].push(s)
|
|
129
|
+
})
|
|
130
|
+
Object.keys(slotsByDay).forEach(function (k) {
|
|
131
|
+
slotsByDay[k].sort(function (a, b) {
|
|
132
|
+
return new Date(a.start) - new Date(b.start)
|
|
133
|
+
})
|
|
55
134
|
})
|
|
56
135
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
136
|
+
windowStart = 0
|
|
137
|
+
console.log('[booking] daysAhead=' + daysAhead + ' allowMultiSlot=' + allowMultiSlot + ' windowStart=' + windowStart)
|
|
138
|
+
renderWindow()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderWindow() {
|
|
142
|
+
clear(slotsEl)
|
|
143
|
+
|
|
144
|
+
var nav = document.createElement('div')
|
|
145
|
+
nav.className = 'bk-nav'
|
|
146
|
+
|
|
147
|
+
var back = document.createElement('button')
|
|
148
|
+
back.type = 'button'
|
|
149
|
+
back.className = 'bk-arrow'
|
|
150
|
+
back.setAttribute('aria-label', 'Previous days')
|
|
151
|
+
back.textContent = '‹'
|
|
152
|
+
back.disabled = windowStart === 0
|
|
153
|
+
back.addEventListener('click', function () {
|
|
154
|
+
windowStart = Math.max(0, windowStart - WINDOW_DAYS)
|
|
155
|
+
console.log('[booking] windowStart=' + windowStart)
|
|
156
|
+
renderWindow()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
var label = document.createElement('span')
|
|
160
|
+
label.className = 'bk-nav-label'
|
|
161
|
+
var lastIdx = Math.min(windowStart + WINDOW_DAYS, allDays.length) - 1
|
|
162
|
+
label.textContent = fmtShortDay(allDays[windowStart]) + ' – ' + fmtShortDay(allDays[lastIdx])
|
|
163
|
+
|
|
164
|
+
var fwd = document.createElement('button')
|
|
165
|
+
fwd.type = 'button'
|
|
166
|
+
fwd.className = 'bk-arrow'
|
|
167
|
+
fwd.setAttribute('aria-label', 'Next days')
|
|
168
|
+
fwd.textContent = '›'
|
|
169
|
+
fwd.disabled = windowStart + WINDOW_DAYS >= allDays.length
|
|
170
|
+
fwd.addEventListener('click', function () {
|
|
171
|
+
// Reachable only when not disabled, so a full step always has room.
|
|
172
|
+
windowStart += WINDOW_DAYS
|
|
173
|
+
console.log('[booking] windowStart=' + windowStart)
|
|
174
|
+
renderWindow()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
nav.appendChild(back)
|
|
178
|
+
nav.appendChild(label)
|
|
179
|
+
nav.appendChild(fwd)
|
|
180
|
+
slotsEl.appendChild(nav)
|
|
181
|
+
|
|
182
|
+
for (var i = windowStart; i < windowStart + WINDOW_DAYS && i < allDays.length; i++) {
|
|
183
|
+
slotsEl.appendChild(renderDayGroup(allDays[i]))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// One day's heading plus its slot buttons (or the empty note). Kept as its own
|
|
188
|
+
// function so each slot button closes over that day's own slot array — a shared
|
|
189
|
+
// loop-scoped var would leave every handler pointing at the last day rendered.
|
|
190
|
+
function renderDayGroup(day) {
|
|
191
|
+
var group = document.createElement('div')
|
|
192
|
+
group.className = 'bk-day'
|
|
193
|
+
|
|
194
|
+
var h = document.createElement('h2')
|
|
195
|
+
h.className = 'bk-day-title'
|
|
196
|
+
h.textContent = fmtDayHeading(day)
|
|
197
|
+
group.appendChild(h)
|
|
198
|
+
|
|
199
|
+
var daySlots = slotsByDay[dayKey(day)]
|
|
200
|
+
if (!daySlots || daySlots.length === 0) {
|
|
201
|
+
var note = document.createElement('p')
|
|
202
|
+
note.className = 'bk-day-empty'
|
|
203
|
+
note.textContent = 'No availability'
|
|
204
|
+
group.appendChild(note)
|
|
205
|
+
return group
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
var row = document.createElement('div')
|
|
209
|
+
row.className = 'bk-day-slots'
|
|
210
|
+
daySlots.forEach(function (s, idx) {
|
|
211
|
+
var btn = document.createElement('button')
|
|
212
|
+
btn.type = 'button'
|
|
213
|
+
btn.className = 'bk-slot'
|
|
214
|
+
btn.textContent = fmtTime(new Date(s.start))
|
|
215
|
+
btn.addEventListener('click', function () {
|
|
216
|
+
chooseSlot(daySlots, idx)
|
|
76
217
|
})
|
|
77
|
-
|
|
78
|
-
|
|
218
|
+
row.appendChild(btn)
|
|
219
|
+
})
|
|
220
|
+
group.appendChild(row)
|
|
221
|
+
return group
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ensureMultiControls() {
|
|
225
|
+
if (multiControls) return multiControls
|
|
226
|
+
multiControls = document.createElement('div')
|
|
227
|
+
multiControls.className = 'bk-extend'
|
|
228
|
+
|
|
229
|
+
var shorten = document.createElement('button')
|
|
230
|
+
shorten.type = 'button'
|
|
231
|
+
shorten.className = 'bk-extend-btn'
|
|
232
|
+
shorten.id = 'bk-shorten'
|
|
233
|
+
shorten.textContent = '− Shorten'
|
|
234
|
+
shorten.addEventListener('click', function () {
|
|
235
|
+
if (block.count > 1) {
|
|
236
|
+
block.count--
|
|
237
|
+
refreshChosen()
|
|
238
|
+
}
|
|
79
239
|
})
|
|
240
|
+
|
|
241
|
+
var extend = document.createElement('button')
|
|
242
|
+
extend.type = 'button'
|
|
243
|
+
extend.className = 'bk-extend-btn'
|
|
244
|
+
extend.id = 'bk-extend'
|
|
245
|
+
extend.textContent = '+ Add next slot'
|
|
246
|
+
extend.addEventListener('click', function () {
|
|
247
|
+
if (nextContiguousSlot()) {
|
|
248
|
+
block.count++
|
|
249
|
+
refreshChosen()
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
multiControls.appendChild(shorten)
|
|
254
|
+
multiControls.appendChild(extend)
|
|
255
|
+
chosenEl.insertAdjacentElement('afterend', multiControls)
|
|
256
|
+
return multiControls
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refreshChosen() {
|
|
260
|
+
chosenEl.textContent = fmtChosen()
|
|
261
|
+
if (!allowMultiSlot) return
|
|
262
|
+
ensureMultiControls()
|
|
263
|
+
document.getElementById('bk-shorten').disabled = block.count <= 1
|
|
264
|
+
document.getElementById('bk-extend').disabled = !nextContiguousSlot()
|
|
80
265
|
}
|
|
81
266
|
|
|
82
|
-
function chooseSlot(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
chosenEl.textContent = fmtDayHeading(start) + ' at ' + fmtTime(start)
|
|
267
|
+
function chooseSlot(daySlots, idx) {
|
|
268
|
+
block = { daySlots: daySlots, startIdx: idx, count: 1 }
|
|
269
|
+
refreshChosen()
|
|
86
270
|
slotsEl.hidden = true
|
|
87
271
|
formEl.hidden = false
|
|
88
272
|
}
|
|
89
273
|
|
|
90
274
|
backBtn.addEventListener('click', function () {
|
|
91
|
-
|
|
275
|
+
block = null
|
|
92
276
|
formEl.hidden = true
|
|
93
277
|
slotsEl.hidden = false
|
|
94
278
|
})
|
|
95
279
|
|
|
96
280
|
formEl.addEventListener('submit', function (e) {
|
|
97
281
|
e.preventDefault()
|
|
98
|
-
if (!
|
|
282
|
+
if (!block) return
|
|
99
283
|
var fd = new FormData(formEl)
|
|
100
284
|
var payload = {
|
|
101
|
-
slotStart:
|
|
102
|
-
slotEnd:
|
|
285
|
+
slotStart: firstSlot().start,
|
|
286
|
+
slotEnd: lastSlot().end,
|
|
103
287
|
name: fd.get('name'),
|
|
104
288
|
email: fd.get('email'),
|
|
105
289
|
note: fd.get('note'),
|
|
@@ -125,8 +309,7 @@
|
|
|
125
309
|
}
|
|
126
310
|
formEl.hidden = true
|
|
127
311
|
doneEl.hidden = false
|
|
128
|
-
|
|
129
|
-
doneMsg.textContent = 'Confirmed for ' + fmtDayHeading(start) + ' at ' + fmtTime(start) + '. A confirmation will follow by email.'
|
|
312
|
+
doneMsg.textContent = 'Confirmed for ' + fmtChosen() + '. A confirmation will follow by email.'
|
|
130
313
|
})
|
|
131
314
|
.catch(function (err) {
|
|
132
315
|
submitBtn.disabled = false
|
|
@@ -137,8 +320,14 @@
|
|
|
137
320
|
|
|
138
321
|
function load() {
|
|
139
322
|
setStatus('Loading available times…')
|
|
323
|
+
// Fetch exactly the horizon the page renders: from now through the end of
|
|
324
|
+
// the last calendar day in [today, today + daysAhead). Bounding `to` to a
|
|
325
|
+
// local-midnight day edge (not now + daysAhead*24h) keeps the fetched range
|
|
326
|
+
// and the rendered `allDays` window in the same local frame, so no slot is
|
|
327
|
+
// returned for a day the window cannot show.
|
|
140
328
|
var from = new Date()
|
|
141
|
-
var to =
|
|
329
|
+
var to = startOfLocalDay(from)
|
|
330
|
+
to.setDate(to.getDate() + daysAhead)
|
|
142
331
|
var url = apiBase + '/api/calendar/free-busy?from=' + encodeURIComponent(from.toISOString()) + '&to=' + encodeURIComponent(to.toISOString())
|
|
143
332
|
fetch(url)
|
|
144
333
|
.then(function (res) {
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<!-- The calendar-site skill rewrites the content of this tag to the brand
|
|
7
7
|
tunnel origin that serves /api/calendar/free-busy. -->
|
|
8
8
|
<meta name="api-base" content="__API_BASE__" />
|
|
9
|
+
<!-- Operator-set booking horizon (positive integer days). The skill rewrites
|
|
10
|
+
the content; booking.js falls back to 14 when it is left unsubstituted. -->
|
|
11
|
+
<meta name="days-ahead" content="__DAYS_AHEAD__" />
|
|
12
|
+
<!-- Operator choice: may a visitor combine consecutive open slots into one
|
|
13
|
+
longer booking? "true" or "false"; booking.js falls back to false. -->
|
|
14
|
+
<meta name="allow-multi-slot" content="__ALLOW_MULTI_SLOT__" />
|
|
9
15
|
<title>Book a time</title>
|
|
10
16
|
<link rel="stylesheet" href="/booking.css" />
|
|
11
17
|
</head>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { LookupFunction } from "node:net";
|
|
2
|
+
import { fetch as undiciFetch, type Dispatcher } from "undici";
|
|
3
|
+
import { type Refusal, type Resolution } from "./ssrf.js";
|
|
2
4
|
import type { ConnectorRecord } from "./store.js";
|
|
3
5
|
export interface CallParams {
|
|
4
6
|
method: string;
|
|
@@ -8,10 +10,16 @@ export interface CallParams {
|
|
|
8
10
|
headers?: Record<string, string>;
|
|
9
11
|
}
|
|
10
12
|
export interface CallDeps {
|
|
11
|
-
/** Override for tests; defaults to
|
|
12
|
-
fetchImpl?: typeof
|
|
13
|
-
/** Override for tests so a loopback stub
|
|
14
|
-
|
|
13
|
+
/** Override for tests; defaults to undici's fetch (same instance as the pinned dispatcher). */
|
|
14
|
+
fetchImpl?: typeof undiciFetch;
|
|
15
|
+
/** Override for tests so a loopback stub resolves; defaults to the real address-validating resolve. */
|
|
16
|
+
resolve?: (targetHost: string, registeredHost: string, kind: "request" | "redirect") => Promise<Resolution>;
|
|
17
|
+
/**
|
|
18
|
+
* Build the dispatcher that pins the connection to the validated addresses. Defaults to
|
|
19
|
+
* the real pinned dispatcher. Tests return `undefined` to skip pinning (a pinned loopback
|
|
20
|
+
* address would otherwise fail the public re-validation).
|
|
21
|
+
*/
|
|
22
|
+
makeDispatcher?: (addresses: string[]) => Dispatcher | undefined;
|
|
15
23
|
/** Sink for `[connector]` lifecycle lines; the secret is never passed to it. */
|
|
16
24
|
log?: (line: string) => void;
|
|
17
25
|
reqId?: string;
|
|
@@ -27,10 +35,20 @@ export type CallResult = {
|
|
|
27
35
|
refused: Refusal;
|
|
28
36
|
};
|
|
29
37
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
38
|
+
* Build the DNS lookup an undici dispatcher uses to dial a connector. It returns the
|
|
39
|
+
* pre-validated addresses without consulting DNS — so no second, independent lookup
|
|
40
|
+
* happens at connect time — and re-validates each is public, failing closed. undici
|
|
41
|
+
* always calls it with `{ all: true }`; the single-result branch is kept for completeness.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pinnedLookup(addresses: string[]): LookupFunction;
|
|
44
|
+
/** Dispatcher that dials only the validated addresses; TLS/SNI still use the hostname, so cert validation is unchanged. */
|
|
45
|
+
export declare function pinnedDispatcher(addresses: string[]): Dispatcher;
|
|
46
|
+
/**
|
|
47
|
+
* Make one authenticated, host-bound request to a registered connector. The host is resolved
|
|
48
|
+
* and validated before the request and before every redirect hop, and the socket is pinned to
|
|
49
|
+
* the validated addresses so no second connect-time lookup can land on a rebind. The credential
|
|
50
|
+
* is injected from the caller-supplied secret and never logged. Returns status + redacted headers
|
|
51
|
+
* + capped body, or a refusal when resolution rejects the target.
|
|
34
52
|
*/
|
|
35
53
|
export declare function performCall(rec: ConnectorRecord, secret: string, params: CallParams, deps?: CallDeps): Promise<CallResult>;
|
|
36
54
|
//# sourceMappingURL=call.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/lib/call.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/lib/call.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAS,KAAK,IAAI,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EAA8B,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,QAAQ;IACvB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,WAAW,CAAC;IAC/B,uGAAuG;IACvG,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5G;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,UAAU,GAAG,SAAS,CAAC;IACjE,gFAAgF;IAChF,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAC/F;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAQpC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,cAAc,CAUhE;AAED,2HAA2H;AAC3H,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,UAAU,CAEhE;AAkBD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,QAAa,GAClB,OAAO,CAAC,UAAU,CAAC,CA8FrB"}
|