@chrysb/alphaclaw 0.7.2-beta.1 → 0.7.2-beta.3
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/theme.css +12 -1
- package/lib/public/js/app.js +10 -2
- package/lib/public/js/components/cron-tab/cron-job-detail.js +18 -2
- package/lib/public/js/components/cron-tab/cron-job-list.js +43 -0
- package/lib/public/js/components/cron-tab/cron-job-trends-panel.js +319 -0
- package/lib/public/js/components/cron-tab/cron-job-usage.js +22 -8
- package/lib/public/js/components/cron-tab/cron-overview.js +17 -13
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +1 -1
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +29 -12
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +109 -53
- package/lib/public/js/components/cron-tab/index.js +6 -0
- package/lib/public/js/components/cron-tab/use-cron-tab.js +51 -0
- package/lib/public/js/components/icons.js +11 -0
- package/lib/public/js/components/nodes-tab/browser-attach/index.js +85 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/index.js +324 -0
- package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +25 -0
- package/lib/public/js/components/nodes-tab/exec-allowlist/index.js +89 -0
- package/lib/public/js/components/nodes-tab/exec-allowlist/use-exec-allowlist.js +78 -0
- package/lib/public/js/components/nodes-tab/exec-config/index.js +118 -0
- package/lib/public/js/components/nodes-tab/exec-config/use-exec-config.js +79 -0
- package/lib/public/js/components/nodes-tab/index.js +55 -0
- package/lib/public/js/components/nodes-tab/setup-wizard/index.js +243 -0
- package/lib/public/js/components/nodes-tab/setup-wizard/use-setup-wizard.js +161 -0
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +36 -0
- package/lib/public/js/components/onboarding/welcome-import-step.js +4 -3
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/routes/nodes-route.js +11 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +11 -3
- package/lib/public/js/lib/api.js +70 -0
- package/lib/public/js/lib/app-navigation.js +2 -0
- package/lib/public/js/lib/format.js +50 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/cron-service.js +230 -1
- package/lib/server/init/register-server-routes.js +8 -0
- package/lib/server/openclaw-version.js +5 -1
- package/lib/server/routes/cron.js +11 -0
- package/lib/server/routes/nodes.js +338 -0
- package/lib/server/routes/pairings.js +2 -2
- package/lib/server/webhook-middleware.js +92 -3
- package/package.json +2 -2
|
@@ -60,7 +60,9 @@ const renderEntrySummaryRow = ({ runEntry = {}, variant = "overview" }) => {
|
|
|
60
60
|
: hasRunTitle
|
|
61
61
|
? html`
|
|
62
62
|
<span class="inline-flex items-center gap-2 min-w-0">
|
|
63
|
-
<span class="truncate text-xs text-gray-300"
|
|
63
|
+
<span class="truncate text-xs text-gray-300"
|
|
64
|
+
>${runTitle}</span
|
|
65
|
+
>
|
|
64
66
|
<span class="text-xs text-gray-500 shrink-0">
|
|
65
67
|
${formatRowTimestamp(runEntry.ts, variant)}
|
|
66
68
|
</span>
|
|
@@ -68,19 +70,26 @@ const renderEntrySummaryRow = ({ runEntry = {}, variant = "overview" }) => {
|
|
|
68
70
|
`
|
|
69
71
|
: html`
|
|
70
72
|
<span class="truncate text-xs text-gray-300">
|
|
71
|
-
${runEntry.jobId} -
|
|
73
|
+
${runEntry.jobId} -
|
|
74
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
72
75
|
</span>
|
|
73
76
|
`}
|
|
74
77
|
</span>
|
|
75
78
|
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
76
79
|
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
77
|
-
<span class="text-gray-400"
|
|
80
|
+
<span class="text-gray-400"
|
|
81
|
+
>${formatDurationCompactMs(runEntry.durationMs)}</span
|
|
82
|
+
>
|
|
78
83
|
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
79
84
|
${isDetail
|
|
80
|
-
? html`<span class="text-gray-500"
|
|
85
|
+
? html`<span class="text-gray-500"
|
|
86
|
+
>${runDeliveryLabel(runEntry)}</span
|
|
87
|
+
>`
|
|
81
88
|
: html`
|
|
82
89
|
<span class="text-gray-500">
|
|
83
|
-
${runEstimatedCost == null
|
|
90
|
+
${runEstimatedCost == null
|
|
91
|
+
? "—"
|
|
92
|
+
: `~${formatCost(runEstimatedCost)}`}
|
|
84
93
|
</span>
|
|
85
94
|
`}
|
|
86
95
|
</span>
|
|
@@ -102,7 +111,8 @@ const getCollapsedGroupAggregates = (entries = []) =>
|
|
|
102
111
|
);
|
|
103
112
|
const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
104
113
|
const entries = Array.isArray(row?.entries) ? row.entries : [];
|
|
105
|
-
const { totalTokens, totalCost, hasAnyCost } =
|
|
114
|
+
const { totalTokens, totalCost, hasAnyCost } =
|
|
115
|
+
getCollapsedGroupAggregates(entries);
|
|
106
116
|
const timeRangeLabel = `${formatOverviewTimestamp(row.oldestTs)} - ${formatOverviewTimestamp(row.newestTs)}`;
|
|
107
117
|
return html`
|
|
108
118
|
<details
|
|
@@ -117,21 +127,25 @@ const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
|
117
127
|
<span class="truncate text-xs text-gray-300">
|
|
118
128
|
${row.jobName} - ${formatTokenCount(row.count)} runs
|
|
119
129
|
</span>
|
|
120
|
-
<span class="text-xs text-gray-500 shrink-0"
|
|
130
|
+
<span class="text-xs text-gray-500 shrink-0"
|
|
131
|
+
>${timeRangeLabel}</span
|
|
132
|
+
>
|
|
121
133
|
</span>
|
|
122
134
|
</span>
|
|
123
135
|
<span class="inline-flex items-center gap-3 shrink-0 text-xs">
|
|
124
|
-
<span class="text-gray-400"
|
|
136
|
+
<span class="text-gray-400"
|
|
137
|
+
>${formatTokenCount(totalTokens)} tk</span
|
|
138
|
+
>
|
|
125
139
|
<span class="text-gray-500">
|
|
126
140
|
${hasAnyCost ? `~${formatCost(totalCost)}` : "—"}
|
|
127
141
|
</span>
|
|
128
142
|
</span>
|
|
129
143
|
</div>
|
|
130
144
|
</summary>
|
|
131
|
-
<div class="
|
|
145
|
+
<div class="border-t border-border pb-2 text-xs">
|
|
132
146
|
${entries.length > 0
|
|
133
147
|
? html`
|
|
134
|
-
<div class="ac-history-list">
|
|
148
|
+
<div class="ac-history-list ac-history-list-tight">
|
|
135
149
|
${entries.map((runEntry, entryIndex) =>
|
|
136
150
|
renderEntryRow({
|
|
137
151
|
row: {
|
|
@@ -142,6 +156,8 @@ const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
|
142
156
|
variant: "overview",
|
|
143
157
|
onSelectJob,
|
|
144
158
|
showOpenJobButton: false,
|
|
159
|
+
itemClassName:
|
|
160
|
+
"ac-history-item ac-history-item-flat border-b border-border rounded-none",
|
|
145
161
|
}),
|
|
146
162
|
)}
|
|
147
163
|
</div>
|
|
@@ -149,7 +165,7 @@ const renderCollapsedGroupRow = ({ row, rowIndex, onSelectJob = () => {} }) => {
|
|
|
149
165
|
: null}
|
|
150
166
|
${row?.jobId
|
|
151
167
|
? html`
|
|
152
|
-
<div>
|
|
168
|
+
<div class="px-2.5 pt-2 pb-0.5">
|
|
153
169
|
<button
|
|
154
170
|
type="button"
|
|
155
171
|
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
@@ -170,6 +186,7 @@ const renderEntryRow = ({
|
|
|
170
186
|
variant = "overview",
|
|
171
187
|
onSelectJob = () => {},
|
|
172
188
|
showOpenJobButton = false,
|
|
189
|
+
itemClassName = "ac-history-item",
|
|
173
190
|
}) => {
|
|
174
191
|
const runEntry = row?.entry || row || {};
|
|
175
192
|
const runUsage = runEntry?.usage || {};
|
|
@@ -184,7 +201,7 @@ const renderEntryRow = ({
|
|
|
184
201
|
return html`
|
|
185
202
|
<details
|
|
186
203
|
key=${`entry:${rowIndex}:${runEntry.ts}:${runEntry.jobId || ""}`}
|
|
187
|
-
class
|
|
204
|
+
class=${itemClassName}
|
|
188
205
|
>
|
|
189
206
|
<summary class="ac-history-summary">
|
|
190
207
|
<div class="inline-flex items-center gap-2 min-w-0 w-full">
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { formatChartBucketLabel } from "../../lib/format.js";
|
|
4
5
|
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formatCost,
|
|
8
|
+
formatTokenCount,
|
|
9
|
+
getCronRunEstimatedCost,
|
|
10
|
+
getCronRunTotalTokens,
|
|
11
|
+
} from "./cron-helpers.js";
|
|
6
12
|
|
|
7
13
|
const html = htm.bind(h);
|
|
8
14
|
|
|
@@ -15,6 +21,14 @@ const kRanges = [
|
|
|
15
21
|
{ label: "7d", value: kRange7d },
|
|
16
22
|
{ label: "30d", value: kRange30d },
|
|
17
23
|
];
|
|
24
|
+
const kMetricOutcomes = "outcomes";
|
|
25
|
+
const kMetricTokens = "tokens";
|
|
26
|
+
const kMetricCost = "cost";
|
|
27
|
+
const kMetricOptions = [
|
|
28
|
+
{ label: "outcomes", value: kMetricOutcomes },
|
|
29
|
+
{ label: "tokens", value: kMetricTokens },
|
|
30
|
+
{ label: "cost", value: kMetricCost },
|
|
31
|
+
];
|
|
18
32
|
|
|
19
33
|
const startOfLocalDayMs = (valueMs) => {
|
|
20
34
|
const dateValue = new Date(valueMs);
|
|
@@ -34,9 +48,7 @@ const getBucketConfig = (range = kRange7d) => {
|
|
|
34
48
|
bucketCount: 24,
|
|
35
49
|
bucketMs: 60 * 60 * 1000,
|
|
36
50
|
formatLabel: (valueMs) =>
|
|
37
|
-
|
|
38
|
-
hour: "numeric",
|
|
39
|
-
}),
|
|
51
|
+
formatChartBucketLabel(valueMs, { range: kRange24h, valueType: "epoch-ms" }),
|
|
40
52
|
showLabel: (_, index, total) => index % 3 === 0 || index === total - 1,
|
|
41
53
|
alignToLocalDay: false,
|
|
42
54
|
};
|
|
@@ -45,7 +57,8 @@ const getBucketConfig = (range = kRange7d) => {
|
|
|
45
57
|
return {
|
|
46
58
|
bucketCount: 30,
|
|
47
59
|
bucketMs: 24 * 60 * 60 * 1000,
|
|
48
|
-
formatLabel: (valueMs) =>
|
|
60
|
+
formatLabel: (valueMs) =>
|
|
61
|
+
formatChartBucketLabel(valueMs, { range: kRange30d, valueType: "epoch-ms" }),
|
|
49
62
|
showLabel: (_, index, total) => index % 5 === 0 || index === total - 1,
|
|
50
63
|
alignToLocalDay: true,
|
|
51
64
|
};
|
|
@@ -53,12 +66,8 @@ const getBucketConfig = (range = kRange7d) => {
|
|
|
53
66
|
return {
|
|
54
67
|
bucketCount: 7,
|
|
55
68
|
bucketMs: 24 * 60 * 60 * 1000,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
weekday: "short",
|
|
59
|
-
month: "numeric",
|
|
60
|
-
day: "numeric",
|
|
61
|
-
}),
|
|
69
|
+
formatLabel: (valueMs) =>
|
|
70
|
+
formatChartBucketLabel(valueMs, { range: kRange7d, valueType: "epoch-ms" }),
|
|
62
71
|
showLabel: () => true,
|
|
63
72
|
alignToLocalDay: true,
|
|
64
73
|
};
|
|
@@ -86,6 +95,7 @@ const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRan
|
|
|
86
95
|
ok: 0,
|
|
87
96
|
error: 0,
|
|
88
97
|
skipped: 0,
|
|
98
|
+
totalTokens: 0,
|
|
89
99
|
totalCost: 0,
|
|
90
100
|
costCount: 0,
|
|
91
101
|
};
|
|
@@ -110,6 +120,7 @@ const buildTrendData = ({ bulkRunsByJobId = {}, nowMs = Date.now(), range = kRan
|
|
|
110
120
|
if (!Number.isFinite(Number(bucketIndex))) return;
|
|
111
121
|
if (bucketIndex < 0 || bucketIndex >= config.bucketCount) return;
|
|
112
122
|
points[bucketIndex][status] += 1;
|
|
123
|
+
points[bucketIndex].totalTokens += getCronRunTotalTokens(entry);
|
|
113
124
|
const estimatedCost = getCronRunEstimatedCost(entry);
|
|
114
125
|
if (estimatedCost != null) {
|
|
115
126
|
points[bucketIndex].totalCost += estimatedCost;
|
|
@@ -142,6 +153,7 @@ export const CronRunsTrendCard = ({
|
|
|
142
153
|
}) => {
|
|
143
154
|
const chartCanvasRef = useRef(null);
|
|
144
155
|
const chartInstanceRef = useRef(null);
|
|
156
|
+
const [metric, setMetric] = useState(kMetricOutcomes);
|
|
145
157
|
const [range, setRange] = useState(
|
|
146
158
|
initialRange === kRange30d
|
|
147
159
|
? kRange30d
|
|
@@ -176,48 +188,74 @@ export const CronRunsTrendCard = ({
|
|
|
176
188
|
const fullAlpha = "0.86";
|
|
177
189
|
const isDimmed = (index) => selectedPointIndex >= 0 && selectedPointIndex !== index;
|
|
178
190
|
const labels = trend.points.map((point) => (point.showLabel ? point.label : ""));
|
|
191
|
+
if (metric === kMetricOutcomes) {
|
|
192
|
+
return {
|
|
193
|
+
labels,
|
|
194
|
+
datasets: [
|
|
195
|
+
{
|
|
196
|
+
label: "ok",
|
|
197
|
+
data: trend.points.map((point) => Number(point.ok || 0)),
|
|
198
|
+
stack: "outcomes",
|
|
199
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
200
|
+
`rgba(34,255,170,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
201
|
+
borderColor: trend.points.map((_, index) =>
|
|
202
|
+
`rgba(34,255,170,${isDimmed(index) ? "0.35" : "1"})`),
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderRadius: 0,
|
|
205
|
+
borderSkipped: false,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: "error",
|
|
209
|
+
data: trend.points.map((point) => Number(point.error || 0)),
|
|
210
|
+
stack: "outcomes",
|
|
211
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
212
|
+
`rgba(255,74,138,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
213
|
+
borderColor: trend.points.map((_, index) =>
|
|
214
|
+
`rgba(255,74,138,${isDimmed(index) ? "0.35" : "1"})`),
|
|
215
|
+
borderWidth: 1,
|
|
216
|
+
borderRadius: 0,
|
|
217
|
+
borderSkipped: false,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
label: "skipped",
|
|
221
|
+
data: trend.points.map((point) => Number(point.skipped || 0)),
|
|
222
|
+
stack: "outcomes",
|
|
223
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
224
|
+
`rgba(255,214,64,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
225
|
+
borderColor: trend.points.map((_, index) =>
|
|
226
|
+
`rgba(255,214,64,${isDimmed(index) ? "0.35" : "1"})`),
|
|
227
|
+
borderWidth: 1,
|
|
228
|
+
borderRadius: 0,
|
|
229
|
+
borderSkipped: false,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const datasetLabel = metric === kMetricTokens ? "tokens" : "cost";
|
|
179
235
|
return {
|
|
180
236
|
labels,
|
|
181
237
|
datasets: [
|
|
182
238
|
{
|
|
183
|
-
label:
|
|
184
|
-
data: trend.points.map((point) =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
borderColor: trend.points.map((_, index) =>
|
|
189
|
-
`rgba(34,255,170,${isDimmed(index) ? "0.35" : "1"})`),
|
|
190
|
-
borderWidth: 1,
|
|
191
|
-
borderRadius: 0,
|
|
192
|
-
borderSkipped: false,
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
label: "error",
|
|
196
|
-
data: trend.points.map((point) => Number(point.error || 0)),
|
|
197
|
-
stack: "outcomes",
|
|
198
|
-
backgroundColor: trend.points.map((_, index) =>
|
|
199
|
-
`rgba(255,74,138,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
200
|
-
borderColor: trend.points.map((_, index) =>
|
|
201
|
-
`rgba(255,74,138,${isDimmed(index) ? "0.35" : "1"})`),
|
|
202
|
-
borderWidth: 1,
|
|
203
|
-
borderRadius: 0,
|
|
204
|
-
borderSkipped: false,
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
label: "skipped",
|
|
208
|
-
data: trend.points.map((point) => Number(point.skipped || 0)),
|
|
209
|
-
stack: "outcomes",
|
|
239
|
+
label: datasetLabel,
|
|
240
|
+
data: trend.points.map((point) =>
|
|
241
|
+
metric === kMetricTokens
|
|
242
|
+
? Number(point.totalTokens || 0)
|
|
243
|
+
: Number(point.totalCost || 0)),
|
|
210
244
|
backgroundColor: trend.points.map((_, index) =>
|
|
211
|
-
|
|
245
|
+
metric === kMetricTokens
|
|
246
|
+
? `rgba(34,211,238,${isDimmed(index) ? dimAlpha : "0.72"})`
|
|
247
|
+
: `rgba(167,139,250,${isDimmed(index) ? dimAlpha : "0.72"})`),
|
|
212
248
|
borderColor: trend.points.map((_, index) =>
|
|
213
|
-
|
|
249
|
+
metric === kMetricTokens
|
|
250
|
+
? `rgba(34,211,238,${isDimmed(index) ? "0.35" : "1"})`
|
|
251
|
+
: `rgba(167,139,250,${isDimmed(index) ? "0.35" : "1"})`),
|
|
214
252
|
borderWidth: 1,
|
|
215
253
|
borderRadius: 0,
|
|
216
254
|
borderSkipped: false,
|
|
217
255
|
},
|
|
218
256
|
],
|
|
219
257
|
};
|
|
220
|
-
}, [selectedPointIndex, trend.points]);
|
|
258
|
+
}, [metric, selectedPointIndex, trend.points]);
|
|
221
259
|
|
|
222
260
|
useEffect(() => {
|
|
223
261
|
const canvas = chartCanvasRef.current;
|
|
@@ -266,7 +304,7 @@ export const CronRunsTrendCard = ({
|
|
|
266
304
|
},
|
|
267
305
|
scales: {
|
|
268
306
|
x: {
|
|
269
|
-
stacked:
|
|
307
|
+
stacked: metric === kMetricOutcomes,
|
|
270
308
|
grid: { color: "rgba(148,163,184,0.08)" },
|
|
271
309
|
ticks: {
|
|
272
310
|
color: "rgba(156,163,175,1)",
|
|
@@ -275,12 +313,16 @@ export const CronRunsTrendCard = ({
|
|
|
275
313
|
},
|
|
276
314
|
},
|
|
277
315
|
y: {
|
|
278
|
-
stacked:
|
|
316
|
+
stacked: metric === kMetricOutcomes,
|
|
279
317
|
beginAtZero: true,
|
|
280
318
|
grid: { color: "rgba(148,163,184,0.12)" },
|
|
281
319
|
ticks: {
|
|
282
|
-
precision: 0,
|
|
320
|
+
precision: metric === kMetricCost ? undefined : 0,
|
|
283
321
|
color: "rgba(156,163,175,1)",
|
|
322
|
+
callback: (value) =>
|
|
323
|
+
metric === kMetricCost
|
|
324
|
+
? formatCost(Number(value || 0))
|
|
325
|
+
: formatTokenCount(Number(value || 0)),
|
|
284
326
|
},
|
|
285
327
|
},
|
|
286
328
|
},
|
|
@@ -296,14 +338,21 @@ export const CronRunsTrendCard = ({
|
|
|
296
338
|
tooltip: {
|
|
297
339
|
callbacks: {
|
|
298
340
|
title: (items) => String(items?.[0]?.label || ""),
|
|
299
|
-
label: (context) =>
|
|
341
|
+
label: (context) => {
|
|
342
|
+
const value = Number(context.parsed.y || 0);
|
|
343
|
+
if (metric === kMetricCost) {
|
|
344
|
+
return `${context.dataset.label}: ${formatCost(value)}`;
|
|
345
|
+
}
|
|
346
|
+
return `${context.dataset.label}: ${formatTokenCount(value)}`;
|
|
347
|
+
},
|
|
300
348
|
footer: (items) => {
|
|
301
349
|
const index = Number(items?.[0]?.dataIndex);
|
|
302
350
|
const point = trend.points[index];
|
|
303
351
|
if (!point) return "";
|
|
304
352
|
const costLabel =
|
|
305
353
|
point.costCount > 0 ? `~${formatCost(point.totalCost)}` : "—";
|
|
306
|
-
|
|
354
|
+
const tokensLabel = formatTokenCount(point.totalTokens || 0);
|
|
355
|
+
return `runs: ${point.total}\ntokens: ${tokensLabel}\ncost: ${costLabel}`;
|
|
307
356
|
},
|
|
308
357
|
},
|
|
309
358
|
},
|
|
@@ -316,17 +365,24 @@ export const CronRunsTrendCard = ({
|
|
|
316
365
|
chartInstanceRef.current = null;
|
|
317
366
|
}
|
|
318
367
|
};
|
|
319
|
-
}, [chartData, onBucketFilterChange, range, selectedBucketKey, trend.points]);
|
|
368
|
+
}, [chartData, metric, onBucketFilterChange, range, selectedBucketKey, trend.points]);
|
|
320
369
|
|
|
321
370
|
return html`
|
|
322
371
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
323
372
|
<div class="flex items-center justify-between gap-2">
|
|
324
|
-
<h3 class="card-label cron-calendar-title">
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
373
|
+
<h3 class="card-label cron-calendar-title">Trends</h3>
|
|
374
|
+
<div class="flex items-center gap-2">
|
|
375
|
+
<${SegmentedControl}
|
|
376
|
+
options=${kMetricOptions}
|
|
377
|
+
value=${metric}
|
|
378
|
+
onChange=${setMetric}
|
|
379
|
+
/>
|
|
380
|
+
<${SegmentedControl}
|
|
381
|
+
options=${kRanges}
|
|
382
|
+
value=${range}
|
|
383
|
+
onChange=${setRange}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
330
386
|
</div>
|
|
331
387
|
<div class="h-40">
|
|
332
388
|
<canvas ref=${chartCanvasRef}></canvas>
|
|
@@ -176,6 +176,7 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
176
176
|
<${CronJobDetail}
|
|
177
177
|
job=${state.selectedJob}
|
|
178
178
|
runEntries=${state.runEntries}
|
|
179
|
+
filteredRunEntries=${state.filteredRunEntries}
|
|
179
180
|
runTotal=${state.runTotal}
|
|
180
181
|
runHasMore=${state.runHasMore}
|
|
181
182
|
loadingMoreRuns=${state.loadingMoreRuns}
|
|
@@ -187,8 +188,13 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
187
188
|
onToggleEnabled=${actions.setSelectedJobEnabled}
|
|
188
189
|
togglingJobEnabled=${state.togglingJobEnabled}
|
|
189
190
|
usage=${state.usage}
|
|
191
|
+
jobTrends=${state.jobTrends}
|
|
192
|
+
jobTrendRange=${state.jobTrendRange}
|
|
193
|
+
selectedJobTrendBucketFilter=${state.selectedJobTrendBucketFilter}
|
|
190
194
|
usageDays=${state.usageDays}
|
|
191
195
|
onSetUsageDays=${actions.setUsageDays}
|
|
196
|
+
onSetJobTrendRange=${actions.setJobTrendRange}
|
|
197
|
+
onSetSelectedJobTrendBucketFilter=${actions.setSelectedJobTrendBucketFilter}
|
|
192
198
|
promptValue=${state.promptValue}
|
|
193
199
|
savedPromptValue=${state.savedPromptValue}
|
|
194
200
|
onChangePrompt=${actions.setPromptValue}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
fetchCronBulkRuns,
|
|
12
12
|
fetchCronBulkUsage,
|
|
13
13
|
fetchCronJobRuns,
|
|
14
|
+
fetchCronJobTrends,
|
|
14
15
|
fetchCronJobs,
|
|
15
16
|
fetchCronJobUsage,
|
|
16
17
|
fetchCronStatus,
|
|
@@ -30,6 +31,9 @@ const kListPanelWidthUiSettingKey = "cronListPanelWidthPx";
|
|
|
30
31
|
const kRunsPageSize = 25;
|
|
31
32
|
const kCalendarUsageDays = 30;
|
|
32
33
|
const kCalendarPastDays = 30;
|
|
34
|
+
const kTrendRange24h = "24h";
|
|
35
|
+
const kTrendRange7d = "7d";
|
|
36
|
+
const kTrendRange30d = "30d";
|
|
33
37
|
const kRoutingDefaults = {
|
|
34
38
|
sessionTarget: "main",
|
|
35
39
|
wakeMode: "now",
|
|
@@ -79,6 +83,8 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
79
83
|
const [togglingJobEnabled, setTogglingJobEnabled] = useState(false);
|
|
80
84
|
const [routingDraft, setRoutingDraft] = useState(kRoutingDefaults);
|
|
81
85
|
const [usageDays, setUsageDays] = useState(30);
|
|
86
|
+
const [jobTrendRange, setJobTrendRange] = useState(kTrendRange7d);
|
|
87
|
+
const [selectedJobTrendBucketFilter, setSelectedJobTrendBucketFilter] = useState(null);
|
|
82
88
|
const {
|
|
83
89
|
sessions: deliverySessions,
|
|
84
90
|
loading: loadingDeliverySessions,
|
|
@@ -122,6 +128,14 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
122
128
|
60000,
|
|
123
129
|
{ enabled: !!selectedJobId },
|
|
124
130
|
);
|
|
131
|
+
const trendsPoll = usePolling(
|
|
132
|
+
() => {
|
|
133
|
+
if (!selectedJobId) return Promise.resolve({ ok: true, trends: null });
|
|
134
|
+
return fetchCronJobTrends(selectedJobId, { range: jobTrendRange });
|
|
135
|
+
},
|
|
136
|
+
60000,
|
|
137
|
+
{ enabled: !!selectedJobId },
|
|
138
|
+
);
|
|
125
139
|
const bulkUsagePoll = usePolling(
|
|
126
140
|
() => fetchCronBulkUsage({ days: kCalendarUsageDays }),
|
|
127
141
|
60000,
|
|
@@ -201,6 +215,29 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
201
215
|
if (!selectedJobId) return;
|
|
202
216
|
usagePoll.refresh();
|
|
203
217
|
}, [selectedJobId, usageDays]);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!selectedJobId) return;
|
|
220
|
+
setSelectedJobTrendBucketFilter(null);
|
|
221
|
+
trendsPoll.refresh();
|
|
222
|
+
}, [jobTrendRange, selectedJobId]);
|
|
223
|
+
const filteredRunEntries = useMemo(() => {
|
|
224
|
+
const entries = Array.isArray(runEntries) ? runEntries : [];
|
|
225
|
+
const filterValue = selectedJobTrendBucketFilter;
|
|
226
|
+
if (!filterValue) return entries;
|
|
227
|
+
const startMs = Number(filterValue?.startMs || 0);
|
|
228
|
+
const endMs = Number(filterValue?.endMs || 0);
|
|
229
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) {
|
|
230
|
+
return entries;
|
|
231
|
+
}
|
|
232
|
+
return entries.filter((entry) => {
|
|
233
|
+
const timestampMs = Number(entry?.ts || 0);
|
|
234
|
+
return (
|
|
235
|
+
Number.isFinite(timestampMs) &&
|
|
236
|
+
timestampMs >= startMs &&
|
|
237
|
+
timestampMs < endMs
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
}, [runEntries, selectedJobTrendBucketFilter]);
|
|
204
241
|
|
|
205
242
|
const resizeListPanelWithClientX = useCallback((clientX) => {
|
|
206
243
|
const listPanelElement = listPanelRef.current;
|
|
@@ -257,6 +294,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
257
294
|
statusPoll.refresh();
|
|
258
295
|
runsPoll.refresh();
|
|
259
296
|
usagePoll.refresh();
|
|
297
|
+
trendsPoll.refresh();
|
|
260
298
|
bulkUsagePoll.refresh();
|
|
261
299
|
bulkRunsPoll.refresh();
|
|
262
300
|
}, [
|
|
@@ -265,6 +303,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
265
303
|
jobsPoll.refresh,
|
|
266
304
|
runsPoll.refresh,
|
|
267
305
|
statusPoll.refresh,
|
|
306
|
+
trendsPoll.refresh,
|
|
268
307
|
usagePoll.refresh,
|
|
269
308
|
]);
|
|
270
309
|
|
|
@@ -417,6 +456,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
417
456
|
listPanelWidthPx,
|
|
418
457
|
isResizingListPanel,
|
|
419
458
|
runEntries,
|
|
459
|
+
filteredRunEntries,
|
|
420
460
|
runHasMore,
|
|
421
461
|
runNextOffset,
|
|
422
462
|
runTotal,
|
|
@@ -424,8 +464,17 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
424
464
|
runsError: runsPoll.error,
|
|
425
465
|
loadingMoreRuns,
|
|
426
466
|
usage: usagePoll.data?.usage || null,
|
|
467
|
+
jobTrends: trendsPoll.data?.trends || null,
|
|
427
468
|
usageError: usagePoll.error,
|
|
469
|
+
trendsError: trendsPoll.error,
|
|
428
470
|
usageDays,
|
|
471
|
+
jobTrendRange:
|
|
472
|
+
jobTrendRange === kTrendRange30d
|
|
473
|
+
? kTrendRange30d
|
|
474
|
+
: jobTrendRange === kTrendRange24h
|
|
475
|
+
? kTrendRange24h
|
|
476
|
+
: kTrendRange7d,
|
|
477
|
+
selectedJobTrendBucketFilter,
|
|
429
478
|
bulkUsageByJobId: bulkUsagePoll.data?.usage?.byJobId || {},
|
|
430
479
|
bulkUsageError: bulkUsagePoll.error,
|
|
431
480
|
bulkRunsByJobId: bulkRunsPoll.data?.runs?.byJobId || {},
|
|
@@ -444,6 +493,8 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
444
493
|
actions: {
|
|
445
494
|
setRunStatusFilter,
|
|
446
495
|
setUsageDays,
|
|
496
|
+
setJobTrendRange,
|
|
497
|
+
setSelectedJobTrendBucketFilter,
|
|
447
498
|
setPromptValue,
|
|
448
499
|
saveChanges,
|
|
449
500
|
refreshAll,
|
|
@@ -450,3 +450,14 @@ export const FullscreenLineIcon = ({ className = "" }) => html`
|
|
|
450
450
|
<path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" />
|
|
451
451
|
</svg>
|
|
452
452
|
`;
|
|
453
|
+
|
|
454
|
+
export const ComputerLineIcon = ({ className = "" }) => html`
|
|
455
|
+
<svg
|
|
456
|
+
class=${className}
|
|
457
|
+
viewBox="0 0 24 24"
|
|
458
|
+
fill="currentColor"
|
|
459
|
+
aria-hidden="true"
|
|
460
|
+
>
|
|
461
|
+
<path d="M4 16H20V5H4V16ZM13 18V20H17V22H7V20H11V18H2.9918C2.44405 18 2 17.5511 2 16.9925V4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V16.9925C22 17.5489 21.5447 18 21.0082 18H13Z" />
|
|
462
|
+
</svg>
|
|
463
|
+
`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useMemo } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { marked } from "https://esm.sh/marked";
|
|
5
|
+
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
const kReleaseNotesUrl =
|
|
9
|
+
"https://github.com/openclaw/openclaw/releases/tag/v2026.3.13";
|
|
10
|
+
const kSetupInstructionsMarkdown = `Release reference: [OpenClaw 2026.3.13](${kReleaseNotesUrl})
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- OpenClaw 2026.3.13+
|
|
15
|
+
- Chrome 144+
|
|
16
|
+
- Node.js installed on the Mac node so \`npx\` is available
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
### 1) Enable remote debugging in Chrome
|
|
21
|
+
|
|
22
|
+
Open \`chrome://inspect/#remote-debugging\` and turn it on. Do **not** launch Chrome with \`--remote-debugging-port\`.
|
|
23
|
+
|
|
24
|
+
### 2) Configure the node
|
|
25
|
+
|
|
26
|
+
In \`~/.openclaw/openclaw.json\` on the Mac node:
|
|
27
|
+
|
|
28
|
+
\`\`\`json
|
|
29
|
+
{
|
|
30
|
+
"browser": {
|
|
31
|
+
"defaultProfile": "user"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
The built-in \`user\` profile uses live Chrome attach. You do not need a custom \`existing-session\` profile.
|
|
37
|
+
|
|
38
|
+
### 3) Approve Chrome consent
|
|
39
|
+
|
|
40
|
+
On first connect, Chrome prompts for DevTools MCP access. Click **Allow**.
|
|
41
|
+
|
|
42
|
+
## Troubleshooting
|
|
43
|
+
|
|
44
|
+
| Problem | Fix |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| Browser proxy times out (20s) | Restart Chrome cleanly and run the check again. |
|
|
47
|
+
| Config validation error on existing-session | Do not define a custom existing-session profile. Use \`defaultProfile: "user"\`. |
|
|
48
|
+
| EADDRINUSE on port 9222 | Quit Chrome launched with \`--remote-debugging-port\` and relaunch normally. |
|
|
49
|
+
| Consent dialog appears but attach hangs | Quit Chrome, relaunch, and approve the dialog again. |
|
|
50
|
+
| \`npx chrome-devtools-mcp\` not found | Install Node.js on the Mac node so \`npx\` exists in PATH. |`;
|
|
51
|
+
|
|
52
|
+
export const BrowserAttachCard = () => {
|
|
53
|
+
const setupInstructionsHtml = useMemo(
|
|
54
|
+
() =>
|
|
55
|
+
marked.parse(kSetupInstructionsMarkdown, {
|
|
56
|
+
gfm: true,
|
|
57
|
+
breaks: true,
|
|
58
|
+
}),
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return html`
|
|
63
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
64
|
+
<div class="space-y-1">
|
|
65
|
+
<h3 class="font-semibold text-sm">Live Chrome Attach (Mac Node)</h3>
|
|
66
|
+
<p class="text-xs text-gray-500">
|
|
67
|
+
Connect your agent to real Chrome sessions (logged-in tabs, cookies,
|
|
68
|
+
and all) using the built-in <code>user</code> profile.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<details class="rounded-lg border border-border bg-black/20 px-3 py-2.5">
|
|
73
|
+
<summary
|
|
74
|
+
class="cursor-pointer text-xs text-gray-300 hover:text-gray-200"
|
|
75
|
+
>
|
|
76
|
+
Setup instructions
|
|
77
|
+
</summary>
|
|
78
|
+
<div
|
|
79
|
+
class="pt-3 file-viewer-preview release-notes-preview text-xs leading-5"
|
|
80
|
+
dangerouslySetInnerHTML=${{ __html: setupInstructionsHtml }}
|
|
81
|
+
></div>
|
|
82
|
+
</details>
|
|
83
|
+
</div>
|
|
84
|
+
`;
|
|
85
|
+
};
|