@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.
Files changed (192) 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/PLUGIN.md +1 -0
  5. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +82 -0
  6. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/book.ts +112 -0
  7. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +100 -0
  8. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +155 -0
  9. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +47 -0
  10. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +18 -0
  11. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/wrangler.toml +14 -0
  12. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -0
  13. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +84 -0
  14. package/payload/platform/plugins/docs/PLUGIN.md +1 -0
  15. package/payload/platform/plugins/docs/references/admin-ui.md +26 -0
  16. package/payload/platform/plugins/docs/references/calendar-booking.md +56 -0
  17. package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts +2 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js +45 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts +2 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +89 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -0
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts +22 -0
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts.map +1 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js +31 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js.map +1 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +28 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +70 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts +21 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts.map +1 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +280 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -0
  38. package/payload/platform/templates/specialists/agents/content-producer.md +2 -0
  39. package/payload/server/{chunk-5BG6CHGH.js → chunk-QAKVFSEJ.js} +15 -0
  40. package/payload/server/maxy-edge.js +1 -1
  41. package/payload/server/public/assets/AdminLoginScreens-KVvngpT2.js +1 -0
  42. package/payload/server/public/assets/AdminShell-DDKZhJRo.js +1 -0
  43. package/payload/server/public/assets/Checkbox-BJlS0tR2.js +1 -0
  44. package/payload/server/public/assets/OperatorConversations-_P3_KmAW.js +1 -0
  45. package/payload/server/public/assets/admin-BTQWgciK.js +1 -0
  46. package/payload/server/public/assets/{OperatorConversations-BMIZQR9t.css → admin-auth-fetch-9JCQ2FFm.css} +1 -1
  47. package/payload/server/public/assets/admin-auth-fetch-BMOxrEnl.js +9 -0
  48. package/payload/server/public/assets/{arc-CxLB9WCE.js → arc-Crkz45LN.js} +1 -1
  49. package/payload/server/public/assets/architecture-YZFGNWBL-Dma4Bgha.js +1 -0
  50. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-D-EdJPSY.js → architectureDiagram-Q4EWVU46-C9JThHgP.js} +1 -1
  51. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CXx9RgCm.js → blockDiagram-DXYQGD6D-DpsV_HXT.js} +1 -1
  52. package/payload/server/public/assets/browser-CpOL9dN0.js +1 -0
  53. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-RFBXEe0B.js → c4Diagram-AHTNJAMY-CTm_6aKi.js} +1 -1
  54. package/payload/server/public/assets/calendar-dRLyhghx.js +1 -0
  55. package/payload/server/public/assets/channel-B_-5bau1.js +1 -0
  56. package/payload/server/public/assets/chat-prZCVWvK.js +1 -0
  57. package/payload/server/public/assets/{chunk-2KRD3SAO-DBqAFwK9.js → chunk-2KRD3SAO-1KSJW1q6.js} +1 -1
  58. package/payload/server/public/assets/{chunk-336JU56O-DdnFEz6R.js → chunk-336JU56O-C3t08Ip-.js} +2 -2
  59. package/payload/server/public/assets/chunk-426QAEUC-Dirxdwjh.js +1 -0
  60. package/payload/server/public/assets/{chunk-4BX2VUAB-cnw-3Gbs.js → chunk-4BX2VUAB-kl1ze33n.js} +1 -1
  61. package/payload/server/public/assets/{chunk-4TB4RGXK-BnqJ0bDc.js → chunk-4TB4RGXK-DxpoDoHg.js} +1 -1
  62. package/payload/server/public/assets/{chunk-55IACEB6-BwyHvvzo.js → chunk-55IACEB6-WM9VJzha.js} +1 -1
  63. package/payload/server/public/assets/{chunk-5FUZZQ4R-DXA9U02I.js → chunk-5FUZZQ4R-CLH63ZH3.js} +1 -1
  64. package/payload/server/public/assets/{chunk-5PVQY5BW-D78bZ_8D.js → chunk-5PVQY5BW-DCTb8IAT.js} +1 -1
  65. package/payload/server/public/assets/{chunk-67CJDMHE-CtE_Mgv7.js → chunk-67CJDMHE-BSYzyaoH.js} +1 -1
  66. package/payload/server/public/assets/{chunk-7N4EOEYR-CUZDaySl.js → chunk-7N4EOEYR-CarJ7O2P.js} +1 -1
  67. package/payload/server/public/assets/{chunk-AA7GKIK3-CYQ3jO4X.js → chunk-AA7GKIK3-DTNLyBpH.js} +1 -1
  68. package/payload/server/public/assets/{chunk-BSJP7CBP-BbA_HyTR.js → chunk-BSJP7CBP-BFmHflHY.js} +1 -1
  69. package/payload/server/public/assets/{chunk-CIAEETIT-D7_YqRhG.js → chunk-CIAEETIT-BpjG0IzU.js} +1 -1
  70. package/payload/server/public/assets/{chunk-EDXVE4YY-B23LrtSw.js → chunk-EDXVE4YY-2fSowhlW.js} +1 -1
  71. package/payload/server/public/assets/{chunk-ENJZ2VHE-D3fTHYKN.js → chunk-ENJZ2VHE-ClcranGV.js} +1 -1
  72. package/payload/server/public/assets/{chunk-FMBD7UC4-DFAA2tyh.js → chunk-FMBD7UC4-DLq4BItQ.js} +1 -1
  73. package/payload/server/public/assets/{chunk-FOC6F5B3-BF4IHvlI.js → chunk-FOC6F5B3-B9QI3Guz.js} +1 -1
  74. package/payload/server/public/assets/{chunk-ICPOFSXX-DBUGemHy.js → chunk-ICPOFSXX-BAr-Mv1Z.js} +2 -2
  75. package/payload/server/public/assets/{chunk-K5T4RW27-rMMs08Eh.js → chunk-K5T4RW27-8PvAHasY.js} +1 -1
  76. package/payload/server/public/assets/{chunk-KGLVRYIC-BD2r8R2G.js → chunk-KGLVRYIC--aGaDRH5.js} +1 -1
  77. package/payload/server/public/assets/{chunk-LIHQZDEY-D4sYTTwM.js → chunk-LIHQZDEY-BrWj7mw2.js} +1 -1
  78. package/payload/server/public/assets/{chunk-ORNJ4GCN-CAs_uFzb.js → chunk-ORNJ4GCN-kYQmkMTi.js} +1 -1
  79. package/payload/server/public/assets/{chunk-OYMX7WX6-Fkp-2ORc.js → chunk-OYMX7WX6-CIsryCWH.js} +1 -1
  80. package/payload/server/public/assets/chunk-QZHKN3VN-Duipo1kI.js +1 -0
  81. package/payload/server/public/assets/{chunk-U2HBQHQK-D_pxZh25.js → chunk-U2HBQHQK-68X653e3.js} +1 -1
  82. package/payload/server/public/assets/{chunk-X2U36JSP-CeVmvvRf.js → chunk-X2U36JSP-v3v9soX3.js} +1 -1
  83. package/payload/server/public/assets/{chunk-XPW4576I-iWP54fKp.js → chunk-XPW4576I-CW5DPpop.js} +1 -1
  84. package/payload/server/public/assets/{chunk-YZCP3GAM-CNtbWJNN.js → chunk-YZCP3GAM-B1VP7Rl2.js} +1 -1
  85. package/payload/server/public/assets/{chunk-ZZ45TVLE-BmlFjU_9.js → chunk-ZZ45TVLE-DiJrujNY.js} +1 -1
  86. package/payload/server/public/assets/classDiagram-6PBFFD2Q-B3kX5SuB.js +1 -0
  87. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D761c7Lr.js +1 -0
  88. package/payload/server/public/assets/clone-BrhFv6Tb.js +1 -0
  89. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C7YmF1Pg.js → cose-bilkent-S5V4N54A-BscXxLMc.js} +1 -1
  90. package/payload/server/public/assets/{dagre-KV5264BT-Ca3ESJhc.js → dagre-KV5264BT-DVgGlTQ1.js} +1 -1
  91. package/payload/server/public/assets/{dagre-BAmiDDtR.js → dagre-MBv6WlCS.js} +1 -1
  92. package/payload/server/public/assets/data-CeUT45cU.js +1 -0
  93. package/payload/server/public/assets/{diagram-5BDNPKRD-BgT1ORf2.js → diagram-5BDNPKRD-BmhCaO0a.js} +1 -1
  94. package/payload/server/public/assets/{diagram-G4DWMVQ6-BTaUIiem.js → diagram-G4DWMVQ6-ChU9L8vz.js} +1 -1
  95. package/payload/server/public/assets/{diagram-MMDJMWI5-Bdb2g6Uo.js → diagram-MMDJMWI5-Oy409Zc_.js} +1 -1
  96. package/payload/server/public/assets/{diagram-TYMM5635-CkLGC0zZ.js → diagram-TYMM5635-Cn1YByqk.js} +1 -1
  97. package/payload/server/public/assets/{dist-Bkbhs3jw.js → dist-B0CkUodR.js} +1 -1
  98. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D_sj4DMg.js → erDiagram-SMLLAGMA-e17yfLvr.js} +1 -1
  99. package/payload/server/public/assets/{flatten-E2Sr0FeD.js → flatten-Cl1Bc3dR.js} +1 -1
  100. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-iLERoZTe.js → flowDiagram-DWJPFMVM-Dqt7sCVZ.js} +1 -1
  101. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-D60hjMBy.js → ganttDiagram-T4ZO3ILL-B4IhwS2r.js} +1 -1
  102. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BCQAqtLy.js +1 -0
  103. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di0XM6fi.js → gitGraphDiagram-UUTBAWPF-BDw-PKcI.js} +1 -1
  104. package/payload/server/public/assets/{graph-Bnsvbnkf.js → graph-B8h8YAJu.js} +3 -3
  105. package/payload/server/public/assets/graph-labels-BFrQ5kyH.js +1 -0
  106. package/payload/server/public/assets/{graphlib-C8fUP7uV.js → graphlib-DPIYk_sg.js} +1 -1
  107. package/payload/server/public/assets/info-OMHHGYJF-qU7hK50T.js +1 -0
  108. package/payload/server/public/assets/infoDiagram-42DDH7IO-KGdcJeTs.js +2 -0
  109. package/payload/server/public/assets/{isEmpty-CLz-UprQ.js → isEmpty-D1pGOD1J.js} +1 -1
  110. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CwgI4B5Z.js → ishikawaDiagram-UXIWVN3A-BRIHsB3G.js} +1 -1
  111. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CxBu-tze.js → journeyDiagram-VCZTEJTY-aKNgmoHd.js} +1 -1
  112. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-D2qlheA5.js → kanban-definition-6JOO6SKY-cwBwT-nm.js} +1 -1
  113. package/payload/server/public/assets/{line-Bdod7uzQ.js → line-DVtVRzUA.js} +1 -1
  114. package/payload/server/public/assets/{linear-BiVe6QzC.js → linear-DtoOFPO2.js} +1 -1
  115. package/payload/server/public/assets/{mermaid-parser.core-xZN7hC6M.js → mermaid-parser.core-G7YD_zO3.js} +2 -2
  116. package/payload/server/public/assets/{mermaid.core-D0IylAac.js → mermaid.core-DpFyndHm.js} +3 -3
  117. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-J_Szhwf1.js → mindmap-definition-QFDTVHPH-CYXK_Pvz.js} +1 -1
  118. package/payload/server/public/assets/operator-D3fRwMfE.js +1 -0
  119. package/payload/server/public/assets/{ordinal-5RtyPJVt.js → ordinal-CBZeH4Yp.js} +1 -1
  120. package/payload/server/public/assets/packet-4T2RLAQJ-DiU2yS9m.js +1 -0
  121. package/payload/server/public/assets/page-D0hQWIIV.js +30 -0
  122. package/payload/server/public/assets/pie-ZZUOXDRM-zHUKKlVv.js +1 -0
  123. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CV5DxUVx.js → pieDiagram-DEJITSTG-DFo4N6WH.js} +1 -1
  124. package/payload/server/public/assets/public-DX4z0sCJ.js +1 -0
  125. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BsFMVizF.js → quadrantDiagram-34T5L4WZ-BlH5iIhl.js} +1 -1
  126. package/payload/server/public/assets/radar-PYXPWWZC-BUnVoaPg.js +1 -0
  127. package/payload/server/public/assets/{reduce-B3Mw_0s1.js → reduce-PuPlFPRX.js} +1 -1
  128. package/payload/server/public/assets/{requirementDiagram-MS252O5E-C5eRRvGc.js → requirementDiagram-MS252O5E-D5WLty8A.js} +1 -1
  129. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CJYv5gpw.js → sankeyDiagram-XADWPNL6-vB85IxHG.js} +1 -1
  130. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B25563_2.js → sequenceDiagram-FGHM5R23-Cxraa0Ee.js} +1 -1
  131. package/payload/server/public/assets/{src-BKOAoFQc.js → src-CR_MU8uy.js} +1 -1
  132. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BLyNJYOA.js → stateDiagram-FHFEXIEX-BBPssYBx.js} +1 -1
  133. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BWmmUwDt.js +1 -0
  134. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D-HgOM6Q.js → timeline-definition-GMOUNBTQ-Bgk6PT62.js} +1 -1
  135. package/payload/server/public/assets/treeView-SZITEDCU-C3Y-Q52h.js +1 -0
  136. package/payload/server/public/assets/treemap-W4RFUUIX-Dk0qZpAg.js +1 -0
  137. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Cw1JElYp.js → vennDiagram-DHZGUBPP-B-APTs3L.js} +1 -1
  138. package/payload/server/public/assets/wardley-RL74JXVD-BAWI02OA.js +1 -0
  139. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BqMWzEko.js → wardleyDiagram-NUSXRM2D-CkXJSs_i.js} +1 -1
  140. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBp71TLT.js → xychartDiagram-5P7HB3ND-BkyshPMV.js} +1 -1
  141. package/payload/server/public/brand/maxy-app-icon-192.png +0 -0
  142. package/payload/server/public/brand/maxy-app-icon-512.png +0 -0
  143. package/payload/server/public/brand/maxy-app-icon-maskable-512.png +0 -0
  144. package/payload/server/public/browser.html +6 -5
  145. package/payload/server/public/calendar.html +16 -0
  146. package/payload/server/public/chat.html +9 -8
  147. package/payload/server/public/data.html +6 -5
  148. package/payload/server/public/graph.html +8 -7
  149. package/payload/server/public/index.html +9 -8
  150. package/payload/server/public/operator.html +11 -10
  151. package/payload/server/public/public.html +9 -8
  152. package/payload/server/server.js +598 -190
  153. package/payload/server/public/assets/AdminLoginScreens-Brx8CmXN.js +0 -1
  154. package/payload/server/public/assets/AdminShell-CHZMDX2u.js +0 -1
  155. package/payload/server/public/assets/Checkbox-aePjWzRH.js +0 -1
  156. package/payload/server/public/assets/OperatorConversations-DpjPPIOp.js +0 -9
  157. package/payload/server/public/assets/admin-DIDvfti6.js +0 -1
  158. package/payload/server/public/assets/architecture-YZFGNWBL-DN-MYxff.js +0 -1
  159. package/payload/server/public/assets/browser-Bp5kGgyr.js +0 -1
  160. package/payload/server/public/assets/channel-y27xl53N.js +0 -1
  161. package/payload/server/public/assets/chat-C0IWx7FL.js +0 -1
  162. package/payload/server/public/assets/chunk-426QAEUC-D8Iwaegm.js +0 -1
  163. package/payload/server/public/assets/chunk-QZHKN3VN-qHqDpwz3.js +0 -1
  164. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DgVoYWhO.js +0 -1
  165. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-C0WNP3uN.js +0 -1
  166. package/payload/server/public/assets/clone-D50F_pvg.js +0 -1
  167. package/payload/server/public/assets/data-RsMye_06.js +0 -1
  168. package/payload/server/public/assets/gitGraph-7Q5UKJZL-D-L2yzYf.js +0 -1
  169. package/payload/server/public/assets/graph-labels-jduMtwXb.js +0 -1
  170. package/payload/server/public/assets/info-OMHHGYJF-BQn6jndO.js +0 -1
  171. package/payload/server/public/assets/infoDiagram-42DDH7IO-BwMdbiXg.js +0 -2
  172. package/payload/server/public/assets/operator-9K-TElDd.js +0 -1
  173. package/payload/server/public/assets/packet-4T2RLAQJ-DmkQBx4h.js +0 -1
  174. package/payload/server/public/assets/page-BT9hkXHm.js +0 -30
  175. package/payload/server/public/assets/pie-ZZUOXDRM-DDnxYUBm.js +0 -1
  176. package/payload/server/public/assets/public-DvL1Zov1.js +0 -1
  177. package/payload/server/public/assets/radar-PYXPWWZC-7H4k0ChM.js +0 -1
  178. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BSVuxZxz.js +0 -1
  179. package/payload/server/public/assets/treeView-SZITEDCU-BLXWTQtW.js +0 -1
  180. package/payload/server/public/assets/treemap-W4RFUUIX-DiePDF5d.js +0 -1
  181. package/payload/server/public/assets/wardley-RL74JXVD-OSUWK5LN.js +0 -1
  182. /package/payload/server/public/assets/{_baseFor-DVaNaTfF.js → _baseFor-brRjpUOX.js} +0 -0
  183. /package/payload/server/public/assets/{admin-types-CJrGd46U.js → admin-types-Dg11L4MQ.js} +0 -0
  184. /package/payload/server/public/assets/{array-8_H1156C.js → array-BGFCBI0e.js} +0 -0
  185. /package/payload/server/public/assets/{chunk-Pqm5yXtL.js → chunk-CAM3fms7.js} +0 -0
  186. /package/payload/server/public/assets/{cytoscape.esm-h10p_6j-.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
  187. /package/payload/server/public/assets/{defaultLocale-pD7tFa1r.js → defaultLocale-CRZydyG6.js} +0 -0
  188. /package/payload/server/public/assets/{init-CwJ4b81e.js → init-B8gtcn7T.js} +0 -0
  189. /package/payload/server/public/assets/{katex-DJPjIBAI.js → katex-RxgSu_dy.js} +0 -0
  190. /package/payload/server/public/assets/{path-CK8wrAxY.js → path-DZF-JdEe.js} +0 -0
  191. /package/payload/server/public/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CQ36nxok.js} +0 -0
  192. /package/payload/server/public/assets/{rough.esm-Dwml_la6.js → rough.esm-6CnTHTkH.js} +0 -0
@@ -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__"
@@ -16,6 +16,7 @@ This is the entry point for every Cloudflare task on the install. Pick the opera
16
16
  | Tear down corrupt tunnel state and start clean | `references/reset-guide.md` | a fresh `manual-setup.md` run reaches `200` |
17
17
  | Call the Cloudflare API (DNS, zones, tunnels, Access, token list/reuse/revoke) | `references/api.md` | the API call returns success and the change is observable |
18
18
  | Deploy a static / Next.js site to Cloudflare Pages | `references/hosting-sites.md` | the deployed URL serves the new build |
19
+ | Take an approved content tree to a live site on its custom domain (canonical source, OG, optional D1 form, live verify) | `Skill site-deploy` | cache-busted `200` on the custom domain with correct content + `og:*`; a form site records a test POST to D1 |
19
20
  | Capture form/waitlist submissions into a database | `references/d1-data-capture.md` | a test POST appears in `SELECT … WHERE swept = 0` |
20
21
  | Diagnose / enable Web Analytics on a site (0 visitors, no data) | `references/web-analytics.md` | beacon present in live HTML (`curl -s https://<host>/ \| grep cloudflareinsights`) |
21
22
 
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: site-deploy
3
+ description: Assemble an approved content tree into a live Cloudflare Pages deployment on its custom domain that survives the install device being offline. This is the skill behind "deploy this site", "put this landing page / investor site / microsite live on its own domain", "publish the waitlist site", and "the live site is stale / shows the wrong brand". It takes one canonical source tree, reuses the per-scope Pages-and-D1 token from the account secrets file, runs `wrangler pages deploy` to the production alias, attaches the custom domain, wires a contact or waitlist form to D1 when the tree carries one, and gates "done" on a cache-busted live `200` with correct content and Open Graph metadata on the production domain. Admin-run; it holds Bash and the account secrets surface. Distinct from `admin:publish-site` (device-tunnel hosting that dies when the device goes offline) and from `sitedesk:microsite` (single-page builder that stops before deploy).
4
+ ---
5
+
6
+ # Deploy a site to Cloudflare Pages
7
+
8
+ Run by the admin/main session, the operator-infra surface that holds the `Bash` tool and reads the account secrets file. A content specialist authors and approves the site tree; the deploy itself is admin work because it touches the Cloudflare account credentials. When a `content-producer` finishes a site, it hands the deploy off to admin, which loads this skill.
9
+
10
+ This skill is an outcome contract, not a second copy of the Pages mechanics. The mechanics live in the cloudflare references; this skill names the canonical source convention, the auth discipline, the form-to-D1 wiring trigger, the Open Graph correctness check, and the live done-gate, then defers each command block to the matching reference. Read `references/hosting-sites.md` (build, `wrangler pages deploy`, custom domain) and `references/d1-data-capture.md` (form to Pages Function to D1) via `plugin-read` before issuing those commands; read `references/api.md` for the token discipline.
11
+
12
+ ## When this skill, not another
13
+
14
+ | Situation | Skill |
15
+ |---|---|
16
+ | A standalone site (landing, investor, waitlist, microsite output) goes live on its own custom domain, hosted on Cloudflare's edge, surviving the install device being offline | **this skill** |
17
+ | Serve content that the install device itself renders, at a custom domain over the brand tunnel | `admin:publish-site` (device-tunnel `/sites/` path) |
18
+ | Build a single-page site from a brief | `sitedesk:microsite` (it stops before deploy; its output tree is then deployed by this skill) |
19
+
20
+ `admin:publish-site` serves from the install device through the brand tunnel: when the device is offline the site is unreachable. This skill puts the site on Cloudflare Pages, so it stays up independent of the device. Name the distinction when the operator's intent is ambiguous, and pick by whether the site must survive the device going offline.
21
+
22
+ ## One canonical source tree
23
+
24
+ Each site is driven by exactly one source tree at:
25
+
26
+ ```
27
+ <accountDir>/pages/<project>/
28
+ ```
29
+
30
+ `<project>` is the slug of the site's graph node (the `:Organization` or site entity the site represents), so there is one deterministic path per site and no second copy to reconcile. The tree holds the framework source, `wrangler.toml`, the built output directory, and `functions/api/contact.ts` when the site captures form data.
31
+
32
+ There is never a `sites/<brand>/` versus `output/<brand>-site/` pair, a `.bak` clone, or a stale sibling. If more than one candidate tree exists when you start, reconcile to the single canonical path first: confirm which tree is current, move it to `<accountDir>/pages/<project>/`, and remove the others before deploying. Deploying from an unreconciled tree is the root cause of the dev/live drift this skill exists to prevent. `<accountDir>/pages/` is distinct from `<accountDir>/sites/`, which belongs to the device-tunnel `admin:publish-site` path; the two never share a directory.
33
+
34
+ ## Auth: reuse the per-scope token, never mint ad hoc
35
+
36
+ A Pages deploy needs **Account · Cloudflare Pages · Edit**. A site with a D1-backed form also needs **Account · D1 · Edit** on the same token. 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` § Provisioning and reusing a stable per-scope token and `references/d1-data-capture.md` § 0 prescribe. The token is never written into the project tree, never committed, never echoed into chat. The master token stays in the secrets file. Surface every command as verb plus target ("deploying Pages project `<project>`"); `wrangler` prints the deployment URL, which is what gets relayed.
37
+
38
+ ## Deploy and attach the custom domain
39
+
40
+ Follow `references/hosting-sites.md`: build the framework output, write `wrangler.toml` (the project name is `<project>`, the output dir is the framework default), and `wrangler pages deploy <output-dir> --project-name <project> --branch=main` to the production alias. Attach the custom domain to the Pages project via the Cloudflare API with the reused `<brand>-pages-d1` token (the operator clicks **Workers & Pages → project → Custom domains** in the dashboard only when they prefer to do it by hand), so the site answers on the operator's domain, not only on `<project>.pages.dev`.
41
+
42
+ Attaching the custom domain to the project is not the same as the domain resolving. The attach leaves validation **pending** until a DNS record in the zone points the hostname at the Pages site; until that record exists the domain returns `NXDOMAIN` and the page is unreachable. Confirm the record exists and create it when it does not:
43
+
44
+ - A **subdomain** custom domain (`investors.example.com`) needs a `CNAME` to `<project>.pages.dev`.
45
+ - An **apex** custom domain (`example.com`) cannot hold a literal CNAME, so it needs a **proxied (flattened) CNAME** at the apex to `<project>.pages.dev`, plus a `www` CNAME if `www` is also served. This is the case the attach most often leaves pending.
46
+
47
+ Creating that DNS record is a **DNS-scope** operation, not a Pages-scope one: the `<brand>-pages-d1` token returns a `10000 Authentication error` on the DNS endpoint. Use the DNS-scoped `<brand>-dns` token, exactly as `cloudflare/SKILL.md` § Apex hostnames and `references/api.md` § DNS records prescribe, minting it once if absent. The hostname's zone must be on the same Cloudflare account; a hostname whose zone lives in a different account is reached with that account's own `<brand>-dns` token, never by probing a drawer of ad-hoc `CF_DNS_TOKEN_*` strays (that token sprawl is what the `references/api.md` reconcile pass exists to clear). The custom domain is the surface the done-gate checks, so a missing record fails the gate rather than passing silently.
48
+
49
+ ## Wire the form to D1 when one exists
50
+
51
+ When the canonical tree carries a contact or waitlist form, the deploy is not complete until that form records to D1. Follow `references/d1-data-capture.md`: create the database, add the `[[d1_databases]]` binding to `wrangler.toml`, create the table, and ship `functions/api/contact.ts`. The token from the auth step already carries D1 Edit, so the form does not silently 500 at the insert. A site with no form skips this section.
52
+
53
+ ## Open Graph correctness
54
+
55
+ The live page must carry brand-correct Open Graph metadata: `<meta property="og:title">`, `og:description`, `og:image`, and `og:url` reflecting this site and this brand, not a leftover from whatever the tree was cloned from. Stale OG (a live card still naming a previous brand) is a recorded failure mode of hand deployment. Confirm the `og:*` tags in the deployed HTML are correct before claiming done; if they are stale, fix them in the source tree, redeploy, and re-check.
56
+
57
+ ## Done-gate: a live cache-busted 200 on the custom domain
58
+
59
+ "Done" is never "I ran deploy". It is a live behavioural signal on the **production custom domain**, surfaced verbatim in chat:
60
+
61
+ ```bash
62
+ curl -s "https://<custom-domain>/?cb=$(date +%s)" -D - -o /tmp/site.html | head -1
63
+ grep -o '<meta property="og:[^>]*>' /tmp/site.html
64
+ ```
65
+
66
+ The done-gate passes only when all hold:
67
+
68
+ - the domain resolves at all: a `NXDOMAIN` or "could not resolve host" means the custom-domain DNS record was never created, so go back to the custom-domain step and create the CNAME with the `<brand>-dns` token; a `522` is the certificate-provisioning window, so wait and re-curl rather than treating it as done,
69
+ - the status line is `HTTP/2 200` (or `HTTP/1.1 200 OK`),
70
+ - the body carries an expected content marker for this site (a brand string or headline you can name in advance), not a stale marker from a previous brand,
71
+ - the `og:*` tags are present and brand-correct,
72
+ - for a form-bearing site, a test POST appears in D1 unswept, per `references/d1-data-capture.md` § Outcome contract. This sub-check reuses the same `${PAGES_D1}` token and `<db-name>` already loaded when the form was wired, not a fresh token:
73
+
74
+ ```bash
75
+ curl -sS -X POST -d "email=test@example.com&name=verify" "https://<custom-domain>/api/contact" -i | head -1
76
+ CLOUDFLARE_API_TOKEN="${PAGES_D1}" wrangler d1 execute <db-name> --remote --command \
77
+ "SELECT email FROM leads WHERE swept = 0;"
78
+ ```
79
+
80
+ Any other status, a missing or stale content marker, an OG mismatch, or a form POST that does not land in D1 blocks "done" and is reported with the exact failing URL. Record the deployment alias and the cache-busted verification command in the result so a later operator re-runs the same check against the custom domain to confirm live state without reading source.
81
+
82
+ ## Tool discipline
83
+
84
+ The permitted surface is `wrangler`, `curl`, and the Cloudflare API via Bash following the cloudflare references, using the reused per-scope token. The agent does not mint a fresh token per deploy, does not drive the dashboard with Playwright or Chrome DevTools, does not synthesise `wrangler` flags from web search, and never writes or echoes a token. When a step fails, report the exact output with secrets redacted, name the failing URL or command, and stop.
@@ -25,6 +25,7 @@ Load these when users ask about Maxy features or need guidance:
25
25
  - **Access Control** → `references/access-control.md` — who can chat with your public agent, invitations, authentication
26
26
  - **Projects** → `references/projects-guide.md` — creating projects, tracking health, phase transitions, completing
27
27
  - **Slides** → `references/slides.md` — generating, theming, and reviewing HTML slide decks with `/slides`
28
+ - **Calendar & booking** → `references/calendar-booking.md` — the admin calendar at `/calendar`, deploying a public booking page, setting availability, and how bookings become meetings
28
29
  - **Troubleshooting** → `references/troubleshooting.md` — common issues and how to resolve them
29
30
 
30
31
  ## Platform Reference
@@ -385,6 +385,32 @@ Two endpoints, two surfaces, two restart-survival roles:
385
385
  string. Hosted on `maxy-edge.service` so the Software Update modal
386
386
  can read it while the brand service is mid-restart.
387
387
 
388
+ ## Installable PWA
389
+
390
+ The admin (`index.html`), operator (`operator.html`), and chat (`chat.html`)
391
+ shells are installable. Each links its own webmanifest (`/manifest-admin`,
392
+ `/manifest-operator`, `/manifest-chat`) whose `icons` declare an explicit
393
+ `192x192` and `512x512` `any` entry plus a `512x512` `maskable` entry, and an
394
+ `apple-touch-icon`. These all point at an **opaque, padded** app icon baked on a
395
+ white tile — never the transparent brand mark, because iOS/macOS composite a
396
+ transparent app icon onto black. The opaque icons ship per brand
397
+ (`assets.appIcon` / `appIcon192` / `maskableIcon`); `manifest`
398
+ `background_color` only paints the splash screen, so the tile background is
399
+ baked into the PNG. The maskable variant carries a wider safe zone so Android's
400
+ circular crop never clips the mark.
401
+
402
+ The sign-in page is installable too. The remote-auth gate redirects every
403
+ unauthenticated request to it, so a fresh device installs from there; the page
404
+ links the admin manifest and the opaque `apple-touch-icon` so the install adopts
405
+ the brand name and icon instead of a monogram generated from the title. The
406
+ sign-in page registers no service worker — only the authenticated shells do.
407
+
408
+ A boot census logs `[pwa-census] surfaces=3 manifest-linked=3 icons-present=3`
409
+ when every shell links its manifest and the app-icon files are present; a missing
410
+ or mis-named icon asset shows `icons-present<3`, and a dropped head injection
411
+ shows `manifest-linked<3`. The graph, data, and browser shells are intentionally
412
+ non-installable.
413
+
388
414
  ## Related references
389
415
 
390
416
  - [`platform.md`](platform.md) — UI layout, session reconcile model,
@@ -0,0 +1,56 @@
1
+ # Calendar and booking
2
+
3
+ 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.
4
+
5
+ ## The admin calendar
6
+
7
+ Open `/calendar` on your Maxy address (the same sign-in as the dashboard). It shows your meetings in four views:
8
+
9
+ - **Day** and **Week** show an hour grid with each meeting as a timed block.
10
+ - **Month** shows the familiar month grid with a few meetings per day.
11
+ - **Year** shows twelve small months; days with a meeting carry a dot, and clicking a month opens it.
12
+
13
+ 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.
14
+
15
+ The meetings come from the same `:Meeting` records the rest of Maxy uses, scoped to your account.
16
+
17
+ ## The public booking page
18
+
19
+ 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.
20
+
21
+ 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.
22
+
23
+ ## Putting the booking page live
24
+
25
+ Ask the admin to run the **calendar-site** skill ("put up a booking link" or "deploy the booking site"). It will ask for:
26
+
27
+ - your **time zone**,
28
+ - the **meeting length** (for example 30 minutes),
29
+ - a **buffer** between meetings (for example 10 minutes),
30
+ - your **weekly availability** (which days and hours you take bookings),
31
+ - the **web address** the booking page should use.
32
+
33
+ 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.
34
+
35
+ ## How a booking becomes a meeting
36
+
37
+ 1. A visitor submits a booking on the public page.
38
+ 2. The booking is saved immediately at the edge (so it is never lost).
39
+ 3. Your Maxy device picks it up on a short cycle, creates a `:Meeting` for it, and links the visitor as a contact.
40
+ 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).
41
+ 5. The new meeting appears on your `/calendar`.
42
+
43
+ This usually completes within a couple of minutes of the booking.
44
+
45
+ ## If a booking does not show up
46
+
47
+ Bookings are reconciled on a cycle and the system audits itself each run. In the device logs:
48
+
49
+ - `[calendar-booking]` lines trace one booking through submit, write, notify, and done.
50
+ - `[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).
51
+
52
+ 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.
53
+
54
+ ## What this does not do
55
+
56
+ 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.
@@ -102,6 +102,16 @@ Dispatch results are recorded on the Event node (`lastDispatchResult`, `lastDisp
102
102
 
103
103
  Users and plugins both create events with actions via the same `schedule-event` interface — the mechanism is symmetric.
104
104
 
105
+ ## Reconciling public bookings (D1 → graph)
106
+
107
+ `scheduling/mcp/dist/scripts/reconcile-bookings.js` turns public bookings into calendar meetings. The booking site (deployed by the cloudflare `calendar-site` skill) writes each submission to a Cloudflare D1 `bookings` table at the edge, so capture survives a brief Pi outage. This binary pulls accepted, unswept rows from each account's D1, writes one `:Meeting {source:'booking', uid:'booking-<id>'}` per row (idempotent — a re-run writes no duplicate), links the booker as a `:Person` via `[:ATTENDED]` (respecting the global email-uniqueness skip), notifies the admin (email; WhatsApp only when a thread already exists, never cold), then marks the row swept.
108
+
109
+ It is **armed by the always-running `platform/ui` server on an interval**, because the Pi has no other periodic surface (the same gap `check-due-events` has). A PID lock at `<configDir>/reconcile-bookings.lock` prevents overlapping runs. It registers **no agent tool** — there is nothing for the agent to call — so the tool-surface registries are untouched.
110
+
111
+ Env: `PLATFORM_ROOT`, `NEO4J_URI` (and `NEO4J_PASSWORD` or `config/.neo4j-password`). Per account it reads `data/accounts/<id>/calendar-availability.json` (for `bookingDbName`) and `data/accounts/<id>/secrets/cloudflare.env` (for the Pages-and-D1 token), and shells `wrangler` for the D1 reads/writes.
112
+
113
+ Observability: one `[calendar-booking]` line per step keyed by `bookingId` (`reconcile-claim`, `meeting-write`, `notify`, `booked`), and one `[calendar-reconcile] account=… pendingUnreconciled=… orphanMeetings=…` audit line per account per run. A `pendingUnreconciled` that persists across runs means reconcile is stalled; `orphanMeetings` means a booking meeting has no backing D1 row.
114
+
105
115
  ## Ingesting calendar archives (.ics → graph)
106
116
 
107
117
  `schedule-archive-ics` is the bulk-ingest path: it reads an `.ics` export from any calendar app (Google, Apple, Outlook, Fastmail, etc.) and writes one `:Meeting` per VEVENT into the graph, with attendees resolved to `:Person` by lowercased email. Distinct from `schedule-import-ics`, which only parses + returns events for the operator to recreate via `schedule-event` one at a time.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=booking-notify.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-notify.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-notify.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ // Booking notify dispatch (Task 1012). Email always attempted when configured;
2
+ // failures captured not thrown; WhatsApp only with dispatcher + known thread.
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import { notifyBooking } from "../booking-notify.js";
5
+ const booking = {
6
+ bookingId: "abc",
7
+ slotStart: "2026-06-22T09:00:00Z",
8
+ slotEnd: "2026-06-22T09:30:00Z",
9
+ name: "Jo",
10
+ email: "jo@example.com",
11
+ };
12
+ describe("notifyBooking", () => {
13
+ it("dispatches email and returns dispatched=true", async () => {
14
+ const sendEmail = vi.fn(async () => { });
15
+ const r = await notifyBooking({ adminEmail: "admin@x.com", sendEmail }, booking);
16
+ expect(sendEmail).toHaveBeenCalledOnce();
17
+ expect(r).toEqual([{ channel: "email", dispatched: true }]);
18
+ });
19
+ it("captures an email failure as dispatched=false, never throws", async () => {
20
+ const sendEmail = vi.fn(async () => {
21
+ throw new Error("smtp down");
22
+ });
23
+ const r = await notifyBooking({ adminEmail: "admin@x.com", sendEmail }, booking);
24
+ expect(r).toEqual([{ channel: "email", dispatched: false }]);
25
+ });
26
+ it("adds a WhatsApp result only when a dispatcher and thread are supplied", async () => {
27
+ const sendEmail = vi.fn(async () => { });
28
+ const sendWhatsapp = vi.fn(async () => { });
29
+ const r = await notifyBooking({ adminEmail: "admin@x.com", sendEmail, adminWhatsapp: "+440000", sendWhatsapp }, booking);
30
+ expect(sendWhatsapp).toHaveBeenCalledOnce();
31
+ expect(r).toEqual([
32
+ { channel: "email", dispatched: true },
33
+ { channel: "whatsapp", dispatched: true },
34
+ ]);
35
+ });
36
+ it("never sends WhatsApp without a known thread (cold-send ban)", async () => {
37
+ const sendEmail = vi.fn(async () => { });
38
+ const sendWhatsapp = vi.fn(async () => { });
39
+ // dispatcher present but no adminWhatsapp thread -> WhatsApp must not fire.
40
+ const r = await notifyBooking({ adminEmail: "admin@x.com", sendEmail, sendWhatsapp }, booking);
41
+ expect(sendWhatsapp).not.toHaveBeenCalled();
42
+ expect(r).toEqual([{ channel: "email", dispatched: true }]);
43
+ });
44
+ });
45
+ //# sourceMappingURL=booking-notify.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-notify.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-notify.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=booking-reconcile.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-reconcile.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-reconcile.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,89 @@
1
+ // Booking reconcile core (Task 1012). The :Meeting MERGE is keyed on the
2
+ // booking uid (real MERGE idempotency is a live-Neo4j property); these tests
3
+ // pin the contract the JS owns: correct uid/source on the write, the
4
+ // global-email-uniqueness branch (same-account link / cross-account skip / new
5
+ // stub), and the audit arithmetic.
6
+ import { describe, it, expect, vi } from "vitest";
7
+ import { reconcileOne, auditCounts, bookingUid } from "../booking-reconcile.js";
8
+ const booking = {
9
+ bookingId: "abc",
10
+ slotStart: "2026-06-22T09:00:00Z",
11
+ slotEnd: "2026-06-22T09:30:00Z",
12
+ name: "Jo Visitor",
13
+ email: "jo@example.com",
14
+ note: "hello",
15
+ };
16
+ // Fake session that routes each run() by the cypher it sees. The booker link
17
+ // is a single MERGE (p:Person {email}) whose returned `linked` is true when the
18
+ // resolved Person belongs to this account (a new node, or an existing
19
+ // same-account one) and false for a foreign-account node.
20
+ function makeSession(opts) {
21
+ const cyphers = [];
22
+ const params = [];
23
+ const run = vi.fn(async (cypher, p) => {
24
+ cyphers.push(cypher);
25
+ params.push(p);
26
+ if (cypher.includes("MERGE (m:Meeting")) {
27
+ return { records: [{ get: (k) => (k === "created" ? opts.created : "el-meeting-1") }] };
28
+ }
29
+ if (cypher.includes("MERGE (p:Person")) {
30
+ // undefined owner = freshly created under this account.
31
+ const linked = opts.personOwner === undefined || opts.personOwner === p.accountId;
32
+ return { records: [{ get: () => linked }] };
33
+ }
34
+ return { records: [] };
35
+ });
36
+ return { session: { run }, run, cyphers, params };
37
+ }
38
+ describe("reconcileOne", () => {
39
+ it("writes the booking meeting with the right uid + source and links a new booker", async () => {
40
+ const { session, cyphers, params } = makeSession({ created: true, personOwner: undefined });
41
+ const r = await reconcileOne(session, "acct-1", booking);
42
+ expect(r.created).toBe(true);
43
+ expect(r.meetingId).toBe("el-meeting-1");
44
+ expect(r.personLinked).toBe(true);
45
+ expect(r.crossAccountSkipped).toBe(false);
46
+ // uid + source asserted on the meeting MERGE params/cypher.
47
+ expect(params[0].uid).toBe(bookingUid("abc"));
48
+ expect(cyphers[0]).toMatch(/m\.source = 'booking'/);
49
+ // booker link is one atomic MERGE on the unique email, not a check-then-create.
50
+ expect(cyphers.some((c) => c.includes("MERGE (p:Person {email: $email})"))).toBe(true);
51
+ expect(cyphers.some((c) => c.includes("CREATE (p:Person"))).toBe(false);
52
+ });
53
+ it("reports created=false on the idempotent re-run", async () => {
54
+ const { session } = makeSession({ created: false, personOwner: "acct-1" });
55
+ const r = await reconcileOne(session, "acct-1", booking);
56
+ expect(r.created).toBe(false);
57
+ });
58
+ it("links a same-account existing person via ATTENDED", async () => {
59
+ const { session, cyphers } = makeSession({ created: true, personOwner: "acct-1" });
60
+ const r = await reconcileOne(session, "acct-1", booking);
61
+ expect(r.personLinked).toBe(true);
62
+ expect(r.crossAccountSkipped).toBe(false);
63
+ expect(cyphers.some((c) => c.includes("MERGE (p)-[r:ATTENDED]->(m)"))).toBe(true);
64
+ });
65
+ it("skips a cross-account person but still lands the meeting", async () => {
66
+ const { session } = makeSession({ created: true, personOwner: "other-account" });
67
+ const r = await reconcileOne(session, "acct-1", booking);
68
+ expect(r.created).toBe(true);
69
+ expect(r.personLinked).toBe(false);
70
+ expect(r.crossAccountSkipped).toBe(true);
71
+ });
72
+ });
73
+ describe("auditCounts", () => {
74
+ it("counts pending (D1 ahead) and orphans (graph ahead)", async () => {
75
+ const run = vi.fn(async () => ({
76
+ records: [{ get: () => ["booking-a", "booking-b"] }],
77
+ }));
78
+ const counts = await auditCounts({ run }, "acct-1", ["a", "c"]);
79
+ // 'c' accepted in D1 but no meeting -> pending 1; 'booking-b' meeting with no D1 row -> orphan 1.
80
+ expect(counts.pendingUnreconciled).toBe(1);
81
+ expect(counts.orphanMeetings).toBe(1);
82
+ });
83
+ it("is clean when D1 and graph agree", async () => {
84
+ const run = vi.fn(async () => ({ records: [{ get: () => ["booking-a", "booking-b"] }] }));
85
+ const counts = await auditCounts({ run }, "acct-1", ["a", "b"]);
86
+ expect(counts).toEqual({ pendingUnreconciled: 0, orphanMeetings: 0 });
87
+ });
88
+ });
89
+ //# sourceMappingURL=booking-reconcile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-reconcile.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/booking-reconcile.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { BookingRow } from "./booking-reconcile.js";
2
+ export interface NotifyDeps {
3
+ /** Admin destination address; email is skipped when absent. */
4
+ adminEmail?: string;
5
+ sendEmail?: (msg: {
6
+ to: string;
7
+ subject: string;
8
+ body: string;
9
+ }) => Promise<void>;
10
+ /** Admin WhatsApp thread; set ONLY when a thread already exists (reply-only). */
11
+ adminWhatsapp?: string;
12
+ sendWhatsapp?: (msg: {
13
+ to: string;
14
+ text: string;
15
+ }) => Promise<void>;
16
+ }
17
+ export interface NotifyResult {
18
+ channel: "email" | "whatsapp";
19
+ dispatched: boolean;
20
+ }
21
+ export declare function notifyBooking(deps: NotifyDeps, booking: BookingRow): Promise<NotifyResult[]>;
22
+ //# sourceMappingURL=booking-notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-notify.d.ts","sourceRoot":"","sources":["../../src/lib/booking-notify.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,GAAG,UAAU,CAAC;IAC9B,UAAU,EAAE,OAAO,CAAC;CACrB;AAOD,wBAAsB,aAAa,CACjC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BzB"}
@@ -0,0 +1,31 @@
1
+ function summary(booking) {
2
+ return `${booking.name} (${booking.email}) booked ${booking.slotStart}.` +
3
+ (booking.note ? ` Note: ${booking.note}` : "");
4
+ }
5
+ export async function notifyBooking(deps, booking) {
6
+ const results = [];
7
+ if (deps.adminEmail && deps.sendEmail) {
8
+ try {
9
+ await deps.sendEmail({
10
+ to: deps.adminEmail,
11
+ subject: `New booking: ${booking.name}`,
12
+ body: summary(booking),
13
+ });
14
+ results.push({ channel: "email", dispatched: true });
15
+ }
16
+ catch {
17
+ results.push({ channel: "email", dispatched: false });
18
+ }
19
+ }
20
+ if (deps.adminWhatsapp && deps.sendWhatsapp) {
21
+ try {
22
+ await deps.sendWhatsapp({ to: deps.adminWhatsapp, text: summary(booking) });
23
+ results.push({ channel: "whatsapp", dispatched: true });
24
+ }
25
+ catch {
26
+ results.push({ channel: "whatsapp", dispatched: false });
27
+ }
28
+ }
29
+ return results;
30
+ }
31
+ //# sourceMappingURL=booking-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-notify.js","sourceRoot":"","sources":["../../src/lib/booking-notify.ts"],"names":[],"mappings":"AAwBA,SAAS,OAAO,CAAC,OAAmB;IAClC,OAAO,GAAG,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,KAAK,YAAY,OAAO,CAAC,SAAS,GAAG;QACtE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAgB,EAChB,OAAmB;IAEnB,MAAM,OAAO,GAAmB,EAAE,CAAC;IAEnC,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,SAAS,CAAC;gBACnB,EAAE,EAAE,IAAI,CAAC,UAAU;gBACnB,OAAO,EAAE,gBAAgB,OAAO,CAAC,IAAI,EAAE;gBACvC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC;aACvB,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QAC5C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC5E,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Session } from "neo4j-driver";
2
+ export interface BookingRow {
3
+ bookingId: string;
4
+ slotStart: string;
5
+ slotEnd: string;
6
+ name: string;
7
+ email: string;
8
+ note?: string;
9
+ }
10
+ export interface ReconcileResult {
11
+ created: boolean;
12
+ meetingId: string;
13
+ personLinked: boolean;
14
+ crossAccountSkipped: boolean;
15
+ }
16
+ export declare function bookingUid(bookingId: string): string;
17
+ export declare function reconcileOne(session: Session, accountId: string, booking: BookingRow): Promise<ReconcileResult>;
18
+ export interface AuditCounts {
19
+ pendingUnreconciled: number;
20
+ orphanMeetings: number;
21
+ }
22
+ /**
23
+ * Compare D1's accepted booking ids against the account's booking :Meeting set.
24
+ * pendingUnreconciled — accepted in D1 but no :Meeting yet (reconcile lagging).
25
+ * orphanMeetings — a booking :Meeting with no backing D1 row.
26
+ */
27
+ export declare function auditCounts(session: Session, accountId: string, d1AcceptedIds: string[]): Promise<AuditCounts>;
28
+ //# sourceMappingURL=booking-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-reconcile.d.ts","sourceRoot":"","sources":["../../src/lib/booking-reconcile.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,CAAC,CA4D1B;AAED,MAAM,WAAW,WAAW;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,WAAW,CAAC,CAWtB"}
@@ -0,0 +1,70 @@
1
+ export function bookingUid(bookingId) {
2
+ return `booking-${bookingId}`;
3
+ }
4
+ export async function reconcileOne(session, accountId, booking) {
5
+ const uid = bookingUid(booking.bookingId);
6
+ const meetingRes = await session.run(`MERGE (m:Meeting {accountId: $accountId, uid: $uid})
7
+ ON CREATE SET m.title = $title,
8
+ m.startsAt = datetime($slotStart),
9
+ m.endsAt = datetime($slotEnd),
10
+ m.scope = 'booking',
11
+ m.source = 'booking',
12
+ m.bookerEmail = $email,
13
+ m.note = $note,
14
+ m.isAllDay = false,
15
+ m.createdAt = datetime(),
16
+ m.updatedAt = datetime(),
17
+ m.createdByAgent = 'booking-reconcile',
18
+ m.createdBySource = 'booking-reconcile',
19
+ m._created = true
20
+ ON MATCH SET m.updatedAt = datetime(),
21
+ m._created = false
22
+ WITH m, m._created AS created
23
+ REMOVE m._created
24
+ RETURN created, elementId(m) AS meetingId`, {
25
+ accountId,
26
+ uid,
27
+ title: `Booking: ${booking.name}`,
28
+ slotStart: booking.slotStart,
29
+ slotEnd: booking.slotEnd,
30
+ email: booking.email,
31
+ note: booking.note ?? "",
32
+ });
33
+ const created = meetingRes.records[0].get("created");
34
+ const meetingId = meetingRes.records[0].get("meetingId");
35
+ // Resolve the booker in one atomic statement that respects the global
36
+ // Person.email uniqueness constraint. MERGE on email matches any existing
37
+ // Person (without relabeling it — ON CREATE fires only on a fresh node) or
38
+ // creates one under this account. The ATTENDED link is written only when the
39
+ // resolved Person belongs to this account, so a foreign-account person is
40
+ // neither linked nor modified, and there is no check-then-create race that
41
+ // could throw the unique constraint and wedge the row.
42
+ const personRes = await session.run(`MATCH (m:Meeting) WHERE elementId(m) = $meetingId
43
+ MERGE (p:Person {email: $email})
44
+ ON CREATE SET p.accountId = $accountId,
45
+ p.displayName = $name,
46
+ p.scope = 'booking',
47
+ p.source = 'booking-reconcile',
48
+ p.createdAt = datetime()
49
+ FOREACH (_ IN CASE WHEN p.accountId = $accountId THEN [1] ELSE [] END |
50
+ MERGE (p)-[r:ATTENDED]->(m)
51
+ ON CREATE SET r.role = 'booker', r.createdAt = datetime(), r.createdBySource = 'booking-reconcile')
52
+ RETURN p.accountId = $accountId AS linked`, { meetingId, email: booking.email, accountId, name: booking.name });
53
+ const linked = personRes.records[0]?.get("linked") === true;
54
+ return { created, meetingId, personLinked: linked, crossAccountSkipped: !linked };
55
+ }
56
+ /**
57
+ * Compare D1's accepted booking ids against the account's booking :Meeting set.
58
+ * pendingUnreconciled — accepted in D1 but no :Meeting yet (reconcile lagging).
59
+ * orphanMeetings — a booking :Meeting with no backing D1 row.
60
+ */
61
+ export async function auditCounts(session, accountId, d1AcceptedIds) {
62
+ const res = await session.run(`MATCH (m:Meeting {accountId: $accountId, source: 'booking'}) RETURN collect(m.uid) AS uids`, { accountId });
63
+ const uids = res.records[0]?.get("uids") ?? [];
64
+ const uidSet = new Set(uids);
65
+ const idSet = new Set(d1AcceptedIds);
66
+ const pendingUnreconciled = d1AcceptedIds.filter((id) => !uidSet.has(bookingUid(id))).length;
67
+ const orphanMeetings = uids.filter((uid) => !idSet.has(uid.replace(/^booking-/, ""))).length;
68
+ return { pendingUnreconciled, orphanMeetings };
69
+ }
70
+ //# sourceMappingURL=booking-reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-reconcile.js","sourceRoot":"","sources":["../../src/lib/booking-reconcile.ts"],"names":[],"mappings":"AA2BA,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,OAAO,WAAW,SAAS,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAgB,EAChB,SAAiB,EACjB,OAAmB;IAEnB,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAE1C,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC;;;;;;;;;;;;;;;;;;+CAkB2C,EAC3C;QACE,SAAS;QACT,GAAG;QACH,KAAK,EAAE,YAAY,OAAO,CAAC,IAAI,EAAE;QACjC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE;KACzB,CACF,CAAC;IACF,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAY,CAAC;IAChE,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAW,CAAC;IAEnE,sEAAsE;IACtE,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,uDAAuD;IACvD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC;;;;;;;;;;+CAU2C,EAC3C,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CACnE,CAAC;IACF,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IAE5D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,MAAM,EAAE,CAAC;AACpF,CAAC;AAOD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,SAAiB,EACjB,aAAuB;IAEvB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAC3B,4FAA4F,EAC5F,EAAE,SAAS,EAAE,CACd,CAAC;IACF,MAAM,IAAI,GAAa,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;IACrC,MAAM,mBAAmB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7F,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC;AACjD,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Dispatcher script: reconcile-bookings
3
+ *
4
+ * Self-contained, like check-due-events. Pulls accepted booking rows from each
5
+ * account's Cloudflare D1 `bookings` table (written by the booking site's Pages
6
+ * Function), writes each into Neo4j as a :Meeting so it appears on the admin
7
+ * calendar, links the booker as a :Person, and notifies the admin. Marks each
8
+ * row swept so it reconciles exactly once. Ends with the standing audit.
9
+ *
10
+ * The Pi has no running periodic surface today (the legacy cron was removed and
11
+ * the desktop-scheduled-task replacement is not wired), so this binary is armed
12
+ * by the always-running platform/ui server on an interval. A PID lock prevents
13
+ * overlapping runs.
14
+ *
15
+ * Observability — one [calendar-booking] line per step keyed by bookingId, plus
16
+ * one [calendar-reconcile] audit line per account per run.
17
+ *
18
+ * PLATFORM_ROOT=... NEO4J_URI=... node dist/scripts/reconcile-bookings.js
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=reconcile-bookings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile-bookings.d.ts","sourceRoot":"","sources":["../../src/scripts/reconcile-bookings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}