@syncended/dsh-automations 0.1.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/LICENSE +21 -0
- package/README.md +128 -0
- package/cordis.patch.yml +5 -0
- package/dist/agent-executor.d.ts +15 -0
- package/dist/agent-executor.d.ts.map +1 -0
- package/dist/agent-executor.js +173 -0
- package/dist/agent-executor.js.map +1 -0
- package/dist/cron.d.ts +15 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +74 -0
- package/dist/cron.js.map +1 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +203 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/dist/project-policy.d.ts +9 -0
- package/dist/project-policy.d.ts.map +1 -0
- package/dist/project-policy.js +53 -0
- package/dist/project-policy.js.map +1 -0
- package/dist/scheduler.d.ts +58 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +518 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/state.d.ts +8 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +44 -0
- package/dist/state.js.map +1 -0
- package/dist/store.d.ts +21 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +317 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +169 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +13 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +159 -0
- package/dist/validation.js.map +1 -0
- package/docs/architecture.md +195 -0
- package/docs/http-api.md +40 -0
- package/docs/security.md +51 -0
- package/lib/client.js +1251 -0
- package/package.json +105 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@syncended/dsh-automations",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const module = { exports: {} };
|
|
5
|
+
const exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
|
|
8
|
+
// The full settings UI is registered by this browser entry. It is kept as
|
|
9
|
+
// plain Client Plugin JavaScript so external package installs need no DSH
|
|
10
|
+
// monorepo build tooling. The page talks to the automations service over
|
|
11
|
+
// the same-origin HTTP API at /api/automations; every mutation carries the
|
|
12
|
+
// x-dsh-automation-client fence header plus an application/json body.
|
|
13
|
+
const React = require("react");
|
|
14
|
+
const h = React.createElement;
|
|
15
|
+
const { useCallback, useEffect, useRef, useState } = React;
|
|
16
|
+
const inject = ["slots"];
|
|
17
|
+
|
|
18
|
+
const API_PREFIX = "/api/automations";
|
|
19
|
+
const POLL_MS = 5000;
|
|
20
|
+
const HISTORY_SHOWN = 25;
|
|
21
|
+
const DEFAULT_CRON = "0 9 * * 1-5";
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 3600000;
|
|
23
|
+
const MIN_TIMEOUT_MS = 1000;
|
|
24
|
+
const MAX_TIMEOUT_MS = 86400000;
|
|
25
|
+
const JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
26
|
+
|
|
27
|
+
const BROWSER_TIMEZONE = (() => {
|
|
28
|
+
try {
|
|
29
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
30
|
+
} catch {
|
|
31
|
+
return "UTC";
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
const TIMEZONES = Array.from(
|
|
36
|
+
new Set([
|
|
37
|
+
BROWSER_TIMEZONE,
|
|
38
|
+
"UTC",
|
|
39
|
+
"America/New_York",
|
|
40
|
+
"America/Chicago",
|
|
41
|
+
"America/Denver",
|
|
42
|
+
"America/Los_Angeles",
|
|
43
|
+
"Europe/London",
|
|
44
|
+
"Europe/Paris",
|
|
45
|
+
"Europe/Berlin",
|
|
46
|
+
"Asia/Shanghai",
|
|
47
|
+
"Asia/Tokyo",
|
|
48
|
+
"Asia/Singapore",
|
|
49
|
+
"Asia/Kolkata",
|
|
50
|
+
"Australia/Sydney",
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const REASONING_EFFORTS = ["low", "medium", "high"];
|
|
55
|
+
const OVERLAP_OPTIONS = [
|
|
56
|
+
{ value: "skip", label: "skip", hint: "Skip the run if a previous run is still active." },
|
|
57
|
+
{ value: "queue", label: "queue", hint: "Defer the run until the previous run finishes." },
|
|
58
|
+
{ value: "allow", label: "allow", hint: "Run concurrently with any active run." },
|
|
59
|
+
];
|
|
60
|
+
const MISFIRE_OPTIONS = [
|
|
61
|
+
{ value: "skip", label: "skip", hint: "Drop occurrences that were missed while the scheduler was down." },
|
|
62
|
+
{ value: "run-once", label: "run-once", hint: "Run once after downtime for the latest missed occurrence." },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function errMessage(error) {
|
|
66
|
+
return error instanceof Error ? error.message : String(error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function browserTimezone() {
|
|
70
|
+
return BROWSER_TIMEZONE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Mirrors the server-side slugifyJobId (validation.ts) so a freshly typed
|
|
74
|
+
// name yields the same stable id the service would allocate itself.
|
|
75
|
+
function slugifyJobId(name) {
|
|
76
|
+
const slug = name
|
|
77
|
+
.normalize("NFKD")
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
80
|
+
.replace(/^-+|-+$/g, "")
|
|
81
|
+
.slice(0, 48);
|
|
82
|
+
return slug === "" ? "automation" : slug;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatDate(value) {
|
|
86
|
+
if (!value) return "—";
|
|
87
|
+
const date = new Date(value);
|
|
88
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
89
|
+
try {
|
|
90
|
+
return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
91
|
+
} catch {
|
|
92
|
+
return date.toLocaleString();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDuration(run) {
|
|
97
|
+
const start = run.startedAt ? Date.parse(run.startedAt) : NaN;
|
|
98
|
+
if (Number.isNaN(start)) return "";
|
|
99
|
+
const end = run.finishedAt ? Date.parse(run.finishedAt) : Date.now();
|
|
100
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
101
|
+
if (seconds < 60) return seconds + "s";
|
|
102
|
+
const minutes = Math.floor(seconds / 60);
|
|
103
|
+
const rest = seconds % 60;
|
|
104
|
+
if (minutes < 60) return rest > 0 ? minutes + "m " + rest + "s" : minutes + "m";
|
|
105
|
+
const hours = Math.floor(minutes / 60);
|
|
106
|
+
return minutes % 60 > 0 ? hours + "h " + (minutes % 60) + "m" : hours + "h";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatTimeout(ms) {
|
|
110
|
+
if (!Number.isFinite(ms)) return "";
|
|
111
|
+
if (ms % 3600000 === 0) return ms / 3600000 + "h";
|
|
112
|
+
if (ms % 60000 === 0) return ms / 60000 + "m";
|
|
113
|
+
return Math.round(ms / 1000) + "s";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function apiFetch(path, options = {}) {
|
|
117
|
+
const headers = Object.assign(
|
|
118
|
+
{ "content-type": "application/json", "x-dsh-automation-client": "1" },
|
|
119
|
+
options.headers,
|
|
120
|
+
);
|
|
121
|
+
let response;
|
|
122
|
+
try {
|
|
123
|
+
response = await fetch(API_PREFIX + path, Object.assign({}, options, { headers }));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error("Network error: " + errMessage(error));
|
|
126
|
+
}
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
let body = null;
|
|
129
|
+
if (text) {
|
|
130
|
+
try {
|
|
131
|
+
body = JSON.parse(text);
|
|
132
|
+
} catch {
|
|
133
|
+
body = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const message =
|
|
138
|
+
body && body.error && typeof body.error.message === "string"
|
|
139
|
+
? body.error.message
|
|
140
|
+
: "Request failed (" + response.status + ").";
|
|
141
|
+
const error = new Error(message);
|
|
142
|
+
error.status = response.status;
|
|
143
|
+
error.code = body && body.error ? body.error.code : undefined;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
return body;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function emptyDraft(meta) {
|
|
150
|
+
const presets = meta && Array.isArray(meta.permissionPresets) ? meta.permissionPresets : [];
|
|
151
|
+
const permissionPreset = presets.indexOf("workspace-write") !== -1
|
|
152
|
+
? "workspace-write"
|
|
153
|
+
: presets.length > 0
|
|
154
|
+
? presets[0]
|
|
155
|
+
: "workspace-write";
|
|
156
|
+
return {
|
|
157
|
+
id: "",
|
|
158
|
+
name: "",
|
|
159
|
+
enabled: true,
|
|
160
|
+
cron: DEFAULT_CRON,
|
|
161
|
+
timezone: browserTimezone(),
|
|
162
|
+
prompt: "",
|
|
163
|
+
cwd: "",
|
|
164
|
+
provider: "",
|
|
165
|
+
model: "",
|
|
166
|
+
reasoningEffort: "",
|
|
167
|
+
agentPreset: "",
|
|
168
|
+
permissionPreset,
|
|
169
|
+
timeoutMs: String(DEFAULT_TIMEOUT_MS),
|
|
170
|
+
overlap: "skip",
|
|
171
|
+
misfire: "run-once",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function draftFromJob(job) {
|
|
176
|
+
return {
|
|
177
|
+
id: job.id,
|
|
178
|
+
name: job.name,
|
|
179
|
+
enabled: job.enabled,
|
|
180
|
+
cron: job.schedule.cron,
|
|
181
|
+
timezone: job.schedule.timezone,
|
|
182
|
+
prompt: job.task.prompt,
|
|
183
|
+
cwd: job.execution.cwd,
|
|
184
|
+
provider: job.execution.provider || "",
|
|
185
|
+
model: job.execution.model || "",
|
|
186
|
+
reasoningEffort: job.execution.reasoningEffort || "",
|
|
187
|
+
agentPreset: job.execution.agentPreset || "",
|
|
188
|
+
permissionPreset: job.execution.permissionPreset,
|
|
189
|
+
timeoutMs: String(job.execution.timeoutMs),
|
|
190
|
+
overlap: job.policies.overlap,
|
|
191
|
+
misfire: job.policies.misfire,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildSpec(draft) {
|
|
196
|
+
const execution = {
|
|
197
|
+
cwd: draft.cwd.trim(),
|
|
198
|
+
permissionPreset: draft.permissionPreset,
|
|
199
|
+
timeoutMs: Number(draft.timeoutMs),
|
|
200
|
+
};
|
|
201
|
+
const provider = draft.provider.trim();
|
|
202
|
+
if (provider !== "") {
|
|
203
|
+
execution.provider = provider;
|
|
204
|
+
execution.model = draft.model.trim();
|
|
205
|
+
}
|
|
206
|
+
const reasoningEffort = draft.reasoningEffort.trim();
|
|
207
|
+
if (reasoningEffort !== "") execution.reasoningEffort = reasoningEffort;
|
|
208
|
+
const agentPreset = draft.agentPreset.trim();
|
|
209
|
+
if (agentPreset !== "") execution.agentPreset = agentPreset;
|
|
210
|
+
return {
|
|
211
|
+
name: draft.name.trim(),
|
|
212
|
+
enabled: draft.enabled,
|
|
213
|
+
schedule: { cron: draft.cron.trim(), timezone: draft.timezone.trim() },
|
|
214
|
+
task: { kind: "agent", prompt: draft.prompt },
|
|
215
|
+
execution,
|
|
216
|
+
policies: { overlap: draft.overlap, misfire: draft.misfire },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function Field(props) {
|
|
221
|
+
const { label, htmlFor, required, hint, full, children } = props;
|
|
222
|
+
return h(
|
|
223
|
+
"div",
|
|
224
|
+
{ className: "dsh-auto-field" + (full ? " dsh-auto-field-full" : "") },
|
|
225
|
+
h(
|
|
226
|
+
"label",
|
|
227
|
+
{ className: "dsh-auto-label", htmlFor },
|
|
228
|
+
label,
|
|
229
|
+
required ? h("span", { className: "dsh-auto-required", "aria-hidden": "true" }, " *") : null,
|
|
230
|
+
),
|
|
231
|
+
children,
|
|
232
|
+
hint ? h("p", { className: "dsh-auto-hint" }, hint) : null,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function StatusPill(props) {
|
|
237
|
+
return h("span", { className: "dsh-auto-status dsh-auto-status-" + props.status }, props.status);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function JobForm(props) {
|
|
241
|
+
const { meta, draft, editing, saving, error, onChange, onIdTouched, onSubmit, onCancel } = props;
|
|
242
|
+
const providers = meta && Array.isArray(meta.providers) ? meta.providers : [];
|
|
243
|
+
const providerModels = providers.find((provider) => provider.id === draft.provider);
|
|
244
|
+
const models = providerModels ? providerModels.models : [];
|
|
245
|
+
const permissionPresets =
|
|
246
|
+
meta && Array.isArray(meta.permissionPresets) && meta.permissionPresets.length > 0
|
|
247
|
+
? meta.permissionPresets
|
|
248
|
+
: ["workspace-write"];
|
|
249
|
+
const agentPresets = meta && Array.isArray(meta.agentPresets) ? meta.agentPresets : [];
|
|
250
|
+
const creating = editing === null;
|
|
251
|
+
|
|
252
|
+
return h(
|
|
253
|
+
"form",
|
|
254
|
+
{
|
|
255
|
+
className: "dsh-auto-form",
|
|
256
|
+
onSubmit,
|
|
257
|
+
noValidate: true,
|
|
258
|
+
"aria-label": creating ? "New automation" : "Edit automation",
|
|
259
|
+
},
|
|
260
|
+
h("h3", null, creating ? "New automation" : "Edit \u201c" + editing.name + "\u201d"),
|
|
261
|
+
h(
|
|
262
|
+
"div",
|
|
263
|
+
{ className: "dsh-auto-formgrid" },
|
|
264
|
+
h(
|
|
265
|
+
Field,
|
|
266
|
+
{ label: "Name", htmlFor: "dsh-auto-f-name", required: true },
|
|
267
|
+
h("input", {
|
|
268
|
+
id: "dsh-auto-f-name",
|
|
269
|
+
className: "dsh-auto-input",
|
|
270
|
+
type: "text",
|
|
271
|
+
value: draft.name,
|
|
272
|
+
autoFocus: true,
|
|
273
|
+
placeholder: "e.g. Morning standup notes",
|
|
274
|
+
onChange: (event) => onChange("name", event.target.value),
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
creating
|
|
278
|
+
? h(
|
|
279
|
+
Field,
|
|
280
|
+
{ label: "Job id", htmlFor: "dsh-auto-f-id", hint: "Lowercase letters, digits and hyphens; auto-derived from the name. Leave blank to let the server generate one." },
|
|
281
|
+
h("input", {
|
|
282
|
+
id: "dsh-auto-f-id",
|
|
283
|
+
className: "dsh-auto-input",
|
|
284
|
+
type: "text",
|
|
285
|
+
value: draft.id,
|
|
286
|
+
spellCheck: false,
|
|
287
|
+
placeholder: "auto",
|
|
288
|
+
onChange: (event) => {
|
|
289
|
+
onIdTouched();
|
|
290
|
+
onChange("id", event.target.value);
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
)
|
|
294
|
+
: h(
|
|
295
|
+
Field,
|
|
296
|
+
{ label: "Job id", htmlFor: "dsh-auto-f-id", hint: "Fixed after creation." },
|
|
297
|
+
h("input", {
|
|
298
|
+
id: "dsh-auto-f-id",
|
|
299
|
+
className: "dsh-auto-input",
|
|
300
|
+
type: "text",
|
|
301
|
+
value: draft.id,
|
|
302
|
+
disabled: true,
|
|
303
|
+
spellCheck: false,
|
|
304
|
+
}),
|
|
305
|
+
),
|
|
306
|
+
h(
|
|
307
|
+
Field,
|
|
308
|
+
{ label: "Cron", htmlFor: "dsh-auto-f-cron", required: true, hint: "Five-field cron (minute hour day-of-month month day-of-week)." },
|
|
309
|
+
h("input", {
|
|
310
|
+
id: "dsh-auto-f-cron",
|
|
311
|
+
className: "dsh-auto-input",
|
|
312
|
+
type: "text",
|
|
313
|
+
value: draft.cron,
|
|
314
|
+
spellCheck: false,
|
|
315
|
+
onChange: (event) => onChange("cron", event.target.value),
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
h(
|
|
319
|
+
Field,
|
|
320
|
+
{ label: "Timezone", htmlFor: "dsh-auto-f-timezone", required: true },
|
|
321
|
+
h("input", {
|
|
322
|
+
id: "dsh-auto-f-timezone",
|
|
323
|
+
className: "dsh-auto-input",
|
|
324
|
+
type: "text",
|
|
325
|
+
list: "dsh-auto-tz-list",
|
|
326
|
+
value: draft.timezone,
|
|
327
|
+
spellCheck: false,
|
|
328
|
+
onChange: (event) => onChange("timezone", event.target.value),
|
|
329
|
+
}),
|
|
330
|
+
h(
|
|
331
|
+
"datalist",
|
|
332
|
+
{ id: "dsh-auto-tz-list" },
|
|
333
|
+
TIMEZONES.map((zone) => h("option", { key: zone, value: zone })),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
h(
|
|
337
|
+
Field,
|
|
338
|
+
{ label: "Enabled", htmlFor: "dsh-auto-f-enabled" },
|
|
339
|
+
h(
|
|
340
|
+
"label",
|
|
341
|
+
{ className: "dsh-auto-check" },
|
|
342
|
+
h("input", {
|
|
343
|
+
id: "dsh-auto-f-enabled",
|
|
344
|
+
type: "checkbox",
|
|
345
|
+
checked: draft.enabled,
|
|
346
|
+
onChange: (event) => onChange("enabled", event.target.checked),
|
|
347
|
+
}),
|
|
348
|
+
h("span", null, "Schedule is active"),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
h(
|
|
352
|
+
Field,
|
|
353
|
+
{ label: "Timeout (ms)", htmlFor: "dsh-auto-f-timeout", required: true, hint: "Wall-clock limit for each run (1s to 24h)." },
|
|
354
|
+
h("input", {
|
|
355
|
+
id: "dsh-auto-f-timeout",
|
|
356
|
+
className: "dsh-auto-input",
|
|
357
|
+
type: "number",
|
|
358
|
+
min: MIN_TIMEOUT_MS,
|
|
359
|
+
max: MAX_TIMEOUT_MS,
|
|
360
|
+
step: 1000,
|
|
361
|
+
value: draft.timeoutMs,
|
|
362
|
+
onChange: (event) => onChange("timeoutMs", event.target.value),
|
|
363
|
+
}),
|
|
364
|
+
),
|
|
365
|
+
h(
|
|
366
|
+
Field,
|
|
367
|
+
{ label: "Provider", htmlFor: "dsh-auto-f-provider", hint: "Blank uses the current Harness default. You may type an unlisted provider route." },
|
|
368
|
+
h("input", {
|
|
369
|
+
id: "dsh-auto-f-provider",
|
|
370
|
+
className: "dsh-auto-input",
|
|
371
|
+
type: "text",
|
|
372
|
+
list: "dsh-auto-provider-list",
|
|
373
|
+
value: draft.provider,
|
|
374
|
+
spellCheck: false,
|
|
375
|
+
placeholder: "Harness default",
|
|
376
|
+
onChange: (event) => onChange("provider", event.target.value),
|
|
377
|
+
}),
|
|
378
|
+
h(
|
|
379
|
+
"datalist",
|
|
380
|
+
{ id: "dsh-auto-provider-list" },
|
|
381
|
+
providers.map((provider) => h("option", { key: provider.id, value: provider.id })),
|
|
382
|
+
),
|
|
383
|
+
),
|
|
384
|
+
h(
|
|
385
|
+
Field,
|
|
386
|
+
{
|
|
387
|
+
label: "Model",
|
|
388
|
+
htmlFor: "dsh-auto-f-model",
|
|
389
|
+
required: draft.provider !== "",
|
|
390
|
+
hint: draft.provider === ""
|
|
391
|
+
? "Set a provider first, or leave both blank for the Harness default."
|
|
392
|
+
: "Choose a discovered model or type an adapter-supported model id.",
|
|
393
|
+
},
|
|
394
|
+
h("input", {
|
|
395
|
+
id: "dsh-auto-f-model",
|
|
396
|
+
className: "dsh-auto-input",
|
|
397
|
+
type: "text",
|
|
398
|
+
list: "dsh-auto-model-list",
|
|
399
|
+
value: draft.model,
|
|
400
|
+
spellCheck: false,
|
|
401
|
+
placeholder: draft.provider === "" ? "Harness default" : "Model id",
|
|
402
|
+
disabled: draft.provider === "",
|
|
403
|
+
onChange: (event) => onChange("model", event.target.value),
|
|
404
|
+
}),
|
|
405
|
+
h(
|
|
406
|
+
"datalist",
|
|
407
|
+
{ id: "dsh-auto-model-list" },
|
|
408
|
+
models.map((model) => h("option", { key: model, value: model })),
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
h(
|
|
412
|
+
Field,
|
|
413
|
+
{ label: "Reasoning effort", htmlFor: "dsh-auto-f-effort", hint: "Blank uses the provider default." },
|
|
414
|
+
h("input", {
|
|
415
|
+
id: "dsh-auto-f-effort",
|
|
416
|
+
className: "dsh-auto-input",
|
|
417
|
+
type: "text",
|
|
418
|
+
list: "dsh-auto-effort-list",
|
|
419
|
+
value: draft.reasoningEffort,
|
|
420
|
+
spellCheck: false,
|
|
421
|
+
placeholder: "e.g. medium",
|
|
422
|
+
onChange: (event) => onChange("reasoningEffort", event.target.value),
|
|
423
|
+
}),
|
|
424
|
+
h(
|
|
425
|
+
"datalist",
|
|
426
|
+
{ id: "dsh-auto-effort-list" },
|
|
427
|
+
REASONING_EFFORTS.map((effort) => h("option", { key: effort, value: effort })),
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
h(
|
|
431
|
+
Field,
|
|
432
|
+
{ label: "Agent preset", htmlFor: "dsh-auto-f-preset", hint: "Blank uses the current Harness agent-preset default." },
|
|
433
|
+
h(
|
|
434
|
+
"select",
|
|
435
|
+
{
|
|
436
|
+
id: "dsh-auto-f-preset",
|
|
437
|
+
className: "dsh-auto-select",
|
|
438
|
+
value: draft.agentPreset,
|
|
439
|
+
onChange: (event) => onChange("agentPreset", event.target.value),
|
|
440
|
+
},
|
|
441
|
+
h("option", { value: "" }, "Harness default"),
|
|
442
|
+
agentPresets.map((preset) =>
|
|
443
|
+
h(
|
|
444
|
+
"option",
|
|
445
|
+
{ key: preset.id, value: preset.id },
|
|
446
|
+
preset.name + (preset.broken ? " (broken: " + preset.broken + ")" : ""),
|
|
447
|
+
),
|
|
448
|
+
),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
h(
|
|
452
|
+
Field,
|
|
453
|
+
{ label: "Permission preset", htmlFor: "dsh-auto-f-permission", required: true },
|
|
454
|
+
h(
|
|
455
|
+
"select",
|
|
456
|
+
{
|
|
457
|
+
id: "dsh-auto-f-permission",
|
|
458
|
+
className: "dsh-auto-select",
|
|
459
|
+
value: draft.permissionPreset,
|
|
460
|
+
onChange: (event) => onChange("permissionPreset", event.target.value),
|
|
461
|
+
},
|
|
462
|
+
permissionPresets.map((preset) => h("option", { key: preset, value: preset }, preset)),
|
|
463
|
+
),
|
|
464
|
+
),
|
|
465
|
+
h(
|
|
466
|
+
Field,
|
|
467
|
+
{ label: "Overlap policy", htmlFor: "dsh-auto-f-overlap", full: true, hint: "What happens when a scheduled run fires while another run for the same job is still active." },
|
|
468
|
+
h(
|
|
469
|
+
"select",
|
|
470
|
+
{
|
|
471
|
+
id: "dsh-auto-f-overlap",
|
|
472
|
+
className: "dsh-auto-select",
|
|
473
|
+
value: draft.overlap,
|
|
474
|
+
onChange: (event) => onChange("overlap", event.target.value),
|
|
475
|
+
},
|
|
476
|
+
OVERLAP_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
|
|
477
|
+
),
|
|
478
|
+
),
|
|
479
|
+
h(
|
|
480
|
+
Field,
|
|
481
|
+
{ label: "Misfire policy", htmlFor: "dsh-auto-f-misfire", hint: "How a missed occurrence is handled after the scheduler is back." },
|
|
482
|
+
h(
|
|
483
|
+
"select",
|
|
484
|
+
{
|
|
485
|
+
id: "dsh-auto-f-misfire",
|
|
486
|
+
className: "dsh-auto-select",
|
|
487
|
+
value: draft.misfire,
|
|
488
|
+
onChange: (event) => onChange("misfire", event.target.value),
|
|
489
|
+
},
|
|
490
|
+
MISFIRE_OPTIONS.map((option) => h("option", { key: option.value, value: option.value }, option.label)),
|
|
491
|
+
),
|
|
492
|
+
),
|
|
493
|
+
h(
|
|
494
|
+
Field,
|
|
495
|
+
{ label: "Working directory", htmlFor: "dsh-auto-f-cwd", required: true, full: true, hint: "Absolute project directory the agent runs in." },
|
|
496
|
+
h("input", {
|
|
497
|
+
id: "dsh-auto-f-cwd",
|
|
498
|
+
className: "dsh-auto-input",
|
|
499
|
+
type: "text",
|
|
500
|
+
value: draft.cwd,
|
|
501
|
+
spellCheck: false,
|
|
502
|
+
placeholder: "/home/you/workspace/project",
|
|
503
|
+
onChange: (event) => onChange("cwd", event.target.value),
|
|
504
|
+
}),
|
|
505
|
+
),
|
|
506
|
+
h(
|
|
507
|
+
Field,
|
|
508
|
+
{ label: "Prompt", htmlFor: "dsh-auto-f-prompt", required: true, full: true, hint: "Instructions for the agent run. Prompts are never shown in run history." },
|
|
509
|
+
h("textarea", {
|
|
510
|
+
id: "dsh-auto-f-prompt",
|
|
511
|
+
className: "dsh-auto-textarea",
|
|
512
|
+
value: draft.prompt,
|
|
513
|
+
placeholder: "Summarize yesterday's progress and list today's priorities\u2026",
|
|
514
|
+
onChange: (event) => onChange("prompt", event.target.value),
|
|
515
|
+
}),
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
error
|
|
519
|
+
? h("p", { className: "dsh-auto-formerror", role: "alert" }, error)
|
|
520
|
+
: null,
|
|
521
|
+
h(
|
|
522
|
+
"div",
|
|
523
|
+
{ className: "dsh-auto-formactions" },
|
|
524
|
+
h(
|
|
525
|
+
"button",
|
|
526
|
+
{ type: "submit", className: "dsh-auto-btn dsh-auto-btn-primary", disabled: saving },
|
|
527
|
+
saving ? "Saving\u2026" : creating ? "Create automation" : "Save changes",
|
|
528
|
+
),
|
|
529
|
+
h(
|
|
530
|
+
"button",
|
|
531
|
+
{ type: "button", className: "dsh-auto-btn", onClick: onCancel, disabled: saving },
|
|
532
|
+
"Cancel",
|
|
533
|
+
),
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function JobCard(props) {
|
|
539
|
+
const {
|
|
540
|
+
job,
|
|
541
|
+
meta,
|
|
542
|
+
busy,
|
|
543
|
+
busyForJob,
|
|
544
|
+
onToggle,
|
|
545
|
+
onRun,
|
|
546
|
+
onEdit,
|
|
547
|
+
onDelete,
|
|
548
|
+
onDeleteConfirm,
|
|
549
|
+
onDeleteCancel,
|
|
550
|
+
confirmingDelete,
|
|
551
|
+
} = props;
|
|
552
|
+
const toggling = busy["toggle:" + job.id] === true;
|
|
553
|
+
const running = busy["run:" + job.id] === true;
|
|
554
|
+
const deleting = busy["delete:" + job.id] === true;
|
|
555
|
+
const modelLabel = job.execution.provider
|
|
556
|
+
? job.execution.provider + "/" + job.execution.model
|
|
557
|
+
: "Default model";
|
|
558
|
+
const agentPreset = meta && Array.isArray(meta.agentPresets)
|
|
559
|
+
? meta.agentPresets.find((preset) => preset.id === job.execution.agentPreset)
|
|
560
|
+
: undefined;
|
|
561
|
+
const presetLabel = job.execution.agentPreset
|
|
562
|
+
? (agentPreset ? agentPreset.name : job.execution.agentPreset)
|
|
563
|
+
: "";
|
|
564
|
+
|
|
565
|
+
return h(
|
|
566
|
+
"article",
|
|
567
|
+
{ className: "dsh-auto-card" + (job.enabled ? "" : " dsh-auto-card-disabled") },
|
|
568
|
+
h(
|
|
569
|
+
"div",
|
|
570
|
+
{ className: "dsh-auto-card-head" },
|
|
571
|
+
h(
|
|
572
|
+
"div",
|
|
573
|
+
{ className: "dsh-auto-card-title" },
|
|
574
|
+
h("h3", null, job.name),
|
|
575
|
+
h("span", { className: "dsh-auto-card-id", title: "Job id" }, job.id),
|
|
576
|
+
),
|
|
577
|
+
h(
|
|
578
|
+
"label",
|
|
579
|
+
{ className: "dsh-auto-switch" },
|
|
580
|
+
h("input", {
|
|
581
|
+
type: "checkbox",
|
|
582
|
+
role: "switch",
|
|
583
|
+
checked: job.enabled,
|
|
584
|
+
disabled: toggling || busyForJob,
|
|
585
|
+
"aria-label": (job.enabled ? "Disable" : "Enable") + " automation " + job.name,
|
|
586
|
+
onChange: onToggle,
|
|
587
|
+
}),
|
|
588
|
+
h("span", { className: "dsh-auto-switch-track" }),
|
|
589
|
+
h("span", { className: "dsh-auto-switch-text" }, job.enabled ? "Enabled" : "Disabled"),
|
|
590
|
+
),
|
|
591
|
+
),
|
|
592
|
+
h(
|
|
593
|
+
"div",
|
|
594
|
+
{ className: "dsh-auto-card-meta" },
|
|
595
|
+
h("span", { title: "Schedule" }, job.schedule.cron + " \u00b7 " + job.schedule.timezone),
|
|
596
|
+
h(
|
|
597
|
+
"span",
|
|
598
|
+
{ className: "dsh-auto-next" },
|
|
599
|
+
!job.enabled
|
|
600
|
+
? "Paused"
|
|
601
|
+
: job.nextRunAt
|
|
602
|
+
? "Next " + formatDate(job.nextRunAt)
|
|
603
|
+
: "No next run",
|
|
604
|
+
),
|
|
605
|
+
h("span", { title: "Model" }, modelLabel),
|
|
606
|
+
presetLabel !== ""
|
|
607
|
+
? h("span", { title: "Agent preset" }, presetLabel)
|
|
608
|
+
: null,
|
|
609
|
+
h("span", { title: "Permission preset" }, job.execution.permissionPreset),
|
|
610
|
+
h("span", { title: "Timeout" }, formatTimeout(job.execution.timeoutMs)),
|
|
611
|
+
h("span", { className: "dsh-auto-muted", title: "Version" }, "v" + job.version),
|
|
612
|
+
),
|
|
613
|
+
h(
|
|
614
|
+
"div",
|
|
615
|
+
{ className: "dsh-auto-card-actions" },
|
|
616
|
+
h(
|
|
617
|
+
"button",
|
|
618
|
+
{ type: "button", className: "dsh-auto-btn", onClick: onEdit, disabled: busyForJob || running },
|
|
619
|
+
"Edit",
|
|
620
|
+
),
|
|
621
|
+
h(
|
|
622
|
+
"button",
|
|
623
|
+
{
|
|
624
|
+
type: "button",
|
|
625
|
+
className: "dsh-auto-btn dsh-auto-btn-primary",
|
|
626
|
+
onClick: onRun,
|
|
627
|
+
disabled: busyForJob,
|
|
628
|
+
},
|
|
629
|
+
running ? "Starting\u2026" : "Run now",
|
|
630
|
+
),
|
|
631
|
+
confirmingDelete
|
|
632
|
+
? h(
|
|
633
|
+
"span",
|
|
634
|
+
{ className: "dsh-auto-confirm" },
|
|
635
|
+
h(
|
|
636
|
+
"button",
|
|
637
|
+
{
|
|
638
|
+
type: "button",
|
|
639
|
+
className: "dsh-auto-btn dsh-auto-btn-danger",
|
|
640
|
+
onClick: onDeleteConfirm,
|
|
641
|
+
disabled: deleting,
|
|
642
|
+
},
|
|
643
|
+
deleting ? "Deleting\u2026" : "Confirm delete",
|
|
644
|
+
),
|
|
645
|
+
h(
|
|
646
|
+
"button",
|
|
647
|
+
{ type: "button", className: "dsh-auto-btn", onClick: onDeleteCancel, disabled: deleting },
|
|
648
|
+
"Keep",
|
|
649
|
+
),
|
|
650
|
+
)
|
|
651
|
+
: h(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
type: "button",
|
|
655
|
+
className: "dsh-auto-btn dsh-auto-btn-danger-ghost",
|
|
656
|
+
onClick: onDelete,
|
|
657
|
+
disabled: busyForJob,
|
|
658
|
+
},
|
|
659
|
+
"Delete",
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function RunsList(props) {
|
|
666
|
+
const { runs, busy, confirmCancelId, onCancel, onCancelConfirm, onCancelReset } = props;
|
|
667
|
+
const visible = runs.slice(0, HISTORY_SHOWN);
|
|
668
|
+
const isActive = (run) => run.status === "queued" || run.status === "running";
|
|
669
|
+
|
|
670
|
+
return h(
|
|
671
|
+
"section",
|
|
672
|
+
{ className: "dsh-auto-runs", "aria-label": "Recent runs" },
|
|
673
|
+
h("h3", null, "Recent runs" + (runs.length > 0 ? " (" + runs.length + ")" : "")),
|
|
674
|
+
runs.length === 0
|
|
675
|
+
? h(
|
|
676
|
+
"p",
|
|
677
|
+
{ className: "dsh-auto-empty" },
|
|
678
|
+
"No runs yet. Runs appear here once an automation is triggered manually or its schedule fires.",
|
|
679
|
+
)
|
|
680
|
+
: h(
|
|
681
|
+
"ul",
|
|
682
|
+
{ className: "dsh-auto-runlist" },
|
|
683
|
+
visible.map((run) => {
|
|
684
|
+
const active = isActive(run);
|
|
685
|
+
const cancelling = busy["cancel:" + run.id] === true;
|
|
686
|
+
return h(
|
|
687
|
+
"li",
|
|
688
|
+
{ key: run.id, className: "dsh-auto-run" + (active ? " dsh-auto-run-active" : "") },
|
|
689
|
+
h(StatusPill, { status: run.status }),
|
|
690
|
+
h("span", { className: "dsh-auto-run-name", title: run.jobName }, run.jobName),
|
|
691
|
+
h(
|
|
692
|
+
"span",
|
|
693
|
+
{ className: "dsh-auto-run-meta" },
|
|
694
|
+
run.trigger + (run.scheduledFor ? " \u00b7 " + formatDate(run.scheduledFor) : ""),
|
|
695
|
+
),
|
|
696
|
+
run.startedAt
|
|
697
|
+
? h("span", { className: "dsh-auto-run-dur", title: "Elapsed" }, formatDuration(run))
|
|
698
|
+
: null,
|
|
699
|
+
run.error
|
|
700
|
+
? h(
|
|
701
|
+
"span",
|
|
702
|
+
{
|
|
703
|
+
className: "dsh-auto-run-error",
|
|
704
|
+
title: (run.error.code ? run.error.code + ": " : "") + run.error.message,
|
|
705
|
+
},
|
|
706
|
+
run.error.message,
|
|
707
|
+
)
|
|
708
|
+
: null,
|
|
709
|
+
run.skipReason
|
|
710
|
+
? h("span", { className: "dsh-auto-run-skip" }, "skipped: " + run.skipReason)
|
|
711
|
+
: null,
|
|
712
|
+
run.sessionId
|
|
713
|
+
? h(
|
|
714
|
+
"span",
|
|
715
|
+
{ className: "dsh-auto-run-session", title: run.sessionId },
|
|
716
|
+
"session " + run.sessionId.slice(0, 10) + "\u2026",
|
|
717
|
+
)
|
|
718
|
+
: null,
|
|
719
|
+
active
|
|
720
|
+
? confirmCancelId === run.id
|
|
721
|
+
? h(
|
|
722
|
+
"span",
|
|
723
|
+
{ className: "dsh-auto-confirm" },
|
|
724
|
+
h(
|
|
725
|
+
"button",
|
|
726
|
+
{
|
|
727
|
+
type: "button",
|
|
728
|
+
className: "dsh-auto-btn dsh-auto-btn-danger",
|
|
729
|
+
onClick: () => onCancelConfirm(run),
|
|
730
|
+
disabled: cancelling,
|
|
731
|
+
},
|
|
732
|
+
cancelling ? "Cancelling\u2026" : "Confirm",
|
|
733
|
+
),
|
|
734
|
+
h(
|
|
735
|
+
"button",
|
|
736
|
+
{ type: "button", className: "dsh-auto-btn", onClick: onCancelReset, disabled: cancelling },
|
|
737
|
+
"Back",
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
: h(
|
|
741
|
+
"button",
|
|
742
|
+
{
|
|
743
|
+
type: "button",
|
|
744
|
+
className: "dsh-auto-btn dsh-auto-btn-ghost",
|
|
745
|
+
onClick: () => onCancel(run),
|
|
746
|
+
},
|
|
747
|
+
"Cancel",
|
|
748
|
+
)
|
|
749
|
+
: null,
|
|
750
|
+
);
|
|
751
|
+
}),
|
|
752
|
+
runs.length > visible.length
|
|
753
|
+
? h(
|
|
754
|
+
"li",
|
|
755
|
+
{ className: "dsh-auto-run-more" },
|
|
756
|
+
runs.length - visible.length + " more run(s) not shown.",
|
|
757
|
+
)
|
|
758
|
+
: null,
|
|
759
|
+
),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function AutomationsSection() {
|
|
764
|
+
const [meta, setMeta] = useState(null);
|
|
765
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
766
|
+
const [loading, setLoading] = useState(true);
|
|
767
|
+
const [loadError, setLoadError] = useState(null);
|
|
768
|
+
const [metaWarning, setMetaWarning] = useState(null);
|
|
769
|
+
const [flash, setFlash] = useState(null);
|
|
770
|
+
const [busy, setBusy] = useState({});
|
|
771
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
772
|
+
const [editing, setEditing] = useState(null);
|
|
773
|
+
const [draft, setDraft] = useState(() => emptyDraft(null));
|
|
774
|
+
const [formError, setFormError] = useState(null);
|
|
775
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
|
776
|
+
const [confirmCancelId, setConfirmCancelId] = useState(null);
|
|
777
|
+
|
|
778
|
+
const aliveRef = useRef(true);
|
|
779
|
+
const pollRef = useRef(false);
|
|
780
|
+
const hasDataRef = useRef(false);
|
|
781
|
+
const idTouchedRef = useRef(false);
|
|
782
|
+
const flashTimerRef = useRef(null);
|
|
783
|
+
|
|
784
|
+
const loadMeta = useCallback(async () => {
|
|
785
|
+
try {
|
|
786
|
+
const data = await apiFetch("/meta");
|
|
787
|
+
if (!aliveRef.current) return;
|
|
788
|
+
setMeta(data);
|
|
789
|
+
setMetaWarning(null);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (!aliveRef.current) return;
|
|
792
|
+
setMetaWarning(errMessage(error));
|
|
793
|
+
}
|
|
794
|
+
}, []);
|
|
795
|
+
|
|
796
|
+
const loadSnapshot = useCallback(async (loud) => {
|
|
797
|
+
if (pollRef.current) return;
|
|
798
|
+
pollRef.current = true;
|
|
799
|
+
try {
|
|
800
|
+
const data = await apiFetch("?limit=100");
|
|
801
|
+
if (!aliveRef.current) return;
|
|
802
|
+
hasDataRef.current = true;
|
|
803
|
+
setSnapshot(data);
|
|
804
|
+
setLoading(false);
|
|
805
|
+
setLoadError(null);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (!aliveRef.current) return;
|
|
808
|
+
if (!hasDataRef.current) setLoadError(errMessage(error));
|
|
809
|
+
} finally {
|
|
810
|
+
pollRef.current = false;
|
|
811
|
+
}
|
|
812
|
+
}, []);
|
|
813
|
+
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
aliveRef.current = true;
|
|
816
|
+
loadMeta();
|
|
817
|
+
loadSnapshot(true);
|
|
818
|
+
const timer = window.setInterval(() => loadSnapshot(false), POLL_MS);
|
|
819
|
+
return () => {
|
|
820
|
+
aliveRef.current = false;
|
|
821
|
+
window.clearInterval(timer);
|
|
822
|
+
window.clearTimeout(flashTimerRef.current);
|
|
823
|
+
};
|
|
824
|
+
}, [loadMeta, loadSnapshot]);
|
|
825
|
+
|
|
826
|
+
const flashMessage = useCallback((kind, message) => {
|
|
827
|
+
setFlash({ kind, message });
|
|
828
|
+
window.clearTimeout(flashTimerRef.current);
|
|
829
|
+
flashTimerRef.current = window.setTimeout(() => setFlash(null), 6000);
|
|
830
|
+
}, []);
|
|
831
|
+
|
|
832
|
+
const setBusyKey = useCallback((key, value) => {
|
|
833
|
+
setBusy((previous) => Object.assign({}, previous, { [key]: value }));
|
|
834
|
+
}, []);
|
|
835
|
+
|
|
836
|
+
const runAction = useCallback(
|
|
837
|
+
async (key, action, onSuccess) => {
|
|
838
|
+
setBusyKey(key, true);
|
|
839
|
+
try {
|
|
840
|
+
const result = await action();
|
|
841
|
+
await loadSnapshot(true);
|
|
842
|
+
if (onSuccess) onSuccess(result);
|
|
843
|
+
return result;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
flashMessage("error", errMessage(error));
|
|
846
|
+
return null;
|
|
847
|
+
} finally {
|
|
848
|
+
setBusyKey(key, false);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
[flashMessage, loadSnapshot, setBusyKey],
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
const handleToggle = useCallback(
|
|
855
|
+
(job) => {
|
|
856
|
+
runAction("toggle:" + job.id, () =>
|
|
857
|
+
apiFetch("/jobs/" + encodeURIComponent(job.id) + "/enabled", {
|
|
858
|
+
method: "POST",
|
|
859
|
+
body: JSON.stringify({ enabled: !job.enabled, expectedVersion: job.version }),
|
|
860
|
+
}),
|
|
861
|
+
);
|
|
862
|
+
},
|
|
863
|
+
[runAction],
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
const handleRunNow = useCallback(
|
|
867
|
+
(job) => {
|
|
868
|
+
runAction(
|
|
869
|
+
"run:" + job.id,
|
|
870
|
+
() => apiFetch("/jobs/" + encodeURIComponent(job.id) + "/run", { method: "POST", body: "{}" }),
|
|
871
|
+
(run) => flashMessage(
|
|
872
|
+
run && run.status === "skipped" ? "error" : "success",
|
|
873
|
+
run && run.status === "skipped"
|
|
874
|
+
? "Run skipped for \u201c" + job.name + "\u201d (" + (run.skipReason || "policy") + ")."
|
|
875
|
+
: "Run queued for \u201c" + job.name + "\u201d.",
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
},
|
|
879
|
+
[runAction, flashMessage],
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
const handleDeleteStart = useCallback((job) => {
|
|
883
|
+
setConfirmCancelId(null);
|
|
884
|
+
setConfirmDeleteId(job.id);
|
|
885
|
+
}, []);
|
|
886
|
+
|
|
887
|
+
const handleDeleteConfirm = useCallback(
|
|
888
|
+
(job) => {
|
|
889
|
+
runAction(
|
|
890
|
+
"delete:" + job.id,
|
|
891
|
+
async () => {
|
|
892
|
+
await apiFetch("/jobs/" + encodeURIComponent(job.id), { method: "DELETE", body: "{}" });
|
|
893
|
+
setConfirmDeleteId(null);
|
|
894
|
+
},
|
|
895
|
+
() => flashMessage("success", "Automation \u201c" + job.name + "\u201d deleted."),
|
|
896
|
+
);
|
|
897
|
+
},
|
|
898
|
+
[runAction, flashMessage],
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
const handleDeleteCancel = useCallback(() => setConfirmDeleteId(null), []);
|
|
902
|
+
|
|
903
|
+
const handleCancelRunStart = useCallback((run) => {
|
|
904
|
+
setConfirmDeleteId(null);
|
|
905
|
+
setConfirmCancelId(run.id);
|
|
906
|
+
}, []);
|
|
907
|
+
|
|
908
|
+
const handleCancelRunConfirm = useCallback(
|
|
909
|
+
(run) => {
|
|
910
|
+
runAction(
|
|
911
|
+
"cancel:" + run.id,
|
|
912
|
+
() => apiFetch("/runs/" + encodeURIComponent(run.id) + "/cancel", { method: "POST", body: "{}" }),
|
|
913
|
+
() => {
|
|
914
|
+
setConfirmCancelId(null);
|
|
915
|
+
flashMessage("success", "Run cancellation requested.");
|
|
916
|
+
},
|
|
917
|
+
);
|
|
918
|
+
},
|
|
919
|
+
[runAction, flashMessage],
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const handleCancelRunReset = useCallback(() => setConfirmCancelId(null), []);
|
|
923
|
+
|
|
924
|
+
const openCreate = useCallback(() => {
|
|
925
|
+
setEditing(null);
|
|
926
|
+
idTouchedRef.current = false;
|
|
927
|
+
setDraft(emptyDraft(meta));
|
|
928
|
+
setFormError(null);
|
|
929
|
+
setFormOpen(true);
|
|
930
|
+
}, [meta]);
|
|
931
|
+
|
|
932
|
+
const openEdit = useCallback((job) => {
|
|
933
|
+
setEditing(job);
|
|
934
|
+
idTouchedRef.current = true;
|
|
935
|
+
setDraft(draftFromJob(job));
|
|
936
|
+
setFormError(null);
|
|
937
|
+
setFormOpen(true);
|
|
938
|
+
}, []);
|
|
939
|
+
|
|
940
|
+
const closeForm = useCallback(() => {
|
|
941
|
+
setFormOpen(false);
|
|
942
|
+
setEditing(null);
|
|
943
|
+
setFormError(null);
|
|
944
|
+
}, []);
|
|
945
|
+
|
|
946
|
+
const handleDraftChange = useCallback(
|
|
947
|
+
(key, value) => {
|
|
948
|
+
setDraft((previous) => {
|
|
949
|
+
let next = Object.assign({}, previous, { [key]: value });
|
|
950
|
+
if (key === "name" && editing === null && !idTouchedRef.current) {
|
|
951
|
+
next = Object.assign({}, next, { id: slugifyJobId(value) });
|
|
952
|
+
}
|
|
953
|
+
if (key === "provider") {
|
|
954
|
+
next = Object.assign({}, next, { model: "" });
|
|
955
|
+
}
|
|
956
|
+
return next;
|
|
957
|
+
});
|
|
958
|
+
},
|
|
959
|
+
[editing],
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
const handleIdTouched = useCallback(() => {
|
|
963
|
+
idTouchedRef.current = true;
|
|
964
|
+
}, []);
|
|
965
|
+
|
|
966
|
+
const handleSubmit = useCallback(
|
|
967
|
+
async (event) => {
|
|
968
|
+
event.preventDefault();
|
|
969
|
+
const creating = editing === null;
|
|
970
|
+
|
|
971
|
+
const name = draft.name.trim();
|
|
972
|
+
if (name === "") return setFormError("Give the automation a name.");
|
|
973
|
+
const cron = draft.cron.trim();
|
|
974
|
+
if (cron === "") return setFormError("A cron expression is required, e.g. 0 9 * * 1-5.");
|
|
975
|
+
const timezone = draft.timezone.trim();
|
|
976
|
+
if (timezone === "") return setFormError("A timezone is required.");
|
|
977
|
+
const cwd = draft.cwd.trim();
|
|
978
|
+
if (cwd === "") return setFormError("The working directory is required.");
|
|
979
|
+
const looksAbsolute = cwd.charAt(0) === "/" || /^[A-Za-z]:[\\/]/.test(cwd) || cwd.startsWith("\\\\");
|
|
980
|
+
if (!looksAbsolute) {
|
|
981
|
+
return setFormError("The working directory must be an absolute filesystem path.");
|
|
982
|
+
}
|
|
983
|
+
const prompt = draft.prompt.trim();
|
|
984
|
+
if (prompt === "") return setFormError("A prompt is required.");
|
|
985
|
+
const timeoutMs = Number(draft.timeoutMs);
|
|
986
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < MIN_TIMEOUT_MS || timeoutMs > MAX_TIMEOUT_MS) {
|
|
987
|
+
return setFormError(
|
|
988
|
+
"Timeout must be a whole number of milliseconds between " + MIN_TIMEOUT_MS + " and " + MAX_TIMEOUT_MS + ".",
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
const provider = draft.provider.trim();
|
|
992
|
+
const model = draft.model.trim();
|
|
993
|
+
if (provider !== "" && model === "") {
|
|
994
|
+
return setFormError("Pick a model for the chosen provider (or leave both blank for the Harness default).");
|
|
995
|
+
}
|
|
996
|
+
if (provider === "" && model !== "") {
|
|
997
|
+
return setFormError("A model needs a provider (or leave both blank for the Harness default).");
|
|
998
|
+
}
|
|
999
|
+
let id = "";
|
|
1000
|
+
if (creating) {
|
|
1001
|
+
id = draft.id.trim().toLowerCase();
|
|
1002
|
+
if (id !== "" && !JOB_ID_PATTERN.test(id)) {
|
|
1003
|
+
return setFormError("Job id must match [a-z0-9][a-z0-9-]{0,62} (lowercase letters, digits, hyphens).");
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const spec = buildSpec(draft);
|
|
1007
|
+
setFormError(null);
|
|
1008
|
+
setBusyKey("save", true);
|
|
1009
|
+
try {
|
|
1010
|
+
if (creating) {
|
|
1011
|
+
await apiFetch("/jobs", {
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
body: JSON.stringify(id === "" ? { spec } : { spec, id }),
|
|
1014
|
+
});
|
|
1015
|
+
} else {
|
|
1016
|
+
await apiFetch("/jobs/" + encodeURIComponent(editing.id), {
|
|
1017
|
+
method: "PUT",
|
|
1018
|
+
body: JSON.stringify({ expectedVersion: editing.version, spec }),
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
await loadSnapshot(true);
|
|
1022
|
+
flashMessage("success", creating ? "Automation created." : "Automation updated.");
|
|
1023
|
+
closeForm();
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
setFormError(errMessage(error));
|
|
1026
|
+
} finally {
|
|
1027
|
+
setBusyKey("save", false);
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
[draft, editing, flashMessage, loadSnapshot, closeForm, setBusyKey],
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
const jobs = snapshot ? snapshot.jobs : [];
|
|
1034
|
+
const runs = snapshot ? snapshot.runs : [];
|
|
1035
|
+
const busyForJob = (id) =>
|
|
1036
|
+
["toggle:" + id, "run:" + id, "delete:" + id].some((key) => busy[key] === true);
|
|
1037
|
+
|
|
1038
|
+
return h(
|
|
1039
|
+
"section",
|
|
1040
|
+
{ className: "dsh-auto-root", "aria-busy": loading ? "true" : null },
|
|
1041
|
+
h(
|
|
1042
|
+
"div",
|
|
1043
|
+
{ className: "dsh-auto-head" },
|
|
1044
|
+
h(
|
|
1045
|
+
"div",
|
|
1046
|
+
null,
|
|
1047
|
+
h("h2", null, "Automations"),
|
|
1048
|
+
h(
|
|
1049
|
+
"p",
|
|
1050
|
+
{ className: "dsh-auto-sub" },
|
|
1051
|
+
snapshot
|
|
1052
|
+
? "Scheduled agent jobs \u00b7 revision " + snapshot.revision + " \u00b7 refreshes every " + POLL_MS / 1000 + "s."
|
|
1053
|
+
: "Scheduled agent jobs.",
|
|
1054
|
+
),
|
|
1055
|
+
),
|
|
1056
|
+
!formOpen
|
|
1057
|
+
? h(
|
|
1058
|
+
"button",
|
|
1059
|
+
{ type: "button", className: "dsh-auto-btn dsh-auto-btn-primary", onClick: openCreate },
|
|
1060
|
+
"New automation",
|
|
1061
|
+
)
|
|
1062
|
+
: null,
|
|
1063
|
+
),
|
|
1064
|
+
flash
|
|
1065
|
+
? h("p", { className: "dsh-auto-flash dsh-auto-flash-" + flash.kind, role: "status" }, flash.message)
|
|
1066
|
+
: null,
|
|
1067
|
+
metaWarning
|
|
1068
|
+
? h(
|
|
1069
|
+
"p",
|
|
1070
|
+
{ className: "dsh-auto-flash dsh-auto-flash-warn", role: "status" },
|
|
1071
|
+
"Could not load provider/model options: ",
|
|
1072
|
+
metaWarning,
|
|
1073
|
+
" ",
|
|
1074
|
+
h("button", { type: "button", className: "dsh-auto-linkbtn", onClick: loadMeta }, "Retry"),
|
|
1075
|
+
)
|
|
1076
|
+
: null,
|
|
1077
|
+
formOpen
|
|
1078
|
+
? h(JobForm, {
|
|
1079
|
+
meta,
|
|
1080
|
+
draft,
|
|
1081
|
+
editing,
|
|
1082
|
+
saving: busy.save === true,
|
|
1083
|
+
error: formError,
|
|
1084
|
+
onChange: handleDraftChange,
|
|
1085
|
+
onIdTouched: handleIdTouched,
|
|
1086
|
+
onSubmit: handleSubmit,
|
|
1087
|
+
onCancel: closeForm,
|
|
1088
|
+
})
|
|
1089
|
+
: null,
|
|
1090
|
+
loadError !== null && snapshot === null
|
|
1091
|
+
? h(
|
|
1092
|
+
"div",
|
|
1093
|
+
{ className: "dsh-auto-error", role: "alert" },
|
|
1094
|
+
h("p", null, "Could not load automations: ", loadError),
|
|
1095
|
+
h(
|
|
1096
|
+
"button",
|
|
1097
|
+
{ type: "button", className: "dsh-auto-btn dsh-auto-btn-primary", onClick: () => loadSnapshot(true) },
|
|
1098
|
+
"Retry",
|
|
1099
|
+
),
|
|
1100
|
+
)
|
|
1101
|
+
: loading && snapshot === null
|
|
1102
|
+
? h("div", { className: "dsh-auto-loading", role: "status" }, "Loading automations\u2026")
|
|
1103
|
+
: h(
|
|
1104
|
+
"div",
|
|
1105
|
+
{ className: "dsh-auto-jobs" },
|
|
1106
|
+
jobs.length === 0
|
|
1107
|
+
? h(
|
|
1108
|
+
"p",
|
|
1109
|
+
{ className: "dsh-auto-empty" },
|
|
1110
|
+
"No automations yet. Create your first scheduled agent job.",
|
|
1111
|
+
)
|
|
1112
|
+
: jobs.map((job) =>
|
|
1113
|
+
h(JobCard, {
|
|
1114
|
+
key: job.id,
|
|
1115
|
+
job,
|
|
1116
|
+
meta,
|
|
1117
|
+
busy,
|
|
1118
|
+
busyForJob: busyForJob(job.id),
|
|
1119
|
+
onToggle: () => handleToggle(job),
|
|
1120
|
+
onRun: () => handleRunNow(job),
|
|
1121
|
+
onEdit: () => openEdit(job),
|
|
1122
|
+
onDelete: () => handleDeleteStart(job),
|
|
1123
|
+
onDeleteConfirm: () => handleDeleteConfirm(job),
|
|
1124
|
+
onDeleteCancel: handleDeleteCancel,
|
|
1125
|
+
confirmingDelete: confirmDeleteId === job.id,
|
|
1126
|
+
}),
|
|
1127
|
+
),
|
|
1128
|
+
),
|
|
1129
|
+
snapshot !== null
|
|
1130
|
+
? h(RunsList, {
|
|
1131
|
+
runs,
|
|
1132
|
+
busy,
|
|
1133
|
+
confirmCancelId,
|
|
1134
|
+
onCancel: handleCancelRunStart,
|
|
1135
|
+
onCancelConfirm: handleCancelRunConfirm,
|
|
1136
|
+
onCancelReset: handleCancelRunReset,
|
|
1137
|
+
})
|
|
1138
|
+
: null,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const STYLE_CSS = [
|
|
1143
|
+
".dsh-auto-root{--dsh-auto-border:var(--dsw-alias-border-subtle,rgba(128,128,128,.3));--dsh-auto-bg:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.05));box-sizing:border-box;max-width:820px;display:flex;flex-direction:column;gap:16px;font-family:inherit;color:var(--dsw-alias-label-primary,#e5e7eb);}",
|
|
1144
|
+
".dsh-auto-root *,.dsh-auto-root *::before,.dsh-auto-root *::after{box-sizing:border-box;}",
|
|
1145
|
+
".dsh-auto-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;}",
|
|
1146
|
+
".dsh-auto-head h2{margin:0;font-size:18px;font-weight:600;line-height:1.35;}",
|
|
1147
|
+
".dsh-auto-sub{margin:3px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9ca3af);max-width:540px;}",
|
|
1148
|
+
".dsh-auto-flash{margin:0;padding:9px 12px;border-radius:10px;font-size:13px;line-height:1.45;word-break:break-word;}",
|
|
1149
|
+
".dsh-auto-flash-error{background:rgba(239,68,68,.13);color:var(--dsw-alias-label-error,#f87171);}",
|
|
1150
|
+
".dsh-auto-flash-success{background:rgba(34,197,94,.13);color:var(--dsw-alias-label-success,#4ade80);}",
|
|
1151
|
+
".dsh-auto-flash-warn{background:rgba(234,179,8,.13);color:var(--dsw-alias-label-warning,#fbbf24);}",
|
|
1152
|
+
".dsh-auto-linkbtn{font:inherit;font-size:inherit;color:inherit;text-decoration:underline;cursor:pointer;background:none;border:none;padding:0;}",
|
|
1153
|
+
".dsh-auto-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:30px;padding:5px 12px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#e5e7eb);font-family:inherit;font-size:13px;line-height:1.2;cursor:pointer;transition:background .15s ease,opacity .15s ease;white-space:nowrap;}",
|
|
1154
|
+
".dsh-auto-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14));}",
|
|
1155
|
+
".dsh-auto-btn:disabled{opacity:.5;cursor:default;}",
|
|
1156
|
+
".dsh-auto-btn-primary{background:var(--dsw-alias-brand-primary,#3b82f6);border-color:transparent;color:#fff;}",
|
|
1157
|
+
".dsh-auto-btn-primary:hover:not(:disabled){background:var(--dsw-alias-brand-primary-hover,#2563eb);}",
|
|
1158
|
+
".dsh-auto-btn-danger{background:var(--dsw-alias-label-error,#ef4444);border-color:transparent;color:#fff;}",
|
|
1159
|
+
".dsh-auto-btn-danger:hover:not(:disabled){background:#dc2626;}",
|
|
1160
|
+
".dsh-auto-btn-danger-ghost{color:var(--dsw-alias-label-error,#ef4444);border-color:currentColor;}",
|
|
1161
|
+
".dsh-auto-btn-danger-ghost:hover:not(:disabled){background:rgba(239,68,68,.12);}",
|
|
1162
|
+
".dsh-auto-btn-ghost{color:var(--dsw-alias-label-secondary,#9ca3af);}",
|
|
1163
|
+
".dsh-auto-input,.dsh-auto-select,.dsh-auto-textarea{width:100%;min-height:34px;padding:6px 10px;border:1px solid var(--dsh-auto-border);border-radius:8px;background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.03));color:var(--dsw-alias-label-primary,#e5e7eb);font-family:inherit;font-size:13px;line-height:1.5;}",
|
|
1164
|
+
".dsh-auto-input:focus-visible,.dsh-auto-select:focus-visible,.dsh-auto-textarea:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#3b82f6);outline-offset:1px;border-color:transparent;}",
|
|
1165
|
+
".dsh-auto-input:disabled,.dsh-auto-select:disabled,.dsh-auto-textarea:disabled{opacity:.55;cursor:default;}",
|
|
1166
|
+
".dsh-auto-textarea{resize:vertical;min-height:110px;}",
|
|
1167
|
+
".dsh-auto-check{display:inline-flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;min-height:34px;}",
|
|
1168
|
+
".dsh-auto-check input{width:16px;height:16px;margin:0;accent-color:var(--dsw-alias-brand-primary,#3b82f6);cursor:pointer;}",
|
|
1169
|
+
".dsh-auto-form{border:1px solid var(--dsh-auto-border);border-radius:14px;padding:16px;background:var(--dsh-auto-bg);display:flex;flex-direction:column;gap:14px;}",
|
|
1170
|
+
".dsh-auto-form h3{margin:0;font-size:15px;font-weight:600;}",
|
|
1171
|
+
".dsh-auto-formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;}",
|
|
1172
|
+
".dsh-auto-field{display:flex;flex-direction:column;gap:4px;min-width:0;}",
|
|
1173
|
+
".dsh-auto-field-full{grid-column:1/-1;}",
|
|
1174
|
+
".dsh-auto-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary,#9ca3af);}",
|
|
1175
|
+
".dsh-auto-required{color:var(--dsw-alias-label-error,#ef4444);}",
|
|
1176
|
+
".dsh-auto-hint{margin:0;font-size:11px;line-height:1.45;color:var(--dsw-alias-label-tertiary,#9ca3af);}",
|
|
1177
|
+
".dsh-auto-formerror{margin:0;padding:8px 10px;border-radius:8px;background:rgba(239,68,68,.12);color:var(--dsw-alias-label-error,#ef4444);font-size:12px;line-height:1.45;}",
|
|
1178
|
+
".dsh-auto-formactions{display:flex;gap:8px;justify-content:flex-end;align-items:center;flex-wrap:wrap;}",
|
|
1179
|
+
".dsh-auto-jobs{display:flex;flex-direction:column;gap:10px;}",
|
|
1180
|
+
".dsh-auto-card{border:1px solid var(--dsh-auto-border);border-radius:12px;padding:12px 14px;background:var(--dsh-auto-bg);display:flex;flex-direction:column;gap:10px;}",
|
|
1181
|
+
".dsh-auto-card-disabled{opacity:.68;}",
|
|
1182
|
+
".dsh-auto-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;}",
|
|
1183
|
+
".dsh-auto-card-title{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;min-width:0;}",
|
|
1184
|
+
".dsh-auto-card-title h3{margin:0;font-size:14px;font-weight:600;overflow-wrap:anywhere;}",
|
|
1185
|
+
".dsh-auto-card-id{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}",
|
|
1186
|
+
".dsh-auto-card-meta{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);line-height:1.5;}",
|
|
1187
|
+
".dsh-auto-card-meta span{white-space:nowrap;}",
|
|
1188
|
+
".dsh-auto-next{font-variant-numeric:tabular-nums;}",
|
|
1189
|
+
".dsh-auto-muted{color:var(--dsw-alias-label-tertiary,#9ca3af);}",
|
|
1190
|
+
".dsh-auto-card-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
|
|
1191
|
+
".dsh-auto-confirm{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap;}",
|
|
1192
|
+
".dsh-auto-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);user-select:none;}",
|
|
1193
|
+
".dsh-auto-switch input{position:absolute;opacity:0;width:1px;height:1px;margin:0;}",
|
|
1194
|
+
".dsh-auto-switch-track{position:relative;width:32px;height:18px;border-radius:9px;background:var(--dsw-alias-border-strong,rgba(128,128,128,.5));transition:background .15s ease;flex:none;}",
|
|
1195
|
+
".dsh-auto-switch-track::after{content:\"\";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:transform .15s ease;}",
|
|
1196
|
+
".dsh-auto-switch input:checked+.dsh-auto-switch-track{background:var(--dsw-alias-brand-primary,#3b82f6);}",
|
|
1197
|
+
".dsh-auto-switch input:checked+.dsh-auto-switch-track::after{transform:translateX(14px);}",
|
|
1198
|
+
".dsh-auto-switch input:focus-visible+.dsh-auto-switch-track{outline:2px solid var(--dsw-alias-brand-primary,#3b82f6);outline-offset:2px;}",
|
|
1199
|
+
".dsh-auto-switch input:disabled~.dsh-auto-switch-text{opacity:.6;}",
|
|
1200
|
+
".dsh-auto-switch:has(input:disabled){cursor:default;}",
|
|
1201
|
+
".dsh-auto-runs{border-top:1px solid var(--dsh-auto-border);padding-top:14px;display:flex;flex-direction:column;gap:10px;min-width:0;}",
|
|
1202
|
+
".dsh-auto-runs h3{margin:0;font-size:14px;font-weight:600;}",
|
|
1203
|
+
".dsh-auto-runlist{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px;max-height:360px;overflow-y:auto;}",
|
|
1204
|
+
".dsh-auto-run{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:8px 10px;border:1px solid var(--dsh-auto-border);border-radius:10px;background:var(--dsh-auto-bg);font-size:12px;line-height:1.4;}",
|
|
1205
|
+
".dsh-auto-run-active{border-color:var(--dsw-alias-brand-primary,rgba(59,130,246,.5));}",
|
|
1206
|
+
".dsh-auto-run-name{font-weight:500;color:var(--dsw-alias-label-primary,#e5e7eb);max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
1207
|
+
".dsh-auto-run-meta{color:var(--dsw-alias-label-tertiary,#9ca3af);font-variant-numeric:tabular-nums;}",
|
|
1208
|
+
".dsh-auto-run-dur{color:var(--dsw-alias-label-secondary,#9ca3af);font-variant-numeric:tabular-nums;white-space:nowrap;}",
|
|
1209
|
+
".dsh-auto-run-error{color:var(--dsw-alias-label-error,#ef4444);max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
1210
|
+
".dsh-auto-run-skip{color:var(--dsw-alias-label-tertiary,#9ca3af);}",
|
|
1211
|
+
".dsh-auto-run-session{color:var(--dsw-alias-label-tertiary,#9ca3af);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;}",
|
|
1212
|
+
".dsh-auto-run-more{color:var(--dsw-alias-label-tertiary,#9ca3af);font-size:11px;padding:2px 4px;}",
|
|
1213
|
+
".dsh-auto-status{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;text-transform:capitalize;white-space:nowrap;}",
|
|
1214
|
+
".dsh-auto-status::before{content:\"\";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none;}",
|
|
1215
|
+
".dsh-auto-status-queued{background:rgba(128,128,128,.16);color:#9ca3af;}",
|
|
1216
|
+
".dsh-auto-status-running{background:rgba(59,130,246,.16);color:#60a5fa;}",
|
|
1217
|
+
".dsh-auto-status-running::before{animation:dsh-auto-pulse 1.1s ease-in-out infinite;}",
|
|
1218
|
+
".dsh-auto-status-succeeded{background:rgba(34,197,94,.16);color:#4ade80;}",
|
|
1219
|
+
".dsh-auto-status-failed,.dsh-auto-status-timed-out{background:rgba(239,68,68,.16);color:#f87171;}",
|
|
1220
|
+
".dsh-auto-status-cancelled,.dsh-auto-status-skipped,.dsh-auto-status-interrupted{background:rgba(128,128,128,.16);color:#9ca3af;}",
|
|
1221
|
+
"@keyframes dsh-auto-pulse{0%,100%{opacity:1}50%{opacity:.35}}",
|
|
1222
|
+
".dsh-auto-empty,.dsh-auto-loading,.dsh-auto-error{padding:22px 16px;text-align:center;border:1px dashed var(--dsh-auto-border);border-radius:12px;font-size:13px;color:var(--dsw-alias-label-secondary,#9ca3af);}",
|
|
1223
|
+
".dsh-auto-error{color:var(--dsw-alias-label-error,#ef4444);display:flex;flex-direction:column;gap:10px;align-items:center;}",
|
|
1224
|
+
"@media (max-width:640px){.dsh-auto-formgrid{grid-template-columns:1fr;}.dsh-auto-card-head{flex-direction:column;align-items:flex-start;}.dsh-auto-run-name{max-width:150px;}}",
|
|
1225
|
+
].join("\n");
|
|
1226
|
+
|
|
1227
|
+
function apply(ctx) {
|
|
1228
|
+
// The page styles ride a style element owned by this plugin's fiber:
|
|
1229
|
+
// created now and removed when the plugin unloads.
|
|
1230
|
+
ctx.effect(() => {
|
|
1231
|
+
const tag = document.createElement("style");
|
|
1232
|
+
tag.setAttribute("data-plugin", "@syncended/dsh-automations");
|
|
1233
|
+
tag.textContent = STYLE_CSS;
|
|
1234
|
+
document.head.appendChild(tag);
|
|
1235
|
+
return () => {
|
|
1236
|
+
tag.remove();
|
|
1237
|
+
};
|
|
1238
|
+
}, "@syncended/dsh-automations: settings page styles");
|
|
1239
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1240
|
+
name: "settings.section",
|
|
1241
|
+
id: "automations",
|
|
1242
|
+
order: 25,
|
|
1243
|
+
label: "Automations",
|
|
1244
|
+
}, AutomationsSection));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
exports.inject = inject;
|
|
1248
|
+
exports.apply = apply;
|
|
1249
|
+
return module.exports;
|
|
1250
|
+
},
|
|
1251
|
+
});
|