@rubytech/create-maxy-code 0.1.563 → 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 (177) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +20 -1
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +118 -0
  5. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +15 -0
  6. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +25 -7
  7. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +62 -5
  8. package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
  9. package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
  10. package/payload/server/{chunk-G6ULACHG.js → chunk-3ABITJWB.js} +8 -4
  11. package/payload/server/{manager-2VEJKVEO.js → manager-N2RVJN7T.js} +1 -1
  12. package/payload/server/public/activity.html +5 -5
  13. package/payload/server/public/agents.html +4 -4
  14. package/payload/server/public/assets/{AdminLoginScreens-C6e5jf0S.js → AdminLoginScreens-5gI137xF.js} +1 -1
  15. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.br +0 -0
  16. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.gz +0 -0
  17. package/payload/server/public/assets/{AdminShell-DStA0lfm.js → AdminShell-Bfapavlp.js} +2 -2
  18. package/payload/server/public/assets/AdminShell-Bfapavlp.js.br +0 -0
  19. package/payload/server/public/assets/AdminShell-Bfapavlp.js.gz +0 -0
  20. package/payload/server/public/assets/{activity--yyBcNfB.js → activity-DnbsmUgn.js} +1 -1
  21. package/payload/server/public/assets/activity-DnbsmUgn.js.br +0 -0
  22. package/payload/server/public/assets/activity-DnbsmUgn.js.gz +0 -0
  23. package/payload/server/public/assets/admin-BXIv0_oj.js +1 -0
  24. package/payload/server/public/assets/admin-BXIv0_oj.js.br +0 -0
  25. package/payload/server/public/assets/admin-BXIv0_oj.js.gz +0 -0
  26. package/payload/server/public/assets/{agents-odyVOb5E.js → agents-Dl2wOcpj.js} +1 -1
  27. package/payload/server/public/assets/agents-Dl2wOcpj.js.br +0 -0
  28. package/payload/server/public/assets/agents-Dl2wOcpj.js.gz +0 -0
  29. package/payload/server/public/assets/{browser-D1K_MoHt.js → browser-D-Uc9ewO.js} +1 -1
  30. package/payload/server/public/assets/browser-D-Uc9ewO.js.br +0 -0
  31. package/payload/server/public/assets/browser-D-Uc9ewO.js.gz +0 -0
  32. package/payload/server/public/assets/{calendar-jqh9U7Ic.js → calendar-Cu3O6Bay.js} +1 -1
  33. package/payload/server/public/assets/calendar-Cu3O6Bay.js.br +0 -0
  34. package/payload/server/public/assets/calendar-Cu3O6Bay.js.gz +0 -0
  35. package/payload/server/public/assets/chat-Bso94e-f.js +1 -0
  36. package/payload/server/public/assets/chat-Bso94e-f.js.br +0 -0
  37. package/payload/server/public/assets/chat-Bso94e-f.js.gz +0 -0
  38. package/payload/server/public/assets/chevron-left-CDwfR9V-.js +1 -0
  39. package/payload/server/public/assets/chevron-left-CDwfR9V-.js.br +0 -0
  40. package/payload/server/public/assets/clock-DDVrNLt9.js +1 -0
  41. package/payload/server/public/assets/clock-DDVrNLt9.js.br +0 -0
  42. package/payload/server/public/assets/clock-DDVrNLt9.js.gz +0 -0
  43. package/payload/server/public/assets/{copy-DkuHwkaB.js → copy-DPBVV8KZ.js} +1 -1
  44. package/payload/server/public/assets/copy-DPBVV8KZ.js.br +0 -0
  45. package/payload/server/public/assets/copy-DPBVV8KZ.js.gz +0 -0
  46. package/payload/server/public/assets/data-Dei8n2EX.js +1 -0
  47. package/payload/server/public/assets/data-Dei8n2EX.js.br +0 -0
  48. package/payload/server/public/assets/data-Dei8n2EX.js.gz +0 -0
  49. package/payload/server/public/assets/{file-text-C6o_NRjj.js → file-text-Dn82O6ke.js} +1 -1
  50. package/payload/server/public/assets/file-text-Dn82O6ke.js.br +0 -0
  51. package/payload/server/public/assets/file-text-Dn82O6ke.js.gz +0 -0
  52. package/payload/server/public/assets/{graph-BN30v5hJ.js → graph-D2J7a7Ll.js} +1 -1
  53. package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
  54. package/payload/server/public/assets/{graph-BN30v5hJ.js.gz → graph-D2J7a7Ll.js.gz} +0 -0
  55. package/payload/server/public/assets/{graph-labels-DHFyNXQt.js → graph-labels-D3gkY2IH.js} +1 -1
  56. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.br +0 -0
  57. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.gz +0 -0
  58. package/payload/server/public/assets/{operator-DwFYA4Fp.js → operator-Bz_yj1im.js} +1 -1
  59. package/payload/server/public/assets/operator-Bz_yj1im.js.br +0 -0
  60. package/payload/server/public/assets/operator-Bz_yj1im.js.gz +0 -0
  61. package/payload/server/public/assets/page-D6u2xYh7.js +1 -0
  62. package/payload/server/public/assets/page-D6u2xYh7.js.br +0 -0
  63. package/payload/server/public/assets/page-D6u2xYh7.js.gz +0 -0
  64. package/payload/server/public/assets/{page-CEWIP3VF.js → page-mH4nyQ75.js} +2 -2
  65. package/payload/server/public/assets/page-mH4nyQ75.js.br +0 -0
  66. package/payload/server/public/assets/page-mH4nyQ75.js.gz +0 -0
  67. package/payload/server/public/assets/play-ByuChQJJ.js +1 -0
  68. package/payload/server/public/assets/play-ByuChQJJ.js.br +0 -0
  69. package/payload/server/public/assets/play-ByuChQJJ.js.gz +0 -0
  70. package/payload/server/public/assets/{public-Ceb0n7Km.js → public-CtoYVjPR.js} +1 -1
  71. package/payload/server/public/assets/public-CtoYVjPR.js.br +0 -0
  72. package/payload/server/public/assets/public-CtoYVjPR.js.gz +0 -0
  73. package/payload/server/public/assets/{rotate-ccw-CDYdx91V.js → rotate-ccw-BXoIghE3.js} +1 -1
  74. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.br +0 -0
  75. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.gz +0 -0
  76. package/payload/server/public/assets/{routines-DwNTih0y.js → routines-DXleYpDv.js} +1 -1
  77. package/payload/server/public/assets/routines-DXleYpDv.js.br +0 -0
  78. package/payload/server/public/assets/routines-DXleYpDv.js.gz +0 -0
  79. package/payload/server/public/assets/{skills-TIDCdOLQ.js → skills-CMl38MKH.js} +1 -1
  80. package/payload/server/public/assets/skills-CMl38MKH.js.br +0 -0
  81. package/payload/server/public/assets/skills-CMl38MKH.js.gz +0 -0
  82. package/payload/server/public/assets/{tasks-eTXvaWnd.js → tasks-Cshaf0Md.js} +1 -1
  83. package/payload/server/public/assets/tasks-Cshaf0Md.js.br +0 -0
  84. package/payload/server/public/assets/tasks-Cshaf0Md.js.gz +0 -0
  85. package/payload/server/public/assets/{triangle-alert-JwLSrj17.js → triangle-alert-CS-NXr_9.js} +1 -1
  86. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.br +3 -0
  87. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.gz +0 -0
  88. package/payload/server/public/assets/{useCopyFeedback-DjTkNw2p.js → useCopyFeedback-pKLfQJNG.js} +1 -1
  89. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.br +0 -0
  90. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.gz +0 -0
  91. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css +1 -0
  92. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.br +0 -0
  93. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.gz +0 -0
  94. package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js → useMediaQuery-oDCasnxD.js} +1 -1
  95. package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
  96. package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js.gz → useMediaQuery-oDCasnxD.js.gz} +0 -0
  97. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js +2 -0
  98. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.br +0 -0
  99. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.gz +0 -0
  100. package/payload/server/public/assets/{wrench-DutUiRu7.js → wrench-czES9Rdy.js} +1 -1
  101. package/payload/server/public/assets/wrench-czES9Rdy.js.br +0 -0
  102. package/payload/server/public/assets/wrench-czES9Rdy.js.gz +0 -0
  103. package/payload/server/public/browser.html +4 -4
  104. package/payload/server/public/calendar.html +7 -7
  105. package/payload/server/public/chat.html +13 -13
  106. package/payload/server/public/data.html +11 -11
  107. package/payload/server/public/graph.html +9 -9
  108. package/payload/server/public/index.html +13 -13
  109. package/payload/server/public/operator.html +14 -14
  110. package/payload/server/public/public.html +13 -13
  111. package/payload/server/public/routines.html +7 -7
  112. package/payload/server/public/skills.html +5 -5
  113. package/payload/server/public/tasks.html +6 -6
  114. package/payload/server/server.js +2448 -1667
  115. package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.br +0 -0
  116. package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.gz +0 -0
  117. package/payload/server/public/assets/AdminShell-DStA0lfm.js.br +0 -0
  118. package/payload/server/public/assets/AdminShell-DStA0lfm.js.gz +0 -0
  119. package/payload/server/public/assets/activity--yyBcNfB.js.br +0 -0
  120. package/payload/server/public/assets/activity--yyBcNfB.js.gz +0 -0
  121. package/payload/server/public/assets/admin-Dr6Jwe7W.js +0 -1
  122. package/payload/server/public/assets/admin-Dr6Jwe7W.js.br +0 -0
  123. package/payload/server/public/assets/admin-Dr6Jwe7W.js.gz +0 -0
  124. package/payload/server/public/assets/agents-odyVOb5E.js.br +0 -0
  125. package/payload/server/public/assets/agents-odyVOb5E.js.gz +0 -0
  126. package/payload/server/public/assets/browser-D1K_MoHt.js.br +0 -0
  127. package/payload/server/public/assets/browser-D1K_MoHt.js.gz +0 -0
  128. package/payload/server/public/assets/calendar-jqh9U7Ic.js.br +0 -0
  129. package/payload/server/public/assets/calendar-jqh9U7Ic.js.gz +0 -0
  130. package/payload/server/public/assets/chat-QQHBdVor.js +0 -1
  131. package/payload/server/public/assets/chat-QQHBdVor.js.br +0 -0
  132. package/payload/server/public/assets/chat-QQHBdVor.js.gz +0 -0
  133. package/payload/server/public/assets/chevron-left-DwTXgKwr.js +0 -1
  134. package/payload/server/public/assets/clock-DOmh9iKX.js +0 -1
  135. package/payload/server/public/assets/clock-DOmh9iKX.js.br +0 -0
  136. package/payload/server/public/assets/copy-DkuHwkaB.js.br +0 -0
  137. package/payload/server/public/assets/copy-DkuHwkaB.js.gz +0 -0
  138. package/payload/server/public/assets/data-DYmTNqrb.js +0 -1
  139. package/payload/server/public/assets/data-DYmTNqrb.js.br +0 -0
  140. package/payload/server/public/assets/data-DYmTNqrb.js.gz +0 -0
  141. package/payload/server/public/assets/file-text-C6o_NRjj.js.br +0 -0
  142. package/payload/server/public/assets/file-text-C6o_NRjj.js.gz +0 -0
  143. package/payload/server/public/assets/graph-BN30v5hJ.js.br +0 -0
  144. package/payload/server/public/assets/graph-labels-DHFyNXQt.js.br +0 -0
  145. package/payload/server/public/assets/graph-labels-DHFyNXQt.js.gz +0 -0
  146. package/payload/server/public/assets/operator-DwFYA4Fp.js.br +0 -0
  147. package/payload/server/public/assets/operator-DwFYA4Fp.js.gz +0 -0
  148. package/payload/server/public/assets/page-BALVRssF.js +0 -1
  149. package/payload/server/public/assets/page-BALVRssF.js.br +0 -0
  150. package/payload/server/public/assets/page-BALVRssF.js.gz +0 -0
  151. package/payload/server/public/assets/page-CEWIP3VF.js.br +0 -0
  152. package/payload/server/public/assets/page-CEWIP3VF.js.gz +0 -0
  153. package/payload/server/public/assets/play-Bg-BY_oj.js +0 -1
  154. package/payload/server/public/assets/play-Bg-BY_oj.js.br +0 -0
  155. package/payload/server/public/assets/play-Bg-BY_oj.js.gz +0 -0
  156. package/payload/server/public/assets/public-Ceb0n7Km.js.br +0 -0
  157. package/payload/server/public/assets/public-Ceb0n7Km.js.gz +0 -0
  158. package/payload/server/public/assets/rotate-ccw-CDYdx91V.js.gz +0 -0
  159. package/payload/server/public/assets/routines-DwNTih0y.js.br +0 -0
  160. package/payload/server/public/assets/routines-DwNTih0y.js.gz +0 -0
  161. package/payload/server/public/assets/skills-TIDCdOLQ.js.br +0 -0
  162. package/payload/server/public/assets/skills-TIDCdOLQ.js.gz +0 -0
  163. package/payload/server/public/assets/tasks-eTXvaWnd.js.br +0 -0
  164. package/payload/server/public/assets/tasks-eTXvaWnd.js.gz +0 -0
  165. package/payload/server/public/assets/triangle-alert-JwLSrj17.js.br +0 -0
  166. package/payload/server/public/assets/triangle-alert-JwLSrj17.js.gz +0 -0
  167. package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.br +0 -0
  168. package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.gz +0 -0
  169. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css +0 -1
  170. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.br +0 -0
  171. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.gz +0 -0
  172. package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.br +0 -0
  173. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js +0 -2
  174. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.br +0 -0
  175. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.gz +0 -0
  176. package/payload/server/public/assets/wrench-DutUiRu7.js.br +0 -0
  177. package/payload/server/public/assets/wrench-DutUiRu7.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.563",
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:5b93e9b58f687c920da46bec1137f326c5c00d5f2b1d4a460a5f5248eaee0c15
4
+ content-hash: sha256:ad9b32a95086e27720356ba87f13727d7aa883d772dfa2539e67b33f70e6cd91
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2544,6 +2544,25 @@ Ask Maxy: "What messages has the bot received?" or "Show recent Telegram activit
2544
2544
 
2545
2545
  **Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
2546
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
+
2547
2566
  ---
2548
2567
  # Outlook
2549
2568
  Source: https://docs.getmaxy.com/outlook-guide.md
@@ -9,6 +9,12 @@ 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
+
12
18
  ## 2026-08-05 (0.1.563)
13
19
 
14
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.
@@ -1,6 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import {
3
+ MAX_NOTE_LENGTH,
3
4
  MAX_STATUS_LENGTH,
5
+ WORK_DATE_RE,
4
6
  processVisitStatus,
5
7
  } from '../../skills/data-portal/template/functions/api/visit-status'
6
8
  import type {
@@ -174,3 +176,119 @@ describe('the visit-status write gate', () => {
174
176
  expect(joined).not.toContain('granary')
175
177
  })
176
178
  })
179
+
180
+ describe('the working day a status edit may carry', () => {
181
+ const day = (extra: Record<string, unknown>) => ({ visitId: 'v1', status: 'done', ...extra })
182
+
183
+ it('inserts the day, the date and the note when the claim is well formed', async () => {
184
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
185
+ const h = await processVisitStatus(
186
+ day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'second fix' }),
187
+ 'sess',
188
+ env(database),
189
+ () => {},
190
+ 1,
191
+ )
192
+ expect(h.status).toBe(200)
193
+ expect(inserted[0]).toContain(100)
194
+ expect(inserted[0]).toContain('2026-08-05')
195
+ expect(inserted[0]).toContain('second fix')
196
+ })
197
+
198
+ it('defaults to no claim when the body carries no day', async () => {
199
+ // A status-only edit is the common case and must stay a one-field write.
200
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
201
+ const h = await processVisitStatus(day({}), 'sess', env(database), () => {}, 1)
202
+ expect(h.status).toBe(200)
203
+ expect(inserted[0]).toContain(0)
204
+ expect(inserted[0]).toContain('')
205
+ })
206
+
207
+ it('refuses a day outside 0..100 rather than writing it into the graph', async () => {
208
+ // A value above a full day is described by no shipped surface. Refused at
209
+ // the door, exactly as an over-long status is.
210
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
211
+ const e = env(database)
212
+ for (const d of [101, -1, 50.5, '100']) {
213
+ const h = await processVisitStatus(
214
+ day({ dayHundredths: d, workDate: '2026-08-05' }),
215
+ 'sess',
216
+ e,
217
+ () => {},
218
+ 1,
219
+ )
220
+ expect(h.status).toBe(400)
221
+ }
222
+ expect(inserted).toEqual([])
223
+ })
224
+
225
+ it('refuses a claim whose date is missing or malformed', async () => {
226
+ // The date becomes part of the cost key. A claim the roll-up cannot date is
227
+ // told to the person now, not left sitting in the queue.
228
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
229
+ const e = env(database)
230
+ for (const wd of [undefined, '', '5th Aug', '2026-8-5']) {
231
+ const h = await processVisitStatus(
232
+ day({ dayHundredths: 100, workDate: wd }),
233
+ 'sess',
234
+ e,
235
+ () => {},
236
+ 1,
237
+ )
238
+ expect(h.status).toBe(400)
239
+ }
240
+ expect(inserted).toEqual([])
241
+ })
242
+
243
+ it('accepts a date only in the shape the pull will key on', () => {
244
+ expect(WORK_DATE_RE.test('2026-08-05')).toBe(true)
245
+ expect(WORK_DATE_RE.test('2026-08-05T09:00:00Z')).toBe(false)
246
+ })
247
+
248
+ it('refuses a note longer than the cap rather than truncating it', async () => {
249
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
250
+ const h = await processVisitStatus(
251
+ day({
252
+ dayHundredths: 100,
253
+ workDate: '2026-08-05',
254
+ workNote: 'x'.repeat(MAX_NOTE_LENGTH + 1),
255
+ }),
256
+ 'sess',
257
+ env(database),
258
+ () => {},
259
+ 1,
260
+ )
261
+ expect(h.status).toBe(400)
262
+ expect(inserted).toEqual([])
263
+ })
264
+
265
+ it('logs whether the edit carried a day, and never the note', async () => {
266
+ const lines: string[] = []
267
+ const { database } = db(ROWS, person('alice', 'own'))
268
+ await processVisitStatus(
269
+ day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'granary court rewire' }),
270
+ 'sess',
271
+ env(database),
272
+ (l) => lines.push(l),
273
+ 1,
274
+ )
275
+ const joined = lines.join('\n')
276
+ expect(joined).toContain('labour=true')
277
+ expect(joined).not.toContain('granary')
278
+ expect(joined).not.toContain('2026-08-05')
279
+ })
280
+
281
+ it('still refuses another person visit when the edit carries a day', async () => {
282
+ // The gate is the SELECT and the new fields must not have moved it.
283
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
284
+ const h = await processVisitStatus(
285
+ { visitId: 'v2', status: 'done', dayHundredths: 100, workDate: '2026-08-05' },
286
+ 'sess',
287
+ env(database),
288
+ () => {},
289
+ 1,
290
+ )
291
+ expect(h.status).toBe(404)
292
+ expect(inserted).toEqual([])
293
+ })
294
+ })
@@ -147,6 +147,21 @@ ALTER TABLE people ADD COLUMN recordScope TEXT NOT NULL DEFAULT 'own';
147
147
  No backfill follows it. Everyone already enrolled lands on the narrow scope, which is the intended
148
148
  outcome, and widening one person is a re-run of enrolment for that person.
149
149
 
150
+ **A status edit may also carry a working day, and a store that predates those three columns needs
151
+ them added once too.** `dayHundredths` is integer hundredths of a day, which is what the timesheet
152
+ engine consumes: 100 is a full day, and 0 means the edit claims no labour. `workDate` is the day
153
+ worked, and the pull refuses a claim without one rather than dating it from when the sheet was filed.
154
+ `workNote` is what the worker did. The same `IF NOT EXISTS` rule applies, so run all three:
155
+
156
+ ```sql
157
+ ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
158
+ ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
159
+ ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
160
+ ```
161
+
162
+ Until they exist the endpoint's INSERT names columns the table lacks and fails loudly, which is the
163
+ intended outcome: a status edit silently losing its hours is the failure worth avoiding.
164
+
150
165
  **A passcode is never stored in plaintext** — not in `data-portal.json`, not in a log line, nowhere
151
166
  on the device. The operator conveys it to the person out of band. Rotating a passcode is a re-run of
152
167
  the script for that person; there is no separate editor.
@@ -309,14 +309,32 @@ CREATE TABLE IF NOT EXISTS visits_state (
309
309
  -- is counted by the audit. It is never written under another id and never
310
310
  -- dropped: a dropped edit is a promise broken to the person who made it, and it
311
311
  -- emits nothing at the time it happens.
312
+ -- `dayHundredths`, `workDate` and `workNote` are the working day the edit
313
+ -- carries (Task 2432). `dayHundredths` is integer hundredths of a day, which is
314
+ -- exactly what the timesheet engine consumes: 100 is a full day. 0 means this
315
+ -- edit carries no labour claim, the same absent-sentinel convention `ownerId`
316
+ -- uses above for unbound. The pull writes a labour :JobCost only when it is
317
+ -- above 0, and refuses a claim carrying no `workDate` rather than dating it
318
+ -- from anything else: `statusAt` would record the wrong day for a sheet filed
319
+ -- the next morning, and a visit's scheduled date is not evidence of when it was
320
+ -- attended.
321
+ --
322
+ -- IF NOT EXISTS adds none of the three to a table that predates them. Add them
323
+ -- by hand in that case, in this order:
324
+ -- ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
325
+ -- ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
326
+ -- ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
312
327
  CREATE TABLE IF NOT EXISTS visit_status (
313
- id INTEGER PRIMARY KEY AUTOINCREMENT,
314
- accountId TEXT NOT NULL,
315
- visitId TEXT NOT NULL,
316
- ownerId TEXT NOT NULL,
317
- status TEXT NOT NULL,
318
- statusAt TEXT NOT NULL,
319
- claimed INTEGER NOT NULL DEFAULT 0
328
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
329
+ accountId TEXT NOT NULL,
330
+ visitId TEXT NOT NULL,
331
+ ownerId TEXT NOT NULL,
332
+ status TEXT NOT NULL,
333
+ statusAt TEXT NOT NULL,
334
+ dayHundredths INTEGER NOT NULL DEFAULT 0,
335
+ workDate TEXT NOT NULL DEFAULT '',
336
+ workNote TEXT NOT NULL DEFAULT '',
337
+ claimed INTEGER NOT NULL DEFAULT 0
320
338
  );
321
339
 
322
340
  -- The pull selects unclaimed rows for one account every sixty seconds, so the
@@ -12,6 +12,27 @@ import { q } from './_lib/log'
12
12
  */
13
13
  export const MAX_STATUS_LENGTH = 64
14
14
 
15
+ /**
16
+ * The longest work note a person may attach to a day.
17
+ *
18
+ * The note reaches `:JobCost.description` through the device-side pull, so it
19
+ * is graph content arriving from the open web and gets the same treatment the
20
+ * status gets: refused at the door, never truncated. The size is chosen for a
21
+ * description cell on the A4 timesheet row, not derived from anything.
22
+ */
23
+ export const MAX_NOTE_LENGTH = 500
24
+
25
+ /**
26
+ * The only date shape a claim may carry.
27
+ *
28
+ * The date becomes part of the cost key the pull merges on, so a free-form
29
+ * string would produce a key nobody can reproduce. A COPY of this check lives
30
+ * in `platform/ui/server/portal-visits-pull.ts` as the invariant at the writer;
31
+ * the two cannot share a constant because this file ships to Cloudflare Pages
32
+ * and cannot import from the platform tree.
33
+ */
34
+ export const WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
35
+
15
36
  /**
16
37
  * Record a status a person set against one of their visits.
17
38
  *
@@ -56,10 +77,39 @@ export async function processVisitStatus(
56
77
  return { status: 400, payload: { ok: false, error: 'status too long' } }
57
78
  }
58
79
 
80
+ // The working day the edit carries, if any. Absent means a status-only edit,
81
+ // which is the common case and stays a zero-claim write.
82
+ const dayHundredths = b.dayHundredths === undefined ? 0 : b.dayHundredths
83
+ if (
84
+ typeof dayHundredths !== 'number' ||
85
+ !Number.isInteger(dayHundredths) ||
86
+ dayHundredths < 0 ||
87
+ dayHundredths > 100
88
+ ) {
89
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=bad-day`)
90
+ return { status: 400, payload: { ok: false, error: 'dayHundredths must be an integer 0..100' } }
91
+ }
92
+ const workDate = typeof b.workDate === 'string' ? b.workDate : ''
93
+ const workNote = typeof b.workNote === 'string' ? b.workNote : ''
94
+ // A claim the roll-up could not date is refused now rather than queued. The
95
+ // pull holds the same invariant, so a row reaching the table another way is
96
+ // still never written under a guessed date.
97
+ if (dayHundredths > 0 && !WORK_DATE_RE.test(workDate)) {
98
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=undated-claim`)
99
+ return {
100
+ status: 400,
101
+ payload: { ok: false, error: 'a day claim requires workDate as YYYY-MM-DD' },
102
+ }
103
+ }
104
+ if (workNote.length > MAX_NOTE_LENGTH) {
105
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=note-too-long`)
106
+ return { status: 400, payload: { ok: false, error: 'note too long' } }
107
+ }
108
+
59
109
  const scoped = session.recordScope === 'own'
60
110
  const sql =
61
- `INSERT INTO visit_status (accountId, ownerId, status, statusAt, claimed, visitId)
62
- SELECT ?, ?, ?, ?, 0, v.visitId
111
+ `INSERT INTO visit_status (accountId, ownerId, status, statusAt, dayHundredths, workDate, workNote, claimed, visitId)
112
+ SELECT ?, ?, ?, ?, ?, ?, ?, 0, v.visitId
63
113
  FROM visits v
64
114
  JOIN visits_state s
65
115
  ON s.accountId = v.accountId AND s.currentGeneration = v.generation
@@ -74,6 +124,9 @@ export async function processVisitStatus(
74
124
  session.ownerId,
75
125
  status,
76
126
  statusAt,
127
+ dayHundredths,
128
+ workDate,
129
+ workNote,
77
130
  env.PORTAL_ACCOUNT_ID,
78
131
  session.ownerId,
79
132
  visitId,
@@ -83,6 +136,9 @@ export async function processVisitStatus(
83
136
  session.ownerId,
84
137
  status,
85
138
  statusAt,
139
+ dayHundredths,
140
+ workDate,
141
+ workNote,
86
142
  env.PORTAL_ACCOUNT_ID,
87
143
  visitId,
88
144
  )
@@ -100,11 +156,12 @@ export async function processVisitStatus(
100
156
  return { status: 404, payload: { ok: false, error: 'not found' } }
101
157
  }
102
158
 
103
- // Counts, the scope and the outcome. Never the status text: it describes what
104
- // a real person was doing.
159
+ // Counts, the scope, whether a day was claimed, and the outcome. Never the
160
+ // status text, the date or the note: they describe what a real person was
161
+ // doing and when.
105
162
  log(
106
163
  `[data-portal] op=visit-status owner=${q(session.ownerId)} account=${q(session.accountId)} ` +
107
- `scope=${session.recordScope} queued=${changes} result=ok`,
164
+ `scope=${session.recordScope} labour=${dayHundredths > 0} queued=${changes} result=ok`,
108
165
  )
109
166
  return { status: 200, payload: { ok: true, queued: changes } }
110
167
  }
@@ -56,3 +56,22 @@ Ask {{productName}}: "What messages has the bot received?" or "Show recent Teleg
56
56
  **Can't send to a group:** The bot must be a member of the group. Add it via the group settings in Telegram, then try again.
57
57
 
58
58
  **Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
59
+
60
+ ## Sending from the dashboard
61
+
62
+ Open a Telegram conversation in the dashboard and you can type a reply straight
63
+ into it, the same way you already can with WhatsApp. The message goes out from
64
+ your bot.
65
+
66
+ Two things to know:
67
+
68
+ - **Direct messages only.** Group and supergroup chats show no message box. A
69
+ Telegram conversation is filed under the person who spoke, so a message sent
70
+ from a group thread would reach that one person privately rather than the
71
+ group — the box is hidden rather than doing something you did not intend.
72
+ - **History starts now.** Messages are only shown from the point this feature
73
+ was installed onward. A conversation that was already running looks empty
74
+ until the next message arrives, and then fills up from there.
75
+
76
+ Text only for now. Photos, files and voice notes are not available on Telegram
77
+ from the dashboard.
@@ -116,7 +116,7 @@ MERGE keys per label:
116
116
  - `:Contact` — `(accountId, contactId)` — write-time MERGE
117
117
  - `:PurchaseOrderLine` — `(accountId, poNumber, lineId)` — child of `:PurchaseOrder`
118
118
  - `:Note` — `(accountId, noteId)` — child of `:Job` via `(:Note)-[:ABOUT]->(:Job)`, append-only CREATE (a fresh `noteId` per note, not a content MERGE), so a second note never overwrites the first
119
- - `:Engineer` — `(accountId, personId)` — additional label on `:Person`, set via `memory-update`
119
+ - `:Engineer` — `(accountId, engineerId)` — additional label on `:Person`, set via `memory-update`
120
120
  - `:Supplier` — `(accountId, organizationId)` — additional label on `:Organization`
121
121
 
122
122
  ---
@@ -2080,6 +2080,8 @@ function classify(access) {
2080
2080
  }
2081
2081
  return access.agentType === "admin" ? "admin" : "public";
2082
2082
  }
2083
+
2084
+ // app/lib/channel-notify/frame.ts
2083
2085
  function formatNotifyBody(text, mediaType) {
2084
2086
  const source = text.length > 0 ? text : mediaType ?? "";
2085
2087
  if (source.length === 0) return { body: "", truncated: false };
@@ -4237,9 +4239,10 @@ function buildNotifyPending(args) {
4237
4239
  const { body, truncated } = formatNotifyBody(args.text, args.mediaType);
4238
4240
  const trimmed = args.pushName?.trim() ?? "";
4239
4241
  return {
4240
- waMessageId: args.msgKeyId,
4242
+ messageId: args.msgKeyId,
4241
4243
  accountId: args.persistAccountId,
4242
4244
  ledgerAccount: args.ledgerAccount,
4245
+ channel: "whatsapp",
4243
4246
  senderType: args.senderType,
4244
4247
  senderDisplay: trimmed.length > 0 ? trimmed : formatSenderPhone(args.senderPhone),
4245
4248
  body,
@@ -5009,7 +5012,7 @@ function monitorInbound(conn) {
5009
5012
  }
5010
5013
  if (notifyDecision.emit && msg.key.id) {
5011
5014
  console.error(
5012
- `[wa-notify] op=eligible msgId=${msg.key.id} account=${persistAccountId} senderType=${notifyDecision.senderType} reason=${dmAccess?.reason ?? "none"} passive=${dmAccess?.passive === true}` + (dmAccess?.party ? ` party=${dmAccess.party}` : "")
5015
+ `[channel-notify] op=eligible channel=whatsapp msgId=${msg.key.id} account=${persistAccountId} senderType=${notifyDecision.senderType} reason=${dmAccess?.reason ?? "none"} passive=${dmAccess?.passive === true}` + (dmAccess?.party ? ` party=${dmAccess.party}` : "")
5013
5016
  );
5014
5017
  if (onNotify) {
5015
5018
  try {
@@ -5028,13 +5031,13 @@ function monitorInbound(conn) {
5028
5031
  );
5029
5032
  } catch (err) {
5030
5033
  console.error(
5031
- `[wa-notify] op=emit-failed msgId=${msg.key.id} detail=${err instanceof Error ? err.message : String(err)}`
5034
+ `[channel-notify] op=emit-failed channel=whatsapp msgId=${msg.key.id} detail=${err instanceof Error ? err.message : String(err)}`
5032
5035
  );
5033
5036
  }
5034
5037
  }
5035
5038
  } else if (!notifyDecision.emit) {
5036
5039
  console.error(
5037
- `[wa-notify] op=skip msgId=${msg.key.id ?? "none"} reason=${notifyDecision.reason}`
5040
+ `[channel-notify] op=skip channel=whatsapp msgId=${msg.key.id ?? "none"} reason=${notifyDecision.reason}`
5038
5041
  );
5039
5042
  }
5040
5043
  }
@@ -5645,6 +5648,7 @@ export {
5645
5648
  isAdminPhone,
5646
5649
  buildPolicyInheritance,
5647
5650
  shouldDispatchInbound,
5651
+ formatNotifyBody,
5648
5652
  formatSenderPhone,
5649
5653
  checkAccessRateLimit,
5650
5654
  recordAccessFailedAttempt,
@@ -30,7 +30,7 @@ import {
30
30
  startConnection,
31
31
  stopConnection,
32
32
  storeMessage
33
- } from "./chunk-G6ULACHG.js";
33
+ } from "./chunk-3ABITJWB.js";
34
34
  import "./chunk-SKYZG6OT.js";
35
35
  import "./chunk-HYQNUVGO.js";
36
36
  export {
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Activity — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/activity--yyBcNfB.js"></script>
8
+ <script type="module" crossorigin src="/assets/activity-DnbsmUgn.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-CltuBf4Z.js">
10
- <link rel="modulepreload" crossorigin href="/assets/useMediaQuery-D8VDA0RN.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-DStA0lfm.js">
12
- <link rel="modulepreload" crossorigin href="/assets/triangle-alert-JwLSrj17.js">
13
- <link rel="stylesheet" crossorigin href="/assets/useMediaQuery-BsBzbWaf.css">
10
+ <link rel="modulepreload" crossorigin href="/assets/useMediaQuery-oDCasnxD.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-Bfapavlp.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/triangle-alert-CS-NXr_9.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/useMediaQuery-BHFn0-S9.css">
14
14
  <link rel="stylesheet" href="/brand-defaults.css">
15
15
  </head>
16
16
  <body>
@@ -5,11 +5,11 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Agents — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/agents-odyVOb5E.js"></script>
8
+ <script type="module" crossorigin src="/assets/agents-Dl2wOcpj.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-CltuBf4Z.js">
10
- <link rel="modulepreload" crossorigin href="/assets/useMediaQuery-D8VDA0RN.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-DStA0lfm.js">
12
- <link rel="stylesheet" crossorigin href="/assets/useMediaQuery-BsBzbWaf.css">
10
+ <link rel="modulepreload" crossorigin href="/assets/useMediaQuery-oDCasnxD.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/AdminShell-Bfapavlp.js">
12
+ <link rel="stylesheet" crossorigin href="/assets/useMediaQuery-BHFn0-S9.css">
13
13
  <link rel="stylesheet" href="/brand-defaults.css">
14
14
  </head>
15
15
  <body>
@@ -1 +1 @@
1
- import{o as e}from"./chunk-CltuBf4Z.js";import{O as t,Q as n,ct as r,et as i,it as a,nt as o}from"./useMediaQuery-D8VDA0RN.js";import{A as s}from"./useVoiceRecorder-B8vxObZG.js";import{A as c,O as l,k as u,p as d}from"./AdminShell-DStA0lfm.js";var f=e(r(),1);function p(e=`admin`){let[t,n]=(0,f.useState)(`loading`),[r,i]=(0,f.useState)(``),[o,l]=(0,f.useState)(``),[u,d]=(0,f.useState)(``),[p,m]=(0,f.useState)(!1),[h,g]=(0,f.useState)(!1),[_,v]=(0,f.useState)(!1),[y,b]=(0,f.useState)(!1),[x,S]=(0,f.useState)(!1),[C,w]=(0,f.useState)(null),[T,E]=(0,f.useState)(null),[D,O]=(0,f.useState)(void 0),[k,A]=(0,f.useState)(null),[j,M]=(0,f.useState)(void 0),[N,P]=(0,f.useState)(null),[F,I]=(0,f.useState)(null),[L,R]=(0,f.useState)(void 0),z=(0,f.useRef)(void 0),[B,V]=(0,f.useState)(!1),[H,U]=(0,f.useState)(!0);(0,f.useEffect)(()=>{typeof window>`u`||fetch(`/api/remote-auth/status`).then(e=>e.ok?e.json():null).then(e=>{e?.configured&&V(!0)}).catch(()=>{})},[]);let W=(0,f.useRef)(null),G=(0,f.useRef)(null);(0,f.useEffect)(()=>{async function e(){let e=null;try{e=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!e)return!1;try{let t=await fetch(`/api/admin/session?session_key=${encodeURIComponent(e)}`);if(t.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return!1}if(!t.ok)return!1;let r=await t.json();w(r.session_key),I(r.sessionId??null),O(r.businessName),A(r.role??null),M(r.userName===void 0?null:r.userName),P(r.avatar??null);let i=s(r.thinkingView);return z.current=i,R(i),n(`chat`),!0}catch(e){return console.error(`[admin] session restore failed:`,e),!1}}async function t(r=2){try{let i=await fetch(`/api/health`);if(!i.ok){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check returned ${i.status} after retries`),n(`set-pin`);return}let a=await i.json();if(!a.pin_configured){n(`set-pin`);return}U(!!a.claude_authenticated);let o=!1;try{o=new URLSearchParams(window.location.search).get(`connect`)===`claude`}catch{}if(o){try{let e=new URL(window.location.href);e.searchParams.delete(`connect`),window.history.replaceState(null,``,e.pathname+e.search+e.hash)}catch{}if(!a.claude_authenticated){n(`connect-claude`);return}}if(await e())return;n(`enter-pin`)}catch(e){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check failed:`,e),n(`set-pin`)}}t()},[]),(0,f.useEffect)(()=>{t===`chat`&&C&&fetch(`/api/admin/claude-info?session_key=${encodeURIComponent(C)}`).then(e=>{if(e.ok)return e.json()}).then(e=>{e&&E(e)}).catch(()=>{})},[t,C]);let K=(0,f.useRef)(null);(0,f.useEffect)(()=>{if(t!==`chat`)return;let e=setInterval(async()=>{try{let e=await fetch(`/api/health`);if(e.ok){let t=await e.json();U(t.auth_status!==`dead`&&t.auth_status!==`missing`)}}catch{}if(C)try{let e=await fetch(`/api/admin/session?session_key=${encodeURIComponent(C)}`);if(e.status!==401)return;let t=(await e.clone().json().catch(()=>null))?.code??`unknown-401`;if(t===`remote-auth-required`){a(`heartbeat`,`/api/admin/session`);return}console.warn(`[admin-auth] outcome=heartbeat-detected-expiry code=${t}`),K.current?.()}catch{}},300*1e3);return()=>clearInterval(e)},[t,C]),(0,f.useEffect)(()=>{t===`connect-claude`&&fetch(`/api/health`).then(e=>e.ok?e.json():null).then(e=>{e?.claude_authenticated&&(U(!0),n(`enter-pin`))}).catch(()=>{})},[t]);async function q(e,t){g(!0);try{let r=await fetch(`/api/admin/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:e,...t?{accountId:t}:{}})});if(!r.ok){let e=await r.json().catch(()=>({}));if(r.status===401&&e?.code===`remote-auth-required`){a(`enter-pin`,`/api/admin/session`);return}d(e.error||`Invalid PIN`);return}let o=await r.json();w(o.session_key),I(o.sessionId??null),O(o.businessName),A(o.role??null),M(o.userName===void 0?null:o.userName),P(o.avatar??null);let c=s(o.thinkingView);z.current=c,R(c);try{sessionStorage.setItem(`maxy-admin-session-key`,o.session_key)}catch{}i(``),n(`chat`)}catch(e){console.error(`[admin] connection error:`,e),d(`Could not connect.`)}finally{g(!1)}}let J=(0,f.useCallback)(async e=>{if(e.preventDefault(),h)return;d(``);let t=o.trim();if(!t){d(`Please enter your name.`);return}if(r.length<4){d(`PIN must be at least 4 characters.`);return}let s=r;g(!0);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:s,name:t})});if(!e.ok){let t=await e.json().catch(()=>({}));if(e.status===401&&t?.code===`remote-auth-required`){a(`set-pin`,`/api/onboarding/set-pin`);return}if(e.status===409){try{let e=await fetch(`/api/health`);if(e.ok){let r=await e.json();r.pin_configured&&r.claude_authenticated?n(`enter-pin`):r.pin_configured?n(`connect-claude`):d(t.error||`Failed to set PIN.`)}else n(`enter-pin`)}catch{n(`enter-pin`)}return}d(t.error||`Failed to set PIN.`);return}let r=await fetch(`/api/health`);if((r.ok?await r.json():null)?.claude_authenticated){await q(s);return}i(``),n(`connect-claude`)}catch(e){console.error(`[admin] connection error:`,e),d(`Could not connect.`)}finally{g(!1)}},[r,h,o]),Y=(0,f.useCallback)(async e=>{e.preventDefault(),d(``),await q(r)},[r]),X=(0,f.useCallback)(async()=>{S(!0);try{if(!await c())return console.warn(`[admin-ui] claude-disconnect not verified — credentials may persist; staying put`),!1;U(!1),w(null),A(null),M(void 0),P(null);try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return n(`connect-claude`),!0}finally{S(!1)}},[]),Z=(0,f.useCallback)(()=>{w(null),A(null),M(void 0),P(null);try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}i(``),d(``),n(`enter-pin`)},[]);return(0,f.useEffect)(()=>{K.current=Z},[Z]),{appState:t,setAppState:n,pin:r,setPin:i,operatorName:o,setOperatorName:l,pinError:u,setPinError:d,showPin:p,setShowPin:m,pinLoading:h,authPolling:_,setAuthPolling:v,authLoading:y,setAuthLoading:b,disconnecting:x,cacheKey:C,setCacheKey:w,claudeInfo:T,setClaudeInfo:E,businessName:D,role:k,userName:j,userAvatar:N,sessionId:F,setSessionId:I,expandAll:L,setExpandAll:R,expandAllDefaultRef:z,remoteAuthEnabled:B,claudeConnected:H,pinInputRef:W,setPinFormRef:G,handleSetPin:J,handleLogin:Y,handleDisconnect:X,handleLogout:Z}}var m=o();function h({inputRef:e,value:t,onChange:n,onComplete:r,showPin:i,autoFocus:a}){let o=(0,f.useRef)([]);function s(e,r){r.key===`Backspace`?(r.preventDefault(),t[e]?n(t.slice(0,e)+t.slice(e+1)):e>0&&(n(t.slice(0,e-1)+t.slice(e)),o.current[e-1]?.focus())):r.key===`ArrowLeft`&&e>0?o.current[e-1]?.focus():r.key===`ArrowRight`&&e<5?o.current[e+1]?.focus():r.key===`Enter`&&(r.preventDefault(),r.currentTarget.form?.requestSubmit())}function c(e,i){let a=i.nativeEvent.data;if(!a||!/^\d$/.test(a))return;let s=t.split(``);for(s[e]=a;s.length<e;)s.push(``);let c=s.join(``).replace(/\D/g,``).slice(0,6);n(c),c.length===6?r?.(c):e<5&&o.current[e+1]?.focus()}function l(e){e.preventDefault();let t=e.clipboardData.getData(`text`).replace(/\D/g,``).slice(0,6);t&&(n(t),t.length===6?r?.(t):o.current[t.length]?.focus())}return(0,m.jsx)(`div`,{className:`pin-field`,children:Array.from({length:6}).map((n,r)=>(0,m.jsx)(`input`,{ref:t=>{o.current[r]=t,r===0&&e&&(e.current=t)},type:`text`,inputMode:`numeric`,className:`pin-box${t[r]?` pin-box-filled`:``}`,value:t[r]?i?t[r]:`•`:``,onKeyDown:e=>s(r,e),onInput:e=>c(r,e),onPaste:l,onFocus:e=>e.target.select(),autoFocus:a&&r===0,autoComplete:`off`,maxLength:1,"aria-label":`PIN digit ${r+1}`},r))})}function g(e){let{pin:t,setPin:r,showPin:a,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,setPinFormRef:f,onSubmit:p,operatorName:g,setOperatorName:_}=e;return(0,m.jsx)(`div`,{className:`connect-page`,children:(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsxs)(`h1`,{className:`connect-title`,children:[`Welcome to `,n.productName]}),(0,m.jsxs)(`p`,{className:`connect-subtitle`,children:[`Tell `,n.productName,` who you are, then choose a PIN.`]}),(0,m.jsxs)(`form`,{ref:f,onSubmit:p,className:`connect-pin-form`,children:[(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(`input`,{type:`text`,className:`connect-name-input`,placeholder:`Your full name`,value:g,onChange:e=>_(e.target.value),autoComplete:`name`,autoFocus:!0,required:!0,"aria-label":`Your full name`}),(0,m.jsx)(`div`,{style:{width:38,flexShrink:0},"aria-hidden":`true`})]}),(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(h,{inputRef:d,value:t,onChange:r,onComplete:()=>{},showPin:a}),(0,m.jsx)(l,{variant:`send`,type:`submit`,disabled:!t||!g.trim(),loading:s,"aria-label":`Set PIN`,children:(0,m.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,m.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,m.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,m.jsx)(u,{checked:a,onChange:()=>o(e=>!e),label:`Show PIN`})]}),c&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:c})]})})}function _(e){let{pin:t,setPin:r,showPin:a,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,onSubmit:f,remoteAuthEnabled:p,onSignOutRemote:g}=e;return(0,m.jsxs)(`div`,{className:`connect-page`,children:[p&&g&&(0,m.jsx)(`button`,{type:`button`,className:`connect-signout`,onClick:g,children:`Sign out`}),(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsx)(`h1`,{className:`connect-title`,children:n.productName}),(0,m.jsxs)(`form`,{onSubmit:f,className:`connect-pin-form`,children:[(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(h,{inputRef:d,value:t,onChange:r,onComplete:()=>{},showPin:a,autoFocus:!0}),(0,m.jsx)(l,{variant:`send`,type:`submit`,disabled:!t,loading:s,children:(0,m.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,m.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,m.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,m.jsx)(`div`,{className:`pin-options`,children:(0,m.jsx)(u,{checked:a,onChange:()=>o(e=>!e),label:`Show PIN`})})]}),c&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:c})]})]})}function v(e){let{authPolling:t,setAuthPolling:r,authLoading:a,setAuthLoading:o,pinError:s,setPinError:c,setAppState:u}=e,[p,h]=(0,f.useState)(!1),[g,_]=(0,f.useState)(!1),[v,y]=(0,f.useState)(``),[b,x]=(0,f.useState)(``),[S,C]=(0,f.useState)(``),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1);(0,f.useEffect)(()=>{try{D(sessionStorage.getItem(`maxy-admin-session-key`))}catch{}k(!0)},[]);async function A(){C(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`paste-code`,code:b})})).json();e.accepted?(C(`Code submitted. Finishing sign-in…`),x(``)):C(e.error||`Could not submit code.`)}catch(e){console.error(`[admin] paste-code error:`,e),C(`Could not submit code.`)}}async function j(){_(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`launch-browser`})})).json();e.launched?h(!0):e.error&&c(e.error)}catch(e){console.error(`[admin] browser launch error:`,e),c(`Could not launch browser.`)}_(!1)}async function M(){o(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`})).json();if(e.started){r(!0),h(!0),o(!1),y(e.transport||``);for(let e=0;e<120;e++){await new Promise(e=>setTimeout(e,2e3));let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`wait`})})).json();if(typeof e.authTargetId==`string`&&T(e.authTargetId),e.authenticated){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),u(`enter-pin`);return}}c(`Timed out waiting for sign-in. Try again.`),r(!1)}else e.error&&c(e.error)}catch(e){console.error(`[admin] auth flow error:`,e),c(`Could not start auth flow.`)}o(!1)}async function N(){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),r(!1),c(``)}return t||p?(0,m.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,height:`100dvh`,overflow:`auto`},children:[(0,m.jsxs)(`header`,{className:`chat-header`,style:{paddingBottom:`12px`,flexShrink:0,position:`relative`,maxWidth:`680px`,width:`100%`,margin:`0 auto`,padding:`24px 20px 12px`},children:[t?(0,m.jsx)(`button`,{onClick:N,style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Cancel`,children:`✕`}):(0,m.jsx)(`button`,{onClick:()=>h(!1),style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Close browser`,children:`✕`}),(0,m.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`chat-logo`}),(0,m.jsx)(`h1`,{className:`chat-tagline`,children:`Connect Claude`}),(0,m.jsx)(`p`,{className:`chat-intro`,children:t?`Sign in and authorize in the browser below.`:`Open your email or prepare your accounts, then sign in.`}),!t&&(0,m.jsx)(`div`,{style:{marginTop:`12px`},children:(0,m.jsx)(l,{variant:`primary`,onClick:M,disabled:a,children:a?(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`})}),t&&v===`native`&&(0,m.jsxs)(`div`,{style:{marginTop:`14px`,maxWidth:`420px`,margin:`14px auto 0`,display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,m.jsx)(`label`,{style:{fontSize:`12px`,color:`#999`,lineHeight:1.4},children:`If the browser didn't finish sign-in, paste the code or full redirect URL from Claude's page:`}),(0,m.jsxs)(`div`,{style:{display:`flex`,gap:`8px`},children:[(0,m.jsx)(`input`,{type:`text`,value:b,onChange:e=>x(e.target.value),placeholder:`Paste code or redirect URL`,style:{flex:1,padding:`8px 10px`,fontSize:`13px`,border:`1px solid #ccc`,borderRadius:`6px`}}),(0,m.jsx)(l,{variant:`primary`,onClick:A,disabled:!b.trim(),children:`Submit`})]}),S&&(0,m.jsx)(`p`,{style:{fontSize:`12px`,color:`#999`,margin:0},children:S})]})]}),(0,m.jsx)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0,gap:`10px`,padding:`0 0 16px`},children:O&&(0,m.jsx)(d,{preferTargetId:w,cacheKey:E,chrome:!1})}),s&&(0,m.jsx)(`p`,{className:`admin-pin-error`,style:{textAlign:`center`,padding:`0 20px 16px`},children:s})]}):(0,m.jsx)(`div`,{className:`connect-page`,children:(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsxs)(`div`,{className:`connect-logos`,children:[(0,m.jsx)(`div`,{className:`connect-logo-wrap`,children:(0,m.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`connect-logo`})}),(0,m.jsx)(`svg`,{className:`connect-arrow`,viewBox:`0 0 48 24`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:(0,m.jsx)(`path`,{d:`M0 12h44m0 0l-8-8m8 8l-8 8`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})}),(0,m.jsxs)(`div`,{className:`connect-logo-wrap`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsx)(`span`,{className:`connect-logo-label`,children:n.productName})]})]}),(0,m.jsxs)(`h1`,{className:`connect-title`,children:[`Connect Claude to power `,n.productName]}),(0,m.jsx)(`p`,{className:`connect-subtitle`,children:`Sign in with your Anthropic account to get started.`}),(0,m.jsx)(l,{variant:`primary`,onClick:M,disabled:a,children:a?(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`}),(0,m.jsx)(`p`,{style:{marginTop:`6px`,fontSize:`11px`,color:`#999`,maxWidth:`300px`,textAlign:`center`,lineHeight:`1.4`},children:`First time? You may need to sign into your email and Anthropic account in the browser before connecting.`}),(0,m.jsx)(`button`,{onClick:j,disabled:g,style:{marginTop:`12px`,background:`none`,border:`none`,color:`var(--color-primary, #666)`,fontSize:`13px`,cursor:`pointer`,textDecoration:`underline`,textUnderlineOffset:`3px`},children:g?`Launching…`:`Open browser first`}),s&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:s})]})})}function y({auth:e}){return e.appState===`loading`?(0,m.jsx)(t,{surface:`gate`}):e.appState===`set-pin`?(0,m.jsx)(g,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,setPinFormRef:e.setPinFormRef,onSubmit:e.handleSetPin,operatorName:e.operatorName,setOperatorName:e.setOperatorName}):e.appState===`connect-claude`?(0,m.jsx)(v,{authPolling:e.authPolling,setAuthPolling:e.setAuthPolling,authLoading:e.authLoading,setAuthLoading:e.setAuthLoading,pinError:e.pinError,setPinError:e.setPinError,setAppState:e.setAppState}):e.appState===`enter-pin`?(0,m.jsx)(_,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,onSubmit:e.handleLogin,remoteAuthEnabled:e.remoteAuthEnabled,onSignOutRemote:()=>{window.location.href=`/__remote-auth/logout`}}):null}export{p as n,y as t};
1
+ import{o as e}from"./chunk-CltuBf4Z.js";import{O as t,Q as n,ct as r,et as i,it as a,nt as o}from"./useMediaQuery-oDCasnxD.js";import{A as s}from"./useVoiceRecorder-EgO1GmIg.js";import{A as c,O as l,k as u,p as d}from"./AdminShell-Bfapavlp.js";var f=e(r(),1);function p(e=`admin`){let[t,n]=(0,f.useState)(`loading`),[r,i]=(0,f.useState)(``),[o,l]=(0,f.useState)(``),[u,d]=(0,f.useState)(``),[p,m]=(0,f.useState)(!1),[h,g]=(0,f.useState)(!1),[_,v]=(0,f.useState)(!1),[y,b]=(0,f.useState)(!1),[x,S]=(0,f.useState)(!1),[C,w]=(0,f.useState)(null),[T,E]=(0,f.useState)(null),[D,O]=(0,f.useState)(void 0),[k,A]=(0,f.useState)(null),[j,M]=(0,f.useState)(void 0),[N,P]=(0,f.useState)(null),[F,I]=(0,f.useState)(null),[L,R]=(0,f.useState)(void 0),z=(0,f.useRef)(void 0),[B,V]=(0,f.useState)(!1),[H,U]=(0,f.useState)(!0);(0,f.useEffect)(()=>{typeof window>`u`||fetch(`/api/remote-auth/status`).then(e=>e.ok?e.json():null).then(e=>{e?.configured&&V(!0)}).catch(()=>{})},[]);let W=(0,f.useRef)(null),G=(0,f.useRef)(null);(0,f.useEffect)(()=>{async function e(){let e=null;try{e=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!e)return!1;try{let t=await fetch(`/api/admin/session?session_key=${encodeURIComponent(e)}`);if(t.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return!1}if(!t.ok)return!1;let r=await t.json();w(r.session_key),I(r.sessionId??null),O(r.businessName),A(r.role??null),M(r.userName===void 0?null:r.userName),P(r.avatar??null);let i=s(r.thinkingView);return z.current=i,R(i),n(`chat`),!0}catch(e){return console.error(`[admin] session restore failed:`,e),!1}}async function t(r=2){try{let i=await fetch(`/api/health`);if(!i.ok){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check returned ${i.status} after retries`),n(`set-pin`);return}let a=await i.json();if(!a.pin_configured){n(`set-pin`);return}U(!!a.claude_authenticated);let o=!1;try{o=new URLSearchParams(window.location.search).get(`connect`)===`claude`}catch{}if(o){try{let e=new URL(window.location.href);e.searchParams.delete(`connect`),window.history.replaceState(null,``,e.pathname+e.search+e.hash)}catch{}if(!a.claude_authenticated){n(`connect-claude`);return}}if(await e())return;n(`enter-pin`)}catch(e){if(r>0)return await new Promise(e=>setTimeout(e,1500)),t(r-1);console.error(`[admin] health check failed:`,e),n(`set-pin`)}}t()},[]),(0,f.useEffect)(()=>{t===`chat`&&C&&fetch(`/api/admin/claude-info?session_key=${encodeURIComponent(C)}`).then(e=>{if(e.ok)return e.json()}).then(e=>{e&&E(e)}).catch(()=>{})},[t,C]);let K=(0,f.useRef)(null);(0,f.useEffect)(()=>{if(t!==`chat`)return;let e=setInterval(async()=>{try{let e=await fetch(`/api/health`);if(e.ok){let t=await e.json();U(t.auth_status!==`dead`&&t.auth_status!==`missing`)}}catch{}if(C)try{let e=await fetch(`/api/admin/session?session_key=${encodeURIComponent(C)}`);if(e.status!==401)return;let t=(await e.clone().json().catch(()=>null))?.code??`unknown-401`;if(t===`remote-auth-required`){a(`heartbeat`,`/api/admin/session`);return}console.warn(`[admin-auth] outcome=heartbeat-detected-expiry code=${t}`),K.current?.()}catch{}},300*1e3);return()=>clearInterval(e)},[t,C]),(0,f.useEffect)(()=>{t===`connect-claude`&&fetch(`/api/health`).then(e=>e.ok?e.json():null).then(e=>{e?.claude_authenticated&&(U(!0),n(`enter-pin`))}).catch(()=>{})},[t]);async function q(e,t){g(!0);try{let r=await fetch(`/api/admin/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:e,...t?{accountId:t}:{}})});if(!r.ok){let e=await r.json().catch(()=>({}));if(r.status===401&&e?.code===`remote-auth-required`){a(`enter-pin`,`/api/admin/session`);return}d(e.error||`Invalid PIN`);return}let o=await r.json();w(o.session_key),I(o.sessionId??null),O(o.businessName),A(o.role??null),M(o.userName===void 0?null:o.userName),P(o.avatar??null);let c=s(o.thinkingView);z.current=c,R(c);try{sessionStorage.setItem(`maxy-admin-session-key`,o.session_key)}catch{}i(``),n(`chat`)}catch(e){console.error(`[admin] connection error:`,e),d(`Could not connect.`)}finally{g(!1)}}let J=(0,f.useCallback)(async e=>{if(e.preventDefault(),h)return;d(``);let t=o.trim();if(!t){d(`Please enter your name.`);return}if(r.length<4){d(`PIN must be at least 4 characters.`);return}let s=r;g(!0);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:s,name:t})});if(!e.ok){let t=await e.json().catch(()=>({}));if(e.status===401&&t?.code===`remote-auth-required`){a(`set-pin`,`/api/onboarding/set-pin`);return}if(e.status===409){try{let e=await fetch(`/api/health`);if(e.ok){let r=await e.json();r.pin_configured&&r.claude_authenticated?n(`enter-pin`):r.pin_configured?n(`connect-claude`):d(t.error||`Failed to set PIN.`)}else n(`enter-pin`)}catch{n(`enter-pin`)}return}d(t.error||`Failed to set PIN.`);return}let r=await fetch(`/api/health`);if((r.ok?await r.json():null)?.claude_authenticated){await q(s);return}i(``),n(`connect-claude`)}catch(e){console.error(`[admin] connection error:`,e),d(`Could not connect.`)}finally{g(!1)}},[r,h,o]),Y=(0,f.useCallback)(async e=>{e.preventDefault(),d(``),await q(r)},[r]),X=(0,f.useCallback)(async()=>{S(!0);try{if(!await c())return console.warn(`[admin-ui] claude-disconnect not verified — credentials may persist; staying put`),!1;U(!1),w(null),A(null),M(void 0),P(null);try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return n(`connect-claude`),!0}finally{S(!1)}},[]),Z=(0,f.useCallback)(()=>{w(null),A(null),M(void 0),P(null);try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}i(``),d(``),n(`enter-pin`)},[]);return(0,f.useEffect)(()=>{K.current=Z},[Z]),{appState:t,setAppState:n,pin:r,setPin:i,operatorName:o,setOperatorName:l,pinError:u,setPinError:d,showPin:p,setShowPin:m,pinLoading:h,authPolling:_,setAuthPolling:v,authLoading:y,setAuthLoading:b,disconnecting:x,cacheKey:C,setCacheKey:w,claudeInfo:T,setClaudeInfo:E,businessName:D,role:k,userName:j,userAvatar:N,sessionId:F,setSessionId:I,expandAll:L,setExpandAll:R,expandAllDefaultRef:z,remoteAuthEnabled:B,claudeConnected:H,pinInputRef:W,setPinFormRef:G,handleSetPin:J,handleLogin:Y,handleDisconnect:X,handleLogout:Z}}var m=o();function h({inputRef:e,value:t,onChange:n,onComplete:r,showPin:i,autoFocus:a}){let o=(0,f.useRef)([]);function s(e,r){r.key===`Backspace`?(r.preventDefault(),t[e]?n(t.slice(0,e)+t.slice(e+1)):e>0&&(n(t.slice(0,e-1)+t.slice(e)),o.current[e-1]?.focus())):r.key===`ArrowLeft`&&e>0?o.current[e-1]?.focus():r.key===`ArrowRight`&&e<5?o.current[e+1]?.focus():r.key===`Enter`&&(r.preventDefault(),r.currentTarget.form?.requestSubmit())}function c(e,i){let a=i.nativeEvent.data;if(!a||!/^\d$/.test(a))return;let s=t.split(``);for(s[e]=a;s.length<e;)s.push(``);let c=s.join(``).replace(/\D/g,``).slice(0,6);n(c),c.length===6?r?.(c):e<5&&o.current[e+1]?.focus()}function l(e){e.preventDefault();let t=e.clipboardData.getData(`text`).replace(/\D/g,``).slice(0,6);t&&(n(t),t.length===6?r?.(t):o.current[t.length]?.focus())}return(0,m.jsx)(`div`,{className:`pin-field`,children:Array.from({length:6}).map((n,r)=>(0,m.jsx)(`input`,{ref:t=>{o.current[r]=t,r===0&&e&&(e.current=t)},type:`text`,inputMode:`numeric`,className:`pin-box${t[r]?` pin-box-filled`:``}`,value:t[r]?i?t[r]:`•`:``,onKeyDown:e=>s(r,e),onInput:e=>c(r,e),onPaste:l,onFocus:e=>e.target.select(),autoFocus:a&&r===0,autoComplete:`off`,maxLength:1,"aria-label":`PIN digit ${r+1}`},r))})}function g(e){let{pin:t,setPin:r,showPin:a,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,setPinFormRef:f,onSubmit:p,operatorName:g,setOperatorName:_}=e;return(0,m.jsx)(`div`,{className:`connect-page`,children:(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsxs)(`h1`,{className:`connect-title`,children:[`Welcome to `,n.productName]}),(0,m.jsxs)(`p`,{className:`connect-subtitle`,children:[`Tell `,n.productName,` who you are, then choose a PIN.`]}),(0,m.jsxs)(`form`,{ref:f,onSubmit:p,className:`connect-pin-form`,children:[(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(`input`,{type:`text`,className:`connect-name-input`,placeholder:`Your full name`,value:g,onChange:e=>_(e.target.value),autoComplete:`name`,autoFocus:!0,required:!0,"aria-label":`Your full name`}),(0,m.jsx)(`div`,{style:{width:38,flexShrink:0},"aria-hidden":`true`})]}),(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(h,{inputRef:d,value:t,onChange:r,onComplete:()=>{},showPin:a}),(0,m.jsx)(l,{variant:`send`,type:`submit`,disabled:!t||!g.trim(),loading:s,"aria-label":`Set PIN`,children:(0,m.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,m.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,m.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,m.jsx)(u,{checked:a,onChange:()=>o(e=>!e),label:`Show PIN`})]}),c&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:c})]})})}function _(e){let{pin:t,setPin:r,showPin:a,setShowPin:o,pinLoading:s,pinError:c,pinInputRef:d,onSubmit:f,remoteAuthEnabled:p,onSignOutRemote:g}=e;return(0,m.jsxs)(`div`,{className:`connect-page`,children:[p&&g&&(0,m.jsx)(`button`,{type:`button`,className:`connect-signout`,onClick:g,children:`Sign out`}),(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsx)(`h1`,{className:`connect-title`,children:n.productName}),(0,m.jsxs)(`form`,{onSubmit:f,className:`connect-pin-form`,children:[(0,m.jsxs)(`div`,{className:`pin-input-row`,children:[(0,m.jsx)(h,{inputRef:d,value:t,onChange:r,onComplete:()=>{},showPin:a,autoFocus:!0}),(0,m.jsx)(l,{variant:`send`,type:`submit`,disabled:!t,loading:s,children:(0,m.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,m.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,m.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,m.jsx)(`div`,{className:`pin-options`,children:(0,m.jsx)(u,{checked:a,onChange:()=>o(e=>!e),label:`Show PIN`})})]}),c&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:c})]})]})}function v(e){let{authPolling:t,setAuthPolling:r,authLoading:a,setAuthLoading:o,pinError:s,setPinError:c,setAppState:u}=e,[p,h]=(0,f.useState)(!1),[g,_]=(0,f.useState)(!1),[v,y]=(0,f.useState)(``),[b,x]=(0,f.useState)(``),[S,C]=(0,f.useState)(``),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1);(0,f.useEffect)(()=>{try{D(sessionStorage.getItem(`maxy-admin-session-key`))}catch{}k(!0)},[]);async function A(){C(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`paste-code`,code:b})})).json();e.accepted?(C(`Code submitted. Finishing sign-in…`),x(``)):C(e.error||`Could not submit code.`)}catch(e){console.error(`[admin] paste-code error:`,e),C(`Could not submit code.`)}}async function j(){_(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`launch-browser`})})).json();e.launched?h(!0):e.error&&c(e.error)}catch(e){console.error(`[admin] browser launch error:`,e),c(`Could not launch browser.`)}_(!1)}async function M(){o(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`})).json();if(e.started){r(!0),h(!0),o(!1),y(e.transport||``);for(let e=0;e<120;e++){await new Promise(e=>setTimeout(e,2e3));let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`wait`})})).json();if(typeof e.authTargetId==`string`&&T(e.authTargetId),e.authenticated){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),u(`enter-pin`);return}}c(`Timed out waiting for sign-in. Try again.`),r(!1)}else e.error&&c(e.error)}catch(e){console.error(`[admin] auth flow error:`,e),c(`Could not start auth flow.`)}o(!1)}async function N(){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),r(!1),c(``)}return t||p?(0,m.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,height:`100dvh`,overflow:`auto`},children:[(0,m.jsxs)(`header`,{className:`chat-header`,style:{paddingBottom:`12px`,flexShrink:0,position:`relative`,maxWidth:`680px`,width:`100%`,margin:`0 auto`,padding:`24px 20px 12px`},children:[t?(0,m.jsx)(`button`,{onClick:N,style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Cancel`,children:`✕`}):(0,m.jsx)(`button`,{onClick:()=>h(!1),style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Close browser`,children:`✕`}),(0,m.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`chat-logo`}),(0,m.jsx)(`h1`,{className:`chat-tagline`,children:`Connect Claude`}),(0,m.jsx)(`p`,{className:`chat-intro`,children:t?`Sign in and authorize in the browser below.`:`Open your email or prepare your accounts, then sign in.`}),!t&&(0,m.jsx)(`div`,{style:{marginTop:`12px`},children:(0,m.jsx)(l,{variant:`primary`,onClick:M,disabled:a,children:a?(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`})}),t&&v===`native`&&(0,m.jsxs)(`div`,{style:{marginTop:`14px`,maxWidth:`420px`,margin:`14px auto 0`,display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,m.jsx)(`label`,{style:{fontSize:`12px`,color:`#999`,lineHeight:1.4},children:`If the browser didn't finish sign-in, paste the code or full redirect URL from Claude's page:`}),(0,m.jsxs)(`div`,{style:{display:`flex`,gap:`8px`},children:[(0,m.jsx)(`input`,{type:`text`,value:b,onChange:e=>x(e.target.value),placeholder:`Paste code or redirect URL`,style:{flex:1,padding:`8px 10px`,fontSize:`13px`,border:`1px solid #ccc`,borderRadius:`6px`}}),(0,m.jsx)(l,{variant:`primary`,onClick:A,disabled:!b.trim(),children:`Submit`})]}),S&&(0,m.jsx)(`p`,{style:{fontSize:`12px`,color:`#999`,margin:0},children:S})]})]}),(0,m.jsx)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0,gap:`10px`,padding:`0 0 16px`},children:O&&(0,m.jsx)(d,{preferTargetId:w,cacheKey:E,chrome:!1})}),s&&(0,m.jsx)(`p`,{className:`admin-pin-error`,style:{textAlign:`center`,padding:`0 20px 16px`},children:s})]}):(0,m.jsx)(`div`,{className:`connect-page`,children:(0,m.jsxs)(`div`,{className:`connect-content`,children:[(0,m.jsxs)(`div`,{className:`connect-logos`,children:[(0,m.jsx)(`div`,{className:`connect-logo-wrap`,children:(0,m.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`connect-logo`})}),(0,m.jsx)(`svg`,{className:`connect-arrow`,viewBox:`0 0 48 24`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:(0,m.jsx)(`path`,{d:`M0 12h44m0 0l-8-8m8 8l-8 8`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})}),(0,m.jsxs)(`div`,{className:`connect-logo-wrap`,children:[(0,m.jsx)(`img`,{src:i,alt:n.productName,className:`connect-logo connect-logo--maxy`}),!n.logoContainsName&&(0,m.jsx)(`span`,{className:`connect-logo-label`,children:n.productName})]})]}),(0,m.jsxs)(`h1`,{className:`connect-title`,children:[`Connect Claude to power `,n.productName]}),(0,m.jsx)(`p`,{className:`connect-subtitle`,children:`Sign in with your Anthropic account to get started.`}),(0,m.jsx)(l,{variant:`primary`,onClick:M,disabled:a,children:a?(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`}),(0,m.jsx)(`p`,{style:{marginTop:`6px`,fontSize:`11px`,color:`#999`,maxWidth:`300px`,textAlign:`center`,lineHeight:`1.4`},children:`First time? You may need to sign into your email and Anthropic account in the browser before connecting.`}),(0,m.jsx)(`button`,{onClick:j,disabled:g,style:{marginTop:`12px`,background:`none`,border:`none`,color:`var(--color-primary, #666)`,fontSize:`13px`,cursor:`pointer`,textDecoration:`underline`,textUnderlineOffset:`3px`},children:g?`Launching…`:`Open browser first`}),s&&(0,m.jsx)(`p`,{className:`admin-pin-error`,children:s})]})})}function y({auth:e}){return e.appState===`loading`?(0,m.jsx)(t,{surface:`gate`}):e.appState===`set-pin`?(0,m.jsx)(g,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,setPinFormRef:e.setPinFormRef,onSubmit:e.handleSetPin,operatorName:e.operatorName,setOperatorName:e.setOperatorName}):e.appState===`connect-claude`?(0,m.jsx)(v,{authPolling:e.authPolling,setAuthPolling:e.setAuthPolling,authLoading:e.authLoading,setAuthLoading:e.setAuthLoading,pinError:e.pinError,setPinError:e.setPinError,setAppState:e.setAppState}):e.appState===`enter-pin`?(0,m.jsx)(_,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,onSubmit:e.handleLogin,remoteAuthEnabled:e.remoteAuthEnabled,onSignOutRemote:()=>{window.location.href=`/__remote-auth/logout`}}):null}export{p as n,y as t};