@goodandready/dsh-cron 0.3.25 → 0.3.26

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.
Files changed (82) hide show
  1. package/lib/api-actions.js +1 -317
  2. package/lib/api-collection.js +1 -250
  3. package/lib/api-helpers.js +1 -100
  4. package/lib/api-import-export.js +1 -155
  5. package/lib/api-item.js +1 -191
  6. package/lib/api-task-webhook.js +1 -247
  7. package/lib/api-webhook.js +1 -183
  8. package/lib/api.js +1 -178
  9. package/lib/artifacts.js +1 -200
  10. package/lib/chain-checkpoints.js +1 -223
  11. package/lib/client-src/00-open.js +62 -0
  12. package/lib/client-src/10-locales.js +484 -0
  13. package/lib/client-src/20-styles.js +64 -0
  14. package/lib/client-src/30-utils.js +248 -0
  15. package/lib/client-src/40-delivery-settings.js +96 -0
  16. package/lib/client-src/45-dag-graph-view.js +287 -0
  17. package/lib/client-src/46-live-logs-modal.js +108 -0
  18. package/lib/client-src/47-analytics-digest-modal.js +166 -0
  19. package/lib/client-src/48-timeline-heatmap-view.js +176 -0
  20. package/lib/client-src/50-cron-screen-state.js +447 -0
  21. package/lib/client-src/52-cron-screen-actions.js +330 -0
  22. package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
  23. package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
  24. package/lib/client-src/55-cron-screen-view.js +378 -0
  25. package/lib/client-src/60-modal-task-form-head.js +580 -0
  26. package/lib/client-src/62-modal-task-form-tail.js +466 -0
  27. package/lib/client-src/65-modal-dialogs.js +350 -0
  28. package/lib/client-src/70-settings-card.js +420 -0
  29. package/lib/client-src/80-sidebar-jobs.js +200 -0
  30. package/lib/client-src/90-close.js +200 -0
  31. package/lib/commands.js +200 -0
  32. package/lib/durable-journal.js +1 -188
  33. package/lib/external-api.js +1 -74
  34. package/lib/index.js +17 -0
  35. package/lib/metrics.js +1 -125
  36. package/lib/routes/api-actions.js +317 -0
  37. package/lib/routes/api-collection.js +250 -0
  38. package/lib/routes/api-helpers.js +100 -0
  39. package/lib/routes/api-import-export.js +155 -0
  40. package/lib/routes/api-item.js +191 -0
  41. package/lib/routes/api-task-webhook.js +247 -0
  42. package/lib/routes/api-webhook.js +183 -0
  43. package/lib/routes/api.js +178 -0
  44. package/lib/routes/external-api.js +74 -0
  45. package/lib/routes/index.js +11 -0
  46. package/lib/routes/metrics.js +125 -0
  47. package/lib/routes/routes.js +549 -0
  48. package/lib/routes.js +1 -549
  49. package/lib/runner-external.js +1 -352
  50. package/lib/runner-worktree.js +1 -90
  51. package/lib/runner.js +1 -881
  52. package/lib/runtimes/index.js +4 -0
  53. package/lib/runtimes/runner-external.js +352 -0
  54. package/lib/runtimes/runner-worktree.js +90 -0
  55. package/lib/runtimes/runner.js +881 -0
  56. package/lib/runtimes/runtimes.js +198 -0
  57. package/lib/runtimes.js +1 -198
  58. package/lib/scheduler/index.js +6 -0
  59. package/lib/scheduler/scheduler-assists.js +142 -0
  60. package/lib/scheduler/scheduler-completion.js +417 -0
  61. package/lib/scheduler/scheduler-execution.js +360 -0
  62. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  63. package/lib/scheduler/scheduler-preflight.js +195 -0
  64. package/lib/scheduler/scheduler.js +751 -0
  65. package/lib/scheduler-assists.js +1 -142
  66. package/lib/scheduler-completion.js +1 -417
  67. package/lib/scheduler-execution.js +1 -360
  68. package/lib/scheduler-lifecycle.js +1 -157
  69. package/lib/scheduler-preflight.js +1 -195
  70. package/lib/scheduler.js +1 -751
  71. package/lib/store/artifacts.js +200 -0
  72. package/lib/store/chain-checkpoints.js +223 -0
  73. package/lib/store/durable-journal.js +188 -0
  74. package/lib/store/index.js +6 -0
  75. package/lib/store/store.js +1179 -0
  76. package/lib/store/task-schema.js +180 -0
  77. package/lib/store/task-transfer.js +310 -0
  78. package/lib/store.js +2 -1179
  79. package/lib/task-schema.js +1 -180
  80. package/lib/task-transfer.js +1 -310
  81. package/lib/tui-settings.js +157 -0
  82. package/package.json +2 -1
@@ -0,0 +1,640 @@
1
+ const openSettingsModal = async () => {
2
+ setTestStatus(null);
3
+ setSettingsModalOpen(true);
4
+ try {
5
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings');
6
+ if (data && data.ok && data.settings) {
7
+ setSettingsBotToken(data.settings.botToken || '');
8
+ setSettingsChatId(data.settings.chatId || '');
9
+ setSettingsNotifyGlobal(Boolean(data.settings.notifyTelegram));
10
+ setSettingsOnlyOnFailureGlobal(Boolean(data.settings.onlyOnFailure));
11
+ setSettingsKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
12
+ setSettingsDelivery(pickDeliverySettings(data.settings));
13
+ setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
14
+ setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
15
+ }
16
+ } catch (err) {
17
+ console.debug('[dsh-cron] settings fetch failed', err);
18
+ }
19
+ };
20
+
21
+ const handleSaveSettings = async (e) => {
22
+ e.preventDefault();
23
+ try {
24
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(Object.assign({
28
+ botToken: settingsBotToken,
29
+ chatId: settingsChatId,
30
+ notifyTelegram: settingsNotifyGlobal,
31
+ onlyOnFailure: settingsOnlyOnFailureGlobal,
32
+ kanbanBaseUrl: settingsKanbanUrl,
33
+ }, settingsDelivery))
34
+ });
35
+ if (data && data.ok) {
36
+ setSettingsModalOpen(false);
37
+ } else {
38
+ alert(T_KEY(t, 'settings.saveError', { error: (data && data.error) || T_KEY(t, 'settings.saveErrorUnknown') }));
39
+ }
40
+ } catch (err) {
41
+ alert(T_KEY(t, 'settings.saveError', { error: err.message }));
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Update one delivery setting without dropping the rest of the object.
47
+ * `undefined` removes the key so the server default applies again; an
48
+ * empty string is kept so a text field can be cleared on purpose.
49
+ */
50
+ const updateDelivery = (key, value) => {
51
+ setSettingsDelivery((prev) => {
52
+ const next = Object.assign({}, prev);
53
+ if (value === undefined) delete next[key];
54
+ else next[key] = value;
55
+ return next;
56
+ });
57
+ };
58
+
59
+ /** Per-channel template overrides live in a nested map on the server. */
60
+ const updateChannelTemplate = (channelId, value) => {
61
+ setSettingsDelivery((prev) => {
62
+ const templates = Object.assign({}, prev.channelTemplates || {});
63
+ if (value && value.trim()) templates[channelId] = value;
64
+ else delete templates[channelId];
65
+ return Object.assign({}, prev, { channelTemplates: templates });
66
+ });
67
+ };
68
+
69
+ const toggleDeliverySection = (key) => {
70
+ setSettingsSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
71
+ };
72
+
73
+ const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
74
+ try {
75
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ botToken: settingsBotToken,
80
+ chatId: settingsChatId
81
+ })
82
+ });
83
+ if (data && data.ok) {
84
+ setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
85
+ } else {
86
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
87
+ }
88
+ } catch (err) {
89
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
90
+ }
91
+ };
92
+
93
+ const openManualModal = (preset) => {
94
+ setDropdownOpen(false);
95
+ setFormId(null);
96
+ setSelectedTaskMeta(null);
97
+ setFormType('llm');
98
+ setFormDelivery('isolated');
99
+ setFormNotifyTelegram(false);
100
+ setFormChannels([]);
101
+ setFormTemplate('');
102
+ setFormOnlyOnFailure(false);
103
+ setFormTimeoutSeconds(1800);
104
+ setFormOverlapPolicy('skip');
105
+ setFormKanbanMode('none');
106
+ setFormTimezone('');
107
+ setFormMisfirePolicy('skip');
108
+ setFormMaxRetries(0);
109
+ setFormPermissionPreset('default');
110
+ setFormAgentPreset('');
111
+ setFormTargetSessionId('');
112
+ setFormTargetSessionReset('never');
113
+ setFormEnv('');
114
+ setFormCwd('');
115
+ setFormWorkspaceId('');
116
+ setFormWorktree(false);
117
+ setFormKeepWorktree(false);
118
+ setFormCaptureGitDiff(false);
119
+ setFormHttpMethod('GET');
120
+ setFormHttpUrl('');
121
+ setFormHttpHeaders('');
122
+ setFormHttpBody('');
123
+ setFormSshProfileId('');
124
+ setFormSshTarget('');
125
+ setFormSshKeyPath('');
126
+ setFormDockerImage('');
127
+ setFormNodePath('');
128
+ setFormPythonPath('');
129
+ setFormSkillName('');
130
+ setFormWorkflowName('');
131
+ // Reset before the preset branch, so a recipe keeps what it sets.
132
+ setFormFallbackModel('');
133
+ setFormSilentRule('');
134
+ setFormInspectOnFailure(false);
135
+ setFormRequireApproval(false);
136
+ setFormDependsOn('');
137
+ setFormJoinPolicy('all_success');
138
+ setFormAdaptiveEnabled(false);
139
+ setFormAdaptiveMinInterval('5m');
140
+ setFormAdaptiveMaxInterval('2h');
141
+ setFormAdaptiveBackoffFactor('1.5');
142
+ setFormAdaptiveIdleThreshold('3');
143
+ if (preset) {
144
+ setFormTitle(preset.title || '');
145
+ setFormSchedule(preset.schedule || '');
146
+ setFormPrompt(preset.prompt || '');
147
+ // #48: a recipe preset also carries its runtime and delivery shape.
148
+ if (preset.type) setFormType(preset.type);
149
+ if (Array.isArray(preset.channels) && preset.channels.length) setFormChannels(preset.channels.filter((c) => DELIVERY_CHANNELS.includes(c)));
150
+ if (preset.silentRule !== undefined) setFormSilentRule(preset.silentRule || '');
151
+ if (preset.fallbackModel !== undefined) setFormFallbackModel(preset.fallbackModel || '');
152
+ if (preset.inspectOnFailure !== undefined) setFormInspectOnFailure(Boolean(preset.inspectOnFailure));
153
+ if (preset.requireApproval !== undefined) setFormRequireApproval(Boolean(preset.requireApproval));
154
+ setFormOnlyOnFailure(Boolean(preset.onlyOnFailure));
155
+ setFormProvider(defaultModelInfo?.provider || '');
156
+ setFormModel(defaultModelInfo?.model || '');
157
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
158
+ } else {
159
+ setFormTitle('');
160
+ setFormSchedule('0 9 * * 1-5');
161
+ setFormPrompt('');
162
+ setFormProvider(defaultModelInfo?.provider || '');
163
+ setFormModel(defaultModelInfo?.model || '');
164
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
165
+ }
166
+ initialFormSnapshotRef.current = JSON.stringify({
167
+ title: String((preset && preset.title) || '').trim(),
168
+ schedule: String((preset && preset.schedule) || (preset ? '' : '0 9 * * 1-5')).trim(),
169
+ type: String((preset && preset.type) || 'llm'),
170
+ prompt: String((preset && preset.prompt) || '').trim(),
171
+ provider: String(defaultModelInfo?.provider || ''),
172
+ model: String(defaultModelInfo?.model || ''),
173
+ fallbackModel: String((preset && preset.fallbackModel) || '').trim(),
174
+ silentRule: String((preset && preset.silentRule) || '').trim(),
175
+ inspectOnFailure: Boolean(preset && preset.inspectOnFailure),
176
+ requireApproval: Boolean(preset && preset.requireApproval),
177
+ delivery: 'isolated',
178
+ notifyTelegram: false,
179
+ channels: Array.isArray(preset && preset.channels) ? [...preset.channels].filter((c) => DELIVERY_CHANNELS.includes(c)).sort() : [],
180
+ template: '',
181
+ onlyOnFailure: Boolean(preset && preset.onlyOnFailure),
182
+ timeoutSeconds: 1800,
183
+ overlapPolicy: 'skip',
184
+ kanbanMode: 'none',
185
+ onSuccess: '',
186
+ onFailure: '',
187
+ heartbeatIntervalSeconds: 0,
188
+ gracePeriodSeconds: 300,
189
+ preflightType: 'none',
190
+ preflightTarget: '',
191
+ priority: 5,
192
+ concurrencyGroup: 'default',
193
+ selfHealingCommand: '',
194
+ autoDiagnose: false,
195
+ timezone: '',
196
+ misfirePolicy: 'skip',
197
+ maxRetries: 0,
198
+ permissionPreset: 'default',
199
+ agentPreset: '',
200
+ sessionTitle: '',
201
+ targetSessionId: '',
202
+ targetSessionReset: 'never',
203
+ env: '',
204
+ cwd: '',
205
+ workspaceId: '',
206
+ worktree: false,
207
+ keepWorktree: false,
208
+ captureGitDiff: false,
209
+ httpMethod: 'GET',
210
+ httpUrl: '',
211
+ httpHttpHeaders: '',
212
+ httpBody: '',
213
+ sshProfileId: '',
214
+ sshTarget: '',
215
+ sshKeyPath: '',
216
+ dockerImage: '',
217
+ nodePath: '',
218
+ pythonPath: '',
219
+ skillName: '',
220
+ workflowName: '',
221
+ });
222
+ setManualModalOpen(true);
223
+ };
224
+
225
+ const handleRetryDelivery = async (taskId, runId) => {
226
+ setRetryingDeliveryId(runId);
227
+ try {
228
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/retry-delivery', {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ runId }),
232
+ });
233
+ if (data && data.ok) {
234
+ await loadTaskHistory(taskId);
235
+ } else {
236
+ alert(data?.error || 'Failed to retry delivery');
237
+ }
238
+ } catch (e) {
239
+ alert(e.message || 'Error retrying delivery');
240
+ } finally {
241
+ setRetryingDeliveryId(null);
242
+ }
243
+ };
244
+
245
+ const handleApplyDoctorFix = async (taskId, fix) => {
246
+ try {
247
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/doctor/apply', {
248
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fix }),
249
+ });
250
+ if (data && data.ok) { alert('Doctor fix applied successfully! Task code updated.'); await loadTaskHistory(taskId); await fetchTasks(); }
251
+ else { alert(data?.error || 'Failed to apply doctor fix'); }
252
+ } catch (e) { alert(e.message || 'Error applying doctor fix'); }
253
+ };
254
+
255
+ const handleRevertDoctorFix = async (taskId) => {
256
+ try {
257
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/doctor/revert', {
258
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),
259
+ });
260
+ if (data && data.ok) { alert('Reverted to previous version successfully!'); await loadTaskHistory(taskId); await fetchTasks(); }
261
+ else { alert(data?.error || 'Failed to revert doctor fix'); }
262
+ } catch (e) { alert(e.message || 'Error reverting doctor fix'); }
263
+ };
264
+
265
+
266
+
267
+ const handleResolveUncertain = async (taskId, runId, resolution) => {
268
+ try {
269
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/resolve-uncertain', {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({ runId, resolution }),
273
+ });
274
+ if (data && data.ok) {
275
+ await loadTaskHistory(taskId);
276
+ } else {
277
+ alert(data?.error || 'Failed to resolve uncertain run');
278
+ }
279
+ } catch (e) {
280
+ alert(e.message || 'Error resolving uncertain run');
281
+ }
282
+ };
283
+
284
+ const loadTaskHistory = async (taskId) => {
285
+ setHistoryLoading(true);
286
+ try {
287
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
288
+ if (data.ok) {
289
+ setTaskHistory(data.history || []);
290
+ }
291
+ } catch (e) {
292
+ console.error('[dsh-cron] history load failed:', e);
293
+ } finally {
294
+ setHistoryLoading(false);
295
+ }
296
+ };
297
+
298
+ const openEditModal = (task, overrides = {}) => {
299
+ setDropdownOpen(false);
300
+ setFormId(task.id);
301
+ setSelectedTaskMeta(task);
302
+ setModalTab('params');
303
+ setFormTitle(task.title || '');
304
+ setFormSchedule(task.schedule || '');
305
+ setFormType(task.type || 'llm');
306
+ setFormPrompt(overrides.prompt !== undefined ? overrides.prompt : (task.prompt || ''));
307
+ setFormProvider(task.provider || '');
308
+ setFormModel(task.model || '');
309
+ setFormFallbackModel(task.fallbackModel || '');
310
+ setFormSilentRule(task.silentRule || '');
311
+ setFormInspectOnFailure(Boolean(task.inspectOnFailure));
312
+ setFormRequireApproval(Boolean(task.requireApproval));
313
+ setFormDelivery(task.delivery || 'isolated');
314
+ setFormNotifyTelegram(Boolean(task.notifyTelegram));
315
+ setFormChannels(Array.isArray(task.channels) ? task.channels.filter((c) => DELIVERY_CHANNELS.includes(c)) : []);
316
+ setFormTemplate(task.template || '');
317
+ setFormOnlyOnFailure(Boolean(task.onlyOnFailure));
318
+ setFormTimeoutSeconds(task.timeoutSeconds !== undefined ? task.timeoutSeconds : 1800);
319
+ setFormOverlapPolicy(task.overlapPolicy || 'skip');
320
+ setFormKanbanMode(task.kanbanMode || 'none');
321
+ setFormOnSuccess(task.onSuccess || '');
322
+ setFormOnFailure(task.onFailure || '');
323
+ setFormDependsOn(Array.isArray(task.dependsOn) ? task.dependsOn.join(', ') : (task.dependsOn || ''));
324
+ setFormJoinPolicy(task.joinPolicy || 'all_success');
325
+ setFormAdaptiveEnabled(Boolean(task.adaptive?.enabled));
326
+ setFormAdaptiveMinInterval(task.adaptive?.minInterval || '5m');
327
+ setFormAdaptiveMaxInterval(task.adaptive?.maxInterval || '2h');
328
+ setFormAdaptiveBackoffFactor(String(task.adaptive?.backoffFactor || '1.5'));
329
+ setFormAdaptiveIdleThreshold(String(task.adaptive?.idleRunsThreshold || '3'));
330
+ setFormHeartbeatIntervalSeconds(task.heartbeatIntervalSeconds || 0);
331
+ setFormGracePeriodSeconds(task.gracePeriodSeconds || 300);
332
+ setFormPreflightType(task.preflightType || 'none');
333
+ setFormPreflightTarget(task.preflightTarget || '');
334
+ setFormPriority(task.priority !== undefined ? task.priority : 5);
335
+ setFormConcurrencyGroup(task.concurrencyGroup || 'default');
336
+ setFormSelfHealingCommand(task.selfHealingCommand || '');
337
+ setFormAutoDiagnose(Boolean(task.autoDiagnose));
338
+ setSchedulePreviewRuns(null);
339
+ setFormTimezone(task.timezone || '');
340
+ setFormMisfirePolicy(task.misfirePolicy || 'skip');
341
+ setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
342
+ setFormPermissionPreset(task.permissionPreset === 'full' ? 'danger-full-access' : (task.permissionPreset || 'default'));
343
+ setFormAgentPreset(task.agentPreset || '');
344
+ setFormSessionTitle(task.sessionTitle || '');
345
+ setFormTargetSessionId(task.targetSessionId || '');
346
+ setFormTargetSessionReset(task.targetSessionReset || 'never');
347
+ setFormEnv(formatEnvText(task.env));
348
+ setFormCwd(task.cwd || '');
349
+ setFormWorkspaceId(task.workspaceId || '');
350
+ setFormWorktree(Boolean(task.worktree));
351
+ setFormKeepWorktree(Boolean(task.keepWorktree));
352
+ setFormCaptureGitDiff(task.captureGitDiff !== undefined ? Boolean(task.captureGitDiff) : Boolean(task.worktree));
353
+ setFormHttpMethod(task.httpMethod || 'GET');
354
+ setFormHttpUrl(task.httpUrl || '');
355
+ setFormHttpHeaders(typeof task.httpHeaders === 'string' ? task.httpHeaders : (task.httpHeaders ? JSON.stringify(task.httpHeaders) : ''));
356
+ setFormHttpBody(task.httpBody || '');
357
+ setFormSshProfileId(task.sshProfileId || '');
358
+ setFormSshTarget(task.sshTarget || '');
359
+ setFormSshKeyPath(task.sshKeyPath || '');
360
+ setFormDockerImage(task.dockerImage || '');
361
+ setFormNodePath(task.nodePath || '');
362
+ setFormPythonPath(task.pythonPath || '');
363
+ setFormSkillName(task.skillName || '');
364
+ setFormWorkflowName(task.workflowName || '');
365
+ if (task.provider) loadModels(task.provider);
366
+ else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
367
+ loadTaskHistory(task.id);
368
+ setManualModalOpen(true);
369
+ };
370
+
371
+ const handleProviderChange = (e) => {
372
+ const prov = e.target.value;
373
+ setFormProvider(prov);
374
+ setFormModel('');
375
+ loadModels(prov);
376
+ };
377
+
378
+ const handleSaveManual = async (e) => {
379
+ e.preventDefault();
380
+ try {
381
+ const isAgentType = AGENT_FORM_TYPES.includes(formType);
382
+ const payload = {
383
+ id: formId || undefined,
384
+ status: selectedTaskMeta ? selectedTaskMeta.status : 'active',
385
+ title: formTitle,
386
+ schedule: formSchedule,
387
+ prompt: formType === 'http' ? (formPrompt.trim() || formHttpUrl.trim()) : formPrompt,
388
+ type: formType,
389
+ delivery: formDelivery || 'isolated',
390
+ provider: isAgentType ? (formProvider || undefined) : undefined,
391
+ model: isAgentType ? (formModel || undefined) : undefined,
392
+ fallbackModel: isAgentType ? (formFallbackModel.trim() || undefined) : undefined,
393
+ silentRule: isAgentType ? undefined : formSilentRule.trim(),
394
+ inspectOnFailure: isAgentType ? Boolean(formInspectOnFailure) : false,
395
+ notifyTelegram: formNotifyTelegram,
396
+ channels: formChannels,
397
+ template: formTemplate.trim(),
398
+ onlyOnFailure: formOnlyOnFailure,
399
+ timeoutSeconds: Number(formTimeoutSeconds) || 1800,
400
+ overlapPolicy: formOverlapPolicy,
401
+ timezone: formTimezone.trim(),
402
+ misfirePolicy: formMisfirePolicy,
403
+ maxRetries: Number(formMaxRetries) || 0,
404
+ permissionPreset: isAgentType ? formPermissionPreset : 'default',
405
+ agentPreset: formAgentPreset.trim() || undefined,
406
+ sessionTitle: formSessionTitle.trim() || undefined,
407
+ targetSessionId: formTargetSessionId.trim() || undefined,
408
+ targetSessionReset: formTargetSessionReset || 'never',
409
+ kanbanMode: formKanbanMode,
410
+ onSuccess: formOnSuccess.trim() || undefined,
411
+ onFailure: formOnFailure.trim() || undefined,
412
+ dependsOn: formDependsOn.trim() ? formDependsOn.split(/[,;\s]+/).map((s) => s.trim()).filter(Boolean) : undefined,
413
+ joinPolicy: formJoinPolicy || 'all_success',
414
+ adaptive: formAdaptiveEnabled ? { enabled: true, minInterval: formAdaptiveMinInterval || '5m', maxInterval: formAdaptiveMaxInterval || '2h', backoffFactor: Number(formAdaptiveBackoffFactor) || 1.5, idleRunsThreshold: Number(formAdaptiveIdleThreshold) || 3 } : null,
415
+ heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
416
+ gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
417
+ preflightType: formPreflightType || 'none',
418
+ preflightTarget: formPreflightTarget.trim() || undefined,
419
+ priority: Number(formPriority) || 5,
420
+ concurrencyGroup: formConcurrencyGroup.trim() || 'default',
421
+ selfHealingCommand: formSelfHealingCommand.trim() || undefined,
422
+ autoDiagnose: Boolean(formAutoDiagnose),
423
+ requireApproval: Boolean(formRequireApproval),
424
+ env: parseEnvText(formEnv),
425
+ cwd: formCwd.trim(),
426
+ workspaceId: formWorkspaceId.trim(),
427
+ worktree: isAgentType ? Boolean(formWorktree) : false,
428
+ keepWorktree: isAgentType ? Boolean(formKeepWorktree) : false,
429
+ captureGitDiff: Boolean(formCaptureGitDiff),
430
+ httpMethod: formHttpMethod,
431
+ httpUrl: formHttpUrl.trim(),
432
+ httpHeaders: formHttpHeaders.trim() || undefined,
433
+ httpBody: formHttpBody,
434
+ sshProfileId: formSshProfileId.trim(),
435
+ sshTarget: formSshTarget.trim(),
436
+ sshKeyPath: formSshKeyPath.trim(),
437
+ dockerImage: formDockerImage.trim(),
438
+ nodePath: formNodePath.trim(),
439
+ pythonPath: formPythonPath.trim(),
440
+ skillName: formSkillName.trim(),
441
+ workflowName: formWorkflowName.trim(),
442
+ };
443
+ // Cross-origin hardening: every code-executing type needs the explicit
444
+ // confirm header, which forged cross-site posts cannot attach (#86)
445
+ const headers = { 'Content-Type': 'application/json' };
446
+ const hasCodeExec = CODE_FORM_TYPES.includes(formType) ||
447
+ Boolean(payload.command && payload.command.trim()) ||
448
+ payload.preflightType === 'command' ||
449
+ Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
450
+ Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
451
+ if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
452
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks', {
453
+ method: 'POST',
454
+ headers,
455
+ body: JSON.stringify(payload)
456
+ });
457
+ if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
458
+ initialFormSnapshotRef.current = null;
459
+ setManualModalOpen(false);
460
+ fetchTasks();
461
+ } catch (err) { alert(err.message); }
462
+ };
463
+
464
+ const openDshModal = () => {
465
+ setDropdownOpen(false);
466
+ setDshPrompt('');
467
+ setDshModalOpen(true);
468
+ };
469
+
470
+ const handleStartWithDSH = async () => {
471
+ const text = dshPrompt.trim();
472
+ if (!text) return;
473
+
474
+ setDshLoading(true);
475
+ try {
476
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
477
+ method: 'POST',
478
+ headers: { 'Content-Type': 'application/json' },
479
+ body: JSON.stringify({ prompt: text })
480
+ });
481
+ if (!data.ok || !data.sessionId) {
482
+ alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
483
+ setDshLoading(false);
484
+ return;
485
+ }
486
+
487
+ setDshModalOpen(false);
488
+ if (onClose) onClose();
489
+ else toggle.set(false);
490
+
491
+ await openSession(ctx, data.sessionId);
492
+ } catch (err) {
493
+ alert(T_KEY(t, 'dsh.launchError', { error: err.message }));
494
+ } finally {
495
+ setDshLoading(false);
496
+ }
497
+ };
498
+
499
+ const openChainGraphModal = async (task, chainRunId = null) => {
500
+ setChainGraphModalOpen(true);
501
+ setChainGraphLoading(true);
502
+ setChainGraphData(null);
503
+ setChainGraphRunId(chainRunId);
504
+ try {
505
+ if (chainRunId) {
506
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chains/' + encodeURIComponent(chainRunId));
507
+ if (data && data.ok) {
508
+ setChainGraphData(data);
509
+ }
510
+ } else if (task) {
511
+ const graph = buildTaskGraphModel(tasks || [], [], task.id);
512
+ setChainGraphData({ ok: true, graph, task });
513
+ }
514
+ } catch (err) {
515
+ console.debug('[dsh-cron] chain graph fetch failed', err);
516
+ } finally {
517
+ setChainGraphLoading(false);
518
+ }
519
+ };
520
+
521
+ const handleRetryChainStep = async (stepTaskId) => {
522
+ if (!chainGraphRunId || !stepTaskId) return;
523
+ try {
524
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chains/' + encodeURIComponent(chainGraphRunId) + '/retry-from/' + encodeURIComponent(stepTaskId), {
525
+ method: 'POST'
526
+ });
527
+ if (data && data.ok) {
528
+ await openChainGraphModal(null, chainGraphRunId);
529
+ } else {
530
+ alert(data?.error || 'Retry failed');
531
+ }
532
+ } catch (err) {
533
+ alert('Retry failed: ' + err.message);
534
+ }
535
+ };
536
+
537
+ const iconHtml = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
538
+ const svgProps = (html) => ({ style: { display: 'flex', alignItems: 'center' }, ...iconHtml(html) });
539
+
540
+
541
+ function handleOpenLiveLogs(task) {
542
+ if (!task || !task.id) return;
543
+ if (window.__dshLiveLogsEvtSource) {
544
+ try { window.__dshLiveLogsEvtSource.close(); } catch (e) { void e; }
545
+ window.__dshLiveLogsEvtSource = null;
546
+ }
547
+ if (window.__dshLiveLogsPollTimer) {
548
+ clearInterval(window.__dshLiveLogsPollTimer);
549
+ window.__dshLiveLogsPollTimer = null;
550
+ }
551
+ setLiveLogsTaskId(task.id);
552
+ setLiveLogsTaskTitle(task.title || task.id);
553
+ setLiveLogsLines([]);
554
+ setLiveLogsStatus(task.running ? 'connecting' : 'idle');
555
+ setLiveLogsCompleted(false);
556
+ setLiveLogsModalOpen(true);
557
+
558
+ const startPollingFallback = () => {
559
+ if (window.__dshLiveLogsPollTimer) return;
560
+ const pollFn = () => {
561
+ fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/history?limit=1')
562
+ .then((data) => {
563
+ if (data && data.history && data.history.length > 0) {
564
+ const lastRun = data.history[0];
565
+ if (lastRun.output || lastRun.error) {
566
+ const text = lastRun.output || lastRun.error;
567
+ setLiveLogsLines([{ chunk: text, stream: lastRun.error ? 'stderr' : 'stdout', ts: lastRun.at }]);
568
+ }
569
+ if (lastRun.status) {
570
+ setLiveLogsStatus(lastRun.status);
571
+ setLiveLogsCompleted(true);
572
+ if (window.__dshLiveLogsPollTimer) {
573
+ clearInterval(window.__dshLiveLogsPollTimer);
574
+ window.__dshLiveLogsPollTimer = null;
575
+ }
576
+ }
577
+ }
578
+ })
579
+ .catch(() => {});
580
+ };
581
+ pollFn();
582
+ window.__dshLiveLogsPollTimer = setInterval(pollFn, 2000);
583
+ };
584
+
585
+ if (typeof EventSource !== 'undefined') {
586
+ try {
587
+ const streamUrl = '/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/logs/stream';
588
+ const es = new EventSource(streamUrl);
589
+ window.__dshLiveLogsEvtSource = es;
590
+ es.addEventListener('init', (e) => {
591
+ try {
592
+ const d = JSON.parse(e.data);
593
+ if (Array.isArray(d.bufferedLogs)) setLiveLogsLines(d.bufferedLogs);
594
+ if (d.status) setLiveLogsStatus(d.status);
595
+ if (d.status === 'completed') setLiveLogsCompleted(true);
596
+ } catch (err) { void err; }
597
+ });
598
+ es.addEventListener('log', (e) => {
599
+ try {
600
+ const item = JSON.parse(e.data);
601
+ setLiveLogsLines((prev) => [...prev, item]);
602
+ setLiveLogsStatus('running');
603
+ } catch (err) { void err; }
604
+ });
605
+ es.addEventListener('complete', () => {
606
+ setLiveLogsCompleted(true);
607
+ setLiveLogsStatus('completed');
608
+ try { es.close(); } catch (e) { void e; }
609
+ window.__dshLiveLogsEvtSource = null;
610
+ });
611
+ es.onerror = () => {
612
+ try { es.close(); } catch (e) { void e; }
613
+ window.__dshLiveLogsEvtSource = null;
614
+ setLiveLogsStatus((curr) => {
615
+ if (curr === 'running' || curr === 'connecting') {
616
+ startPollingFallback();
617
+ return curr;
618
+ }
619
+ return 'completed';
620
+ });
621
+ };
622
+ } catch (err) {
623
+ startPollingFallback();
624
+ }
625
+ } else {
626
+ startPollingFallback();
627
+ }
628
+ }
629
+
630
+ function handleCloseLiveLogs() {
631
+ if (window.__dshLiveLogsEvtSource) {
632
+ try { window.__dshLiveLogsEvtSource.close(); } catch (e) { void e; }
633
+ window.__dshLiveLogsEvtSource = null;
634
+ }
635
+ if (window.__dshLiveLogsPollTimer) {
636
+ clearInterval(window.__dshLiveLogsPollTimer);
637
+ window.__dshLiveLogsPollTimer = null;
638
+ }
639
+ setLiveLogsModalOpen(false);
640
+ }