@rubytech/create-maxy-code 0.1.562 → 0.1.564

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 (195) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +38 -11
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  4. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +19 -5
  5. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +6 -0
  6. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +77 -6
  7. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +39 -0
  8. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +294 -0
  9. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +112 -0
  10. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +38 -0
  11. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +112 -0
  12. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +31 -4
  13. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +193 -0
  14. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +93 -0
  15. package/payload/platform/plugins/docs/references/deployment.md +14 -8
  16. package/payload/platform/plugins/docs/references/platform.md +4 -2
  17. package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
  18. package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
  19. package/payload/platform/templates/agents/public/IDENTITY.md +3 -3
  20. package/payload/server/{chunk-G6ULACHG.js → chunk-3ABITJWB.js} +8 -4
  21. package/payload/server/{manager-2VEJKVEO.js → manager-N2RVJN7T.js} +1 -1
  22. package/payload/server/maxy-edge.js +302 -33
  23. package/payload/server/public/activity.html +5 -5
  24. package/payload/server/public/agents.html +4 -4
  25. package/payload/server/public/assets/{AdminLoginScreens-DLogIsJT.js → AdminLoginScreens-5gI137xF.js} +1 -1
  26. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.br +0 -0
  27. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.gz +0 -0
  28. package/payload/server/public/assets/AdminShell-Bfapavlp.js +3 -0
  29. package/payload/server/public/assets/AdminShell-Bfapavlp.js.br +0 -0
  30. package/payload/server/public/assets/AdminShell-Bfapavlp.js.gz +0 -0
  31. package/payload/server/public/assets/{activity-AVJSFESV.js → activity-DnbsmUgn.js} +1 -1
  32. package/payload/server/public/assets/activity-DnbsmUgn.js.br +0 -0
  33. package/payload/server/public/assets/activity-DnbsmUgn.js.gz +0 -0
  34. package/payload/server/public/assets/admin-BXIv0_oj.js +1 -0
  35. package/payload/server/public/assets/admin-BXIv0_oj.js.br +0 -0
  36. package/payload/server/public/assets/admin-BXIv0_oj.js.gz +0 -0
  37. package/payload/server/public/assets/{agents-CoWLZMf7.js → agents-Dl2wOcpj.js} +1 -1
  38. package/payload/server/public/assets/agents-Dl2wOcpj.js.br +0 -0
  39. package/payload/server/public/assets/agents-Dl2wOcpj.js.gz +0 -0
  40. package/payload/server/public/assets/{browser-C6K66_fw.js → browser-D-Uc9ewO.js} +1 -1
  41. package/payload/server/public/assets/browser-D-Uc9ewO.js.br +0 -0
  42. package/payload/server/public/assets/browser-D-Uc9ewO.js.gz +0 -0
  43. package/payload/server/public/assets/{calendar-Dnw_cP4b.js → calendar-Cu3O6Bay.js} +1 -1
  44. package/payload/server/public/assets/calendar-Cu3O6Bay.js.br +0 -0
  45. package/payload/server/public/assets/calendar-Cu3O6Bay.js.gz +0 -0
  46. package/payload/server/public/assets/chat-Bso94e-f.js +1 -0
  47. package/payload/server/public/assets/chat-Bso94e-f.js.br +0 -0
  48. package/payload/server/public/assets/chat-Bso94e-f.js.gz +0 -0
  49. package/payload/server/public/assets/chevron-left-CDwfR9V-.js +1 -0
  50. package/payload/server/public/assets/chevron-left-CDwfR9V-.js.br +0 -0
  51. package/payload/server/public/assets/clock-DDVrNLt9.js +1 -0
  52. package/payload/server/public/assets/clock-DDVrNLt9.js.br +0 -0
  53. package/payload/server/public/assets/clock-DDVrNLt9.js.gz +0 -0
  54. package/payload/server/public/assets/{copy-kuNM0Etw.js → copy-DPBVV8KZ.js} +1 -1
  55. package/payload/server/public/assets/copy-DPBVV8KZ.js.br +0 -0
  56. package/payload/server/public/assets/copy-DPBVV8KZ.js.gz +0 -0
  57. package/payload/server/public/assets/data-Dei8n2EX.js +1 -0
  58. package/payload/server/public/assets/data-Dei8n2EX.js.br +0 -0
  59. package/payload/server/public/assets/data-Dei8n2EX.js.gz +0 -0
  60. package/payload/server/public/assets/{file-text-BWkobrcx.js → file-text-Dn82O6ke.js} +1 -1
  61. package/payload/server/public/assets/file-text-Dn82O6ke.js.br +0 -0
  62. package/payload/server/public/assets/file-text-Dn82O6ke.js.gz +0 -0
  63. package/payload/server/public/assets/{graph-BGjTL5Vz.js → graph-D2J7a7Ll.js} +1 -1
  64. package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
  65. package/payload/server/public/assets/graph-D2J7a7Ll.js.gz +0 -0
  66. package/payload/server/public/assets/{graph-labels-Dvc5Kw_r.js → graph-labels-D3gkY2IH.js} +1 -1
  67. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.br +0 -0
  68. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.gz +0 -0
  69. package/payload/server/public/assets/{operator-CzBGv_he.js → operator-Bz_yj1im.js} +1 -1
  70. package/payload/server/public/assets/operator-Bz_yj1im.js.br +0 -0
  71. package/payload/server/public/assets/operator-Bz_yj1im.js.gz +0 -0
  72. package/payload/server/public/assets/page-D6u2xYh7.js +1 -0
  73. package/payload/server/public/assets/page-D6u2xYh7.js.br +0 -0
  74. package/payload/server/public/assets/page-D6u2xYh7.js.gz +0 -0
  75. package/payload/server/public/assets/{page-BIY4Ku8Q.js → page-mH4nyQ75.js} +2 -2
  76. package/payload/server/public/assets/page-mH4nyQ75.js.br +0 -0
  77. package/payload/server/public/assets/page-mH4nyQ75.js.gz +0 -0
  78. package/payload/server/public/assets/play-ByuChQJJ.js +1 -0
  79. package/payload/server/public/assets/play-ByuChQJJ.js.br +0 -0
  80. package/payload/server/public/assets/play-ByuChQJJ.js.gz +0 -0
  81. package/payload/server/public/assets/{public-pwDSvhBv.js → public-CtoYVjPR.js} +1 -1
  82. package/payload/server/public/assets/public-CtoYVjPR.js.br +0 -0
  83. package/payload/server/public/assets/public-CtoYVjPR.js.gz +0 -0
  84. package/payload/server/public/assets/{rotate-ccw-B6s7tkEt.js → rotate-ccw-BXoIghE3.js} +1 -1
  85. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.br +0 -0
  86. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.gz +0 -0
  87. package/payload/server/public/assets/{routines-CFlyRdYg.js → routines-DXleYpDv.js} +1 -1
  88. package/payload/server/public/assets/routines-DXleYpDv.js.br +0 -0
  89. package/payload/server/public/assets/routines-DXleYpDv.js.gz +0 -0
  90. package/payload/server/public/assets/{skills-C45kfL_Q.js → skills-CMl38MKH.js} +1 -1
  91. package/payload/server/public/assets/skills-CMl38MKH.js.br +0 -0
  92. package/payload/server/public/assets/skills-CMl38MKH.js.gz +0 -0
  93. package/payload/server/public/assets/{tasks-BwoCTCu5.js → tasks-Cshaf0Md.js} +1 -1
  94. package/payload/server/public/assets/tasks-Cshaf0Md.js.br +0 -0
  95. package/payload/server/public/assets/tasks-Cshaf0Md.js.gz +0 -0
  96. package/payload/server/public/assets/{triangle-alert-B3bthSzx.js → triangle-alert-CS-NXr_9.js} +1 -1
  97. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.br +3 -0
  98. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.gz +0 -0
  99. package/payload/server/public/assets/{useCopyFeedback-BzJAkFEb.js → useCopyFeedback-pKLfQJNG.js} +1 -1
  100. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.br +0 -0
  101. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.gz +0 -0
  102. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css +1 -0
  103. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.br +0 -0
  104. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.gz +0 -0
  105. package/payload/server/public/assets/useMediaQuery-oDCasnxD.js +14 -0
  106. package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
  107. package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.gz +0 -0
  108. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js +2 -0
  109. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.br +0 -0
  110. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.gz +0 -0
  111. package/payload/server/public/assets/{wrench-Bqux9jaw.js → wrench-czES9Rdy.js} +1 -1
  112. package/payload/server/public/assets/wrench-czES9Rdy.js.br +0 -0
  113. package/payload/server/public/assets/wrench-czES9Rdy.js.gz +0 -0
  114. package/payload/server/public/browser.html +4 -4
  115. package/payload/server/public/calendar.html +7 -7
  116. package/payload/server/public/chat.html +13 -13
  117. package/payload/server/public/data.html +11 -11
  118. package/payload/server/public/graph.html +9 -9
  119. package/payload/server/public/index.html +13 -13
  120. package/payload/server/public/operator.html +14 -14
  121. package/payload/server/public/public.html +13 -13
  122. package/payload/server/public/routines.html +7 -7
  123. package/payload/server/public/skills.html +5 -5
  124. package/payload/server/public/tasks.html +6 -6
  125. package/payload/server/server.js +2946 -1683
  126. package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.br +0 -0
  127. package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.gz +0 -0
  128. package/payload/server/public/assets/AdminShell-B67tPTyE.js +0 -2
  129. package/payload/server/public/assets/AdminShell-B67tPTyE.js.br +0 -0
  130. package/payload/server/public/assets/AdminShell-B67tPTyE.js.gz +0 -0
  131. package/payload/server/public/assets/activity-AVJSFESV.js.br +0 -0
  132. package/payload/server/public/assets/activity-AVJSFESV.js.gz +0 -0
  133. package/payload/server/public/assets/admin-eFgbld42.js +0 -1
  134. package/payload/server/public/assets/admin-eFgbld42.js.br +0 -0
  135. package/payload/server/public/assets/admin-eFgbld42.js.gz +0 -0
  136. package/payload/server/public/assets/agents-CoWLZMf7.js.br +0 -0
  137. package/payload/server/public/assets/agents-CoWLZMf7.js.gz +0 -0
  138. package/payload/server/public/assets/browser-C6K66_fw.js.br +0 -0
  139. package/payload/server/public/assets/browser-C6K66_fw.js.gz +0 -0
  140. package/payload/server/public/assets/calendar-Dnw_cP4b.js.br +0 -0
  141. package/payload/server/public/assets/calendar-Dnw_cP4b.js.gz +0 -0
  142. package/payload/server/public/assets/chat-Cm0fkpg1.js +0 -1
  143. package/payload/server/public/assets/chat-Cm0fkpg1.js.br +0 -0
  144. package/payload/server/public/assets/chat-Cm0fkpg1.js.gz +0 -0
  145. package/payload/server/public/assets/chevron-left-CjRH-BtF.js +0 -1
  146. package/payload/server/public/assets/chevron-left-CjRH-BtF.js.br +0 -0
  147. package/payload/server/public/assets/clock-wbYEt1kM.js +0 -1
  148. package/payload/server/public/assets/clock-wbYEt1kM.js.br +0 -0
  149. package/payload/server/public/assets/clock-wbYEt1kM.js.gz +0 -0
  150. package/payload/server/public/assets/copy-kuNM0Etw.js.br +0 -0
  151. package/payload/server/public/assets/copy-kuNM0Etw.js.gz +0 -0
  152. package/payload/server/public/assets/data-c47T2NCd.js +0 -1
  153. package/payload/server/public/assets/data-c47T2NCd.js.br +0 -1
  154. package/payload/server/public/assets/data-c47T2NCd.js.gz +0 -0
  155. package/payload/server/public/assets/file-text-BWkobrcx.js.br +0 -0
  156. package/payload/server/public/assets/file-text-BWkobrcx.js.gz +0 -0
  157. package/payload/server/public/assets/graph-BGjTL5Vz.js.br +0 -0
  158. package/payload/server/public/assets/graph-BGjTL5Vz.js.gz +0 -0
  159. package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.br +0 -0
  160. package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.gz +0 -0
  161. package/payload/server/public/assets/operator-CzBGv_he.js.br +0 -0
  162. package/payload/server/public/assets/operator-CzBGv_he.js.gz +0 -0
  163. package/payload/server/public/assets/page-BIY4Ku8Q.js.br +0 -0
  164. package/payload/server/public/assets/page-BIY4Ku8Q.js.gz +0 -0
  165. package/payload/server/public/assets/page-DaySqamH.js +0 -1
  166. package/payload/server/public/assets/page-DaySqamH.js.br +0 -0
  167. package/payload/server/public/assets/page-DaySqamH.js.gz +0 -0
  168. package/payload/server/public/assets/play-C8RA52_i.js +0 -1
  169. package/payload/server/public/assets/play-C8RA52_i.js.br +0 -0
  170. package/payload/server/public/assets/play-C8RA52_i.js.gz +0 -0
  171. package/payload/server/public/assets/public-pwDSvhBv.js.br +0 -0
  172. package/payload/server/public/assets/public-pwDSvhBv.js.gz +0 -0
  173. package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.br +0 -0
  174. package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.gz +0 -0
  175. package/payload/server/public/assets/routines-CFlyRdYg.js.br +0 -0
  176. package/payload/server/public/assets/routines-CFlyRdYg.js.gz +0 -0
  177. package/payload/server/public/assets/skills-C45kfL_Q.js.br +0 -0
  178. package/payload/server/public/assets/skills-C45kfL_Q.js.gz +0 -0
  179. package/payload/server/public/assets/tasks-BwoCTCu5.js.br +0 -0
  180. package/payload/server/public/assets/tasks-BwoCTCu5.js.gz +0 -0
  181. package/payload/server/public/assets/triangle-alert-B3bthSzx.js.br +0 -0
  182. package/payload/server/public/assets/triangle-alert-B3bthSzx.js.gz +0 -0
  183. package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.br +0 -0
  184. package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.gz +0 -0
  185. package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js +0 -14
  186. package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.br +0 -0
  187. package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.gz +0 -0
  188. package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css +0 -1
  189. package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.br +0 -0
  190. package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.gz +0 -0
  191. package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js +0 -2
  192. package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.br +0 -0
  193. package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.gz +0 -0
  194. package/payload/server/public/assets/wrench-Bqux9jaw.js.br +0 -0
  195. package/payload/server/public/assets/wrench-Bqux9jaw.js.gz +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.562",
3
+ "version": "0.1.564",
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:2a6a3a040c8d55a472cfc2eda9180b15e5a0ebd8e5ce49a997c4348570ffd37b
4
+ content-hash: sha256:ad9b32a95086e27720356ba87f13727d7aa883d772dfa2539e67b33f70e6cd91
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -294,9 +294,11 @@ The Data search panel ranks results by combining vector similarity with keyword
294
294
 
295
295
  ## Software Update and Cloudflare Setup
296
296
 
297
- Both flows run on the native Claude Code PTY surface in admin chat. There is no in-app upgrade modal and no Cloudflare setup form the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
297
+ Cloudflare setup runs on the native Claude Code PTY surface in admin chat: there is no Cloudflare setup form, the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
298
298
 
299
- - **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`) from a shell; HeaderMenu's version row turns sage when `installed === latest`.
299
+ Software update has two doors. The PTY one is the same shape: the agent runs the installer via Bash and its stdout streams into chat. The dashboard one is a modal over the edge-hosted `/api/admin/actions` routes, which run the installer as a `systemd-run` transient unit and stream its log back by byte offset. Both re-run the same installer; neither mutates state directly.
300
+
301
+ - **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`), from a shell, from admin chat via the `upgrade` skill, or from the dashboard control in the sidebar brand head. The brand head shows the installed version at all times and grows a download icon only while `updateAvailable` is true.
300
302
  - **Cloudflare setup.** Operator asks in chat; the agent invokes `cloudflared` directly via the Bash tool, following the numbered steps in `plugins/cloudflare/references/manual-setup.md`. cloudflared's stdout and stderr stream into the PTY; the OAuth URL printed by `cloudflared tunnel login` is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
301
303
 
302
304
  **Mid-turn stream-drop banners.** If a chat turn ends abruptly the bubble shows one of two messages depending on what actually happened. You see "Server is restarting — reconnect will happen automatically." only when the app server itself emits the restart signal — typically during a Software Update or a Cloudflare setup that re-launches the brand service. You see "Lost connection — retrying." when your browser's connection to the Pi dropped mid-stream while the server was still up — typically a flaky Wi-Fi moment or the tunnel hiccupping. Either way the chat resumes once the connection is back; the previously-rendered messages stay on screen so you don't lose context.
@@ -2542,6 +2544,25 @@ Ask Maxy: "What messages has the bot received?" or "Show recent Telegram activit
2542
2544
 
2543
2545
  **Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
2544
2546
 
2547
+ ## Sending from the dashboard
2548
+
2549
+ Open a Telegram conversation in the dashboard and you can type a reply straight
2550
+ into it, the same way you already can with WhatsApp. The message goes out from
2551
+ your bot.
2552
+
2553
+ Two things to know:
2554
+
2555
+ - **Direct messages only.** Group and supergroup chats show no message box. A
2556
+ Telegram conversation is filed under the person who spoke, so a message sent
2557
+ from a group thread would reach that one person privately rather than the
2558
+ group — the box is hidden rather than doing something you did not intend.
2559
+ - **History starts now.** Messages are only shown from the point this feature
2560
+ was installed onward. A conversation that was already running looks empty
2561
+ until the next message arrives, and then fills up from there.
2562
+
2563
+ Text only for now. Photos, files and voice notes are not available on Telegram
2564
+ from the dashboard.
2565
+
2545
2566
  ---
2546
2567
  # Outlook
2547
2568
  Source: https://docs.getmaxy.com/outlook-guide.md
@@ -6110,11 +6131,11 @@ The logs will show which service failed to start and why. Common causes:
6110
6131
  Each installed brand runs two per-brand `--user` systemd units (earlier platform fixes + — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
6111
6132
 
6112
6133
  - `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19201` (public port + 1). Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade. An earlier fix: the unit carries two port env vars — `PORT=<public>` (canonical public port, read by the upgrade detector) and `MAXY_UI_INTERNAL_PORT=<public+1>` (the port maxy-ui actually binds).
6113
- - `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so the log stream survives the brand service's restart window. Does NOT restart during an upgrade the browser WebSocket stays connected by construction. The operator views the device's Chromium in the admin UI via the standalone `/browser` page (peer to `/graph` and `/data`) and on the Claude sign-in screen; both render CDP screencast frames over `/cdp-screencast`, not the framebuffer. Virtual-mode Chromium is headless, so nothing renders onto the X display and the `/websockify` upgrade has no remaining consumer; deleting that stack is separate work.
6134
+ - `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so they survive the brand service's restart window. It is restarted by the upgrade flow too (`packages/create-maxy-code/src/index.ts:5143`, after the brand service is stopped and before it is restarted), so the update stream drops near the end of every run and resumes by byte offset. It is not a connection that survives; the log file is what survives, and the browser's `EventSource` replays `Last-Event-ID` to pick the tail back up. The operator views the device's Chromium in the admin UI via the standalone `/browser` page (peer to `/graph` and `/data`) and on the Claude sign-in screen; both render CDP screencast frames over `/cdp-screencast`, not the framebuffer. Virtual-mode Chromium is headless, so nothing renders onto the X display and the `/websockify` upgrade has no remaining consumer; deleting that stack is separate work.
6114
6135
 
6115
- Upgrade and Cloudflare setup run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
6136
+ The upgrade runs as a detached action: a `systemd-run --user --unit=maxy-action-<actionId> --collect` transient unit per invocation, with stdout+stderr persisted to `~/.<brand>/logs/actions/<actionId>.log` and streamed to the UI via SSE. The unit has its own cgroup, so it outlives both service restarts the installer performs. Its final log line is `[action-exit] code=<n>`, written by the wrapper, which is how the exit code survives a unit that `--collect` has already reaped. A sidecar `~/.<brand>/logs/actions/<actionId>.json` holds the package, start time and pre-upgrade version, so a modal reopened after a reload can still report what changed. No boot-time service file exists for these. Cloudflare setup is not an action; it runs PTY-native from admin chat.
6116
6137
 
6117
- If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
6138
+ If an action looks stuck, read `~/.<brand>/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record. The lifecycle lines are tagged `[admin-action]` and keyed by `actionId`; they land in `~/.<brand>/logs/edge.log`, not `server.log`, because these routes run on the edge process.
6118
6139
 
6119
6140
  ## Linux laptops: snap-confined Chromium replacement
6120
6141
 
@@ -6223,17 +6244,23 @@ Empty output from step 3 = brand.json resolved cleanly and the badge reflects th
6223
6244
 
6224
6245
  ## Upgrading
6225
6246
 
6226
- To upgrade Maxy to the latest version, ask Maxy: "Upgrade Maxy." The platform checks the current device identity (hostname and port via `system-status`), then re-runs the installer with explicit `--hostname` and `--port` flags to preserve them across the upgrade.
6247
+ There are two ways to upgrade, and both run the same installer.
6248
+
6249
+ From the dashboard, press the download icon in the sidebar header (see below). From chat, ask Maxy: "Upgrade Maxy." The platform checks the current device identity (hostname and port via `system-status`), then re-runs the installer with explicit `--hostname` and `--port` flags to preserve them across the upgrade.
6227
6250
 
6228
6251
  The docs plugin (this plugin) is upgraded in the same step — you always have the documentation that matches your installed version.
6229
6252
 
6230
- ### Automatic upgrade alert
6253
+ ### Upgrading from the dashboard
6254
+
6255
+ The sidebar header shows the version you are running, under the product name, at all times. When a newer one is published a download icon appears beside it. There is no icon when you are already up to date, so the icon appearing is itself the news. If you work with the sidebar collapsed to the narrow rail, the version and the icon are hidden with the rest of the header text and a small coloured dot on the brand icon carries the same signal.
6256
+
6257
+ Pressing the icon opens the Software Update window, which shows the version you have against the version available and an Update button. Pressing Update runs the installer and streams its output into the window as it happens.
6231
6258
 
6232
- Maxy checks for new releases on every admin session start whenever you log in, reload the page, or return to the admin chat. When a newer version is available, the Software Update window opens automatically showing your current and the latest version, with a one-click Upgrade button. Dismissing the window (click outside or the close button) defers the alert until your next login or reload; no alert is shown when you are already on the latest version.
6259
+ Closing the window does not cancel the upgrade. The installer runs in its own background unit, so it keeps going whether or not anything is watching, and reopening the window reattaches to the run in progress.
6233
6260
 
6234
- The upgrade runs inside a live terminal embedded in the Software Update window you see each installation step stream as it happens, and any password prompts from `sudo` appear directly in the terminal for you to answer. Closing the window does not cancel the upgrade; re-opening it reattaches to the same shell so you can see what happened while disconnected.
6261
+ **The connection drops near the end of every upgrade, and that is expected.** The installer restarts the server as its last step, which cuts the browser's connection to it. The window says "Reconnecting" while that happens, then picks the output back up from exactly where it left off. Nothing is lost and nothing needs restarting. When it finishes, the window reports the exit code and whether the version actually moved.
6235
6262
 
6236
- The header menu's version indicator still reflects real-time status: a green dot means you are up to date, and an accent-coloured dot means an upgrade is available. Opening the menu refreshes the version check, so a long-lived session can still surface an upgrade that became available after login without reloading the page.
6263
+ The window reports a genuine failure in two shapes worth knowing. If the installer exits non-zero it says so with the code. If it exits cleanly but the version did not change, it says that too, rather than claiming success.
6237
6264
 
6238
6265
  ---
6239
6266
  # Samba Share
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-05 (0.1.564)
13
+
14
+ - You can now reply to a Telegram conversation from the dashboard. Telegram was read-only, so a conversation you could watch was one you could not answer; it now has a composer like WhatsApp. Sending is restricted to private chats.
15
+ - An arriving Telegram message now raises a card and a desktop notification, the same as WhatsApp. Until now it arrived silently on every admin surface.
16
+ - A portal job sheet now records time, so a day's labour reaches the timesheet. Field work could be marked done but carried no hours and wrote no labour cost at either end.
17
+
18
+ ## 2026-08-05 (0.1.563)
19
+
20
+ - You can now update from the dashboard. The installed version sits in the sidebar header rather than buried in the footer menu, and when a newer one is available the control there starts the update and shows its progress live. Previously the only route was asking the assistant in chat, and an available update was invisible until you opened the menu.
21
+ - A public bot no longer loses the first message it is sent. A message that arrived while the conversation was still starting up was handed over a moment too early and discarded silently; delivery now waits until the conversation is ready to receive it.
22
+ - The data portal can now hold records, not just files. A portal login can be scoped to see only the records assigned to that person rather than everything for the account, work assigned in the office appears on the portal, and what is filled in on site comes back. A standing check reports any exchange that stalls.
23
+
12
24
  ## 2026-08-05 (0.1.562)
13
25
 
14
26
  - A public agent no longer sees anything about you or your machine. A stranger messaging a public bot was having your connected mailbox list, this machine's name and network addresses, its admin and tunnel web addresses, today's date and your working preferences added to every turn, alongside the persona you wrote. A public turn now carries only what the agent's author wrote plus its single reply tool.
@@ -108,6 +108,17 @@ if (foldersArg !== undefined) {
108
108
  folders = [...new Set(names)].join(',')
109
109
  }
110
110
 
111
+ // Which RECORDS this person reads, as distinct from --folders, which is which
112
+ // files they browse. Two values and no third.
113
+ //
114
+ // The absent flag is the NARROW one. Nobody is granted account-wide visibility
115
+ // by leaving a flag off, and a typo is refused rather than resolved to
116
+ // whichever value happens to be nearer — there is no third scope for it to be.
117
+ const recordScope = arg('--record-scope') ?? 'own'
118
+ if (recordScope !== 'own' && recordScope !== 'account') {
119
+ die('--record-scope must be "own" or "account" — there is no third scope')
120
+ }
121
+
111
122
  const saltBytes = new Uint8Array(16)
112
123
  crypto.getRandomValues(saltBytes)
113
124
  const saltHex = randomSaltHex(saltBytes)
@@ -155,14 +166,17 @@ const createdAt = new Date().toISOString()
155
166
  // The dedicated path keeps `accountId` in the DO UPDATE exactly as before, so a
156
167
  // grandfathered portal's behaviour is unchanged in every respect.
157
168
  //
158
- // `folders` IS in the DO UPDATE on both: re-enrolment replaces the grant, so a
159
- // narrower or wider set on re-run takes effect rather than sticking.
169
+ // `folders` and `recordScope` are BOTH in the DO UPDATE on both models:
170
+ // re-enrolment replaces each, so a narrower or wider setting on a re-run takes
171
+ // effect rather than sticking. Narrowing matters most — a person moved off the
172
+ // account-wide scope must actually lose it, and the re-run is the only path
173
+ // there is.
160
174
  const conflict =
161
175
  storageModel === 'shared'
162
- ? 'ON CONFLICT(accountId, ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, folders=excluded.folders'
163
- : 'ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId, folders=excluded.folders'
176
+ ? 'ON CONFLICT(accountId, ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, folders=excluded.folders, recordScope=excluded.recordScope'
177
+ : 'ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId, folders=excluded.folders, recordScope=excluded.recordScope'
164
178
  console.log(
165
- `INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}') ${conflict};`,
179
+ `INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders, recordScope) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}', '${recordScope}') ${conflict};`,
166
180
  )
167
181
 
168
182
  // stderr: everything for the human. Never stdout — see the note at the top.
@@ -102,6 +102,9 @@ describe('sessions', () => {
102
102
  ownerId: 'alice',
103
103
  accountId: ACC,
104
104
  folders: [],
105
+ // The fake's people row carries no recordScope, which is a legacy row.
106
+ // It resolves narrow, not wide (Task 2425).
107
+ recordScope: 'own',
105
108
  })
106
109
  })
107
110
 
@@ -186,6 +189,9 @@ describe('session revocation — the property the design claims', () => {
186
189
  ownerId: 'alice',
187
190
  accountId: ACC,
188
191
  folders: [],
192
+ // The fake's people row carries no recordScope, which is a legacy row.
193
+ // It resolves narrow, not wide (Task 2425).
194
+ recordScope: 'own',
189
195
  })
190
196
 
191
197
  peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
@@ -32,7 +32,7 @@ function parse(r: { stdout: string; stderr: string }) {
32
32
  }
33
33
 
34
34
  const valuesOf = (sql: string) =>
35
- /VALUES \('([^']*)', '((?:[^']|'')*)', '([0-9a-f]+)', '([0-9a-f]+)', '([^']*)', '([^']*)', '([^']*)'\)/.exec(
35
+ /VALUES \('([^']*)', '((?:[^']|'')*)', '([0-9a-f]+)', '([0-9a-f]+)', '([^']*)', '([^']*)', '([^']*)', '([^']*)'\)/.exec(
36
36
  sql,
37
37
  )!
38
38
 
@@ -129,9 +129,9 @@ describe('portal-enrol', () => {
129
129
  const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
130
130
  const { sql } = parse(r)
131
131
  expect(sql).toContain(
132
- 'INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders)',
132
+ 'INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders, recordScope)',
133
133
  )
134
- const [, owner, name, salt, hash, createdAt, accountId, folders] = valuesOf(sql!)
134
+ const [, owner, name, salt, hash, createdAt, accountId, folders, recordScope] = valuesOf(sql!)
135
135
  expect(owner).toBe('alice')
136
136
  expect(name).toBe('Alice')
137
137
  expect(salt).toMatch(/^[0-9a-f]{32}$/)
@@ -139,6 +139,57 @@ describe('portal-enrol', () => {
139
139
  expect(Number.isNaN(Date.parse(createdAt))).toBe(false)
140
140
  expect(accountId).toBe('acc-1')
141
141
  expect(folders).toBe('')
142
+ expect(recordScope).toBe('own')
143
+ })
144
+
145
+ describe('record scope (Task 2425)', () => {
146
+ it('defaults to own when the flag is absent', async () => {
147
+ const { sql } = parse(await enrol(['--owner', 'alice', '--name', 'Alice']))
148
+ expect(valuesOf(sql!)[8]).toBe('own')
149
+ })
150
+
151
+ it('writes account when asked for it', async () => {
152
+ const { sql } = parse(
153
+ await enrol(['--owner', 'marie', '--name', 'Marie', '--record-scope', 'account']),
154
+ )
155
+ expect(valuesOf(sql!)[8]).toBe('account')
156
+ })
157
+
158
+ it('dies on a third value, before any SQL reaches stdout', async () => {
159
+ // There is no third scope. A typo must not silently land as one of the
160
+ // two, and it must not land at all: stdout is empty, so the documented
161
+ // pipe carries nothing into wrangler.
162
+ await expect(
163
+ enrol(['--owner', 'alice', '--name', 'Alice', '--record-scope', 'admin']),
164
+ ).rejects.toMatchObject({ stdout: '' })
165
+ })
166
+
167
+ it('replaces the scope on re-enrolment, on both storage models', async () => {
168
+ const shared = parse(
169
+ await enrol(['--owner', 'a', '--name', 'A', '--record-scope', 'account']),
170
+ )
171
+ expect(shared.sql).toContain('recordScope=excluded.recordScope')
172
+ const dedicated = parse(
173
+ await enrol(['--owner', 'a', '--name', 'A', '--storage-model', 'dedicated']),
174
+ )
175
+ expect(dedicated.sql).toContain('recordScope=excluded.recordScope')
176
+ })
177
+
178
+ it('lands in a real database and the re-run narrows it back', async () => {
179
+ // Against the template's own schema.sql, not a fake: a fake would model
180
+ // the very constraint we are proving we satisfy, which is how 1689
181
+ // shipped a UNIQUE-violating upload.
182
+ const db = await d1('portal-record-scope-')
183
+ const wide = parse(
184
+ await enrol(['--owner', 'alice', '--name', 'Alice', '--record-scope', 'account']),
185
+ )
186
+ await db.exec(wide.sql!)
187
+ expect(await db.query("SELECT recordScope FROM people WHERE ownerId='alice'")).toBe('account')
188
+
189
+ const narrow = parse(await enrol(['--owner', 'alice', '--name', 'Alice']))
190
+ await db.exec(narrow.sql!)
191
+ expect(await db.query("SELECT recordScope FROM people WHERE ownerId='alice'")).toBe('own')
192
+ })
142
193
  })
143
194
 
144
195
  it('refuses enrolment with no --account', async () => {
@@ -175,8 +226,14 @@ describe('portal-enrol', () => {
175
226
  })
176
227
 
177
228
  describe('the conflict target follows the storage model (Task 2099)', () => {
178
- /** A pre-2099 portal's own database: `people` keyed on ownerId alone. */
179
- async function legacyDb(dir: string) {
229
+ /** A pre-2099 portal's own database: `people` keyed on ownerId alone.
230
+ *
231
+ * `withRecordScope` is the hand `ALTER` schema.sql tells an operator to run
232
+ * on a table that predates the column (Task 2425). Both shapes are real and
233
+ * the tests below need each: without it, enrolment must fail loudly, which
234
+ * is what the schema comment claims; with it, rotation must keep working,
235
+ * which is what the composite-target regression is about. */
236
+ async function legacyDb(dir: string, withRecordScope = true) {
180
237
  const { mkdtemp } = await import('node:fs/promises')
181
238
  const { tmpdir } = await import('node:os')
182
239
  const root = await mkdtemp(resolve(tmpdir(), dir))
@@ -185,7 +242,10 @@ describe('portal-enrol', () => {
185
242
  db,
186
243
  'CREATE TABLE people (ownerId TEXT PRIMARY KEY, name TEXT NOT NULL, salt TEXT NOT NULL, ' +
187
244
  "hash TEXT NOT NULL, createdAt TEXT NOT NULL, accountId TEXT NOT NULL DEFAULT '', " +
188
- "folders TEXT NOT NULL DEFAULT '');",
245
+ "folders TEXT NOT NULL DEFAULT '');" +
246
+ (withRecordScope
247
+ ? " ALTER TABLE people ADD COLUMN recordScope TEXT NOT NULL DEFAULT 'own';"
248
+ : ''),
189
249
  ])
190
250
  return {
191
251
  exec: (sql: string) => run('sqlite3', [db, sql]),
@@ -193,6 +253,17 @@ describe('portal-enrol', () => {
193
253
  }
194
254
  }
195
255
 
256
+ it('fails loudly on a table that never got the recordScope column', async () => {
257
+ // schema.sql claims enrolment's INSERT names the column, so a table
258
+ // predating it fails loudly rather than writing a row with no scope. A
259
+ // property a comment asserts has to be a property the code has.
260
+ const db = await legacyDb('enrol-legacy-noscope-', false)
261
+ const r = parse(
262
+ await enrol(['--owner', 'dale', '--name', 'Dale', '--account', 'acct-a', '--storage-model', 'dedicated']),
263
+ )
264
+ await expect(db.exec(r.sql!)).rejects.toThrow(/no column named recordScope/)
265
+ })
266
+
196
267
  it('lets two tenants each enrol the same person on the shared store', async () => {
197
268
  const db = await d1('enrol-two-tenants-')
198
269
  await db.exec(parse(await enrol(['--owner', 'rob', '--name', 'Rob', '--account', 'acct-a'])).sql!)
@@ -72,6 +72,45 @@ describe('resolveSession folders', () => {
72
72
  })
73
73
  })
74
74
 
75
+ describe('resolveSession recordScope', () => {
76
+ const row = (recordScope?: unknown) => ({
77
+ ownerId: 'alice',
78
+ accountId: 'acc-a',
79
+ folders: '',
80
+ ...(recordScope === undefined ? {} : { recordScope }),
81
+ })
82
+
83
+ it('returns account for a person who holds it', async () => {
84
+ const s = await resolveSession(db(row('account')), 'sess', 'acc-a', 1)
85
+ expect(s?.recordScope).toBe('account')
86
+ })
87
+
88
+ it('returns own for a person who holds it', async () => {
89
+ const s = await resolveSession(db(row('own')), 'sess', 'acc-a', 1)
90
+ expect(s?.recordScope).toBe('own')
91
+ })
92
+
93
+ // Every unreadable reading is the NARROW one. Only the exact token widens.
94
+ // Reverting the fallback to 'account' must turn all four of these red — a
95
+ // read failure that grants account-wide visibility is the one outcome this
96
+ // column exists to prevent.
97
+ it('falls back to own for an empty string', async () => {
98
+ expect((await resolveSession(db(row('')), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
99
+ })
100
+
101
+ it('falls back to own for an unknown token', async () => {
102
+ expect((await resolveSession(db(row('admin')), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
103
+ })
104
+
105
+ it('falls back to own for a null value', async () => {
106
+ expect((await resolveSession(db(row(null)), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
107
+ })
108
+
109
+ it('falls back to own for a legacy row with no column at all', async () => {
110
+ expect((await resolveSession(db(row()), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
111
+ })
112
+ })
113
+
75
114
  describe('grantAllows', () => {
76
115
  it('admits everything when the grant is empty', () => {
77
116
  expect(grantAllows([], 'documents/secret.pdf')).toBe(true)