@rubytech/create-maxy-code 0.1.377 → 0.1.378

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 (153) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +25 -4
  3. package/payload/platform/plugins/docs/references/admin-ui.md +24 -3
  4. package/payload/platform/scripts/lib/read-brand-json.sh +19 -0
  5. package/payload/platform/scripts/seed-neo4j.sh +44 -3
  6. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  7. package/payload/platform/services/claude-session-manager/dist/http-server.js +100 -13
  8. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  9. package/payload/server/{chunk-Y4SHSFGK.js → chunk-AQO6KLIH.js} +22 -0
  10. package/payload/server/maxy-edge.js +1 -1
  11. package/payload/server/public/assets/{AdminLoginScreens-DVjwrcZa.js → AdminLoginScreens-B9u1V35g.js} +1 -1
  12. package/payload/server/public/assets/AdminShell-C2UXTLSB.js +1 -0
  13. package/payload/server/public/assets/{Checkbox-B8dm2kDD.js → Checkbox-B2g2SCpw.js} +1 -1
  14. package/payload/server/public/assets/admin-CRBQgWsm.js +1 -0
  15. package/payload/server/public/assets/{arc-Crkz45LN.js → arc-JgkiMDpu.js} +1 -1
  16. package/payload/server/public/assets/architecture-YZFGNWBL-Dh5BzVzk.js +1 -0
  17. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C9JThHgP.js → architectureDiagram-Q4EWVU46-CyIbl1bD.js} +1 -1
  18. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DpsV_HXT.js → blockDiagram-DXYQGD6D-CkcZVblf.js} +1 -1
  19. package/payload/server/public/assets/{browser-BMyQHWKS.js → browser-B8omhYsK.js} +1 -1
  20. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CTm_6aKi.js → c4Diagram-AHTNJAMY-VByNuobb.js} +1 -1
  21. package/payload/server/public/assets/calendar-Blb-5arm.js +1 -0
  22. package/payload/server/public/assets/channel-CFHc3PQ7.js +1 -0
  23. package/payload/server/public/assets/chat--1ajLxwi.js +1 -0
  24. package/payload/server/public/assets/{chunk-2KRD3SAO-1KSJW1q6.js → chunk-2KRD3SAO-BFwiGcqo.js} +1 -1
  25. package/payload/server/public/assets/{chunk-336JU56O-C3t08Ip-.js → chunk-336JU56O-BMCBU-vf.js} +2 -2
  26. package/payload/server/public/assets/chunk-426QAEUC-De9_O1hi.js +1 -0
  27. package/payload/server/public/assets/{chunk-4BX2VUAB-kl1ze33n.js → chunk-4BX2VUAB-RfOeo8_F.js} +1 -1
  28. package/payload/server/public/assets/{chunk-4TB4RGXK-DxpoDoHg.js → chunk-4TB4RGXK-B5xbgonc.js} +1 -1
  29. package/payload/server/public/assets/{chunk-55IACEB6-WM9VJzha.js → chunk-55IACEB6-K8U55ikJ.js} +1 -1
  30. package/payload/server/public/assets/{chunk-5FUZZQ4R-CLH63ZH3.js → chunk-5FUZZQ4R-DgmmsrAc.js} +1 -1
  31. package/payload/server/public/assets/{chunk-5PVQY5BW-DCTb8IAT.js → chunk-5PVQY5BW-DQ6rtehF.js} +1 -1
  32. package/payload/server/public/assets/{chunk-67CJDMHE-BSYzyaoH.js → chunk-67CJDMHE-CC2kfpmQ.js} +1 -1
  33. package/payload/server/public/assets/{chunk-7N4EOEYR-CarJ7O2P.js → chunk-7N4EOEYR-BClZ5bzw.js} +1 -1
  34. package/payload/server/public/assets/{chunk-AA7GKIK3-DTNLyBpH.js → chunk-AA7GKIK3-adtwAFKX.js} +1 -1
  35. package/payload/server/public/assets/{chunk-BSJP7CBP-BFmHflHY.js → chunk-BSJP7CBP-fKY1UWEd.js} +1 -1
  36. package/payload/server/public/assets/{chunk-CIAEETIT-BpjG0IzU.js → chunk-CIAEETIT-knPAMmjG.js} +1 -1
  37. package/payload/server/public/assets/{chunk-EDXVE4YY-2fSowhlW.js → chunk-EDXVE4YY-C7FRKZ5e.js} +1 -1
  38. package/payload/server/public/assets/{chunk-ENJZ2VHE-ClcranGV.js → chunk-ENJZ2VHE-0cgFfv3z.js} +1 -1
  39. package/payload/server/public/assets/{chunk-FMBD7UC4-DLq4BItQ.js → chunk-FMBD7UC4-Dm_Hcblb.js} +1 -1
  40. package/payload/server/public/assets/{chunk-FOC6F5B3-B9QI3Guz.js → chunk-FOC6F5B3-CfCJNXmK.js} +1 -1
  41. package/payload/server/public/assets/{chunk-ICPOFSXX-BAr-Mv1Z.js → chunk-ICPOFSXX-xeu6qrZj.js} +2 -2
  42. package/payload/server/public/assets/{chunk-K5T4RW27-8PvAHasY.js → chunk-K5T4RW27-BQDCSuTh.js} +1 -1
  43. package/payload/server/public/assets/{chunk-KGLVRYIC--aGaDRH5.js → chunk-KGLVRYIC-D6fFAgbM.js} +1 -1
  44. package/payload/server/public/assets/{chunk-LIHQZDEY-BrWj7mw2.js → chunk-LIHQZDEY-Dskwlc-K.js} +1 -1
  45. package/payload/server/public/assets/{chunk-ORNJ4GCN-kYQmkMTi.js → chunk-ORNJ4GCN-CnHIrJeA.js} +1 -1
  46. package/payload/server/public/assets/{chunk-OYMX7WX6-CIsryCWH.js → chunk-OYMX7WX6-B4bnsPkv.js} +1 -1
  47. package/payload/server/public/assets/chunk-QZHKN3VN-COwe7oXR.js +1 -0
  48. package/payload/server/public/assets/{chunk-U2HBQHQK-68X653e3.js → chunk-U2HBQHQK-BeEXzOqV.js} +1 -1
  49. package/payload/server/public/assets/{chunk-X2U36JSP-v3v9soX3.js → chunk-X2U36JSP-D2rpgOqd.js} +1 -1
  50. package/payload/server/public/assets/{chunk-XPW4576I-CW5DPpop.js → chunk-XPW4576I-D-yT82Xf.js} +1 -1
  51. package/payload/server/public/assets/{chunk-YZCP3GAM-B1VP7Rl2.js → chunk-YZCP3GAM-DqxlLBm1.js} +1 -1
  52. package/payload/server/public/assets/{chunk-ZZ45TVLE-DiJrujNY.js → chunk-ZZ45TVLE-CA_Ep0OM.js} +1 -1
  53. package/payload/server/public/assets/classDiagram-6PBFFD2Q-wjL65qaU.js +1 -0
  54. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D_EIplVd.js +1 -0
  55. package/payload/server/public/assets/clone-CEczy_Ef.js +1 -0
  56. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-BscXxLMc.js → cose-bilkent-S5V4N54A-DWb9mxTa.js} +1 -1
  57. package/payload/server/public/assets/{dagre-MBv6WlCS.js → dagre-B3K7jxOZ.js} +1 -1
  58. package/payload/server/public/assets/{dagre-KV5264BT-DVgGlTQ1.js → dagre-KV5264BT-BhiTISRq.js} +1 -1
  59. package/payload/server/public/assets/data-BYUmu98z.js +1 -0
  60. package/payload/server/public/assets/{diagram-5BDNPKRD-BmhCaO0a.js → diagram-5BDNPKRD-D5yBIDpm.js} +1 -1
  61. package/payload/server/public/assets/{diagram-G4DWMVQ6-ChU9L8vz.js → diagram-G4DWMVQ6-CSTE1S4d.js} +1 -1
  62. package/payload/server/public/assets/{diagram-MMDJMWI5-Oy409Zc_.js → diagram-MMDJMWI5-D8llv4XY.js} +1 -1
  63. package/payload/server/public/assets/{diagram-TYMM5635-Cn1YByqk.js → diagram-TYMM5635-CITQ7aDM.js} +1 -1
  64. package/payload/server/public/assets/{erDiagram-SMLLAGMA-e17yfLvr.js → erDiagram-SMLLAGMA-BjR5nlmv.js} +1 -1
  65. package/payload/server/public/assets/{flatten-Cl1Bc3dR.js → flatten-DhYYI_lo.js} +1 -1
  66. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-Dqt7sCVZ.js → flowDiagram-DWJPFMVM-Dl6e-E0f.js} +1 -1
  67. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B4IhwS2r.js → ganttDiagram-T4ZO3ILL-CppkrFAl.js} +1 -1
  68. package/payload/server/public/assets/gitGraph-7Q5UKJZL-KHAc7IxG.js +1 -0
  69. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BDw-PKcI.js → gitGraphDiagram-UUTBAWPF-clV_tnwa.js} +1 -1
  70. package/payload/server/public/assets/{graph-DF_nLsTp.js → graph-DJOKFr_s.js} +3 -3
  71. package/payload/server/public/assets/{graph-labels-HWQ5J7S-.js → graph-labels-DFdBydWC.js} +1 -1
  72. package/payload/server/public/assets/{graphlib-DPIYk_sg.js → graphlib-Cnk6WL-8.js} +1 -1
  73. package/payload/server/public/assets/info-OMHHGYJF-C6nm3sYf.js +1 -0
  74. package/payload/server/public/assets/infoDiagram-42DDH7IO-DhwCejsr.js +2 -0
  75. package/payload/server/public/assets/{isEmpty-D1pGOD1J.js → isEmpty-DGhq_DBP.js} +1 -1
  76. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BRIHsB3G.js → ishikawaDiagram-UXIWVN3A-DUqusmP1.js} +1 -1
  77. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-aKNgmoHd.js → journeyDiagram-VCZTEJTY-RMEvGtDJ.js} +1 -1
  78. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-cwBwT-nm.js → kanban-definition-6JOO6SKY-BxkwQPCP.js} +1 -1
  79. package/payload/server/public/assets/{line-DVtVRzUA.js → line-CiOuSBlJ.js} +1 -1
  80. package/payload/server/public/assets/{linear-DtoOFPO2.js → linear-D22sItNm.js} +1 -1
  81. package/payload/server/public/assets/{mermaid-parser.core-G7YD_zO3.js → mermaid-parser.core-kvN8ajT-.js} +2 -2
  82. package/payload/server/public/assets/{mermaid.core-DpFyndHm.js → mermaid.core-DKHTTwbg.js} +3 -3
  83. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CYXK_Pvz.js → mindmap-definition-QFDTVHPH-CKUJxwdG.js} +1 -1
  84. package/payload/server/public/assets/operator-DUVqlubZ.js +1 -0
  85. package/payload/server/public/assets/{ordinal-CBZeH4Yp.js → ordinal-CibZU3PM.js} +1 -1
  86. package/payload/server/public/assets/packet-4T2RLAQJ-z-APhyNH.js +1 -0
  87. package/payload/server/public/assets/page-BMeSKYGZ.js +32 -0
  88. package/payload/server/public/assets/page-DBkTNfei.js +1 -0
  89. package/payload/server/public/assets/pie-ZZUOXDRM-D_HVZjir.js +1 -0
  90. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DFo4N6WH.js → pieDiagram-DEJITSTG-CRMabeJN.js} +1 -1
  91. package/payload/server/public/assets/{public-NNHS6mWb.js → public-CGl0i1f-.js} +1 -1
  92. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BlH5iIhl.js → quadrantDiagram-34T5L4WZ-DcrlhDuj.js} +1 -1
  93. package/payload/server/public/assets/radar-PYXPWWZC-DtKBIb3o.js +1 -0
  94. package/payload/server/public/assets/{reduce-PuPlFPRX.js → reduce-Da0RBva0.js} +1 -1
  95. package/payload/server/public/assets/{requirementDiagram-MS252O5E-D5WLty8A.js → requirementDiagram-MS252O5E-DP231rM_.js} +1 -1
  96. package/payload/server/public/assets/{rotate-ccw-CKSJX39F.js → rotate-ccw-Cg_V4XIU.js} +1 -1
  97. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-vB85IxHG.js → sankeyDiagram-XADWPNL6--HM5_dC8.js} +1 -1
  98. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Cxraa0Ee.js → sequenceDiagram-FGHM5R23-BsiVmDR9.js} +1 -1
  99. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BBPssYBx.js → stateDiagram-FHFEXIEX-Cjdhqdjh.js} +1 -1
  100. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-6v4alr7n.js +1 -0
  101. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Bgk6PT62.js → timeline-definition-GMOUNBTQ-Cf5xPmSA.js} +1 -1
  102. package/payload/server/public/assets/treeView-SZITEDCU-DbXWs029.js +1 -0
  103. package/payload/server/public/assets/treemap-W4RFUUIX-DUGgqAs9.js +1 -0
  104. package/payload/server/public/assets/useSubAccountSwitcher-DAgBUed_.js +9 -0
  105. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-B-APTs3L.js → vennDiagram-DHZGUBPP-PCor55Sw.js} +1 -1
  106. package/payload/server/public/assets/wardley-RL74JXVD-Cjn85RON.js +1 -0
  107. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CkXJSs_i.js → wardleyDiagram-NUSXRM2D-bokCHsim.js} +1 -1
  108. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BkyshPMV.js → xychartDiagram-5P7HB3ND-B2YnsUoQ.js} +1 -1
  109. package/payload/server/public/browser.html +3 -3
  110. package/payload/server/public/calendar.html +3 -3
  111. package/payload/server/public/chat.html +6 -6
  112. package/payload/server/public/data.html +4 -3
  113. package/payload/server/public/graph.html +6 -6
  114. package/payload/server/public/index.html +7 -5
  115. package/payload/server/public/operator.html +8 -8
  116. package/payload/server/public/public.html +6 -6
  117. package/payload/server/server.js +43 -32
  118. package/payload/server/public/assets/AdminShell-CNQxSO9B.js +0 -1
  119. package/payload/server/public/assets/admin-Dfv2e8fn.js +0 -1
  120. package/payload/server/public/assets/architecture-YZFGNWBL-Dma4Bgha.js +0 -1
  121. package/payload/server/public/assets/calendar-CG-2rQ4m.js +0 -1
  122. package/payload/server/public/assets/channel-B_-5bau1.js +0 -1
  123. package/payload/server/public/assets/chat-CRPVhDCc.js +0 -1
  124. package/payload/server/public/assets/chunk-426QAEUC-Dirxdwjh.js +0 -1
  125. package/payload/server/public/assets/chunk-QZHKN3VN-Duipo1kI.js +0 -1
  126. package/payload/server/public/assets/classDiagram-6PBFFD2Q-B3kX5SuB.js +0 -1
  127. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D761c7Lr.js +0 -1
  128. package/payload/server/public/assets/clone-BrhFv6Tb.js +0 -1
  129. package/payload/server/public/assets/data-Bodds4fv.js +0 -1
  130. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BCQAqtLy.js +0 -1
  131. package/payload/server/public/assets/info-OMHHGYJF-qU7hK50T.js +0 -1
  132. package/payload/server/public/assets/infoDiagram-42DDH7IO-KGdcJeTs.js +0 -2
  133. package/payload/server/public/assets/operator-DH9OAMpe.js +0 -1
  134. package/payload/server/public/assets/packet-4T2RLAQJ-DiU2yS9m.js +0 -1
  135. package/payload/server/public/assets/page--Uj3qG5S.js +0 -32
  136. package/payload/server/public/assets/pie-ZZUOXDRM-zHUKKlVv.js +0 -1
  137. package/payload/server/public/assets/radar-PYXPWWZC-BUnVoaPg.js +0 -1
  138. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BWmmUwDt.js +0 -1
  139. package/payload/server/public/assets/treeView-SZITEDCU-C3Y-Q52h.js +0 -1
  140. package/payload/server/public/assets/treemap-W4RFUUIX-Dk0qZpAg.js +0 -1
  141. package/payload/server/public/assets/useSubAccountSwitcher-DRITc7qp.js +0 -9
  142. package/payload/server/public/assets/wardley-RL74JXVD-BAWI02OA.js +0 -1
  143. /package/payload/server/public/assets/{_baseFor-brRjpUOX.js → _baseFor-BFYpZGkN.js} +0 -0
  144. /package/payload/server/public/assets/{array-BGFCBI0e.js → array-CYkMkqnU.js} +0 -0
  145. /package/payload/server/public/assets/{cytoscape.esm-TvRJ2tGX.js → cytoscape.esm-DqlHsaog.js} +0 -0
  146. /package/payload/server/public/assets/{defaultLocale-CRZydyG6.js → defaultLocale-Beh6XjaL.js} +0 -0
  147. /package/payload/server/public/assets/{dist-B0CkUodR.js → dist-BMqkacmt.js} +0 -0
  148. /package/payload/server/public/assets/{init-B8gtcn7T.js → init-Rr1s_RiX.js} +0 -0
  149. /package/payload/server/public/assets/{katex-RxgSu_dy.js → katex-pVJiI_rc.js} +0 -0
  150. /package/payload/server/public/assets/{path-DZF-JdEe.js → path-BAQ3hXlG.js} +0 -0
  151. /package/payload/server/public/assets/{preload-helper-CQ36nxok.js → preload-helper-B_l5jfgx.js} +0 -0
  152. /package/payload/server/public/assets/{rough.esm-6CnTHTkH.js → rough.esm-nHaDi0Kw.js} +0 -0
  153. /package/payload/server/public/assets/{src-CR_MU8uy.js → src-CsEKjNXD.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.377",
3
+ "version": "0.1.378",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:e15d80a5ac347f9eb8167ecdc10e2ce092db72ca3d6dfa5d0d8ac85d6676ec2a
4
+ content-hash: sha256:7346bd77231c42932ee9d106a401168e51098338ea8967d9fbadbd61f4740144
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2531,7 +2531,7 @@ either is a regression.
2531
2531
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
2532
2532
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
2533
2533
  | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
2534
- | `/session-delete` | POST `{ sessionId, projectDir }`. Best-effort SIGTERM of the live PID (resolved from `sessions/<pid>.json` body match) then unlink the JSONL + `<sid>.meta.json` sidecar. Absent PID file is not an error. Containment: `projectDir` must live under `<CLAUDE_CONFIG_DIR>/projects/`. | `POST /` |
2534
+ | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Forwards the caller's canonical `accountId` (`getAccountIdForSession(cacheKey)`) as `?accountId=<uuid>` on both calls. The manager's fs-watcher is armed only on the house account's project slug, so a session under any other account has no watcher row; without the forwarded accountId the manager 404s a live cross-account session on stop (silently swallowed by this route's `stop.status !== 404` tolerance) and its transcript on delete. With it, the manager derives the project slug from the account (`<accountsRoot>/<accountId>` → slug), gates on file existence alone, and (stop) reads the on-disk scope record to kill a cross-account scope unit that survived a manager restart. Absent accountId the manager keeps its watcher-row fallback. The sibling `/session-stop` (the standalone End control) forwards `accountId` identically, for the same reason. | `POST /` |
2535
2535
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2536
2536
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
2537
2537
 
@@ -2543,7 +2543,7 @@ either is a regression.
2543
2543
 
2544
2544
  **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
2545
2545
 
2546
- **`/chat` header conversation label.** When `/chat` has an active titled conversation, the admin/operator `HeaderMenu` centre shows that title in place of the brand icon + name (the `chat-logo` image is suppressed; the operator's first grid track keeps an empty width-reserved placeholder so centring is unchanged). `AdminChat` resolves the active session's title from the same `GET /api/admin/sidebar-sessions` list (active id = the `?session=` target, or the canonical session for the operator), adopting the row's `title` only when `titleSource {user, ai, first-message}`; a `prefix`-only row (the 8-char UUID fallback) or no matching row keeps the brand block. The label re-resolves on active-id change. One breadcrumb fires on every resolve: `[admin-ui] op=header-label sessionId=<id8> variant=<admin|operator> present=<bool> source=<user|ai|first-message|none> reason=<resolved|untitled|no-match|fetch-failed>`. The lite variant header is unchanged. Sessions named only by a resolved `personName` (public-webchat visitor / admin-webchat author rows, which stay `titleSource:'prefix'`) currently fall back to the icon; adopting those into the header label is tracked as a separate follow-up.
2546
+ **`/chat` header always shows the account name.** The admin/operator `HeaderMenu` centre always renders the active account's name alongside the brand logo, on every chat surface, regardless of the active conversation `headerTitle = businessName || brand.productName`. There is no per-conversation header title: the active conversation's label stays in the sidebar and the Conversations flyout (each resolves its own per-session title independently), never in the header. The account name is `LocalBusiness.name`, resolved by `fetchAccountName(accountId)` in `neo4j-store.ts` with **no theme gate** — unlike `fetchBranding` (which returns null unless a colour/logo property is also set, and stays the source for actual theming). The house account is seeded with `LocalBusiness.name = <brand productName>` (`seed-neo4j.sh`, coalesced ON MATCH so a re-seed backfills a name-less node and never clobbers a name the business-profile recorder set later), so the sub-account picker's house row shows the product name (e.g. "SiteDesk"), not the raw accountId UUID. When a name is absent the resolver emits `[branding] account-name-fallback accountId=<id8>… reason=no-name`, and the picker falls back to the accountId; the seed + backfill is the name source. The lite variant header is unchanged.
2547
2547
 
2548
2548
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
2549
2549
 
@@ -2618,7 +2618,19 @@ recreates that path's directories under the open folder, scope-checked level by
2618
2618
  level, and writes the file under its original basename. A flat upload (no
2619
2619
  `relpath`) is unchanged.
2620
2620
 
2621
- **`/data` operator surface.** `/data` mounts the operator
2621
+ **Data — admin Sidebar pane vs operator `/data` route.** The
2622
+ file/search UI is the headless `DataFileBrowser` component. On the **admin**
2623
+ dashboard it is a Sidebar surface: a "Data" nav row (beside Artefacts/Sessions)
2624
+ sets `activeNav='data'` and, via `onSelectData` → `AdminShell`'s `dataOpen`
2625
+ state, renders `DataFileBrowser` (through `DataPaneSurface`) in the shell's main
2626
+ column — chromed by the admin `HeaderMenu` + `Sidebar`, no full-page nav, no
2627
+ remount. Off `/` the row navigates to `/?data=1`, hydrated on mount
2628
+ (`hydrateDataFromUrl`); the pane is mutually exclusive with the WhatsApp
2629
+ Transcript. The admin burger no longer carries a Data item. The **operator**
2630
+ surface has no Sidebar, so the operator keeps the burger → `/data` full-screen
2631
+ route described next; only the admin path changed.
2632
+
2633
+ **`/data` operator surface.** The `/data` route mounts the operator
2622
2634
  `HeaderMenu` (logo-left / name-centre / burger-right; flyout = Chat / Data /
2623
2635
  Log out) above the toolbar — the burger's Chat item returns to `/`, replacing
2624
2636
  the former standalone Home button. The search bar flexes to fill its toolbar
@@ -2792,6 +2804,14 @@ authoritative. This section names the surfaces and what backs each.
2792
2804
  > operator surface is documented in full in the internal doc
2793
2805
  > `maxy-code/.docs/operator-dashboard.md`.
2794
2806
  >
2807
+ > **Operator `/chat`.** The reduced operator chrome renders on
2808
+ > *every* operator chat surface, not just root `/`: the `/chat` server handler
2809
+ > serves `operator.html` for an operator host (`chat.html` only for admin
2810
+ > hosts), so `/chat` and `/chat?session=<id>` mount the same single-column
2811
+ > operator shell. Unlike the operator root — which forces the canonical session
2812
+ > — operator `/chat` honors `?session=`, opening a specific session inside the
2813
+ > reduced shell. Admin-host `/chat` is unchanged (full developer shell).
2814
+ >
2795
2815
  > **Width.** The operator header, the operator chat column, and the `/data`
2796
2816
  > header+toolbar+body all render in one centred column bounded to
2797
2817
  > `--chat-measure` (48rem). That token is a `:root` design token (beside
@@ -2818,6 +2838,7 @@ authoritative. This section names the surfaces and what backs each.
2818
2838
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
2819
2839
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so Maxy reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
2820
2840
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
2841
+ | Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
2821
2842
  | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row carries a `SessionRowActions` cluster whose sole action is Re-seat (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the cluster can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row — wide it is the inline sliders icon, and below the 400 px width it folds into the `…` overflow menu identically to the Sessions rows; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
2822
2843
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover between consecutive turns so a thread spanning midnight is unambiguous. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
2823
2844
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
@@ -57,7 +57,7 @@ either is a regression.
57
57
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
58
58
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
59
59
  | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
60
- | `/session-delete` | POST `{ sessionId, projectDir }`. Best-effort SIGTERM of the live PID (resolved from `sessions/<pid>.json` body match) then unlink the JSONL + `<sid>.meta.json` sidecar. Absent PID file is not an error. Containment: `projectDir` must live under `<CLAUDE_CONFIG_DIR>/projects/`. | `POST /` |
60
+ | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Forwards the caller's canonical `accountId` (`getAccountIdForSession(cacheKey)`) as `?accountId=<uuid>` on both calls. The manager's fs-watcher is armed only on the house account's project slug, so a session under any other account has no watcher row; without the forwarded accountId the manager 404s a live cross-account session on stop (silently swallowed by this route's `stop.status !== 404` tolerance) and its transcript on delete. With it, the manager derives the project slug from the account (`<accountsRoot>/<accountId>` → slug), gates on file existence alone, and (stop) reads the on-disk scope record to kill a cross-account scope unit that survived a manager restart. Absent accountId the manager keeps its watcher-row fallback. The sibling `/session-stop` (the standalone End control) forwards `accountId` identically, for the same reason. | `POST /` |
61
61
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
62
62
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
63
63
 
@@ -69,7 +69,7 @@ either is a regression.
69
69
 
70
70
  **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
71
71
 
72
- **`/chat` header conversation label.** When `/chat` has an active titled conversation, the admin/operator `HeaderMenu` centre shows that title in place of the brand icon + name (the `chat-logo` image is suppressed; the operator's first grid track keeps an empty width-reserved placeholder so centring is unchanged). `AdminChat` resolves the active session's title from the same `GET /api/admin/sidebar-sessions` list (active id = the `?session=` target, or the canonical session for the operator), adopting the row's `title` only when `titleSource {user, ai, first-message}`; a `prefix`-only row (the 8-char UUID fallback) or no matching row keeps the brand block. The label re-resolves on active-id change. One breadcrumb fires on every resolve: `[admin-ui] op=header-label sessionId=<id8> variant=<admin|operator> present=<bool> source=<user|ai|first-message|none> reason=<resolved|untitled|no-match|fetch-failed>`. The lite variant header is unchanged. Sessions named only by a resolved `personName` (public-webchat visitor / admin-webchat author rows, which stay `titleSource:'prefix'`) currently fall back to the icon; adopting those into the header label is tracked as a separate follow-up.
72
+ **`/chat` header always shows the account name.** The admin/operator `HeaderMenu` centre always renders the active account's name alongside the brand logo, on every chat surface, regardless of the active conversation `headerTitle = businessName || brand.productName`. There is no per-conversation header title: the active conversation's label stays in the sidebar and the Conversations flyout (each resolves its own per-session title independently), never in the header. The account name is `LocalBusiness.name`, resolved by `fetchAccountName(accountId)` in `neo4j-store.ts` with **no theme gate** — unlike `fetchBranding` (which returns null unless a colour/logo property is also set, and stays the source for actual theming). The house account is seeded with `LocalBusiness.name = <brand productName>` (`seed-neo4j.sh`, coalesced ON MATCH so a re-seed backfills a name-less node and never clobbers a name the business-profile recorder set later), so the sub-account picker's house row shows the product name (e.g. "SiteDesk"), not the raw accountId UUID. When a name is absent the resolver emits `[branding] account-name-fallback accountId=<id8>… reason=no-name`, and the picker falls back to the accountId; the seed + backfill is the name source. The lite variant header is unchanged.
73
73
 
74
74
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
75
75
 
@@ -144,7 +144,19 @@ recreates that path's directories under the open folder, scope-checked level by
144
144
  level, and writes the file under its original basename. A flat upload (no
145
145
  `relpath`) is unchanged.
146
146
 
147
- **`/data` operator surface.** `/data` mounts the operator
147
+ **Data — admin Sidebar pane vs operator `/data` route.** The
148
+ file/search UI is the headless `DataFileBrowser` component. On the **admin**
149
+ dashboard it is a Sidebar surface: a "Data" nav row (beside Artefacts/Sessions)
150
+ sets `activeNav='data'` and, via `onSelectData` → `AdminShell`'s `dataOpen`
151
+ state, renders `DataFileBrowser` (through `DataPaneSurface`) in the shell's main
152
+ column — chromed by the admin `HeaderMenu` + `Sidebar`, no full-page nav, no
153
+ remount. Off `/` the row navigates to `/?data=1`, hydrated on mount
154
+ (`hydrateDataFromUrl`); the pane is mutually exclusive with the WhatsApp
155
+ Transcript. The admin burger no longer carries a Data item. The **operator**
156
+ surface has no Sidebar, so the operator keeps the burger → `/data` full-screen
157
+ route described next; only the admin path changed.
158
+
159
+ **`/data` operator surface.** The `/data` route mounts the operator
148
160
  `HeaderMenu` (logo-left / name-centre / burger-right; flyout = Chat / Data /
149
161
  Log out) above the toolbar — the burger's Chat item returns to `/`, replacing
150
162
  the former standalone Home button. The search bar flexes to fill its toolbar
@@ -318,6 +330,14 @@ authoritative. This section names the surfaces and what backs each.
318
330
  > operator surface is documented in full in the internal doc
319
331
  > `maxy-code/.docs/operator-dashboard.md`.
320
332
  >
333
+ > **Operator `/chat`.** The reduced operator chrome renders on
334
+ > *every* operator chat surface, not just root `/`: the `/chat` server handler
335
+ > serves `operator.html` for an operator host (`chat.html` only for admin
336
+ > hosts), so `/chat` and `/chat?session=<id>` mount the same single-column
337
+ > operator shell. Unlike the operator root — which forces the canonical session
338
+ > — operator `/chat` honors `?session=`, opening a specific session inside the
339
+ > reduced shell. Admin-host `/chat` is unchanged (full developer shell).
340
+ >
321
341
  > **Width.** The operator header, the operator chat column, and the `/data`
322
342
  > header+toolbar+body all render in one centred column bounded to
323
343
  > `--chat-measure` (48rem). That token is a `:root` design token (beside
@@ -344,6 +364,7 @@ authoritative. This section names the surfaces and what backs each.
344
364
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
345
365
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so Maxy reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
346
366
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
367
+ | Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
347
368
  | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row carries a `SessionRowActions` cluster whose sole action is Re-seat (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the cluster can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row — wide it is the inline sliders icon, and below the 400 px width it folds into the `…` overflow menu identically to the Sessions rows; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
348
369
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover between consecutive turns so a thread spanning midnight is unambiguous. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
349
370
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
@@ -67,3 +67,22 @@ business_type_cypher() {
67
67
  echo "'$slug'"
68
68
  fi
69
69
  }
70
+
71
+ # cypher_string_literal <value>
72
+ # Emits a Cypher literal for an arbitrary string: the bare keyword null
73
+ # for empty input, otherwise the value wrapped in single quotes with
74
+ # backslashes and single quotes escaped. Unlike business_type_cypher
75
+ # (which quotes a closed-enum slug that needs no escaping), this is for
76
+ # free-text values such as brand.json#productName, so it escapes to keep
77
+ # an apostrophe (e.g. "O'Brien") from breaking the MERGE.
78
+ cypher_string_literal() {
79
+ local s="$1"
80
+ if [ -z "$s" ]; then
81
+ echo "null"
82
+ return 0
83
+ fi
84
+ local q="'"
85
+ s="${s//\\/\\\\}" # backslash -> two backslashes
86
+ s="${s//$q/\\$q}" # single quote -> backslash + quote
87
+ printf "%s\n" "$q$s$q"
88
+ }
@@ -125,6 +125,14 @@ LOCALBUSINESS_BUSINESS_TYPE=$(derive_business_type "$BRAND_JSON_VALUE")
125
125
  LOCALBUSINESS_BT_CYPHER=$(business_type_cypher "$LOCALBUSINESS_BUSINESS_TYPE")
126
126
  LOCALBUSINESS_BT_LOG="${LOCALBUSINESS_BUSINESS_TYPE:-none}"
127
127
 
128
+ # The house LocalBusiness carries the brand product name (e.g. "SiteDesk") so
129
+ # the sub-account picker and header render a name, not the raw accountId UUID.
130
+ # Coalesced ON MATCH below so a name set later by the business-profile recorder
131
+ # is never clobbered, and a re-seed backfills a name-less existing node.
132
+ read_brand_json_key "$BRAND_JSON" "productName"
133
+ LOCALBUSINESS_PRODUCT_NAME="$BRAND_JSON_VALUE"
134
+ LOCALBUSINESS_NAME_CYPHER=$(cypher_string_literal "$LOCALBUSINESS_PRODUCT_NAME")
135
+
128
136
  # Pre-count distinguishes created vs already-exists for the log line.
129
137
  # Simpler than an in-cypher transient flag; the extra round-trip is
130
138
  # sub-millisecond on a local Neo4j.
@@ -137,15 +145,32 @@ if ! LOCALBUSINESS_EXISTING_RAW=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PA
137
145
  fi
138
146
  LOCALBUSINESS_EXISTING=$(echo "$LOCALBUSINESS_EXISTING_RAW" | tail -1 | tr -d ' \r')
139
147
 
148
+ # Pre-read the current name so the log line reports set|preserved. A name-less
149
+ # or absent node is backfilled to the product name; an existing non-null name
150
+ # is preserved by the coalesce below. --format plain wraps a string value in
151
+ # double quotes, so strip them alongside whitespace.
152
+ if ! LOCALBUSINESS_PRENAME_RAW=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
153
+ --format plain \
154
+ "MATCH (lb:LocalBusiness {accountId: '$ACCOUNT_ID'}) RETURN coalesce(lb.name, '')" 2>&1); then
155
+ LOCALBUSINESS_REASON=$(echo "$LOCALBUSINESS_PRENAME_RAW" | tr '\n' '|' | head -c 500)
156
+ echo " [install-invariant] localbusiness-bootstrap accountId=${ACCOUNT_ID:0:8} result=failed reason=$LOCALBUSINESS_REASON" >&2
157
+ exit 1
158
+ fi
159
+ LOCALBUSINESS_PRENAME=$(echo "$LOCALBUSINESS_PRENAME_RAW" | tail -1 | tr -d ' \r"')
160
+
140
161
  # $ACCOUNT_ID is a UUID minted by the resolver; $LOCALBUSINESS_BT_CYPHER is
141
162
  # either a single-quoted slug from a closed enum (brands/*/brand.json) or the
163
+ # bare keyword null; $LOCALBUSINESS_NAME_CYPHER is a Cypher string literal from
164
+ # brand.json#productName, single-quote-escaped by cypher_string_literal, or the
142
165
  # bare keyword null. No operator input is interpolated.
143
166
  if ! LOCALBUSINESS_OUT=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" 2>&1 << CYPHER_EOF
144
167
  MERGE (lb:LocalBusiness {accountId: '$ACCOUNT_ID'})
145
168
  ON CREATE SET lb.createdBySource = 'installer',
146
169
  lb.createdAt = datetime(),
147
- lb.businessType = $LOCALBUSINESS_BT_CYPHER
148
- ON MATCH SET lb.businessType = coalesce(lb.businessType, $LOCALBUSINESS_BT_CYPHER)
170
+ lb.businessType = $LOCALBUSINESS_BT_CYPHER,
171
+ lb.name = $LOCALBUSINESS_NAME_CYPHER
172
+ ON MATCH SET lb.businessType = coalesce(lb.businessType, $LOCALBUSINESS_BT_CYPHER),
173
+ lb.name = coalesce(lb.name, $LOCALBUSINESS_NAME_CYPHER)
149
174
  RETURN lb.accountId;
150
175
  CYPHER_EOF
151
176
  ); then
@@ -159,7 +184,23 @@ if [ "$LOCALBUSINESS_EXISTING" = "0" ]; then
159
184
  else
160
185
  LOCALBUSINESS_RESULT="already-exists"
161
186
  fi
162
- echo " [install-invariant] localbusiness-bootstrap accountId=${ACCOUNT_ID:0:8} businessType=$LOCALBUSINESS_BT_LOG result=$LOCALBUSINESS_RESULT"
187
+
188
+ # name verdict: absent brand productName sets nothing; a fresh node is created
189
+ # with the name; a pre-existing non-null name is preserved by the coalesce;
190
+ # a pre-existing name-less node is backfilled. Gate on the existence pre-count
191
+ # first: for a node that did not exist, the pre-name MATCH returned zero rows
192
+ # (so $LOCALBUSINESS_PRENAME holds the header line, not a value) — that garbage
193
+ # is never read here. Greppable so a future name-less seed surfaces as a signal.
194
+ if [ -z "$LOCALBUSINESS_PRODUCT_NAME" ]; then
195
+ LOCALBUSINESS_NAME_LOG="absent-no-productName"
196
+ elif [ "$LOCALBUSINESS_EXISTING" = "0" ]; then
197
+ LOCALBUSINESS_NAME_LOG="set"
198
+ elif [ -n "$LOCALBUSINESS_PRENAME" ]; then
199
+ LOCALBUSINESS_NAME_LOG="preserved"
200
+ else
201
+ LOCALBUSINESS_NAME_LOG="set"
202
+ fi
203
+ echo " [install-invariant] localbusiness-bootstrap accountId=${ACCOUNT_ID:0:8} businessType=$LOCALBUSINESS_BT_LOG result=$LOCALBUSINESS_RESULT name=$LOCALBUSINESS_NAME_LOG"
163
204
 
164
205
  # ------------------------------------------------------------------
165
206
  # 4. Create AdminUser node + ADMIN_OF relationship
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAe3B,OAAO,EAkBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AA4BzB,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAgFlE,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;;6BAGyB;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;;sCAGkC;IAClC,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;IAC3C;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAsFD;;;;;;mEAMmE;AACnE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC9D;AAMD;;;;;;iBAMiB;AACjB,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAmCvF;AAID;;;;;;yBAMyB;AACzB,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAiCzD;AAkCD,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpG;AAgBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMvE;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,MAAM,CAER;AAkCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe5F;AAwJD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AA8CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CA+oFpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAe3B,OAAO,EAkBL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AA4BzB,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAA;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAgFlE,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAS1D,eAAO,MAAM,4BAA4B,QAAS,CAAA;AAIlD,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;IAC9B;;gFAE4E;IAC5E,cAAc,EAAE,cAAc,CAAA;IAC9B;;;6BAGyB;IACzB,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC;;;sCAGkC;IAClC,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;IAC3C;;gFAE4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAsFD;;;;;;mEAMmE;AACnE,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC9D;AAMD;;;;;;iBAMiB;AACjB,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAmCvF;AAID;;;;;;yBAMyB;AACzB,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAiCzD;AAkDD,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpG;AAgBD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAUD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMvE;AAMD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,MAAM,CAER;AAkCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe5F;AAwJD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AA8CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAqtFpD"}
@@ -33,7 +33,7 @@ import { buildRcChildEnv, reapplyAdminLevers } from './rc-daemon.js';
33
33
  import { runConnectorAuthLogin } from './connector-auth.js';
34
34
  import { isEffortLevel, isNewSessionEffortLevel, isPermissionMode, SONNET_MODEL } from '../../../lib/models/dist/index.js';
35
35
  import { spawnClaudeSession, stopSession, writeInterruptToPty, isLive, onPtyExit, livePtyCount, liveSessionIds, getPtyTrackerForTests, livePidForSession, classifyLiveness, priorExitedCountForSession, PERMISSION_MODES, openFdCount, registerRcSpawnPty, } from './pty-spawner.js';
36
- import { writeScopeRecord, removeScopeRecord } from './scope-record.js';
36
+ import { writeScopeRecord, removeScopeRecord, readAllScopeRecords } from './scope-record.js';
37
37
  import { writeReseatMarker, resolveReseatChain, clearReseatMarker } from './reseat-ledger.js';
38
38
  import { getScopeMainPid, defaultProcessSignals, defaultSystemctlRunner } from './systemd-scope.js';
39
39
  import { readSidecar, updateSidecar, appendBridgeId, writeSidecar } from './session-sidecar.js';
@@ -319,6 +319,22 @@ export function tailIsOpenTurn(jsonlPath) {
319
319
  /** Resolve any of the 4 keys to the canonical (sessionId, row) pair.
320
320
  * Tries: intrinsic sessionId, bridgeSessionId, bridgeSuffix, numeric pid.
321
321
  * Returns undefined when nothing matches. */
322
+ /** Task 1324 — resolve the on-disk project slug for a supplied `accountId`,
323
+ * mirroring the /rc-spawn account derivation (`accountsRoot =
324
+ * dirname(platformRoot)/data/accounts`) and config.ts's
325
+ * `accountIdFromSessionCwd` invariant that an account's spawn cwd is
326
+ * `<accountsRoot>/<accountId>`. Returns null when the accountId names no
327
+ * account dir in the registry, so the caller can 400 the mismatch instead of
328
+ * scanning a slug that cannot exist. The caller is responsible for
329
+ * shape-validating `accountId` (UUID) BEFORE calling — the value is joined
330
+ * into a filesystem path here, so an unvalidated caller would open a path
331
+ * traversal. */
332
+ function accountSlugForId(platformRoot, accountId) {
333
+ const accountDir = join(dirname(platformRoot), 'data', 'accounts', accountId);
334
+ if (!existsSync(join(accountDir, 'account.json')))
335
+ return null;
336
+ return projectSlugForCwd(accountDir);
337
+ }
322
338
  function resolveRow(watcher, id) {
323
339
  const direct = watcher.getBySessionId(id);
324
340
  if (direct)
@@ -1573,12 +1589,45 @@ export function buildHttpApp(deps) {
1573
1589
  timed(deps.logger, 'POST', `/${id}/stop`, 400, Date.now() - start);
1574
1590
  return c.json({ error: 'invalid-session-id' }, 400);
1575
1591
  }
1592
+ const accountId = c.req.query('accountId') || undefined;
1576
1593
  const row = resolveRow(deps.watcher, id);
1594
+ const sessionId = row?.sessionId ?? id;
1595
+ // Task 1324 — account-scoped resolution for the `!row` branch. On a
1596
+ // multi-account install the watcher is armed only on the house slug, so a
1597
+ // cross-account session has no row. When the admin UI forwards the caller's
1598
+ // canonical accountId, resolve the session from `sessionId + accountId`
1599
+ // instead of the watcher, so a false 404 no longer swallows the stop. The
1600
+ // token to actually terminate a scope unit that survived a manager restart
1601
+ // comes from the on-disk scope record (keyed by sessionId, account-agnostic)
1602
+ // — the watcher row is the only other source and it is absent here.
1603
+ let accountScoped = false;
1604
+ let scopeTokenFromRecord = null;
1577
1605
  if (!row && !isLive(id)) {
1578
- timed(deps.logger, 'POST', `/${id}/stop`, 404, Date.now() - start);
1579
- return c.json({ error: 'session-not-found' }, 404);
1606
+ if (accountId) {
1607
+ // Resolve the slug once (UUID gate first accountId is joined into a
1608
+ // path). A null result means the id names no account in the registry.
1609
+ const slug = UUID_PATTERN.test(accountId) ? accountSlugForId(deps.platformRoot, accountId) : null;
1610
+ if (slug === null) {
1611
+ deps.logger(`stop-account-mismatch sessionId=${sessionId.slice(0, 8)} accountId=${accountId.slice(0, 8)}`);
1612
+ timed(deps.logger, 'POST', `/${id}/stop`, 400, Date.now() - start);
1613
+ return c.json({ error: 'accountId not in this install registry' }, 400);
1614
+ }
1615
+ const hasTop = existsSync(jsonlPathForSlug(deps.claudeConfigDir, slug, sessionId));
1616
+ const hasArchived = existsSync(jsonlPathForSlug(deps.claudeConfigDir, slug, sessionId, { archived: true }));
1617
+ const rec = readAllScopeRecords(sessionsDir, deps.logger).find((r) => r.sessionId === sessionId);
1618
+ if (!hasTop && !hasArchived && !rec) {
1619
+ deps.logger(`stop-not-found sessionId=${sessionId.slice(0, 8)} accountId=${accountId.slice(0, 8)} reason=file-missing`);
1620
+ timed(deps.logger, 'POST', `/${id}/stop`, 404, Date.now() - start);
1621
+ return c.json({ error: 'session-not-found' }, 404);
1622
+ }
1623
+ accountScoped = true;
1624
+ scopeTokenFromRecord = rec?.scopeUnitToken ?? null;
1625
+ }
1626
+ else {
1627
+ timed(deps.logger, 'POST', `/${id}/stop`, 404, Date.now() - start);
1628
+ return c.json({ error: 'session-not-found' }, 404);
1629
+ }
1580
1630
  }
1581
- const sessionId = row?.sessionId ?? id;
1582
1631
  // Task 455 — snapshot the live pid and prior-exited-count BEFORE
1583
1632
  // stopSession runs (the call mutates module state). When prior history
1584
1633
  // exists, emit a single observability line naming the pid the lookup
@@ -1597,7 +1646,9 @@ export function buildHttpApp(deps) {
1597
1646
  // Task 427 — when the row was reattached after a manager restart,
1598
1647
  // `livePtys` has no entry but `row.scopeUnitToken` is set; pass it
1599
1648
  // so stopSession can take the row-scope fallback path.
1600
- rowScopeUnitToken: row?.scopeUnitToken ?? null,
1649
+ // Task 1324 — for a cross-account session there is no row; the token
1650
+ // comes from the on-disk scope record instead, resolved above.
1651
+ rowScopeUnitToken: row?.scopeUnitToken ?? scopeTokenFromRecord,
1601
1652
  // Task 451 — paired with rowScopeUnitToken so the row-scope branch
1602
1653
  // can post-flight verify the kill via kill(pid, 0).
1603
1654
  rowPid: row?.pid ?? null,
@@ -1607,6 +1658,13 @@ export function buildHttpApp(deps) {
1607
1658
  // formatting (Task 250 contract); this is the single audit line the
1608
1659
  // operator and tests can grep against to confirm which branch ran.
1609
1660
  deps.logger(`[stop-session] path=${outcome.path} outcome=${outcome.scopeOutcome} sessionId=${sessionId.slice(0, 8)} unit=${row?.scopeUnitToken ?? 'none'} pid=${row?.pid ?? 'unknown'}`);
1661
+ // Task 1324 — one diffable audit line whenever the stop resolved from the
1662
+ // caller's accountId rather than a watcher row. Distinct from the unified
1663
+ // line above so a log read can tell the account-scoped path from the
1664
+ // house-account row/liveptys paths.
1665
+ if (accountScoped) {
1666
+ deps.logger(`[stop-session] path=account-scoped outcome=${outcome.scopeOutcome} sessionId=${sessionId.slice(0, 8)} accountId=${accountId.slice(0, 8)}`);
1667
+ }
1610
1668
  // Task 250 — surface scope-stop failures as 500 so the operator sees
1611
1669
  // the End-click fail when systemd cannot stop the unit. The liveptys
1612
1670
  // happy path keeps its original 204 response (Task 250 contract).
@@ -1660,6 +1718,7 @@ export function buildHttpApp(deps) {
1660
1718
  timed(deps.logger, 'DELETE', `/${id}`, 400, Date.now() - start);
1661
1719
  return c.json({ error: 'invalid-session-id' }, 400);
1662
1720
  }
1721
+ const accountId = c.req.query('accountId') || undefined;
1663
1722
  const row = resolveRow(deps.watcher, id);
1664
1723
  const sessionId = row?.sessionId ?? id;
1665
1724
  if (isLive(sessionId)) {
@@ -1669,20 +1728,48 @@ export function buildHttpApp(deps) {
1669
1728
  }
1670
1729
  // Task 260 — purge from BOTH locations: a row may have its JSONL
1671
1730
  // at the top level (live or just-ended) or under archive/ (archived).
1672
- // Task 1298resolve the slug that actually holds the JSONL first;
1673
- // the list (sidebar-sessions.ts) walks every slug, so delete must too.
1674
- // Fall back to spawnCwd's slug only when nothing is found anywhere —
1675
- // hasTop/hasArchived will both be false regardless, so the fallback
1676
- // only affects which (nonexistent) paths appear in the 404 case.
1677
- const resolvedSlug = slugForExistingJsonl(deps.claudeConfigDir, sessionId) ?? projectSlugForCwd(deps.spawnCwd);
1731
+ // Task 1324when the admin UI forwards the caller's canonical accountId,
1732
+ // derive the slug from it and gate ONLY on file existence, bypassing the
1733
+ // watcher row (which is single-account-scoped and absent for any session
1734
+ // outside the manager's house account). When accountId is absent, keep the
1735
+ // Task 1298 watcher-row path unchanged for back-compat, but log the
1736
+ // degraded fallback so the account-less path is visible in the log.
1737
+ let resolvedSlug;
1738
+ if (accountId) {
1739
+ // Resolve the slug once (UUID gate first — accountId is joined into a
1740
+ // path). A null result means the id names no account in the registry.
1741
+ const acctSlug = UUID_PATTERN.test(accountId) ? accountSlugForId(deps.platformRoot, accountId) : null;
1742
+ if (acctSlug === null) {
1743
+ deps.logger(`delete-account-mismatch sessionId=${sessionId.slice(0, 8)} accountId=${accountId.slice(0, 8)}`);
1744
+ timed(deps.logger, 'DELETE', `/${id}`, 400, Date.now() - start);
1745
+ return c.json({ error: 'accountId not in this install registry' }, 400);
1746
+ }
1747
+ resolvedSlug = acctSlug;
1748
+ }
1749
+ else {
1750
+ deps.logger(`delete-fallback sessionId=${sessionId.slice(0, 8)} reason=no-accountId-supplied`);
1751
+ // Task 1298 — resolve the slug that actually holds the JSONL; the list
1752
+ // (sidebar-sessions.ts) walks every slug, so delete must too. Fall back
1753
+ // to spawnCwd's slug only when nothing is found anywhere.
1754
+ resolvedSlug = slugForExistingJsonl(deps.claudeConfigDir, sessionId) ?? projectSlugForCwd(deps.spawnCwd);
1755
+ }
1678
1756
  const topJsonl = jsonlPathForSlug(deps.claudeConfigDir, resolvedSlug, sessionId);
1679
1757
  const archivedJsonl = jsonlPathForSlug(deps.claudeConfigDir, resolvedSlug, sessionId, { archived: true });
1680
1758
  const topSidecar = sidecarPathForSlug(deps.claudeConfigDir, resolvedSlug, sessionId);
1681
1759
  const archivedSidecar = sidecarPathForSlug(deps.claudeConfigDir, resolvedSlug, sessionId, { archived: true });
1682
1760
  const hasTop = existsSync(topJsonl);
1683
1761
  const hasArchived = existsSync(archivedJsonl);
1684
- if (!row || (!hasTop && !hasArchived)) {
1685
- deps.logger(`delete-not-found sessionId=${sessionId.slice(0, 8)} reason=not-found-anywhere`);
1762
+ // Account-scoped path gates on file existence alone (the row is expected to
1763
+ // be absent). Fallback path keeps the `!row` term so a bare id with no row
1764
+ // still 404s as `not-found-anywhere` (Task 1298 contract).
1765
+ const notFound = accountId ? (!hasTop && !hasArchived) : (!row || (!hasTop && !hasArchived));
1766
+ if (notFound) {
1767
+ if (accountId) {
1768
+ deps.logger(`delete-not-found sessionId=${sessionId.slice(0, 8)} accountId=${accountId.slice(0, 8)} reason=file-missing`);
1769
+ }
1770
+ else {
1771
+ deps.logger(`delete-not-found sessionId=${sessionId.slice(0, 8)} reason=not-found-anywhere`);
1772
+ }
1686
1773
  timed(deps.logger, 'DELETE', `/${id}`, 404, Date.now() - start);
1687
1774
  return c.json({ error: 'session-not-found' }, 404);
1688
1775
  }