@goodandready/dsh-cron 0.2.2 → 0.2.4
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/README.md +99 -14
- package/docs/README.ru.md +96 -15
- package/docs/README.zh.md +99 -14
- package/docs/design/DESIGN.md +13 -7
- package/lib/channels.js +422 -0
- package/lib/client.js +697 -26
- package/lib/index.js +278 -19
- package/lib/integrations.js +3 -1
- package/lib/runner.js +214 -29
- package/lib/runtimes.js +174 -0
- package/lib/scheduler.js +179 -73
- package/lib/secrets.js +123 -0
- package/lib/store.js +92 -21
- package/lib/telegram.js +20 -4
- package/lib/templates.js +88 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -53,7 +53,7 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
'actions.delete': 'Delete task',
|
|
54
54
|
'actions.pause': 'Pause',
|
|
55
55
|
'actions.resume': 'Resume',
|
|
56
|
-
'actions.settings': '
|
|
56
|
+
'actions.settings': 'Notifications & channels',
|
|
57
57
|
'search.placeholder': 'Search scheduled tasks',
|
|
58
58
|
'stats.activeTasks': 'Active tasks',
|
|
59
59
|
'stats.totalRuns': 'Total runs',
|
|
@@ -94,7 +94,32 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
'form.namePlaceholder': 'For example: dsh-tags-watch',
|
|
95
95
|
'form.typeLabel': 'Execution type',
|
|
96
96
|
'form.typeLlm': 'LLM (AI agent)',
|
|
97
|
-
'form.typeScript': '
|
|
97
|
+
'form.typeScript': 'Shell command / script',
|
|
98
|
+
'form.typeNode': 'Node.js',
|
|
99
|
+
'form.typePython': 'Python',
|
|
100
|
+
'form.typeHttp': 'HTTP request / webhook',
|
|
101
|
+
'form.typeSsh': 'Remote SSH',
|
|
102
|
+
'form.typeDocker': 'Docker container',
|
|
103
|
+
'form.typeSkill': 'DSH Skill',
|
|
104
|
+
'form.typeWorkflow': 'DSH Workflow',
|
|
105
|
+
'form.envLabel': 'Environment variables (KEY=VALUE per line)',
|
|
106
|
+
'form.envPlaceholder': 'NODE_ENV=production\nTZ=Europe/Berlin',
|
|
107
|
+
'form.envHint': 'Applied to non-LLM runtimes only. Do not store secrets here.',
|
|
108
|
+
'form.cwdLabel': 'Working directory (optional)',
|
|
109
|
+
'form.workspaceLabel': 'Workspace id or path (optional)',
|
|
110
|
+
'form.worktreeLabel': 'Run in an isolated git worktree',
|
|
111
|
+
'form.keepWorktreeLabel': 'Keep the worktree after the run',
|
|
112
|
+
'form.httpMethodLabel': 'HTTP method',
|
|
113
|
+
'form.httpUrlLabel': 'URL',
|
|
114
|
+
'form.httpHeadersLabel': 'Headers (JSON, optional)',
|
|
115
|
+
'form.httpBodyLabel': 'Body (optional)',
|
|
116
|
+
'form.sshProfileLabel': 'dsh-remote-workspace profile id',
|
|
117
|
+
'form.sshProfileHint': 'Preferred: host and credentials stay in the remote-workspace plugin.',
|
|
118
|
+
'form.sshTargetLabel': 'Fallback target (user@host, optional)',
|
|
119
|
+
'form.sshKeyLabel': 'Private key path (fallback, optional)',
|
|
120
|
+
'form.dockerImageLabel': 'Container image',
|
|
121
|
+
'form.skillNameLabel': 'Skill name',
|
|
122
|
+
'form.workflowNameLabel': 'Workflow name',
|
|
98
123
|
'form.scheduleLabel': 'Schedule (cron or interval)',
|
|
99
124
|
'form.schedulePlaceholder': '0 */6 * * * or every 2h or in 30m or at: 2026-09-05T15:00:00Z',
|
|
100
125
|
'form.timeoutLabel': 'Execution timeout (seconds)',
|
|
@@ -114,18 +139,79 @@ window.__ModuleLoader__.load({
|
|
|
114
139
|
'form.promptLabelLlm': 'Instruction / Prompt for the agent',
|
|
115
140
|
'form.promptPlaceholderScript': 'curl -fsSL https://... || exit 1',
|
|
116
141
|
'form.promptPlaceholderLlm': 'Describe exactly what the agent should do...',
|
|
142
|
+
'form.timezoneLabel': 'Time zone (IANA, optional)',
|
|
143
|
+
'form.timezonePlaceholder': 'e.g. Europe/Berlin (empty = server local)',
|
|
144
|
+
'form.misfireLabel': 'On missed run (after downtime)',
|
|
145
|
+
'form.misfireSkip': 'Skip the missed run (skip)',
|
|
146
|
+
'form.misfireRunOnce': 'Run once late (runOnce)',
|
|
147
|
+
'form.misfireCatchUp': 'Run late once and record the gap (catchUpAll)',
|
|
148
|
+
'form.retriesLabel': 'Automatic retries on failure (0 = off)',
|
|
149
|
+
'form.permissionLabel': 'Session permissions',
|
|
150
|
+
'form.permissionDefault': 'Default (as configured in DSH)',
|
|
151
|
+
'form.permissionReadonly': 'Read-only',
|
|
152
|
+
'form.permissionWrite': 'Workspace write',
|
|
153
|
+
'form.permissionFull': 'Full',
|
|
154
|
+
'list.runningNow': 'Running now',
|
|
155
|
+
'history.openSession': 'Open session',
|
|
117
156
|
'form.notifyTitle': 'Telegram notifications',
|
|
118
157
|
'form.notifyTelegram': 'Send the run report to Telegram',
|
|
119
158
|
'form.notifyOnlyFailure': 'Only on failures (onlyOnFailure / silent on success)',
|
|
159
|
+
'form.channelsTitle': 'Delivery channels',
|
|
160
|
+
'form.channelsHint': 'Pick the channels for this task. An explicit selection overrides the Telegram switch above; leave everything unchecked to fall back to it.',
|
|
161
|
+
'form.templateLabel': 'Message template (optional)',
|
|
162
|
+
'form.templatePlaceholder': '⏰ {title} — {status} in {duration}',
|
|
163
|
+
'form.templateHint': 'Variables: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}. Empty = built-in text.',
|
|
164
|
+
'form.channelTelegram': 'Telegram',
|
|
165
|
+
'form.channelKanban': 'dsh-kanban card',
|
|
166
|
+
'form.channelDiscord': 'Discord',
|
|
167
|
+
'form.channelSlack': 'Slack',
|
|
168
|
+
'form.channelNtfy': 'ntfy',
|
|
169
|
+
'form.channelBark': 'Bark',
|
|
170
|
+
'form.channelPushplus': 'PushPlus',
|
|
171
|
+
'form.channelEmail': 'Email',
|
|
172
|
+
'form.channelTts': 'Voice (dsh-tts)',
|
|
173
|
+
'form.channelGitea': 'Gitea issue',
|
|
174
|
+
'settings.deliverySection': 'Delivery channels',
|
|
175
|
+
'settings.secretsSection': 'Credentials (references)',
|
|
176
|
+
'settings.secretsHint': 'Enter the NAME of a DSH credential, not the secret itself. Secrets stay in the credentials store.',
|
|
177
|
+
'settings.telegramSection': 'Telegram',
|
|
178
|
+
'settings.templatesSection': 'Message templates',
|
|
179
|
+
'settings.channelTemplateLabel': 'Template for {channel}',
|
|
180
|
+
'settings.botTokenRefLabel': 'Bot token credential name',
|
|
181
|
+
'settings.botTokenRefPlaceholder': 'e.g. TELEGRAM_BOT_TOKEN',
|
|
182
|
+
'settings.discordLabel': 'Discord webhook URL',
|
|
183
|
+
'settings.slackLabel': 'Slack webhook URL',
|
|
184
|
+
'settings.ntfyUrlLabel': 'ntfy server URL',
|
|
185
|
+
'settings.ntfyTopicLabel': 'ntfy topic',
|
|
186
|
+
'settings.ntfyTokenRefLabel': 'ntfy token credential name',
|
|
187
|
+
'settings.barkServerLabel': 'Bark server URL',
|
|
188
|
+
'settings.barkKeyLabel': 'Bark device key',
|
|
189
|
+
'settings.pushplusTokenRefLabel': 'PushPlus token credential name',
|
|
190
|
+
'settings.pushplusUrlLabel': 'PushPlus endpoint',
|
|
191
|
+
'settings.smtpHostLabel': 'SMTP host',
|
|
192
|
+
'settings.smtpPortLabel': 'SMTP port',
|
|
193
|
+
'settings.smtpUserLabel': 'SMTP user',
|
|
194
|
+
'settings.smtpPasswordRefLabel': 'SMTP password credential name',
|
|
195
|
+
'settings.smtpFromLabel': 'From address',
|
|
196
|
+
'settings.smtpToLabel': 'Recipients (comma separated)',
|
|
197
|
+
'settings.smtpSecureLabel': 'SMTP over TLS (implicit)',
|
|
198
|
+
'settings.ttsBaseUrlLabel': 'dsh-tts base URL',
|
|
199
|
+
'settings.giteaBaseUrlLabel': 'Gitea base URL',
|
|
200
|
+
'settings.giteaRepoLabel': 'Gitea repository (owner/repo)',
|
|
201
|
+
'settings.giteaTokenRefLabel': 'Gitea token credential name',
|
|
202
|
+
'settings.templateLabel': 'Default message template',
|
|
203
|
+
'settings.templateHint': 'Applies to every channel without its own template. Empty = built-in text.',
|
|
204
|
+
'settings.deliveryTimeoutMsLabel': 'Delivery timeout per channel (ms)',
|
|
205
|
+
'settings.secretsOnCard': 'Credentials referenced from the settings panel are shown masked and stored by name.',
|
|
120
206
|
'form.nextRunLabel': 'Next run: ',
|
|
121
207
|
'form.notScheduled': 'not scheduled',
|
|
122
208
|
'settings.loading': 'Loading settings...',
|
|
123
209
|
'settings.unavailable': 'Settings are unavailable right now.',
|
|
124
210
|
'settings.retry': 'Retry',
|
|
125
211
|
'settings.cardTitle': '⏰ Task scheduler (Cron)',
|
|
126
|
-
'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram
|
|
212
|
+
'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios, and delivery of run reports to Telegram, Discord, Slack, ntfy, Bark, PushPlus, email, voice and Gitea.',
|
|
127
213
|
'settings.openPanel': 'Open task panel',
|
|
128
|
-
'settings.modalTitle': '
|
|
214
|
+
'settings.modalTitle': 'Notifications & delivery channels',
|
|
129
215
|
'settings.tokenLabel': 'Telegram Bot Token',
|
|
130
216
|
'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
131
217
|
'settings.tokenPlaceholderDefault': 'Using the bot configured in DSH settings (or enter your own)',
|
|
@@ -217,6 +303,169 @@ window.__ModuleLoader__.load({
|
|
|
217
303
|
}
|
|
218
304
|
const ErrorBoundary = createErrorBoundary();
|
|
219
305
|
|
|
306
|
+
const AGENT_FORM_TYPES = ['llm', 'skill', 'workflow'];
|
|
307
|
+
const CODE_FORM_TYPES = ['script', 'node', 'python', 'ssh', 'docker'];
|
|
308
|
+
|
|
309
|
+
// Delivery channels (#26) — must stay in sync with lib/channels.js.
|
|
310
|
+
const DELIVERY_CHANNELS = ['telegram', 'kanban', 'discord', 'slack', 'ntfy', 'bark', 'pushplus', 'email', 'tts', 'gitea'];
|
|
311
|
+
const CHANNEL_LABEL_KEYS = {
|
|
312
|
+
telegram: 'form.channelTelegram',
|
|
313
|
+
kanban: 'form.channelKanban',
|
|
314
|
+
discord: 'form.channelDiscord',
|
|
315
|
+
slack: 'form.channelSlack',
|
|
316
|
+
ntfy: 'form.channelNtfy',
|
|
317
|
+
bark: 'form.channelBark',
|
|
318
|
+
pushplus: 'form.channelPushplus',
|
|
319
|
+
email: 'form.channelEmail',
|
|
320
|
+
tts: 'form.channelTts',
|
|
321
|
+
gitea: 'form.channelGitea',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Non-secret delivery settings shown in the settings panel. Secrets are
|
|
325
|
+
// referenced by credential name only (#51) — never typed as values.
|
|
326
|
+
const DELIVERY_SETTING_FIELDS = [
|
|
327
|
+
{ key: 'botTokenRef', labelKey: 'settings.botTokenRefLabel', placeholderKey: 'settings.botTokenRefPlaceholder', group: 'secrets' },
|
|
328
|
+
{ key: 'ntfyTokenRef', labelKey: 'settings.ntfyTokenRefLabel', group: 'secrets' },
|
|
329
|
+
{ key: 'pushplusTokenRef', labelKey: 'settings.pushplusTokenRefLabel', group: 'secrets' },
|
|
330
|
+
{ key: 'smtpPasswordRef', labelKey: 'settings.smtpPasswordRefLabel', group: 'secrets' },
|
|
331
|
+
{ key: 'giteaTokenRef', labelKey: 'settings.giteaTokenRefLabel', group: 'secrets' },
|
|
332
|
+
{ key: 'discordWebhookUrl', labelKey: 'settings.discordLabel', group: 'channels' },
|
|
333
|
+
{ key: 'slackWebhookUrl', labelKey: 'settings.slackLabel', group: 'channels' },
|
|
334
|
+
{ key: 'ntfyUrl', labelKey: 'settings.ntfyUrlLabel', group: 'channels' },
|
|
335
|
+
{ key: 'ntfyTopic', labelKey: 'settings.ntfyTopicLabel', group: 'channels' },
|
|
336
|
+
{ key: 'barkServerUrl', labelKey: 'settings.barkServerLabel', group: 'channels' },
|
|
337
|
+
{ key: 'barkKey', labelKey: 'settings.barkKeyLabel', group: 'channels' },
|
|
338
|
+
{ key: 'pushplusUrl', labelKey: 'settings.pushplusUrlLabel', group: 'channels' },
|
|
339
|
+
{ key: 'smtpHost', labelKey: 'settings.smtpHostLabel', group: 'channels' },
|
|
340
|
+
{ key: 'smtpPort', labelKey: 'settings.smtpPortLabel', group: 'channels' },
|
|
341
|
+
{ key: 'smtpUser', labelKey: 'settings.smtpUserLabel', group: 'channels' },
|
|
342
|
+
{ key: 'smtpFrom', labelKey: 'settings.smtpFromLabel', group: 'channels' },
|
|
343
|
+
{ key: 'smtpTo', labelKey: 'settings.smtpToLabel', group: 'channels' },
|
|
344
|
+
{ key: 'ttsBaseUrl', labelKey: 'settings.ttsBaseUrlLabel', group: 'channels' },
|
|
345
|
+
{ key: 'giteaBaseUrl', labelKey: 'settings.giteaBaseUrlLabel', group: 'channels' },
|
|
346
|
+
{ key: 'giteaRepo', labelKey: 'settings.giteaRepoLabel', group: 'channels' },
|
|
347
|
+
{ key: 'deliveryTimeoutMs', labelKey: 'settings.deliveryTimeoutMsLabel', group: 'channels', numeric: true },
|
|
348
|
+
];
|
|
349
|
+
const DELIVERY_TOGGLE_FIELDS = [
|
|
350
|
+
{ key: 'smtpSecure', labelKey: 'settings.smtpSecureLabel' },
|
|
351
|
+
];
|
|
352
|
+
const DELIVERY_SETTING_KEYS = DELIVERY_SETTING_FIELDS.map((f) => f.key).concat(DELIVERY_TOGGLE_FIELDS.map((f) => f.key));
|
|
353
|
+
// The global template plus the per-channel override map (server model).
|
|
354
|
+
const TEMPLATE_SETTING_KEYS = ['template', 'channelTemplates'];
|
|
355
|
+
|
|
356
|
+
/** Collect every delivery key the settings endpoints may return. */
|
|
357
|
+
function pickDeliverySettings(source) {
|
|
358
|
+
const out = {};
|
|
359
|
+
for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
|
|
360
|
+
if (source && source[key] !== undefined && source[key] !== null) out[key] = source[key];
|
|
361
|
+
}
|
|
362
|
+
if (out.channelTemplates && typeof out.channelTemplates === 'object') {
|
|
363
|
+
out.channelTemplates = Object.assign({}, out.channelTemplates);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Shared delivery form used by both the task panel modal and the plugin
|
|
370
|
+
* settings card, so a new channel is added in exactly one place.
|
|
371
|
+
* Sections collapse via the canonical aria-expanded head contract.
|
|
372
|
+
*/
|
|
373
|
+
function renderDeliverySettings({ T, values, onChange, onChannelTemplate, sections, onToggleSection }) {
|
|
374
|
+
const val = (key) => (values[key] === undefined || values[key] === null ? '' : String(values[key]));
|
|
375
|
+
const field = (f) => React.createElement('div', { className: 'dsh-cron-form-group', key: f.key, style: { marginBottom: '10px' } },
|
|
376
|
+
React.createElement('label', null, T(f.labelKey)),
|
|
377
|
+
React.createElement('input', f.numeric ? {
|
|
378
|
+
type: 'number',
|
|
379
|
+
min: 1000,
|
|
380
|
+
step: 1000,
|
|
381
|
+
value: val(f.key),
|
|
382
|
+
placeholder: '15000',
|
|
383
|
+
onChange: (e) => onChange(f.key, e.target.value === '' ? undefined : Number(e.target.value)),
|
|
384
|
+
} : {
|
|
385
|
+
type: 'text',
|
|
386
|
+
value: val(f.key),
|
|
387
|
+
placeholder: f.placeholderKey ? T(f.placeholderKey) : '',
|
|
388
|
+
onChange: (e) => onChange(f.key, e.target.value)
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
const section = (key, titleKey, children) => {
|
|
392
|
+
const open = Boolean(sections[key]);
|
|
393
|
+
return React.createElement('div', { className: 'dsh-cron-section', key },
|
|
394
|
+
React.createElement('button', {
|
|
395
|
+
type: 'button',
|
|
396
|
+
className: 'dsh-cron-section-head',
|
|
397
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
398
|
+
onClick: () => onToggleSection(key)
|
|
399
|
+
},
|
|
400
|
+
React.createElement('span', null, T(titleKey)),
|
|
401
|
+
React.createElement('span', { className: 'dsh-cron-section-chevron', 'data-open': open ? 'true' : 'false' }, '▾')
|
|
402
|
+
),
|
|
403
|
+
open && React.createElement('div', { className: 'dsh-cron-section-body' }, children)
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return [
|
|
408
|
+
section('secrets', 'settings.secretsSection', [
|
|
409
|
+
React.createElement('div', { className: 'dsh-cron-hint', key: 'hint', style: { marginBottom: '10px' } }, T('settings.secretsHint')),
|
|
410
|
+
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'secrets').map(field),
|
|
411
|
+
]),
|
|
412
|
+
section('channels', 'settings.deliverySection', [
|
|
413
|
+
React.createElement('div', { className: 'dsh-cron-form-row', key: 'row' },
|
|
414
|
+
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'channels').map(field)
|
|
415
|
+
),
|
|
416
|
+
...DELIVERY_TOGGLE_FIELDS.map((f) => React.createElement('label', { className: 'dsh-cron-checkbox-row', key: f.key, style: { marginBottom: '10px' } },
|
|
417
|
+
React.createElement('input', {
|
|
418
|
+
type: 'checkbox',
|
|
419
|
+
checked: Boolean(values[f.key]),
|
|
420
|
+
onChange: (e) => onChange(f.key, e.target.checked)
|
|
421
|
+
}),
|
|
422
|
+
React.createElement('span', null, T(f.labelKey))
|
|
423
|
+
)),
|
|
424
|
+
]),
|
|
425
|
+
section('templates', 'settings.templatesSection', [
|
|
426
|
+
React.createElement('div', { className: 'dsh-cron-form-group', key: 'global', style: { marginBottom: '10px' } },
|
|
427
|
+
React.createElement('label', null, T('settings.templateLabel')),
|
|
428
|
+
React.createElement('input', {
|
|
429
|
+
type: 'text',
|
|
430
|
+
value: val('template'),
|
|
431
|
+
onChange: (e) => onChange('template', e.target.value)
|
|
432
|
+
}),
|
|
433
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T('settings.templateHint'))
|
|
434
|
+
),
|
|
435
|
+
React.createElement('div', { className: 'dsh-cron-form-row', key: 'perchannel' },
|
|
436
|
+
...DELIVERY_CHANNELS.map((id) => React.createElement('div', { className: 'dsh-cron-form-group', key: id, style: { marginBottom: '10px' } },
|
|
437
|
+
React.createElement('label', null, T('settings.channelTemplateLabel', { channel: T(CHANNEL_LABEL_KEYS[id]) })),
|
|
438
|
+
React.createElement('input', {
|
|
439
|
+
type: 'text',
|
|
440
|
+
value: (values.channelTemplates && values.channelTemplates[id]) || '',
|
|
441
|
+
onChange: (e) => onChannelTemplate(id, e.target.value)
|
|
442
|
+
})
|
|
443
|
+
))
|
|
444
|
+
),
|
|
445
|
+
]),
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Parse a KEY=VALUE textarea into the task env object (#38). */
|
|
450
|
+
function parseEnvText(text) {
|
|
451
|
+
const env = {};
|
|
452
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
453
|
+
const line = rawLine.trim();
|
|
454
|
+
if (!line || line.startsWith('#')) continue;
|
|
455
|
+
const idx = line.indexOf('=');
|
|
456
|
+
if (idx <= 0) continue;
|
|
457
|
+
const key = line.slice(0, idx).trim();
|
|
458
|
+
const value = line.slice(idx + 1).trim();
|
|
459
|
+
if (key) env[key] = value;
|
|
460
|
+
}
|
|
461
|
+
return Object.keys(env).length ? env : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function formatEnvText(env) {
|
|
465
|
+
if (!env || typeof env !== 'object') return '';
|
|
466
|
+
return Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
220
469
|
// Status colors: core semantic state tokens first (same set dsh-clinebot
|
|
221
470
|
// uses), plugin fallbacks as a single point of change (#105).
|
|
222
471
|
const STYLES = `
|
|
@@ -302,6 +551,12 @@ window.__ModuleLoader__.load({
|
|
|
302
551
|
|
|
303
552
|
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
304
553
|
.dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
|
|
554
|
+
.dsh-cron-modal-scroll { max-width: 620px; max-height: 88vh; overflow-y: auto; }
|
|
555
|
+
.dsh-cron-section-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0; margin-bottom: 10px; font: inherit; font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; }
|
|
556
|
+
.dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary, #eee); }
|
|
557
|
+
.dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
|
|
558
|
+
.dsh-cron-section-chevron[data-open="false"] { transform: rotate(-90deg); }
|
|
559
|
+
.dsh-cron-section-body { padding-bottom: 4px; }
|
|
305
560
|
.dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
|
|
306
561
|
.dsh-cron-form-group { margin-bottom: 16px; }
|
|
307
562
|
.dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 6px; }
|
|
@@ -359,10 +614,18 @@ window.__ModuleLoader__.load({
|
|
|
359
614
|
.dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dsw-alias-label-secondary, #ccc); cursor: pointer; user-select: none; }
|
|
360
615
|
.dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
|
|
361
616
|
.dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
|
|
617
|
+
.dsh-cron-channel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px 12px; }
|
|
618
|
+
.dsh-cron-section { border-top: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-top: 16px; padding-top: 12px; }
|
|
619
|
+
.dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 10px; }
|
|
620
|
+
.dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
|
|
621
|
+
.dsh-cron-secret-row input { flex: 1; }
|
|
622
|
+
.dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; }
|
|
362
623
|
.dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
|
|
363
624
|
.dsh-cron-status-banner[data-type="success"] { background: var(--dsh-cron-success-bg); border: 1px solid var(--dsh-cron-success); color: var(--dsh-cron-success); }
|
|
364
625
|
.dsh-cron-status-banner[data-type="error"] { background: var(--dsh-cron-danger-bg); border: 1px solid var(--dsh-cron-danger); color: var(--dsh-cron-danger); }
|
|
365
626
|
.dsh-cron-status-banner[data-type="info"] { background: var(--dsh-cron-info-bg); border: 1px solid var(--dsh-cron-info); color: var(--dsh-cron-info); }
|
|
627
|
+
@keyframes dsh-cron-pulse-ring { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); } 70% { box-shadow: 0 0 0 9px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
|
|
628
|
+
.dsh-cron-pulse { animation: dsh-cron-pulse-ring 1.6s infinite; border-color: var(--dsh-cron-success); color: var(--dsh-cron-success); }
|
|
366
629
|
`;
|
|
367
630
|
|
|
368
631
|
function ensureStyles() {
|
|
@@ -480,12 +743,49 @@ window.__ModuleLoader__.load({
|
|
|
480
743
|
const [taskHistory, setTaskHistory] = React.useState([]);
|
|
481
744
|
const [historyLoading, setHistoryLoading] = React.useState(false);
|
|
482
745
|
const [formNotifyTelegram, setFormNotifyTelegram] = React.useState(false);
|
|
746
|
+
const [formChannels, setFormChannels] = React.useState([]);
|
|
747
|
+
const [formTemplate, setFormTemplate] = React.useState('');
|
|
483
748
|
const [formOnlyOnFailure, setFormOnlyOnFailure] = React.useState(false);
|
|
484
749
|
const [formTimeoutSeconds, setFormTimeoutSeconds] = React.useState(1800);
|
|
485
750
|
const [formOverlapPolicy, setFormOverlapPolicy] = React.useState('skip');
|
|
486
751
|
const [formKanbanMode, setFormKanbanMode] = React.useState('none');
|
|
752
|
+
const [formTimezone, setFormTimezone] = React.useState('');
|
|
753
|
+
const [formMisfirePolicy, setFormMisfirePolicy] = React.useState('skip');
|
|
754
|
+
const [formMaxRetries, setFormMaxRetries] = React.useState(0);
|
|
755
|
+
const [formPermissionPreset, setFormPermissionPreset] = React.useState('default');
|
|
756
|
+
const [formEnv, setFormEnv] = React.useState('');
|
|
757
|
+
const [formCwd, setFormCwd] = React.useState('');
|
|
758
|
+
const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
|
|
759
|
+
const [formWorktree, setFormWorktree] = React.useState(false);
|
|
760
|
+
const [formKeepWorktree, setFormKeepWorktree] = React.useState(false);
|
|
761
|
+
const [formHttpMethod, setFormHttpMethod] = React.useState('GET');
|
|
762
|
+
const [formHttpUrl, setFormHttpUrl] = React.useState('');
|
|
763
|
+
const [formHttpHeaders, setFormHttpHeaders] = React.useState('');
|
|
764
|
+
const [formHttpBody, setFormHttpBody] = React.useState('');
|
|
765
|
+
const [formSshProfileId, setFormSshProfileId] = React.useState('');
|
|
766
|
+
const [formSshTarget, setFormSshTarget] = React.useState('');
|
|
767
|
+
const [formSshKeyPath, setFormSshKeyPath] = React.useState('');
|
|
768
|
+
const [formDockerImage, setFormDockerImage] = React.useState('');
|
|
769
|
+
const [formNodePath, setFormNodePath] = React.useState('');
|
|
770
|
+
const [formPythonPath, setFormPythonPath] = React.useState('');
|
|
771
|
+
const [formSkillName, setFormSkillName] = React.useState('');
|
|
772
|
+
const [formWorkflowName, setFormWorkflowName] = React.useState('');
|
|
487
773
|
const [stats, setStats] = React.useState(null);
|
|
488
774
|
|
|
775
|
+
// Live execution indicator (#35): a light 1s tick only while some task
|
|
776
|
+
// is actually running.
|
|
777
|
+
const tasksRef = React.useRef([]);
|
|
778
|
+
const [nowTick, setNowTick] = React.useState(0);
|
|
779
|
+
React.useEffect(() => {
|
|
780
|
+
tasksRef.current = tasks;
|
|
781
|
+
}, [tasks]);
|
|
782
|
+
React.useEffect(() => {
|
|
783
|
+
const tick = setInterval(() => {
|
|
784
|
+
if (tasksRef.current.some((t2) => t2.running)) setNowTick((v) => v + 1);
|
|
785
|
+
}, 1000);
|
|
786
|
+
return () => clearInterval(tick);
|
|
787
|
+
}, []);
|
|
788
|
+
|
|
489
789
|
// Settings modal state
|
|
490
790
|
const [settingsModalOpen, setSettingsModalOpen] = React.useState(false);
|
|
491
791
|
const [settingsBotToken, setSettingsBotToken] = React.useState('');
|
|
@@ -493,6 +793,8 @@ window.__ModuleLoader__.load({
|
|
|
493
793
|
const [settingsNotifyGlobal, setSettingsNotifyGlobal] = React.useState(false);
|
|
494
794
|
const [settingsOnlyOnFailureGlobal, setSettingsOnlyOnFailureGlobal] = React.useState(false);
|
|
495
795
|
const [settingsKanbanUrl, setSettingsKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
796
|
+
const [settingsDelivery, setSettingsDelivery] = React.useState({});
|
|
797
|
+
const [settingsSections, setSettingsSections] = React.useState({ channels: true });
|
|
496
798
|
const [settingsHasDefault, setSettingsHasDefault] = React.useState(false);
|
|
497
799
|
const [settingsHasCustomBotToken, setSettingsHasCustomBotToken] = React.useState(false);
|
|
498
800
|
const [testStatus, setTestStatus] = React.useState(null);
|
|
@@ -620,6 +922,7 @@ window.__ModuleLoader__.load({
|
|
|
620
922
|
setSettingsNotifyGlobal(Boolean(data.settings.notifyTelegram));
|
|
621
923
|
setSettingsOnlyOnFailureGlobal(Boolean(data.settings.onlyOnFailure));
|
|
622
924
|
setSettingsKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
925
|
+
setSettingsDelivery(pickDeliverySettings(data.settings));
|
|
623
926
|
setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
624
927
|
setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
|
|
625
928
|
}
|
|
@@ -632,13 +935,13 @@ window.__ModuleLoader__.load({
|
|
|
632
935
|
const res = await fetch('/dsh-cron/settings', {
|
|
633
936
|
method: 'POST',
|
|
634
937
|
headers: { 'Content-Type': 'application/json' },
|
|
635
|
-
body: JSON.stringify({
|
|
938
|
+
body: JSON.stringify(Object.assign({
|
|
636
939
|
botToken: settingsBotToken,
|
|
637
940
|
chatId: settingsChatId,
|
|
638
941
|
notifyTelegram: settingsNotifyGlobal,
|
|
639
942
|
onlyOnFailure: settingsOnlyOnFailureGlobal,
|
|
640
943
|
kanbanBaseUrl: settingsKanbanUrl,
|
|
641
|
-
})
|
|
944
|
+
}, settingsDelivery))
|
|
642
945
|
});
|
|
643
946
|
const data = await res.json();
|
|
644
947
|
if (data && data.ok) {
|
|
@@ -651,8 +954,35 @@ window.__ModuleLoader__.load({
|
|
|
651
954
|
}
|
|
652
955
|
};
|
|
653
956
|
|
|
654
|
-
|
|
655
|
-
|
|
957
|
+
/**
|
|
958
|
+
* Update one delivery setting without dropping the rest of the object.
|
|
959
|
+
* `undefined` removes the key so the server default applies again; an
|
|
960
|
+
* empty string is kept so a text field can be cleared on purpose.
|
|
961
|
+
*/
|
|
962
|
+
const updateDelivery = (key, value) => {
|
|
963
|
+
setSettingsDelivery((prev) => {
|
|
964
|
+
const next = Object.assign({}, prev);
|
|
965
|
+
if (value === undefined) delete next[key];
|
|
966
|
+
else next[key] = value;
|
|
967
|
+
return next;
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
/** Per-channel template overrides live in a nested map on the server. */
|
|
972
|
+
const updateChannelTemplate = (channelId, value) => {
|
|
973
|
+
setSettingsDelivery((prev) => {
|
|
974
|
+
const templates = Object.assign({}, prev.channelTemplates || {});
|
|
975
|
+
if (value && value.trim()) templates[channelId] = value;
|
|
976
|
+
else delete templates[channelId];
|
|
977
|
+
return Object.assign({}, prev, { channelTemplates: templates });
|
|
978
|
+
});
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const toggleDeliverySection = (key) => {
|
|
982
|
+
setSettingsSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
656
986
|
try {
|
|
657
987
|
const res = await fetch('/dsh-cron/telegram/test', {
|
|
658
988
|
method: 'POST',
|
|
@@ -680,10 +1010,33 @@ window.__ModuleLoader__.load({
|
|
|
680
1010
|
setFormType('llm');
|
|
681
1011
|
setFormDelivery('isolated');
|
|
682
1012
|
setFormNotifyTelegram(false);
|
|
1013
|
+
setFormChannels([]);
|
|
1014
|
+
setFormTemplate('');
|
|
683
1015
|
setFormOnlyOnFailure(false);
|
|
684
1016
|
setFormTimeoutSeconds(1800);
|
|
685
1017
|
setFormOverlapPolicy('skip');
|
|
686
1018
|
setFormKanbanMode('none');
|
|
1019
|
+
setFormTimezone('');
|
|
1020
|
+
setFormMisfirePolicy('skip');
|
|
1021
|
+
setFormMaxRetries(0);
|
|
1022
|
+
setFormPermissionPreset('default');
|
|
1023
|
+
setFormEnv('');
|
|
1024
|
+
setFormCwd('');
|
|
1025
|
+
setFormWorkspaceId('');
|
|
1026
|
+
setFormWorktree(false);
|
|
1027
|
+
setFormKeepWorktree(false);
|
|
1028
|
+
setFormHttpMethod('GET');
|
|
1029
|
+
setFormHttpUrl('');
|
|
1030
|
+
setFormHttpHeaders('');
|
|
1031
|
+
setFormHttpBody('');
|
|
1032
|
+
setFormSshProfileId('');
|
|
1033
|
+
setFormSshTarget('');
|
|
1034
|
+
setFormSshKeyPath('');
|
|
1035
|
+
setFormDockerImage('');
|
|
1036
|
+
setFormNodePath('');
|
|
1037
|
+
setFormPythonPath('');
|
|
1038
|
+
setFormSkillName('');
|
|
1039
|
+
setFormWorkflowName('');
|
|
687
1040
|
if (preset) {
|
|
688
1041
|
setFormTitle(preset.title || '');
|
|
689
1042
|
setFormSchedule(preset.schedule || '');
|
|
@@ -730,10 +1083,33 @@ window.__ModuleLoader__.load({
|
|
|
730
1083
|
setFormModel(task.model || '');
|
|
731
1084
|
setFormDelivery(task.delivery || 'isolated');
|
|
732
1085
|
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
1086
|
+
setFormChannels(Array.isArray(task.channels) ? task.channels.filter((c) => DELIVERY_CHANNELS.includes(c)) : []);
|
|
1087
|
+
setFormTemplate(task.template || '');
|
|
733
1088
|
setFormOnlyOnFailure(Boolean(task.onlyOnFailure));
|
|
734
1089
|
setFormTimeoutSeconds(task.timeoutSeconds !== undefined ? task.timeoutSeconds : 1800);
|
|
735
1090
|
setFormOverlapPolicy(task.overlapPolicy || 'skip');
|
|
736
1091
|
setFormKanbanMode(task.kanbanMode || 'none');
|
|
1092
|
+
setFormTimezone(task.timezone || '');
|
|
1093
|
+
setFormMisfirePolicy(task.misfirePolicy || 'skip');
|
|
1094
|
+
setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
|
|
1095
|
+
setFormPermissionPreset(task.permissionPreset || 'default');
|
|
1096
|
+
setFormEnv(formatEnvText(task.env));
|
|
1097
|
+
setFormCwd(task.cwd || '');
|
|
1098
|
+
setFormWorkspaceId(task.workspaceId || '');
|
|
1099
|
+
setFormWorktree(Boolean(task.worktree));
|
|
1100
|
+
setFormKeepWorktree(Boolean(task.keepWorktree));
|
|
1101
|
+
setFormHttpMethod(task.httpMethod || 'GET');
|
|
1102
|
+
setFormHttpUrl(task.httpUrl || '');
|
|
1103
|
+
setFormHttpHeaders(typeof task.httpHeaders === 'string' ? task.httpHeaders : (task.httpHeaders ? JSON.stringify(task.httpHeaders) : ''));
|
|
1104
|
+
setFormHttpBody(task.httpBody || '');
|
|
1105
|
+
setFormSshProfileId(task.sshProfileId || '');
|
|
1106
|
+
setFormSshTarget(task.sshTarget || '');
|
|
1107
|
+
setFormSshKeyPath(task.sshKeyPath || '');
|
|
1108
|
+
setFormDockerImage(task.dockerImage || '');
|
|
1109
|
+
setFormNodePath(task.nodePath || '');
|
|
1110
|
+
setFormPythonPath(task.pythonPath || '');
|
|
1111
|
+
setFormSkillName(task.skillName || '');
|
|
1112
|
+
setFormWorkflowName(task.workflowName || '');
|
|
737
1113
|
if (task.provider) loadModels(task.provider);
|
|
738
1114
|
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
739
1115
|
loadTaskHistory(task.id);
|
|
@@ -750,26 +1126,50 @@ window.__ModuleLoader__.load({
|
|
|
750
1126
|
const handleSaveManual = async (e) => {
|
|
751
1127
|
e.preventDefault();
|
|
752
1128
|
try {
|
|
1129
|
+
const isAgentType = AGENT_FORM_TYPES.includes(formType);
|
|
753
1130
|
const payload = {
|
|
754
1131
|
id: formId || undefined,
|
|
755
1132
|
status: selectedTaskMeta ? selectedTaskMeta.status : 'active',
|
|
756
1133
|
title: formTitle,
|
|
757
1134
|
schedule: formSchedule,
|
|
758
|
-
prompt: formPrompt,
|
|
1135
|
+
prompt: formType === 'http' ? (formPrompt.trim() || formHttpUrl.trim()) : formPrompt,
|
|
759
1136
|
type: formType,
|
|
760
1137
|
delivery: formDelivery || 'isolated',
|
|
761
|
-
provider:
|
|
762
|
-
model:
|
|
1138
|
+
provider: isAgentType ? (formProvider || undefined) : undefined,
|
|
1139
|
+
model: isAgentType ? (formModel || undefined) : undefined,
|
|
763
1140
|
notifyTelegram: formNotifyTelegram,
|
|
1141
|
+
channels: formChannels,
|
|
1142
|
+
template: formTemplate.trim(),
|
|
764
1143
|
onlyOnFailure: formOnlyOnFailure,
|
|
765
1144
|
timeoutSeconds: Number(formTimeoutSeconds) || 1800,
|
|
766
1145
|
overlapPolicy: formOverlapPolicy,
|
|
1146
|
+
timezone: formTimezone.trim(),
|
|
1147
|
+
misfirePolicy: formMisfirePolicy,
|
|
1148
|
+
maxRetries: Number(formMaxRetries) || 0,
|
|
1149
|
+
permissionPreset: formPermissionPreset,
|
|
767
1150
|
kanbanMode: formKanbanMode,
|
|
1151
|
+
env: parseEnvText(formEnv),
|
|
1152
|
+
cwd: formCwd.trim(),
|
|
1153
|
+
workspaceId: formWorkspaceId.trim(),
|
|
1154
|
+
worktree: isAgentType ? Boolean(formWorktree) : false,
|
|
1155
|
+
keepWorktree: isAgentType ? Boolean(formKeepWorktree) : false,
|
|
1156
|
+
httpMethod: formHttpMethod,
|
|
1157
|
+
httpUrl: formHttpUrl.trim(),
|
|
1158
|
+
httpHeaders: formHttpHeaders.trim() || undefined,
|
|
1159
|
+
httpBody: formHttpBody,
|
|
1160
|
+
sshProfileId: formSshProfileId.trim(),
|
|
1161
|
+
sshTarget: formSshTarget.trim(),
|
|
1162
|
+
sshKeyPath: formSshKeyPath.trim(),
|
|
1163
|
+
dockerImage: formDockerImage.trim(),
|
|
1164
|
+
nodePath: formNodePath.trim(),
|
|
1165
|
+
pythonPath: formPythonPath.trim(),
|
|
1166
|
+
skillName: formSkillName.trim(),
|
|
1167
|
+
workflowName: formWorkflowName.trim(),
|
|
768
1168
|
};
|
|
769
|
-
// Cross-origin hardening:
|
|
770
|
-
// header, which
|
|
1169
|
+
// Cross-origin hardening: every code-executing type needs the explicit
|
|
1170
|
+
// confirm header, which forged cross-site posts cannot attach (#86)
|
|
771
1171
|
const headers = { 'Content-Type': 'application/json' };
|
|
772
|
-
if (formType
|
|
1172
|
+
if (CODE_FORM_TYPES.includes(formType)) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
773
1173
|
const res = await fetch('/dsh-cron/tasks', {
|
|
774
1174
|
method: 'POST',
|
|
775
1175
|
headers,
|
|
@@ -940,7 +1340,7 @@ window.__ModuleLoader__.load({
|
|
|
940
1340
|
},
|
|
941
1341
|
React.createElement('button', {
|
|
942
1342
|
type: 'button',
|
|
943
|
-
className: 'dsh-cron-task-status-btn',
|
|
1343
|
+
className: 'dsh-cron-task-status-btn' + (task.running ? ' dsh-cron-pulse' : ''),
|
|
944
1344
|
'data-active': task.status === 'active' ? 'true' : undefined,
|
|
945
1345
|
title: task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
|
|
946
1346
|
'aria-label': task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
|
|
@@ -962,7 +1362,10 @@ window.__ModuleLoader__.load({
|
|
|
962
1362
|
),
|
|
963
1363
|
React.createElement('div', { className: 'dsh-cron-task-sched' },
|
|
964
1364
|
task.scheduleText || task.schedule,
|
|
965
|
-
task.nextRunAt ? T_KEY(t, 'list.next', { time: new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : ''
|
|
1365
|
+
task.nextRunAt ? T_KEY(t, 'list.next', { time: new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : '',
|
|
1366
|
+
task.running && task.runningSince
|
|
1367
|
+
? ' · ' + T_KEY(t, 'list.runningNow') + ' ' + Math.max(0, Math.round((Date.now() - task.runningSince) / 1000)) + 's'
|
|
1368
|
+
: ''
|
|
966
1369
|
),
|
|
967
1370
|
task.prompt ? React.createElement('div', {
|
|
968
1371
|
className: 'dsh-cron-task-prompt-preview',
|
|
@@ -1085,7 +1488,13 @@ window.__ModuleLoader__.load({
|
|
|
1085
1488
|
)
|
|
1086
1489
|
),
|
|
1087
1490
|
run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
|
|
1088
|
-
run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: 'var(--dsh-cron-danger)', borderColor: 'var(--dsh-cron-danger-bg)' } }, run.error)
|
|
1491
|
+
run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: 'var(--dsh-cron-danger)', borderColor: 'var(--dsh-cron-danger-bg)' } }, run.error),
|
|
1492
|
+
run.sessionId && React.createElement('button', {
|
|
1493
|
+
type: 'button',
|
|
1494
|
+
className: 'dsh-cron-btn-secondary',
|
|
1495
|
+
style: { marginTop: '8px', fontSize: '12px', padding: '4px 10px' },
|
|
1496
|
+
onClick: () => openSession(ctx, run.sessionId)
|
|
1497
|
+
}, T_KEY(t, 'history.openSession'))
|
|
1089
1498
|
))
|
|
1090
1499
|
),
|
|
1091
1500
|
React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '18px' } },
|
|
@@ -1121,7 +1530,14 @@ window.__ModuleLoader__.load({
|
|
|
1121
1530
|
onChange: (e) => setFormType(e.target.value)
|
|
1122
1531
|
},
|
|
1123
1532
|
React.createElement('option', { value: 'llm' }, T_KEY(t, 'form.typeLlm')),
|
|
1124
|
-
React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript'))
|
|
1533
|
+
React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript')),
|
|
1534
|
+
React.createElement('option', { value: 'node' }, T_KEY(t, 'form.typeNode')),
|
|
1535
|
+
React.createElement('option', { value: 'python' }, T_KEY(t, 'form.typePython')),
|
|
1536
|
+
React.createElement('option', { value: 'http' }, T_KEY(t, 'form.typeHttp')),
|
|
1537
|
+
React.createElement('option', { value: 'ssh' }, T_KEY(t, 'form.typeSsh')),
|
|
1538
|
+
React.createElement('option', { value: 'docker' }, T_KEY(t, 'form.typeDocker')),
|
|
1539
|
+
React.createElement('option', { value: 'skill' }, T_KEY(t, 'form.typeSkill')),
|
|
1540
|
+
React.createElement('option', { value: 'workflow' }, T_KEY(t, 'form.typeWorkflow'))
|
|
1125
1541
|
)
|
|
1126
1542
|
),
|
|
1127
1543
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
@@ -1157,7 +1573,52 @@ window.__ModuleLoader__.load({
|
|
|
1157
1573
|
)
|
|
1158
1574
|
)
|
|
1159
1575
|
),
|
|
1576
|
+
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1577
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1578
|
+
React.createElement('label', null, T_KEY(t, 'form.timezoneLabel')),
|
|
1579
|
+
React.createElement('input', {
|
|
1580
|
+
value: formTimezone,
|
|
1581
|
+
placeholder: T_KEY(t, 'form.timezonePlaceholder'),
|
|
1582
|
+
onChange: (e) => setFormTimezone(e.target.value)
|
|
1583
|
+
})
|
|
1584
|
+
),
|
|
1585
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1586
|
+
React.createElement('label', null, T_KEY(t, 'form.misfireLabel')),
|
|
1587
|
+
React.createElement('select', {
|
|
1588
|
+
value: formMisfirePolicy,
|
|
1589
|
+
onChange: (e) => setFormMisfirePolicy(e.target.value)
|
|
1590
|
+
},
|
|
1591
|
+
React.createElement('option', { value: 'skip' }, T_KEY(t, 'form.misfireSkip')),
|
|
1592
|
+
React.createElement('option', { value: 'runOnce' }, T_KEY(t, 'form.misfireRunOnce')),
|
|
1593
|
+
React.createElement('option', { value: 'catchUpAll' }, T_KEY(t, 'form.misfireCatchUp'))
|
|
1594
|
+
)
|
|
1595
|
+
)
|
|
1596
|
+
),
|
|
1597
|
+
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1160
1598
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1599
|
+
React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
|
|
1600
|
+
React.createElement('input', {
|
|
1601
|
+
type: 'number',
|
|
1602
|
+
min: 0,
|
|
1603
|
+
value: formMaxRetries,
|
|
1604
|
+
placeholder: '0',
|
|
1605
|
+
onChange: (e) => setFormMaxRetries(e.target.value)
|
|
1606
|
+
})
|
|
1607
|
+
),
|
|
1608
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1609
|
+
React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
|
|
1610
|
+
React.createElement('select', {
|
|
1611
|
+
value: formPermissionPreset,
|
|
1612
|
+
onChange: (e) => setFormPermissionPreset(e.target.value)
|
|
1613
|
+
},
|
|
1614
|
+
React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
|
|
1615
|
+
React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
|
|
1616
|
+
React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
|
|
1617
|
+
React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
|
|
1618
|
+
)
|
|
1619
|
+
)
|
|
1620
|
+
),
|
|
1621
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1161
1622
|
React.createElement('label', null, T_KEY(t, 'form.kanbanLabel')),
|
|
1162
1623
|
React.createElement('select', {
|
|
1163
1624
|
value: formKanbanMode,
|
|
@@ -1168,7 +1629,7 @@ window.__ModuleLoader__.load({
|
|
|
1168
1629
|
React.createElement('option', { value: 'always' }, T_KEY(t, 'form.kanbanAlways'))
|
|
1169
1630
|
)
|
|
1170
1631
|
),
|
|
1171
|
-
formType
|
|
1632
|
+
AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1172
1633
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1173
1634
|
React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
|
|
1174
1635
|
React.createElement('select', {
|
|
@@ -1190,14 +1651,165 @@ window.__ModuleLoader__.load({
|
|
|
1190
1651
|
)
|
|
1191
1652
|
)
|
|
1192
1653
|
),
|
|
1193
|
-
React.createElement('div', { className: 'dsh-cron-form-
|
|
1194
|
-
React.createElement('
|
|
1654
|
+
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1655
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1656
|
+
React.createElement('label', null, T_KEY(t, 'form.httpMethodLabel')),
|
|
1657
|
+
React.createElement('select', {
|
|
1658
|
+
value: formHttpMethod,
|
|
1659
|
+
onChange: (e) => setFormHttpMethod(e.target.value)
|
|
1660
|
+
},
|
|
1661
|
+
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].map((m) => React.createElement('option', { key: m, value: m }, m))
|
|
1662
|
+
)
|
|
1663
|
+
),
|
|
1664
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1665
|
+
React.createElement('label', null, T_KEY(t, 'form.httpUrlLabel')),
|
|
1666
|
+
React.createElement('input', {
|
|
1667
|
+
required: true,
|
|
1668
|
+
value: formHttpUrl,
|
|
1669
|
+
placeholder: 'https://example.test/webhook',
|
|
1670
|
+
onChange: (e) => setFormHttpUrl(e.target.value)
|
|
1671
|
+
})
|
|
1672
|
+
)
|
|
1673
|
+
),
|
|
1674
|
+
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1675
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1676
|
+
React.createElement('label', null, T_KEY(t, 'form.httpHeadersLabel')),
|
|
1677
|
+
React.createElement('input', {
|
|
1678
|
+
value: formHttpHeaders,
|
|
1679
|
+
placeholder: '{"Authorization":"Bearer ..."}',
|
|
1680
|
+
onChange: (e) => setFormHttpHeaders(e.target.value)
|
|
1681
|
+
})
|
|
1682
|
+
),
|
|
1683
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1684
|
+
React.createElement('label', null, T_KEY(t, 'form.httpBodyLabel')),
|
|
1685
|
+
React.createElement('input', {
|
|
1686
|
+
value: formHttpBody,
|
|
1687
|
+
onChange: (e) => setFormHttpBody(e.target.value)
|
|
1688
|
+
})
|
|
1689
|
+
)
|
|
1690
|
+
),
|
|
1691
|
+
formType === 'ssh' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1692
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1693
|
+
React.createElement('label', null, T_KEY(t, 'form.sshProfileLabel')),
|
|
1694
|
+
React.createElement('input', {
|
|
1695
|
+
value: formSshProfileId,
|
|
1696
|
+
placeholder: 'prod-web',
|
|
1697
|
+
onChange: (e) => setFormSshProfileId(e.target.value)
|
|
1698
|
+
}),
|
|
1699
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.sshProfileHint'))
|
|
1700
|
+
),
|
|
1701
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1702
|
+
React.createElement('label', null, T_KEY(t, 'form.sshTargetLabel')),
|
|
1703
|
+
React.createElement('input', {
|
|
1704
|
+
value: formSshTarget,
|
|
1705
|
+
placeholder: 'root@10.0.0.5',
|
|
1706
|
+
onChange: (e) => setFormSshTarget(e.target.value)
|
|
1707
|
+
}),
|
|
1708
|
+
React.createElement('input', {
|
|
1709
|
+
style: { marginTop: '6px' },
|
|
1710
|
+
value: formSshKeyPath,
|
|
1711
|
+
placeholder: '~/.ssh/id_ed25519',
|
|
1712
|
+
onChange: (e) => setFormSshKeyPath(e.target.value)
|
|
1713
|
+
})
|
|
1714
|
+
)
|
|
1715
|
+
),
|
|
1716
|
+
formType === 'docker' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1717
|
+
React.createElement('label', null, T_KEY(t, 'form.dockerImageLabel')),
|
|
1718
|
+
React.createElement('input', {
|
|
1719
|
+
required: true,
|
|
1720
|
+
value: formDockerImage,
|
|
1721
|
+
placeholder: 'alpine:3.20',
|
|
1722
|
+
onChange: (e) => setFormDockerImage(e.target.value)
|
|
1723
|
+
})
|
|
1724
|
+
),
|
|
1725
|
+
formType === 'skill' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1726
|
+
React.createElement('label', null, T_KEY(t, 'form.skillNameLabel')),
|
|
1727
|
+
React.createElement('input', {
|
|
1728
|
+
value: formSkillName,
|
|
1729
|
+
placeholder: 'gitea',
|
|
1730
|
+
onChange: (e) => setFormSkillName(e.target.value)
|
|
1731
|
+
})
|
|
1732
|
+
),
|
|
1733
|
+
formType === 'workflow' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1734
|
+
React.createElement('label', null, T_KEY(t, 'form.workflowNameLabel')),
|
|
1735
|
+
React.createElement('input', {
|
|
1736
|
+
value: formWorkflowName,
|
|
1737
|
+
placeholder: 'nightly-release',
|
|
1738
|
+
onChange: (e) => setFormWorkflowName(e.target.value)
|
|
1739
|
+
})
|
|
1740
|
+
),
|
|
1741
|
+
(formType === 'node' || formType === 'python') && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1742
|
+
React.createElement('label', null, formType === 'node' ? 'Node.js binary (optional)' : 'Python interpreter (optional)'),
|
|
1743
|
+
React.createElement('input', {
|
|
1744
|
+
value: formType === 'node' ? formNodePath : formPythonPath,
|
|
1745
|
+
placeholder: formType === 'node' ? 'auto' : '.venv/bin/python',
|
|
1746
|
+
onChange: (e) => (formType === 'node' ? setFormNodePath(e.target.value) : setFormPythonPath(e.target.value))
|
|
1747
|
+
})
|
|
1748
|
+
),
|
|
1749
|
+
!AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1750
|
+
React.createElement('label', null, T_KEY(t, 'form.envLabel')),
|
|
1751
|
+
React.createElement('textarea', {
|
|
1752
|
+
rows: 3,
|
|
1753
|
+
style: { fontFamily: 'monospace', fontSize: '12.5px' },
|
|
1754
|
+
value: formEnv,
|
|
1755
|
+
placeholder: T_KEY(t, 'form.envPlaceholder'),
|
|
1756
|
+
onChange: (e) => setFormEnv(e.target.value)
|
|
1757
|
+
}),
|
|
1758
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.envHint'))
|
|
1759
|
+
),
|
|
1760
|
+
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1761
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1762
|
+
React.createElement('label', null, T_KEY(t, 'form.cwdLabel')),
|
|
1763
|
+
React.createElement('input', {
|
|
1764
|
+
value: formCwd,
|
|
1765
|
+
placeholder: '/srv/app',
|
|
1766
|
+
onChange: (e) => setFormCwd(e.target.value)
|
|
1767
|
+
})
|
|
1768
|
+
),
|
|
1769
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1770
|
+
React.createElement('label', null, T_KEY(t, 'form.workspaceLabel')),
|
|
1771
|
+
React.createElement('input', {
|
|
1772
|
+
value: formWorkspaceId,
|
|
1773
|
+
onChange: (e) => setFormWorkspaceId(e.target.value)
|
|
1774
|
+
})
|
|
1775
|
+
)
|
|
1776
|
+
),
|
|
1777
|
+
AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1778
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
1779
|
+
React.createElement('input', {
|
|
1780
|
+
type: 'checkbox',
|
|
1781
|
+
checked: formWorktree,
|
|
1782
|
+
onChange: (e) => setFormWorktree(e.target.checked)
|
|
1783
|
+
}),
|
|
1784
|
+
React.createElement('span', null, T_KEY(t, 'form.worktreeLabel'))
|
|
1785
|
+
),
|
|
1786
|
+
formWorktree && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1787
|
+
React.createElement('input', {
|
|
1788
|
+
type: 'checkbox',
|
|
1789
|
+
checked: formKeepWorktree,
|
|
1790
|
+
onChange: (e) => setFormKeepWorktree(e.target.checked)
|
|
1791
|
+
}),
|
|
1792
|
+
React.createElement('span', null, T_KEY(t, 'form.keepWorktreeLabel'))
|
|
1793
|
+
)
|
|
1794
|
+
),
|
|
1795
|
+
formType !== 'http' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1796
|
+
React.createElement('label', null, AGENT_FORM_TYPES.includes(formType) ? T_KEY(t, 'form.promptLabelLlm') : T_KEY(t, 'form.promptLabelScript')),
|
|
1195
1797
|
React.createElement('textarea', {
|
|
1196
1798
|
required: true,
|
|
1197
1799
|
rows: 9,
|
|
1198
1800
|
style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
|
|
1199
1801
|
value: formPrompt,
|
|
1200
|
-
placeholder:
|
|
1802
|
+
placeholder: AGENT_FORM_TYPES.includes(formType)
|
|
1803
|
+
? T_KEY(t, 'form.promptPlaceholderLlm')
|
|
1804
|
+
: formType === 'docker'
|
|
1805
|
+
? 'python -V'
|
|
1806
|
+
: formType === 'ssh'
|
|
1807
|
+
? 'uname -a'
|
|
1808
|
+
: formType === 'node'
|
|
1809
|
+
? 'console.log("hello")'
|
|
1810
|
+
: formType === 'python'
|
|
1811
|
+
? 'print("hello")'
|
|
1812
|
+
: T_KEY(t, 'form.promptPlaceholderScript'),
|
|
1201
1813
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
1202
1814
|
})
|
|
1203
1815
|
),
|
|
@@ -1222,13 +1834,43 @@ window.__ModuleLoader__.load({
|
|
|
1222
1834
|
}),
|
|
1223
1835
|
React.createElement('span', null, T_KEY(t, 'form.notifyTelegram'))
|
|
1224
1836
|
),
|
|
1225
|
-
formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1837
|
+
(formNotifyTelegram || formChannels.length > 0) && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1226
1838
|
React.createElement('input', {
|
|
1227
1839
|
type: 'checkbox',
|
|
1228
1840
|
checked: formOnlyOnFailure,
|
|
1229
1841
|
onChange: (e) => setFormOnlyOnFailure(e.target.checked)
|
|
1230
1842
|
}),
|
|
1231
1843
|
React.createElement('span', null, T_KEY(t, 'form.notifyOnlyFailure'))
|
|
1844
|
+
),
|
|
1845
|
+
React.createElement('div', { style: { marginTop: '12px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l2, #2e2e2e)' } },
|
|
1846
|
+
React.createElement('div', { style: { fontWeight: 500, fontSize: '12.5px', color: 'var(--dsw-alias-label-secondary, #aaa)', marginBottom: '8px' } }, T_KEY(t, 'form.channelsTitle')),
|
|
1847
|
+
React.createElement('div', { className: 'dsh-cron-channel-grid' },
|
|
1848
|
+
...DELIVERY_CHANNELS.map((id) => React.createElement('label', { key: id, className: 'dsh-cron-checkbox-row' },
|
|
1849
|
+
React.createElement('input', {
|
|
1850
|
+
type: 'checkbox',
|
|
1851
|
+
checked: formChannels.indexOf(id) >= 0,
|
|
1852
|
+
onChange: (e) => {
|
|
1853
|
+
setFormChannels((prev) => {
|
|
1854
|
+
const set = new Set(prev);
|
|
1855
|
+
if (e.target.checked) set.add(id); else set.delete(id);
|
|
1856
|
+
return DELIVERY_CHANNELS.filter((c) => set.has(c));
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
}),
|
|
1860
|
+
React.createElement('span', null, T_KEY(t, CHANNEL_LABEL_KEYS[id]))
|
|
1861
|
+
))
|
|
1862
|
+
),
|
|
1863
|
+
React.createElement('div', { className: 'dsh-cron-hint', style: { marginTop: '6px' } }, T_KEY(t, 'form.channelsHint'))
|
|
1864
|
+
),
|
|
1865
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginTop: '12px', marginBottom: 0 } },
|
|
1866
|
+
React.createElement('label', null, T_KEY(t, 'form.templateLabel')),
|
|
1867
|
+
React.createElement('input', {
|
|
1868
|
+
type: 'text',
|
|
1869
|
+
value: formTemplate,
|
|
1870
|
+
placeholder: T_KEY(t, 'form.templatePlaceholder'),
|
|
1871
|
+
onChange: (e) => setFormTemplate(e.target.value)
|
|
1872
|
+
}),
|
|
1873
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.templateHint'))
|
|
1232
1874
|
)
|
|
1233
1875
|
),
|
|
1234
1876
|
selectedTaskMeta && React.createElement('div', {
|
|
@@ -1276,7 +1918,7 @@ window.__ModuleLoader__.load({
|
|
|
1276
1918
|
|
|
1277
1919
|
// 2. Settings modal
|
|
1278
1920
|
settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
|
|
1279
|
-
React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
1921
|
+
React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
1280
1922
|
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
1281
1923
|
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1282
1924
|
React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
|
|
@@ -1325,6 +1967,14 @@ window.__ModuleLoader__.load({
|
|
|
1325
1967
|
React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
|
|
1326
1968
|
)
|
|
1327
1969
|
),
|
|
1970
|
+
...renderDeliverySettings({
|
|
1971
|
+
T: (key, vars) => T_KEY(t, key, vars),
|
|
1972
|
+
values: settingsDelivery,
|
|
1973
|
+
onChange: updateDelivery,
|
|
1974
|
+
onChannelTemplate: updateChannelTemplate,
|
|
1975
|
+
sections: settingsSections,
|
|
1976
|
+
onToggleSection: toggleDeliverySection,
|
|
1977
|
+
}),
|
|
1328
1978
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
|
|
1329
1979
|
React.createElement('button', {
|
|
1330
1980
|
type: 'button',
|
|
@@ -1410,6 +2060,8 @@ window.__ModuleLoader__.load({
|
|
|
1410
2060
|
const [notify, setNotify] = React.useState(false);
|
|
1411
2061
|
const [onlyFailure, setOnlyFailure] = React.useState(false);
|
|
1412
2062
|
const [kanbanUrl, setKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
2063
|
+
const [delivery, setDelivery] = React.useState({});
|
|
2064
|
+
const [sections, setSections] = React.useState({ channels: true });
|
|
1413
2065
|
const [hasDefault, setHasDefault] = React.useState(false);
|
|
1414
2066
|
const [saved, setSaved] = React.useState(false);
|
|
1415
2067
|
const [testInfo, setTestInfo] = React.useState(null);
|
|
@@ -1434,6 +2086,7 @@ window.__ModuleLoader__.load({
|
|
|
1434
2086
|
setNotify(Boolean(data.settings.notifyTelegram));
|
|
1435
2087
|
setOnlyFailure(Boolean(data.settings.onlyOnFailure));
|
|
1436
2088
|
setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
2089
|
+
setDelivery(pickDeliverySettings(data.settings));
|
|
1437
2090
|
setHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
1438
2091
|
setLoadState('ready');
|
|
1439
2092
|
} else {
|
|
@@ -1450,13 +2103,13 @@ window.__ModuleLoader__.load({
|
|
|
1450
2103
|
const res = await fetch('/dsh-cron/settings', {
|
|
1451
2104
|
method: 'POST',
|
|
1452
2105
|
headers: { 'Content-Type': 'application/json' },
|
|
1453
|
-
body: JSON.stringify({
|
|
2106
|
+
body: JSON.stringify(Object.assign({
|
|
1454
2107
|
botToken: token,
|
|
1455
2108
|
chatId,
|
|
1456
2109
|
notifyTelegram: notify,
|
|
1457
2110
|
onlyOnFailure: onlyFailure,
|
|
1458
2111
|
kanbanBaseUrl: kanbanUrl
|
|
1459
|
-
})
|
|
2112
|
+
}, delivery))
|
|
1460
2113
|
});
|
|
1461
2114
|
const data = await res.json();
|
|
1462
2115
|
if (data && data.ok) {
|
|
@@ -1586,6 +2239,24 @@ window.__ModuleLoader__.load({
|
|
|
1586
2239
|
React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
|
|
1587
2240
|
)
|
|
1588
2241
|
),
|
|
2242
|
+
...renderDeliverySettings({
|
|
2243
|
+
T: (key, vars) => T_KEY(t, key, vars),
|
|
2244
|
+
values: delivery,
|
|
2245
|
+
onChange: (key, value) => setDelivery((prev) => {
|
|
2246
|
+
const next = Object.assign({}, prev);
|
|
2247
|
+
if (value === undefined) delete next[key];
|
|
2248
|
+
else next[key] = value;
|
|
2249
|
+
return next;
|
|
2250
|
+
}),
|
|
2251
|
+
onChannelTemplate: (channelId, value) => setDelivery((prev) => {
|
|
2252
|
+
const templates = Object.assign({}, prev.channelTemplates || {});
|
|
2253
|
+
if (value && value.trim()) templates[channelId] = value;
|
|
2254
|
+
else delete templates[channelId];
|
|
2255
|
+
return Object.assign({}, prev, { channelTemplates: templates });
|
|
2256
|
+
}),
|
|
2257
|
+
sections,
|
|
2258
|
+
onToggleSection: (key) => setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] })),
|
|
2259
|
+
}),
|
|
1589
2260
|
testInfo && testInfo.message && React.createElement('div', {
|
|
1590
2261
|
className: 'dsh-cron-status-banner',
|
|
1591
2262
|
'data-type': testInfo.success ? 'success' : 'error',
|