@rubytech/create-maxy-code 0.1.341 → 0.1.343
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/config/brand.json +4 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +88 -1
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +82 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/book.ts +112 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +100 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +47 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +18 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/wrangler.toml +14 -0
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +84 -0
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +26 -0
- package/payload/platform/plugins/docs/references/calendar-booking.md +56 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js +45 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +89 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts +22 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js +31 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +28 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +70 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts +21 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +280 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +2 -0
- package/payload/server/{chunk-5BG6CHGH.js → chunk-QAKVFSEJ.js} +15 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminLoginScreens-KVvngpT2.js +1 -0
- package/payload/server/public/assets/AdminShell-DDKZhJRo.js +1 -0
- package/payload/server/public/assets/Checkbox-BJlS0tR2.js +1 -0
- package/payload/server/public/assets/OperatorConversations-_P3_KmAW.js +1 -0
- package/payload/server/public/assets/admin-BTQWgciK.js +1 -0
- package/payload/server/public/assets/{OperatorConversations-BMIZQR9t.css → admin-auth-fetch-9JCQ2FFm.css} +1 -1
- package/payload/server/public/assets/admin-auth-fetch-BMOxrEnl.js +9 -0
- package/payload/server/public/assets/{arc-CxLB9WCE.js → arc-Crkz45LN.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-Dma4Bgha.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-D-EdJPSY.js → architectureDiagram-Q4EWVU46-C9JThHgP.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CXx9RgCm.js → blockDiagram-DXYQGD6D-DpsV_HXT.js} +1 -1
- package/payload/server/public/assets/browser-CpOL9dN0.js +1 -0
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-RFBXEe0B.js → c4Diagram-AHTNJAMY-CTm_6aKi.js} +1 -1
- package/payload/server/public/assets/calendar-dRLyhghx.js +1 -0
- package/payload/server/public/assets/channel-B_-5bau1.js +1 -0
- package/payload/server/public/assets/chat-prZCVWvK.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-DBqAFwK9.js → chunk-2KRD3SAO-1KSJW1q6.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-DdnFEz6R.js → chunk-336JU56O-C3t08Ip-.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-Dirxdwjh.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-cnw-3Gbs.js → chunk-4BX2VUAB-kl1ze33n.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BnqJ0bDc.js → chunk-4TB4RGXK-DxpoDoHg.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-BwyHvvzo.js → chunk-55IACEB6-WM9VJzha.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DXA9U02I.js → chunk-5FUZZQ4R-CLH63ZH3.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-D78bZ_8D.js → chunk-5PVQY5BW-DCTb8IAT.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-CtE_Mgv7.js → chunk-67CJDMHE-BSYzyaoH.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-CUZDaySl.js → chunk-7N4EOEYR-CarJ7O2P.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-CYQ3jO4X.js → chunk-AA7GKIK3-DTNLyBpH.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-BbA_HyTR.js → chunk-BSJP7CBP-BFmHflHY.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-D7_YqRhG.js → chunk-CIAEETIT-BpjG0IzU.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-B23LrtSw.js → chunk-EDXVE4YY-2fSowhlW.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-D3fTHYKN.js → chunk-ENJZ2VHE-ClcranGV.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-DFAA2tyh.js → chunk-FMBD7UC4-DLq4BItQ.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-BF4IHvlI.js → chunk-FOC6F5B3-B9QI3Guz.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DBUGemHy.js → chunk-ICPOFSXX-BAr-Mv1Z.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-rMMs08Eh.js → chunk-K5T4RW27-8PvAHasY.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-BD2r8R2G.js → chunk-KGLVRYIC--aGaDRH5.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-D4sYTTwM.js → chunk-LIHQZDEY-BrWj7mw2.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-CAs_uFzb.js → chunk-ORNJ4GCN-kYQmkMTi.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-Fkp-2ORc.js → chunk-OYMX7WX6-CIsryCWH.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Duipo1kI.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-D_pxZh25.js → chunk-U2HBQHQK-68X653e3.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CeVmvvRf.js → chunk-X2U36JSP-v3v9soX3.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-iWP54fKp.js → chunk-XPW4576I-CW5DPpop.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CNtbWJNN.js → chunk-YZCP3GAM-B1VP7Rl2.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BmlFjU_9.js → chunk-ZZ45TVLE-DiJrujNY.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B3kX5SuB.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D761c7Lr.js +1 -0
- package/payload/server/public/assets/clone-BrhFv6Tb.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C7YmF1Pg.js → cose-bilkent-S5V4N54A-BscXxLMc.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-Ca3ESJhc.js → dagre-KV5264BT-DVgGlTQ1.js} +1 -1
- package/payload/server/public/assets/{dagre-BAmiDDtR.js → dagre-MBv6WlCS.js} +1 -1
- package/payload/server/public/assets/data-CeUT45cU.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-BgT1ORf2.js → diagram-5BDNPKRD-BmhCaO0a.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BTaUIiem.js → diagram-G4DWMVQ6-ChU9L8vz.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-Bdb2g6Uo.js → diagram-MMDJMWI5-Oy409Zc_.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CkLGC0zZ.js → diagram-TYMM5635-Cn1YByqk.js} +1 -1
- package/payload/server/public/assets/{dist-Bkbhs3jw.js → dist-B0CkUodR.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D_sj4DMg.js → erDiagram-SMLLAGMA-e17yfLvr.js} +1 -1
- package/payload/server/public/assets/{flatten-E2Sr0FeD.js → flatten-Cl1Bc3dR.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-iLERoZTe.js → flowDiagram-DWJPFMVM-Dqt7sCVZ.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-D60hjMBy.js → ganttDiagram-T4ZO3ILL-B4IhwS2r.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-BCQAqtLy.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di0XM6fi.js → gitGraphDiagram-UUTBAWPF-BDw-PKcI.js} +1 -1
- package/payload/server/public/assets/{graph-Bnsvbnkf.js → graph-B8h8YAJu.js} +3 -3
- package/payload/server/public/assets/graph-labels-BFrQ5kyH.js +1 -0
- package/payload/server/public/assets/{graphlib-C8fUP7uV.js → graphlib-DPIYk_sg.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-qU7hK50T.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-KGdcJeTs.js +2 -0
- package/payload/server/public/assets/{isEmpty-CLz-UprQ.js → isEmpty-D1pGOD1J.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CwgI4B5Z.js → ishikawaDiagram-UXIWVN3A-BRIHsB3G.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CxBu-tze.js → journeyDiagram-VCZTEJTY-aKNgmoHd.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-D2qlheA5.js → kanban-definition-6JOO6SKY-cwBwT-nm.js} +1 -1
- package/payload/server/public/assets/{line-Bdod7uzQ.js → line-DVtVRzUA.js} +1 -1
- package/payload/server/public/assets/{linear-BiVe6QzC.js → linear-DtoOFPO2.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-xZN7hC6M.js → mermaid-parser.core-G7YD_zO3.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-D0IylAac.js → mermaid.core-DpFyndHm.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-J_Szhwf1.js → mindmap-definition-QFDTVHPH-CYXK_Pvz.js} +1 -1
- package/payload/server/public/assets/operator-D3fRwMfE.js +1 -0
- package/payload/server/public/assets/{ordinal-5RtyPJVt.js → ordinal-CBZeH4Yp.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DiU2yS9m.js +1 -0
- package/payload/server/public/assets/page-D0hQWIIV.js +30 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-zHUKKlVv.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CV5DxUVx.js → pieDiagram-DEJITSTG-DFo4N6WH.js} +1 -1
- package/payload/server/public/assets/public-DX4z0sCJ.js +1 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BsFMVizF.js → quadrantDiagram-34T5L4WZ-BlH5iIhl.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-BUnVoaPg.js +1 -0
- package/payload/server/public/assets/{reduce-B3Mw_0s1.js → reduce-PuPlFPRX.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-C5eRRvGc.js → requirementDiagram-MS252O5E-D5WLty8A.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CJYv5gpw.js → sankeyDiagram-XADWPNL6-vB85IxHG.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B25563_2.js → sequenceDiagram-FGHM5R23-Cxraa0Ee.js} +1 -1
- package/payload/server/public/assets/{src-BKOAoFQc.js → src-CR_MU8uy.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BLyNJYOA.js → stateDiagram-FHFEXIEX-BBPssYBx.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BWmmUwDt.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D-HgOM6Q.js → timeline-definition-GMOUNBTQ-Bgk6PT62.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-C3Y-Q52h.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-Dk0qZpAg.js +1 -0
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Cw1JElYp.js → vennDiagram-DHZGUBPP-B-APTs3L.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-BAWI02OA.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BqMWzEko.js → wardleyDiagram-NUSXRM2D-CkXJSs_i.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBp71TLT.js → xychartDiagram-5P7HB3ND-BkyshPMV.js} +1 -1
- package/payload/server/public/brand/maxy-app-icon-192.png +0 -0
- package/payload/server/public/brand/maxy-app-icon-512.png +0 -0
- package/payload/server/public/brand/maxy-app-icon-maskable-512.png +0 -0
- package/payload/server/public/browser.html +6 -5
- package/payload/server/public/calendar.html +16 -0
- package/payload/server/public/chat.html +9 -8
- package/payload/server/public/data.html +6 -5
- package/payload/server/public/graph.html +8 -7
- package/payload/server/public/index.html +9 -8
- package/payload/server/public/operator.html +11 -10
- package/payload/server/public/public.html +9 -8
- package/payload/server/server.js +598 -190
- package/payload/server/public/assets/AdminLoginScreens-Brx8CmXN.js +0 -1
- package/payload/server/public/assets/AdminShell-CHZMDX2u.js +0 -1
- package/payload/server/public/assets/Checkbox-aePjWzRH.js +0 -1
- package/payload/server/public/assets/OperatorConversations-DpjPPIOp.js +0 -9
- package/payload/server/public/assets/admin-DIDvfti6.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-DN-MYxff.js +0 -1
- package/payload/server/public/assets/browser-Bp5kGgyr.js +0 -1
- package/payload/server/public/assets/channel-y27xl53N.js +0 -1
- package/payload/server/public/assets/chat-C0IWx7FL.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-D8Iwaegm.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-qHqDpwz3.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DgVoYWhO.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-C0WNP3uN.js +0 -1
- package/payload/server/public/assets/clone-D50F_pvg.js +0 -1
- package/payload/server/public/assets/data-RsMye_06.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-D-L2yzYf.js +0 -1
- package/payload/server/public/assets/graph-labels-jduMtwXb.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-BQn6jndO.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-BwMdbiXg.js +0 -2
- package/payload/server/public/assets/operator-9K-TElDd.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DmkQBx4h.js +0 -1
- package/payload/server/public/assets/page-BT9hkXHm.js +0 -30
- package/payload/server/public/assets/pie-ZZUOXDRM-DDnxYUBm.js +0 -1
- package/payload/server/public/assets/public-DvL1Zov1.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-7H4k0ChM.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BSVuxZxz.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-BLXWTQtW.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-DiePDF5d.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-OSUWK5LN.js +0 -1
- /package/payload/server/public/assets/{_baseFor-DVaNaTfF.js → _baseFor-brRjpUOX.js} +0 -0
- /package/payload/server/public/assets/{admin-types-CJrGd46U.js → admin-types-Dg11L4MQ.js} +0 -0
- /package/payload/server/public/assets/{array-8_H1156C.js → array-BGFCBI0e.js} +0 -0
- /package/payload/server/public/assets/{chunk-Pqm5yXtL.js → chunk-CAM3fms7.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-h10p_6j-.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-pD7tFa1r.js → defaultLocale-CRZydyG6.js} +0 -0
- /package/payload/server/public/assets/{init-CwJ4b81e.js → init-B8gtcn7T.js} +0 -0
- /package/payload/server/public/assets/{katex-DJPjIBAI.js → katex-RxgSu_dy.js} +0 -0
- /package/payload/server/public/assets/{path-CK8wrAxY.js → path-DZF-JdEe.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CQ36nxok.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-Dwml_la6.js → rough.esm-6CnTHTkH.js} +0 -0
package/package.json
CHANGED
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"logo": "maxy-square.png",
|
|
41
41
|
"icon": "maxy-monochrome.png",
|
|
42
42
|
"favicon": "favicon.ico",
|
|
43
|
-
"ogCard": "maxy-og-image.png"
|
|
43
|
+
"ogCard": "maxy-og-image.png",
|
|
44
|
+
"appIcon": "maxy-app-icon-512.png",
|
|
45
|
+
"appIcon192": "maxy-app-icon-192.png",
|
|
46
|
+
"maskableIcon": "maxy-app-icon-maskable-512.png"
|
|
44
47
|
},
|
|
45
48
|
|
|
46
49
|
"npm": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:fd3531bf43b1c57e87512398e56b517563f2a5b44b28a00250dceeab8a68a92c
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -1880,6 +1880,67 @@ A QuickBooks connection goes stale if it sits unused for about 100 days, or if y
|
|
|
1880
1880
|
|
|
1881
1881
|
Switching your Intuit app from sandbox to production requires passing Intuit's own production security review. That is between you and Intuit; it is not something the agent does for you.
|
|
1882
1882
|
|
|
1883
|
+
---
|
|
1884
|
+
# Calendar and Booking
|
|
1885
|
+
Source: https://docs.getmaxy.com/calendar-booking.md
|
|
1886
|
+
|
|
1887
|
+
# Calendar and booking
|
|
1888
|
+
|
|
1889
|
+
Maxy gives you a calendar of your meetings and a public page where people can book time with you. They are two sides of the same data: the meetings the calendar shows are the same meetings a booking creates.
|
|
1890
|
+
|
|
1891
|
+
## The admin calendar
|
|
1892
|
+
|
|
1893
|
+
Open `/calendar` on your Maxy address (the same sign-in as the dashboard). It shows your meetings in four views:
|
|
1894
|
+
|
|
1895
|
+
- **Day** and **Week** show an hour grid with each meeting as a timed block.
|
|
1896
|
+
- **Month** shows the familiar month grid with a few meetings per day.
|
|
1897
|
+
- **Year** shows twelve small months; days with a meeting carry a dot, and clicking a month opens it.
|
|
1898
|
+
|
|
1899
|
+
Use **Today** and the arrows to move around, and the view buttons to switch. The calendar reads your live meetings, so anything in your graph (imported from a calendar export, or created by a booking) appears here. The view is read-only: it shows meetings, it does not create or edit them.
|
|
1900
|
+
|
|
1901
|
+
The meetings come from the same `:Meeting` records the rest of Maxy uses, scoped to your account.
|
|
1902
|
+
|
|
1903
|
+
## The public booking page
|
|
1904
|
+
|
|
1905
|
+
The booking page is a separate, public site on its own web address (for example `book.yourdomain.com`). A visitor sees only open time slots, never the titles or details of your existing meetings. They pick a slot, enter their name, email, and an optional note, and submit. That is the whole flow, like a simple Calendly.
|
|
1906
|
+
|
|
1907
|
+
The page is hosted on Cloudflare so it keeps working and keeps taking bookings even if your Maxy device is briefly offline. Each submission is stored safely at the edge first, then turned into a meeting on your calendar shortly after.
|
|
1908
|
+
|
|
1909
|
+
## Putting the booking page live
|
|
1910
|
+
|
|
1911
|
+
Ask the admin to run the **calendar-site** skill ("put up a booking link" or "deploy the booking site"). It will ask for:
|
|
1912
|
+
|
|
1913
|
+
- your **time zone**,
|
|
1914
|
+
- the **meeting length** (for example 30 minutes),
|
|
1915
|
+
- a **buffer** between meetings (for example 10 minutes),
|
|
1916
|
+
- your **weekly availability** (which days and hours you take bookings),
|
|
1917
|
+
- the **web address** the booking page should use.
|
|
1918
|
+
|
|
1919
|
+
It writes those settings, builds the page, puts it live on that address, and checks that a test booking is captured. To change your availability later, run the same skill again with the new numbers.
|
|
1920
|
+
|
|
1921
|
+
## How a booking becomes a meeting
|
|
1922
|
+
|
|
1923
|
+
1. A visitor submits a booking on the public page.
|
|
1924
|
+
2. The booking is saved immediately at the edge (so it is never lost).
|
|
1925
|
+
3. Your Maxy device picks it up on a short cycle, creates a `:Meeting` for it, and links the visitor as a contact.
|
|
1926
|
+
4. You are notified by email (and by WhatsApp only if you already have a thread with the agent; it never starts a new WhatsApp conversation).
|
|
1927
|
+
5. The new meeting appears on your `/calendar`.
|
|
1928
|
+
|
|
1929
|
+
This usually completes within a couple of minutes of the booking.
|
|
1930
|
+
|
|
1931
|
+
## If a booking does not show up
|
|
1932
|
+
|
|
1933
|
+
Bookings are reconciled on a cycle and the system audits itself each run. In the device logs:
|
|
1934
|
+
|
|
1935
|
+
- `[calendar-booking]` lines trace one booking through submit, write, notify, and done.
|
|
1936
|
+
- `[calendar-reconcile]` lines report two health numbers per account: `pendingUnreconciled` (bookings captured but not yet turned into meetings) and `orphanMeetings` (booking meetings with no matching saved booking).
|
|
1937
|
+
|
|
1938
|
+
A `pendingUnreconciled` number that stays above zero across several runs means the reconcile step is stuck and is worth investigating. To read them: `journalctl --user -u <brand-service> | grep -E '\[calendar-booking\]|\[calendar-reconcile\]'`, then filter by one booking id for its full story. The edge half of a booking (before your device sees it) is in the Cloudflare Pages function logs.
|
|
1939
|
+
|
|
1940
|
+
## What this does not do
|
|
1941
|
+
|
|
1942
|
+
One meeting type only (one length, one weekly window, one buffer). No syncing to Google or iCloud calendars: your meetings in Maxy are the source of truth. No rescheduling or cancellation links, reminders, or visitor time-zone selection. The booking page shows availability live from your device, so while your device is offline the page can still take a booking but may not show fresh open times until it is back.
|
|
1943
|
+
|
|
1883
1944
|
---
|
|
1884
1945
|
# LinkedIn Extension
|
|
1885
1946
|
Source: https://docs.getmaxy.com/linkedin-extension.md
|
|
@@ -2650,6 +2711,32 @@ Two endpoints, two surfaces, two restart-survival roles:
|
|
|
2650
2711
|
string. Hosted on `maxy-edge.service` so the Software Update modal
|
|
2651
2712
|
can read it while the brand service is mid-restart.
|
|
2652
2713
|
|
|
2714
|
+
## Installable PWA
|
|
2715
|
+
|
|
2716
|
+
The admin (`index.html`), operator (`operator.html`), and chat (`chat.html`)
|
|
2717
|
+
shells are installable. Each links its own webmanifest (`/manifest-admin`,
|
|
2718
|
+
`/manifest-operator`, `/manifest-chat`) whose `icons` declare an explicit
|
|
2719
|
+
`192x192` and `512x512` `any` entry plus a `512x512` `maskable` entry, and an
|
|
2720
|
+
`apple-touch-icon`. These all point at an **opaque, padded** app icon baked on a
|
|
2721
|
+
white tile — never the transparent brand mark, because iOS/macOS composite a
|
|
2722
|
+
transparent app icon onto black. The opaque icons ship per brand
|
|
2723
|
+
(`assets.appIcon` / `appIcon192` / `maskableIcon`); `manifest`
|
|
2724
|
+
`background_color` only paints the splash screen, so the tile background is
|
|
2725
|
+
baked into the PNG. The maskable variant carries a wider safe zone so Android's
|
|
2726
|
+
circular crop never clips the mark.
|
|
2727
|
+
|
|
2728
|
+
The sign-in page is installable too. The remote-auth gate redirects every
|
|
2729
|
+
unauthenticated request to it, so a fresh device installs from there; the page
|
|
2730
|
+
links the admin manifest and the opaque `apple-touch-icon` so the install adopts
|
|
2731
|
+
the brand name and icon instead of a monogram generated from the title. The
|
|
2732
|
+
sign-in page registers no service worker — only the authenticated shells do.
|
|
2733
|
+
|
|
2734
|
+
A boot census logs `[pwa-census] surfaces=3 manifest-linked=3 icons-present=3`
|
|
2735
|
+
when every shell links its manifest and the app-icon files are present; a missing
|
|
2736
|
+
or mis-named icon asset shows `icons-present<3`, and a dropped head injection
|
|
2737
|
+
shows `manifest-linked<3`. The graph, data, and browser shells are intentionally
|
|
2738
|
+
non-installable.
|
|
2739
|
+
|
|
2653
2740
|
## Related references
|
|
2654
2741
|
|
|
2655
2742
|
- [`platform.md`](platform.md) — UI layout, session reconcile model,
|
|
@@ -32,6 +32,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
|
|
|
32
32
|
| Skill | Purpose |
|
|
33
33
|
|---|---|
|
|
34
34
|
| [cloudflare/SKILL.md](skills/cloudflare/SKILL.md) | The entry point for every Cloudflare operation. Routes each operation class to its reference, names the outcome contracts (tunnel external HTTP 200; hosting deployed URL; D1 round-trip), and states the tool-discipline + secret-redaction rules that bind the agent. |
|
|
35
|
+
| [site-deploy/SKILL.md](skills/site-deploy/SKILL.md) | Assemble one canonical content tree into a live Pages deployment on its custom domain that survives the install device being offline. Names the single-source-tree convention, the reused per-scope token, the form-to-D1 trigger, Open Graph correctness, and the cache-busted live-`200` done-gate; defers the command blocks to `hosting-sites.md` and `d1-data-capture.md`. Admin-run; a content specialist hands the deploy off to it. |
|
|
35
36
|
|
|
36
37
|
### References
|
|
37
38
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: calendar-site
|
|
3
|
+
description: Stand up a public "book time with me" page on a custom domain, backed by the account calendar. This is the skill behind "give me a booking link", "put up a Calendly-style page", "let people book a call with me", "deploy the booking site", and "set my availability for bookings". It writes the account availability window (one duration, one weekly window, one buffer), assembles the booking page plus its D1 capture into the canonical Pages tree, provisions the D1 bookings table, deploys to Cloudflare Pages via the site-deploy skill on the booking domain, and gates done on a live booking page whose open slots load from the brand tunnel and whose test submission lands in D1. The admin calendar that shows the resulting meetings is served separately at /calendar on the brand tunnel and needs no deploy. Admin-run; holds Bash and the account secrets surface.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stand up a calendar booking site
|
|
7
|
+
|
|
8
|
+
Run by the admin/main session, the surface that holds `Bash` and reads the account secrets file. This skill composes `cloudflare:site-deploy` for the Pages deploy and adds the two things a booking site needs that a plain landing page does not: the account availability config the platform reads to compute open slots, and a `bookings` D1 table written by the booking page rather than a contact form.
|
|
9
|
+
|
|
10
|
+
This is an outcome contract, not a copy of the Pages mechanics. The deploy, custom-domain attach, token discipline, and live done-gate are owned by `site-deploy`; read it with `plugin-read` before deploying. The D1 mechanics live in `references/d1-data-capture.md` and the token discipline in `references/api.md`; read those before issuing their commands.
|
|
11
|
+
|
|
12
|
+
## What this skill produces, and what it does not
|
|
13
|
+
|
|
14
|
+
- It produces the **public booking page** on a custom domain (the booking domain), backed by a D1 `bookings` table.
|
|
15
|
+
- It writes the **availability config** the platform free/busy endpoint reads.
|
|
16
|
+
- It does **not** deploy the admin calendar. The four-view admin calendar (Day, Week, Month, Year) is served by the install itself at `https://<brand-tunnel-domain>/calendar`, behind the existing admin sign-in. Point the operator there once the booking site is live; there is nothing to deploy for it.
|
|
17
|
+
|
|
18
|
+
## The two routes and why only one is deployed
|
|
19
|
+
|
|
20
|
+
A booking visitor and the admin look at the same `:Meeting` data from opposite sides:
|
|
21
|
+
|
|
22
|
+
| Surface | Where it lives | Gate | Reads |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| Admin calendar (4 views) | the install, at `/calendar` on the brand tunnel | admin sign-in | full `:Meeting` detail for the account |
|
|
25
|
+
| Public booking page | Cloudflare Pages, on the booking domain | open | free/busy only, never any meeting detail |
|
|
26
|
+
|
|
27
|
+
Only the public page is deployed here, because it must stay reachable and keep capturing bookings even when the install device is briefly offline. The admin calendar is dynamic, account-scoped, and authenticated, so it is served by the install over the tunnel and is same-origin with its data. Do not try to put the admin calendar on Pages.
|
|
28
|
+
|
|
29
|
+
## Set the availability window first
|
|
30
|
+
|
|
31
|
+
The public page shows open slots computed as the weekly window minus busy `:Meeting` intervals minus a buffer. Those inputs live in one file the platform reads:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
<accountDir>/calendar-availability.json
|
|
35
|
+
```
|
|
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.
|
|
38
|
+
|
|
39
|
+
## Assemble the canonical tree
|
|
40
|
+
|
|
41
|
+
Copy the skill template into the one canonical Pages source path `site-deploy` expects:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
<accountDir>/pages/<project>/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`. Two substitutions make it concrete: set the `<meta name="api-base">` content in `public/index.html` to the `apiBase` value (the booking page fetches free/busy cross-origin from the brand tunnel), 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
|
+
|
|
49
|
+
## Provision the D1 bookings table
|
|
50
|
+
|
|
51
|
+
This is a `bookings` table, not the `leads` table of a contact form, so the booking-specific schema ships with the template. Load the stable `<brand>-pages-d1` token from the account secrets file, minting it once only if absent and persisting it, exactly as `references/api.md` and `references/d1-data-capture.md` prescribe; the token needs both Pages Edit and D1 Edit. Create the database, record its id into `wrangler.toml`, and apply `schema.sql` to the remote database. The token is never written into the tree, never committed, never echoed.
|
|
52
|
+
|
|
53
|
+
## Deploy through site-deploy
|
|
54
|
+
|
|
55
|
+
Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a static tree, runs `wrangler pages deploy public --project-name <project> --branch=main`, attaches the booking domain (creating the DNS record with the `<brand>-dns` token when the attach leaves validation pending), and reuses the same per-scope token. Everything in `site-deploy`'s auth, custom-domain, and DNS guidance applies unchanged.
|
|
56
|
+
|
|
57
|
+
## Done-gate
|
|
58
|
+
|
|
59
|
+
Done is a live behavioural signal on the booking domain, surfaced verbatim:
|
|
60
|
+
|
|
61
|
+
- The booking domain returns `HTTP/2 200` on a cache-busted request.
|
|
62
|
+
- The page's free/busy call succeeds: `curl` the brand tunnel free/busy endpoint and confirm it returns a `slots` array and carries no meeting title, description, or location field. A `404` here means availability was not written; a blocked or unauthenticated response means the free/busy path is not reachable on the brand host.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl -s "https://<brand-tunnel-domain>/api/calendar/free-busy?from=$(date -u +%Y-%m-%dT00:00:00Z)&to=$(date -u -v+14d +%Y-%m-%dT00:00:00Z)" | head -c 400
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- A test submission lands in D1 unswept, reusing the same token and database name already loaded:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -sS -X POST -H 'content-type: application/json' \
|
|
72
|
+
-d '{"slotStart":"2026-01-01T09:00:00Z","slotEnd":"2026-01-01T09:30:00Z","name":"verify","email":"verify@example.com"}' \
|
|
73
|
+
"https://<booking-domain>/api/book" -i | head -1
|
|
74
|
+
CLOUDFLARE_API_TOKEN="${PAGES_D1}" wrangler d1 execute <db-name> --remote --command \
|
|
75
|
+
"SELECT bookingId FROM bookings WHERE swept = 0;"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The install reconciles accepted rows into `:Meeting` nodes on its own interval, so the test submission will appear in the admin calendar at `/calendar` within a couple of minutes and can be removed there or left as proof. Tell the operator the booking link (the booking domain) and the admin calendar link (`/calendar` on the brand tunnel) in the result.
|
|
79
|
+
|
|
80
|
+
## Tool discipline
|
|
81
|
+
|
|
82
|
+
The permitted surface is `wrangler`, `curl`, and the Cloudflare API via Bash following the cloudflare references, using the reused per-scope token. Do not mint a fresh token per deploy, do not drive the dashboard with a browser, do not synthesise `wrangler` flags from web search, and never write or echo a token. When a step fails, report the exact output with secrets redacted, name the failing URL or command, and stop.
|
package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/book.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Booking capture Pages Function (calendar-site skill).
|
|
2
|
+
//
|
|
3
|
+
// Served at POST /api/book on the booking domain. Writes the submission
|
|
4
|
+
// straight to D1 so capture survives a brief Pi outage — the platform reconcile
|
|
5
|
+
// pass turns accepted rows into Neo4j :Meeting nodes later. Emits the
|
|
6
|
+
// [calendar-booking] submit + d1-write lifeline (Cloudflare Pages captures
|
|
7
|
+
// console output).
|
|
8
|
+
//
|
|
9
|
+
// The handler is split so its logic is verifiable without the Pages runtime:
|
|
10
|
+
// processBooking takes an injected DB + logger and returns the HTTP shape.
|
|
11
|
+
|
|
12
|
+
interface D1Result {
|
|
13
|
+
meta?: { changes?: number }
|
|
14
|
+
}
|
|
15
|
+
interface D1PreparedStatement {
|
|
16
|
+
bind: (...values: unknown[]) => D1PreparedStatement
|
|
17
|
+
run: () => Promise<D1Result>
|
|
18
|
+
}
|
|
19
|
+
interface D1Database {
|
|
20
|
+
prepare: (query: string) => D1PreparedStatement
|
|
21
|
+
}
|
|
22
|
+
interface BookingEnv {
|
|
23
|
+
DB: D1Database
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface BookingBody {
|
|
27
|
+
slotStart?: unknown
|
|
28
|
+
slotEnd?: unknown
|
|
29
|
+
name?: unknown
|
|
30
|
+
email?: unknown
|
|
31
|
+
note?: unknown
|
|
32
|
+
company?: unknown // honeypot — real users never fill this
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Logger = (line: string) => void
|
|
36
|
+
|
|
37
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
38
|
+
return typeof v === 'string' && v.trim().length > 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Field caps so a malicious POST cannot store an unbounded blob in D1.
|
|
42
|
+
const MAX = { name: 200, email: 320, note: 2000, slot: 40 }
|
|
43
|
+
|
|
44
|
+
function isIsoTimestamp(v: unknown): v is string {
|
|
45
|
+
return typeof v === 'string' && v.length <= MAX.slot && !Number.isNaN(Date.parse(v))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function processBooking(
|
|
49
|
+
body: BookingBody,
|
|
50
|
+
env: BookingEnv,
|
|
51
|
+
log: Logger,
|
|
52
|
+
newId: () => string,
|
|
53
|
+
): Promise<{ status: number; payload: Record<string, unknown> }> {
|
|
54
|
+
// Honeypot: a populated `company` field means a bot. Return ok (so the bot
|
|
55
|
+
// sees success) but write nothing.
|
|
56
|
+
if (isNonEmptyString(body.company)) {
|
|
57
|
+
return { status: 200, payload: { ok: true } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
!isIsoTimestamp(body.slotStart) ||
|
|
62
|
+
!isIsoTimestamp(body.slotEnd) ||
|
|
63
|
+
!isNonEmptyString(body.name) ||
|
|
64
|
+
!isNonEmptyString(body.email)
|
|
65
|
+
) {
|
|
66
|
+
return { status: 400, payload: { ok: false, error: 'name, email, and ISO slotStart/slotEnd are required' } }
|
|
67
|
+
}
|
|
68
|
+
if (body.name.length > MAX.name || body.email.length > MAX.email) {
|
|
69
|
+
return { status: 400, payload: { ok: false, error: 'name or email too long' } }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bookingId = newId()
|
|
73
|
+
const note = isNonEmptyString(body.note) ? body.note.slice(0, MAX.note) : ''
|
|
74
|
+
const createdAt = new Date().toISOString()
|
|
75
|
+
log(`[calendar-booking] op=submit bookingId=${bookingId} slotStart=${body.slotStart} email=${body.email}`)
|
|
76
|
+
|
|
77
|
+
const result = await env.DB.prepare(
|
|
78
|
+
`INSERT INTO bookings (bookingId, slotStart, slotEnd, name, email, note, status, createdAt, swept)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, 'accepted', ?, 0)`,
|
|
80
|
+
)
|
|
81
|
+
.bind(bookingId, body.slotStart, body.slotEnd, body.name, body.email, note, createdAt)
|
|
82
|
+
.run()
|
|
83
|
+
|
|
84
|
+
const rowsWritten = result.meta?.changes ?? 0
|
|
85
|
+
log(`[calendar-booking] op=d1-write bookingId=${bookingId} rowsWritten=${rowsWritten}`)
|
|
86
|
+
|
|
87
|
+
if (rowsWritten < 1) {
|
|
88
|
+
return { status: 500, payload: { ok: false, error: 'capture failed' } }
|
|
89
|
+
}
|
|
90
|
+
return { status: 200, payload: { ok: true, bookingId } }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface PagesContext {
|
|
94
|
+
request: Request
|
|
95
|
+
env: BookingEnv
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function onRequestPost(context: PagesContext): Promise<Response> {
|
|
99
|
+
let body: BookingBody
|
|
100
|
+
try {
|
|
101
|
+
body = (await context.request.json()) as BookingBody
|
|
102
|
+
} catch {
|
|
103
|
+
return Response.json({ ok: false, error: 'invalid JSON' }, { status: 400 })
|
|
104
|
+
}
|
|
105
|
+
const { status, payload } = await processBooking(
|
|
106
|
+
body,
|
|
107
|
+
context.env,
|
|
108
|
+
(line) => console.log(line),
|
|
109
|
+
() => crypto.randomUUID(),
|
|
110
|
+
)
|
|
111
|
+
return Response.json(payload, { status })
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* Public booking page (calendar-site skill). Clean single-column Calendly-style
|
|
2
|
+
layout. Self-contained — no shared tokens, since this ships to Cloudflare
|
|
3
|
+
Pages, not the admin shell. The skill may overlay brand colors. */
|
|
4
|
+
:root {
|
|
5
|
+
--bk-bg: #f6f6f4;
|
|
6
|
+
--bk-card: #ffffff;
|
|
7
|
+
--bk-ink: #1d1d1f;
|
|
8
|
+
--bk-muted: #6b6b70;
|
|
9
|
+
--bk-line: #e3e3e0;
|
|
10
|
+
--bk-accent: #4b6358;
|
|
11
|
+
--bk-accent-ink: #ffffff;
|
|
12
|
+
--bk-radius: 12px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
background: var(--bk-bg);
|
|
20
|
+
color: var(--bk-ink);
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
22
|
+
display: flex;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
padding: 32px 16px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.bk-card {
|
|
28
|
+
width: 100%;
|
|
29
|
+
max-width: 520px;
|
|
30
|
+
background: var(--bk-card);
|
|
31
|
+
border: 1px solid var(--bk-line);
|
|
32
|
+
border-radius: var(--bk-radius);
|
|
33
|
+
padding: 28px;
|
|
34
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.bk-title { margin: 0 0 4px; font-size: 1.6rem; }
|
|
38
|
+
.bk-sub { margin: 0 0 20px; color: var(--bk-muted); }
|
|
39
|
+
.bk-status { margin: 8px 0; color: var(--bk-muted); }
|
|
40
|
+
|
|
41
|
+
.bk-day { margin-bottom: 20px; }
|
|
42
|
+
.bk-day-title { font-size: 0.95rem; margin: 0 0 8px; color: var(--bk-ink); }
|
|
43
|
+
.bk-day-slots { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
44
|
+
|
|
45
|
+
.bk-slot {
|
|
46
|
+
border: 1px solid var(--bk-accent);
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: var(--bk-accent);
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
padding: 8px 14px;
|
|
51
|
+
font-size: 0.9rem;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
.bk-slot:hover { background: var(--bk-accent); color: var(--bk-accent-ink); }
|
|
55
|
+
|
|
56
|
+
.bk-form { display: flex; flex-direction: column; gap: 14px; }
|
|
57
|
+
.bk-chosen { font-weight: 600; margin: 0 0 4px; }
|
|
58
|
+
.bk-label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--bk-muted); }
|
|
59
|
+
.bk-input {
|
|
60
|
+
border: 1px solid var(--bk-line);
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
padding: 10px 12px;
|
|
63
|
+
font-size: 1rem;
|
|
64
|
+
color: var(--bk-ink);
|
|
65
|
+
font-family: inherit;
|
|
66
|
+
}
|
|
67
|
+
.bk-input:focus { outline: 2px solid var(--bk-accent); border-color: var(--bk-accent); }
|
|
68
|
+
|
|
69
|
+
/* honeypot — visually and from the layout removed, still in the DOM for bots */
|
|
70
|
+
.bk-hp {
|
|
71
|
+
position: absolute;
|
|
72
|
+
left: -9999px;
|
|
73
|
+
width: 1px;
|
|
74
|
+
height: 1px;
|
|
75
|
+
opacity: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bk-actions { display: flex; justify-content: space-between; gap: 12px; margin-top: 4px; }
|
|
79
|
+
.bk-back {
|
|
80
|
+
background: transparent;
|
|
81
|
+
border: 1px solid var(--bk-line);
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
padding: 10px 16px;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
color: var(--bk-muted);
|
|
86
|
+
}
|
|
87
|
+
.bk-submit {
|
|
88
|
+
background: var(--bk-accent);
|
|
89
|
+
color: var(--bk-accent-ink);
|
|
90
|
+
border: none;
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
padding: 10px 20px;
|
|
93
|
+
font-size: 1rem;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
.bk-submit:disabled { opacity: 0.6; cursor: default; }
|
|
97
|
+
|
|
98
|
+
.bk-done { text-align: center; padding: 16px 0; }
|
|
99
|
+
.bk-done-title { margin: 0 0 8px; font-size: 1.3rem; }
|
|
100
|
+
.bk-done-msg { color: var(--bk-muted); margin: 0; }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Public booking page (calendar-site skill). Dependency-free.
|
|
2
|
+
// 1. Fetch open slots from the brand tunnel free/busy endpoint.
|
|
3
|
+
// 2. Let the visitor pick one, then collect name/email/note.
|
|
4
|
+
// 3. POST the booking to the same-origin Pages Function (/api/book → D1).
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
var DAYS_AHEAD = 14
|
|
9
|
+
var apiBaseMeta = document.querySelector('meta[name="api-base"]')
|
|
10
|
+
var apiBase = (apiBaseMeta && apiBaseMeta.getAttribute('content')) || ''
|
|
11
|
+
|
|
12
|
+
var statusEl = document.getElementById('bk-status')
|
|
13
|
+
var slotsEl = document.getElementById('bk-slots')
|
|
14
|
+
var formEl = document.getElementById('bk-form')
|
|
15
|
+
var chosenEl = document.getElementById('bk-chosen')
|
|
16
|
+
var doneEl = document.getElementById('bk-done')
|
|
17
|
+
var doneMsg = document.getElementById('bk-done-msg')
|
|
18
|
+
var durationEl = document.getElementById('bk-duration')
|
|
19
|
+
var backBtn = document.getElementById('bk-back')
|
|
20
|
+
|
|
21
|
+
var chosenSlot = null
|
|
22
|
+
|
|
23
|
+
function setStatus(msg) {
|
|
24
|
+
statusEl.textContent = msg || ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fmtDayHeading(d) {
|
|
28
|
+
return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
|
|
29
|
+
}
|
|
30
|
+
function fmtTime(d) {
|
|
31
|
+
return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderSlots(data) {
|
|
35
|
+
var slots = (data && data.slots) || []
|
|
36
|
+
if (data && data.durationMins) {
|
|
37
|
+
durationEl.textContent = data.durationMins + ' minute meeting'
|
|
38
|
+
}
|
|
39
|
+
if (slots.length === 0) {
|
|
40
|
+
setStatus('No open times in the next two weeks. Please check back later.')
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
setStatus('')
|
|
44
|
+
|
|
45
|
+
var byDay = {}
|
|
46
|
+
var order = []
|
|
47
|
+
slots.forEach(function (s) {
|
|
48
|
+
var start = new Date(s.start)
|
|
49
|
+
var key = start.toDateString()
|
|
50
|
+
if (!byDay[key]) {
|
|
51
|
+
byDay[key] = []
|
|
52
|
+
order.push(key)
|
|
53
|
+
}
|
|
54
|
+
byDay[key].push(s)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
order.forEach(function (key) {
|
|
58
|
+
var group = document.createElement('div')
|
|
59
|
+
group.className = 'bk-day'
|
|
60
|
+
var h = document.createElement('h2')
|
|
61
|
+
h.className = 'bk-day-title'
|
|
62
|
+
h.textContent = fmtDayHeading(new Date(key))
|
|
63
|
+
group.appendChild(h)
|
|
64
|
+
|
|
65
|
+
var row = document.createElement('div')
|
|
66
|
+
row.className = 'bk-day-slots'
|
|
67
|
+
byDay[key].forEach(function (s) {
|
|
68
|
+
var btn = document.createElement('button')
|
|
69
|
+
btn.type = 'button'
|
|
70
|
+
btn.className = 'bk-slot'
|
|
71
|
+
btn.textContent = fmtTime(new Date(s.start))
|
|
72
|
+
btn.addEventListener('click', function () {
|
|
73
|
+
chooseSlot(s)
|
|
74
|
+
})
|
|
75
|
+
row.appendChild(btn)
|
|
76
|
+
})
|
|
77
|
+
group.appendChild(row)
|
|
78
|
+
slotsEl.appendChild(group)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function chooseSlot(slot) {
|
|
83
|
+
chosenSlot = slot
|
|
84
|
+
var start = new Date(slot.start)
|
|
85
|
+
chosenEl.textContent = fmtDayHeading(start) + ' at ' + fmtTime(start)
|
|
86
|
+
slotsEl.hidden = true
|
|
87
|
+
formEl.hidden = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
backBtn.addEventListener('click', function () {
|
|
91
|
+
chosenSlot = null
|
|
92
|
+
formEl.hidden = true
|
|
93
|
+
slotsEl.hidden = false
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
formEl.addEventListener('submit', function (e) {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
if (!chosenSlot) return
|
|
99
|
+
var fd = new FormData(formEl)
|
|
100
|
+
var payload = {
|
|
101
|
+
slotStart: chosenSlot.start,
|
|
102
|
+
slotEnd: chosenSlot.end,
|
|
103
|
+
name: fd.get('name'),
|
|
104
|
+
email: fd.get('email'),
|
|
105
|
+
note: fd.get('note'),
|
|
106
|
+
company: fd.get('company'),
|
|
107
|
+
}
|
|
108
|
+
var submitBtn = formEl.querySelector('.bk-submit')
|
|
109
|
+
submitBtn.disabled = true
|
|
110
|
+
submitBtn.textContent = 'Booking…'
|
|
111
|
+
|
|
112
|
+
fetch('/api/book', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'content-type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(payload),
|
|
116
|
+
})
|
|
117
|
+
.then(function (res) {
|
|
118
|
+
return res.json().then(function (body) {
|
|
119
|
+
return { ok: res.ok, body: body }
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
.then(function (r) {
|
|
123
|
+
if (!r.ok || !r.body.ok) {
|
|
124
|
+
throw new Error((r.body && r.body.error) || 'Booking failed')
|
|
125
|
+
}
|
|
126
|
+
formEl.hidden = true
|
|
127
|
+
doneEl.hidden = false
|
|
128
|
+
var start = new Date(chosenSlot.start)
|
|
129
|
+
doneMsg.textContent = 'Confirmed for ' + fmtDayHeading(start) + ' at ' + fmtTime(start) + '. A confirmation will follow by email.'
|
|
130
|
+
})
|
|
131
|
+
.catch(function (err) {
|
|
132
|
+
submitBtn.disabled = false
|
|
133
|
+
submitBtn.textContent = 'Confirm booking'
|
|
134
|
+
setStatus('Sorry, that did not go through: ' + err.message + '. Please try again.')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function load() {
|
|
139
|
+
setStatus('Loading available times…')
|
|
140
|
+
var from = new Date()
|
|
141
|
+
var to = new Date(from.getTime() + DAYS_AHEAD * 24 * 60 * 60 * 1000)
|
|
142
|
+
var url = apiBase + '/api/calendar/free-busy?from=' + encodeURIComponent(from.toISOString()) + '&to=' + encodeURIComponent(to.toISOString())
|
|
143
|
+
fetch(url)
|
|
144
|
+
.then(function (res) {
|
|
145
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
146
|
+
return res.json()
|
|
147
|
+
})
|
|
148
|
+
.then(renderSlots)
|
|
149
|
+
.catch(function () {
|
|
150
|
+
setStatus('Could not load available times right now. Please refresh in a moment.')
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
load()
|
|
155
|
+
})()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<!-- The calendar-site skill rewrites the content of this tag to the brand
|
|
7
|
+
tunnel origin that serves /api/calendar/free-busy. -->
|
|
8
|
+
<meta name="api-base" content="__API_BASE__" />
|
|
9
|
+
<title>Book a time</title>
|
|
10
|
+
<link rel="stylesheet" href="/booking.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main class="bk-card">
|
|
14
|
+
<h1 class="bk-title">Book a time</h1>
|
|
15
|
+
<p class="bk-sub" id="bk-duration"></p>
|
|
16
|
+
|
|
17
|
+
<div id="bk-status" class="bk-status" role="status"></div>
|
|
18
|
+
|
|
19
|
+
<section id="bk-slots" class="bk-slots" aria-label="Available times"></section>
|
|
20
|
+
|
|
21
|
+
<form id="bk-form" class="bk-form" hidden>
|
|
22
|
+
<p class="bk-chosen" id="bk-chosen"></p>
|
|
23
|
+
<label class="bk-label">Name
|
|
24
|
+
<input class="bk-input" type="text" name="name" required autocomplete="name" />
|
|
25
|
+
</label>
|
|
26
|
+
<label class="bk-label">Email
|
|
27
|
+
<input class="bk-input" type="email" name="email" required autocomplete="email" />
|
|
28
|
+
</label>
|
|
29
|
+
<label class="bk-label">Note (optional)
|
|
30
|
+
<textarea class="bk-input" name="note" rows="3"></textarea>
|
|
31
|
+
</label>
|
|
32
|
+
<!-- honeypot: hidden from people, tempting to bots -->
|
|
33
|
+
<input class="bk-hp" type="text" name="company" tabindex="-1" autocomplete="off" aria-hidden="true" />
|
|
34
|
+
<div class="bk-actions">
|
|
35
|
+
<button class="bk-back" type="button" id="bk-back">Back</button>
|
|
36
|
+
<button class="bk-submit" type="submit">Confirm booking</button>
|
|
37
|
+
</div>
|
|
38
|
+
</form>
|
|
39
|
+
|
|
40
|
+
<div id="bk-done" class="bk-done" hidden>
|
|
41
|
+
<h2 class="bk-done-title">You are booked</h2>
|
|
42
|
+
<p id="bk-done-msg" class="bk-done-msg"></p>
|
|
43
|
+
</div>
|
|
44
|
+
</main>
|
|
45
|
+
<script src="/booking.js"></script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- D1 schema for the booking site (calendar-site skill).
|
|
2
|
+
-- One row per public booking submission. Captured at the edge so a submission
|
|
3
|
+
-- survives a brief Pi outage; the platform reconcile pass later reads accepted,
|
|
4
|
+
-- unswept rows and writes each into Neo4j as a :Meeting.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS bookings (
|
|
6
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
bookingId TEXT UNIQUE NOT NULL,
|
|
8
|
+
slotStart TEXT NOT NULL,
|
|
9
|
+
slotEnd TEXT NOT NULL,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
email TEXT NOT NULL,
|
|
12
|
+
note TEXT,
|
|
13
|
+
status TEXT NOT NULL DEFAULT 'accepted',
|
|
14
|
+
createdAt TEXT NOT NULL,
|
|
15
|
+
swept INTEGER NOT NULL DEFAULT 0
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS bookings_unswept ON bookings (status, swept);
|