@chrysb/alphaclaw 0.6.2-beta.5 → 0.6.2-beta.6
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 +91 -39
- package/lib/public/js/components/cron-tab/cron-calendar.js +287 -164
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +325 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +69 -56
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +20 -2
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
- package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
- package/lib/public/js/components/file-viewer/utils.js +1 -5
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +88 -59
- package/package.json +1 -1
|
@@ -1,211 +1,20 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { ActionButton } from "../action-button.js";
|
|
5
|
-
import {
|
|
6
|
-
import { ToggleSwitch } from "../toggle-switch.js";
|
|
7
|
-
import { EditorSurface } from "../file-viewer/editor-surface.js";
|
|
8
|
-
import { countTextLines, shouldUseSimpleEditorMode } from "../file-viewer/utils.js";
|
|
9
|
-
import {
|
|
10
|
-
kLargeFileSimpleEditorCharThreshold,
|
|
11
|
-
kLargeFileSimpleEditorLineThreshold,
|
|
12
|
-
} from "../file-viewer/constants.js";
|
|
13
|
-
import { highlightEditorLines } from "../../lib/syntax-highlighters/index.js";
|
|
14
|
-
import {
|
|
15
|
-
formatCronScheduleLabel,
|
|
16
|
-
formatNextRunRelativeMs,
|
|
17
|
-
formatTokenCount,
|
|
18
|
-
} from "./cron-helpers.js";
|
|
5
|
+
import { formatTokenCount } from "./cron-helpers.js";
|
|
19
6
|
import { CronJobUsage } from "./cron-job-usage.js";
|
|
20
7
|
import { CronRunHistoryPanel } from "./cron-run-history-panel.js";
|
|
21
|
-
import {
|
|
8
|
+
import { CronPromptEditor } from "./cron-prompt-editor.js";
|
|
9
|
+
import { CronJobSettingsCard } from "./cron-job-settings-card.js";
|
|
22
10
|
|
|
23
11
|
const html = htm.bind(h);
|
|
24
|
-
const kCronPromptEditorHeightUiSettingKey = "cronPromptEditorHeightPx";
|
|
25
|
-
const kCronPromptEditorDefaultHeightPx = 280;
|
|
26
|
-
const kCronPromptEditorMinHeightPx = 180;
|
|
27
|
-
const clampPromptEditorHeight = (value) => {
|
|
28
|
-
const parsed = Number(value);
|
|
29
|
-
const normalized = Number.isFinite(parsed)
|
|
30
|
-
? Math.round(parsed)
|
|
31
|
-
: kCronPromptEditorDefaultHeightPx;
|
|
32
|
-
return Math.max(kCronPromptEditorMinHeightPx, normalized);
|
|
33
|
-
};
|
|
34
|
-
const readCssHeightPx = (element) => {
|
|
35
|
-
if (!element) return 0;
|
|
36
|
-
const computedHeight = Number.parseFloat(window.getComputedStyle(element).height || "0");
|
|
37
|
-
return Number.isFinite(computedHeight) ? computedHeight : 0;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const PromptEditor = ({
|
|
41
|
-
promptValue = "",
|
|
42
|
-
savedPromptValue = "",
|
|
43
|
-
onChangePrompt = () => {},
|
|
44
|
-
onSaveChanges = () => {},
|
|
45
|
-
}) => {
|
|
46
|
-
const promptEditorShellRef = useRef(null);
|
|
47
|
-
const editorTextareaRef = useRef(null);
|
|
48
|
-
const editorLineNumbersRef = useRef(null);
|
|
49
|
-
const editorLineNumberRowRefs = useRef([]);
|
|
50
|
-
const editorHighlightRef = useRef(null);
|
|
51
|
-
const editorHighlightLineRefs = useRef([]);
|
|
52
|
-
const [promptEditorHeightPx, setPromptEditorHeightPx] = useState(() => {
|
|
53
|
-
const settings = readUiSettings();
|
|
54
|
-
return clampPromptEditorHeight(settings?.[kCronPromptEditorHeightUiSettingKey]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const lineCount = countTextLines(promptValue);
|
|
58
|
-
const editorLineNumbers = useMemo(
|
|
59
|
-
() => Array.from({ length: lineCount }, (_, index) => index + 1),
|
|
60
|
-
[lineCount],
|
|
61
|
-
);
|
|
62
|
-
const shouldUseHighlightedEditor = !shouldUseSimpleEditorMode({
|
|
63
|
-
contentLength: promptValue.length,
|
|
64
|
-
lineCount,
|
|
65
|
-
charThreshold: kLargeFileSimpleEditorCharThreshold,
|
|
66
|
-
lineThreshold: kLargeFileSimpleEditorLineThreshold,
|
|
67
|
-
});
|
|
68
|
-
const highlightedEditorLines = useMemo(
|
|
69
|
-
() =>
|
|
70
|
-
shouldUseHighlightedEditor
|
|
71
|
-
? highlightEditorLines(promptValue, "markdown")
|
|
72
|
-
: [],
|
|
73
|
-
[promptValue, shouldUseHighlightedEditor],
|
|
74
|
-
);
|
|
75
|
-
const isDirty = promptValue !== savedPromptValue;
|
|
76
|
-
|
|
77
|
-
const handleEditorScroll = (event) => {
|
|
78
|
-
const scrollTop = event.currentTarget.scrollTop;
|
|
79
|
-
if (editorLineNumbersRef.current) editorLineNumbersRef.current.scrollTop = scrollTop;
|
|
80
|
-
if (editorHighlightRef.current) editorHighlightRef.current.scrollTop = scrollTop;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleEditorKeyDown = (event) => {
|
|
84
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
|
85
|
-
event.preventDefault();
|
|
86
|
-
onSaveChanges();
|
|
87
|
-
}
|
|
88
|
-
if (event.key === "Tab") {
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
const textarea = editorTextareaRef.current;
|
|
91
|
-
if (!textarea) return;
|
|
92
|
-
const start = textarea.selectionStart;
|
|
93
|
-
const end = textarea.selectionEnd;
|
|
94
|
-
const nextValue = `${promptValue.slice(0, start)} ${promptValue.slice(end)}`;
|
|
95
|
-
onChangePrompt(nextValue);
|
|
96
|
-
window.requestAnimationFrame(() => {
|
|
97
|
-
textarea.selectionStart = start + 2;
|
|
98
|
-
textarea.selectionEnd = start + 2;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
const shellElement = promptEditorShellRef.current;
|
|
105
|
-
if (!shellElement || typeof ResizeObserver === "undefined") return () => {};
|
|
106
|
-
|
|
107
|
-
let saveTimer = null;
|
|
108
|
-
const observer = new ResizeObserver((entries) => {
|
|
109
|
-
const entry = entries?.[0];
|
|
110
|
-
const nextHeight = clampPromptEditorHeight(readCssHeightPx(entry?.target));
|
|
111
|
-
setPromptEditorHeightPx((currentValue) =>
|
|
112
|
-
Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue
|
|
113
|
-
);
|
|
114
|
-
if (saveTimer) window.clearTimeout(saveTimer);
|
|
115
|
-
saveTimer = window.setTimeout(() => {
|
|
116
|
-
const settings = readUiSettings();
|
|
117
|
-
settings[kCronPromptEditorHeightUiSettingKey] = nextHeight;
|
|
118
|
-
writeUiSettings(settings);
|
|
119
|
-
}, 120);
|
|
120
|
-
});
|
|
121
|
-
observer.observe(shellElement);
|
|
122
|
-
return () => {
|
|
123
|
-
observer.disconnect();
|
|
124
|
-
if (saveTimer) window.clearTimeout(saveTimer);
|
|
125
|
-
};
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
return html`
|
|
129
|
-
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
130
|
-
<div class="flex items-center justify-between gap-2">
|
|
131
|
-
<h3 class="card-label inline-flex items-center gap-1.5">
|
|
132
|
-
Prompt
|
|
133
|
-
${isDirty ? html`<span class="file-viewer-dirty-dot"></span>` : null}
|
|
134
|
-
</h3>
|
|
135
|
-
</div>
|
|
136
|
-
<div
|
|
137
|
-
class="cron-prompt-editor-shell"
|
|
138
|
-
ref=${promptEditorShellRef}
|
|
139
|
-
style=${{ height: `${promptEditorHeightPx}px` }}
|
|
140
|
-
>
|
|
141
|
-
<${EditorSurface}
|
|
142
|
-
editorShellClassName="file-viewer-editor-shell"
|
|
143
|
-
editorLineNumbers=${editorLineNumbers}
|
|
144
|
-
editorLineNumbersRef=${editorLineNumbersRef}
|
|
145
|
-
editorLineNumberRowRefs=${editorLineNumberRowRefs}
|
|
146
|
-
shouldUseHighlightedEditor=${shouldUseHighlightedEditor}
|
|
147
|
-
highlightedEditorLines=${highlightedEditorLines}
|
|
148
|
-
editorHighlightRef=${editorHighlightRef}
|
|
149
|
-
editorHighlightLineRefs=${editorHighlightLineRefs}
|
|
150
|
-
editorTextareaRef=${editorTextareaRef}
|
|
151
|
-
renderContent=${promptValue}
|
|
152
|
-
handleContentInput=${(event) => onChangePrompt(event.target.value)}
|
|
153
|
-
handleEditorKeyDown=${handleEditorKeyDown}
|
|
154
|
-
handleEditorScroll=${handleEditorScroll}
|
|
155
|
-
handleEditorSelectionChange=${() => {}}
|
|
156
|
-
isEditBlocked=${false}
|
|
157
|
-
isPreviewOnly=${false}
|
|
158
|
-
/>
|
|
159
|
-
</div>
|
|
160
|
-
</section>
|
|
161
|
-
`;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const kMetaCardClassName = "ac-surface-inset rounded-lg p-2.5 space-y-1.5";
|
|
165
12
|
const kRunStatusFilterOptions = [
|
|
166
13
|
{ label: "all", value: "all" },
|
|
167
14
|
{ label: "ok", value: "ok" },
|
|
168
15
|
{ label: "error", value: "error" },
|
|
169
16
|
{ label: "skipped", value: "skipped" },
|
|
170
17
|
];
|
|
171
|
-
const kSessionTargetOptions = [
|
|
172
|
-
{ label: "main", value: "main" },
|
|
173
|
-
{ label: "isolated", value: "isolated" },
|
|
174
|
-
];
|
|
175
|
-
const kWakeModeOptions = [
|
|
176
|
-
{ label: "now", value: "now" },
|
|
177
|
-
{ label: "next-heartbeat", value: "next-heartbeat" },
|
|
178
|
-
];
|
|
179
|
-
const kDeliveryNoneValue = "__none__";
|
|
180
|
-
const isSameCalendarDay = (leftDate, rightDate) =>
|
|
181
|
-
leftDate.getFullYear() === rightDate.getFullYear() &&
|
|
182
|
-
leftDate.getMonth() === rightDate.getMonth() &&
|
|
183
|
-
leftDate.getDate() === rightDate.getDate();
|
|
184
|
-
|
|
185
|
-
const formatCompactMeridiemTime = (dateValue) =>
|
|
186
|
-
dateValue
|
|
187
|
-
.toLocaleTimeString([], {
|
|
188
|
-
hour: "numeric",
|
|
189
|
-
minute: "2-digit",
|
|
190
|
-
})
|
|
191
|
-
.replace(/\s*([AP])M$/i, (_, marker) => `${String(marker || "").toLowerCase()}m`)
|
|
192
|
-
.replace(/\s+/g, "");
|
|
193
|
-
|
|
194
|
-
const formatNextRunAbsolute = (value) => {
|
|
195
|
-
const timestamp = Number(value || 0);
|
|
196
|
-
if (!Number.isFinite(timestamp) || timestamp <= 0) return "—";
|
|
197
|
-
const dateValue = new Date(timestamp);
|
|
198
|
-
if (Number.isNaN(dateValue.getTime())) return "—";
|
|
199
|
-
const nowValue = new Date();
|
|
200
|
-
const tomorrowValue = new Date(nowValue);
|
|
201
|
-
tomorrowValue.setDate(nowValue.getDate() + 1);
|
|
202
|
-
const isToday = isSameCalendarDay(dateValue, nowValue);
|
|
203
|
-
const isTomorrow = isSameCalendarDay(dateValue, tomorrowValue);
|
|
204
|
-
const compactTime = formatCompactMeridiemTime(dateValue);
|
|
205
|
-
if (isToday) return compactTime;
|
|
206
|
-
if (isTomorrow) return `Tomorrow ${compactTime}`;
|
|
207
|
-
return `${dateValue.toLocaleDateString()} ${compactTime}`;
|
|
208
|
-
};
|
|
209
18
|
|
|
210
19
|
export const CronJobDetail = ({
|
|
211
20
|
job = null,
|
|
@@ -244,181 +53,47 @@ export const CronJobDetail = ({
|
|
|
244
53
|
`;
|
|
245
54
|
}
|
|
246
55
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!key) return;
|
|
265
|
-
if (key === selectedKey) selectedPresent = true;
|
|
266
|
-
const label = String(sessionRow?.label || sessionRow?.key || "Session").trim();
|
|
267
|
-
const dedupeKey = label.toLowerCase();
|
|
268
|
-
if (seenLabels.has(dedupeKey)) return;
|
|
269
|
-
seenLabels.add(dedupeKey);
|
|
270
|
-
deduped.push(sessionRow);
|
|
271
|
-
});
|
|
272
|
-
if (!selectedPresent && selectedKey) {
|
|
273
|
-
const selectedRow = (Array.isArray(deliverySessions) ? deliverySessions : []).find(
|
|
274
|
-
(sessionRow) => String(sessionRow?.key || "").trim() === selectedKey,
|
|
275
|
-
);
|
|
276
|
-
if (selectedRow) deduped.unshift(selectedRow);
|
|
277
|
-
}
|
|
278
|
-
return deduped;
|
|
279
|
-
}, [deliverySessions, destinationSessionKey]);
|
|
280
|
-
const deliverySelectValue =
|
|
281
|
-
deliveryMode === "announce" && String(destinationSessionKey || "").trim()
|
|
282
|
-
? String(destinationSessionKey || "")
|
|
283
|
-
: kDeliveryNoneValue;
|
|
284
|
-
const isRoutingDirty =
|
|
285
|
-
sessionTarget !== currentSessionTarget ||
|
|
286
|
-
wakeMode !== currentWakeMode ||
|
|
287
|
-
deliveryMode !== currentDeliveryMode;
|
|
56
|
+
const isRoutingDirty = useMemo(() => {
|
|
57
|
+
const sessionTarget = String(
|
|
58
|
+
routingDraft?.sessionTarget || job?.sessionTarget || "main",
|
|
59
|
+
);
|
|
60
|
+
const wakeMode = String(routingDraft?.wakeMode || job?.wakeMode || "now");
|
|
61
|
+
const deliveryMode = String(
|
|
62
|
+
routingDraft?.deliveryMode || job?.delivery?.mode || "none",
|
|
63
|
+
);
|
|
64
|
+
const currentSessionTarget = String(job?.sessionTarget || "main");
|
|
65
|
+
const currentWakeMode = String(job?.wakeMode || "now");
|
|
66
|
+
const currentDeliveryMode = String(job?.delivery?.mode || "none");
|
|
67
|
+
return (
|
|
68
|
+
sessionTarget !== currentSessionTarget ||
|
|
69
|
+
wakeMode !== currentWakeMode ||
|
|
70
|
+
deliveryMode !== currentDeliveryMode
|
|
71
|
+
);
|
|
72
|
+
}, [job, routingDraft?.deliveryMode, routingDraft?.sessionTarget, routingDraft?.wakeMode]);
|
|
288
73
|
const isPromptDirty = promptValue !== savedPromptValue;
|
|
289
74
|
const hasUnsavedChanges = isRoutingDirty || isPromptDirty;
|
|
290
75
|
|
|
291
76
|
return html`
|
|
292
77
|
<div class="cron-detail-scroll">
|
|
293
78
|
<div class="cron-detail-content">
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
311
|
-
<div class=${kMetaCardClassName}>
|
|
312
|
-
<div class="text-gray-500">Schedule</div>
|
|
313
|
-
<div class="text-gray-300 font-mono">
|
|
314
|
-
${formatCronScheduleLabel(job.schedule, {
|
|
315
|
-
includeTimeZoneWhenDifferent: true,
|
|
316
|
-
})}
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
<div class=${kMetaCardClassName}>
|
|
320
|
-
<div class="text-gray-500">Next run</div>
|
|
321
|
-
<div class="text-gray-300 font-mono">
|
|
322
|
-
${formatNextRunAbsolute(job?.state?.nextRunAtMs)}
|
|
323
|
-
<span class="text-gray-500">
|
|
324
|
-
${` (${formatNextRunRelativeMs(job?.state?.nextRunAtMs)})`}
|
|
325
|
-
</span>
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
330
|
-
<div class=${kMetaCardClassName}>
|
|
331
|
-
<div class="text-gray-500">Session target</div>
|
|
332
|
-
<div class="pt-1">
|
|
333
|
-
<${SegmentedControl}
|
|
334
|
-
options=${kSessionTargetOptions}
|
|
335
|
-
value=${sessionTarget}
|
|
336
|
-
onChange=${(value) =>
|
|
337
|
-
onChangeRoutingDraft((currentValue = {}) => ({
|
|
338
|
-
...currentValue,
|
|
339
|
-
sessionTarget: String(value || "main"),
|
|
340
|
-
}))}
|
|
341
|
-
/>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
<div class=${kMetaCardClassName}>
|
|
345
|
-
<div class="text-gray-500">Wake mode</div>
|
|
346
|
-
<div class="pt-1">
|
|
347
|
-
<${SegmentedControl}
|
|
348
|
-
options=${kWakeModeOptions}
|
|
349
|
-
value=${wakeMode}
|
|
350
|
-
onChange=${(value) =>
|
|
351
|
-
onChangeRoutingDraft((currentValue = {}) => ({
|
|
352
|
-
...currentValue,
|
|
353
|
-
wakeMode: String(value || "now"),
|
|
354
|
-
}))}
|
|
355
|
-
/>
|
|
356
|
-
</div>
|
|
357
|
-
</div>
|
|
358
|
-
<div class=${kMetaCardClassName}>
|
|
359
|
-
<div class="text-gray-500">Delivery</div>
|
|
360
|
-
<div class="pt-1">
|
|
361
|
-
<select
|
|
362
|
-
value=${deliverySelectValue}
|
|
363
|
-
onInput=${(event) => {
|
|
364
|
-
const nextValue = String(event.currentTarget?.value || "");
|
|
365
|
-
if (!nextValue || nextValue === kDeliveryNoneValue) {
|
|
366
|
-
onChangeRoutingDraft((currentValue = {}) => ({
|
|
367
|
-
...currentValue,
|
|
368
|
-
deliveryMode: "none",
|
|
369
|
-
deliveryChannel: "",
|
|
370
|
-
deliveryTo: "",
|
|
371
|
-
}));
|
|
372
|
-
onChangeDestinationSessionKey("");
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
onChangeDestinationSessionKey(nextValue);
|
|
376
|
-
onChangeRoutingDraft((currentValue = {}) => ({
|
|
377
|
-
...currentValue,
|
|
378
|
-
deliveryMode: "announce",
|
|
379
|
-
}));
|
|
380
|
-
}}
|
|
381
|
-
disabled=${savingChanges}
|
|
382
|
-
class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-[11px] text-gray-200 focus:border-gray-500"
|
|
383
|
-
>
|
|
384
|
-
<option value=${kDeliveryNoneValue}>None</option>
|
|
385
|
-
${deliverySessionOptions.map(
|
|
386
|
-
(sessionRow) => html`
|
|
387
|
-
<option value=${String(sessionRow?.key || "")}>
|
|
388
|
-
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
389
|
-
</option>
|
|
390
|
-
`,
|
|
391
|
-
)}
|
|
392
|
-
</select>
|
|
393
|
-
</div>
|
|
394
|
-
${loadingDeliverySessions
|
|
395
|
-
? html`<div class="text-[11px] text-gray-500 pt-1">Loading delivery sessions...</div>`
|
|
396
|
-
: null}
|
|
397
|
-
${deliverySessionsError
|
|
398
|
-
? html`<div class="text-[11px] text-red-400 pt-1">${deliverySessionsError}</div>`
|
|
399
|
-
: null}
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
<div class="flex items-center justify-between gap-3">
|
|
403
|
-
<${ToggleSwitch}
|
|
404
|
-
checked=${job.enabled !== false}
|
|
405
|
-
disabled=${togglingJobEnabled || savingChanges}
|
|
406
|
-
onChange=${onToggleEnabled}
|
|
407
|
-
label=${job.enabled === false ? "Disabled" : "Enabled"}
|
|
408
|
-
/>
|
|
409
|
-
<${ActionButton}
|
|
410
|
-
onClick=${onRunNow}
|
|
411
|
-
loading=${runningJob}
|
|
412
|
-
disabled=${hasUnsavedChanges || savingChanges}
|
|
413
|
-
tone="secondary"
|
|
414
|
-
size="sm"
|
|
415
|
-
idleLabel="Run now"
|
|
416
|
-
loadingLabel="Running..."
|
|
417
|
-
/>
|
|
418
|
-
</div>
|
|
419
|
-
</section>
|
|
79
|
+
<${CronJobSettingsCard}
|
|
80
|
+
job=${job}
|
|
81
|
+
routingDraft=${routingDraft}
|
|
82
|
+
onChangeRoutingDraft=${onChangeRoutingDraft}
|
|
83
|
+
destinationSessionKey=${destinationSessionKey}
|
|
84
|
+
onChangeDestinationSessionKey=${onChangeDestinationSessionKey}
|
|
85
|
+
deliverySessions=${deliverySessions}
|
|
86
|
+
loadingDeliverySessions=${loadingDeliverySessions}
|
|
87
|
+
deliverySessionsError=${deliverySessionsError}
|
|
88
|
+
savingChanges=${savingChanges}
|
|
89
|
+
togglingJobEnabled=${togglingJobEnabled}
|
|
90
|
+
onToggleEnabled=${onToggleEnabled}
|
|
91
|
+
onRunNow=${onRunNow}
|
|
92
|
+
runningJob=${runningJob}
|
|
93
|
+
hasUnsavedChanges=${hasUnsavedChanges}
|
|
94
|
+
/>
|
|
420
95
|
|
|
421
|
-
<${
|
|
96
|
+
<${CronPromptEditor}
|
|
422
97
|
promptValue=${promptValue}
|
|
423
98
|
savedPromptValue=${savedPromptValue}
|
|
424
99
|
onChangePrompt=${onChangePrompt}
|
|
@@ -0,0 +1,233 @@
|
|
|
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 { ActionButton } from "../action-button.js";
|
|
5
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
6
|
+
import { ToggleSwitch } from "../toggle-switch.js";
|
|
7
|
+
import {
|
|
8
|
+
formatCronScheduleLabel,
|
|
9
|
+
formatNextRunRelativeMs,
|
|
10
|
+
} from "./cron-helpers.js";
|
|
11
|
+
|
|
12
|
+
const html = htm.bind(h);
|
|
13
|
+
const kMetaCardClassName = "ac-surface-inset rounded-lg p-2.5 space-y-1.5";
|
|
14
|
+
const kSessionTargetOptions = [
|
|
15
|
+
{ label: "main", value: "main" },
|
|
16
|
+
{ label: "isolated", value: "isolated" },
|
|
17
|
+
];
|
|
18
|
+
const kWakeModeOptions = [
|
|
19
|
+
{ label: "now", value: "now" },
|
|
20
|
+
{ label: "next-heartbeat", value: "next-heartbeat" },
|
|
21
|
+
];
|
|
22
|
+
const kDeliveryNoneValue = "__none__";
|
|
23
|
+
|
|
24
|
+
const isSameCalendarDay = (leftDate, rightDate) =>
|
|
25
|
+
leftDate.getFullYear() === rightDate.getFullYear() &&
|
|
26
|
+
leftDate.getMonth() === rightDate.getMonth() &&
|
|
27
|
+
leftDate.getDate() === rightDate.getDate();
|
|
28
|
+
|
|
29
|
+
const formatCompactMeridiemTime = (dateValue) =>
|
|
30
|
+
dateValue
|
|
31
|
+
.toLocaleTimeString([], {
|
|
32
|
+
hour: "numeric",
|
|
33
|
+
minute: "2-digit",
|
|
34
|
+
})
|
|
35
|
+
.replace(/\s*([AP])M$/i, (_, marker) =>
|
|
36
|
+
`${String(marker || "").toLowerCase()}m`,
|
|
37
|
+
)
|
|
38
|
+
.replace(/\s+/g, "");
|
|
39
|
+
|
|
40
|
+
const formatNextRunAbsolute = (value) => {
|
|
41
|
+
const timestamp = Number(value || 0);
|
|
42
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) return "—";
|
|
43
|
+
const dateValue = new Date(timestamp);
|
|
44
|
+
if (Number.isNaN(dateValue.getTime())) return "—";
|
|
45
|
+
const nowValue = new Date();
|
|
46
|
+
const tomorrowValue = new Date(nowValue);
|
|
47
|
+
tomorrowValue.setDate(nowValue.getDate() + 1);
|
|
48
|
+
const isToday = isSameCalendarDay(dateValue, nowValue);
|
|
49
|
+
const isTomorrow = isSameCalendarDay(dateValue, tomorrowValue);
|
|
50
|
+
const compactTime = formatCompactMeridiemTime(dateValue);
|
|
51
|
+
if (isToday) return compactTime;
|
|
52
|
+
if (isTomorrow) return `Tomorrow ${compactTime}`;
|
|
53
|
+
return `${dateValue.toLocaleDateString()} ${compactTime}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const CronJobSettingsCard = ({
|
|
57
|
+
job = null,
|
|
58
|
+
routingDraft = null,
|
|
59
|
+
onChangeRoutingDraft = () => {},
|
|
60
|
+
destinationSessionKey = "",
|
|
61
|
+
onChangeDestinationSessionKey = () => {},
|
|
62
|
+
deliverySessions = [],
|
|
63
|
+
loadingDeliverySessions = false,
|
|
64
|
+
deliverySessionsError = "",
|
|
65
|
+
savingChanges = false,
|
|
66
|
+
togglingJobEnabled = false,
|
|
67
|
+
onToggleEnabled = () => {},
|
|
68
|
+
onRunNow = () => {},
|
|
69
|
+
runningJob = false,
|
|
70
|
+
hasUnsavedChanges = false,
|
|
71
|
+
}) => {
|
|
72
|
+
if (!job) return null;
|
|
73
|
+
|
|
74
|
+
const sessionTarget = String(
|
|
75
|
+
routingDraft?.sessionTarget || job?.sessionTarget || "main",
|
|
76
|
+
);
|
|
77
|
+
const wakeMode = String(routingDraft?.wakeMode || job?.wakeMode || "now");
|
|
78
|
+
const deliveryMode = String(
|
|
79
|
+
routingDraft?.deliveryMode || job?.delivery?.mode || "none",
|
|
80
|
+
);
|
|
81
|
+
const deliverySessionOptions = useMemo(() => {
|
|
82
|
+
const seenLabels = new Set();
|
|
83
|
+
const deduped = [];
|
|
84
|
+
const selectedKey = String(destinationSessionKey || "").trim();
|
|
85
|
+
let selectedPresent = false;
|
|
86
|
+
(Array.isArray(deliverySessions) ? deliverySessions : []).forEach(
|
|
87
|
+
(sessionRow) => {
|
|
88
|
+
const key = String(sessionRow?.key || "").trim();
|
|
89
|
+
if (!key) return;
|
|
90
|
+
if (key === selectedKey) selectedPresent = true;
|
|
91
|
+
const label = String(
|
|
92
|
+
sessionRow?.label || sessionRow?.key || "Session",
|
|
93
|
+
).trim();
|
|
94
|
+
const dedupeKey = label.toLowerCase();
|
|
95
|
+
if (seenLabels.has(dedupeKey)) return;
|
|
96
|
+
seenLabels.add(dedupeKey);
|
|
97
|
+
deduped.push(sessionRow);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
if (!selectedPresent && selectedKey) {
|
|
101
|
+
const selectedRow = (
|
|
102
|
+
Array.isArray(deliverySessions) ? deliverySessions : []
|
|
103
|
+
).find((sessionRow) => String(sessionRow?.key || "").trim() === selectedKey);
|
|
104
|
+
if (selectedRow) deduped.unshift(selectedRow);
|
|
105
|
+
}
|
|
106
|
+
return deduped;
|
|
107
|
+
}, [deliverySessions, destinationSessionKey]);
|
|
108
|
+
const deliverySelectValue =
|
|
109
|
+
deliveryMode === "announce" && String(destinationSessionKey || "").trim()
|
|
110
|
+
? String(destinationSessionKey || "")
|
|
111
|
+
: kDeliveryNoneValue;
|
|
112
|
+
|
|
113
|
+
return html`
|
|
114
|
+
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
115
|
+
<div class="flex items-center justify-between gap-3">
|
|
116
|
+
<div class="text-xs text-gray-500">ID: <code>${job.id}</code></div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
119
|
+
<div class=${kMetaCardClassName}>
|
|
120
|
+
<div class="text-gray-500">Schedule</div>
|
|
121
|
+
<div class="text-gray-300 font-mono">
|
|
122
|
+
${formatCronScheduleLabel(job.schedule, {
|
|
123
|
+
includeTimeZoneWhenDifferent: true,
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div class=${kMetaCardClassName}>
|
|
128
|
+
<div class="text-gray-500">Next run</div>
|
|
129
|
+
<div class="text-gray-300 font-mono">
|
|
130
|
+
${formatNextRunAbsolute(job?.state?.nextRunAtMs)}
|
|
131
|
+
<span class="text-gray-500">
|
|
132
|
+
${` (${formatNextRunRelativeMs(job?.state?.nextRunAtMs)})`}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
138
|
+
<div class=${kMetaCardClassName}>
|
|
139
|
+
<div class="text-gray-500">Session target</div>
|
|
140
|
+
<div class="pt-1">
|
|
141
|
+
<${SegmentedControl}
|
|
142
|
+
options=${kSessionTargetOptions}
|
|
143
|
+
value=${sessionTarget}
|
|
144
|
+
onChange=${(value) =>
|
|
145
|
+
onChangeRoutingDraft((currentValue = {}) => ({
|
|
146
|
+
...currentValue,
|
|
147
|
+
sessionTarget: String(value || "main"),
|
|
148
|
+
}))}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class=${kMetaCardClassName}>
|
|
153
|
+
<div class="text-gray-500">Wake mode</div>
|
|
154
|
+
<div class="pt-1">
|
|
155
|
+
<${SegmentedControl}
|
|
156
|
+
options=${kWakeModeOptions}
|
|
157
|
+
value=${wakeMode}
|
|
158
|
+
onChange=${(value) =>
|
|
159
|
+
onChangeRoutingDraft((currentValue = {}) => ({
|
|
160
|
+
...currentValue,
|
|
161
|
+
wakeMode: String(value || "now"),
|
|
162
|
+
}))}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class=${kMetaCardClassName}>
|
|
167
|
+
<div class="text-gray-500">Delivery</div>
|
|
168
|
+
<div class="pt-1">
|
|
169
|
+
<select
|
|
170
|
+
value=${deliverySelectValue}
|
|
171
|
+
onInput=${(event) => {
|
|
172
|
+
const nextValue = String(event.currentTarget?.value || "");
|
|
173
|
+
if (!nextValue || nextValue === kDeliveryNoneValue) {
|
|
174
|
+
onChangeRoutingDraft((currentValue = {}) => ({
|
|
175
|
+
...currentValue,
|
|
176
|
+
deliveryMode: "none",
|
|
177
|
+
deliveryChannel: "",
|
|
178
|
+
deliveryTo: "",
|
|
179
|
+
}));
|
|
180
|
+
onChangeDestinationSessionKey("");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
onChangeDestinationSessionKey(nextValue);
|
|
184
|
+
onChangeRoutingDraft((currentValue = {}) => ({
|
|
185
|
+
...currentValue,
|
|
186
|
+
deliveryMode: "announce",
|
|
187
|
+
}));
|
|
188
|
+
}}
|
|
189
|
+
disabled=${savingChanges}
|
|
190
|
+
class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-[11px] text-gray-200 focus:border-gray-500"
|
|
191
|
+
>
|
|
192
|
+
<option value=${kDeliveryNoneValue}>None</option>
|
|
193
|
+
${deliverySessionOptions.map(
|
|
194
|
+
(sessionRow) => html`
|
|
195
|
+
<option value=${String(sessionRow?.key || "")}>
|
|
196
|
+
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
197
|
+
</option>
|
|
198
|
+
`,
|
|
199
|
+
)}
|
|
200
|
+
</select>
|
|
201
|
+
</div>
|
|
202
|
+
${loadingDeliverySessions
|
|
203
|
+
? html`<div class="text-[11px] text-gray-500 pt-1">
|
|
204
|
+
Loading delivery sessions...
|
|
205
|
+
</div>`
|
|
206
|
+
: null}
|
|
207
|
+
${deliverySessionsError
|
|
208
|
+
? html`<div class="text-[11px] text-red-400 pt-1">
|
|
209
|
+
${deliverySessionsError}
|
|
210
|
+
</div>`
|
|
211
|
+
: null}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="flex items-center justify-between gap-3">
|
|
215
|
+
<${ToggleSwitch}
|
|
216
|
+
checked=${job.enabled !== false}
|
|
217
|
+
disabled=${togglingJobEnabled || savingChanges}
|
|
218
|
+
onChange=${onToggleEnabled}
|
|
219
|
+
label=${job.enabled === false ? "Disabled" : "Enabled"}
|
|
220
|
+
/>
|
|
221
|
+
<${ActionButton}
|
|
222
|
+
onClick=${onRunNow}
|
|
223
|
+
loading=${runningJob}
|
|
224
|
+
disabled=${hasUnsavedChanges || savingChanges}
|
|
225
|
+
tone="secondary"
|
|
226
|
+
size="sm"
|
|
227
|
+
idleLabel="Run now"
|
|
228
|
+
loadingLabel="Running..."
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</section>
|
|
232
|
+
`;
|
|
233
|
+
};
|