@chrysb/alphaclaw 0.6.2-beta.2 → 0.6.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/cron.css +9 -1
- package/lib/public/js/components/cron-tab/cron-job-detail.js +161 -127
- package/lib/public/js/components/cron-tab/cron-job-usage.js +11 -18
- package/lib/public/js/components/cron-tab/cron-overview.js +13 -274
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +296 -0
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +155 -96
- package/lib/public/js/components/cron-tab/index.js +9 -4
- package/lib/public/js/components/cron-tab/use-cron-tab.js +118 -24
- package/lib/public/js/lib/api.js +25 -0
- package/lib/server/cron-service.js +57 -4
- package/lib/server/onboarding/import/import-applier.js +15 -3
- package/lib/server/routes/cron.js +21 -0
- package/package.json +1 -1
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
-
import { Tooltip } from "../tooltip.js";
|
|
6
5
|
import { formatCost } from "./cron-helpers.js";
|
|
7
6
|
|
|
8
7
|
const html = htm.bind(h);
|
|
@@ -146,6 +145,8 @@ export const CronRunsTrendCard = ({
|
|
|
146
145
|
selectedBucketFilter = null,
|
|
147
146
|
onBucketFilterChange = () => {},
|
|
148
147
|
}) => {
|
|
148
|
+
const chartCanvasRef = useRef(null);
|
|
149
|
+
const chartInstanceRef = useRef(null);
|
|
149
150
|
const [range, setRange] = useState(
|
|
150
151
|
initialRange === kRange30d ? kRange30d : kRange7d,
|
|
151
152
|
);
|
|
@@ -166,6 +167,156 @@ export const CronRunsTrendCard = ({
|
|
|
166
167
|
);
|
|
167
168
|
return matchingPoint?.key || "";
|
|
168
169
|
}, [range, selectedBucketFilter, trend.points]);
|
|
170
|
+
const selectedPointIndex = useMemo(
|
|
171
|
+
() => trend.points.findIndex((point) => point.key === selectedBucketKey),
|
|
172
|
+
[selectedBucketKey, trend.points],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const chartData = useMemo(() => {
|
|
176
|
+
const dimAlpha = "0.22";
|
|
177
|
+
const fullAlpha = "0.86";
|
|
178
|
+
const isDimmed = (index) => selectedPointIndex >= 0 && selectedPointIndex !== index;
|
|
179
|
+
const labels = trend.points.map((point) => (point.showLabel ? point.label : ""));
|
|
180
|
+
return {
|
|
181
|
+
labels,
|
|
182
|
+
datasets: [
|
|
183
|
+
{
|
|
184
|
+
label: "ok",
|
|
185
|
+
data: trend.points.map((point) => Number(point.ok || 0)),
|
|
186
|
+
stack: "outcomes",
|
|
187
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
188
|
+
`rgba(34,255,170,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
189
|
+
borderColor: trend.points.map((_, index) =>
|
|
190
|
+
`rgba(34,255,170,${isDimmed(index) ? "0.35" : "1"})`),
|
|
191
|
+
borderWidth: 1,
|
|
192
|
+
borderRadius: 0,
|
|
193
|
+
borderSkipped: false,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
label: "error",
|
|
197
|
+
data: trend.points.map((point) => Number(point.error || 0)),
|
|
198
|
+
stack: "outcomes",
|
|
199
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
200
|
+
`rgba(255,74,138,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
201
|
+
borderColor: trend.points.map((_, index) =>
|
|
202
|
+
`rgba(255,74,138,${isDimmed(index) ? "0.35" : "1"})`),
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderRadius: 0,
|
|
205
|
+
borderSkipped: false,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: "skipped",
|
|
209
|
+
data: trend.points.map((point) => Number(point.skipped || 0)),
|
|
210
|
+
stack: "outcomes",
|
|
211
|
+
backgroundColor: trend.points.map((_, index) =>
|
|
212
|
+
`rgba(255,214,64,${isDimmed(index) ? dimAlpha : fullAlpha})`),
|
|
213
|
+
borderColor: trend.points.map((_, index) =>
|
|
214
|
+
`rgba(255,214,64,${isDimmed(index) ? "0.35" : "1"})`),
|
|
215
|
+
borderWidth: 1,
|
|
216
|
+
borderRadius: 0,
|
|
217
|
+
borderSkipped: false,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}, [selectedPointIndex, trend.points]);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const canvas = chartCanvasRef.current;
|
|
225
|
+
const Chart = window.Chart;
|
|
226
|
+
if (!canvas || !Chart) return;
|
|
227
|
+
if (chartInstanceRef.current) {
|
|
228
|
+
chartInstanceRef.current.destroy();
|
|
229
|
+
chartInstanceRef.current = null;
|
|
230
|
+
}
|
|
231
|
+
const getBucketFilter = (index) => {
|
|
232
|
+
const selectedPoint = trend.points[index];
|
|
233
|
+
if (!selectedPoint) return null;
|
|
234
|
+
return {
|
|
235
|
+
key: selectedPoint.key,
|
|
236
|
+
label: selectedPoint.label,
|
|
237
|
+
range,
|
|
238
|
+
startMs: Number(selectedPoint.startMs || 0),
|
|
239
|
+
endMs: Number(selectedPoint.endMs || 0),
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
chartInstanceRef.current = new Chart(canvas, {
|
|
243
|
+
type: "bar",
|
|
244
|
+
data: chartData,
|
|
245
|
+
options: {
|
|
246
|
+
responsive: true,
|
|
247
|
+
maintainAspectRatio: false,
|
|
248
|
+
interaction: { mode: "index", intersect: false },
|
|
249
|
+
animation: false,
|
|
250
|
+
onHover: (event, elements) => {
|
|
251
|
+
const target = event?.native?.target;
|
|
252
|
+
if (!target || !target.style) return;
|
|
253
|
+
target.style.cursor = Array.isArray(elements) && elements.length > 0
|
|
254
|
+
? "pointer"
|
|
255
|
+
: "default";
|
|
256
|
+
},
|
|
257
|
+
onClick: (_event, elements) => {
|
|
258
|
+
const index = Number(elements?.[0]?.index);
|
|
259
|
+
if (!Number.isFinite(index)) return;
|
|
260
|
+
const nextFilter = getBucketFilter(index);
|
|
261
|
+
if (!nextFilter) return;
|
|
262
|
+
if (nextFilter.key === selectedBucketKey) {
|
|
263
|
+
onBucketFilterChange(null);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
onBucketFilterChange(nextFilter);
|
|
267
|
+
},
|
|
268
|
+
scales: {
|
|
269
|
+
x: {
|
|
270
|
+
stacked: true,
|
|
271
|
+
grid: { color: "rgba(148,163,184,0.08)" },
|
|
272
|
+
ticks: {
|
|
273
|
+
color: "rgba(156,163,175,1)",
|
|
274
|
+
maxRotation: 0,
|
|
275
|
+
autoSkip: false,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
y: {
|
|
279
|
+
stacked: true,
|
|
280
|
+
beginAtZero: true,
|
|
281
|
+
grid: { color: "rgba(148,163,184,0.12)" },
|
|
282
|
+
ticks: {
|
|
283
|
+
precision: 0,
|
|
284
|
+
color: "rgba(156,163,175,1)",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
plugins: {
|
|
289
|
+
legend: {
|
|
290
|
+
labels: {
|
|
291
|
+
color: "rgba(209,213,219,1)",
|
|
292
|
+
boxWidth: 10,
|
|
293
|
+
boxHeight: 10,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
tooltip: {
|
|
297
|
+
callbacks: {
|
|
298
|
+
title: (items) => String(items?.[0]?.label || ""),
|
|
299
|
+
label: (context) => `${context.dataset.label}: ${Number(context.parsed.y || 0)}`,
|
|
300
|
+
footer: (items) => {
|
|
301
|
+
const index = Number(items?.[0]?.dataIndex);
|
|
302
|
+
const point = trend.points[index];
|
|
303
|
+
if (!point) return "";
|
|
304
|
+
const costLabel =
|
|
305
|
+
point.costCount > 0 ? `~${formatCost(point.totalCost)}` : "—";
|
|
306
|
+
return `total: ${point.total}\ncost: ${costLabel}`;
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
return () => {
|
|
314
|
+
if (chartInstanceRef.current) {
|
|
315
|
+
chartInstanceRef.current.destroy();
|
|
316
|
+
chartInstanceRef.current = null;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}, [chartData, onBucketFilterChange, range, selectedBucketKey, trend.points]);
|
|
169
320
|
|
|
170
321
|
return html`
|
|
171
322
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
@@ -177,100 +328,8 @@ export const CronRunsTrendCard = ({
|
|
|
177
328
|
onChange=${setRange}
|
|
178
329
|
/>
|
|
179
330
|
</div>
|
|
180
|
-
<div
|
|
181
|
-
|
|
182
|
-
style=${{ "--cron-runs-trend-columns": String(trend.points.length || 1) }}
|
|
183
|
-
>
|
|
184
|
-
${trend.points.map((point) => {
|
|
185
|
-
const isSelected = selectedBucketKey === point.key;
|
|
186
|
-
const isDimmed = !!selectedBucketKey && !isSelected;
|
|
187
|
-
const totalHeightPercent = (point.total / trend.maxTotal) * 100;
|
|
188
|
-
const okHeightPercent = point.total > 0 ? (point.ok / point.total) * 100 : 0;
|
|
189
|
-
const errorHeightPercent = point.total > 0 ? (point.error / point.total) * 100 : 0;
|
|
190
|
-
const skippedHeightPercent = point.total > 0 ? (point.skipped / point.total) * 100 : 0;
|
|
191
|
-
const tooltipText = [
|
|
192
|
-
`${point.label}`,
|
|
193
|
-
`ok: ${point.ok}`,
|
|
194
|
-
`error: ${point.error}`,
|
|
195
|
-
`skipped: ${point.skipped}`,
|
|
196
|
-
`total: ${point.total}`,
|
|
197
|
-
`cost: ${point.costCount > 0 ? `~${formatCost(point.totalCost)}` : "—"}`,
|
|
198
|
-
].join("\n");
|
|
199
|
-
return html`
|
|
200
|
-
<${Tooltip}
|
|
201
|
-
text=${tooltipText}
|
|
202
|
-
widthClass="w-40"
|
|
203
|
-
tooltipClassName="whitespace-pre-line"
|
|
204
|
-
triggerClassName="inline-flex justify-center w-full"
|
|
205
|
-
>
|
|
206
|
-
<div
|
|
207
|
-
class=${`cron-runs-trend-col ${isSelected ? "is-selected" : ""} ${isDimmed ? "is-dimmed" : ""}`}
|
|
208
|
-
role="button"
|
|
209
|
-
tabindex="0"
|
|
210
|
-
onClick=${() => {
|
|
211
|
-
if (isSelected) {
|
|
212
|
-
onBucketFilterChange(null);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
onBucketFilterChange({
|
|
216
|
-
key: point.key,
|
|
217
|
-
label: point.label,
|
|
218
|
-
range,
|
|
219
|
-
startMs: Number(point.startMs || 0),
|
|
220
|
-
endMs: Number(point.endMs || 0),
|
|
221
|
-
});
|
|
222
|
-
}}
|
|
223
|
-
onKeyDown=${(event) => {
|
|
224
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
225
|
-
event.preventDefault();
|
|
226
|
-
if (isSelected) {
|
|
227
|
-
onBucketFilterChange(null);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
onBucketFilterChange({
|
|
231
|
-
key: point.key,
|
|
232
|
-
label: point.label,
|
|
233
|
-
range,
|
|
234
|
-
startMs: Number(point.startMs || 0),
|
|
235
|
-
endMs: Number(point.endMs || 0),
|
|
236
|
-
});
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
<div class="cron-runs-trend-track">
|
|
240
|
-
<div class="cron-runs-trend-bar" style=${{ height: `${totalHeightPercent}%` }}>
|
|
241
|
-
<div
|
|
242
|
-
class="cron-runs-trend-segment-skipped"
|
|
243
|
-
style=${{ height: `${skippedHeightPercent}%` }}
|
|
244
|
-
></div>
|
|
245
|
-
<div
|
|
246
|
-
class="cron-runs-trend-segment-error"
|
|
247
|
-
style=${{ height: `${errorHeightPercent}%` }}
|
|
248
|
-
></div>
|
|
249
|
-
<div
|
|
250
|
-
class="cron-runs-trend-segment-ok"
|
|
251
|
-
style=${{ height: `${okHeightPercent}%` }}
|
|
252
|
-
></div>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
<span class="cron-runs-trend-label">${point.showLabel ? point.label : ""}</span>
|
|
256
|
-
</div>
|
|
257
|
-
</${Tooltip}>
|
|
258
|
-
`;
|
|
259
|
-
})}
|
|
260
|
-
</div>
|
|
261
|
-
<div class="cron-runs-trend-legend">
|
|
262
|
-
<span class="cron-runs-trend-legend-item">
|
|
263
|
-
<span class="cron-runs-trend-legend-dot is-ok"></span>
|
|
264
|
-
ok
|
|
265
|
-
</span>
|
|
266
|
-
<span class="cron-runs-trend-legend-item">
|
|
267
|
-
<span class="cron-runs-trend-legend-dot is-error"></span>
|
|
268
|
-
error
|
|
269
|
-
</span>
|
|
270
|
-
<span class="cron-runs-trend-legend-item">
|
|
271
|
-
<span class="cron-runs-trend-legend-dot is-skipped"></span>
|
|
272
|
-
skipped
|
|
273
|
-
</span>
|
|
331
|
+
<div class="h-40">
|
|
332
|
+
<canvas ref=${chartCanvasRef}></canvas>
|
|
274
333
|
</div>
|
|
275
334
|
</section>
|
|
276
335
|
`;
|
|
@@ -75,9 +75,7 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
75
75
|
runHasMore=${state.runHasMore}
|
|
76
76
|
loadingMoreRuns=${state.loadingMoreRuns}
|
|
77
77
|
runStatusFilter=${state.runStatusFilter}
|
|
78
|
-
runDeliveryFilter=${state.runDeliveryFilter}
|
|
79
78
|
onSetRunStatusFilter=${actions.setRunStatusFilter}
|
|
80
|
-
onSetRunDeliveryFilter=${actions.setRunDeliveryFilter}
|
|
81
79
|
onLoadMoreRuns=${actions.loadMoreRuns}
|
|
82
80
|
onRunNow=${actions.runSelectedJobNow}
|
|
83
81
|
runningJob=${state.runningJob}
|
|
@@ -89,8 +87,15 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
89
87
|
promptValue=${state.promptValue}
|
|
90
88
|
savedPromptValue=${state.savedPromptValue}
|
|
91
89
|
onChangePrompt=${actions.setPromptValue}
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
onSaveChanges=${actions.saveChanges}
|
|
91
|
+
savingChanges=${state.savingChanges}
|
|
92
|
+
routingDraft=${state.routingDraft}
|
|
93
|
+
onChangeRoutingDraft=${actions.setRoutingDraft}
|
|
94
|
+
deliverySessions=${state.deliverySessions}
|
|
95
|
+
loadingDeliverySessions=${state.loadingDeliverySessions}
|
|
96
|
+
deliverySessionsError=${state.deliverySessionsError}
|
|
97
|
+
destinationSessionKey=${state.destinationSessionKey}
|
|
98
|
+
onChangeDestinationSessionKey=${actions.setDestinationSessionKey}
|
|
94
99
|
/>
|
|
95
100
|
`}
|
|
96
101
|
</main>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from "https://esm.sh/preact/hooks";
|
|
8
8
|
import { usePolling } from "../../hooks/usePolling.js";
|
|
9
|
+
import { useDestinationSessionSelection } from "../../hooks/use-destination-session-selection.js";
|
|
9
10
|
import {
|
|
10
11
|
fetchCronBulkRuns,
|
|
11
12
|
fetchCronBulkUsage,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
setCronJobEnabled,
|
|
17
18
|
triggerCronJobRun,
|
|
18
19
|
updateCronJobPrompt,
|
|
20
|
+
updateCronJobRouting,
|
|
19
21
|
} from "../../lib/api.js";
|
|
20
22
|
import { readUiSettings, writeUiSettings } from "../../lib/ui-settings.js";
|
|
21
23
|
import { showToast } from "../toast.js";
|
|
@@ -28,6 +30,20 @@ const kListPanelWidthUiSettingKey = "cronListPanelWidthPx";
|
|
|
28
30
|
const kRunsPageSize = 25;
|
|
29
31
|
const kCalendarUsageDays = 30;
|
|
30
32
|
const kCalendarPastDays = 30;
|
|
33
|
+
const kRoutingDefaults = {
|
|
34
|
+
sessionTarget: "main",
|
|
35
|
+
wakeMode: "now",
|
|
36
|
+
deliveryMode: "none",
|
|
37
|
+
deliveryChannel: "",
|
|
38
|
+
deliveryTo: "",
|
|
39
|
+
};
|
|
40
|
+
const readRoutingDraftFromJob = (job = null) => ({
|
|
41
|
+
sessionTarget: String(job?.sessionTarget || kRoutingDefaults.sessionTarget),
|
|
42
|
+
wakeMode: String(job?.wakeMode || kRoutingDefaults.wakeMode),
|
|
43
|
+
deliveryMode: String(job?.delivery?.mode || kRoutingDefaults.deliveryMode),
|
|
44
|
+
deliveryChannel: String(job?.delivery?.channel || ""),
|
|
45
|
+
deliveryTo: String(job?.delivery?.to || ""),
|
|
46
|
+
});
|
|
31
47
|
|
|
32
48
|
const clampListPanelWidth = (value) =>
|
|
33
49
|
Math.max(kListPanelMinWidthPx, Math.min(kListPanelMaxWidthPx, value));
|
|
@@ -38,6 +54,9 @@ const normalizeRouteJobId = (jobId = "") => {
|
|
|
38
54
|
};
|
|
39
55
|
|
|
40
56
|
export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
57
|
+
const selectedRouteKey = normalizeRouteJobId(jobId);
|
|
58
|
+
const selectedJobId =
|
|
59
|
+
selectedRouteKey === kAllCronJobsRouteKey ? "" : selectedRouteKey;
|
|
41
60
|
const listPanelRef = useRef(null);
|
|
42
61
|
const [listPanelWidthPx, setListPanelWidthPx] = useState(() => {
|
|
43
62
|
const settings = readUiSettings();
|
|
@@ -48,7 +67,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
48
67
|
});
|
|
49
68
|
const [isResizingListPanel, setIsResizingListPanel] = useState(false);
|
|
50
69
|
const [runStatusFilter, setRunStatusFilter] = useState("all");
|
|
51
|
-
const [runDeliveryFilter, setRunDeliveryFilter] = useState("all");
|
|
52
70
|
const [runEntries, setRunEntries] = useState([]);
|
|
53
71
|
const [runHasMore, setRunHasMore] = useState(false);
|
|
54
72
|
const [runNextOffset, setRunNextOffset] = useState(0);
|
|
@@ -56,14 +74,22 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
56
74
|
const [loadingMoreRuns, setLoadingMoreRuns] = useState(false);
|
|
57
75
|
const [promptValue, setPromptValue] = useState("");
|
|
58
76
|
const [savedPromptValue, setSavedPromptValue] = useState("");
|
|
59
|
-
const [
|
|
77
|
+
const [savingChanges, setSavingChanges] = useState(false);
|
|
60
78
|
const [runningJob, setRunningJob] = useState(false);
|
|
61
79
|
const [togglingJobEnabled, setTogglingJobEnabled] = useState(false);
|
|
80
|
+
const [routingDraft, setRoutingDraft] = useState(kRoutingDefaults);
|
|
62
81
|
const [usageDays, setUsageDays] = useState(30);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
const {
|
|
83
|
+
sessions: deliverySessions,
|
|
84
|
+
loading: loadingDeliverySessions,
|
|
85
|
+
error: deliverySessionsError,
|
|
86
|
+
destinationSessionKey,
|
|
87
|
+
setDestinationSessionKey,
|
|
88
|
+
selectedDestination,
|
|
89
|
+
} = useDestinationSessionSelection({
|
|
90
|
+
enabled: !!selectedJobId,
|
|
91
|
+
resetKey: String(selectedJobId || ""),
|
|
92
|
+
});
|
|
67
93
|
|
|
68
94
|
const jobsPoll = usePolling(
|
|
69
95
|
() => fetchCronJobs({ sortBy: "nextRunAtMs", sortDir: "asc" }),
|
|
@@ -82,7 +108,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
82
108
|
limit: kRunsPageSize,
|
|
83
109
|
offset: 0,
|
|
84
110
|
status: runStatusFilter,
|
|
85
|
-
deliveryStatus: runDeliveryFilter,
|
|
86
111
|
sortDir: "desc",
|
|
87
112
|
});
|
|
88
113
|
},
|
|
@@ -144,13 +169,25 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
144
169
|
if (!selectedJobId) {
|
|
145
170
|
setPromptValue("");
|
|
146
171
|
setSavedPromptValue("");
|
|
172
|
+
setRoutingDraft(kRoutingDefaults);
|
|
147
173
|
return;
|
|
148
174
|
}
|
|
149
175
|
const prompt = String(selectedJob?.payload?.message || "");
|
|
150
176
|
setPromptValue(prompt);
|
|
151
177
|
setSavedPromptValue(prompt);
|
|
178
|
+
setRoutingDraft(readRoutingDraftFromJob(selectedJob));
|
|
152
179
|
}, [selectedJobId, selectedJob?.payload?.message]);
|
|
153
180
|
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!selectedJobId) return;
|
|
183
|
+
setRoutingDraft(readRoutingDraftFromJob(selectedJob));
|
|
184
|
+
}, [
|
|
185
|
+
selectedJobId,
|
|
186
|
+
selectedJob?.sessionTarget,
|
|
187
|
+
selectedJob?.wakeMode,
|
|
188
|
+
selectedJob?.delivery?.mode,
|
|
189
|
+
]);
|
|
190
|
+
|
|
154
191
|
useEffect(() => {
|
|
155
192
|
setRunEntries([]);
|
|
156
193
|
setRunHasMore(false);
|
|
@@ -158,7 +195,7 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
158
195
|
setRunTotal(0);
|
|
159
196
|
if (!selectedJobId) return;
|
|
160
197
|
runsPoll.refresh();
|
|
161
|
-
}, [selectedJobId, runStatusFilter
|
|
198
|
+
}, [selectedJobId, runStatusFilter]);
|
|
162
199
|
|
|
163
200
|
useEffect(() => {
|
|
164
201
|
if (!selectedJobId) return;
|
|
@@ -273,7 +310,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
273
310
|
limit: kRunsPageSize,
|
|
274
311
|
offset: runNextOffset,
|
|
275
312
|
status: runStatusFilter,
|
|
276
|
-
deliveryStatus: runDeliveryFilter,
|
|
277
313
|
sortDir: "desc",
|
|
278
314
|
});
|
|
279
315
|
const nextEntries = Array.isArray(data?.runs?.entries)
|
|
@@ -290,28 +326,81 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
290
326
|
}
|
|
291
327
|
}, [
|
|
292
328
|
loadingMoreRuns,
|
|
293
|
-
runDeliveryFilter,
|
|
294
329
|
runHasMore,
|
|
295
330
|
runNextOffset,
|
|
296
331
|
runStatusFilter,
|
|
297
332
|
selectedJobId,
|
|
298
333
|
]);
|
|
299
334
|
|
|
300
|
-
const
|
|
301
|
-
if (!selectedJobId ||
|
|
302
|
-
|
|
303
|
-
|
|
335
|
+
const saveChanges = useCallback(async () => {
|
|
336
|
+
if (!selectedJobId || !selectedJob || savingChanges) return;
|
|
337
|
+
const currentRouting = readRoutingDraftFromJob(selectedJob);
|
|
338
|
+
const nextRouting = {
|
|
339
|
+
sessionTarget: String(routingDraft?.sessionTarget || kRoutingDefaults.sessionTarget),
|
|
340
|
+
wakeMode: String(routingDraft?.wakeMode || kRoutingDefaults.wakeMode),
|
|
341
|
+
deliveryMode: String(routingDraft?.deliveryMode || kRoutingDefaults.deliveryMode),
|
|
342
|
+
deliveryChannel: String(routingDraft?.deliveryChannel || ""),
|
|
343
|
+
deliveryTo: String(routingDraft?.deliveryTo || ""),
|
|
344
|
+
};
|
|
345
|
+
const routingUnchanged =
|
|
346
|
+
nextRouting.sessionTarget === currentRouting.sessionTarget &&
|
|
347
|
+
nextRouting.wakeMode === currentRouting.wakeMode &&
|
|
348
|
+
nextRouting.deliveryMode === currentRouting.deliveryMode &&
|
|
349
|
+
nextRouting.deliveryChannel === currentRouting.deliveryChannel &&
|
|
350
|
+
nextRouting.deliveryTo === currentRouting.deliveryTo;
|
|
351
|
+
const promptUnchanged = promptValue === savedPromptValue;
|
|
352
|
+
if (routingUnchanged && promptUnchanged) return;
|
|
353
|
+
setSavingChanges(true);
|
|
304
354
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
355
|
+
if (!routingUnchanged) {
|
|
356
|
+
await updateCronJobRouting(selectedJobId, nextRouting);
|
|
357
|
+
}
|
|
358
|
+
if (!promptUnchanged) {
|
|
359
|
+
await updateCronJobPrompt(selectedJobId, promptValue);
|
|
360
|
+
setSavedPromptValue(promptValue);
|
|
361
|
+
}
|
|
362
|
+
showToast("Changes saved", "success");
|
|
308
363
|
refreshAll();
|
|
309
364
|
} catch (error) {
|
|
310
|
-
showToast(error.message || "Could not
|
|
365
|
+
showToast(error.message || "Could not save changes", "error");
|
|
311
366
|
} finally {
|
|
312
|
-
|
|
367
|
+
setSavingChanges(false);
|
|
313
368
|
}
|
|
314
|
-
}, [
|
|
369
|
+
}, [
|
|
370
|
+
promptValue,
|
|
371
|
+
refreshAll,
|
|
372
|
+
routingDraft,
|
|
373
|
+
savedPromptValue,
|
|
374
|
+
savingChanges,
|
|
375
|
+
selectedJob,
|
|
376
|
+
selectedJobId,
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (!selectedJobId) return;
|
|
381
|
+
if (String(routingDraft?.deliveryMode || "none") !== "announce") return;
|
|
382
|
+
if (!selectedDestination?.channel && !selectedDestination?.to) return;
|
|
383
|
+
setRoutingDraft((currentValue = kRoutingDefaults) => {
|
|
384
|
+
const nextChannel = String(selectedDestination?.channel || currentValue.deliveryChannel || "");
|
|
385
|
+
const nextTo = String(selectedDestination?.to || currentValue.deliveryTo || "");
|
|
386
|
+
if (
|
|
387
|
+
nextChannel === String(currentValue.deliveryChannel || "") &&
|
|
388
|
+
nextTo === String(currentValue.deliveryTo || "")
|
|
389
|
+
) {
|
|
390
|
+
return currentValue;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
...currentValue,
|
|
394
|
+
deliveryChannel: nextChannel,
|
|
395
|
+
deliveryTo: nextTo,
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}, [
|
|
399
|
+
routingDraft?.deliveryMode,
|
|
400
|
+
selectedDestination?.channel,
|
|
401
|
+
selectedDestination?.to,
|
|
402
|
+
selectedJobId,
|
|
403
|
+
]);
|
|
315
404
|
|
|
316
405
|
return {
|
|
317
406
|
refs: {
|
|
@@ -332,7 +421,6 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
332
421
|
runNextOffset,
|
|
333
422
|
runTotal,
|
|
334
423
|
runStatusFilter,
|
|
335
|
-
runDeliveryFilter,
|
|
336
424
|
runsError: runsPoll.error,
|
|
337
425
|
loadingMoreRuns,
|
|
338
426
|
usage: usagePoll.data?.usage || null,
|
|
@@ -344,16 +432,20 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
344
432
|
bulkRunsError: bulkRunsPoll.error,
|
|
345
433
|
promptValue,
|
|
346
434
|
savedPromptValue,
|
|
347
|
-
|
|
435
|
+
savingChanges,
|
|
348
436
|
runningJob,
|
|
349
437
|
togglingJobEnabled,
|
|
438
|
+
routingDraft,
|
|
439
|
+
deliverySessions,
|
|
440
|
+
loadingDeliverySessions,
|
|
441
|
+
deliverySessionsError,
|
|
442
|
+
destinationSessionKey,
|
|
350
443
|
},
|
|
351
444
|
actions: {
|
|
352
445
|
setRunStatusFilter,
|
|
353
|
-
setRunDeliveryFilter,
|
|
354
446
|
setUsageDays,
|
|
355
447
|
setPromptValue,
|
|
356
|
-
|
|
448
|
+
saveChanges,
|
|
357
449
|
refreshAll,
|
|
358
450
|
loadMoreRuns,
|
|
359
451
|
runSelectedJobNow,
|
|
@@ -361,6 +453,8 @@ export const useCronTab = ({ jobId = "", onSetLocation = () => {} } = {}) => {
|
|
|
361
453
|
selectAllJobs,
|
|
362
454
|
selectJob,
|
|
363
455
|
onListResizerPointerDown,
|
|
456
|
+
setRoutingDraft,
|
|
457
|
+
setDestinationSessionKey,
|
|
364
458
|
},
|
|
365
459
|
};
|
|
366
460
|
};
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -544,6 +544,31 @@ export async function updateCronJobPrompt(id, message) {
|
|
|
544
544
|
return parseJsonOrThrow(res, "Could not update cron prompt");
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
export async function updateCronJobRouting(
|
|
548
|
+
id,
|
|
549
|
+
{
|
|
550
|
+
sessionTarget = "",
|
|
551
|
+
wakeMode = "",
|
|
552
|
+
deliveryMode = "",
|
|
553
|
+
deliveryChannel = "",
|
|
554
|
+
deliveryTo = "",
|
|
555
|
+
} = {},
|
|
556
|
+
) {
|
|
557
|
+
const safeId = encodeURIComponent(String(id || ""));
|
|
558
|
+
const res = await authFetch(`/api/cron/jobs/${safeId}/routing`, {
|
|
559
|
+
method: "PUT",
|
|
560
|
+
headers: { "Content-Type": "application/json" },
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
sessionTarget: String(sessionTarget || ""),
|
|
563
|
+
wakeMode: String(wakeMode || ""),
|
|
564
|
+
deliveryMode: String(deliveryMode || ""),
|
|
565
|
+
deliveryChannel: String(deliveryChannel || ""),
|
|
566
|
+
deliveryTo: String(deliveryTo || ""),
|
|
567
|
+
}),
|
|
568
|
+
});
|
|
569
|
+
return parseJsonOrThrow(res, "Could not update cron routing");
|
|
570
|
+
}
|
|
571
|
+
|
|
547
572
|
export async function fetchDevicePairings() {
|
|
548
573
|
const res = await authFetch("/api/devices");
|
|
549
574
|
return res.json();
|