@goodandready/dsh-cron 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -291,7 +291,271 @@ window.__ModuleLoader__.load({
291
291
  'dsh.startFailed': 'Failed to start the agent dialog: {error}',
292
292
  'dsh.launchError': 'Launch failed: {error}',
293
293
  'confirm.delete': 'Delete this task?',
294
- 'list.loadErrorPrefix': 'Failed to load tasks'
294
+ 'list.loadErrorPrefix': 'Failed to load tasks',
295
+ 'actions.dryRun': 'Dry run',
296
+ 'actions.dryRunTitle': 'Dry run (test execution without side effects or notifications)',
297
+ 'actions.archive': 'Archive',
298
+ 'actions.archiveTitle': 'Execution archive and logs',
299
+ 'dryRun.title': '🧪 Dry Run Simulation',
300
+ 'dryRun.task': 'Task',
301
+ 'dryRun.status': 'Simulation Status',
302
+ 'dryRun.duration': 'Duration',
303
+ 'dryRun.output': 'Simulated Output',
304
+ 'dryRun.preflight': 'Preflight Check',
305
+ 'dryRun.close': 'Close',
306
+ 'archive.title': '📜 Execution Archive',
307
+ 'archive.totalRuns': '{total} total runs recorded',
308
+ 'archive.empty': 'No archived runs recorded for this task yet.',
309
+ 'archive.loadMore': 'Load more ({shown} of {total})',
310
+ 'archive.close': 'Close',
311
+ 'form.reliabilityTitle': '🛡️ Reliability, Heartbeat & Self-Healing',
312
+ 'form.preflightLabel': 'Pre-flight check',
313
+ 'form.preflightNone': 'None',
314
+ 'form.preflightHttp': 'HTTP (2xx probe)',
315
+ 'form.preflightShell': 'Shell command (exit code 0)',
316
+ 'form.preflightDisk': 'Disk space (path:minPercent)',
317
+ 'form.preflightTargetLabel': 'Pre-flight target',
318
+ 'form.preflightTargetPlaceholder': 'https://example.com/healthz OR ls /data OR /:10%',
319
+ 'form.heartbeatIntervalLabel': 'Heartbeat deadline (seconds, 0 = disabled)',
320
+ 'form.gracePeriodLabel': 'Heartbeat grace period (seconds)',
321
+ 'form.priorityLabel': 'Queue priority (1-10, higher runs first)',
322
+ 'form.concurrencyGroupLabel': 'Concurrency group',
323
+ 'form.selfHealingLabel': 'Self-healing runbook command (shell)',
324
+ 'form.selfHealingPlaceholder': 'systemctl restart app || true',
325
+ 'form.autoDiagnoseLabel': 'Auto-diagnose failures with AI agent',
326
+ 'form.agentPresetLabel': 'Agent preset',
327
+ 'form.agentPresetPlaceholder': 'standard (leave empty for configured default)',
328
+ 'form.agentPresetHint': 'Agent preset from dsh-agent-presets (e.g. standard, minimal, cordis, ptc). Mounts workspace and coding tools.',
329
+ 'form.previewSchedule': 'Preview next runs',
330
+ 'form.previewScheduleTitle': 'Next 5 scheduled runs:',
331
+ },
332
+ zh: {
333
+ 'sidebar.label': '定时任务',
334
+ 'sidebar.jobsTitle': '运行中任务',
335
+ 'sidebar.noActiveJobs': '无运行中任务',
336
+ 'sidebar.moreJobs': '及另外 {count} 个',
337
+ 'chip.title': '定时任务 (Cron)',
338
+ 'panel.back': '返回对话',
339
+ 'panel.title': '定时任务',
340
+ 'panel.subtitle': '让 DSH 规划任务、设置提醒或跟踪状态',
341
+ 'actions.create': '新建',
342
+ 'actions.createWithDsh': '💬 让 DSH 创建',
343
+ 'actions.setupManually': '✏️ 手动配置',
344
+ 'actions.runNow': '立即运行',
345
+ 'actions.runNowShort': '运行',
346
+ 'actions.edit': '编辑 / 详情',
347
+ 'actions.duplicate': '复制任务',
348
+ 'actions.tuneWithDsh': '让 DSH 优化',
349
+ 'tune.startFailed': '无法启动优化会话: {error}',
350
+ 'actions.delete': '删除任务',
351
+ 'duplicate.done': '已创建副本并处于暂停状态:恢复前请先核对调度时间。',
352
+ 'duplicate.oneShotHint': '该副本为一次性任务且已暂停,请设定新的执行时间后再启用。',
353
+ 'duplicate.failed': '复制任务失败: {error}',
354
+ 'transfer.exportTitle': '导出任务',
355
+ 'transfer.importTitle': '导入任务',
356
+ 'transfer.exported': '已成功导出 {count} 个任务。',
357
+ 'transfer.exportFailed': '导出失败: {error}',
358
+ 'transfer.importTitleModal': '导入任务',
359
+ 'transfer.importHint': '仅导出任务静态配置:不包含历史记录与计数器。不包含密钥——渠道通过凭据名称引用。',
360
+ 'transfer.strategyAdd': '作为新任务导入(分配新 ID)',
361
+ 'transfer.strategyReplace': '覆盖同 ID 任务',
362
+ 'transfer.strategySkip': '跳过已存在的同 ID 任务',
363
+ 'transfer.summary': '该文件包含 {total} 个任务:{add} 个新增,{replace} 个覆盖,{skip} 个跳过。',
364
+ 'transfer.confirm': '开始导入',
365
+ 'transfer.imported': '已成功导出 {count} 个任务。',
366
+ 'transfer.importFailed': '导入失败: {error}',
367
+ 'transfer.badFile': '所选文件不是有效的 JSON 导出文件: {error}',
368
+ 'actions.pause': '暂停',
369
+ 'actions.resume': '恢复',
370
+ 'actions.settings': '通知与渠道设置',
371
+ 'actions.dryRun': '空跑测试',
372
+ 'actions.dryRunTitle': '空跑测试(模拟执行,不产生副作用且不发送通知)',
373
+ 'actions.archive': '历史归档',
374
+ 'actions.archiveTitle': '查看执行历史与日志归档',
375
+ 'dryRun.title': '🧪 空跑模拟结果',
376
+ 'dryRun.task': '任务',
377
+ 'dryRun.status': '模拟状态',
378
+ 'dryRun.duration': '耗时',
379
+ 'dryRun.output': '模拟输出',
380
+ 'dryRun.preflight': '预检结果',
381
+ 'dryRun.close': '关闭',
382
+ 'archive.title': '📜 运行历史归档',
383
+ 'archive.totalRuns': '共记录 {total} 次运行',
384
+ 'archive.empty': '该任务暂无历史运行归档。',
385
+ 'archive.loadMore': '加载更多 ({shown} / {total})',
386
+ 'archive.close': '关闭',
387
+ 'form.reliabilityTitle': '🛡️ 可靠性、心跳监听与自愈',
388
+ 'form.preflightLabel': '预检健康探测',
389
+ 'form.preflightNone': '无',
390
+ 'form.preflightHttp': 'HTTP (2xx 探测)',
391
+ 'form.preflightShell': 'Shell 探测 (退出码 0)',
392
+ 'form.preflightDisk': '磁盘空间探测 (挂载点:最小百分比)',
393
+ 'form.preflightTargetLabel': '预检目标参数',
394
+ 'form.preflightTargetPlaceholder': 'https://example.com/healthz 或 ls /data 或 /:10%',
395
+ 'form.heartbeatIntervalLabel': '心跳打卡时限 (秒, 0 = 禁用)',
396
+ 'form.gracePeriodLabel': '心跳宽限期 (秒)',
397
+ 'form.priorityLabel': '队列优先级 (1-10,数值越大越先执行)',
398
+ 'form.concurrencyGroupLabel': '并发分组名称',
399
+ 'form.selfHealingLabel': '故障自愈脚本 (Shell)',
400
+ 'form.selfHealingPlaceholder': 'systemctl restart app || true',
401
+ 'form.autoDiagnoseLabel': '任务失败时启用 AI 智能分析诊断',
402
+ 'form.agentPresetLabel': '智能体预设 (Agent Preset)',
403
+ 'form.agentPresetPlaceholder': 'standard (留空使用全局默认预设)',
404
+ 'form.agentPresetHint': '来自 dsh-agent-presets 的智能体预设(如 standard, minimal, cordis, ptc),挂载工作区工具与文件读写能力。',
405
+ 'form.previewSchedule': '预测下次运行时间',
406
+ 'form.previewScheduleTitle': '未来 5 次计划运行时间:',
407
+ 'search.placeholder': '搜索定时任务',
408
+ 'filters.title': '筛选',
409
+ 'filters.type': '类型',
410
+ 'filters.model': '模型',
411
+ 'filters.channel': '渠道',
412
+ 'filters.any': '全部',
413
+ 'filters.reset': '重置筛选',
414
+ 'filters.empty': '没有匹配当前筛选条件的任务。',
415
+ 'filters.showing': '显示 {shown} / 共 {total}',
416
+ 'stats.activeTasks': '活跃任务',
417
+ 'stats.totalRuns': '总运行次数',
418
+ 'stats.totalTokens': '消耗 Token',
419
+ 'stats.totalCost': '预估成本',
420
+ 'tabs.all': '全部',
421
+ 'tabs.active': '运行中',
422
+ 'tabs.paused': '已暂停',
423
+ 'tabs.completed': '已完成',
424
+ 'list.empty': '暂无定时任务',
425
+ 'list.emptyHint': '点击下方按钮让 DSH 规划任务,或手动配置。',
426
+ 'list.oneShot': '一次性',
427
+ 'list.runCount': '{count} 次运行',
428
+ 'list.consecutiveFailures': '连续 {count} 次失败',
429
+ 'list.lastRun': '上次: {time}',
430
+ 'list.nextRun': '下次: {time}',
431
+ 'list.badgeActive': '活跃',
432
+ 'list.badgePaused': '已暂停',
433
+ 'list.badgeCompleted': '已完成',
434
+ 'list.configManaged': '配置只读',
435
+ 'channel.telegram': 'Telegram',
436
+ 'channel.discord': 'Discord',
437
+ 'channel.slack': 'Slack',
438
+ 'channel.ntfy': 'ntfy',
439
+ 'channel.bark': 'Bark',
440
+ 'channel.pushplus': 'PushPlus',
441
+ 'channel.voice': '语音通话',
442
+ 'channel.gitea': 'Gitea',
443
+ 'channel.webhook': 'Webhook',
444
+ 'modal.editTitle': '编辑任务',
445
+ 'modal.newTitle': '新建定时任务',
446
+ 'modal.statusActive': '活跃',
447
+ 'modal.statusPaused': '已暂停',
448
+ 'modal.params': '配置参数',
449
+ 'modal.history': '运行历史 ({count})',
450
+ 'modal.cancel': '取消',
451
+ 'modal.save': '保存修改',
452
+ 'modal.create': '创建任务',
453
+ 'history.loading': '正在加载历史记录...',
454
+ 'history.empty': '暂无运行记录',
455
+ 'history.summaryTokens': '消耗 Token: {count}',
456
+ 'history.summaryCost': '预估成本: ${cost}',
457
+ 'history.errorLabel': '错误详情:',
458
+ 'history.outputLabel': '执行输出:',
459
+ 'form.titleLabel': '任务名称 *',
460
+ 'form.titlePlaceholder': '例如: 每周备份检查',
461
+ 'form.typeLabel': '执行类型 *',
462
+ 'form.promptLabel': '提示词内容 *',
463
+ 'form.promptPlaceholder': '输入发送给智能体的执行指令...',
464
+ 'form.modelLabel': 'AI 模型',
465
+ 'form.modelDefault': '默认模型',
466
+ 'form.fallbackModelLabel': '备用模型 (主模型失败时使用)',
467
+ 'form.fallbackModelNone': '不使用备用模型',
468
+ 'form.pipelineGroupLabel': '任务流水线串联',
469
+ 'form.onSuccessLabel': '成功后触发任务 (ID 或名称)',
470
+ 'form.onSuccessPlaceholder': '例如: task_upload_backup',
471
+ 'form.onFailureLabel': '失败后触发告警任务 (ID 或名称)',
472
+ 'form.onFailurePlaceholder': '例如: task_notify_incident',
473
+ 'form.scheduleLabel': '调度计划 (Cron 或相对时间) *',
474
+ 'form.schedulePlaceholder': '例如: 0 9 * * 1-5 或 15m 或 in 2 hours',
475
+ 'form.channelsLabel': '通知渠道',
476
+ 'form.channelsHint': '任务执行完毕后推送报告的目标渠道。',
477
+ 'form.templateLabel': '自定义消息模板',
478
+ 'form.templatePlaceholder': '留空使用默认摘要',
479
+ 'form.templateHint': '支持变量: {title}, {status}, {output}, {error}, {durationMs}, {time}',
480
+ 'form.silentRuleLabel': '静默规则 (可选)',
481
+ 'form.silentRulePlaceholder': '例如: ALL_OK 或 status == 200',
482
+ 'form.silentRuleHint': '若输出符合此规则,将不发送任何通知。',
483
+ 'form.advancedToggle': '高级执行选项',
484
+ 'form.timezoneLabel': '时区 (IANA 时区名)',
485
+ 'form.timezonePlaceholder': '例如: Asia/Shanghai 或 UTC (留空使用服务器时区)',
486
+ 'form.misfireLabel': '错过执行策略 (Misfire Policy)',
487
+ 'form.misfireSkip': '跳过 (默认)',
488
+ 'form.misfireRunOnce': '补跑一次',
489
+ 'form.maxRetriesLabel': '失败重试次数 (0 = 不重试)',
490
+ 'form.permissionPresetLabel': '权限预设',
491
+ 'form.permissionDefault': '默认 (继承主配置)',
492
+ 'form.permissionRestricted': '受限模式 (只读)',
493
+ 'form.permissionFull': '完全访问权限',
494
+ 'form.envLabel': '环境变量 (KEY=VALUE, 每行一个)',
495
+ 'form.envPlaceholder': 'FOO=bar\nAPI_KEY=secret',
496
+ 'form.cwdLabel': '工作目录 (绝对路径)',
497
+ 'form.cwdPlaceholder': '/var/www/my-project',
498
+ 'form.workspaceIdLabel': '关联 DSH Workspace ID',
499
+ 'form.workspaceIdPlaceholder': '留空使用默认工作区',
500
+ 'form.worktreeLabel': '在独立的临时 Git Worktree 中执行',
501
+ 'form.keepWorktreeLabel': '执行结束后保留 Git Worktree (不自动清理)',
502
+ 'form.timeoutLabel': '超时时间 (秒)',
503
+ 'form.overlapLabel': '并发策略',
504
+ 'form.overlapSkip': '跳过 (默认)',
505
+ 'form.overlapQueue': '排队等待',
506
+ 'form.overlapTerminate': '终止前次运行',
507
+ 'form.overlapAllow': '允许并发',
508
+ 'form.kanbanLabel': '看板同步模式',
509
+ 'form.kanbanNone': '不同步到看板',
510
+ 'form.kanbanCreate': '仅创建新卡片',
511
+ 'form.kanbanSync': '保持卡片同步更新',
512
+ 'settings.deliveryChannels': '推送渠道配置',
513
+ 'settings.credentialsHint': '安全凭据在 DSH Credentials 服务中统一管理。',
514
+ 'settings.saveChannels': '保存渠道配置',
515
+ 'settings.testing': '正在测试...',
516
+ 'settings.testSuccess': '✅ 测试成功!',
517
+ 'settings.automationHint': '用于静默规则研判和故障诊断的辅助模型。留空沿用任务自身模型。',
518
+ 'settings.silentRuleModelLabel': '静默规则研判模型',
519
+ 'settings.secretsOnCard': '设置中引用的凭据将在前端脱敏展示,并通过名称引用。',
520
+ 'form.nextRunLabel': '下次执行: ',
521
+ 'form.notScheduled': '未安排计划',
522
+ 'settings.loading': '正在加载设置...',
523
+ 'settings.unavailable': '设置当前不可用。',
524
+ 'settings.retry': '重试',
525
+ 'settings.cardTitle': '⏰ 定时任务调度器 (Cron)',
526
+ 'settings.cardDesc': '定时计划任务(Cron/间隔)、后台智能体场景以及支持 Telegram、Discord、Slack、ntfy、Bark、PushPlus、语音和 Gitea 的通知推送。',
527
+ 'settings.openPanel': '打开任务管理看板',
528
+ 'settings.modalTitle': '通知与渠道设置',
529
+ 'settings.tokenLabel': 'Telegram Bot Token',
530
+ 'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
531
+ 'settings.tokenPlaceholderDefault': '使用 DSH 全局配置的机器人 (或在此输入自定义 Token)',
532
+ 'settings.defaultHint': '✓ 检测到 DSH 环境已配置默认 Bot',
533
+ 'settings.chatIdLabel': 'Telegram Chat ID',
534
+ 'settings.chatIdPlaceholder': '例如: 345678901',
535
+ 'settings.kanbanUrlLabel': 'Kanban 服务基础地址',
536
+ 'settings.globalNotify': '全局开启所有任务结果推送至 Telegram',
537
+ 'settings.globalOnlyFailure': '全局模式:仅在失败时推送 (成功静默)',
538
+ 'settings.testTelegram': '🔔 测试 Telegram 发送',
539
+ 'settings.testKanban': '📋 测试 Kanban',
540
+ 'settings.sending': '正在发送...',
541
+ 'settings.checkingKanban': '正在连接 Kanban...',
542
+ 'settings.testOk': '✅ 测试消息已成功发送至 Telegram!',
543
+ 'settings.testFail': '❌ 发送失败: {error}',
544
+ 'settings.kanbanOk': '✅ Kanban 卡片创建成功',
545
+ 'settings.kanbanFail': '❌ {error}',
546
+ 'settings.save': '保存设置',
547
+ 'settings.saved': '✓ 已保存',
548
+ 'settings.saveError': '保存失败: {error}',
549
+ 'settings.saveErrorUnknown': '未知错误',
550
+ 'dsh.modalTitle': '让 DSH 智能体创建任务',
551
+ 'dsh.descLabel': '请描述你的任务目标和执行频率:',
552
+ 'dsh.placeholder': '例如: 每两小时检查一次磁盘空间和失败的 systemd 守护进程,并在异常时报警...',
553
+ 'dsh.sendHint': '按 Ctrl+Enter 发送',
554
+ 'dsh.sendTitle': '提交给智能体',
555
+ 'dsh.startFailed': '启动智能体对话失败: {error}',
556
+ 'dsh.launchError': '执行错误: {error}',
557
+ 'confirm.delete': '确定删除该任务吗?',
558
+ 'list.loadErrorPrefix': '加载任务列表失败'
295
559
  }
296
560
  };
297
561
 
@@ -877,10 +1141,27 @@ window.__ModuleLoader__.load({
877
1141
  const [formKanbanMode, setFormKanbanMode] = React.useState('none');
878
1142
  const [formOnSuccess, setFormOnSuccess] = React.useState('');
879
1143
  const [formOnFailure, setFormOnFailure] = React.useState('');
1144
+ const [formHeartbeatIntervalSeconds, setFormHeartbeatIntervalSeconds] = React.useState(0);
1145
+ const [formGracePeriodSeconds, setFormGracePeriodSeconds] = React.useState(300);
1146
+ const [formPreflightType, setFormPreflightType] = React.useState('none');
1147
+ const [formPreflightTarget, setFormPreflightTarget] = React.useState('');
1148
+ const [formPriority, setFormPriority] = React.useState(5);
1149
+ const [formConcurrencyGroup, setFormConcurrencyGroup] = React.useState('default');
1150
+ const [formSelfHealingCommand, setFormSelfHealingCommand] = React.useState('');
1151
+ const [formAutoDiagnose, setFormAutoDiagnose] = React.useState(false);
1152
+ const [schedulePreviewRuns, setSchedulePreviewRuns] = React.useState(null);
1153
+ const [dryRunResult, setDryRunResult] = React.useState(null);
1154
+ const [archiveModalOpen, setArchiveModalOpen] = React.useState(false);
1155
+ const [archiveTask, setArchiveTask] = React.useState(null);
1156
+ const [archiveRuns, setArchiveRuns] = React.useState([]);
1157
+ const [archiveLoading, setArchiveLoading] = React.useState(false);
1158
+ const [archiveOffset, setArchiveOffset] = React.useState(0);
1159
+ const [archiveTotal, setArchiveTotal] = React.useState(0);
880
1160
  const [formTimezone, setFormTimezone] = React.useState('');
881
1161
  const [formMisfirePolicy, setFormMisfirePolicy] = React.useState('skip');
882
1162
  const [formMaxRetries, setFormMaxRetries] = React.useState(0);
883
1163
  const [formPermissionPreset, setFormPermissionPreset] = React.useState('default');
1164
+ const [formAgentPreset, setFormAgentPreset] = React.useState('');
884
1165
  const [formEnv, setFormEnv] = React.useState('');
885
1166
  const [formCwd, setFormCwd] = React.useState('');
886
1167
  const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
@@ -1118,6 +1399,53 @@ window.__ModuleLoader__.load({
1118
1399
  };
1119
1400
 
1120
1401
  /** #41: duplicate a task; the server copies config, resets run state (#41). */
1402
+ const handleDryRun = async (taskId) => {
1403
+ try {
1404
+ const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
1405
+ const data = await res.json();
1406
+ setDryRunResult(data);
1407
+ } catch (err) {
1408
+ alert('Dry run error: ' + err.message);
1409
+ }
1410
+ };
1411
+
1412
+ const openArchiveModal = async (task, offset = 0) => {
1413
+ setArchiveTask(task);
1414
+ setArchiveOffset(offset);
1415
+ setArchiveLoading(true);
1416
+ setArchiveModalOpen(true);
1417
+ try {
1418
+ const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
1419
+ const data = await res.json();
1420
+ if (data.ok) {
1421
+ setArchiveRuns(data.runs || []);
1422
+ setArchiveTotal(data.total || 0);
1423
+ }
1424
+ } catch (err) {
1425
+ console.error('[dsh-cron] archive load error:', err);
1426
+ } finally {
1427
+ setArchiveLoading(false);
1428
+ }
1429
+ };
1430
+
1431
+ const handlePreviewSchedule = async (scheduleExpr) => {
1432
+ try {
1433
+ const res = await fetch('/dsh-cron/schedule/preview', {
1434
+ method: 'POST',
1435
+ headers: { 'Content-Type': 'application/json' },
1436
+ body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
1437
+ });
1438
+ const data = await res.json();
1439
+ if (data.ok && data.runs) {
1440
+ setSchedulePreviewRuns(data.runs);
1441
+ } else {
1442
+ setSchedulePreviewRuns([]);
1443
+ }
1444
+ } catch {
1445
+ setSchedulePreviewRuns([]);
1446
+ }
1447
+ };
1448
+
1121
1449
  const handleDuplicate = async (task) => {
1122
1450
  try {
1123
1451
  const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
@@ -1384,6 +1712,7 @@ window.__ModuleLoader__.load({
1384
1712
  setFormMisfirePolicy('skip');
1385
1713
  setFormMaxRetries(0);
1386
1714
  setFormPermissionPreset('default');
1715
+ setFormAgentPreset('');
1387
1716
  setFormEnv('');
1388
1717
  setFormCwd('');
1389
1718
  setFormWorkspaceId('');
@@ -1469,10 +1798,20 @@ window.__ModuleLoader__.load({
1469
1798
  setFormKanbanMode(task.kanbanMode || 'none');
1470
1799
  setFormOnSuccess(task.onSuccess || '');
1471
1800
  setFormOnFailure(task.onFailure || '');
1801
+ setFormHeartbeatIntervalSeconds(task.heartbeatIntervalSeconds || 0);
1802
+ setFormGracePeriodSeconds(task.gracePeriodSeconds || 300);
1803
+ setFormPreflightType(task.preflightType || 'none');
1804
+ setFormPreflightTarget(task.preflightTarget || '');
1805
+ setFormPriority(task.priority !== undefined ? task.priority : 5);
1806
+ setFormConcurrencyGroup(task.concurrencyGroup || 'default');
1807
+ setFormSelfHealingCommand(task.selfHealingCommand || '');
1808
+ setFormAutoDiagnose(Boolean(task.autoDiagnose));
1809
+ setSchedulePreviewRuns(null);
1472
1810
  setFormTimezone(task.timezone || '');
1473
1811
  setFormMisfirePolicy(task.misfirePolicy || 'skip');
1474
1812
  setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
1475
1813
  setFormPermissionPreset(task.permissionPreset || 'default');
1814
+ setFormAgentPreset(task.agentPreset || '');
1476
1815
  setFormEnv(formatEnvText(task.env));
1477
1816
  setFormCwd(task.cwd || '');
1478
1817
  setFormWorkspaceId(task.workspaceId || '');
@@ -1530,9 +1869,18 @@ window.__ModuleLoader__.load({
1530
1869
  misfirePolicy: formMisfirePolicy,
1531
1870
  maxRetries: Number(formMaxRetries) || 0,
1532
1871
  permissionPreset: formPermissionPreset,
1872
+ agentPreset: formAgentPreset.trim() || undefined,
1533
1873
  kanbanMode: formKanbanMode,
1534
1874
  onSuccess: formOnSuccess.trim() || undefined,
1535
1875
  onFailure: formOnFailure.trim() || undefined,
1876
+ heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
1877
+ gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
1878
+ preflightType: formPreflightType || 'none',
1879
+ preflightTarget: formPreflightTarget.trim() || undefined,
1880
+ priority: Number(formPriority) || 5,
1881
+ concurrencyGroup: formConcurrencyGroup.trim() || 'default',
1882
+ selfHealingCommand: formSelfHealingCommand.trim() || undefined,
1883
+ autoDiagnose: Boolean(formAutoDiagnose),
1536
1884
  env: parseEnvText(formEnv),
1537
1885
  cwd: formCwd.trim(),
1538
1886
  workspaceId: formWorkspaceId.trim(),
@@ -1840,6 +2188,9 @@ window.__ModuleLoader__.load({
1840
2188
  }, T_KEY(t, 'list.configManaged')) : null,
1841
2189
  task.lastDurationMs > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag', title: 'Last execution duration' }, task.lastDurationMs + 'ms') : null,
1842
2190
  task.onSuccess ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'On success: triggers ' + task.onSuccess, style: { background: 'rgba(56, 189, 248, 0.15)', color: '#38bdf8' } }, '➜ ' + task.onSuccess) : null,
2191
+ task.onFailure ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'On failure: triggers ' + task.onFailure, style: { background: 'rgba(239, 68, 68, 0.15)', color: '#ef4444' } }, '↳ ' + task.onFailure) : null,
2192
+ task.heartbeatIntervalSeconds > 0 ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'Heartbeat: every ' + task.heartbeatIntervalSeconds + 's', style: { background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' } }, '💓 ' + task.heartbeatIntervalSeconds + 's') : null,
2193
+ task.preflightType && task.preflightType !== 'none' ? React.createElement('span', { className: 'dsh-cron-type-tag', title: 'Preflight: ' + task.preflightType, style: { background: 'rgba(234, 179, 8, 0.15)', color: '#eab308' } }, '🛡️ ' + task.preflightType) : null,
1843
2194
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
1844
2195
  task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null
1845
2196
  ),
@@ -1859,6 +2210,18 @@ window.__ModuleLoader__.load({
1859
2210
  className: 'dsh-cron-task-actions',
1860
2211
  onClick: (e) => e.stopPropagation()
1861
2212
  },
2213
+ React.createElement('button', {
2214
+ type: 'button',
2215
+ className: 'dsh-cron-icon-btn',
2216
+ title: T_KEY(t, 'actions.dryRunTitle'),
2217
+ onClick: () => handleDryRun(task.id)
2218
+ }, '🧪'),
2219
+ React.createElement('button', {
2220
+ type: 'button',
2221
+ className: 'dsh-cron-icon-btn',
2222
+ title: T_KEY(t, 'actions.archiveTitle'),
2223
+ onClick: () => openArchiveModal(task, 0)
2224
+ }, '📜'),
1862
2225
  React.createElement('button', {
1863
2226
  type: 'button',
1864
2227
  className: 'dsh-cron-icon-btn',
@@ -2074,7 +2437,18 @@ window.__ModuleLoader__.load({
2074
2437
  value: formSchedule,
2075
2438
  placeholder: T_KEY(t, 'form.schedulePlaceholder'),
2076
2439
  onChange: (e) => setFormSchedule(e.target.value)
2077
- })
2440
+ }),
2441
+ React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '6px' } },
2442
+ React.createElement('button', {
2443
+ type: 'button',
2444
+ className: 'dsh-cron-btn-secondary',
2445
+ style: { padding: '3px 8px', fontSize: '11.5px' },
2446
+ onClick: () => handlePreviewSchedule(formSchedule)
2447
+ }, T_KEY(t, 'form.previewSchedule')),
2448
+ schedulePreviewRuns && schedulePreviewRuns.length > 0 && React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #999)' } },
2449
+ T_KEY(t, 'form.previewScheduleTitle') + ' ' + schedulePreviewRuns.slice(0, 3).map(r => new Date(r).toLocaleTimeString()).join(', ')
2450
+ )
2451
+ )
2078
2452
  )
2079
2453
  ),
2080
2454
  React.createElement('div', { className: 'dsh-cron-form-row' },
@@ -2156,6 +2530,16 @@ window.__ModuleLoader__.load({
2156
2530
  React.createElement('option', { value: 'always' }, T_KEY(t, 'form.kanbanAlways'))
2157
2531
  )
2158
2532
  ),
2533
+ AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group' },
2534
+ React.createElement('label', null, T_KEY(t, 'form.agentPresetLabel')),
2535
+ React.createElement('input', {
2536
+ type: 'text',
2537
+ value: formAgentPreset,
2538
+ placeholder: T_KEY(t, 'form.agentPresetPlaceholder'),
2539
+ onChange: (e) => setFormAgentPreset(e.target.value)
2540
+ }),
2541
+ React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.agentPresetHint'))
2542
+ ),
2159
2543
  AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-row' },
2160
2544
  React.createElement('div', { className: 'dsh-cron-form-group' },
2161
2545
  React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
@@ -2432,6 +2816,98 @@ window.__ModuleLoader__.load({
2432
2816
  React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.silentRuleHint'))
2433
2817
  )
2434
2818
  ),
2819
+
2820
+ // Reliability, Heartbeat & Self-Healing section
2821
+ React.createElement('div', {
2822
+ style: {
2823
+ marginTop: '16px',
2824
+ paddingTop: '14px',
2825
+ borderTop: '1px solid var(--dsw-alias-border-l2, #2e2e2e)'
2826
+ }
2827
+ },
2828
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', marginBottom: '10px' } }, T_KEY(t, 'form.reliabilityTitle')),
2829
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
2830
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2831
+ React.createElement('label', null, T_KEY(t, 'form.preflightLabel')),
2832
+ React.createElement('select', {
2833
+ value: formPreflightType,
2834
+ onChange: (e) => setFormPreflightType(e.target.value)
2835
+ },
2836
+ React.createElement('option', { value: 'none' }, T_KEY(t, 'form.preflightNone')),
2837
+ React.createElement('option', { value: 'http' }, T_KEY(t, 'form.preflightHttp')),
2838
+ React.createElement('option', { value: 'shell' }, T_KEY(t, 'form.preflightShell')),
2839
+ React.createElement('option', { value: 'disk' }, T_KEY(t, 'form.preflightDisk'))
2840
+ )
2841
+ ),
2842
+ formPreflightType !== 'none' && React.createElement('div', { className: 'dsh-cron-form-group' },
2843
+ React.createElement('label', null, T_KEY(t, 'form.preflightTargetLabel')),
2844
+ React.createElement('input', {
2845
+ type: 'text',
2846
+ value: formPreflightTarget,
2847
+ placeholder: T_KEY(t, 'form.preflightTargetPlaceholder'),
2848
+ onChange: (e) => setFormPreflightTarget(e.target.value)
2849
+ })
2850
+ )
2851
+ ),
2852
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
2853
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2854
+ React.createElement('label', null, T_KEY(t, 'form.heartbeatIntervalLabel')),
2855
+ React.createElement('input', {
2856
+ type: 'number',
2857
+ value: formHeartbeatIntervalSeconds,
2858
+ min: 0,
2859
+ onChange: (e) => setFormHeartbeatIntervalSeconds(Number(e.target.value))
2860
+ })
2861
+ ),
2862
+ formHeartbeatIntervalSeconds > 0 && React.createElement('div', { className: 'dsh-cron-form-group' },
2863
+ React.createElement('label', null, T_KEY(t, 'form.gracePeriodLabel')),
2864
+ React.createElement('input', {
2865
+ type: 'number',
2866
+ value: formGracePeriodSeconds,
2867
+ min: 1,
2868
+ onChange: (e) => setFormGracePeriodSeconds(Number(e.target.value))
2869
+ })
2870
+ )
2871
+ ),
2872
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
2873
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2874
+ React.createElement('label', null, T_KEY(t, 'form.priorityLabel')),
2875
+ React.createElement('input', {
2876
+ type: 'number',
2877
+ value: formPriority,
2878
+ min: 1,
2879
+ max: 10,
2880
+ onChange: (e) => setFormPriority(Number(e.target.value))
2881
+ })
2882
+ ),
2883
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2884
+ React.createElement('label', null, T_KEY(t, 'form.concurrencyGroupLabel')),
2885
+ React.createElement('input', {
2886
+ type: 'text',
2887
+ value: formConcurrencyGroup,
2888
+ onChange: (e) => setFormConcurrencyGroup(e.target.value)
2889
+ })
2890
+ )
2891
+ ),
2892
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2893
+ React.createElement('label', null, T_KEY(t, 'form.selfHealingLabel')),
2894
+ React.createElement('input', {
2895
+ type: 'text',
2896
+ value: formSelfHealingCommand,
2897
+ placeholder: T_KEY(t, 'form.selfHealingPlaceholder'),
2898
+ onChange: (e) => setFormSelfHealingCommand(e.target.value)
2899
+ })
2900
+ ),
2901
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '6px' } },
2902
+ React.createElement('input', {
2903
+ type: 'checkbox',
2904
+ id: 'formAutoDiagnoseCheckbox',
2905
+ checked: formAutoDiagnose,
2906
+ onChange: (e) => setFormAutoDiagnose(e.target.checked)
2907
+ }),
2908
+ React.createElement('label', { htmlFor: 'formAutoDiagnoseCheckbox', style: { margin: 0, fontSize: '12.5px', cursor: 'pointer' } }, T_KEY(t, 'form.autoDiagnoseLabel'))
2909
+ )
2910
+ ),
2435
2911
  selectedTaskMeta && React.createElement('div', {
2436
2912
  style: {
2437
2913
  background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
@@ -2475,7 +2951,123 @@ window.__ModuleLoader__.load({
2475
2951
  )
2476
2952
  ),
2477
2953
 
2478
- // 2. Settings modal
2954
+
2955
+ // Dry Run simulation modal
2956
+ dryRunResult && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setDryRunResult(null) },
2957
+ React.createElement('div', {
2958
+ className: 'dsh-cron-modal',
2959
+ style: { maxWidth: '580px' },
2960
+ role: 'dialog',
2961
+ 'aria-modal': 'true',
2962
+ onClick: (e) => e.stopPropagation()
2963
+ },
2964
+ React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
2965
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'dryRun.title')),
2966
+ React.createElement('button', {
2967
+ type: 'button',
2968
+ className: 'dsh-cron-icon-btn',
2969
+ onClick: () => setDryRunResult(null)
2970
+ }, '✕')
2971
+ ),
2972
+ React.createElement('div', { style: { marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '13px' } },
2973
+ React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.status') + ': '), React.createElement('span', { style: { color: dryRunResult.ok ? 'var(--dsh-cron-success, #22c55e)' : 'var(--dsh-cron-danger, #ef4444)' } }, dryRunResult.ok ? (dryRunResult.result && dryRunResult.result.status || 'success') : 'failed')),
2974
+ dryRunResult.result && dryRunResult.result.preflight && React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.preflight') + ': '), React.createElement('span', null, dryRunResult.result.preflight.passed ? '✓ Passed' : '✗ Failed (' + (dryRunResult.result.preflight.reason || '') + ')')),
2975
+ dryRunResult.result && React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.duration') + ': '), React.createElement('span', null, (dryRunResult.result.durationMs || 0) + 'ms')),
2976
+ React.createElement('div', { style: { marginTop: '6px' } }, React.createElement('strong', null, T_KEY(t, 'dryRun.output') + ':')),
2977
+ React.createElement('pre', {
2978
+ style: {
2979
+ background: 'var(--dsw-alias-bg-layer-2, #18181b)',
2980
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
2981
+ borderRadius: '6px',
2982
+ padding: '10px',
2983
+ maxHeight: '220px',
2984
+ overflowY: 'auto',
2985
+ fontSize: '12px',
2986
+ whiteSpace: 'pre-wrap',
2987
+ wordBreak: 'break-word',
2988
+ color: 'var(--dsw-alias-label-primary, #e4e4e7)'
2989
+ }
2990
+ }, (dryRunResult.result && dryRunResult.result.output) || (dryRunResult.error) || '(no output)')
2991
+ ),
2992
+ React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '16px' } },
2993
+ React.createElement('button', {
2994
+ type: 'button',
2995
+ className: 'dsh-cron-btn-primary',
2996
+ onClick: () => setDryRunResult(null)
2997
+ }, T_KEY(t, 'dryRun.close'))
2998
+ )
2999
+ )
3000
+ ),
3001
+
3002
+ // Archive runs modal
3003
+ archiveModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setArchiveModalOpen(false) },
3004
+ React.createElement('div', {
3005
+ className: 'dsh-cron-modal',
3006
+ style: { maxWidth: '700px' },
3007
+ role: 'dialog',
3008
+ 'aria-modal': 'true',
3009
+ onClick: (e) => e.stopPropagation()
3010
+ },
3011
+ React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3012
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'archive.title') + (archiveTask ? ' - ' + archiveTask.title : '')),
3013
+ React.createElement('button', {
3014
+ type: 'button',
3015
+ className: 'dsh-cron-icon-btn',
3016
+ onClick: () => setArchiveModalOpen(false)
3017
+ }, '✕')
3018
+ ),
3019
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #999)', margin: '4px 0 12px 0' } },
3020
+ T_KEY(t, 'archive.totalRuns', { total: archiveTotal })
3021
+ ),
3022
+ archiveLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.loading')) :
3023
+ archiveRuns.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'archive.empty')) :
3024
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '380px', overflowY: 'auto' } },
3025
+ archiveRuns.map((r, idx) => React.createElement('div', {
3026
+ key: idx,
3027
+ style: {
3028
+ background: 'var(--dsw-alias-bg-layer-2, #18181b)',
3029
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
3030
+ borderRadius: '6px',
3031
+ padding: '8px 12px',
3032
+ fontSize: '12px'
3033
+ }
3034
+ },
3035
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '4px' } },
3036
+ React.createElement('span', { style: { fontWeight: '600', color: r.status === 'success' ? 'var(--dsh-cron-success, #22c55e)' : 'var(--dsh-cron-danger, #ef4444)' } }, (r.status || 'unknown').toUpperCase()),
3037
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #888)' } }, new Date(r.timestamp || r.startedAt || Date.now()).toLocaleString())
3038
+ ),
3039
+ r.durationMs > 0 && React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary, #888)', marginBottom: '4px' } }, 'Duration: ' + r.durationMs + 'ms'),
3040
+ r.error && React.createElement('div', { style: { color: 'var(--dsh-cron-danger, #ef4444)', marginBottom: '4px' } }, 'Error: ' + r.error),
3041
+ r.output && React.createElement('div', {
3042
+ style: {
3043
+ color: 'var(--dsw-alias-label-primary, #ccc)',
3044
+ whiteSpace: 'pre-wrap',
3045
+ wordBreak: 'break-word',
3046
+ maxHeight: '80px',
3047
+ overflowY: 'auto'
3048
+ }
3049
+ }, r.output.slice(0, 300) + (r.output.length > 300 ? '...' : ''))
3050
+ ))
3051
+ ),
3052
+ React.createElement('div', { className: 'dsh-cron-modal-foot', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '16px' } },
3053
+ archiveRuns.length < archiveTotal ? React.createElement('button', {
3054
+ type: 'button',
3055
+ className: 'dsh-cron-btn-secondary',
3056
+ onClick: () => {
3057
+ const nextOffset = archiveOffset + 15;
3058
+ if (archiveTask) openArchiveModal(archiveTask, nextOffset);
3059
+ }
3060
+ }, T_KEY(t, 'archive.loadMore', { shown: archiveRuns.length, total: archiveTotal })) : React.createElement('div', null),
3061
+ React.createElement('button', {
3062
+ type: 'button',
3063
+ className: 'dsh-cron-btn-primary',
3064
+ onClick: () => setArchiveModalOpen(false)
3065
+ }, T_KEY(t, 'archive.close'))
3066
+ )
3067
+ )
3068
+ ),
3069
+
3070
+ // 2. Settings modal
2479
3071
  settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
2480
3072
  React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
2481
3073
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },