@chrysb/alphaclaw 0.4.0 → 0.4.1-beta.1
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/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +80 -5
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +3 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/toolbar.js +13 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
- package/lib/public/js/components/google/account-row.js +34 -1
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +193 -44
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +5 -6
- package/lib/public/js/components/sidebar.js +3 -1
- package/lib/public/js/components/telegram-workspace/onboarding.js +1 -1
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +180 -127
- package/lib/public/js/lib/api.js +106 -1
- package/lib/public/js/lib/format.js +71 -0
- package/lib/server/constants.js +27 -0
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +705 -0
- package/lib/server/google-state.js +130 -0
- package/lib/server/helpers.js +5 -7
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/openclaw.js +9 -1
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +19 -0
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +47 -14
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +213 -64
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +1 -1
- package/package.json +1 -1
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
3
8
|
import htm from "https://esm.sh/htm";
|
|
4
9
|
import {
|
|
5
10
|
checkGoogleApis,
|
|
6
11
|
disconnectGoogle,
|
|
12
|
+
fetchGoogleCredentials,
|
|
7
13
|
saveGoogleAccount,
|
|
8
14
|
} from "../../lib/api.js";
|
|
9
15
|
import { getDefaultScopes, toggleScopeLogic } from "../scope-picker.js";
|
|
@@ -15,6 +21,8 @@ import { ActionButton } from "../action-button.js";
|
|
|
15
21
|
import { GoogleAccountRow } from "./account-row.js";
|
|
16
22
|
import { AddGoogleAccountModal } from "./add-account-modal.js";
|
|
17
23
|
import { useGoogleAccounts } from "./use-google-accounts.js";
|
|
24
|
+
import { useGmailWatch } from "./use-gmail-watch.js";
|
|
25
|
+
import { GmailSetupWizard } from "./gmail-setup-wizard.js";
|
|
18
26
|
|
|
19
27
|
const html = htm.bind(h);
|
|
20
28
|
|
|
@@ -26,13 +34,13 @@ const isPersonalAccount = (account = {}) => Boolean(account.personal);
|
|
|
26
34
|
|
|
27
35
|
const kGoogleIconPath = "/assets/icons/google_icon.svg";
|
|
28
36
|
|
|
29
|
-
export const Google = ({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
export const Google = ({
|
|
38
|
+
gatewayStatus,
|
|
39
|
+
onRestartRequired = () => {},
|
|
40
|
+
onOpenGmailWebhook = () => {},
|
|
41
|
+
}) => {
|
|
42
|
+
const { accounts, loading, hasCompanyCredentials, refreshAccounts } =
|
|
43
|
+
useGoogleAccounts({ gatewayStatus });
|
|
36
44
|
const [expandedAccountId, setExpandedAccountId] = useState("");
|
|
37
45
|
const [scopesByAccountId, setScopesByAccountId] = useState({});
|
|
38
46
|
const [savedScopesByAccountId, setSavedScopesByAccountId] = useState({});
|
|
@@ -52,6 +60,21 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
52
60
|
const [addCompanyModalOpen, setAddCompanyModalOpen] = useState(false);
|
|
53
61
|
const [savingAddCompany, setSavingAddCompany] = useState(false);
|
|
54
62
|
const [disconnectAccountId, setDisconnectAccountId] = useState("");
|
|
63
|
+
const [gmailWizardState, setGmailWizardState] = useState({
|
|
64
|
+
visible: false,
|
|
65
|
+
accountId: "",
|
|
66
|
+
});
|
|
67
|
+
const {
|
|
68
|
+
loading: gmailLoading,
|
|
69
|
+
watchByAccountId,
|
|
70
|
+
clientConfigByClient,
|
|
71
|
+
busyByAccountId,
|
|
72
|
+
savingClient,
|
|
73
|
+
refresh: refreshGmailWatch,
|
|
74
|
+
saveClientSetup,
|
|
75
|
+
startWatchForAccount,
|
|
76
|
+
stopWatchForAccount,
|
|
77
|
+
} = useGmailWatch({ gatewayStatus, accounts });
|
|
55
78
|
|
|
56
79
|
const hasPersonalAccount = useMemo(
|
|
57
80
|
() => accounts.some((account) => isPersonalAccount(account)),
|
|
@@ -68,12 +91,16 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
68
91
|
);
|
|
69
92
|
|
|
70
93
|
const ensureScopesForAccount = useCallback((account) => {
|
|
71
|
-
const nextScopes =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
const nextScopes =
|
|
95
|
+
Array.isArray(account.activeScopes) && account.activeScopes.length
|
|
96
|
+
? account.activeScopes
|
|
97
|
+
: Array.isArray(account.services) && account.services.length
|
|
98
|
+
? account.services
|
|
99
|
+
: getDefaultScopes();
|
|
100
|
+
setSavedScopesByAccountId((prev) => ({
|
|
101
|
+
...prev,
|
|
102
|
+
[account.id]: [...nextScopes],
|
|
103
|
+
}));
|
|
77
104
|
setScopesByAccountId((prev) => {
|
|
78
105
|
const current = prev[account.id];
|
|
79
106
|
if (!current || !hasScopesChanged(current, nextScopes)) {
|
|
@@ -103,7 +130,10 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
103
130
|
(accountId) => {
|
|
104
131
|
const account = getAccountById(accountId);
|
|
105
132
|
if (!account) return;
|
|
106
|
-
const scopes =
|
|
133
|
+
const scopes =
|
|
134
|
+
scopesByAccountId[accountId] ||
|
|
135
|
+
account.activeScopes ||
|
|
136
|
+
getDefaultScopes();
|
|
107
137
|
if (!scopes.length) {
|
|
108
138
|
window.alert("Select at least one service");
|
|
109
139
|
return;
|
|
@@ -138,7 +168,10 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
138
168
|
try {
|
|
139
169
|
const data = await checkGoogleApis(accountId);
|
|
140
170
|
if (data.results) {
|
|
141
|
-
setApiStatusByAccountId((prev) => ({
|
|
171
|
+
setApiStatusByAccountId((prev) => ({
|
|
172
|
+
...prev,
|
|
173
|
+
[accountId]: data.results,
|
|
174
|
+
}));
|
|
142
175
|
}
|
|
143
176
|
} finally {
|
|
144
177
|
setCheckingByAccountId((prev) => {
|
|
@@ -157,16 +190,20 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
157
190
|
const accountId = String(event.data?.accountId || "").trim();
|
|
158
191
|
setApiStatusByAccountId({});
|
|
159
192
|
await refreshAccounts();
|
|
193
|
+
await refreshGmailWatch();
|
|
160
194
|
if (accountId) {
|
|
161
195
|
await handleCheckApis(accountId);
|
|
162
196
|
}
|
|
163
197
|
} else if (event.data?.google === "error") {
|
|
164
|
-
showToast(
|
|
198
|
+
showToast(
|
|
199
|
+
`✗ Google auth failed: ${event.data.message || "unknown"}`,
|
|
200
|
+
"error",
|
|
201
|
+
);
|
|
165
202
|
}
|
|
166
203
|
};
|
|
167
204
|
window.addEventListener("message", handler);
|
|
168
205
|
return () => window.removeEventListener("message", handler);
|
|
169
|
-
}, [handleCheckApis, refreshAccounts]);
|
|
206
|
+
}, [handleCheckApis, refreshAccounts, refreshGmailWatch]);
|
|
170
207
|
|
|
171
208
|
useEffect(() => {
|
|
172
209
|
if (!expandedAccountId) return;
|
|
@@ -197,6 +234,7 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
197
234
|
return next;
|
|
198
235
|
});
|
|
199
236
|
await refreshAccounts();
|
|
237
|
+
await refreshGmailWatch();
|
|
200
238
|
};
|
|
201
239
|
|
|
202
240
|
const openCredentialsModal = ({
|
|
@@ -225,6 +263,9 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
225
263
|
};
|
|
226
264
|
|
|
227
265
|
const handleCredentialsSaved = async (account) => {
|
|
266
|
+
if (account?.id) {
|
|
267
|
+
setExpandedAccountId(account.id);
|
|
268
|
+
}
|
|
228
269
|
await refreshAccounts();
|
|
229
270
|
if (account?.id) startAuth(account.id);
|
|
230
271
|
};
|
|
@@ -243,6 +284,9 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
243
284
|
return;
|
|
244
285
|
}
|
|
245
286
|
setAddCompanyModalOpen(false);
|
|
287
|
+
if (data.accountId) {
|
|
288
|
+
setExpandedAccountId(data.accountId);
|
|
289
|
+
}
|
|
246
290
|
await refreshAccounts();
|
|
247
291
|
if (data.accountId) startAuth(data.accountId);
|
|
248
292
|
} finally {
|
|
@@ -276,40 +320,113 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
276
320
|
});
|
|
277
321
|
};
|
|
278
322
|
|
|
279
|
-
const handleEditCredentials = (accountId) => {
|
|
323
|
+
const handleEditCredentials = async (accountId) => {
|
|
280
324
|
const account = getAccountById(accountId);
|
|
281
325
|
if (!account) return;
|
|
282
326
|
const personal = isPersonalAccount(account);
|
|
327
|
+
const client = personal ? "personal" : account.client || "default";
|
|
328
|
+
let credentialValues = {};
|
|
329
|
+
try {
|
|
330
|
+
const credentialResponse = await fetchGoogleCredentials({
|
|
331
|
+
accountId: account.id,
|
|
332
|
+
client,
|
|
333
|
+
});
|
|
334
|
+
if (credentialResponse?.ok) {
|
|
335
|
+
credentialValues = {
|
|
336
|
+
clientId: String(credentialResponse.clientId || ""),
|
|
337
|
+
clientSecret: String(credentialResponse.clientSecret || ""),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
showToast("Could not load saved client credentials", "warning");
|
|
342
|
+
}
|
|
283
343
|
openCredentialsModal({
|
|
284
344
|
accountId: account.id,
|
|
285
|
-
client
|
|
345
|
+
client,
|
|
286
346
|
personal,
|
|
287
347
|
title: `Edit Credentials (${account.email})`,
|
|
288
348
|
submitLabel: "Save Credentials",
|
|
289
349
|
defaultInstrType: personal ? "personal" : "workspace",
|
|
290
350
|
initialValues: {
|
|
291
351
|
email: account.email,
|
|
352
|
+
...credentialValues,
|
|
292
353
|
},
|
|
293
354
|
});
|
|
294
355
|
};
|
|
295
356
|
|
|
357
|
+
const openGmailSetupWizard = (accountId) => {
|
|
358
|
+
setGmailWizardState({
|
|
359
|
+
visible: true,
|
|
360
|
+
accountId: String(accountId || ""),
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const closeGmailSetupWizard = () => {
|
|
365
|
+
setGmailWizardState({
|
|
366
|
+
visible: false,
|
|
367
|
+
accountId: "",
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const handleEnableGmailWatch = async (accountId) => {
|
|
372
|
+
const account = getAccountById(accountId);
|
|
373
|
+
if (!account) return;
|
|
374
|
+
const client = String(account.client || "default").trim() || "default";
|
|
375
|
+
const clientConfig = clientConfigByClient.get(client);
|
|
376
|
+
if (!clientConfig?.configured) {
|
|
377
|
+
openGmailSetupWizard(accountId);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const result = await startWatchForAccount(accountId);
|
|
382
|
+
if (result?.restartRequired) {
|
|
383
|
+
onRestartRequired(true);
|
|
384
|
+
}
|
|
385
|
+
showToast("Gmail watch enabled", "success");
|
|
386
|
+
} catch (err) {
|
|
387
|
+
showToast(err.message || "Could not enable Gmail watch", "error");
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const handleDisableGmailWatch = async (accountId) => {
|
|
392
|
+
try {
|
|
393
|
+
await stopWatchForAccount(accountId);
|
|
394
|
+
showToast("Gmail watch disabled", "info");
|
|
395
|
+
} catch (err) {
|
|
396
|
+
showToast(err.message || "Could not disable Gmail watch", "error");
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const handleFinishGmailSetupWizard = async ({ client, projectId }) => {
|
|
401
|
+
const accountId = String(gmailWizardState.accountId || "").trim();
|
|
402
|
+
if (!accountId) return;
|
|
403
|
+
await saveClientSetup({
|
|
404
|
+
client,
|
|
405
|
+
projectId,
|
|
406
|
+
regeneratePushToken: false,
|
|
407
|
+
});
|
|
408
|
+
await startWatchForAccount(accountId);
|
|
409
|
+
showToast("Gmail setup complete and watch enabled", "success");
|
|
410
|
+
};
|
|
411
|
+
|
|
296
412
|
const renderEmptyState = () => html`
|
|
297
|
-
<div class="text-center space-y-2
|
|
413
|
+
<div class="text-center space-y-2 pt-3">
|
|
298
414
|
<div class="rounded-lg border border-border bg-black/20 px-3 py-5">
|
|
299
|
-
<div class="flex flex-col items-center justify-center gap-
|
|
415
|
+
<div class="flex flex-col items-center justify-center gap-3">
|
|
300
416
|
<img
|
|
301
417
|
src=${kGoogleIconPath}
|
|
302
418
|
alt="Google logo"
|
|
303
|
-
class="h-
|
|
419
|
+
class="h-5 w-5 shrink-0"
|
|
304
420
|
loading="lazy"
|
|
305
421
|
decoding="async"
|
|
306
422
|
/>
|
|
307
423
|
<p class="text-xs text-gray-500">
|
|
308
|
-
Connect Gmail, Calendar, Contacts, Drive, Sheets, Tasks, Docs, and
|
|
424
|
+
Connect Gmail, Calendar, Contacts, Drive, Sheets, Tasks, Docs, and
|
|
425
|
+
Meet.
|
|
309
426
|
</p>
|
|
310
427
|
</div>
|
|
311
428
|
</div>
|
|
312
|
-
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
429
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 pt-2">
|
|
313
430
|
<${ActionButton}
|
|
314
431
|
onClick=${handleAddCompanyClick}
|
|
315
432
|
tone="primary"
|
|
@@ -373,28 +490,46 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
373
490
|
`}
|
|
374
491
|
/>
|
|
375
492
|
${loading
|
|
376
|
-
? html`<div class="text-gray-500 text-sm text-center py-2">
|
|
493
|
+
? html`<div class="text-gray-500 text-sm text-center py-2">
|
|
494
|
+
Loading...
|
|
495
|
+
</div>`
|
|
377
496
|
: accounts.length
|
|
378
497
|
? html`
|
|
379
498
|
<div class="space-y-2 mt-3">
|
|
380
|
-
${accounts.map(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
499
|
+
${accounts.map(
|
|
500
|
+
(account) =>
|
|
501
|
+
html`<${GoogleAccountRow}
|
|
502
|
+
key=${account.id}
|
|
503
|
+
account=${account}
|
|
504
|
+
personal=${isPersonalAccount(account)}
|
|
505
|
+
expanded=${expandedAccountId === account.id}
|
|
506
|
+
onToggleExpanded=${(accountId) =>
|
|
507
|
+
setExpandedAccountId((prev) =>
|
|
508
|
+
prev === accountId ? "" : accountId,
|
|
509
|
+
)}
|
|
510
|
+
scopes=${scopesByAccountId[account.id] ||
|
|
511
|
+
account.activeScopes ||
|
|
512
|
+
getDefaultScopes()}
|
|
513
|
+
savedScopes=${savedScopesByAccountId[account.id] ||
|
|
514
|
+
account.activeScopes ||
|
|
515
|
+
getDefaultScopes()}
|
|
516
|
+
apiStatus=${apiStatusByAccountId[account.id] || {}}
|
|
517
|
+
checkingApis=${expandedAccountId === account.id &&
|
|
518
|
+
Boolean(checkingByAccountId[account.id])}
|
|
519
|
+
onToggleScope=${handleToggleScope}
|
|
520
|
+
onCheckApis=${handleCheckApis}
|
|
521
|
+
onUpdatePermissions=${(accountId) => startAuth(accountId)}
|
|
522
|
+
onEditCredentials=${handleEditCredentials}
|
|
523
|
+
onDisconnect=${(accountId) =>
|
|
524
|
+
setDisconnectAccountId(accountId)}
|
|
525
|
+
gmailWatchStatus=${watchByAccountId.get(account.id) ||
|
|
526
|
+
null}
|
|
527
|
+
gmailWatchBusy=${Boolean(busyByAccountId[account.id])}
|
|
528
|
+
onEnableGmailWatch=${handleEnableGmailWatch}
|
|
529
|
+
onDisableGmailWatch=${handleDisableGmailWatch}
|
|
530
|
+
onOpenGmailSetup=${openGmailSetupWizard}
|
|
531
|
+
onOpenGmailWebhook=${onOpenGmailWebhook}
|
|
532
|
+
/>`,
|
|
398
533
|
)}
|
|
399
534
|
</div>
|
|
400
535
|
`
|
|
@@ -422,6 +557,20 @@ export const Google = ({ gatewayStatus }) => {
|
|
|
422
557
|
title="Add Company Account"
|
|
423
558
|
/>
|
|
424
559
|
|
|
560
|
+
<${GmailSetupWizard}
|
|
561
|
+
visible=${gmailWizardState.visible}
|
|
562
|
+
account=${getAccountById(gmailWizardState.accountId)}
|
|
563
|
+
clientConfig=${clientConfigByClient.get(
|
|
564
|
+
String(
|
|
565
|
+
getAccountById(gmailWizardState.accountId)?.client || "default",
|
|
566
|
+
).trim() || "default",
|
|
567
|
+
) || null}
|
|
568
|
+
saving=${savingClient || gmailLoading}
|
|
569
|
+
onClose=${closeGmailSetupWizard}
|
|
570
|
+
onSaveSetup=${saveClientSetup}
|
|
571
|
+
onFinish=${handleFinishGmailSetupWizard}
|
|
572
|
+
/>
|
|
573
|
+
|
|
425
574
|
<${ConfirmDialog}
|
|
426
575
|
visible=${Boolean(disconnectAccountId)}
|
|
427
576
|
title="Disconnect Google account?"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
fetchGmailConfig,
|
|
4
|
+
renewGmailWatch,
|
|
5
|
+
saveGmailConfig,
|
|
6
|
+
startGmailWatch,
|
|
7
|
+
stopGmailWatch,
|
|
8
|
+
} from "../../lib/api.js";
|
|
9
|
+
|
|
10
|
+
export const useGmailWatch = ({ gatewayStatus, accounts = [] }) => {
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [config, setConfig] = useState(null);
|
|
13
|
+
const [busyByAccountId, setBusyByAccountId] = useState({});
|
|
14
|
+
const [savingClient, setSavingClient] = useState(false);
|
|
15
|
+
|
|
16
|
+
const refresh = useCallback(async () => {
|
|
17
|
+
setLoading(true);
|
|
18
|
+
try {
|
|
19
|
+
const nextConfig = await fetchGmailConfig();
|
|
20
|
+
setConfig(nextConfig);
|
|
21
|
+
return nextConfig;
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (gatewayStatus !== "running") return;
|
|
29
|
+
refresh();
|
|
30
|
+
}, [gatewayStatus, refresh]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (gatewayStatus !== "running") return;
|
|
34
|
+
if (!accounts.length) return;
|
|
35
|
+
refresh();
|
|
36
|
+
}, [accounts, gatewayStatus, refresh]);
|
|
37
|
+
|
|
38
|
+
const watchByAccountId = useMemo(() => {
|
|
39
|
+
const map = new Map();
|
|
40
|
+
for (const entry of config?.accounts || []) {
|
|
41
|
+
map.set(String(entry.accountId || ""), entry);
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}, [config]);
|
|
45
|
+
|
|
46
|
+
const clientConfigByClient = useMemo(() => {
|
|
47
|
+
const map = new Map();
|
|
48
|
+
for (const clientConfig of config?.clients || []) {
|
|
49
|
+
map.set(String(clientConfig.client || "default"), clientConfig);
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}, [config]);
|
|
53
|
+
|
|
54
|
+
const setBusy = (accountId, busy) => {
|
|
55
|
+
setBusyByAccountId((prev) => {
|
|
56
|
+
const key = String(accountId || "");
|
|
57
|
+
if (!key) return prev;
|
|
58
|
+
if (busy) return { ...prev, [key]: true };
|
|
59
|
+
if (!prev[key]) return prev;
|
|
60
|
+
const next = { ...prev };
|
|
61
|
+
delete next[key];
|
|
62
|
+
return next;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const startWatchForAccount = useCallback(async (accountId) => {
|
|
67
|
+
const key = String(accountId || "");
|
|
68
|
+
setBusy(key, true);
|
|
69
|
+
try {
|
|
70
|
+
const data = await startGmailWatch(key);
|
|
71
|
+
await refresh();
|
|
72
|
+
return data;
|
|
73
|
+
} finally {
|
|
74
|
+
setBusy(key, false);
|
|
75
|
+
}
|
|
76
|
+
}, [refresh]);
|
|
77
|
+
|
|
78
|
+
const stopWatchForAccount = useCallback(async (accountId) => {
|
|
79
|
+
const key = String(accountId || "");
|
|
80
|
+
setBusy(key, true);
|
|
81
|
+
try {
|
|
82
|
+
await stopGmailWatch(key);
|
|
83
|
+
await refresh();
|
|
84
|
+
} finally {
|
|
85
|
+
setBusy(key, false);
|
|
86
|
+
}
|
|
87
|
+
}, [refresh]);
|
|
88
|
+
|
|
89
|
+
const renewForAccount = useCallback(async (accountId = "") => {
|
|
90
|
+
const key = String(accountId || "");
|
|
91
|
+
if (key) setBusy(key, true);
|
|
92
|
+
try {
|
|
93
|
+
await renewGmailWatch({ accountId: key, force: true });
|
|
94
|
+
await refresh();
|
|
95
|
+
} finally {
|
|
96
|
+
if (key) setBusy(key, false);
|
|
97
|
+
}
|
|
98
|
+
}, [refresh]);
|
|
99
|
+
|
|
100
|
+
const saveClientSetup = useCallback(async ({
|
|
101
|
+
client = "default",
|
|
102
|
+
projectId = "",
|
|
103
|
+
regeneratePushToken = false,
|
|
104
|
+
} = {}) => {
|
|
105
|
+
setSavingClient(true);
|
|
106
|
+
try {
|
|
107
|
+
const data = await saveGmailConfig({
|
|
108
|
+
client,
|
|
109
|
+
projectId,
|
|
110
|
+
regeneratePushToken,
|
|
111
|
+
});
|
|
112
|
+
await refresh();
|
|
113
|
+
return data;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const message = String(err?.message || "");
|
|
116
|
+
if (message.toLowerCase().includes("not found")) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Gmail watch API route not found. Restart AlphaClaw so /api/gmail routes are loaded.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
} finally {
|
|
123
|
+
setSavingClient(false);
|
|
124
|
+
}
|
|
125
|
+
}, [refresh]);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
loading,
|
|
129
|
+
config,
|
|
130
|
+
watchByAccountId,
|
|
131
|
+
clientConfigByClient,
|
|
132
|
+
busyByAccountId,
|
|
133
|
+
savingClient,
|
|
134
|
+
refresh,
|
|
135
|
+
saveClientSetup,
|
|
136
|
+
startWatchForAccount,
|
|
137
|
+
stopWatchForAccount,
|
|
138
|
+
renewForAccount,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
@@ -68,7 +68,7 @@ export function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
|
|
|
68
68
|
<button
|
|
69
69
|
type="button"
|
|
70
70
|
onclick=${() => setShowAll((prev) => !prev)}
|
|
71
|
-
class="text-xs text-gray-500 hover:text-gray-300"
|
|
71
|
+
class="ml-3 text-xs text-gray-500 hover:text-gray-300"
|
|
72
72
|
>
|
|
73
73
|
${showAll ? 'Show fewer services' : `Show more services (${SERVICES.length - kVisibleCount})`}
|
|
74
74
|
</button>
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { fetchBrowseGitSummary, syncBrowseChanges } from "../lib/api.js";
|
|
5
|
+
import { formatLocaleDateTime } from "../lib/format.js";
|
|
5
6
|
import { ActionButton } from "./action-button.js";
|
|
6
7
|
import { GitBranchLineIcon, GithubFillIcon } from "./icons.js";
|
|
7
8
|
import { LoadingSpinner } from "./loading-spinner.js";
|
|
@@ -13,12 +14,10 @@ const kSyncCommitFileNameLimit = 4;
|
|
|
13
14
|
const kCommitHistoryLimit = 12;
|
|
14
15
|
|
|
15
16
|
const formatCommitTime = (unixSeconds) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return "";
|
|
21
|
-
}
|
|
17
|
+
return formatLocaleDateTime(unixSeconds, {
|
|
18
|
+
fallback: "",
|
|
19
|
+
valueIsUnixSeconds: true,
|
|
20
|
+
});
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
const getRepoName = (summary) => {
|
|
@@ -12,7 +12,7 @@ const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
|
|
|
12
12
|
const kBrowsePanelMinHeightPx = 120;
|
|
13
13
|
const kBrowseBottomMinHeightPx = 120;
|
|
14
14
|
const kBrowseResizerHeightPx = 6;
|
|
15
|
-
const kDefaultBrowseBottomPanelHeightPx =
|
|
15
|
+
const kDefaultBrowseBottomPanelHeightPx = 260;
|
|
16
16
|
|
|
17
17
|
const readStoredBrowseBottomPanelHeight = () => {
|
|
18
18
|
try {
|
|
@@ -89,6 +89,8 @@ export const AppSidebar = ({
|
|
|
89
89
|
const layoutElement = browseLayoutRef.current;
|
|
90
90
|
if (!layoutElement || typeof ResizeObserver === "undefined") return () => {};
|
|
91
91
|
const observer = new ResizeObserver(() => {
|
|
92
|
+
const layoutRect = layoutElement.getBoundingClientRect();
|
|
93
|
+
if (layoutRect.height <= 0) return;
|
|
92
94
|
setBrowseBottomPanelHeightPx((currentHeight) =>
|
|
93
95
|
getClampedBrowseBottomPanelHeight(currentHeight),
|
|
94
96
|
);
|
|
@@ -116,7 +116,7 @@ export const CreateGroupStep = ({ onNext, onBack }) => html`
|
|
|
116
116
|
<p class="text-xs font-medium text-gray-300">Create the group</p>
|
|
117
117
|
<ol class="text-xs text-gray-400 space-y-2 list-decimal list-inside">
|
|
118
118
|
<li>
|
|
119
|
-
Open Telegram and create a
|
|
119
|
+
Open Telegram and create a${" "}
|
|
120
120
|
<span class="text-gray-300">new group</span>
|
|
121
121
|
</li>
|
|
122
122
|
<li>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h } from 'https://esm.sh/preact';
|
|
2
2
|
import { useState, useEffect } from 'https://esm.sh/preact/hooks';
|
|
3
|
+
import { createPortal } from 'https://esm.sh/preact/compat';
|
|
3
4
|
import htm from 'https://esm.sh/htm';
|
|
4
5
|
const html = htm.bind(h);
|
|
5
6
|
|
|
@@ -50,11 +51,14 @@ export function ToastContainer({
|
|
|
50
51
|
|
|
51
52
|
if (toasts.length === 0) return null;
|
|
52
53
|
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
${t.text
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
return createPortal(
|
|
55
|
+
html`<div class=${className} style=${{ zIndex: 70 }}>
|
|
56
|
+
${toasts.map(t => html`
|
|
57
|
+
<div key=${t.id} class="${kToastClassByType[normalizeToastType(t.type)]} px-4 py-2 rounded-lg text-sm">
|
|
58
|
+
${t.text}
|
|
59
|
+
</div>
|
|
60
|
+
`)}
|
|
61
|
+
</div>`,
|
|
62
|
+
document.body,
|
|
63
|
+
);
|
|
60
64
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const kColorPalette = [
|
|
2
|
+
"#7dd3fc",
|
|
3
|
+
"#22d3ee",
|
|
4
|
+
"#34d399",
|
|
5
|
+
"#fbbf24",
|
|
6
|
+
"#fb7185",
|
|
7
|
+
"#a78bfa",
|
|
8
|
+
"#f472b6",
|
|
9
|
+
"#60a5fa",
|
|
10
|
+
"#4ade80",
|
|
11
|
+
"#f97316",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const kBadgeToneClass = {
|
|
15
|
+
cyan: "border-cyan-400/30 text-cyan-300 bg-cyan-400/10",
|
|
16
|
+
blue: "border-blue-400/30 text-blue-300 bg-blue-400/10",
|
|
17
|
+
purple: "border-purple-400/30 text-purple-300 bg-purple-400/10",
|
|
18
|
+
gray: "border-gray-400/30 text-gray-400 bg-gray-400/10",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const kRangeOptions = [
|
|
22
|
+
{ label: "7d", value: 7 },
|
|
23
|
+
{ label: "30d", value: 30 },
|
|
24
|
+
{ label: "90d", value: 90 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const kDefaultUsageDays = 30;
|
|
28
|
+
export const kDefaultUsageMetric = "tokens";
|
|
29
|
+
export const kUsageDaysUiSettingKey = "usageDays";
|
|
30
|
+
export const kUsageMetricUiSettingKey = "usageMetric";
|
|
31
|
+
export const kUsageSourceOrder = ["chat", "hooks", "cron"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { kColorPalette } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export const toLocalDayKey = (value) => {
|
|
4
|
+
const d = value instanceof Date ? value : new Date(value ?? Date.now());
|
|
5
|
+
const year = d.getFullYear();
|
|
6
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
7
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
8
|
+
return `${year}-${month}-${day}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const toChartColor = (key) => {
|
|
12
|
+
const raw = String(key || "");
|
|
13
|
+
let hash = 0;
|
|
14
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
15
|
+
hash = ((hash << 5) - hash + raw.charCodeAt(index)) | 0;
|
|
16
|
+
}
|
|
17
|
+
return kColorPalette[Math.abs(hash) % kColorPalette.length];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const renderSourceLabel = (source) => {
|
|
21
|
+
if (source === "hooks") return "Hooks";
|
|
22
|
+
if (source === "cron") return "Cron";
|
|
23
|
+
return "Chat";
|
|
24
|
+
};
|