@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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +4 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +88 -1
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +82 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/book.ts +112 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +100 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +155 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +47 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +18 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/wrangler.toml +14 -0
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +26 -0
- package/payload/platform/plugins/docs/references/calendar-booking.md +56 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js +45 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +89 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts +22 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js +31 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-notify.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +28 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +70 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts +21 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +280 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -0
- package/payload/server/{chunk-5BG6CHGH.js → chunk-QAKVFSEJ.js} +15 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminLoginScreens-KVvngpT2.js +1 -0
- package/payload/server/public/assets/AdminShell-DDKZhJRo.js +1 -0
- package/payload/server/public/assets/Checkbox-BJlS0tR2.js +1 -0
- package/payload/server/public/assets/OperatorConversations-_P3_KmAW.js +1 -0
- package/payload/server/public/assets/admin-BTQWgciK.js +1 -0
- package/payload/server/public/assets/{OperatorConversations-BMIZQR9t.css → admin-auth-fetch-9JCQ2FFm.css} +1 -1
- package/payload/server/public/assets/admin-auth-fetch-BMOxrEnl.js +9 -0
- package/payload/server/public/assets/{arc-CxLB9WCE.js → arc-Crkz45LN.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-Dma4Bgha.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-D-EdJPSY.js → architectureDiagram-Q4EWVU46-C9JThHgP.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CXx9RgCm.js → blockDiagram-DXYQGD6D-DpsV_HXT.js} +1 -1
- package/payload/server/public/assets/browser-CpOL9dN0.js +1 -0
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-RFBXEe0B.js → c4Diagram-AHTNJAMY-CTm_6aKi.js} +1 -1
- package/payload/server/public/assets/calendar-dRLyhghx.js +1 -0
- package/payload/server/public/assets/channel-B_-5bau1.js +1 -0
- package/payload/server/public/assets/chat-prZCVWvK.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-DBqAFwK9.js → chunk-2KRD3SAO-1KSJW1q6.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-DdnFEz6R.js → chunk-336JU56O-C3t08Ip-.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-Dirxdwjh.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-cnw-3Gbs.js → chunk-4BX2VUAB-kl1ze33n.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BnqJ0bDc.js → chunk-4TB4RGXK-DxpoDoHg.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-BwyHvvzo.js → chunk-55IACEB6-WM9VJzha.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DXA9U02I.js → chunk-5FUZZQ4R-CLH63ZH3.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-D78bZ_8D.js → chunk-5PVQY5BW-DCTb8IAT.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-CtE_Mgv7.js → chunk-67CJDMHE-BSYzyaoH.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-CUZDaySl.js → chunk-7N4EOEYR-CarJ7O2P.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-CYQ3jO4X.js → chunk-AA7GKIK3-DTNLyBpH.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-BbA_HyTR.js → chunk-BSJP7CBP-BFmHflHY.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-D7_YqRhG.js → chunk-CIAEETIT-BpjG0IzU.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-B23LrtSw.js → chunk-EDXVE4YY-2fSowhlW.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-D3fTHYKN.js → chunk-ENJZ2VHE-ClcranGV.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-DFAA2tyh.js → chunk-FMBD7UC4-DLq4BItQ.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-BF4IHvlI.js → chunk-FOC6F5B3-B9QI3Guz.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DBUGemHy.js → chunk-ICPOFSXX-BAr-Mv1Z.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-rMMs08Eh.js → chunk-K5T4RW27-8PvAHasY.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-BD2r8R2G.js → chunk-KGLVRYIC--aGaDRH5.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-D4sYTTwM.js → chunk-LIHQZDEY-BrWj7mw2.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-CAs_uFzb.js → chunk-ORNJ4GCN-kYQmkMTi.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-Fkp-2ORc.js → chunk-OYMX7WX6-CIsryCWH.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Duipo1kI.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-D_pxZh25.js → chunk-U2HBQHQK-68X653e3.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CeVmvvRf.js → chunk-X2U36JSP-v3v9soX3.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-iWP54fKp.js → chunk-XPW4576I-CW5DPpop.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CNtbWJNN.js → chunk-YZCP3GAM-B1VP7Rl2.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BmlFjU_9.js → chunk-ZZ45TVLE-DiJrujNY.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B3kX5SuB.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D761c7Lr.js +1 -0
- package/payload/server/public/assets/clone-BrhFv6Tb.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C7YmF1Pg.js → cose-bilkent-S5V4N54A-BscXxLMc.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-Ca3ESJhc.js → dagre-KV5264BT-DVgGlTQ1.js} +1 -1
- package/payload/server/public/assets/{dagre-BAmiDDtR.js → dagre-MBv6WlCS.js} +1 -1
- package/payload/server/public/assets/data-CeUT45cU.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-BgT1ORf2.js → diagram-5BDNPKRD-BmhCaO0a.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BTaUIiem.js → diagram-G4DWMVQ6-ChU9L8vz.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-Bdb2g6Uo.js → diagram-MMDJMWI5-Oy409Zc_.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CkLGC0zZ.js → diagram-TYMM5635-Cn1YByqk.js} +1 -1
- package/payload/server/public/assets/{dist-Bkbhs3jw.js → dist-B0CkUodR.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D_sj4DMg.js → erDiagram-SMLLAGMA-e17yfLvr.js} +1 -1
- package/payload/server/public/assets/{flatten-E2Sr0FeD.js → flatten-Cl1Bc3dR.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-iLERoZTe.js → flowDiagram-DWJPFMVM-Dqt7sCVZ.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-D60hjMBy.js → ganttDiagram-T4ZO3ILL-B4IhwS2r.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-BCQAqtLy.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di0XM6fi.js → gitGraphDiagram-UUTBAWPF-BDw-PKcI.js} +1 -1
- package/payload/server/public/assets/{graph-Bnsvbnkf.js → graph-B8h8YAJu.js} +3 -3
- package/payload/server/public/assets/graph-labels-BFrQ5kyH.js +1 -0
- package/payload/server/public/assets/{graphlib-C8fUP7uV.js → graphlib-DPIYk_sg.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-qU7hK50T.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-KGdcJeTs.js +2 -0
- package/payload/server/public/assets/{isEmpty-CLz-UprQ.js → isEmpty-D1pGOD1J.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CwgI4B5Z.js → ishikawaDiagram-UXIWVN3A-BRIHsB3G.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CxBu-tze.js → journeyDiagram-VCZTEJTY-aKNgmoHd.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-D2qlheA5.js → kanban-definition-6JOO6SKY-cwBwT-nm.js} +1 -1
- package/payload/server/public/assets/{line-Bdod7uzQ.js → line-DVtVRzUA.js} +1 -1
- package/payload/server/public/assets/{linear-BiVe6QzC.js → linear-DtoOFPO2.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-xZN7hC6M.js → mermaid-parser.core-G7YD_zO3.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-D0IylAac.js → mermaid.core-DpFyndHm.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-J_Szhwf1.js → mindmap-definition-QFDTVHPH-CYXK_Pvz.js} +1 -1
- package/payload/server/public/assets/operator-D3fRwMfE.js +1 -0
- package/payload/server/public/assets/{ordinal-5RtyPJVt.js → ordinal-CBZeH4Yp.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DiU2yS9m.js +1 -0
- package/payload/server/public/assets/page-D0hQWIIV.js +30 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-zHUKKlVv.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CV5DxUVx.js → pieDiagram-DEJITSTG-DFo4N6WH.js} +1 -1
- package/payload/server/public/assets/public-DX4z0sCJ.js +1 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BsFMVizF.js → quadrantDiagram-34T5L4WZ-BlH5iIhl.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-BUnVoaPg.js +1 -0
- package/payload/server/public/assets/{reduce-B3Mw_0s1.js → reduce-PuPlFPRX.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-C5eRRvGc.js → requirementDiagram-MS252O5E-D5WLty8A.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CJYv5gpw.js → sankeyDiagram-XADWPNL6-vB85IxHG.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-B25563_2.js → sequenceDiagram-FGHM5R23-Cxraa0Ee.js} +1 -1
- package/payload/server/public/assets/{src-BKOAoFQc.js → src-CR_MU8uy.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BLyNJYOA.js → stateDiagram-FHFEXIEX-BBPssYBx.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BWmmUwDt.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D-HgOM6Q.js → timeline-definition-GMOUNBTQ-Bgk6PT62.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-C3Y-Q52h.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-Dk0qZpAg.js +1 -0
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Cw1JElYp.js → vennDiagram-DHZGUBPP-B-APTs3L.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-BAWI02OA.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BqMWzEko.js → wardleyDiagram-NUSXRM2D-CkXJSs_i.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DBp71TLT.js → xychartDiagram-5P7HB3ND-BkyshPMV.js} +1 -1
- package/payload/server/public/brand/maxy-app-icon-192.png +0 -0
- package/payload/server/public/brand/maxy-app-icon-512.png +0 -0
- package/payload/server/public/brand/maxy-app-icon-maskable-512.png +0 -0
- package/payload/server/public/browser.html +6 -5
- package/payload/server/public/calendar.html +16 -0
- package/payload/server/public/chat.html +9 -8
- package/payload/server/public/data.html +6 -5
- package/payload/server/public/graph.html +8 -7
- package/payload/server/public/index.html +9 -8
- package/payload/server/public/operator.html +11 -10
- package/payload/server/public/public.html +9 -8
- package/payload/server/server.js +598 -190
- package/payload/server/public/assets/AdminLoginScreens-Brx8CmXN.js +0 -1
- package/payload/server/public/assets/AdminShell-CHZMDX2u.js +0 -1
- package/payload/server/public/assets/Checkbox-aePjWzRH.js +0 -1
- package/payload/server/public/assets/OperatorConversations-DpjPPIOp.js +0 -9
- package/payload/server/public/assets/admin-DIDvfti6.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-DN-MYxff.js +0 -1
- package/payload/server/public/assets/browser-Bp5kGgyr.js +0 -1
- package/payload/server/public/assets/channel-y27xl53N.js +0 -1
- package/payload/server/public/assets/chat-C0IWx7FL.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-D8Iwaegm.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-qHqDpwz3.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DgVoYWhO.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-C0WNP3uN.js +0 -1
- package/payload/server/public/assets/clone-D50F_pvg.js +0 -1
- package/payload/server/public/assets/data-RsMye_06.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-D-L2yzYf.js +0 -1
- package/payload/server/public/assets/graph-labels-jduMtwXb.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-BQn6jndO.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-BwMdbiXg.js +0 -2
- package/payload/server/public/assets/operator-9K-TElDd.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DmkQBx4h.js +0 -1
- package/payload/server/public/assets/page-BT9hkXHm.js +0 -30
- package/payload/server/public/assets/pie-ZZUOXDRM-DDnxYUBm.js +0 -1
- package/payload/server/public/assets/public-DvL1Zov1.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-7H4k0ChM.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BSVuxZxz.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-BLXWTQtW.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-DiePDF5d.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-OSUWK5LN.js +0 -1
- /package/payload/server/public/assets/{_baseFor-DVaNaTfF.js → _baseFor-brRjpUOX.js} +0 -0
- /package/payload/server/public/assets/{admin-types-CJrGd46U.js → admin-types-Dg11L4MQ.js} +0 -0
- /package/payload/server/public/assets/{array-8_H1156C.js → array-BGFCBI0e.js} +0 -0
- /package/payload/server/public/assets/{chunk-Pqm5yXtL.js → chunk-CAM3fms7.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-h10p_6j-.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-pD7tFa1r.js → defaultLocale-CRZydyG6.js} +0 -0
- /package/payload/server/public/assets/{init-CwJ4b81e.js → init-B8gtcn7T.js} +0 -0
- /package/payload/server/public/assets/{katex-DJPjIBAI.js → katex-RxgSu_dy.js} +0 -0
- /package/payload/server/public/assets/{path-CK8wrAxY.js → path-DZF-JdEe.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CQ36nxok.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-Dwml_la6.js → rough.esm-6CnTHTkH.js} +0 -0
|
@@ -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.
|
package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-notify.test.d.ts.map
ADDED
|
@@ -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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"}
|
package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.d.ts.map
ADDED
|
@@ -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
|
package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map
ADDED
|
@@ -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"}
|