@rubytech/create-maxy-code 0.1.488 → 0.1.490

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 (282) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +748 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +1716 -0
  4. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +121 -0
  5. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +103 -0
  6. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +51 -0
  7. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -1
  8. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +2 -2
  9. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +20 -4
  11. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -1
  12. package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +55 -0
  13. package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +19 -4
  14. package/payload/platform/lib/graph-style/dist/index.d.ts +24 -0
  15. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  16. package/payload/platform/lib/graph-style/dist/index.js +25 -1
  17. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  18. package/payload/platform/lib/graph-style/src/__tests__/additional-base-labels.test.ts +22 -0
  19. package/payload/platform/lib/graph-style/src/index.ts +27 -0
  20. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +15 -0
  21. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/audit.d.ts +34 -1
  23. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  24. package/payload/platform/lib/storage-broker/dist/audit.js +28 -4
  25. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -0
  27. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/cf-exec.js +22 -0
  29. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  30. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +21 -0
  31. package/payload/platform/lib/storage-broker/src/audit.ts +67 -3
  32. package/payload/platform/lib/storage-broker/src/cf-exec.ts +48 -0
  33. package/payload/platform/neo4j/schema.cypher +105 -0
  34. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +34 -5
  35. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -2
  36. package/payload/platform/plugins/cloudflare/PLUGIN.md +22 -2
  37. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +161 -41
  38. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +95 -0
  39. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +75 -13
  40. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +216 -16
  41. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +38 -2
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +83 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +48 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +62 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +103 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +211 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +168 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +138 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +179 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +14 -13
  51. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +87 -13
  52. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +87 -9
  53. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +74 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +91 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +23 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +45 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +72 -5
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +80 -13
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +358 -16
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +12 -1
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +45 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +258 -21
  63. package/payload/platform/plugins/docs/references/calendar-booking.md +5 -1
  64. package/payload/platform/plugins/docs/references/internals.md +2 -0
  65. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +61 -31
  66. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
  67. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
  68. package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
  69. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  70. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +70 -5
  71. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -1
  72. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js +31 -6
  73. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js.map +1 -1
  74. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +31 -4
  75. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  76. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +18 -7
  77. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  78. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts +15 -6
  79. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts.map +1 -1
  80. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +24 -11
  81. package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
  82. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +5 -122
  83. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  84. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +84 -5
  85. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  86. package/payload/platform/scripts/__tests__/quarantine-restore.test.sh +143 -0
  87. package/payload/platform/scripts/quarantine-restore.mjs +175 -0
  88. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +24 -7
  89. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +52 -74
  91. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/index.js +11 -8
  93. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  94. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +32 -0
  95. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  96. package/payload/platform/services/whatsapp-channel/dist/notification.js +32 -2
  97. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  98. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +6 -1
  99. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  100. package/payload/platform/services/whatsapp-channel/dist/targets.js +6 -0
  101. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  102. package/payload/platform/templates/account-schema/SCHEMA.md +7 -4
  103. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  104. package/payload/platform/templates/agents/passive/IDENTITY.md +6 -0
  105. package/payload/platform/templates/agents/public/IDENTITY.md +6 -0
  106. package/payload/platform/templates/specialists/agents/citation-auditor.md +6 -0
  107. package/payload/platform/templates/specialists/agents/coding-assistant.md +6 -0
  108. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +6 -0
  109. package/payload/platform/templates/specialists/agents/content-producer.md +6 -0
  110. package/payload/platform/templates/specialists/agents/data-manager.md +6 -0
  111. package/payload/platform/templates/specialists/agents/database-operator.md +6 -0
  112. package/payload/platform/templates/specialists/agents/librarian.md +6 -0
  113. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -0
  114. package/payload/platform/templates/specialists/agents/project-manager.md +6 -0
  115. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +6 -0
  116. package/payload/platform/templates/specialists/agents/research-assistant.md +7 -1
  117. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +6 -0
  118. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +6 -0
  119. package/payload/server/{chunk-REWXLAPC.js → chunk-KZFE3MOY.js} +52 -4
  120. package/payload/server/public/activity.html +5 -6
  121. package/payload/server/public/assets/AdminLoginScreens-DgVFsHtd.js +1 -0
  122. package/payload/server/public/assets/AdminShell-BwNqwb4k.js +2 -0
  123. package/payload/server/public/assets/activity-2DL2DwLr.js +1 -0
  124. package/payload/server/public/assets/admin-COe0dT2U.js +1 -0
  125. package/payload/server/public/assets/{arc-D-QzyndF.js → arc-CFMJyMm-.js} +1 -1
  126. package/payload/server/public/assets/architecture-YZFGNWBL-Cypt5qHL.js +1 -0
  127. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-W7f0Us4S.js → architectureDiagram-Q4EWVU46-CiSSgdL8.js} +1 -1
  128. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CfPS4FXg.js → blockDiagram-DXYQGD6D-_sXtIW5c.js} +1 -1
  129. package/payload/server/public/assets/{bot-DnuyRsLS.js → bot-CpBuDQlF.js} +1 -1
  130. package/payload/server/public/assets/{browser-q4EFqKvR.js → browser-CSApuZrv.js} +1 -1
  131. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Dq60cLVI.js → c4Diagram-AHTNJAMY-Bh7OAMXs.js} +1 -1
  132. package/payload/server/public/assets/{calendar-C-q9kWmA.js → calendar-DlvwZnS7.js} +1 -1
  133. package/payload/server/public/assets/channel-BMyXE4zc.js +1 -0
  134. package/payload/server/public/assets/chat-DWvYv9jq.js +1 -0
  135. package/payload/server/public/assets/chevron-down-Dnuuw6Q2.js +1 -0
  136. package/payload/server/public/assets/chevron-left-CC4zmWOr.js +1 -0
  137. package/payload/server/public/assets/chevron-right-CG_lHTx_.js +1 -0
  138. package/payload/server/public/assets/{chunk-2KRD3SAO-CXDjuDN0.js → chunk-2KRD3SAO-D4fktAwW.js} +1 -1
  139. package/payload/server/public/assets/{chunk-336JU56O-926ox0tj.js → chunk-336JU56O-CcQoKH2G.js} +2 -2
  140. package/payload/server/public/assets/chunk-426QAEUC-Xu6JKP4d.js +1 -0
  141. package/payload/server/public/assets/{chunk-4BX2VUAB-C2nrPCfN.js → chunk-4BX2VUAB-C13Wz3zS.js} +1 -1
  142. package/payload/server/public/assets/{chunk-4TB4RGXK-CuW3FOR6.js → chunk-4TB4RGXK-BL48D7gN.js} +1 -1
  143. package/payload/server/public/assets/{chunk-55IACEB6-B4_-In90.js → chunk-55IACEB6-TmOKdy8a.js} +1 -1
  144. package/payload/server/public/assets/{chunk-5FUZZQ4R-DnYGr9CP.js → chunk-5FUZZQ4R-DZ_jSF6N.js} +1 -1
  145. package/payload/server/public/assets/{chunk-5PVQY5BW-BYCOi9bf.js → chunk-5PVQY5BW-De4G1vfV.js} +1 -1
  146. package/payload/server/public/assets/{chunk-67CJDMHE-CBxsC4JP.js → chunk-67CJDMHE-Cif9Uu5C.js} +1 -1
  147. package/payload/server/public/assets/{chunk-7N4EOEYR-CAACaqFr.js → chunk-7N4EOEYR-WjTHCXnd.js} +1 -1
  148. package/payload/server/public/assets/{chunk-AA7GKIK3-BoamZdS6.js → chunk-AA7GKIK3-CwCfdQmb.js} +1 -1
  149. package/payload/server/public/assets/{chunk-BSJP7CBP-CHk82eFy.js → chunk-BSJP7CBP-9A67yXwT.js} +1 -1
  150. package/payload/server/public/assets/{chunk-CIAEETIT-DdeW9zxl.js → chunk-CIAEETIT-DPMiWSBi.js} +1 -1
  151. package/payload/server/public/assets/{chunk-EDXVE4YY-DJqSRv2w.js → chunk-EDXVE4YY-C2f_Qvx-.js} +1 -1
  152. package/payload/server/public/assets/{chunk-ENJZ2VHE-BRi4BwM5.js → chunk-ENJZ2VHE-B-Q9EbSQ.js} +1 -1
  153. package/payload/server/public/assets/{chunk-FMBD7UC4-OJp2CtUE.js → chunk-FMBD7UC4-Bmt_ZRlV.js} +1 -1
  154. package/payload/server/public/assets/{chunk-FOC6F5B3-BUAQ6spG.js → chunk-FOC6F5B3-DfGJGxpX.js} +1 -1
  155. package/payload/server/public/assets/{chunk-ICPOFSXX-DeRggD-Q.js → chunk-ICPOFSXX-Wyym1Q8K.js} +2 -2
  156. package/payload/server/public/assets/{chunk-K5T4RW27-EHiXZ8Py.js → chunk-K5T4RW27-DMpoaqw2.js} +1 -1
  157. package/payload/server/public/assets/{chunk-KGLVRYIC-tOajPcpn.js → chunk-KGLVRYIC-DYUFgri2.js} +1 -1
  158. package/payload/server/public/assets/{chunk-LIHQZDEY-uwO6oHIU.js → chunk-LIHQZDEY-x3LmeKmx.js} +1 -1
  159. package/payload/server/public/assets/{chunk-ORNJ4GCN-Dpa1_BCA.js → chunk-ORNJ4GCN-BeuNAWkW.js} +1 -1
  160. package/payload/server/public/assets/{chunk-OYMX7WX6-ChdDqaQL.js → chunk-OYMX7WX6-BUWVAfQA.js} +1 -1
  161. package/payload/server/public/assets/chunk-QZHKN3VN-7_kS3RBy.js +1 -0
  162. package/payload/server/public/assets/{chunk-U2HBQHQK-5JgWosPv.js → chunk-U2HBQHQK-DKS5adQo.js} +1 -1
  163. package/payload/server/public/assets/{chunk-X2U36JSP-BGJqysB2.js → chunk-X2U36JSP-CT_UM4IN.js} +1 -1
  164. package/payload/server/public/assets/{chunk-XPW4576I-GERE2ekx.js → chunk-XPW4576I-ByFnN4JO.js} +1 -1
  165. package/payload/server/public/assets/{chunk-YZCP3GAM-BskqX1V5.js → chunk-YZCP3GAM-DCetC20J.js} +1 -1
  166. package/payload/server/public/assets/{chunk-ZZ45TVLE-DG1iY-L9.js → chunk-ZZ45TVLE-Bh1QIv8N.js} +1 -1
  167. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DJqbNcuC.js +1 -0
  168. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CGglqfQA.js +1 -0
  169. package/payload/server/public/assets/clone-BiTMTzp_.js +1 -0
  170. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C8Rl4ykh.js → cose-bilkent-S5V4N54A-DgLEaApT.js} +1 -1
  171. package/payload/server/public/assets/{dagre-KV5264BT-C0YrJPF3.js → dagre-KV5264BT-BfkUPrEV.js} +1 -1
  172. package/payload/server/public/assets/{dagre-DTlv-FFy.js → dagre-unhMIyVo.js} +1 -1
  173. package/payload/server/public/assets/data-CJbC_cVy.js +1 -0
  174. package/payload/server/public/assets/{diagram-5BDNPKRD-Dfk5oepI.js → diagram-5BDNPKRD-BjjwcTD9.js} +1 -1
  175. package/payload/server/public/assets/{diagram-G4DWMVQ6-QRj55DiB.js → diagram-G4DWMVQ6-BbgWtVSS.js} +1 -1
  176. package/payload/server/public/assets/{diagram-MMDJMWI5-BAPKsohU.js → diagram-MMDJMWI5-DyAnkWGy.js} +1 -1
  177. package/payload/server/public/assets/{diagram-TYMM5635-C3iZXPkC.js → diagram-TYMM5635-SAMMoLxq.js} +1 -1
  178. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DIbI7g_5.js → erDiagram-SMLLAGMA-Cvo-5-fD.js} +1 -1
  179. package/payload/server/public/assets/{flatten-DlAJeN0L.js → flatten-BXzaHgtL.js} +1 -1
  180. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-l3eJJr8R.js → flowDiagram-DWJPFMVM-y3o2Ycld.js} +1 -1
  181. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-aaEzHxIq.js → ganttDiagram-T4ZO3ILL-BEm_CQaK.js} +1 -1
  182. package/payload/server/public/assets/gitGraph-7Q5UKJZL-C_-XjxLd.js +1 -0
  183. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di-1eg_m.js → gitGraphDiagram-UUTBAWPF-CqmOxS7F.js} +1 -1
  184. package/payload/server/public/assets/{graph-CNgt7ygi.js → graph-kaxS9a_7.js} +2 -2
  185. package/payload/server/public/assets/graph-labels-GgejRtgV.js +1 -0
  186. package/payload/server/public/assets/{graphlib-D5awC2yp.js → graphlib-_HfbQcBf.js} +1 -1
  187. package/payload/server/public/assets/info-OMHHGYJF-BH8yDCfL.js +1 -0
  188. package/payload/server/public/assets/infoDiagram-42DDH7IO-DyDXyJ6U.js +2 -0
  189. package/payload/server/public/assets/{isEmpty-Dh1dF-tM.js → isEmpty-uB1dEu_Z.js} +1 -1
  190. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CLOd_cNf.js → ishikawaDiagram-UXIWVN3A-C_d0vcZC.js} +1 -1
  191. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-cGyfW6Tg.js → journeyDiagram-VCZTEJTY-DsjRBqlW.js} +1 -1
  192. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CRTKWPlh.js → kanban-definition-6JOO6SKY-BwtK6LS-.js} +1 -1
  193. package/payload/server/public/assets/{line-B8Brm0TD.js → line-szBCFOLJ.js} +1 -1
  194. package/payload/server/public/assets/{linear-BHnSKXBe.js → linear-X3O7lwK3.js} +1 -1
  195. package/payload/server/public/assets/{maximize-2-k3Fu5hdP.js → maximize-2-CPq3nwZA.js} +1 -1
  196. package/payload/server/public/assets/{mermaid-parser.core-BtNpdQct.js → mermaid-parser.core-BavvMAhU.js} +2 -2
  197. package/payload/server/public/assets/{mermaid.core-BY87anQZ.js → mermaid.core-Br2F9Hdm.js} +3 -3
  198. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-rUSv9qN1.js → mindmap-definition-QFDTVHPH-4qlwXoJ-.js} +1 -1
  199. package/payload/server/public/assets/operator-CldXXilj.js +1 -0
  200. package/payload/server/public/assets/{ordinal-Bnl5GL-x.js → ordinal-CZY9laxg.js} +1 -1
  201. package/payload/server/public/assets/packet-4T2RLAQJ-Bajcns4r.js +1 -0
  202. package/payload/server/public/assets/{page-Bvx0A2jB.js → page-BE6DkJP9.js} +1 -1
  203. package/payload/server/public/assets/{page-DhwPG5Zf.js → page-BIwdWXnv.js} +3 -3
  204. package/payload/server/public/assets/{pdf-render-CrpAi4Ba.js → pdf-render-H884L3EI.js} +1 -1
  205. package/payload/server/public/assets/pie-ZZUOXDRM-D7hjM5w1.js +1 -0
  206. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Cjn9IP-C.js → pieDiagram-DEJITSTG-BbXryIg0.js} +1 -1
  207. package/payload/server/public/assets/{public-BJ5Oo2Fw.js → public-BKQEQGPE.js} +1 -1
  208. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BkSHkSsn.js → quadrantDiagram-34T5L4WZ-BHLjcvh3.js} +1 -1
  209. package/payload/server/public/assets/radar-PYXPWWZC-B46d5x7a.js +1 -0
  210. package/payload/server/public/assets/{reduce-Bp5oNw1n.js → reduce-BGQzAenn.js} +1 -1
  211. package/payload/server/public/assets/{requirementDiagram-MS252O5E-hLwgyB-F.js → requirementDiagram-MS252O5E-BXaVO0lk.js} +1 -1
  212. package/payload/server/public/assets/{rotate-ccw-D-ov5jM4.js → rotate-ccw-DieZzl10.js} +1 -1
  213. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D6Dl1RIa.js → sankeyDiagram-XADWPNL6-CmN1s17Q.js} +1 -1
  214. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-j0QBqkbL.js → sequenceDiagram-FGHM5R23-DC08e5NY.js} +1 -1
  215. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-TvEBNsuV.js → stateDiagram-FHFEXIEX-DnoQMuOW.js} +1 -1
  216. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CrstH34p.js +1 -0
  217. package/payload/server/public/assets/{tasks-DHMxTZVD.js → tasks-DYyCGz8W.js} +1 -1
  218. package/payload/server/public/assets/{time-entry-format-EmV5lz6o.js → time-entry-format-CC_9jFuR.js} +1 -1
  219. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BfivOP-j.js → timeline-definition-GMOUNBTQ-eTn3uRaH.js} +1 -1
  220. package/payload/server/public/assets/treeView-SZITEDCU-3XQh5xQ1.js +1 -0
  221. package/payload/server/public/assets/treemap-W4RFUUIX-DwR-_l5z.js +1 -0
  222. package/payload/server/public/assets/{triangle-alert-CivyPfAL.js → triangle-alert-7mQfCQWP.js} +1 -1
  223. package/payload/server/public/assets/{useCopyFeedback-hbnPS-r9.js → useCopyFeedback-os7N0yCx.js} +1 -1
  224. package/payload/server/public/assets/useSubAccountSwitcher-Bgfqig1V.css +1 -0
  225. package/payload/server/public/assets/{useVoiceRecorder-QN9_GqB_.js → useVoiceRecorder-ZFwt5i6d.js} +2 -2
  226. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Bvfw9aFJ.js → vennDiagram-DHZGUBPP-DNx6o_ZQ.js} +1 -1
  227. package/payload/server/public/assets/wardley-RL74JXVD-DZp9oCGv.js +1 -0
  228. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C9h04G6b.js → wardleyDiagram-NUSXRM2D-BZOchqF3.js} +1 -1
  229. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BMvi2m2q.js → xychartDiagram-5P7HB3ND-ZBEGH6ZI.js} +1 -1
  230. package/payload/server/public/browser.html +4 -4
  231. package/payload/server/public/calendar.html +6 -6
  232. package/payload/server/public/chat.html +14 -14
  233. package/payload/server/public/data.html +11 -11
  234. package/payload/server/public/graph.html +10 -11
  235. package/payload/server/public/index.html +16 -17
  236. package/payload/server/public/operator.html +15 -16
  237. package/payload/server/public/public.html +14 -14
  238. package/payload/server/public/tasks.html +6 -6
  239. package/payload/server/server.js +1836 -998
  240. package/payload/server/{src-JJ6PHO5I.js → src-UYDFMWF4.js} +1 -1
  241. package/payload/server/public/assets/AdminLoginScreens-Dic_fPt6.js +0 -1
  242. package/payload/server/public/assets/AdminShell-JN7rZ8aG.js +0 -2
  243. package/payload/server/public/assets/Checkbox-CB74kINA.js +0 -1
  244. package/payload/server/public/assets/activity-DbfpHg3i.js +0 -1
  245. package/payload/server/public/assets/admin-CThbXaYH.js +0 -1
  246. package/payload/server/public/assets/architecture-YZFGNWBL-CGvD4r9F.js +0 -1
  247. package/payload/server/public/assets/channel-Bpg-ADC0.js +0 -1
  248. package/payload/server/public/assets/chat-DcefIV9q.js +0 -1
  249. package/payload/server/public/assets/chevron-down-Blma_CKl.js +0 -1
  250. package/payload/server/public/assets/chevron-left-xeYDxFiC.js +0 -1
  251. package/payload/server/public/assets/chevron-right-BonbtjqJ.js +0 -1
  252. package/payload/server/public/assets/chunk-426QAEUC-1poG7Nd3.js +0 -1
  253. package/payload/server/public/assets/chunk-QZHKN3VN-GN1tIO8J.js +0 -1
  254. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DMGVq8Jv.js +0 -1
  255. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfddT8UV.js +0 -1
  256. package/payload/server/public/assets/clone-Cc9vFIEk.js +0 -1
  257. package/payload/server/public/assets/data-D5dp6nj6.js +0 -1
  258. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DtjXE0UI.js +0 -1
  259. package/payload/server/public/assets/graph-labels-DA48HrRR.js +0 -1
  260. package/payload/server/public/assets/info-OMHHGYJF-CmArhGYf.js +0 -1
  261. package/payload/server/public/assets/infoDiagram-42DDH7IO-utWz7oZy.js +0 -2
  262. package/payload/server/public/assets/operator-8NVpNLeD.js +0 -1
  263. package/payload/server/public/assets/packet-4T2RLAQJ-BTfK0Zw4.js +0 -1
  264. package/payload/server/public/assets/pie-ZZUOXDRM-BvtPeMQY.js +0 -1
  265. package/payload/server/public/assets/radar-PYXPWWZC-D2UXvQgn.js +0 -1
  266. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ur4eM6rb.js +0 -1
  267. package/payload/server/public/assets/treeView-SZITEDCU-0dE7FvM2.js +0 -1
  268. package/payload/server/public/assets/treemap-W4RFUUIX-BrwlXpr5.js +0 -1
  269. package/payload/server/public/assets/useSubAccountSwitcher-kwhG-aJj.css +0 -1
  270. package/payload/server/public/assets/wardley-RL74JXVD-CO9HdfJ-.js +0 -1
  271. /package/payload/server/public/assets/{_baseFor-BBkmqPEk.js → _baseFor-CMWvz0uA.js} +0 -0
  272. /package/payload/server/public/assets/{array-Bjtb2C_5.js → array-Cny_NT6T.js} +0 -0
  273. /package/payload/server/public/assets/{cytoscape.esm-B1dQ664k.js → cytoscape.esm-De0CCmB2.js} +0 -0
  274. /package/payload/server/public/assets/{defaultLocale-iwYA7ne8.js → defaultLocale-BahRht8U.js} +0 -0
  275. /package/payload/server/public/assets/{dist-CLmrZbY7.js → dist-C9D_xz81.js} +0 -0
  276. /package/payload/server/public/assets/{init-CxnBAgcf.js → init-DoIvjoJ_.js} +0 -0
  277. /package/payload/server/public/assets/{katex-DojHoLIr.js → katex-Bfn1OZEl.js} +0 -0
  278. /package/payload/server/public/assets/{path-JZiojYzQ.js → path-CTezU8L5.js} +0 -0
  279. /package/payload/server/public/assets/{preload-helper-CT1Z6Pdu.js → preload-helper-D_sPnjeL.js} +0 -0
  280. /package/payload/server/public/assets/{rough.esm-D5NinLFK.js → rough.esm-BJo4pvXu.js} +0 -0
  281. /package/payload/server/public/assets/{src-DQ0MSD6q.js → src-BTKk8KAG.js} +0 -0
  282. /package/payload/server/public/assets/{useSubAccountSwitcher-DotTrvX3.js → useSubAccountSwitcher-Q941cZYV.js} +0 -0
@@ -0,0 +1,91 @@
1
+ // Where an upload may land (Task 1910).
2
+ //
3
+ // Before this, an upload had no destination at all: the key was a flat
4
+ // `ownerId/filename` and the client could not say which folder they meant. This
5
+ // module is the one definition of a legal target on the portal side.
6
+ //
7
+ // It is NOT the security boundary. The device re-derives the account's exposed
8
+ // folder set itself and refuses anything outside it, exactly as the download
9
+ // path already does. Both checks exist for the same reason: if this end is ever
10
+ // wrong — a stale index, a bug here, a compromised Pages project — the device
11
+ // is what contains it. A parity test pins the two rules to the same answers.
12
+
13
+ import type { D1Database } from './types'
14
+ import { grantAllows } from './session'
15
+ import { ownerPrefix } from './authorize'
16
+
17
+ /**
18
+ * One path segment of a legal target: not empty, not a traversal token, and
19
+ * carrying no separator of either flavour.
20
+ *
21
+ * Rejected outright rather than normalised, the same rule and the same reason
22
+ * as authorizeKey — a normaliser is one more thing that can be wrong, and no
23
+ * legitimate folder name contains any of these.
24
+ *
25
+ * Deliberately identical to the device's own safeSegment in
26
+ * portal-uploads-pull.ts. The two are separate implementations on purpose (no
27
+ * import crosses that boundary) and the parity test is what keeps them honest.
28
+ */
29
+ export function isSafeTargetSegment(s: string): boolean {
30
+ if (!s || s === '.' || s === '..') return false
31
+ return !s.includes('/') && !s.includes('\\') && !s.includes('\0')
32
+ }
33
+
34
+ export type TargetCheck = { ok: true } | { ok: false; reason: 'segment' | 'grant' | 'index' }
35
+
36
+ /**
37
+ * Whether this person may upload into `targetPath`.
38
+ *
39
+ * Three clauses, in the order that costs least: every segment is safe, the
40
+ * person's grant admits the top-level name, and the account's published index
41
+ * carries a folder row for the exact path.
42
+ *
43
+ * That last clause is the whole permission model: you can only drop where you
44
+ * can see. The legal target set is exactly the set of folders already rendered
45
+ * to this person, so no new concept is introduced and no folder can be named
46
+ * that the client could not already open.
47
+ *
48
+ * The empty target is always legal and costs no query. It is the default
49
+ * uploads area, which is deliberately never in the index.
50
+ *
51
+ * The three reasons are distinct in the log because they mean different things
52
+ * to an operator: `segment` is a malformed request, `grant` is a person seeing
53
+ * less than their account exposes, and `index` is usually a folder that exists
54
+ * on the device but has not been published yet.
55
+ */
56
+ export async function checkTarget(
57
+ db: D1Database,
58
+ accountId: string,
59
+ grant: string[],
60
+ targetPath: string,
61
+ ): Promise<TargetCheck> {
62
+ if (targetPath === '') return { ok: true }
63
+ if (!targetPath.split('/').every(isSafeTargetSegment)) return { ok: false, reason: 'segment' }
64
+ if (!grantAllows(grant, targetPath)) return { ok: false, reason: 'grant' }
65
+ const row = await db
66
+ .prepare(
67
+ `SELECT d.relPath AS relPath
68
+ FROM directory d
69
+ JOIN directory_state s
70
+ ON s.accountId = d.accountId AND s.currentGeneration = d.generation
71
+ WHERE d.accountId = ? AND d.relPath = ? AND d.isDir = 1`,
72
+ )
73
+ .bind(accountId, targetPath)
74
+ .first<{ relPath: string }>()
75
+ return row ? { ok: true } : { ok: false, reason: 'index' }
76
+ }
77
+
78
+ /**
79
+ * `ownerId/targetPath/filename`, collapsing to `ownerId/filename` when
80
+ * untargeted.
81
+ *
82
+ * The target is IN the key because `objectKey` is UNIQUE. With a flat key the
83
+ * same filename dropped into two folders would collide: the upsert would
84
+ * overwrite the first object, reset its ingested flag, and the first file would
85
+ * never reach the device at all.
86
+ */
87
+ export function uploadKey(ownerId: string, targetPath: string, filename: string): string {
88
+ return targetPath
89
+ ? `${ownerPrefix(ownerId)}${targetPath}/${filename}`
90
+ : `${ownerPrefix(ownerId)}${filename}`
91
+ }
@@ -24,11 +24,34 @@ export interface R2ObjectBody {
24
24
  size: number
25
25
  }
26
26
 
27
+ /** One completed part, as R2 returns it and as `complete` wants it back. */
28
+ export interface R2UploadedPart {
29
+ partNumber: number
30
+ etag: string
31
+ }
32
+
33
+ /**
34
+ * An open multipart session (Task 1910).
35
+ *
36
+ * The client slices a large file and sends parts, because an upload body is
37
+ * read wholly into the Function's memory: one 1 GiB request would OOM the
38
+ * isolate, and an OOM emits no [data-portal] line at all. Parts keep the
39
+ * existing per-request bound and let a large file respect it repeatedly.
40
+ */
41
+ export interface R2MultipartUpload {
42
+ uploadId: string
43
+ uploadPart: (partNumber: number, value: ArrayBuffer | Uint8Array) => Promise<R2UploadedPart>
44
+ complete: (parts: R2UploadedPart[]) => Promise<{ size?: number } | null>
45
+ abort: () => Promise<void>
46
+ }
47
+
27
48
  export interface R2Bucket {
28
49
  put: (key: string, value: ArrayBuffer | Uint8Array) => Promise<unknown>
29
50
  get: (key: string) => Promise<R2ObjectBody | null>
30
51
  delete: (key: string) => Promise<void>
31
52
  head: (key: string) => Promise<{ size: number } | null>
53
+ createMultipartUpload: (key: string) => Promise<R2MultipartUpload>
54
+ resumeMultipartUpload: (key: string, uploadId: string) => R2MultipartUpload
32
55
  }
33
56
 
34
57
  export interface PortalEnv {
@@ -22,6 +22,51 @@ export async function processDelete(
22
22
  }
23
23
  const owner = q(session.ownerId)
24
24
 
25
+ // What the client is asking to remove decides HOW it is removed, so the row
26
+ // is read before anything is written (Task 1910).
27
+ const row = await env.DB.prepare('SELECT devicePath FROM manifest WHERE objectKey = ?')
28
+ .bind(key)
29
+ .first<{ devicePath: string }>()
30
+ if (!row) {
31
+ log(`[data-portal] op=delete owner=${owner} key=${q(key)} result=absent`)
32
+ return { status: 404, payload: { ok: false, error: 'not found' } }
33
+ }
34
+
35
+ // A moved file's bytes are on the device and its R2 object is already gone.
36
+ // Deleting here would report success while the client's file stayed on the
37
+ // device forever. Instead the row is marked, and the pull removes the device
38
+ // file, confirms it is absent, and only then removes the row — so a
39
+ // withdrawal completes on BOTH sides or on neither.
40
+ //
41
+ // `pending: true` is not decoration: the client is told the file is on its
42
+ // way out rather than gone, because it will still be there for up to a
43
+ // minute and saying otherwise would be a lie the next refresh exposes.
44
+ if (row.devicePath) {
45
+ try {
46
+ await env.DB.prepare('UPDATE manifest SET deleted = 1 WHERE objectKey = ?').bind(key).run()
47
+ } catch (err) {
48
+ log(
49
+ `[data-portal] op=withdraw owner=${owner} key=${q(key)} ` +
50
+ `result=failed err="${(err as Error).message}"`,
51
+ )
52
+ return { status: 502, payload: { ok: false, error: 'withdraw failed' } }
53
+ }
54
+ // Re-read rather than assume the UPDATE landed: an unmarked row is a
55
+ // withdrawal the pull will never see, and nothing else would report it.
56
+ const marked = await env.DB.prepare('SELECT deleted FROM manifest WHERE objectKey = ?')
57
+ .bind(key)
58
+ .first<{ deleted: number }>()
59
+ const ok = marked?.deleted === 1
60
+ log(
61
+ `[data-portal] op=withdraw owner=${owner} key=${q(key)} devicePath=${q(row.devicePath)} ` +
62
+ `ms=${now() - startedMs} result=${ok ? 'marked' : 'not-marked'}`,
63
+ )
64
+ return {
65
+ status: ok ? 200 : 500,
66
+ payload: { ok, pending: true },
67
+ }
68
+ }
69
+
25
70
  // Both writes are wrapped and each emits its own line, mirroring upload's
26
71
  // steps 3-5. Unwrapped, a throw here escapes the handler, Pages returns a
27
72
  // bare 500, and NOTHING is logged — and if the R2 delete succeeded before the
@@ -2,10 +2,13 @@ import type { Handler, Logger, PortalEnv, R2ObjectBody } from './_lib/types'
2
2
  import { grantAllows, readSessionCookie, resolveSession } from './_lib/session'
3
3
  import { q } from './_lib/log'
4
4
  import { authorizeKey } from './_lib/authorize'
5
- import { buildSignedUrl, SIGNED_TTL_MS } from './_lib/portal-sign.mjs'
5
+ import { buildOwnUploadUrl, buildSignedUrl, SIGNED_TTL_MS } from './_lib/portal-sign.mjs'
6
6
 
7
7
  interface DownloadResult extends Handler {
8
8
  body?: R2ObjectBody | null
9
+ /** Set when the row has moved: the bytes are on the device, so the caller
10
+ * 302s to a signed own-upload link instead of streaming R2. */
11
+ url?: string
9
12
  }
10
13
 
11
14
  export async function processDownload(
@@ -25,9 +28,49 @@ export async function processDownload(
25
28
  log(`[data-portal] op=download owner=${q(session.ownerId)} result=denied reason=not-owner`)
26
29
  return { status: 403, payload: { ok: false, error: 'denied' } }
27
30
  }
31
+ // Where the bytes are decides how they are served (Task 1910). A moved row's
32
+ // R2 object has been deleted, so reading it here would 404 the client's own
33
+ // upload the moment the device collected it.
34
+ const row = await env.DB.prepare('SELECT devicePath FROM manifest WHERE objectKey = ?')
35
+ .bind(key)
36
+ .first<{ devicePath: string }>()
37
+ if (!row) {
38
+ log(`[data-portal] op=download owner=${q(session.ownerId)} result=absent`)
39
+ return { status: 404, payload: { ok: false, error: 'not found' } }
40
+ }
41
+
42
+ if (row.devicePath) {
43
+ const secret = env.PORTAL_FETCH_SECRET
44
+ const origin = env.PORTAL_INSTALL_ORIGIN
45
+ if (!secret || !origin) {
46
+ // Reported as offline rather than a server error, the same judgement the
47
+ // indexed path makes: from the client's side the file genuinely is
48
+ // unreachable, and the operator's signal is this line, not a 500.
49
+ log(`[data-portal] op=download owner=${q(session.ownerId)} result=unconfigured`)
50
+ return { status: 503, payload: { ok: false, error: 'device-offline' } }
51
+ }
52
+ const expiresAt = nowMs + SIGNED_TTL_MS
53
+ const url = await buildOwnUploadUrl(
54
+ origin,
55
+ secret,
56
+ session.accountId,
57
+ session.ownerId,
58
+ row.devicePath,
59
+ expiresAt,
60
+ )
61
+ log(
62
+ `[data-portal] op=download owner=${q(session.ownerId)} key=${q(key)} ` +
63
+ `devicePath=${q(row.devicePath)} result=moved`,
64
+ )
65
+ return { status: 200, payload: { ok: true, url }, url }
66
+ }
67
+
28
68
  const obj = await env.BUCKET.get(key)
29
69
  if (!obj) {
30
- log(`[data-portal] op=download owner=${q(session.ownerId)} result=absent`)
70
+ // The row says the bytes are in R2 and they are not. That is a phantom row,
71
+ // which emits nothing at request time anywhere else — the audit's
72
+ // reconcile is what counts them, and this line is what dates one.
73
+ log(`[data-portal] op=download owner=${q(session.ownerId)} result=absent reason=no-object`)
31
74
  return { status: 404, payload: { ok: false, error: 'not found' } }
32
75
  }
33
76
  log(`[data-portal] op=download owner=${q(session.ownerId)} result=ok bytes=${obj.size ?? 'na'}`)
@@ -83,15 +126,33 @@ export async function processIndexedDownload(
83
126
  return { status: 404, payload: { ok: false, error: 'not found' } }
84
127
  }
85
128
 
86
- const row = await env.DB.prepare('SELECT relPath FROM directory WHERE accountId = ? AND relPath = ?')
129
+ const row = await env.DB.prepare(
130
+ // Through the generation pointer, like every other reader: a row staged by
131
+ // a push in flight must not be servable before the flip blesses it.
132
+ `SELECT d.relPath AS relPath, d.isDir AS isDir
133
+ FROM directory d
134
+ JOIN directory_state s
135
+ ON s.accountId = d.accountId AND s.currentGeneration = d.generation
136
+ WHERE d.accountId = ? AND d.relPath = ?`,
137
+ )
87
138
  .bind(session.accountId, relPath)
88
- .first<{ relPath: string }>()
139
+ .first<{ relPath: string; isDir: number }>()
89
140
  if (!row) {
90
141
  log(
91
142
  `[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} result=absent`,
92
143
  )
93
144
  return { status: 404, payload: { ok: false, error: 'not found' } }
94
145
  }
146
+ if (row.isDir) {
147
+ // Folders are in the index, so a client can name one here. A folder has no
148
+ // bytes: minting a link would send the device a path it cannot serve, and
149
+ // the browser an opaque failure. Refused before the probe, and reported
150
+ // distinguishably from not-granted and absent.
151
+ log(
152
+ `[data-portal] op=fetch-through owner=${q(session.ownerId)} path=${q(relPath)} result=is-directory`,
153
+ )
154
+ return { status: 404, payload: { ok: false, error: 'not found' } }
155
+ }
95
156
 
96
157
  const secret = env.PORTAL_FETCH_SECRET
97
158
  const origin = env.PORTAL_INSTALL_ORIGIN
@@ -164,13 +225,19 @@ export async function onRequestGet(context: PagesContext): Promise<Response> {
164
225
  }
165
226
 
166
227
  const key = params.get('key') ?? ''
167
- const { status, payload, body } = await processDownload(
228
+ const { status, payload, body, url } = await processDownload(
168
229
  sessionId,
169
230
  key,
170
231
  context.env,
171
232
  (line) => console.log(line),
172
233
  Date.now(),
173
234
  )
235
+ // A moved row has no bytes here to stream. Both the click path and a drag-out
236
+ // follow a 302 to the device, so this needs no redirect=1 discriminator the
237
+ // way the indexed path does: there is no JSON alternative to fall back to.
238
+ if (status === 200 && url) {
239
+ return Response.redirect(url, 302)
240
+ }
174
241
  if (status === 200 && body) {
175
242
  return new Response(body.body, {
176
243
  status: 200,
@@ -6,6 +6,7 @@ interface DirRow {
6
6
  relPath: string
7
7
  sizeBytes: number
8
8
  modifiedAt: string
9
+ isDir: number
9
10
  }
10
11
 
11
12
  export interface DirEntry {
@@ -19,10 +20,12 @@ export interface DirEntry {
19
20
  /**
20
21
  * Collapse the flat `directory` rows into one level of listing under `prefix`.
21
22
  *
22
- * Folders are synthesised from path segments rather than stored, so the device
23
- * publishes files alone and an empty folder simply does not exist. The prefix
24
- * matches on a whole segment (`quotes/`, not `quotes`), so a partial name can
25
- * never reach a sibling folder's contents.
23
+ * The device publishes a row per folder as well as per file, so a folder
24
+ * holding no files at any depth is a real entry that opens and reads as empty.
25
+ * Folders are ALSO derived from the path segments of deeper rows, and the two
26
+ * sources collapse onto one entry per name. The prefix matches on a whole
27
+ * segment (`quotes/`, not `quotes`), so a partial name can never reach a
28
+ * sibling folder's contents.
26
29
  */
27
30
  export function entriesUnder(rows: DirRow[], prefix: string): DirEntry[] {
28
31
  const base = prefix ? `${prefix}/` : ''
@@ -34,6 +37,21 @@ export function entriesUnder(rows: DirRow[], prefix: string): DirEntry[] {
34
37
  if (!rest) continue
35
38
  const slash = rest.indexOf('/')
36
39
  if (slash === -1) {
40
+ // A folder's own row has nothing left after the prefix, exactly like a
41
+ // file's. `isDir` is the only thing separating them; without it a folder
42
+ // lands in the file list and a click tries to download it.
43
+ if (row.isDir) {
44
+ if (!dirs.has(rest)) {
45
+ dirs.set(rest, {
46
+ name: rest,
47
+ kind: 'directory',
48
+ relPath: row.relPath,
49
+ sizeBytes: null,
50
+ modifiedAt: null,
51
+ })
52
+ }
53
+ continue
54
+ }
37
55
  files.push({
38
56
  name: rest,
39
57
  kind: 'file',
@@ -62,6 +80,33 @@ export function entriesUnder(rows: DirRow[], prefix: string): DirEntry[] {
62
80
  return [...dirList, ...files]
63
81
  }
64
82
 
83
+ /**
84
+ * Whether an upload row belongs in the client's own uploads panel (Task 1910).
85
+ *
86
+ * One rule covers both halves of the move: a row is listed while its bytes are
87
+ * still in R2, and after the move ONLY when the file is not reachable in the
88
+ * folder tree.
89
+ *
90
+ * So a targeted file leaves the panel and appears in the folder it was dropped
91
+ * into, and an untargeted file — which lands in `uploads/`, never exposed —
92
+ * stays in the panel and downloads through the device. Nothing is listed twice
93
+ * and nothing becomes unreachable.
94
+ *
95
+ * A withdrawn row is hidden while the device removal is pending. That is a
96
+ * brief state, not a tombstone filter: the row is deleted outright once the
97
+ * device file is confirmed absent.
98
+ */
99
+ export function shouldListUpload(
100
+ row: { devicePath: string; deleted: number },
101
+ visibleTops: Set<string>,
102
+ ): boolean {
103
+ if (row.deleted) return false
104
+ if (!row.devicePath) return true
105
+ // The whole top segment, never a prefix — `job` must not satisfy `jobs`, the
106
+ // same rule grantAllows follows for the same reason.
107
+ return !visibleTops.has(row.devicePath.split('/')[0])
108
+ }
109
+
65
110
  export async function processFiles(
66
111
  sessionId: string,
67
112
  env: PortalEnv,
@@ -76,34 +121,56 @@ export async function processFiles(
76
121
  }
77
122
  // Scoped by ownerId, not by key prefix: the owner is the authority.
78
123
  const rows = await env.DB.prepare(
79
- 'SELECT fileId, filename, objectKey, size, uploadedAt, ingested FROM manifest WHERE ownerId = ? ORDER BY uploadedAt DESC',
124
+ 'SELECT fileId, filename, objectKey, size, uploadedAt, ingested, targetPath, devicePath, deleted ' +
125
+ 'FROM manifest WHERE ownerId = ? ORDER BY uploadedAt DESC',
80
126
  )
81
127
  .bind(session.ownerId)
82
128
  .all<Record<string, unknown>>()
83
- const files = rows.results ?? []
129
+ const allFiles = rows.results ?? []
84
130
 
85
- // The account's published folder index, scoped by accountId. No account bound
86
- // means no folders: an enrolment predating that column lands here, and the
87
- // empty `account=` field in the log line is what says so.
131
+ // The account's published folder index, scoped by accountId and read through
132
+ // the generation pointer so a push in flight is never half-visible. No
133
+ // account bound means no folders: an enrolment predating that column lands
134
+ // here, and the empty `account=` field in the log line is what says so.
88
135
  let entries: DirEntry[] = []
89
136
  const grant = session.folders
137
+ // The tops the client can actually browse. Built from the SAME grant-filtered
138
+ // rows the tree is built from, so the uploads panel and the tree can never
139
+ // disagree about what is reachable.
140
+ const visibleTops = new Set<string>()
90
141
  if (session.accountId) {
91
142
  const dir = await env.DB.prepare(
92
- 'SELECT relPath, sizeBytes, modifiedAt FROM directory WHERE accountId = ?',
143
+ `SELECT d.relPath AS relPath, d.sizeBytes AS sizeBytes, d.modifiedAt AS modifiedAt,
144
+ d.isDir AS isDir
145
+ FROM directory d
146
+ JOIN directory_state s
147
+ ON s.accountId = d.accountId AND s.currentGeneration = d.generation
148
+ WHERE d.accountId = ?`,
93
149
  )
94
150
  .bind(session.accountId)
95
151
  .all<DirRow>()
96
152
  // The grant narrows below the account's exposed set: keep only rows the
97
153
  // grant admits. An empty grant admits everything, which is the full set and
98
154
  // today's behaviour.
99
- const rows = (dir.results ?? []).filter((row) => grantAllows(grant, row.relPath))
100
- entries = entriesUnder(rows, prefix)
155
+ const granted = (dir.results ?? []).filter((row) => grantAllows(grant, row.relPath))
156
+ for (const row of granted) visibleTops.add(row.relPath.split('/')[0])
157
+ entries = entriesUnder(granted, prefix)
101
158
  }
102
159
 
160
+ // Filtered AFTER the directory read, because the rule needs to know what the
161
+ // client can reach. A moved file is not gone; it is listed where it lives.
162
+ const files = allFiles.filter((f) =>
163
+ shouldListUpload(
164
+ { devicePath: String(f.devicePath ?? ''), deleted: Number(f.deleted ?? 0) },
165
+ visibleTops,
166
+ ),
167
+ )
168
+
103
169
  log(
104
170
  `[data-portal] op=list owner=${q(session.ownerId)} account=${q(session.accountId)} ` +
105
171
  `granted=${grant.length === 0 ? 'full' : grant.length} ` +
106
- `prefix=${q(prefix)} files=${files.length} entries=${entries.length} result=ok`,
172
+ `prefix=${q(prefix)} files=${files.length} moved=${allFiles.length - files.length} ` +
173
+ `entries=${entries.length} result=ok`,
107
174
  )
108
175
  return { status: 200, payload: { ok: true, files, entries } }
109
176
  }