@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.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +20 -1
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +118 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +15 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +25 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +62 -5
- package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
- package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
- 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/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-C6e5jf0S.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-DStA0lfm.js → AdminShell-Bfapavlp.js} +2 -2
- 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--yyBcNfB.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-odyVOb5E.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-D1K_MoHt.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-jqh9U7Ic.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-DkuHwkaB.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-C6o_NRjj.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-BN30v5hJ.js → graph-D2J7a7Ll.js} +1 -1
- package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
- package/payload/server/public/assets/{graph-BN30v5hJ.js.gz → graph-D2J7a7Ll.js.gz} +0 -0
- package/payload/server/public/assets/{graph-labels-DHFyNXQt.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-DwFYA4Fp.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-CEWIP3VF.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-Ceb0n7Km.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-CDYdx91V.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-DwNTih0y.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-TIDCdOLQ.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-eTXvaWnd.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-JwLSrj17.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-DjTkNw2p.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-D8VDA0RN.js → useMediaQuery-oDCasnxD.js} +1 -1
- package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
- package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js.gz → 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-DutUiRu7.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 +2448 -1667
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.gz +0 -0
- package/payload/server/public/assets/activity--yyBcNfB.js.br +0 -0
- package/payload/server/public/assets/activity--yyBcNfB.js.gz +0 -0
- package/payload/server/public/assets/admin-Dr6Jwe7W.js +0 -1
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.br +0 -0
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.gz +0 -0
- package/payload/server/public/assets/agents-odyVOb5E.js.br +0 -0
- package/payload/server/public/assets/agents-odyVOb5E.js.gz +0 -0
- package/payload/server/public/assets/browser-D1K_MoHt.js.br +0 -0
- package/payload/server/public/assets/browser-D1K_MoHt.js.gz +0 -0
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.br +0 -0
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.gz +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js +0 -1
- package/payload/server/public/assets/chat-QQHBdVor.js.br +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DwTXgKwr.js +0 -1
- package/payload/server/public/assets/clock-DOmh9iKX.js +0 -1
- package/payload/server/public/assets/clock-DOmh9iKX.js.br +0 -0
- package/payload/server/public/assets/copy-DkuHwkaB.js.br +0 -0
- package/payload/server/public/assets/copy-DkuHwkaB.js.gz +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js +0 -1
- package/payload/server/public/assets/data-DYmTNqrb.js.br +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js.gz +0 -0
- package/payload/server/public/assets/file-text-C6o_NRjj.js.br +0 -0
- package/payload/server/public/assets/file-text-C6o_NRjj.js.gz +0 -0
- package/payload/server/public/assets/graph-BN30v5hJ.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.gz +0 -0
- package/payload/server/public/assets/operator-DwFYA4Fp.js.br +0 -0
- package/payload/server/public/assets/operator-DwFYA4Fp.js.gz +0 -0
- package/payload/server/public/assets/page-BALVRssF.js +0 -1
- package/payload/server/public/assets/page-BALVRssF.js.br +0 -0
- package/payload/server/public/assets/page-BALVRssF.js.gz +0 -0
- package/payload/server/public/assets/page-CEWIP3VF.js.br +0 -0
- package/payload/server/public/assets/page-CEWIP3VF.js.gz +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js +0 -1
- package/payload/server/public/assets/play-Bg-BY_oj.js.br +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js.gz +0 -0
- package/payload/server/public/assets/public-Ceb0n7Km.js.br +0 -0
- package/payload/server/public/assets/public-Ceb0n7Km.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-CDYdx91V.js.gz +0 -0
- package/payload/server/public/assets/routines-DwNTih0y.js.br +0 -0
- package/payload/server/public/assets/routines-DwNTih0y.js.gz +0 -0
- package/payload/server/public/assets/skills-TIDCdOLQ.js.br +0 -0
- package/payload/server/public/assets/skills-TIDCdOLQ.js.gz +0 -0
- package/payload/server/public/assets/tasks-eTXvaWnd.js.br +0 -0
- package/payload/server/public/assets/tasks-eTXvaWnd.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css +0 -1
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.gz +0 -0
- package/payload/server/public/assets/wrench-DutUiRu7.js.br +0 -0
- package/payload/server/public/assets/wrench-DutUiRu7.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
|
---
|
|
@@ -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
|
|
314
|
-
accountId
|
|
315
|
-
visitId
|
|
316
|
-
ownerId
|
|
317
|
-
status
|
|
318
|
-
statusAt
|
|
319
|
-
|
|
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
|
|
104
|
-
// a real person was
|
|
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,
|
|
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
|
-
|
|
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
|
-
`[
|
|
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
|
-
`[
|
|
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
|
-
`[
|
|
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,
|
|
@@ -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
|
|
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-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/triangle-alert-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/useMediaQuery-
|
|
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-
|
|
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-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/useMediaQuery-
|
|
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>
|
package/payload/server/public/assets/{AdminLoginScreens-C6e5jf0S.js → AdminLoginScreens-5gI137xF.js}
RENAMED
|
@@ -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};
|
|
Binary file
|
|
Binary file
|