@rubytech/create-maxy-code 0.1.342 → 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.
Files changed (188) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +4 -1
  3. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +88 -1
  4. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +82 -0
  5. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/book.ts +112 -0
  6. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +100 -0
  7. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +155 -0
  8. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +47 -0
  9. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +18 -0
  10. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/wrangler.toml +14 -0
  11. package/payload/platform/plugins/docs/PLUGIN.md +1 -0
  12. package/payload/platform/plugins/docs/references/admin-ui.md +26 -0
  13. package/payload/platform/plugins/docs/references/calendar-booking.md +56 -0
  14. package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts +2 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js +45 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js.map +1 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts +2 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts.map +1 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +89 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts +22 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts.map +1 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js +31 -0
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js.map +1 -0
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +28 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +70 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts +21 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts.map +1 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +280 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -0
  35. package/payload/server/{chunk-5BG6CHGH.js → chunk-QAKVFSEJ.js} +15 -0
  36. package/payload/server/maxy-edge.js +1 -1
  37. package/payload/server/public/assets/AdminLoginScreens-KVvngpT2.js +1 -0
  38. package/payload/server/public/assets/AdminShell-DDKZhJRo.js +1 -0
  39. package/payload/server/public/assets/Checkbox-BJlS0tR2.js +1 -0
  40. package/payload/server/public/assets/OperatorConversations-_P3_KmAW.js +1 -0
  41. package/payload/server/public/assets/admin-BTQWgciK.js +1 -0
  42. package/payload/server/public/assets/{OperatorConversations-BMIZQR9t.css → admin-auth-fetch-9JCQ2FFm.css} +1 -1
  43. package/payload/server/public/assets/admin-auth-fetch-BMOxrEnl.js +9 -0
  44. package/payload/server/public/assets/{arc-CxLB9WCE.js → arc-Crkz45LN.js} +1 -1
  45. package/payload/server/public/assets/architecture-YZFGNWBL-Dma4Bgha.js +1 -0
  46. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-D-EdJPSY.js → architectureDiagram-Q4EWVU46-C9JThHgP.js} +1 -1
  47. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CXx9RgCm.js → blockDiagram-DXYQGD6D-DpsV_HXT.js} +1 -1
  48. package/payload/server/public/assets/browser-CpOL9dN0.js +1 -0
  49. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-RFBXEe0B.js → c4Diagram-AHTNJAMY-CTm_6aKi.js} +1 -1
  50. package/payload/server/public/assets/calendar-dRLyhghx.js +1 -0
  51. package/payload/server/public/assets/channel-B_-5bau1.js +1 -0
  52. package/payload/server/public/assets/chat-prZCVWvK.js +1 -0
  53. package/payload/server/public/assets/{chunk-2KRD3SAO-DBqAFwK9.js → chunk-2KRD3SAO-1KSJW1q6.js} +1 -1
  54. package/payload/server/public/assets/{chunk-336JU56O-DdnFEz6R.js → chunk-336JU56O-C3t08Ip-.js} +2 -2
  55. package/payload/server/public/assets/chunk-426QAEUC-Dirxdwjh.js +1 -0
  56. package/payload/server/public/assets/{chunk-4BX2VUAB-cnw-3Gbs.js → chunk-4BX2VUAB-kl1ze33n.js} +1 -1
  57. package/payload/server/public/assets/{chunk-4TB4RGXK-BnqJ0bDc.js → chunk-4TB4RGXK-DxpoDoHg.js} +1 -1
  58. package/payload/server/public/assets/{chunk-55IACEB6-BwyHvvzo.js → chunk-55IACEB6-WM9VJzha.js} +1 -1
  59. package/payload/server/public/assets/{chunk-5FUZZQ4R-DXA9U02I.js → chunk-5FUZZQ4R-CLH63ZH3.js} +1 -1
  60. package/payload/server/public/assets/{chunk-5PVQY5BW-D78bZ_8D.js → chunk-5PVQY5BW-DCTb8IAT.js} +1 -1
  61. package/payload/server/public/assets/{chunk-67CJDMHE-CtE_Mgv7.js → chunk-67CJDMHE-BSYzyaoH.js} +1 -1
  62. package/payload/server/public/assets/{chunk-7N4EOEYR-CUZDaySl.js → chunk-7N4EOEYR-CarJ7O2P.js} +1 -1
  63. package/payload/server/public/assets/{chunk-AA7GKIK3-CYQ3jO4X.js → chunk-AA7GKIK3-DTNLyBpH.js} +1 -1
  64. package/payload/server/public/assets/{chunk-BSJP7CBP-BbA_HyTR.js → chunk-BSJP7CBP-BFmHflHY.js} +1 -1
  65. package/payload/server/public/assets/{chunk-CIAEETIT-D7_YqRhG.js → chunk-CIAEETIT-BpjG0IzU.js} +1 -1
  66. package/payload/server/public/assets/{chunk-EDXVE4YY-B23LrtSw.js → chunk-EDXVE4YY-2fSowhlW.js} +1 -1
  67. package/payload/server/public/assets/{chunk-ENJZ2VHE-D3fTHYKN.js → chunk-ENJZ2VHE-ClcranGV.js} +1 -1
  68. package/payload/server/public/assets/{chunk-FMBD7UC4-DFAA2tyh.js → chunk-FMBD7UC4-DLq4BItQ.js} +1 -1
  69. package/payload/server/public/assets/{chunk-FOC6F5B3-BF4IHvlI.js → chunk-FOC6F5B3-B9QI3Guz.js} +1 -1
  70. package/payload/server/public/assets/{chunk-ICPOFSXX-DBUGemHy.js → chunk-ICPOFSXX-BAr-Mv1Z.js} +2 -2
  71. package/payload/server/public/assets/{chunk-K5T4RW27-rMMs08Eh.js → chunk-K5T4RW27-8PvAHasY.js} +1 -1
  72. package/payload/server/public/assets/{chunk-KGLVRYIC-BD2r8R2G.js → chunk-KGLVRYIC--aGaDRH5.js} +1 -1
  73. package/payload/server/public/assets/{chunk-LIHQZDEY-D4sYTTwM.js → chunk-LIHQZDEY-BrWj7mw2.js} +1 -1
  74. package/payload/server/public/assets/{chunk-ORNJ4GCN-CAs_uFzb.js → chunk-ORNJ4GCN-kYQmkMTi.js} +1 -1
  75. package/payload/server/public/assets/{chunk-OYMX7WX6-Fkp-2ORc.js → chunk-OYMX7WX6-CIsryCWH.js} +1 -1
  76. package/payload/server/public/assets/chunk-QZHKN3VN-Duipo1kI.js +1 -0
  77. package/payload/server/public/assets/{chunk-U2HBQHQK-D_pxZh25.js → chunk-U2HBQHQK-68X653e3.js} +1 -1
  78. package/payload/server/public/assets/{chunk-X2U36JSP-CeVmvvRf.js → chunk-X2U36JSP-v3v9soX3.js} +1 -1
  79. package/payload/server/public/assets/{chunk-XPW4576I-iWP54fKp.js → chunk-XPW4576I-CW5DPpop.js} +1 -1
  80. package/payload/server/public/assets/{chunk-YZCP3GAM-CNtbWJNN.js → chunk-YZCP3GAM-B1VP7Rl2.js} +1 -1
  81. package/payload/server/public/assets/{chunk-ZZ45TVLE-BmlFjU_9.js → chunk-ZZ45TVLE-DiJrujNY.js} +1 -1
  82. package/payload/server/public/assets/classDiagram-6PBFFD2Q-B3kX5SuB.js +1 -0
  83. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D761c7Lr.js +1 -0
  84. package/payload/server/public/assets/clone-BrhFv6Tb.js +1 -0
  85. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C7YmF1Pg.js → cose-bilkent-S5V4N54A-BscXxLMc.js} +1 -1
  86. package/payload/server/public/assets/{dagre-KV5264BT-Ca3ESJhc.js → dagre-KV5264BT-DVgGlTQ1.js} +1 -1
  87. package/payload/server/public/assets/{dagre-BAmiDDtR.js → dagre-MBv6WlCS.js} +1 -1
  88. package/payload/server/public/assets/data-CeUT45cU.js +1 -0
  89. package/payload/server/public/assets/{diagram-5BDNPKRD-BgT1ORf2.js → diagram-5BDNPKRD-BmhCaO0a.js} +1 -1
  90. package/payload/server/public/assets/{diagram-G4DWMVQ6-BTaUIiem.js → diagram-G4DWMVQ6-ChU9L8vz.js} +1 -1
  91. package/payload/server/public/assets/{diagram-MMDJMWI5-Bdb2g6Uo.js → diagram-MMDJMWI5-Oy409Zc_.js} +1 -1
  92. package/payload/server/public/assets/{diagram-TYMM5635-CkLGC0zZ.js → diagram-TYMM5635-Cn1YByqk.js} +1 -1
  93. package/payload/server/public/assets/{dist-Bkbhs3jw.js → dist-B0CkUodR.js} +1 -1
  94. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D_sj4DMg.js → erDiagram-SMLLAGMA-e17yfLvr.js} +1 -1
  95. package/payload/server/public/assets/{flatten-E2Sr0FeD.js → flatten-Cl1Bc3dR.js} +1 -1
  96. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-iLERoZTe.js → flowDiagram-DWJPFMVM-Dqt7sCVZ.js} +1 -1
  97. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-D60hjMBy.js → ganttDiagram-T4ZO3ILL-B4IhwS2r.js} +1 -1
  98. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BCQAqtLy.js +1 -0
  99. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di0XM6fi.js → gitGraphDiagram-UUTBAWPF-BDw-PKcI.js} +1 -1
  100. package/payload/server/public/assets/{graph-Bnsvbnkf.js → graph-B8h8YAJu.js} +3 -3
  101. package/payload/server/public/assets/graph-labels-BFrQ5kyH.js +1 -0
  102. package/payload/server/public/assets/{graphlib-C8fUP7uV.js → graphlib-DPIYk_sg.js} +1 -1
  103. package/payload/server/public/assets/info-OMHHGYJF-qU7hK50T.js +1 -0
  104. package/payload/server/public/assets/infoDiagram-42DDH7IO-KGdcJeTs.js +2 -0
  105. package/payload/server/public/assets/{isEmpty-CLz-UprQ.js → isEmpty-D1pGOD1J.js} +1 -1
  106. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CwgI4B5Z.js → ishikawaDiagram-UXIWVN3A-BRIHsB3G.js} +1 -1
  107. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CxBu-tze.js → journeyDiagram-VCZTEJTY-aKNgmoHd.js} +1 -1
  108. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-D2qlheA5.js → kanban-definition-6JOO6SKY-cwBwT-nm.js} +1 -1
  109. package/payload/server/public/assets/{line-Bdod7uzQ.js → line-DVtVRzUA.js} +1 -1
  110. package/payload/server/public/assets/{linear-BiVe6QzC.js → linear-DtoOFPO2.js} +1 -1
  111. package/payload/server/public/assets/{mermaid-parser.core-xZN7hC6M.js → mermaid-parser.core-G7YD_zO3.js} +2 -2
  112. package/payload/server/public/assets/{mermaid.core-D0IylAac.js → mermaid.core-DpFyndHm.js} +3 -3
  113. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-J_Szhwf1.js → mindmap-definition-QFDTVHPH-CYXK_Pvz.js} +1 -1
  114. package/payload/server/public/assets/operator-D3fRwMfE.js +1 -0
  115. package/payload/server/public/assets/{ordinal-5RtyPJVt.js → ordinal-CBZeH4Yp.js} +1 -1
  116. package/payload/server/public/assets/packet-4T2RLAQJ-DiU2yS9m.js +1 -0
  117. package/payload/server/public/assets/page-D0hQWIIV.js +30 -0
  118. package/payload/server/public/assets/pie-ZZUOXDRM-zHUKKlVv.js +1 -0
  119. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CV5DxUVx.js → pieDiagram-DEJITSTG-DFo4N6WH.js} +1 -1
  120. package/payload/server/public/assets/public-DX4z0sCJ.js +1 -0
  121. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BsFMVizF.js → quadrantDiagram-34T5L4WZ-BlH5iIhl.js} +1 -1
  122. package/payload/server/public/assets/radar-PYXPWWZC-BUnVoaPg.js +1 -0
  123. package/payload/server/public/assets/{reduce-B3Mw_0s1.js → reduce-PuPlFPRX.js} +1 -1
  124. package/payload/server/public/assets/{requirementDiagram-MS252O5E-C5eRRvGc.js → requirementDiagram-MS252O5E-D5WLty8A.js} +1 -1
  125. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CJYv5gpw.js → sankeyDiagram-XADWPNL6-vB85IxHG.js} +1 -1
  126. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B25563_2.js → sequenceDiagram-FGHM5R23-Cxraa0Ee.js} +1 -1
  127. package/payload/server/public/assets/{src-BKOAoFQc.js → src-CR_MU8uy.js} +1 -1
  128. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BLyNJYOA.js → stateDiagram-FHFEXIEX-BBPssYBx.js} +1 -1
  129. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BWmmUwDt.js +1 -0
  130. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D-HgOM6Q.js → timeline-definition-GMOUNBTQ-Bgk6PT62.js} +1 -1
  131. package/payload/server/public/assets/treeView-SZITEDCU-C3Y-Q52h.js +1 -0
  132. package/payload/server/public/assets/treemap-W4RFUUIX-Dk0qZpAg.js +1 -0
  133. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Cw1JElYp.js → vennDiagram-DHZGUBPP-B-APTs3L.js} +1 -1
  134. package/payload/server/public/assets/wardley-RL74JXVD-BAWI02OA.js +1 -0
  135. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BqMWzEko.js → wardleyDiagram-NUSXRM2D-CkXJSs_i.js} +1 -1
  136. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBp71TLT.js → xychartDiagram-5P7HB3ND-BkyshPMV.js} +1 -1
  137. package/payload/server/public/brand/maxy-app-icon-192.png +0 -0
  138. package/payload/server/public/brand/maxy-app-icon-512.png +0 -0
  139. package/payload/server/public/brand/maxy-app-icon-maskable-512.png +0 -0
  140. package/payload/server/public/browser.html +6 -5
  141. package/payload/server/public/calendar.html +16 -0
  142. package/payload/server/public/chat.html +9 -8
  143. package/payload/server/public/data.html +6 -5
  144. package/payload/server/public/graph.html +8 -7
  145. package/payload/server/public/index.html +9 -8
  146. package/payload/server/public/operator.html +11 -10
  147. package/payload/server/public/public.html +9 -8
  148. package/payload/server/server.js +598 -190
  149. package/payload/server/public/assets/AdminLoginScreens-Brx8CmXN.js +0 -1
  150. package/payload/server/public/assets/AdminShell-CHZMDX2u.js +0 -1
  151. package/payload/server/public/assets/Checkbox-aePjWzRH.js +0 -1
  152. package/payload/server/public/assets/OperatorConversations-DpjPPIOp.js +0 -9
  153. package/payload/server/public/assets/admin-DIDvfti6.js +0 -1
  154. package/payload/server/public/assets/architecture-YZFGNWBL-DN-MYxff.js +0 -1
  155. package/payload/server/public/assets/browser-Bp5kGgyr.js +0 -1
  156. package/payload/server/public/assets/channel-y27xl53N.js +0 -1
  157. package/payload/server/public/assets/chat-C0IWx7FL.js +0 -1
  158. package/payload/server/public/assets/chunk-426QAEUC-D8Iwaegm.js +0 -1
  159. package/payload/server/public/assets/chunk-QZHKN3VN-qHqDpwz3.js +0 -1
  160. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DgVoYWhO.js +0 -1
  161. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-C0WNP3uN.js +0 -1
  162. package/payload/server/public/assets/clone-D50F_pvg.js +0 -1
  163. package/payload/server/public/assets/data-RsMye_06.js +0 -1
  164. package/payload/server/public/assets/gitGraph-7Q5UKJZL-D-L2yzYf.js +0 -1
  165. package/payload/server/public/assets/graph-labels-jduMtwXb.js +0 -1
  166. package/payload/server/public/assets/info-OMHHGYJF-BQn6jndO.js +0 -1
  167. package/payload/server/public/assets/infoDiagram-42DDH7IO-BwMdbiXg.js +0 -2
  168. package/payload/server/public/assets/operator-9K-TElDd.js +0 -1
  169. package/payload/server/public/assets/packet-4T2RLAQJ-DmkQBx4h.js +0 -1
  170. package/payload/server/public/assets/page-BT9hkXHm.js +0 -30
  171. package/payload/server/public/assets/pie-ZZUOXDRM-DDnxYUBm.js +0 -1
  172. package/payload/server/public/assets/public-DvL1Zov1.js +0 -1
  173. package/payload/server/public/assets/radar-PYXPWWZC-7H4k0ChM.js +0 -1
  174. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BSVuxZxz.js +0 -1
  175. package/payload/server/public/assets/treeView-SZITEDCU-BLXWTQtW.js +0 -1
  176. package/payload/server/public/assets/treemap-W4RFUUIX-DiePDF5d.js +0 -1
  177. package/payload/server/public/assets/wardley-RL74JXVD-OSUWK5LN.js +0 -1
  178. /package/payload/server/public/assets/{_baseFor-DVaNaTfF.js → _baseFor-brRjpUOX.js} +0 -0
  179. /package/payload/server/public/assets/{admin-types-CJrGd46U.js → admin-types-Dg11L4MQ.js} +0 -0
  180. /package/payload/server/public/assets/{array-8_H1156C.js → array-BGFCBI0e.js} +0 -0
  181. /package/payload/server/public/assets/{chunk-Pqm5yXtL.js → chunk-CAM3fms7.js} +0 -0
  182. /package/payload/server/public/assets/{cytoscape.esm-h10p_6j-.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
  183. /package/payload/server/public/assets/{defaultLocale-pD7tFa1r.js → defaultLocale-CRZydyG6.js} +0 -0
  184. /package/payload/server/public/assets/{init-CwJ4b81e.js → init-B8gtcn7T.js} +0 -0
  185. /package/payload/server/public/assets/{katex-DJPjIBAI.js → katex-RxgSu_dy.js} +0 -0
  186. /package/payload/server/public/assets/{path-CK8wrAxY.js → path-DZF-JdEe.js} +0 -0
  187. /package/payload/server/public/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CQ36nxok.js} +0 -0
  188. /package/payload/server/public/assets/{rough.esm-Dwml_la6.js → rough.esm-6CnTHTkH.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.342",
3
+ "version": "0.1.343",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -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:7276f9a571c11e5350ab9a29cdc60f6416f4f0b685f58679217efff1b53413e9
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,
@@ -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.
@@ -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);
@@ -0,0 +1,14 @@
1
+ # Cloudflare Pages config for the booking site (calendar-site skill).
2
+ # The skill fills the placeholders at assemble time:
3
+ # __PROJECT_NAME__ the Pages project name
4
+ # __D1_DATABASE_NAME__ the D1 database name
5
+ # __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
6
+ name = "__PROJECT_NAME__"
7
+ pages_build_output_dir = "public"
8
+ compatibility_date = "2024-01-01"
9
+ compatibility_flags = ["nodejs_compat"]
10
+
11
+ [[d1_databases]]
12
+ binding = "DB"
13
+ database_name = "__D1_DATABASE_NAME__"
14
+ database_id = "__D1_DATABASE_ID__"