@rubytech/create-maxy 1.0.652 → 1.0.654

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 (137) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +19 -4
  3. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +162 -63
  4. package/payload/platform/plugins/docs/references/platform.md +3 -1
  5. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  6. package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
  7. package/payload/server/public/assets/{admin-DQxieG3v.js → admin-CVZaji3A.js} +12 -12
  8. package/payload/server/public/assets/{arc-CRqJUbyK.js → arc-BMhgytDB.js} +1 -1
  9. package/payload/server/public/assets/architecture-YZFGNWBL-S9-oeq_x.js +1 -0
  10. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DtICG195.js → architectureDiagram-Q4EWVU46-BePoi8XC.js} +1 -1
  11. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-nw1V7I38.js → blockDiagram-DXYQGD6D-BkiwLTtq.js} +1 -1
  12. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-C1eEC43O.js → c4Diagram-AHTNJAMY-bpjPj2Ln.js} +1 -1
  13. package/payload/server/public/assets/channel-D3U0_a1j.js +1 -0
  14. package/payload/server/public/assets/{chunk-2KRD3SAO-Bybqj-wj.js → chunk-2KRD3SAO-ZcHg_orY.js} +1 -1
  15. package/payload/server/public/assets/{chunk-336JU56O-Dszn2qEY.js → chunk-336JU56O-BpATJiGl.js} +2 -2
  16. package/payload/server/public/assets/chunk-426QAEUC-Wz6Bpsil.js +1 -0
  17. package/payload/server/public/assets/{chunk-4BX2VUAB-DrKtrnWH.js → chunk-4BX2VUAB-zJekz2NU.js} +1 -1
  18. package/payload/server/public/assets/{chunk-4TB4RGXK-CBFzVYqS.js → chunk-4TB4RGXK-CLXL19Wd.js} +1 -1
  19. package/payload/server/public/assets/{chunk-55IACEB6-BNsOFSNf.js → chunk-55IACEB6-CzqB8aoU.js} +1 -1
  20. package/payload/server/public/assets/{chunk-5FUZZQ4R-CXZykYh_.js → chunk-5FUZZQ4R-BoTfWHuW.js} +1 -1
  21. package/payload/server/public/assets/{chunk-5PVQY5BW-CLNppenz.js → chunk-5PVQY5BW-RhIfPCRB.js} +1 -1
  22. package/payload/server/public/assets/{chunk-67CJDMHE-DFyE0-n0.js → chunk-67CJDMHE-mM1sFmlz.js} +1 -1
  23. package/payload/server/public/assets/{chunk-7N4EOEYR-BIKZD1_4.js → chunk-7N4EOEYR-GUck0jv1.js} +1 -1
  24. package/payload/server/public/assets/{chunk-AA7GKIK3-D4_g24le.js → chunk-AA7GKIK3-BYhfUc1V.js} +1 -1
  25. package/payload/server/public/assets/{chunk-BSJP7CBP-Cd9H-V61.js → chunk-BSJP7CBP-CTsYuARh.js} +1 -1
  26. package/payload/server/public/assets/{chunk-CIAEETIT-CAU9PIQi.js → chunk-CIAEETIT-CGsGmUze.js} +1 -1
  27. package/payload/server/public/assets/{chunk-EDXVE4YY-CR1JfOwe.js → chunk-EDXVE4YY-utELKGQK.js} +1 -1
  28. package/payload/server/public/assets/{chunk-ENJZ2VHE-CuXW3Isg.js → chunk-ENJZ2VHE-CNHjq5xK.js} +1 -1
  29. package/payload/server/public/assets/{chunk-FMBD7UC4-BwGAtkIr.js → chunk-FMBD7UC4-DaRrfk3s.js} +1 -1
  30. package/payload/server/public/assets/{chunk-FOC6F5B3-Cn0552qP.js → chunk-FOC6F5B3-BaeLcJVt.js} +1 -1
  31. package/payload/server/public/assets/{chunk-ICPOFSXX-DEZT2XyQ.js → chunk-ICPOFSXX-Di63NBur.js} +2 -2
  32. package/payload/server/public/assets/{chunk-K5T4RW27-KwBFTzJ9.js → chunk-K5T4RW27-CTTOezMH.js} +1 -1
  33. package/payload/server/public/assets/{chunk-KGLVRYIC-1-3y582Z.js → chunk-KGLVRYIC-DCkohKP2.js} +1 -1
  34. package/payload/server/public/assets/{chunk-LIHQZDEY-DXIBsDHL.js → chunk-LIHQZDEY-osQO30uB.js} +1 -1
  35. package/payload/server/public/assets/{chunk-ORNJ4GCN-CRbOike7.js → chunk-ORNJ4GCN-DoLajOOe.js} +1 -1
  36. package/payload/server/public/assets/{chunk-OYMX7WX6-CVT9itnY.js → chunk-OYMX7WX6-BSPzqyxs.js} +1 -1
  37. package/payload/server/public/assets/chunk-QZHKN3VN-BAQp1OEl.js +1 -0
  38. package/payload/server/public/assets/{chunk-U2HBQHQK-BLgNHWFf.js → chunk-U2HBQHQK-BZnA7c4T.js} +1 -1
  39. package/payload/server/public/assets/{chunk-X2U36JSP-DHYLiYqc.js → chunk-X2U36JSP-DpQ2OA_c.js} +1 -1
  40. package/payload/server/public/assets/{chunk-XPW4576I-DBdiQ3Zy.js → chunk-XPW4576I-BccP1mlQ.js} +1 -1
  41. package/payload/server/public/assets/{chunk-YZCP3GAM-DXaosB5Z.js → chunk-YZCP3GAM-BAkNXu0G.js} +1 -1
  42. package/payload/server/public/assets/{chunk-ZZ45TVLE-B5dCmOpH.js → chunk-ZZ45TVLE-DBSm41oP.js} +1 -1
  43. package/payload/server/public/assets/classDiagram-6PBFFD2Q-6EGGLDD_.js +1 -0
  44. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfAV4tgE.js +1 -0
  45. package/payload/server/public/assets/clone-BoV8noAi.js +1 -0
  46. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-DaHtPQvk.js → cose-bilkent-S5V4N54A-Boeb8aWs.js} +1 -1
  47. package/payload/server/public/assets/{dagre-KV5264BT-CAL9V_HR.js → dagre-KV5264BT-BkvWofSp.js} +1 -1
  48. package/payload/server/public/assets/{dagre-N8C5Xujx.js → dagre-nvPNAunb.js} +1 -1
  49. package/payload/server/public/assets/data-DgI19qYm.js +1 -0
  50. package/payload/server/public/assets/{diagram-5BDNPKRD-pzBSPqlM.js → diagram-5BDNPKRD-CMEgyt4E.js} +1 -1
  51. package/payload/server/public/assets/{diagram-G4DWMVQ6-DStdLqos.js → diagram-G4DWMVQ6-ChorrAF0.js} +1 -1
  52. package/payload/server/public/assets/{diagram-MMDJMWI5-D-SfeX-6.js → diagram-MMDJMWI5-D_iD27po.js} +1 -1
  53. package/payload/server/public/assets/{diagram-TYMM5635-Cdr1DQ84.js → diagram-TYMM5635-8qXI1ioG.js} +1 -1
  54. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CIg1dDZT.js → erDiagram-SMLLAGMA-BFjtKDSB.js} +1 -1
  55. package/payload/server/public/assets/{file-dBmvpAuH.js → file-J1JpJF4E.js} +1 -1
  56. package/payload/server/public/assets/{flatten-CpKIi5d2.js → flatten-ya0TqRLc.js} +1 -1
  57. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CJtU1T6d.js → flowDiagram-DWJPFMVM-Bpd7IL9l.js} +1 -1
  58. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-R4fuRAT1.js → ganttDiagram-T4ZO3ILL-CwOozU85.js} +1 -1
  59. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BOC4CldZ.js +1 -0
  60. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-sifugSGn.js → gitGraphDiagram-UUTBAWPF-CcPILiC9.js} +1 -1
  61. package/payload/server/public/assets/{graph-3snSy3WW.js → graph-CFwxUVS0.js} +19 -19
  62. package/payload/server/public/assets/{graphlib-DrlxPM8j.js → graphlib-B_mcXEVr.js} +1 -1
  63. package/payload/server/public/assets/{house-IMEjNkQf.js → house-Dche6_m0.js} +1 -1
  64. package/payload/server/public/assets/info-OMHHGYJF-BSCPTUIx.js +1 -0
  65. package/payload/server/public/assets/infoDiagram-42DDH7IO-T2sn--WJ.js +2 -0
  66. package/payload/server/public/assets/{isEmpty-C3Vxk1It.js → isEmpty-h-wRi_o9.js} +1 -1
  67. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CYUJOA2c.js → ishikawaDiagram-UXIWVN3A-DOP9-Q8H.js} +1 -1
  68. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-UltrLajs.js → journeyDiagram-VCZTEJTY-DGATg0WC.js} +1 -1
  69. package/payload/server/public/assets/{jsx-runtime-CXoJCO3U.js → jsx-runtime-BE1CBORz.js} +1 -1
  70. package/payload/server/public/assets/jsx-runtime-C7zbe_Pq.css +1 -0
  71. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BBaThtP3.js → kanban-definition-6JOO6SKY-C5PigmKg.js} +1 -1
  72. package/payload/server/public/assets/{line-BhOwLD_o.js → line-DlKKhwkO.js} +1 -1
  73. package/payload/server/public/assets/{linear-D76hoLvZ.js → linear-DD4JiB1l.js} +1 -1
  74. package/payload/server/public/assets/{mermaid-parser.core-D8n5xV7A.js → mermaid-parser.core-C8xGCa9p.js} +2 -2
  75. package/payload/server/public/assets/{mermaid.core-C3TZA9fX.js → mermaid.core-CCUSwZB_.js} +3 -3
  76. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-Bv1kghvk.js → mindmap-definition-QFDTVHPH-75k-IVhC.js} +1 -1
  77. package/payload/server/public/assets/{ordinal-BLrOss5K.js → ordinal-Dwxksj1B.js} +1 -1
  78. package/payload/server/public/assets/packet-4T2RLAQJ-pBa_ZhNI.js +1 -0
  79. package/payload/server/public/assets/pie-ZZUOXDRM-BzYOyiMb.js +1 -0
  80. package/payload/server/public/assets/{pieDiagram-DEJITSTG-BSYldcKa.js → pieDiagram-DEJITSTG-DN5RsDwZ.js} +1 -1
  81. package/payload/server/public/assets/public-LhnMTdDE.js +5 -0
  82. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-lCDshgz1.js → quadrantDiagram-34T5L4WZ-Sd9x6pNe.js} +1 -1
  83. package/payload/server/public/assets/radar-PYXPWWZC-CTVOaAq6.js +1 -0
  84. package/payload/server/public/assets/{reduce-C5tBOlxC.js → reduce-BUuWaDl2.js} +1 -1
  85. package/payload/server/public/assets/{requirementDiagram-MS252O5E-C7j42RrO.js → requirementDiagram-MS252O5E-BDgifYzj.js} +1 -1
  86. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-plPbHhuF.js → sankeyDiagram-XADWPNL6-BX9VULNJ.js} +1 -1
  87. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D3Y8MXiX.js → sequenceDiagram-FGHM5R23-z3vMxhgE.js} +1 -1
  88. package/payload/server/public/assets/{share-2-ARoCxH5K.js → share-2-6hJtFYgM.js} +1 -1
  89. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D4BdhMPy.js → stateDiagram-FHFEXIEX-DlP0hBxF.js} +1 -1
  90. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DSddQStC.js +1 -0
  91. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Lh9jrYCl.js → timeline-definition-GMOUNBTQ-DwQbhKCo.js} +1 -1
  92. package/payload/server/public/assets/treeView-SZITEDCU-OTnF4Qzw.js +1 -0
  93. package/payload/server/public/assets/treemap-W4RFUUIX-DlIRmHFb.js +1 -0
  94. package/payload/server/public/assets/{useVoiceRecorder-Cw8gxj1L.js → useVoiceRecorder-PUde6itK.js} +4 -4
  95. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Cx0v19iv.js → vennDiagram-DHZGUBPP-WTqmZWWa.js} +1 -1
  96. package/payload/server/public/assets/wardley-RL74JXVD-DwMXAC4U.js +1 -0
  97. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BYewCTre.js → wardleyDiagram-NUSXRM2D-BUY50x5T.js} +1 -1
  98. package/payload/server/public/assets/x-DmqRGGHj.js +1 -0
  99. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CkfIdbJu.js → xychartDiagram-5P7HB3ND-Btdq-fDj.js} +1 -1
  100. package/payload/server/public/data.html +6 -7
  101. package/payload/server/public/graph.html +6 -6
  102. package/payload/server/public/index.html +8 -9
  103. package/payload/server/public/public.html +5 -6
  104. package/payload/server/server.js +409 -102
  105. package/payload/server/public/assets/architecture-YZFGNWBL-CXIHKKCa.js +0 -1
  106. package/payload/server/public/assets/channel-CA7njeKl.js +0 -1
  107. package/payload/server/public/assets/chunk-426QAEUC-tWQOa3-I.js +0 -1
  108. package/payload/server/public/assets/chunk-QZHKN3VN-BwkFBCAY.js +0 -1
  109. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CUZ9BU_6.js +0 -1
  110. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BGYsCDux.js +0 -1
  111. package/payload/server/public/assets/clone-BjouONkW.js +0 -1
  112. package/payload/server/public/assets/data-CAKMrPTQ.js +0 -1
  113. package/payload/server/public/assets/gitGraph-7Q5UKJZL-tvzbaNdg.js +0 -1
  114. package/payload/server/public/assets/info-OMHHGYJF-ByeBaFw5.js +0 -1
  115. package/payload/server/public/assets/infoDiagram-42DDH7IO-CjgCxerY.js +0 -2
  116. package/payload/server/public/assets/jsx-runtime-BJhXEiL3.css +0 -1
  117. package/payload/server/public/assets/packet-4T2RLAQJ-Csybj5RO.js +0 -1
  118. package/payload/server/public/assets/pie-ZZUOXDRM-Iw1du1Bn.js +0 -1
  119. package/payload/server/public/assets/public-b43rEAhq.js +0 -5
  120. package/payload/server/public/assets/radar-PYXPWWZC-rEet4TBV.js +0 -1
  121. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DhKxVkX3.js +0 -1
  122. package/payload/server/public/assets/trash-2-D_Rm8z21.js +0 -1
  123. package/payload/server/public/assets/treeView-SZITEDCU-PaLYyjtc.js +0 -1
  124. package/payload/server/public/assets/treemap-W4RFUUIX-CEhGYFbO.js +0 -1
  125. package/payload/server/public/assets/wardley-RL74JXVD-xtJ4_o4d.js +0 -1
  126. package/payload/server/public/assets/x-CFPIrGuL.js +0 -1
  127. /package/payload/server/public/assets/{_baseFor-WfS9pKAn.js → _baseFor-Dn4GSmI6.js} +0 -0
  128. /package/payload/server/public/assets/{array-HeX70jSN.js → array-DJN9YAVf.js} +0 -0
  129. /package/payload/server/public/assets/{cytoscape.esm-CDZo0kst.js → cytoscape.esm-BcJTl1re.js} +0 -0
  130. /package/payload/server/public/assets/{defaultLocale-GJwWH1Jr.js → defaultLocale-B4F_XsBB.js} +0 -0
  131. /package/payload/server/public/assets/{dist-BKbAaes5.js → dist-CrzV1W3-.js} +0 -0
  132. /package/payload/server/public/assets/{init-BPLPMQ3Y.js → init-DX0Y1qU4.js} +0 -0
  133. /package/payload/server/public/assets/{katex-CKZ-HWMQ.js → katex-CjHJ1D7d.js} +0 -0
  134. /package/payload/server/public/assets/{path-YdFzr2W6.js → path-7vUsG-o2.js} +0 -0
  135. /package/payload/server/public/assets/{preload-helper-BEFjQwLd.js → preload-helper-qlgyTAkD.js} +0 -0
  136. /package/payload/server/public/assets/{rough.esm-HAx67Hnb.js → rough.esm-NLRoWnq-.js} +0 -0
  137. /package/payload/server/public/assets/{src-BvrHnOMG.js → src-Bo15iQ7w.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.652",
3
+ "version": "1.0.654",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -6,13 +6,28 @@
6
6
  # helpers to emit phase lines and tee subprocess output into the same
7
7
  # per-conversation file the chat UI's server-side tailer reads.
8
8
  #
9
- # Contract (read by platform/ui/app/api/admin/chat/route.ts tailer and
9
+ # Contract (read by platform/ui/app/lib/script-stream-tailer.ts tailer and
10
10
  # .docs/platform.md):
11
11
  # [<ISO-ts>] [<scope>] <kv …>
12
12
  # [<ISO-ts>] [<scope>:<subprocess-tag>] <raw line>
13
- # where <scope> {setup-tunnel, reset-tunnel}. The tailer regex is
14
- # ^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\]
15
- # so any prefix change must be made on both sides atomically.
13
+ # Canonical regex SCRIPT_STREAM_RE at platform/ui/app/lib/script-stream-tailer.ts:51:
14
+ # ^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$
15
+ # <scope> is any `[a-z][a-z0-9-]*` token (Task 592 generalised from the
16
+ # pre-592 enum `setup-tunnel|reset-tunnel`, which silently filtered out the
17
+ # `[list-cf-domains]` lines Task 589 emitted). <subprocess-tag> may contain
18
+ # lowercase, digits, `-`, `_`, `:`. Adding a new <scope> requires no edit to
19
+ # the regex — the shape is the only contract. Any prefix-shape change must
20
+ # be made on both sides (this helper + script-stream-tailer.ts) atomically.
21
+ #
22
+ # Inner layers (e.g. a node/python helper a .sh wrapper spawns — Task 598's
23
+ # `list-cf-domains.sh` → `list-cf-domains.ts` pattern) must write phase lines
24
+ # directly to STREAM_LOG_PATH with the same prefix shape: stderr alone is
25
+ # silently discarded by runFormSpawn on exit 0. The build-gate
26
+ # `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this
27
+ # by rejecting any .sh under `platform/plugins/*/scripts/` that sources this
28
+ # helper and invokes an interpreter subprocess whose target does not pair a
29
+ # STREAM_LOG_PATH env read with an append/write call. Opt out per invocation
30
+ # with `# stream-log-contract: stderr-only (reason: <prose>)`.
16
31
 
17
32
  # Exit 1 loudly with the variable name and the invoking scope so direct-SSH
18
33
  # invocations fail fast and the operator reads exactly what to set. No
@@ -17,6 +17,23 @@
17
17
  // routing and cannot drift without breaking their entire dashboard; it
18
18
  // is the most stable surface to key off.
19
19
  //
20
+ // Why href-only (no page-text fallback): Task 599 removed an earlier
21
+ // <main>-wide FQDN text walker ("Source B") after a reproduction returned
22
+ // `count=1` with a bogus string (`leg.interest` — a static footer FQDN-shape
23
+ // that hit the regex). The walker's non-empty result short-circuited the
24
+ // poll-to-success branch before zones hydrated 500 ms later, silently
25
+ // returning wrong data with `reason=ok`. The walker's intended role —
26
+ // "defend against CF redesign removing href links" — is already served by
27
+ // the existing `empty-or-drift` body-dump branch when href scraping returns
28
+ // nothing; a silent fallback that produces wrong answers is strictly worse
29
+ // than a loud drift signal with a snapshotted HTML dump for diagnosis.
30
+ //
31
+ // Why poll-to-stable: the CF dashboard lazy-loads its zone list after the
32
+ // initial document-ready signal. A naive "return on first non-empty poll"
33
+ // can race into a partial result (e.g. first zone rendered at t=500 ms, all
34
+ // zones at t=1000 ms). The `scrapeDomains` poll waits for the observed count
35
+ // to stabilise across two consecutive iterations before returning.
36
+ //
20
37
  // Contract with the caller (list-cf-domains.sh → route → form):
21
38
  // stdout on exit 0: JSON `string[]` (empty array means signed-in-but-empty)
22
39
  // stderr on any path: phase_line-formatted lines, `[list-cf-domains] …`
@@ -29,6 +46,7 @@ import { appendFileSync } from "node:fs";
29
46
  import { writeFile } from "node:fs/promises";
30
47
  import { resolve } from "node:path";
31
48
  import { homedir } from "node:os";
49
+ import { fileURLToPath } from "node:url";
32
50
 
33
51
  // CDP host/port default to the VNC Chromium's localhost bind; env overrides
34
52
  // exist solely so the vitest regression under platform/ui/__tests__ can force
@@ -272,86 +290,154 @@ async function waitForSignedIn(cdp: CdpClient): Promise<string> {
272
290
  die("not-signed-in", "no /<accountId>/… path reached within budget");
273
291
  }
274
292
 
275
- // Extract every domain mentioned on the Domains Overview page via URL-
276
- // pattern match, then merge with any FQDN-shaped text found in table cells.
277
- // Two sources: (a) `/<accountId>/<hostname>` URL hrefs on the page; (b) any
278
- // text node on the page matching a valid FQDN pattern — the overview page
279
- // renders the zone name as both a link and a table cell, so either source
280
- // catches it. The merge is conservative: we require the string look like a
281
- // domain (2+ labels, last label 2-63 alpha) AND not be a cloudflare-owned
282
- // marketing host.
283
- const SCRAPE_EXPRESSION = `(function() {
284
- const accountIdMatch = location.pathname.match(/^\\/([a-f0-9]{32})/);
285
- if (!accountIdMatch) return { reason: 'no-account-id', domains: [] };
293
+ export interface ScrapeOutcome {
294
+ reason: "ok" | "no-account-id";
295
+ domains: string[];
296
+ }
297
+
298
+ // Pure scrape logic extracts the zones on the current Cloudflare dashboard
299
+ // page by matching `<a href="/<accountId>/<zone>…">` links. Exported for
300
+ // direct in-process testing under JSDOM (see `list-cf-domains-scrape.test.ts`),
301
+ // and serialised into `SCRAPE_EXPRESSION` below for execution inside the
302
+ // operator's VNC Chromium via CDP `Runtime.evaluate`. The function takes its
303
+ // document and location as arguments (rather than reading globals) so the
304
+ // JSDOM test can pass in its window's document+location directly — test and
305
+ // prod share the same implementation, with no duplication and no eval.
306
+ //
307
+ // Note: this function body is serialised via `Function.prototype.toString()`,
308
+ // so it must be fully self-contained — no imports, no captures from outer
309
+ // scope. Node-side helpers belong outside.
310
+ export function scrapeCurrentPage(
311
+ document: Document,
312
+ location: Location,
313
+ ): ScrapeOutcome {
314
+ const accountIdMatch = location.pathname.match(/^\/([a-f0-9]{32})/);
315
+ if (!accountIdMatch) return { reason: "no-account-id", domains: [] };
286
316
  const accountId = accountIdMatch[1];
287
317
 
288
- const out = new Set();
289
- const pushIfDomain = (s) => {
290
- if (typeof s !== 'string') return;
318
+ const out = new Set<string>();
319
+ const pushIfDomain = (s: unknown): void => {
320
+ if (typeof s !== "string") return;
291
321
  const t = s.trim().toLowerCase();
292
- if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(t)) return;
293
- if (t.endsWith('.cloudflare.com') || t === 'cloudflare.com') return;
322
+ if (
323
+ !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(
324
+ t,
325
+ )
326
+ )
327
+ return;
328
+ if (t.endsWith(".cloudflare.com") || t === "cloudflare.com") return;
294
329
  out.add(t);
295
330
  };
296
331
 
297
- // Source A: /<accountId>/<host> hrefs — the canonical CF routing pattern
298
- // that the overview page uses to link each zone to its management screens.
299
- const hrefPrefix = '/' + accountId + '/';
300
- for (const a of document.querySelectorAll('a[href]')) {
301
- const href = a.getAttribute('href') || '';
332
+ // The only scrape source: `/<accountId>/<host>` hrefs — the canonical CF
333
+ // routing pattern the overview page uses to link each zone to its
334
+ // management screens. Task 599 removed a full-<main> FQDN text walker
335
+ // after it returned a bogus FQDN-shaped string from static page copy
336
+ // (see module header). When this source returns empty, the caller's
337
+ // `empty-or-drift` branch snapshots the page HTML for diagnosis — loud
338
+ // drift signal, not silent wrong data.
339
+ const hrefPrefix = "/" + accountId + "/";
340
+ for (const a of document.querySelectorAll("a[href]")) {
341
+ const href = a.getAttribute("href") || "";
302
342
  if (!href.startsWith(hrefPrefix)) continue;
303
- const tail = href.slice(hrefPrefix.length).split('?')[0].split('#')[0];
304
- const firstSegment = tail.split('/')[0];
343
+ const tail = href.slice(hrefPrefix.length).split("?")[0].split("#")[0];
344
+ const firstSegment = tail.split("/")[0];
305
345
  pushIfDomain(firstSegment);
306
346
  }
307
347
 
308
- // Source B: explicit FQDN-shaped text in the main content region. Guards
309
- // against a hypothetical CF redesign that drops the link tree — the zone
310
- // name still appears as rendered text in the table row.
311
- const main = document.querySelector('main') || document.body;
312
- if (main) {
313
- const treeWalker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT);
314
- let node;
315
- while ((node = treeWalker.nextNode())) {
316
- const text = (node.nodeValue || '').trim();
317
- if (text.length < 4 || text.length > 253) continue;
318
- pushIfDomain(text);
319
- }
320
- }
321
-
322
- return { reason: 'ok', domains: Array.from(out).sort() };
323
- })()`;
324
-
325
- interface ScrapeOutcome {
326
- reason: "ok" | "no-account-id";
327
- domains: string[];
348
+ return { reason: "ok", domains: Array.from(out).sort() };
328
349
  }
329
350
 
330
- async function scrapeDomains(cdp: CdpClient): Promise<string[]> {
351
+ // The serialised form the CDP Runtime.evaluate executes inside the operator's
352
+ // VNC Chromium. Deriving it from `scrapeCurrentPage.toString()` means the
353
+ // browser-side and the test-side share one implementation — drift between
354
+ // them is impossible by construction.
355
+ export const SCRAPE_EXPRESSION = `(${scrapeCurrentPage.toString()})(document, location)`;
356
+
357
+ // Minimum surface `scrapeDomains` needs from the CDP client: execute a JS
358
+ // expression in the target page and return the serialised value. Typing the
359
+ // dependency at this width lets the vitest regression inject a mock without
360
+ // constructing a full CdpClient + WebSocket.
361
+ export type CdpEvaluator = (expression: string) => Promise<unknown>;
362
+
363
+ // Number of consecutive polls returning an identical non-empty count that
364
+ // proves the SPA zone-list has finished hydrating. Two iterations at 500 ms
365
+ // each = 1 s of observed stability, which is the empirical ceiling on the
366
+ // CF dashboard's lazy zone-list fetch post-document-ready. Shorter thresholds
367
+ // risk returning a partial list; longer thresholds add user-visible latency
368
+ // without proportional robustness.
369
+ const STABLE_POLL_THRESHOLD = 2;
370
+
371
+ export async function scrapeDomains(evaluator: CdpEvaluator): Promise<string[]> {
331
372
  const deadline = Date.now() + SCRAPE_POLL_MS;
332
373
  let lastOutcome: ScrapeOutcome | null = null;
374
+ // Track the most recent non-empty observation so deadline-reached without
375
+ // stability returns the caller's best evidence rather than falling through
376
+ // to the drift-dump (which is reserved for the always-empty case).
377
+ let lastNonEmptyDomains: string[] = [];
378
+ let stableCount = -1;
379
+ let stableIterations = 0;
380
+ let polls = 0;
381
+
333
382
  while (Date.now() < deadline) {
383
+ polls += 1;
334
384
  try {
335
- const outcome = await evaluate<ScrapeOutcome>(cdp, SCRAPE_EXPRESSION);
385
+ const outcome = (await evaluator(SCRAPE_EXPRESSION)) as ScrapeOutcome;
336
386
  lastOutcome = outcome;
387
+
337
388
  if (outcome.reason === "ok" && outcome.domains.length > 0) {
338
- logPhase(`phase=dom-scrape-complete result=ok count=${outcome.domains.length}`);
339
- return outcome.domains;
389
+ lastNonEmptyDomains = outcome.domains;
390
+ if (outcome.domains.length === stableCount) {
391
+ stableIterations += 1;
392
+ if (stableIterations >= STABLE_POLL_THRESHOLD) {
393
+ logPhase(
394
+ `phase=dom-scrape-complete result=ok count=${outcome.domains.length} polls=${polls} stable_polls=${stableIterations} unstable=false`,
395
+ );
396
+ return outcome.domains;
397
+ }
398
+ } else {
399
+ stableCount = outcome.domains.length;
400
+ stableIterations = 1;
401
+ }
402
+ } else {
403
+ // `ok` with zero domains OR a non-ok reason: treat as still-hydrating.
404
+ // Keep polling; the final iteration's zero result is the empty-account
405
+ // signal. Reset stability tracking so a 0 → N → 0 sequence doesn't
406
+ // falsely satisfy the threshold.
407
+ stableCount = -1;
408
+ stableIterations = 0;
340
409
  }
341
- // `ok` with zero domains is ambiguous during SPA hydration — keep
342
- // polling in case the table renders after a data fetch. The final
343
- // iteration's zero result is the empty-account signal.
344
410
  } catch (err) {
345
- logPhase(`phase=scrape-retry err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`);
411
+ logPhase(
412
+ `phase=scrape-retry err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`,
413
+ );
346
414
  }
347
415
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
348
416
  }
349
- // Poll exhausted. Distinguish genuine empty account from selector drift by
350
- // dumping the body HTML (bounded) for manual inspection, then reporting
351
- // empty with the diagnostic enum. The script still exits 0 — the caller
352
- // treats empty as a valid signal and shows the "add a domain" empty state.
417
+
418
+ // Deadline reached. Two distinct branches:
419
+ //
420
+ // (i) We saw non-empty results but they never stabilised across two
421
+ // consecutive polls — the page is either paginating or re-rendering
422
+ // the zone list. Return the last non-empty observation and flag
423
+ // `unstable=true` on the phase line so the operator can see the race
424
+ // in the stream log. This is NOT drift — the HTML dump would be
425
+ // misleading noise, so we suppress it.
426
+ //
427
+ // (ii) We never saw non-empty results — this is either a genuinely empty
428
+ // account OR CF has drifted the href URL shape so Source A yields
429
+ // nothing. The `empty-or-drift` body-dump branch snapshots the page
430
+ // HTML so the operator can distinguish the two by inspecting the
431
+ // dump file.
432
+ if (lastNonEmptyDomains.length > 0) {
433
+ logPhase(
434
+ `phase=dom-scrape-complete result=ok count=${lastNonEmptyDomains.length} polls=${polls} stable_polls=${stableIterations} unstable=true`,
435
+ );
436
+ return lastNonEmptyDomains;
437
+ }
438
+
353
439
  try {
354
- const html = await evaluate<string>(cdp, "document.documentElement.outerHTML.slice(0, 100000)");
440
+ const html = (await evaluator("document.documentElement.outerHTML.slice(0, 100000)")) as string;
355
441
  // CONFIG_DIR is set by list-cf-domains.sh before the spawn. A silent
356
442
  // fallback would dump logs into the wrong brand's directory on a Real
357
443
  // Agent install — a silent-miswrite masking the wrapper-side break it
@@ -359,15 +445,21 @@ async function scrapeDomains(cdp: CdpClient): Promise<string[]> {
359
445
  // values.
360
446
  const configDir = process.env.CONFIG_DIR;
361
447
  if (!configDir) {
362
- throw new Error("CONFIG_DIR env var not set by wrapper — refusing to guess brand log directory");
448
+ throw new Error(
449
+ "CONFIG_DIR env var not set by wrapper — refusing to guess brand log directory",
450
+ );
363
451
  }
364
452
  const logDir = resolve(homedir(), configDir, "logs");
365
453
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
366
454
  const dumpPath = resolve(logDir, `list-cf-domains-${ts}.html`);
367
455
  await writeFile(dumpPath, typeof html === "string" ? html : String(html), "utf-8");
368
- logPhase(`phase=dom-scrape-complete result=empty-or-drift dump=${dumpPath} lastReason=${lastOutcome?.reason ?? "unknown"}`);
456
+ logPhase(
457
+ `phase=dom-scrape-complete result=empty-or-drift dump=${dumpPath} lastReason=${lastOutcome?.reason ?? "unknown"} polls=${polls}`,
458
+ );
369
459
  } catch (err) {
370
- logPhase(`phase=dom-scrape-complete result=empty-or-drift dump=failed lastReason=${lastOutcome?.reason ?? "unknown"} err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`);
460
+ logPhase(
461
+ `phase=dom-scrape-complete result=empty-or-drift dump=failed lastReason=${lastOutcome?.reason ?? "unknown"} polls=${polls} err="${(err instanceof Error ? err.message : String(err)).slice(0, 120)}"`,
462
+ );
371
463
  }
372
464
  return [];
373
465
  }
@@ -419,7 +511,7 @@ async function main(): Promise<void> {
419
511
  await navigate(cdp, overviewUrl);
420
512
  await waitForDocumentReady(cdp);
421
513
 
422
- const domains = await scrapeDomains(cdp);
514
+ const domains = await scrapeDomains((expr) => evaluate(cdp, expr));
423
515
 
424
516
  logPhase(`phase=target-closed`);
425
517
  process.stdout.write(JSON.stringify(domains) + "\n");
@@ -433,6 +525,13 @@ async function main(): Promise<void> {
433
525
  }
434
526
  }
435
527
 
436
- main().catch((err: unknown) => {
437
- die("runtime-error", err instanceof Error ? err.message : String(err));
438
- });
528
+ // Entry-point gate: only run `main()` when this module is the script Node was
529
+ // invoked with, not when imported by the vitest regressions that exercise
530
+ // `scrapeCurrentPage` / `scrapeDomains` directly. Without the gate, importing
531
+ // this module would trigger a live CDP connection attempt and process.exit(1)
532
+ // inside the test process.
533
+ if (process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url)) {
534
+ main().catch((err: unknown) => {
535
+ die("runtime-error", err instanceof Error ? err.message : String(err));
536
+ });
537
+ }
@@ -66,7 +66,9 @@ The chat input auto-grows as you type — it expands to fit your message and shr
66
66
 
67
67
  The admin UI includes a live terminal surface that opens a real shell on your Pi in the browser, reached via the Software Update modal. Under the hood it's a WebSocket (`/admin/terminal/ws`) attached through `@xterm/xterm` to `ttyd` on `127.0.0.1:7681`, backed by a persistent tmux session named `maxy-pty`.
68
68
 
69
- The tmux session outlives admin-server restarts — running an upgrade inside this terminal means you see the live shell output continuously, even through the admin server's own restart mid-upgrade. Closing the browser tab does not kill the running work; re-opening the Software Update window reattaches to the same session and scrollback shows everything that happened in the meantime. Password-protected `sudo` prompts appear natively inside the terminal, and the password you type never leaves the Pi — the admin-server proxy is a raw byte pipe that never inspects frame payloads.
69
+ The tmux session outlives admin-server restarts — running an upgrade inside this terminal means you see the live shell output continuously, even through the admin server's own restart mid-upgrade. Closing the browser tab does not kill the running work; re-opening the Software Update window reattaches to the same session during an active upgrade and scrollback shows everything that happened in the meantime. Password-protected `sudo` prompts appear natively inside the terminal, and the password you type never leaves the Pi — the admin-server proxy is a raw byte pipe that never inspects frame payloads.
70
+
71
+ The Software Update window mounts the terminal lazily: the WebSocket is opened on the first Upgrade click, not when the window opens. Until you click Upgrade, the terminal area shows "Ready to upgrade." and no network traffic flows. If the admin server cannot reach `ttyd`, the window renders an inline "Admin terminal not available" message with the exact re-install command and a Try again button — no silent reconnect loops, no empty black rectangle. The scrollback-across-reopen behaviour above still applies during an active upgrade (a sessionStorage flag remembers that an upgrade is in flight so reopening the window re-mounts the terminal and reattaches).
70
72
 
71
73
  ## AI Content Provenance
72
74
 
@@ -103,7 +103,7 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
103
103
 
104
104
  **How the tee decides which file to write to (Task 532):** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
105
105
 
106
- **`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh` and `reset-tunnel.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[[^]]+\] \[(setup-tunnel|reset-tunnel)(:[^]]+)?\] ` and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract.
106
+ **`STREAM_LOG_PATH` reaches every Claude Code child (Task 556).** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh`, `reset-tunnel.sh`, and `list-cf-domains.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] ` — any lowercase scope shape participates on first write (Task 592 generalised from the pre-592 enum `setup-tunnel|reset-tunnel`) — and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract. Inner-layer helpers that a .sh wrapper spawns (e.g. `list-cf-domains.ts` via `node --experimental-strip-types`) must write phase lines directly to `STREAM_LOG_PATH` rather than relying on stderr propagation (Task 598); the build-gate `platform/ui/scripts/check-stream-log-contract.mjs` (Task 600) enforces this and is the definitive reference for the three allowed patterns (tee-wrapped, direct-write, or explicit stderr-only marker).
107
107
 
108
108
  **Retrieve MCP diagnostic lines for a conversation:**
109
109
 
@@ -109,6 +109,22 @@ This is safe — the tmux session survives the ttyd restart because `tmux new-se
109
109
 
110
110
  **Terminal unit missing entirely?** If `sudo systemctl --user status maxy-ttyd` reports `unit not found`, the installer's ttyd provisioning step failed — typically on a Bookworm device where `ttyd` is not available via apt and the upstream download (or SHA256 check) failed at install time. Re-run `npx -y @rubytech/create-maxy@latest`; the operator-visible remediation command is also printed at install time in `~/.maxy/logs/install-*.log`.
111
111
 
112
+ ---
113
+
114
+ ## "Admin terminal not available" in the Software Update window
115
+
116
+ **Symptom:** The Software Update window displays "Admin terminal not available. Re-run the installer from a shell: `npx -y @rubytech/create-maxy@latest`" with a Try again button, instead of the usual terminal area.
117
+
118
+ **What it means:** The admin server could not reach `ttyd` on `127.0.0.1:7681`. Either `maxy-ttyd.service` is not running, or it failed to install during setup.
119
+
120
+ **Fix:** Run the exact command shown in the error message from a shell on the device:
121
+
122
+ ```bash
123
+ npx -y @rubytech/create-maxy@latest
124
+ ```
125
+
126
+ Then return to the upgrade window and click **Try again**. The window re-probes `/api/health` and, once ttyd is listening, the terminal area mounts as normal. If the problem persists, check the boot log for `[ttyd] upstream NOT reachable on 127.0.0.1:7681` and follow the `maxy-ttyd` restart steps above.
127
+
112
128
  ## Orphan Account Directory Archived to `.trash/`
113
129
 
114
130
  **What happened:** During upgrade, the installer detected multiple account directories under `~/maxy/data/accounts/` and identified one as live (its `admins` list matches the device's `users.json`). Non-matching siblings are archived — not deleted — under `~/maxy/data/accounts/.trash/<uuid>-<ISO8601-ts>/`.