@chrysb/alphaclaw 0.6.0 → 0.6.2-beta.0
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/agents.css +1 -1
- package/lib/public/css/cron.css +535 -0
- package/lib/public/css/theme.css +72 -0
- package/lib/public/js/app.js +45 -10
- package/lib/public/js/components/action-button.js +26 -20
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
- package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
- package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
- package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
- package/lib/public/js/components/agents-tab/index.js +4 -0
- package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
- package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
- package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
- package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
- package/lib/public/js/components/cron-tab/index.js +100 -0
- package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
- package/lib/public/js/components/doctor/summary-cards.js +5 -11
- package/lib/public/js/components/google/gmail-setup-wizard.js +30 -30
- package/lib/public/js/components/google/index.js +1 -1
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/pill-tabs.js +33 -0
- package/lib/public/js/components/pop-actions.js +58 -0
- package/lib/public/js/components/routes/agents-route.js +4 -0
- package/lib/public/js/components/routes/cron-route.js +9 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/segmented-control.js +15 -9
- package/lib/public/js/components/summary-stat-card.js +17 -0
- package/lib/public/js/components/tooltip.js +50 -4
- package/lib/public/js/components/watchdog-tab.js +46 -1
- package/lib/public/js/lib/api.js +94 -0
- package/lib/public/js/lib/app-navigation.js +2 -0
- package/lib/public/js/lib/storage-keys.js +1 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/agents/agents.js +15 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/cost-utils.js +312 -0
- package/lib/server/cron-service.js +461 -0
- package/lib/server/db/usage/index.js +100 -1
- package/lib/server/db/usage/pricing.js +1 -83
- package/lib/server/db/usage/sessions.js +4 -1
- package/lib/server/db/usage/shared.js +2 -1
- package/lib/server/db/usage/summary.js +5 -1
- package/lib/server/gmail-watch.js +0 -1
- package/lib/server/onboarding/index.js +39 -5
- package/lib/server/onboarding/openclaw.js +25 -19
- package/lib/server/onboarding/validation.js +28 -0
- package/lib/server/routes/cron.js +148 -0
- package/lib/server.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
8
|
+
import { usePolling } from "../../hooks/usePolling.js";
|
|
9
|
+
import {
|
|
10
|
+
fetchCronBulkRuns,
|
|
11
|
+
fetchCronBulkUsage,
|
|
12
|
+
fetchCronJobRuns,
|
|
13
|
+
fetchCronJobs,
|
|
14
|
+
fetchCronJobUsage,
|
|
15
|
+
fetchCronStatus,
|
|
16
|
+
setCronJobEnabled,
|
|
17
|
+
triggerCronJobRun,
|
|
18
|
+
updateCronJobPrompt,
|
|
19
|
+
} from "../../lib/api.js";
|
|
20
|
+
import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
|
|
21
|
+
import { showToast } from "../toast.js";
|
|
22
|
+
import { kAllCronJobsRouteKey } from "./cron-helpers.js";
|
|
23
|
+
|
|
24
|
+
const kDefaultListPanelWidthPx = 372;
|
|
25
|
+
const kListPanelMinWidthPx = 220;
|
|
26
|
+
const kListPanelMaxWidthPx = 480;
|
|
27
|
+
const kListPanelWidthUiSettingKey = "cronListPanelWidthPx";
|
|
28
|
+
const kRunsPageSize = 25;
|
|
29
|
+
const kCalendarUsageDays = 30;
|
|
30
|
+
const kCalendarPastDays = 30;
|
|
31
|
+
|
|
32
|
+
const clampListPanelWidth = (value) =>
|
|
33
|
+
Math.max(kListPanelMinWidthPx, Math.min(kListPanelMaxWidthPx, value));
|
|
34
|
+
|
|
35
|
+
const normalizeRouteJobId = (jobId = "") => {
|
|
36
|
+
const normalized = String(jobId || "").trim();
|
|
37
|
+
return normalized || kAllCronJobsRouteKey;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
41
|
+
const listPanelRef = useRef(null);
|
|
42
|
+
const [listPanelWidthPx, setListPanelWidthPx] = useState(() => {
|
|
43
|
+
const settings = readUiSettings();
|
|
44
|
+
if (!Number.isFinite(settings?.[kListPanelWidthUiSettingKey])) {
|
|
45
|
+
return kDefaultListPanelWidthPx;
|
|
46
|
+
}
|
|
47
|
+
return clampListPanelWidth(settings[kListPanelWidthUiSettingKey]);
|
|
48
|
+
});
|
|
49
|
+
const [isResizingListPanel, setIsResizingListPanel] = useState(false);
|
|
50
|
+
const [runStatusFilter, setRunStatusFilter] = useState("all");
|
|
51
|
+
const [runDeliveryFilter, setRunDeliveryFilter] = useState("all");
|
|
52
|
+
const [runEntries, setRunEntries] = useState([]);
|
|
53
|
+
const [runHasMore, setRunHasMore] = useState(false);
|
|
54
|
+
const [runNextOffset, setRunNextOffset] = useState(0);
|
|
55
|
+
const [runTotal, setRunTotal] = useState(0);
|
|
56
|
+
const [loadingMoreRuns, setLoadingMoreRuns] = useState(false);
|
|
57
|
+
const [promptValue, setPromptValue] = useState("");
|
|
58
|
+
const [savedPromptValue, setSavedPromptValue] = useState("");
|
|
59
|
+
const [savingPrompt, setSavingPrompt] = useState(false);
|
|
60
|
+
const [runningJob, setRunningJob] = useState(false);
|
|
61
|
+
const [togglingJobEnabled, setTogglingJobEnabled] = useState(false);
|
|
62
|
+
const [usageDays, setUsageDays] = useState(30);
|
|
63
|
+
|
|
64
|
+
const selectedRouteKey = normalizeRouteJobId(jobId);
|
|
65
|
+
const selectedJobId =
|
|
66
|
+
selectedRouteKey === kAllCronJobsRouteKey ? "" : selectedRouteKey;
|
|
67
|
+
|
|
68
|
+
const jobsPoll = usePolling(
|
|
69
|
+
() => fetchCronJobs({ sortBy: "nextRunAtMs", sortDir: "asc" }),
|
|
70
|
+
15000,
|
|
71
|
+
);
|
|
72
|
+
const statusPoll = usePolling(fetchCronStatus, 30000);
|
|
73
|
+
const runsPoll = usePolling(
|
|
74
|
+
() => {
|
|
75
|
+
if (!selectedJobId) {
|
|
76
|
+
return Promise.resolve({
|
|
77
|
+
ok: true,
|
|
78
|
+
runs: { entries: [], hasMore: false, nextOffset: 0 },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return fetchCronJobRuns(selectedJobId, {
|
|
82
|
+
limit: kRunsPageSize,
|
|
83
|
+
offset: 0,
|
|
84
|
+
status: runStatusFilter,
|
|
85
|
+
deliveryStatus: runDeliveryFilter,
|
|
86
|
+
sortDir: "desc",
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
10000,
|
|
90
|
+
{ enabled: !!selectedJobId },
|
|
91
|
+
);
|
|
92
|
+
const usagePoll = usePolling(
|
|
93
|
+
() => {
|
|
94
|
+
if (!selectedJobId) return Promise.resolve({ ok: true, usage: null });
|
|
95
|
+
return fetchCronJobUsage(selectedJobId, { days: usageDays });
|
|
96
|
+
},
|
|
97
|
+
60000,
|
|
98
|
+
{ enabled: !!selectedJobId },
|
|
99
|
+
);
|
|
100
|
+
const bulkUsagePoll = usePolling(
|
|
101
|
+
() => fetchCronBulkUsage({ days: kCalendarUsageDays }),
|
|
102
|
+
60000,
|
|
103
|
+
{ enabled: !selectedJobId },
|
|
104
|
+
);
|
|
105
|
+
const bulkRunsPoll = usePolling(
|
|
106
|
+
() =>
|
|
107
|
+
fetchCronBulkRuns({
|
|
108
|
+
sinceMs: Date.now() - kCalendarPastDays * 24 * 60 * 60 * 1000,
|
|
109
|
+
limitPerJob: 1200,
|
|
110
|
+
}),
|
|
111
|
+
30000,
|
|
112
|
+
{ enabled: !selectedJobId },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const settings = readUiSettings();
|
|
117
|
+
settings[kListPanelWidthUiSettingKey] = listPanelWidthPx;
|
|
118
|
+
writeUiSettings(settings);
|
|
119
|
+
}, [listPanelWidthPx]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!runsPoll.data?.runs) return;
|
|
123
|
+
setRunEntries(
|
|
124
|
+
Array.isArray(runsPoll.data.runs.entries)
|
|
125
|
+
? runsPoll.data.runs.entries
|
|
126
|
+
: [],
|
|
127
|
+
);
|
|
128
|
+
setRunHasMore(!!runsPoll.data.runs.hasMore);
|
|
129
|
+
setRunNextOffset(Number(runsPoll.data.runs.nextOffset || 0));
|
|
130
|
+
setRunTotal(Number(runsPoll.data.runs.total || 0));
|
|
131
|
+
}, [runsPoll.data]);
|
|
132
|
+
|
|
133
|
+
const jobs = useMemo(
|
|
134
|
+
() => (Array.isArray(jobsPoll.data?.jobs) ? jobsPoll.data.jobs : []),
|
|
135
|
+
[jobsPoll.data],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const selectedJob = useMemo(
|
|
139
|
+
() => jobs.find((job) => String(job?.id || "") === selectedJobId) || null,
|
|
140
|
+
[jobs, selectedJobId],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!selectedJobId) {
|
|
145
|
+
setPromptValue("");
|
|
146
|
+
setSavedPromptValue("");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const prompt = String(selectedJob?.payload?.message || "");
|
|
150
|
+
setPromptValue(prompt);
|
|
151
|
+
setSavedPromptValue(prompt);
|
|
152
|
+
}, [selectedJobId, selectedJob?.payload?.message]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
setRunEntries([]);
|
|
156
|
+
setRunHasMore(false);
|
|
157
|
+
setRunNextOffset(0);
|
|
158
|
+
setRunTotal(0);
|
|
159
|
+
if (!selectedJobId) return;
|
|
160
|
+
runsPoll.refresh();
|
|
161
|
+
}, [selectedJobId, runStatusFilter, runDeliveryFilter]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!selectedJobId) return;
|
|
165
|
+
usagePoll.refresh();
|
|
166
|
+
}, [selectedJobId, usageDays]);
|
|
167
|
+
|
|
168
|
+
const resizeListPanelWithClientX = useCallback((clientX) => {
|
|
169
|
+
const listPanelElement = listPanelRef.current;
|
|
170
|
+
if (!listPanelElement) return;
|
|
171
|
+
const parentBounds =
|
|
172
|
+
listPanelElement.parentElement?.getBoundingClientRect();
|
|
173
|
+
if (!parentBounds) return;
|
|
174
|
+
const nextWidth = clampListPanelWidth(
|
|
175
|
+
Math.round(clientX - parentBounds.left),
|
|
176
|
+
);
|
|
177
|
+
setListPanelWidthPx(nextWidth);
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
const onListResizerPointerDown = useCallback(
|
|
181
|
+
(event) => {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
setIsResizingListPanel(true);
|
|
184
|
+
resizeListPanelWithClientX(event.clientX);
|
|
185
|
+
},
|
|
186
|
+
[resizeListPanelWithClientX],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!isResizingListPanel) return () => {};
|
|
191
|
+
const onPointerMove = (event) => resizeListPanelWithClientX(event.clientX);
|
|
192
|
+
const onPointerUp = () => setIsResizingListPanel(false);
|
|
193
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
194
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
195
|
+
const previousUserSelect = document.body.style.userSelect;
|
|
196
|
+
const previousCursor = document.body.style.cursor;
|
|
197
|
+
document.body.style.userSelect = "none";
|
|
198
|
+
document.body.style.cursor = "col-resize";
|
|
199
|
+
return () => {
|
|
200
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
201
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
202
|
+
document.body.style.userSelect = previousUserSelect;
|
|
203
|
+
document.body.style.cursor = previousCursor;
|
|
204
|
+
};
|
|
205
|
+
}, [isResizingListPanel, resizeListPanelWithClientX]);
|
|
206
|
+
|
|
207
|
+
const selectAllJobs = useCallback(() => {
|
|
208
|
+
onSetLocation("/cron");
|
|
209
|
+
}, [onSetLocation]);
|
|
210
|
+
|
|
211
|
+
const selectJob = useCallback(
|
|
212
|
+
(nextJobId) => {
|
|
213
|
+
onSetLocation(`/cron/${encodeURIComponent(String(nextJobId || ""))}`);
|
|
214
|
+
},
|
|
215
|
+
[onSetLocation],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const refreshAll = useCallback(() => {
|
|
219
|
+
jobsPoll.refresh();
|
|
220
|
+
statusPoll.refresh();
|
|
221
|
+
runsPoll.refresh();
|
|
222
|
+
usagePoll.refresh();
|
|
223
|
+
bulkUsagePoll.refresh();
|
|
224
|
+
bulkRunsPoll.refresh();
|
|
225
|
+
}, [
|
|
226
|
+
bulkRunsPoll.refresh,
|
|
227
|
+
bulkUsagePoll.refresh,
|
|
228
|
+
jobsPoll.refresh,
|
|
229
|
+
runsPoll.refresh,
|
|
230
|
+
statusPoll.refresh,
|
|
231
|
+
usagePoll.refresh,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const runSelectedJobNow = useCallback(async () => {
|
|
235
|
+
if (!selectedJobId || runningJob) return;
|
|
236
|
+
setRunningJob(true);
|
|
237
|
+
try {
|
|
238
|
+
await triggerCronJobRun(selectedJobId);
|
|
239
|
+
showToast("Cron run triggered", "success");
|
|
240
|
+
refreshAll();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
showToast(error.message || "Could not run cron job", "error");
|
|
243
|
+
} finally {
|
|
244
|
+
setRunningJob(false);
|
|
245
|
+
}
|
|
246
|
+
}, [refreshAll, runningJob, selectedJobId]);
|
|
247
|
+
|
|
248
|
+
const setSelectedJobEnabled = useCallback(
|
|
249
|
+
async (enabled) => {
|
|
250
|
+
if (!selectedJobId || togglingJobEnabled) return;
|
|
251
|
+
setTogglingJobEnabled(true);
|
|
252
|
+
try {
|
|
253
|
+
await setCronJobEnabled(selectedJobId, enabled);
|
|
254
|
+
showToast(
|
|
255
|
+
enabled ? "Cron job enabled" : "Cron job disabled",
|
|
256
|
+
"success",
|
|
257
|
+
);
|
|
258
|
+
refreshAll();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
showToast(error.message || "Could not update cron job", "error");
|
|
261
|
+
} finally {
|
|
262
|
+
setTogglingJobEnabled(false);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[refreshAll, selectedJobId, togglingJobEnabled],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const loadMoreRuns = useCallback(async () => {
|
|
269
|
+
if (!selectedJobId || !runHasMore || loadingMoreRuns) return;
|
|
270
|
+
setLoadingMoreRuns(true);
|
|
271
|
+
try {
|
|
272
|
+
const data = await fetchCronJobRuns(selectedJobId, {
|
|
273
|
+
limit: kRunsPageSize,
|
|
274
|
+
offset: runNextOffset,
|
|
275
|
+
status: runStatusFilter,
|
|
276
|
+
deliveryStatus: runDeliveryFilter,
|
|
277
|
+
sortDir: "desc",
|
|
278
|
+
});
|
|
279
|
+
const nextEntries = Array.isArray(data?.runs?.entries)
|
|
280
|
+
? data.runs.entries
|
|
281
|
+
: [];
|
|
282
|
+
setRunEntries((currentValue) => [...currentValue, ...nextEntries]);
|
|
283
|
+
setRunHasMore(!!data?.runs?.hasMore);
|
|
284
|
+
setRunNextOffset(Number(data?.runs?.nextOffset || 0));
|
|
285
|
+
setRunTotal(Number(data?.runs?.total || 0));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
showToast(error.message || "Could not load more runs", "error");
|
|
288
|
+
} finally {
|
|
289
|
+
setLoadingMoreRuns(false);
|
|
290
|
+
}
|
|
291
|
+
}, [
|
|
292
|
+
loadingMoreRuns,
|
|
293
|
+
runDeliveryFilter,
|
|
294
|
+
runHasMore,
|
|
295
|
+
runNextOffset,
|
|
296
|
+
runStatusFilter,
|
|
297
|
+
selectedJobId,
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const savePrompt = useCallback(async () => {
|
|
301
|
+
if (!selectedJobId || savingPrompt) return;
|
|
302
|
+
if (promptValue === savedPromptValue) return;
|
|
303
|
+
setSavingPrompt(true);
|
|
304
|
+
try {
|
|
305
|
+
await updateCronJobPrompt(selectedJobId, promptValue);
|
|
306
|
+
setSavedPromptValue(promptValue);
|
|
307
|
+
showToast("Cron prompt updated", "success");
|
|
308
|
+
refreshAll();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
showToast(error.message || "Could not update prompt", "error");
|
|
311
|
+
} finally {
|
|
312
|
+
setSavingPrompt(false);
|
|
313
|
+
}
|
|
314
|
+
}, [promptValue, refreshAll, savedPromptValue, savingPrompt, selectedJobId]);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
refs: {
|
|
318
|
+
listPanelRef,
|
|
319
|
+
},
|
|
320
|
+
state: {
|
|
321
|
+
jobs,
|
|
322
|
+
jobsError: jobsPoll.error,
|
|
323
|
+
status: statusPoll.data?.status || null,
|
|
324
|
+
statusError: statusPoll.error,
|
|
325
|
+
selectedRouteKey,
|
|
326
|
+
selectedJobId,
|
|
327
|
+
selectedJob,
|
|
328
|
+
listPanelWidthPx,
|
|
329
|
+
isResizingListPanel,
|
|
330
|
+
runEntries,
|
|
331
|
+
runHasMore,
|
|
332
|
+
runNextOffset,
|
|
333
|
+
runTotal,
|
|
334
|
+
runStatusFilter,
|
|
335
|
+
runDeliveryFilter,
|
|
336
|
+
runsError: runsPoll.error,
|
|
337
|
+
loadingMoreRuns,
|
|
338
|
+
usage: usagePoll.data?.usage || null,
|
|
339
|
+
usageError: usagePoll.error,
|
|
340
|
+
usageDays,
|
|
341
|
+
bulkUsageByJobId: bulkUsagePoll.data?.usage?.byJobId || {},
|
|
342
|
+
bulkUsageError: bulkUsagePoll.error,
|
|
343
|
+
bulkRunsByJobId: bulkRunsPoll.data?.runs?.byJobId || {},
|
|
344
|
+
bulkRunsError: bulkRunsPoll.error,
|
|
345
|
+
promptValue,
|
|
346
|
+
savedPromptValue,
|
|
347
|
+
savingPrompt,
|
|
348
|
+
runningJob,
|
|
349
|
+
togglingJobEnabled,
|
|
350
|
+
},
|
|
351
|
+
actions: {
|
|
352
|
+
setRunStatusFilter,
|
|
353
|
+
setRunDeliveryFilter,
|
|
354
|
+
setUsageDays,
|
|
355
|
+
setPromptValue,
|
|
356
|
+
savePrompt,
|
|
357
|
+
refreshAll,
|
|
358
|
+
loadMoreRuns,
|
|
359
|
+
runSelectedJobNow,
|
|
360
|
+
setSelectedJobEnabled,
|
|
361
|
+
selectAllJobs,
|
|
362
|
+
selectJob,
|
|
363
|
+
onListResizerPointerDown,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
};
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { SummaryStatCard } from "../summary-stat-card.js";
|
|
3
4
|
import { buildDoctorPriorityCounts } from "./helpers.js";
|
|
4
5
|
|
|
5
6
|
const html = htm.bind(h);
|
|
6
7
|
|
|
7
|
-
const SummaryCard = ({ title = "", value = 0, toneClassName = "" }) => html`
|
|
8
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
9
|
-
<h3 class="card-label text-xs">${title}</h3>
|
|
10
|
-
<div class=${`text-lg font-semibold mt-1 ${toneClassName}`}>${value}</div>
|
|
11
|
-
</div>
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
8
|
export const DoctorSummaryCards = ({ cards = [] }) => {
|
|
15
9
|
const counts = buildDoctorPriorityCounts(cards);
|
|
16
10
|
return html`
|
|
17
11
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
18
|
-
<${
|
|
19
|
-
<${
|
|
20
|
-
<${
|
|
21
|
-
<${
|
|
12
|
+
<${SummaryStatCard} title="Open Findings" value=${cards.length} />
|
|
13
|
+
<${SummaryStatCard} title="P0" value=${counts.P0} toneClassName="text-red-400" />
|
|
14
|
+
<${SummaryStatCard} title="P1" value=${counts.P1} toneClassName="text-yellow-400" />
|
|
15
|
+
<${SummaryStatCard} title="P2" value=${counts.P2} toneClassName="text-gray-300" />
|
|
22
16
|
</div>
|
|
23
17
|
`;
|
|
24
18
|
};
|
|
@@ -47,14 +47,14 @@ const copyText = async (value) => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const
|
|
50
|
+
const kSetupStepTitles = [
|
|
51
51
|
"Install + Authenticate gcloud",
|
|
52
52
|
"Enable APIs",
|
|
53
53
|
"Create Topic + IAM",
|
|
54
54
|
"Create Push Subscription",
|
|
55
55
|
"Build with your Agent",
|
|
56
56
|
];
|
|
57
|
-
const
|
|
57
|
+
const kTutorialStepTitles = kSetupStepTitles.slice(0, 3);
|
|
58
58
|
const kNoSessionSelectedValue = kNoDestinationSessionValue;
|
|
59
59
|
|
|
60
60
|
const renderCommandBlock = (command = "", onCopy = () => {}) => html`
|
|
@@ -136,6 +136,8 @@ export const GmailSetupWizard = ({
|
|
|
136
136
|
String(clientConfig?.projectId || "").trim() ||
|
|
137
137
|
"<project-id>";
|
|
138
138
|
const hasExistingWebhookSetup = Boolean(clientConfig?.webhookExists);
|
|
139
|
+
const stepTitles = hasExistingWebhookSetup ? kTutorialStepTitles : kSetupStepTitles;
|
|
140
|
+
const totalSteps = stepTitles.length;
|
|
139
141
|
const client =
|
|
140
142
|
String(account?.client || clientConfig?.client || "default").trim() ||
|
|
141
143
|
"default";
|
|
@@ -172,7 +174,7 @@ export const GmailSetupWizard = ({
|
|
|
172
174
|
destination: selectedDestination,
|
|
173
175
|
});
|
|
174
176
|
setWatchEnabled(true);
|
|
175
|
-
setStep((prev) => Math.min(prev + 1,
|
|
177
|
+
setStep((prev) => Math.min(prev + 1, totalSteps - 1));
|
|
176
178
|
} catch (err) {
|
|
177
179
|
setLocalError(err.message || "Could not finish setup");
|
|
178
180
|
}
|
|
@@ -196,7 +198,7 @@ export const GmailSetupWizard = ({
|
|
|
196
198
|
}
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
|
-
setStep((prev) => Math.min(prev + 1,
|
|
201
|
+
setStep((prev) => Math.min(prev + 1, totalSteps - 1));
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
const handleSendToAgent = async () => {
|
|
@@ -240,7 +242,7 @@ export const GmailSetupWizard = ({
|
|
|
240
242
|
</button>
|
|
241
243
|
<div class="text-xs text-gray-500">Gmail Pub / Sub Setup</div>
|
|
242
244
|
<div class="flex items-center gap-1">
|
|
243
|
-
${
|
|
245
|
+
${stepTitles.map(
|
|
244
246
|
(title, idx) => html`
|
|
245
247
|
<div
|
|
246
248
|
class=${`h-1 flex-1 rounded-full transition-colors ${idx <= step ? "bg-accent" : "bg-border"}`}
|
|
@@ -251,7 +253,7 @@ export const GmailSetupWizard = ({
|
|
|
251
253
|
)}
|
|
252
254
|
</div>
|
|
253
255
|
<${PageHeader}
|
|
254
|
-
title=${`Step ${step + 1} of ${
|
|
256
|
+
title=${`Step ${step + 1} of ${totalSteps}: ${stepTitles[step]}`}
|
|
255
257
|
actions=${null}
|
|
256
258
|
/>
|
|
257
259
|
${localError ? html`<div class="text-xs text-red-400">${localError}</div>` : null}
|
|
@@ -341,7 +343,7 @@ export const GmailSetupWizard = ({
|
|
|
341
343
|
: null
|
|
342
344
|
}
|
|
343
345
|
${
|
|
344
|
-
!needsProjectId && step === 3
|
|
346
|
+
!hasExistingWebhookSetup && !needsProjectId && step === 3
|
|
345
347
|
? html`
|
|
346
348
|
${renderCommandBlock(commands?.createSubscription || "", () =>
|
|
347
349
|
handleCopy(commands?.createSubscription || ""),
|
|
@@ -376,7 +378,7 @@ export const GmailSetupWizard = ({
|
|
|
376
378
|
: null
|
|
377
379
|
}
|
|
378
380
|
${
|
|
379
|
-
step === 4
|
|
381
|
+
!hasExistingWebhookSetup && step === 4
|
|
380
382
|
? html`
|
|
381
383
|
<div
|
|
382
384
|
class="rounded-lg border border-border bg-black/20 p-3 space-y-3"
|
|
@@ -469,7 +471,18 @@ export const GmailSetupWizard = ({
|
|
|
469
471
|
/>`
|
|
470
472
|
}
|
|
471
473
|
${
|
|
472
|
-
step
|
|
474
|
+
!hasExistingWebhookSetup && step === totalSteps - 2
|
|
475
|
+
? html`<${ActionButton}
|
|
476
|
+
onClick=${handleFinish}
|
|
477
|
+
disabled=${false}
|
|
478
|
+
loading=${saving}
|
|
479
|
+
idleLabel="Enable watch"
|
|
480
|
+
loadingLabel="Enabling..."
|
|
481
|
+
tone="primary"
|
|
482
|
+
size="md"
|
|
483
|
+
className="w-full justify-center"
|
|
484
|
+
/>`
|
|
485
|
+
: step < totalSteps - 1
|
|
473
486
|
? html`<${ActionButton}
|
|
474
487
|
onClick=${handleNext}
|
|
475
488
|
disabled=${saving || (needsProjectId && !canAdvance)}
|
|
@@ -478,27 +491,14 @@ export const GmailSetupWizard = ({
|
|
|
478
491
|
size="md"
|
|
479
492
|
className="w-full justify-center"
|
|
480
493
|
/>`
|
|
481
|
-
:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
: "Enabling..."}
|
|
490
|
-
tone="primary"
|
|
491
|
-
size="md"
|
|
492
|
-
className="w-full justify-center"
|
|
493
|
-
/>`
|
|
494
|
-
: html`<${ActionButton}
|
|
495
|
-
onClick=${onClose}
|
|
496
|
-
disabled=${saving || sendingToAgent}
|
|
497
|
-
idleLabel="Done"
|
|
498
|
-
tone="secondary"
|
|
499
|
-
size="md"
|
|
500
|
-
className="w-full justify-center"
|
|
501
|
-
/>`
|
|
494
|
+
: html`<${ActionButton}
|
|
495
|
+
onClick=${onClose}
|
|
496
|
+
disabled=${saving || sendingToAgent}
|
|
497
|
+
idleLabel="Done"
|
|
498
|
+
tone="secondary"
|
|
499
|
+
size="md"
|
|
500
|
+
className="w-full justify-center"
|
|
501
|
+
/>`
|
|
502
502
|
}
|
|
503
503
|
</div>
|
|
504
504
|
</${ModalShell}>
|
|
@@ -373,7 +373,7 @@ export const Google = ({
|
|
|
373
373
|
if (!account) return;
|
|
374
374
|
const client = String(account.client || "default").trim() || "default";
|
|
375
375
|
const clientConfig = clientConfigByClient.get(client);
|
|
376
|
-
if (!clientConfig?.configured) {
|
|
376
|
+
if (!clientConfig?.configured || !clientConfig?.webhookExists) {
|
|
377
377
|
openGmailSetupWizard(accountId);
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
@@ -415,3 +415,16 @@ export const RestartLineIcon = ({ className = "" }) => html`
|
|
|
415
415
|
/>
|
|
416
416
|
</svg>
|
|
417
417
|
`;
|
|
418
|
+
|
|
419
|
+
export const ErrorWarningLineIcon = ({ className = "" }) => html`
|
|
420
|
+
<svg
|
|
421
|
+
class=${className}
|
|
422
|
+
viewBox="0 0 24 24"
|
|
423
|
+
fill="currentColor"
|
|
424
|
+
aria-hidden="true"
|
|
425
|
+
>
|
|
426
|
+
<path
|
|
427
|
+
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z"
|
|
428
|
+
/>
|
|
429
|
+
</svg>
|
|
430
|
+
`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
const kPillBaseClassName =
|
|
7
|
+
"inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-medium transition-colors";
|
|
8
|
+
const kPillActiveClassName =
|
|
9
|
+
"border-cyan-500/40 bg-cyan-500/10 text-cyan-200 shadow-[0_0_0_1px_rgba(34,211,238,0.08)]";
|
|
10
|
+
const kPillInactiveClassName =
|
|
11
|
+
"border-border bg-black/20 text-gray-500 hover:border-gray-500 hover:text-gray-300";
|
|
12
|
+
|
|
13
|
+
export const PillTabs = ({
|
|
14
|
+
tabs = [],
|
|
15
|
+
activeTab = "",
|
|
16
|
+
onSelectTab = () => {},
|
|
17
|
+
className = "flex items-center gap-2",
|
|
18
|
+
} = {}) => html`
|
|
19
|
+
<div class=${className}>
|
|
20
|
+
${tabs.map(
|
|
21
|
+
(tab) => html`
|
|
22
|
+
<button
|
|
23
|
+
key=${String(tab?.value || "")}
|
|
24
|
+
type="button"
|
|
25
|
+
class=${`${kPillBaseClassName} ${activeTab === tab?.value ? kPillActiveClassName : kPillInactiveClassName}`}
|
|
26
|
+
onclick=${() => onSelectTab(tab?.value)}
|
|
27
|
+
>
|
|
28
|
+
${String(tab?.label || tab?.value || "")}
|
|
29
|
+
</button>
|
|
30
|
+
`,
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
const kEnterDurationMs = 260;
|
|
8
|
+
const kExitDurationMs = 200;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrapper that pop-animates children in/out based on `visible`.
|
|
12
|
+
* Use for header save/cancel actions or any contextual action group.
|
|
13
|
+
*
|
|
14
|
+
* @param {boolean} props.visible Whether the actions should be shown.
|
|
15
|
+
* @param {string} [props.className] Extra classes on the container.
|
|
16
|
+
* @param {preact.ComponentChildren} props.children
|
|
17
|
+
*/
|
|
18
|
+
export const PopActions = ({ visible = false, className = "", children }) => {
|
|
19
|
+
const [phase, setPhase] = useState(visible ? "visible" : "hidden");
|
|
20
|
+
const enterTimerRef = useRef(null);
|
|
21
|
+
const exitTimerRef = useRef(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
clearTimeout(enterTimerRef.current);
|
|
25
|
+
clearTimeout(exitTimerRef.current);
|
|
26
|
+
if (visible) {
|
|
27
|
+
if (phase !== "visible") {
|
|
28
|
+
setPhase("entering");
|
|
29
|
+
enterTimerRef.current = setTimeout(
|
|
30
|
+
() => setPhase("visible"),
|
|
31
|
+
kEnterDurationMs,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} else if (phase !== "hidden") {
|
|
35
|
+
setPhase("exiting");
|
|
36
|
+
exitTimerRef.current = setTimeout(() => setPhase("hidden"), kExitDurationMs);
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(enterTimerRef.current);
|
|
40
|
+
clearTimeout(exitTimerRef.current);
|
|
41
|
+
};
|
|
42
|
+
}, [visible, phase]);
|
|
43
|
+
|
|
44
|
+
const phaseClass =
|
|
45
|
+
phase === "entering"
|
|
46
|
+
? "ac-pop-actions-in"
|
|
47
|
+
: phase === "exiting"
|
|
48
|
+
? "ac-pop-actions-out"
|
|
49
|
+
: phase === "visible"
|
|
50
|
+
? "ac-pop-actions-visible"
|
|
51
|
+
: "ac-pop-actions-hidden";
|
|
52
|
+
|
|
53
|
+
return html`
|
|
54
|
+
<div class=${`ac-pop-actions ${phaseClass} ${className}`.trim()}>
|
|
55
|
+
${children}
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
};
|
|
@@ -10,7 +10,9 @@ export const AgentsRoute = ({
|
|
|
10
10
|
saving = false,
|
|
11
11
|
agentsActions = {},
|
|
12
12
|
selectedAgentId = "",
|
|
13
|
+
activeTab = "overview",
|
|
13
14
|
onSelectAgent = () => {},
|
|
15
|
+
onSelectTab = () => {},
|
|
14
16
|
onNavigateToBrowseFile = () => {},
|
|
15
17
|
onSetLocation = () => {},
|
|
16
18
|
}) => html`
|
|
@@ -20,7 +22,9 @@ export const AgentsRoute = ({
|
|
|
20
22
|
saving=${saving}
|
|
21
23
|
agentsActions=${agentsActions}
|
|
22
24
|
selectedAgentId=${selectedAgentId}
|
|
25
|
+
activeTab=${activeTab}
|
|
23
26
|
onSelectAgent=${onSelectAgent}
|
|
27
|
+
onSelectTab=${onSelectTab}
|
|
24
28
|
onNavigateToBrowseFile=${onNavigateToBrowseFile}
|
|
25
29
|
onSetLocation=${onSetLocation}
|
|
26
30
|
/>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { CronTab } from "../cron-tab/index.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const CronRoute = ({ jobId = "", onSetLocation = () => {} }) => html`
|
|
8
|
+
<${CronTab} jobId=${jobId} onSetLocation=${onSetLocation} />
|
|
9
|
+
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AgentsRoute } from "./agents-route.js";
|
|
2
2
|
export { BrowseRoute } from "./browse-route.js";
|
|
3
|
+
export { CronRoute } from "./cron-route.js";
|
|
3
4
|
export { DoctorRoute } from "./doctor-route.js";
|
|
4
5
|
export { EnvarsRoute } from "./envars-route.js";
|
|
5
6
|
export { GeneralRoute } from "./general-route.js";
|