@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.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +38 -11
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +19 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +6 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +77 -6
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +39 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +294 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +112 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +38 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +112 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +31 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +93 -0
- package/payload/platform/plugins/docs/references/deployment.md +14 -8
- package/payload/platform/plugins/docs/references/platform.md +4 -2
- package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
- package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
- package/payload/platform/templates/agents/public/IDENTITY.md +3 -3
- package/payload/server/{chunk-G6ULACHG.js → chunk-3ABITJWB.js} +8 -4
- package/payload/server/{manager-2VEJKVEO.js → manager-N2RVJN7T.js} +1 -1
- package/payload/server/maxy-edge.js +302 -33
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-DLogIsJT.js → AdminLoginScreens-5gI137xF.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-Bfapavlp.js +3 -0
- package/payload/server/public/assets/AdminShell-Bfapavlp.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Bfapavlp.js.gz +0 -0
- package/payload/server/public/assets/{activity-AVJSFESV.js → activity-DnbsmUgn.js} +1 -1
- package/payload/server/public/assets/activity-DnbsmUgn.js.br +0 -0
- package/payload/server/public/assets/activity-DnbsmUgn.js.gz +0 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js +1 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js.br +0 -0
- package/payload/server/public/assets/admin-BXIv0_oj.js.gz +0 -0
- package/payload/server/public/assets/{agents-CoWLZMf7.js → agents-Dl2wOcpj.js} +1 -1
- package/payload/server/public/assets/agents-Dl2wOcpj.js.br +0 -0
- package/payload/server/public/assets/agents-Dl2wOcpj.js.gz +0 -0
- package/payload/server/public/assets/{browser-C6K66_fw.js → browser-D-Uc9ewO.js} +1 -1
- package/payload/server/public/assets/browser-D-Uc9ewO.js.br +0 -0
- package/payload/server/public/assets/browser-D-Uc9ewO.js.gz +0 -0
- package/payload/server/public/assets/{calendar-Dnw_cP4b.js → calendar-Cu3O6Bay.js} +1 -1
- package/payload/server/public/assets/calendar-Cu3O6Bay.js.br +0 -0
- package/payload/server/public/assets/calendar-Cu3O6Bay.js.gz +0 -0
- package/payload/server/public/assets/chat-Bso94e-f.js +1 -0
- package/payload/server/public/assets/chat-Bso94e-f.js.br +0 -0
- package/payload/server/public/assets/chat-Bso94e-f.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-CDwfR9V-.js +1 -0
- package/payload/server/public/assets/chevron-left-CDwfR9V-.js.br +0 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js +1 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js.br +0 -0
- package/payload/server/public/assets/clock-DDVrNLt9.js.gz +0 -0
- package/payload/server/public/assets/{copy-kuNM0Etw.js → copy-DPBVV8KZ.js} +1 -1
- package/payload/server/public/assets/copy-DPBVV8KZ.js.br +0 -0
- package/payload/server/public/assets/copy-DPBVV8KZ.js.gz +0 -0
- package/payload/server/public/assets/data-Dei8n2EX.js +1 -0
- package/payload/server/public/assets/data-Dei8n2EX.js.br +0 -0
- package/payload/server/public/assets/data-Dei8n2EX.js.gz +0 -0
- package/payload/server/public/assets/{file-text-BWkobrcx.js → file-text-Dn82O6ke.js} +1 -1
- package/payload/server/public/assets/file-text-Dn82O6ke.js.br +0 -0
- package/payload/server/public/assets/file-text-Dn82O6ke.js.gz +0 -0
- package/payload/server/public/assets/{graph-BGjTL5Vz.js → graph-D2J7a7Ll.js} +1 -1
- package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
- package/payload/server/public/assets/graph-D2J7a7Ll.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-Dvc5Kw_r.js → graph-labels-D3gkY2IH.js} +1 -1
- package/payload/server/public/assets/graph-labels-D3gkY2IH.js.br +0 -0
- package/payload/server/public/assets/graph-labels-D3gkY2IH.js.gz +0 -0
- package/payload/server/public/assets/{operator-CzBGv_he.js → operator-Bz_yj1im.js} +1 -1
- package/payload/server/public/assets/operator-Bz_yj1im.js.br +0 -0
- package/payload/server/public/assets/operator-Bz_yj1im.js.gz +0 -0
- package/payload/server/public/assets/page-D6u2xYh7.js +1 -0
- package/payload/server/public/assets/page-D6u2xYh7.js.br +0 -0
- package/payload/server/public/assets/page-D6u2xYh7.js.gz +0 -0
- package/payload/server/public/assets/{page-BIY4Ku8Q.js → page-mH4nyQ75.js} +2 -2
- package/payload/server/public/assets/page-mH4nyQ75.js.br +0 -0
- package/payload/server/public/assets/page-mH4nyQ75.js.gz +0 -0
- package/payload/server/public/assets/play-ByuChQJJ.js +1 -0
- package/payload/server/public/assets/play-ByuChQJJ.js.br +0 -0
- package/payload/server/public/assets/play-ByuChQJJ.js.gz +0 -0
- package/payload/server/public/assets/{public-pwDSvhBv.js → public-CtoYVjPR.js} +1 -1
- package/payload/server/public/assets/public-CtoYVjPR.js.br +0 -0
- package/payload/server/public/assets/public-CtoYVjPR.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-B6s7tkEt.js → rotate-ccw-BXoIghE3.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.gz +0 -0
- package/payload/server/public/assets/{routines-CFlyRdYg.js → routines-DXleYpDv.js} +1 -1
- package/payload/server/public/assets/routines-DXleYpDv.js.br +0 -0
- package/payload/server/public/assets/routines-DXleYpDv.js.gz +0 -0
- package/payload/server/public/assets/{skills-C45kfL_Q.js → skills-CMl38MKH.js} +1 -1
- package/payload/server/public/assets/skills-CMl38MKH.js.br +0 -0
- package/payload/server/public/assets/skills-CMl38MKH.js.gz +0 -0
- package/payload/server/public/assets/{tasks-BwoCTCu5.js → tasks-Cshaf0Md.js} +1 -1
- package/payload/server/public/assets/tasks-Cshaf0Md.js.br +0 -0
- package/payload/server/public/assets/tasks-Cshaf0Md.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-B3bthSzx.js → triangle-alert-CS-NXr_9.js} +1 -1
- package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.br +3 -0
- package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BzJAkFEb.js → useCopyFeedback-pKLfQJNG.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css +1 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-oDCasnxD.js +14 -0
- package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js +2 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.gz +0 -0
- package/payload/server/public/assets/{wrench-Bqux9jaw.js → wrench-czES9Rdy.js} +1 -1
- package/payload/server/public/assets/wrench-czES9Rdy.js.br +0 -0
- package/payload/server/public/assets/wrench-czES9Rdy.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +13 -13
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +7 -7
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +2946 -1683
- package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-B67tPTyE.js +0 -2
- package/payload/server/public/assets/AdminShell-B67tPTyE.js.br +0 -0
- package/payload/server/public/assets/AdminShell-B67tPTyE.js.gz +0 -0
- package/payload/server/public/assets/activity-AVJSFESV.js.br +0 -0
- package/payload/server/public/assets/activity-AVJSFESV.js.gz +0 -0
- package/payload/server/public/assets/admin-eFgbld42.js +0 -1
- package/payload/server/public/assets/admin-eFgbld42.js.br +0 -0
- package/payload/server/public/assets/admin-eFgbld42.js.gz +0 -0
- package/payload/server/public/assets/agents-CoWLZMf7.js.br +0 -0
- package/payload/server/public/assets/agents-CoWLZMf7.js.gz +0 -0
- package/payload/server/public/assets/browser-C6K66_fw.js.br +0 -0
- package/payload/server/public/assets/browser-C6K66_fw.js.gz +0 -0
- package/payload/server/public/assets/calendar-Dnw_cP4b.js.br +0 -0
- package/payload/server/public/assets/calendar-Dnw_cP4b.js.gz +0 -0
- package/payload/server/public/assets/chat-Cm0fkpg1.js +0 -1
- package/payload/server/public/assets/chat-Cm0fkpg1.js.br +0 -0
- package/payload/server/public/assets/chat-Cm0fkpg1.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-CjRH-BtF.js +0 -1
- package/payload/server/public/assets/chevron-left-CjRH-BtF.js.br +0 -0
- package/payload/server/public/assets/clock-wbYEt1kM.js +0 -1
- package/payload/server/public/assets/clock-wbYEt1kM.js.br +0 -0
- package/payload/server/public/assets/clock-wbYEt1kM.js.gz +0 -0
- package/payload/server/public/assets/copy-kuNM0Etw.js.br +0 -0
- package/payload/server/public/assets/copy-kuNM0Etw.js.gz +0 -0
- package/payload/server/public/assets/data-c47T2NCd.js +0 -1
- package/payload/server/public/assets/data-c47T2NCd.js.br +0 -1
- package/payload/server/public/assets/data-c47T2NCd.js.gz +0 -0
- package/payload/server/public/assets/file-text-BWkobrcx.js.br +0 -0
- package/payload/server/public/assets/file-text-BWkobrcx.js.gz +0 -0
- package/payload/server/public/assets/graph-BGjTL5Vz.js.br +0 -0
- package/payload/server/public/assets/graph-BGjTL5Vz.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.gz +0 -0
- package/payload/server/public/assets/operator-CzBGv_he.js.br +0 -0
- package/payload/server/public/assets/operator-CzBGv_he.js.gz +0 -0
- package/payload/server/public/assets/page-BIY4Ku8Q.js.br +0 -0
- package/payload/server/public/assets/page-BIY4Ku8Q.js.gz +0 -0
- package/payload/server/public/assets/page-DaySqamH.js +0 -1
- package/payload/server/public/assets/page-DaySqamH.js.br +0 -0
- package/payload/server/public/assets/page-DaySqamH.js.gz +0 -0
- package/payload/server/public/assets/play-C8RA52_i.js +0 -1
- package/payload/server/public/assets/play-C8RA52_i.js.br +0 -0
- package/payload/server/public/assets/play-C8RA52_i.js.gz +0 -0
- package/payload/server/public/assets/public-pwDSvhBv.js.br +0 -0
- package/payload/server/public/assets/public-pwDSvhBv.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.gz +0 -0
- package/payload/server/public/assets/routines-CFlyRdYg.js.br +0 -0
- package/payload/server/public/assets/routines-CFlyRdYg.js.gz +0 -0
- package/payload/server/public/assets/skills-C45kfL_Q.js.br +0 -0
- package/payload/server/public/assets/skills-C45kfL_Q.js.gz +0 -0
- package/payload/server/public/assets/tasks-BwoCTCu5.js.br +0 -0
- package/payload/server/public/assets/tasks-BwoCTCu5.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-B3bthSzx.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-B3bthSzx.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js +0 -14
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css +0 -1
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.gz +0 -0
- package/payload/server/public/assets/wrench-Bqux9jaw.js.br +0 -0
- package/payload/server/public/assets/wrench-Bqux9jaw.js.gz +0 -0
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
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`
|
|
159
|
-
// narrower or wider
|
|
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
|
-
|
|
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)
|