@goodandready/dsh-cron 0.2.10 → 0.2.11
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 +20 -2
- package/README.ru.md +481 -0
- package/README.zh.md +481 -0
- package/docs/README.ru.md +34 -1
- package/docs/README.zh.md +34 -1
- package/lib/api.js +93 -2
- package/lib/client.js +575 -3
- package/lib/index.js +18 -0
- package/lib/scheduler.js +239 -4
- package/lib/store.js +33 -0
- package/lib/task-transfer.js +8 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -291,7 +291,265 @@ 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.previewSchedule': 'Preview next runs',
|
|
327
|
+
'form.previewScheduleTitle': 'Next 5 scheduled runs:',
|
|
328
|
+
},
|
|
329
|
+
zh: {
|
|
330
|
+
'sidebar.label': '定时任务',
|
|
331
|
+
'sidebar.jobsTitle': '运行中任务',
|
|
332
|
+
'sidebar.noActiveJobs': '无运行中任务',
|
|
333
|
+
'sidebar.moreJobs': '及另外 {count} 个',
|
|
334
|
+
'chip.title': '定时任务 (Cron)',
|
|
335
|
+
'panel.back': '返回对话',
|
|
336
|
+
'panel.title': '定时任务',
|
|
337
|
+
'panel.subtitle': '让 DSH 规划任务、设置提醒或跟踪状态',
|
|
338
|
+
'actions.create': '新建',
|
|
339
|
+
'actions.createWithDsh': '💬 让 DSH 创建',
|
|
340
|
+
'actions.setupManually': '✏️ 手动配置',
|
|
341
|
+
'actions.runNow': '立即运行',
|
|
342
|
+
'actions.runNowShort': '运行',
|
|
343
|
+
'actions.edit': '编辑 / 详情',
|
|
344
|
+
'actions.duplicate': '复制任务',
|
|
345
|
+
'actions.tuneWithDsh': '让 DSH 优化',
|
|
346
|
+
'tune.startFailed': '无法启动优化会话: {error}',
|
|
347
|
+
'actions.delete': '删除任务',
|
|
348
|
+
'duplicate.done': '已创建副本并处于暂停状态:恢复前请先核对调度时间。',
|
|
349
|
+
'duplicate.oneShotHint': '该副本为一次性任务且已暂停,请设定新的执行时间后再启用。',
|
|
350
|
+
'duplicate.failed': '复制任务失败: {error}',
|
|
351
|
+
'transfer.exportTitle': '导出任务',
|
|
352
|
+
'transfer.importTitle': '导入任务',
|
|
353
|
+
'transfer.exported': '已成功导出 {count} 个任务。',
|
|
354
|
+
'transfer.exportFailed': '导出失败: {error}',
|
|
355
|
+
'transfer.importTitleModal': '导入任务',
|
|
356
|
+
'transfer.importHint': '仅导出任务静态配置:不包含历史记录与计数器。不包含密钥——渠道通过凭据名称引用。',
|
|
357
|
+
'transfer.strategyAdd': '作为新任务导入(分配新 ID)',
|
|
358
|
+
'transfer.strategyReplace': '覆盖同 ID 任务',
|
|
359
|
+
'transfer.strategySkip': '跳过已存在的同 ID 任务',
|
|
360
|
+
'transfer.summary': '该文件包含 {total} 个任务:{add} 个新增,{replace} 个覆盖,{skip} 个跳过。',
|
|
361
|
+
'transfer.confirm': '开始导入',
|
|
362
|
+
'transfer.imported': '已成功导出 {count} 个任务。',
|
|
363
|
+
'transfer.importFailed': '导入失败: {error}',
|
|
364
|
+
'transfer.badFile': '所选文件不是有效的 JSON 导出文件: {error}',
|
|
365
|
+
'actions.pause': '暂停',
|
|
366
|
+
'actions.resume': '恢复',
|
|
367
|
+
'actions.settings': '通知与渠道设置',
|
|
368
|
+
'actions.dryRun': '空跑测试',
|
|
369
|
+
'actions.dryRunTitle': '空跑测试(模拟执行,不产生副作用且不发送通知)',
|
|
370
|
+
'actions.archive': '历史归档',
|
|
371
|
+
'actions.archiveTitle': '查看执行历史与日志归档',
|
|
372
|
+
'dryRun.title': '🧪 空跑模拟结果',
|
|
373
|
+
'dryRun.task': '任务',
|
|
374
|
+
'dryRun.status': '模拟状态',
|
|
375
|
+
'dryRun.duration': '耗时',
|
|
376
|
+
'dryRun.output': '模拟输出',
|
|
377
|
+
'dryRun.preflight': '预检结果',
|
|
378
|
+
'dryRun.close': '关闭',
|
|
379
|
+
'archive.title': '📜 运行历史归档',
|
|
380
|
+
'archive.totalRuns': '共记录 {total} 次运行',
|
|
381
|
+
'archive.empty': '该任务暂无历史运行归档。',
|
|
382
|
+
'archive.loadMore': '加载更多 ({shown} / {total})',
|
|
383
|
+
'archive.close': '关闭',
|
|
384
|
+
'form.reliabilityTitle': '🛡️ 可靠性、心跳监听与自愈',
|
|
385
|
+
'form.preflightLabel': '预检健康探测',
|
|
386
|
+
'form.preflightNone': '无',
|
|
387
|
+
'form.preflightHttp': 'HTTP (2xx 探测)',
|
|
388
|
+
'form.preflightShell': 'Shell 探测 (退出码 0)',
|
|
389
|
+
'form.preflightDisk': '磁盘空间探测 (挂载点:最小百分比)',
|
|
390
|
+
'form.preflightTargetLabel': '预检目标参数',
|
|
391
|
+
'form.preflightTargetPlaceholder': 'https://example.com/healthz 或 ls /data 或 /:10%',
|
|
392
|
+
'form.heartbeatIntervalLabel': '心跳打卡时限 (秒, 0 = 禁用)',
|
|
393
|
+
'form.gracePeriodLabel': '心跳宽限期 (秒)',
|
|
394
|
+
'form.priorityLabel': '队列优先级 (1-10,数值越大越先执行)',
|
|
395
|
+
'form.concurrencyGroupLabel': '并发分组名称',
|
|
396
|
+
'form.selfHealingLabel': '故障自愈脚本 (Shell)',
|
|
397
|
+
'form.selfHealingPlaceholder': 'systemctl restart app || true',
|
|
398
|
+
'form.autoDiagnoseLabel': '任务失败时启用 AI 智能分析诊断',
|
|
399
|
+
'form.previewSchedule': '预测下次运行时间',
|
|
400
|
+
'form.previewScheduleTitle': '未来 5 次计划运行时间:',
|
|
401
|
+
'search.placeholder': '搜索定时任务',
|
|
402
|
+
'filters.title': '筛选',
|
|
403
|
+
'filters.type': '类型',
|
|
404
|
+
'filters.model': '模型',
|
|
405
|
+
'filters.channel': '渠道',
|
|
406
|
+
'filters.any': '全部',
|
|
407
|
+
'filters.reset': '重置筛选',
|
|
408
|
+
'filters.empty': '没有匹配当前筛选条件的任务。',
|
|
409
|
+
'filters.showing': '显示 {shown} / 共 {total}',
|
|
410
|
+
'stats.activeTasks': '活跃任务',
|
|
411
|
+
'stats.totalRuns': '总运行次数',
|
|
412
|
+
'stats.totalTokens': '消耗 Token',
|
|
413
|
+
'stats.totalCost': '预估成本',
|
|
414
|
+
'tabs.all': '全部',
|
|
415
|
+
'tabs.active': '运行中',
|
|
416
|
+
'tabs.paused': '已暂停',
|
|
417
|
+
'tabs.completed': '已完成',
|
|
418
|
+
'list.empty': '暂无定时任务',
|
|
419
|
+
'list.emptyHint': '点击下方按钮让 DSH 规划任务,或手动配置。',
|
|
420
|
+
'list.oneShot': '一次性',
|
|
421
|
+
'list.runCount': '{count} 次运行',
|
|
422
|
+
'list.consecutiveFailures': '连续 {count} 次失败',
|
|
423
|
+
'list.lastRun': '上次: {time}',
|
|
424
|
+
'list.nextRun': '下次: {time}',
|
|
425
|
+
'list.badgeActive': '活跃',
|
|
426
|
+
'list.badgePaused': '已暂停',
|
|
427
|
+
'list.badgeCompleted': '已完成',
|
|
428
|
+
'list.configManaged': '配置只读',
|
|
429
|
+
'channel.telegram': 'Telegram',
|
|
430
|
+
'channel.discord': 'Discord',
|
|
431
|
+
'channel.slack': 'Slack',
|
|
432
|
+
'channel.ntfy': 'ntfy',
|
|
433
|
+
'channel.bark': 'Bark',
|
|
434
|
+
'channel.pushplus': 'PushPlus',
|
|
435
|
+
'channel.voice': '语音通话',
|
|
436
|
+
'channel.gitea': 'Gitea',
|
|
437
|
+
'channel.webhook': 'Webhook',
|
|
438
|
+
'modal.editTitle': '编辑任务',
|
|
439
|
+
'modal.newTitle': '新建定时任务',
|
|
440
|
+
'modal.statusActive': '活跃',
|
|
441
|
+
'modal.statusPaused': '已暂停',
|
|
442
|
+
'modal.params': '配置参数',
|
|
443
|
+
'modal.history': '运行历史 ({count})',
|
|
444
|
+
'modal.cancel': '取消',
|
|
445
|
+
'modal.save': '保存修改',
|
|
446
|
+
'modal.create': '创建任务',
|
|
447
|
+
'history.loading': '正在加载历史记录...',
|
|
448
|
+
'history.empty': '暂无运行记录',
|
|
449
|
+
'history.summaryTokens': '消耗 Token: {count}',
|
|
450
|
+
'history.summaryCost': '预估成本: ${cost}',
|
|
451
|
+
'history.errorLabel': '错误详情:',
|
|
452
|
+
'history.outputLabel': '执行输出:',
|
|
453
|
+
'form.titleLabel': '任务名称 *',
|
|
454
|
+
'form.titlePlaceholder': '例如: 每周备份检查',
|
|
455
|
+
'form.typeLabel': '执行类型 *',
|
|
456
|
+
'form.promptLabel': '提示词内容 *',
|
|
457
|
+
'form.promptPlaceholder': '输入发送给智能体的执行指令...',
|
|
458
|
+
'form.modelLabel': 'AI 模型',
|
|
459
|
+
'form.modelDefault': '默认模型',
|
|
460
|
+
'form.fallbackModelLabel': '备用模型 (主模型失败时使用)',
|
|
461
|
+
'form.fallbackModelNone': '不使用备用模型',
|
|
462
|
+
'form.pipelineGroupLabel': '任务流水线串联',
|
|
463
|
+
'form.onSuccessLabel': '成功后触发任务 (ID 或名称)',
|
|
464
|
+
'form.onSuccessPlaceholder': '例如: task_upload_backup',
|
|
465
|
+
'form.onFailureLabel': '失败后触发告警任务 (ID 或名称)',
|
|
466
|
+
'form.onFailurePlaceholder': '例如: task_notify_incident',
|
|
467
|
+
'form.scheduleLabel': '调度计划 (Cron 或相对时间) *',
|
|
468
|
+
'form.schedulePlaceholder': '例如: 0 9 * * 1-5 或 15m 或 in 2 hours',
|
|
469
|
+
'form.channelsLabel': '通知渠道',
|
|
470
|
+
'form.channelsHint': '任务执行完毕后推送报告的目标渠道。',
|
|
471
|
+
'form.templateLabel': '自定义消息模板',
|
|
472
|
+
'form.templatePlaceholder': '留空使用默认摘要',
|
|
473
|
+
'form.templateHint': '支持变量: {title}, {status}, {output}, {error}, {durationMs}, {time}',
|
|
474
|
+
'form.silentRuleLabel': '静默规则 (可选)',
|
|
475
|
+
'form.silentRulePlaceholder': '例如: ALL_OK 或 status == 200',
|
|
476
|
+
'form.silentRuleHint': '若输出符合此规则,将不发送任何通知。',
|
|
477
|
+
'form.advancedToggle': '高级执行选项',
|
|
478
|
+
'form.timezoneLabel': '时区 (IANA 时区名)',
|
|
479
|
+
'form.timezonePlaceholder': '例如: Asia/Shanghai 或 UTC (留空使用服务器时区)',
|
|
480
|
+
'form.misfireLabel': '错过执行策略 (Misfire Policy)',
|
|
481
|
+
'form.misfireSkip': '跳过 (默认)',
|
|
482
|
+
'form.misfireRunOnce': '补跑一次',
|
|
483
|
+
'form.maxRetriesLabel': '失败重试次数 (0 = 不重试)',
|
|
484
|
+
'form.permissionPresetLabel': '权限预设',
|
|
485
|
+
'form.permissionDefault': '默认 (继承主配置)',
|
|
486
|
+
'form.permissionRestricted': '受限模式 (只读)',
|
|
487
|
+
'form.permissionFull': '完全访问权限',
|
|
488
|
+
'form.envLabel': '环境变量 (KEY=VALUE, 每行一个)',
|
|
489
|
+
'form.envPlaceholder': 'FOO=bar\nAPI_KEY=secret',
|
|
490
|
+
'form.cwdLabel': '工作目录 (绝对路径)',
|
|
491
|
+
'form.cwdPlaceholder': '/var/www/my-project',
|
|
492
|
+
'form.workspaceIdLabel': '关联 DSH Workspace ID',
|
|
493
|
+
'form.workspaceIdPlaceholder': '留空使用默认工作区',
|
|
494
|
+
'form.worktreeLabel': '在独立的临时 Git Worktree 中执行',
|
|
495
|
+
'form.keepWorktreeLabel': '执行结束后保留 Git Worktree (不自动清理)',
|
|
496
|
+
'form.timeoutLabel': '超时时间 (秒)',
|
|
497
|
+
'form.overlapLabel': '并发策略',
|
|
498
|
+
'form.overlapSkip': '跳过 (默认)',
|
|
499
|
+
'form.overlapQueue': '排队等待',
|
|
500
|
+
'form.overlapTerminate': '终止前次运行',
|
|
501
|
+
'form.overlapAllow': '允许并发',
|
|
502
|
+
'form.kanbanLabel': '看板同步模式',
|
|
503
|
+
'form.kanbanNone': '不同步到看板',
|
|
504
|
+
'form.kanbanCreate': '仅创建新卡片',
|
|
505
|
+
'form.kanbanSync': '保持卡片同步更新',
|
|
506
|
+
'settings.deliveryChannels': '推送渠道配置',
|
|
507
|
+
'settings.credentialsHint': '安全凭据在 DSH Credentials 服务中统一管理。',
|
|
508
|
+
'settings.saveChannels': '保存渠道配置',
|
|
509
|
+
'settings.testing': '正在测试...',
|
|
510
|
+
'settings.testSuccess': '✅ 测试成功!',
|
|
511
|
+
'settings.automationHint': '用于静默规则研判和故障诊断的辅助模型。留空沿用任务自身模型。',
|
|
512
|
+
'settings.silentRuleModelLabel': '静默规则研判模型',
|
|
513
|
+
'settings.secretsOnCard': '设置中引用的凭据将在前端脱敏展示,并通过名称引用。',
|
|
514
|
+
'form.nextRunLabel': '下次执行: ',
|
|
515
|
+
'form.notScheduled': '未安排计划',
|
|
516
|
+
'settings.loading': '正在加载设置...',
|
|
517
|
+
'settings.unavailable': '设置当前不可用。',
|
|
518
|
+
'settings.retry': '重试',
|
|
519
|
+
'settings.cardTitle': '⏰ 定时任务调度器 (Cron)',
|
|
520
|
+
'settings.cardDesc': '定时计划任务(Cron/间隔)、后台智能体场景以及支持 Telegram、Discord、Slack、ntfy、Bark、PushPlus、语音和 Gitea 的通知推送。',
|
|
521
|
+
'settings.openPanel': '打开任务管理看板',
|
|
522
|
+
'settings.modalTitle': '通知与渠道设置',
|
|
523
|
+
'settings.tokenLabel': 'Telegram Bot Token',
|
|
524
|
+
'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
525
|
+
'settings.tokenPlaceholderDefault': '使用 DSH 全局配置的机器人 (或在此输入自定义 Token)',
|
|
526
|
+
'settings.defaultHint': '✓ 检测到 DSH 环境已配置默认 Bot',
|
|
527
|
+
'settings.chatIdLabel': 'Telegram Chat ID',
|
|
528
|
+
'settings.chatIdPlaceholder': '例如: 345678901',
|
|
529
|
+
'settings.kanbanUrlLabel': 'Kanban 服务基础地址',
|
|
530
|
+
'settings.globalNotify': '全局开启所有任务结果推送至 Telegram',
|
|
531
|
+
'settings.globalOnlyFailure': '全局模式:仅在失败时推送 (成功静默)',
|
|
532
|
+
'settings.testTelegram': '🔔 测试 Telegram 发送',
|
|
533
|
+
'settings.testKanban': '📋 测试 Kanban',
|
|
534
|
+
'settings.sending': '正在发送...',
|
|
535
|
+
'settings.checkingKanban': '正在连接 Kanban...',
|
|
536
|
+
'settings.testOk': '✅ 测试消息已成功发送至 Telegram!',
|
|
537
|
+
'settings.testFail': '❌ 发送失败: {error}',
|
|
538
|
+
'settings.kanbanOk': '✅ Kanban 卡片创建成功',
|
|
539
|
+
'settings.kanbanFail': '❌ {error}',
|
|
540
|
+
'settings.save': '保存设置',
|
|
541
|
+
'settings.saved': '✓ 已保存',
|
|
542
|
+
'settings.saveError': '保存失败: {error}',
|
|
543
|
+
'settings.saveErrorUnknown': '未知错误',
|
|
544
|
+
'dsh.modalTitle': '让 DSH 智能体创建任务',
|
|
545
|
+
'dsh.descLabel': '请描述你的任务目标和执行频率:',
|
|
546
|
+
'dsh.placeholder': '例如: 每两小时检查一次磁盘空间和失败的 systemd 守护进程,并在异常时报警...',
|
|
547
|
+
'dsh.sendHint': '按 Ctrl+Enter 发送',
|
|
548
|
+
'dsh.sendTitle': '提交给智能体',
|
|
549
|
+
'dsh.startFailed': '启动智能体对话失败: {error}',
|
|
550
|
+
'dsh.launchError': '执行错误: {error}',
|
|
551
|
+
'confirm.delete': '确定删除该任务吗?',
|
|
552
|
+
'list.loadErrorPrefix': '加载任务列表失败'
|
|
295
553
|
}
|
|
296
554
|
};
|
|
297
555
|
|
|
@@ -877,6 +1135,22 @@ window.__ModuleLoader__.load({
|
|
|
877
1135
|
const [formKanbanMode, setFormKanbanMode] = React.useState('none');
|
|
878
1136
|
const [formOnSuccess, setFormOnSuccess] = React.useState('');
|
|
879
1137
|
const [formOnFailure, setFormOnFailure] = React.useState('');
|
|
1138
|
+
const [formHeartbeatIntervalSeconds, setFormHeartbeatIntervalSeconds] = React.useState(0);
|
|
1139
|
+
const [formGracePeriodSeconds, setFormGracePeriodSeconds] = React.useState(300);
|
|
1140
|
+
const [formPreflightType, setFormPreflightType] = React.useState('none');
|
|
1141
|
+
const [formPreflightTarget, setFormPreflightTarget] = React.useState('');
|
|
1142
|
+
const [formPriority, setFormPriority] = React.useState(5);
|
|
1143
|
+
const [formConcurrencyGroup, setFormConcurrencyGroup] = React.useState('default');
|
|
1144
|
+
const [formSelfHealingCommand, setFormSelfHealingCommand] = React.useState('');
|
|
1145
|
+
const [formAutoDiagnose, setFormAutoDiagnose] = React.useState(false);
|
|
1146
|
+
const [schedulePreviewRuns, setSchedulePreviewRuns] = React.useState(null);
|
|
1147
|
+
const [dryRunResult, setDryRunResult] = React.useState(null);
|
|
1148
|
+
const [archiveModalOpen, setArchiveModalOpen] = React.useState(false);
|
|
1149
|
+
const [archiveTask, setArchiveTask] = React.useState(null);
|
|
1150
|
+
const [archiveRuns, setArchiveRuns] = React.useState([]);
|
|
1151
|
+
const [archiveLoading, setArchiveLoading] = React.useState(false);
|
|
1152
|
+
const [archiveOffset, setArchiveOffset] = React.useState(0);
|
|
1153
|
+
const [archiveTotal, setArchiveTotal] = React.useState(0);
|
|
880
1154
|
const [formTimezone, setFormTimezone] = React.useState('');
|
|
881
1155
|
const [formMisfirePolicy, setFormMisfirePolicy] = React.useState('skip');
|
|
882
1156
|
const [formMaxRetries, setFormMaxRetries] = React.useState(0);
|
|
@@ -1118,6 +1392,53 @@ window.__ModuleLoader__.load({
|
|
|
1118
1392
|
};
|
|
1119
1393
|
|
|
1120
1394
|
/** #41: duplicate a task; the server copies config, resets run state (#41). */
|
|
1395
|
+
const handleDryRun = async (taskId) => {
|
|
1396
|
+
try {
|
|
1397
|
+
const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
|
|
1398
|
+
const data = await res.json();
|
|
1399
|
+
setDryRunResult(data);
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
alert('Dry run error: ' + err.message);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
const openArchiveModal = async (task, offset = 0) => {
|
|
1406
|
+
setArchiveTask(task);
|
|
1407
|
+
setArchiveOffset(offset);
|
|
1408
|
+
setArchiveLoading(true);
|
|
1409
|
+
setArchiveModalOpen(true);
|
|
1410
|
+
try {
|
|
1411
|
+
const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
|
|
1412
|
+
const data = await res.json();
|
|
1413
|
+
if (data.ok) {
|
|
1414
|
+
setArchiveRuns(data.runs || []);
|
|
1415
|
+
setArchiveTotal(data.total || 0);
|
|
1416
|
+
}
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error('[dsh-cron] archive load error:', err);
|
|
1419
|
+
} finally {
|
|
1420
|
+
setArchiveLoading(false);
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
const handlePreviewSchedule = async (scheduleExpr) => {
|
|
1425
|
+
try {
|
|
1426
|
+
const res = await fetch('/dsh-cron/schedule/preview', {
|
|
1427
|
+
method: 'POST',
|
|
1428
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1429
|
+
body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
|
|
1430
|
+
});
|
|
1431
|
+
const data = await res.json();
|
|
1432
|
+
if (data.ok && data.runs) {
|
|
1433
|
+
setSchedulePreviewRuns(data.runs);
|
|
1434
|
+
} else {
|
|
1435
|
+
setSchedulePreviewRuns([]);
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
setSchedulePreviewRuns([]);
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1121
1442
|
const handleDuplicate = async (task) => {
|
|
1122
1443
|
try {
|
|
1123
1444
|
const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
|
|
@@ -1469,6 +1790,15 @@ window.__ModuleLoader__.load({
|
|
|
1469
1790
|
setFormKanbanMode(task.kanbanMode || 'none');
|
|
1470
1791
|
setFormOnSuccess(task.onSuccess || '');
|
|
1471
1792
|
setFormOnFailure(task.onFailure || '');
|
|
1793
|
+
setFormHeartbeatIntervalSeconds(task.heartbeatIntervalSeconds || 0);
|
|
1794
|
+
setFormGracePeriodSeconds(task.gracePeriodSeconds || 300);
|
|
1795
|
+
setFormPreflightType(task.preflightType || 'none');
|
|
1796
|
+
setFormPreflightTarget(task.preflightTarget || '');
|
|
1797
|
+
setFormPriority(task.priority !== undefined ? task.priority : 5);
|
|
1798
|
+
setFormConcurrencyGroup(task.concurrencyGroup || 'default');
|
|
1799
|
+
setFormSelfHealingCommand(task.selfHealingCommand || '');
|
|
1800
|
+
setFormAutoDiagnose(Boolean(task.autoDiagnose));
|
|
1801
|
+
setSchedulePreviewRuns(null);
|
|
1472
1802
|
setFormTimezone(task.timezone || '');
|
|
1473
1803
|
setFormMisfirePolicy(task.misfirePolicy || 'skip');
|
|
1474
1804
|
setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
|
|
@@ -1533,6 +1863,14 @@ window.__ModuleLoader__.load({
|
|
|
1533
1863
|
kanbanMode: formKanbanMode,
|
|
1534
1864
|
onSuccess: formOnSuccess.trim() || undefined,
|
|
1535
1865
|
onFailure: formOnFailure.trim() || undefined,
|
|
1866
|
+
heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
|
|
1867
|
+
gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
|
|
1868
|
+
preflightType: formPreflightType || 'none',
|
|
1869
|
+
preflightTarget: formPreflightTarget.trim() || undefined,
|
|
1870
|
+
priority: Number(formPriority) || 5,
|
|
1871
|
+
concurrencyGroup: formConcurrencyGroup.trim() || 'default',
|
|
1872
|
+
selfHealingCommand: formSelfHealingCommand.trim() || undefined,
|
|
1873
|
+
autoDiagnose: Boolean(formAutoDiagnose),
|
|
1536
1874
|
env: parseEnvText(formEnv),
|
|
1537
1875
|
cwd: formCwd.trim(),
|
|
1538
1876
|
workspaceId: formWorkspaceId.trim(),
|
|
@@ -1840,6 +2178,9 @@ window.__ModuleLoader__.load({
|
|
|
1840
2178
|
}, T_KEY(t, 'list.configManaged')) : null,
|
|
1841
2179
|
task.lastDurationMs > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag', title: 'Last execution duration' }, task.lastDurationMs + 'ms') : null,
|
|
1842
2180
|
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,
|
|
2181
|
+
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,
|
|
2182
|
+
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,
|
|
2183
|
+
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
2184
|
task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
|
|
1844
2185
|
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
2186
|
),
|
|
@@ -1859,6 +2200,18 @@ window.__ModuleLoader__.load({
|
|
|
1859
2200
|
className: 'dsh-cron-task-actions',
|
|
1860
2201
|
onClick: (e) => e.stopPropagation()
|
|
1861
2202
|
},
|
|
2203
|
+
React.createElement('button', {
|
|
2204
|
+
type: 'button',
|
|
2205
|
+
className: 'dsh-cron-icon-btn',
|
|
2206
|
+
title: T_KEY(t, 'actions.dryRunTitle'),
|
|
2207
|
+
onClick: () => handleDryRun(task.id)
|
|
2208
|
+
}, '🧪'),
|
|
2209
|
+
React.createElement('button', {
|
|
2210
|
+
type: 'button',
|
|
2211
|
+
className: 'dsh-cron-icon-btn',
|
|
2212
|
+
title: T_KEY(t, 'actions.archiveTitle'),
|
|
2213
|
+
onClick: () => openArchiveModal(task, 0)
|
|
2214
|
+
}, '📜'),
|
|
1862
2215
|
React.createElement('button', {
|
|
1863
2216
|
type: 'button',
|
|
1864
2217
|
className: 'dsh-cron-icon-btn',
|
|
@@ -2074,7 +2427,18 @@ window.__ModuleLoader__.load({
|
|
|
2074
2427
|
value: formSchedule,
|
|
2075
2428
|
placeholder: T_KEY(t, 'form.schedulePlaceholder'),
|
|
2076
2429
|
onChange: (e) => setFormSchedule(e.target.value)
|
|
2077
|
-
})
|
|
2430
|
+
}),
|
|
2431
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '6px' } },
|
|
2432
|
+
React.createElement('button', {
|
|
2433
|
+
type: 'button',
|
|
2434
|
+
className: 'dsh-cron-btn-secondary',
|
|
2435
|
+
style: { padding: '3px 8px', fontSize: '11.5px' },
|
|
2436
|
+
onClick: () => handlePreviewSchedule(formSchedule)
|
|
2437
|
+
}, T_KEY(t, 'form.previewSchedule')),
|
|
2438
|
+
schedulePreviewRuns && schedulePreviewRuns.length > 0 && React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #999)' } },
|
|
2439
|
+
T_KEY(t, 'form.previewScheduleTitle') + ' ' + schedulePreviewRuns.slice(0, 3).map(r => new Date(r).toLocaleTimeString()).join(', ')
|
|
2440
|
+
)
|
|
2441
|
+
)
|
|
2078
2442
|
)
|
|
2079
2443
|
),
|
|
2080
2444
|
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
@@ -2432,6 +2796,98 @@ window.__ModuleLoader__.load({
|
|
|
2432
2796
|
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.silentRuleHint'))
|
|
2433
2797
|
)
|
|
2434
2798
|
),
|
|
2799
|
+
|
|
2800
|
+
// Reliability, Heartbeat & Self-Healing section
|
|
2801
|
+
React.createElement('div', {
|
|
2802
|
+
style: {
|
|
2803
|
+
marginTop: '16px',
|
|
2804
|
+
paddingTop: '14px',
|
|
2805
|
+
borderTop: '1px solid var(--dsw-alias-border-l2, #2e2e2e)'
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', marginBottom: '10px' } }, T_KEY(t, 'form.reliabilityTitle')),
|
|
2809
|
+
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
|
|
2810
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2811
|
+
React.createElement('label', null, T_KEY(t, 'form.preflightLabel')),
|
|
2812
|
+
React.createElement('select', {
|
|
2813
|
+
value: formPreflightType,
|
|
2814
|
+
onChange: (e) => setFormPreflightType(e.target.value)
|
|
2815
|
+
},
|
|
2816
|
+
React.createElement('option', { value: 'none' }, T_KEY(t, 'form.preflightNone')),
|
|
2817
|
+
React.createElement('option', { value: 'http' }, T_KEY(t, 'form.preflightHttp')),
|
|
2818
|
+
React.createElement('option', { value: 'shell' }, T_KEY(t, 'form.preflightShell')),
|
|
2819
|
+
React.createElement('option', { value: 'disk' }, T_KEY(t, 'form.preflightDisk'))
|
|
2820
|
+
)
|
|
2821
|
+
),
|
|
2822
|
+
formPreflightType !== 'none' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2823
|
+
React.createElement('label', null, T_KEY(t, 'form.preflightTargetLabel')),
|
|
2824
|
+
React.createElement('input', {
|
|
2825
|
+
type: 'text',
|
|
2826
|
+
value: formPreflightTarget,
|
|
2827
|
+
placeholder: T_KEY(t, 'form.preflightTargetPlaceholder'),
|
|
2828
|
+
onChange: (e) => setFormPreflightTarget(e.target.value)
|
|
2829
|
+
})
|
|
2830
|
+
)
|
|
2831
|
+
),
|
|
2832
|
+
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
|
|
2833
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2834
|
+
React.createElement('label', null, T_KEY(t, 'form.heartbeatIntervalLabel')),
|
|
2835
|
+
React.createElement('input', {
|
|
2836
|
+
type: 'number',
|
|
2837
|
+
value: formHeartbeatIntervalSeconds,
|
|
2838
|
+
min: 0,
|
|
2839
|
+
onChange: (e) => setFormHeartbeatIntervalSeconds(Number(e.target.value))
|
|
2840
|
+
})
|
|
2841
|
+
),
|
|
2842
|
+
formHeartbeatIntervalSeconds > 0 && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2843
|
+
React.createElement('label', null, T_KEY(t, 'form.gracePeriodLabel')),
|
|
2844
|
+
React.createElement('input', {
|
|
2845
|
+
type: 'number',
|
|
2846
|
+
value: formGracePeriodSeconds,
|
|
2847
|
+
min: 1,
|
|
2848
|
+
onChange: (e) => setFormGracePeriodSeconds(Number(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.priorityLabel')),
|
|
2855
|
+
React.createElement('input', {
|
|
2856
|
+
type: 'number',
|
|
2857
|
+
value: formPriority,
|
|
2858
|
+
min: 1,
|
|
2859
|
+
max: 10,
|
|
2860
|
+
onChange: (e) => setFormPriority(Number(e.target.value))
|
|
2861
|
+
})
|
|
2862
|
+
),
|
|
2863
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2864
|
+
React.createElement('label', null, T_KEY(t, 'form.concurrencyGroupLabel')),
|
|
2865
|
+
React.createElement('input', {
|
|
2866
|
+
type: 'text',
|
|
2867
|
+
value: formConcurrencyGroup,
|
|
2868
|
+
onChange: (e) => setFormConcurrencyGroup(e.target.value)
|
|
2869
|
+
})
|
|
2870
|
+
)
|
|
2871
|
+
),
|
|
2872
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2873
|
+
React.createElement('label', null, T_KEY(t, 'form.selfHealingLabel')),
|
|
2874
|
+
React.createElement('input', {
|
|
2875
|
+
type: 'text',
|
|
2876
|
+
value: formSelfHealingCommand,
|
|
2877
|
+
placeholder: T_KEY(t, 'form.selfHealingPlaceholder'),
|
|
2878
|
+
onChange: (e) => setFormSelfHealingCommand(e.target.value)
|
|
2879
|
+
})
|
|
2880
|
+
),
|
|
2881
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '6px' } },
|
|
2882
|
+
React.createElement('input', {
|
|
2883
|
+
type: 'checkbox',
|
|
2884
|
+
id: 'formAutoDiagnoseCheckbox',
|
|
2885
|
+
checked: formAutoDiagnose,
|
|
2886
|
+
onChange: (e) => setFormAutoDiagnose(e.target.checked)
|
|
2887
|
+
}),
|
|
2888
|
+
React.createElement('label', { htmlFor: 'formAutoDiagnoseCheckbox', style: { margin: 0, fontSize: '12.5px', cursor: 'pointer' } }, T_KEY(t, 'form.autoDiagnoseLabel'))
|
|
2889
|
+
)
|
|
2890
|
+
),
|
|
2435
2891
|
selectedTaskMeta && React.createElement('div', {
|
|
2436
2892
|
style: {
|
|
2437
2893
|
background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
|
|
@@ -2475,7 +2931,123 @@ window.__ModuleLoader__.load({
|
|
|
2475
2931
|
)
|
|
2476
2932
|
),
|
|
2477
2933
|
|
|
2478
|
-
|
|
2934
|
+
|
|
2935
|
+
// Dry Run simulation modal
|
|
2936
|
+
dryRunResult && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setDryRunResult(null) },
|
|
2937
|
+
React.createElement('div', {
|
|
2938
|
+
className: 'dsh-cron-modal',
|
|
2939
|
+
style: { maxWidth: '580px' },
|
|
2940
|
+
role: 'dialog',
|
|
2941
|
+
'aria-modal': 'true',
|
|
2942
|
+
onClick: (e) => e.stopPropagation()
|
|
2943
|
+
},
|
|
2944
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2945
|
+
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'dryRun.title')),
|
|
2946
|
+
React.createElement('button', {
|
|
2947
|
+
type: 'button',
|
|
2948
|
+
className: 'dsh-cron-icon-btn',
|
|
2949
|
+
onClick: () => setDryRunResult(null)
|
|
2950
|
+
}, '✕')
|
|
2951
|
+
),
|
|
2952
|
+
React.createElement('div', { style: { marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '13px' } },
|
|
2953
|
+
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')),
|
|
2954
|
+
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 || '') + ')')),
|
|
2955
|
+
dryRunResult.result && React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.duration') + ': '), React.createElement('span', null, (dryRunResult.result.durationMs || 0) + 'ms')),
|
|
2956
|
+
React.createElement('div', { style: { marginTop: '6px' } }, React.createElement('strong', null, T_KEY(t, 'dryRun.output') + ':')),
|
|
2957
|
+
React.createElement('pre', {
|
|
2958
|
+
style: {
|
|
2959
|
+
background: 'var(--dsw-alias-bg-layer-2, #18181b)',
|
|
2960
|
+
border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
|
|
2961
|
+
borderRadius: '6px',
|
|
2962
|
+
padding: '10px',
|
|
2963
|
+
maxHeight: '220px',
|
|
2964
|
+
overflowY: 'auto',
|
|
2965
|
+
fontSize: '12px',
|
|
2966
|
+
whiteSpace: 'pre-wrap',
|
|
2967
|
+
wordBreak: 'break-word',
|
|
2968
|
+
color: 'var(--dsw-alias-label-primary, #e4e4e7)'
|
|
2969
|
+
}
|
|
2970
|
+
}, (dryRunResult.result && dryRunResult.result.output) || (dryRunResult.error) || '(no output)')
|
|
2971
|
+
),
|
|
2972
|
+
React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '16px' } },
|
|
2973
|
+
React.createElement('button', {
|
|
2974
|
+
type: 'button',
|
|
2975
|
+
className: 'dsh-cron-btn-primary',
|
|
2976
|
+
onClick: () => setDryRunResult(null)
|
|
2977
|
+
}, T_KEY(t, 'dryRun.close'))
|
|
2978
|
+
)
|
|
2979
|
+
)
|
|
2980
|
+
),
|
|
2981
|
+
|
|
2982
|
+
// Archive runs modal
|
|
2983
|
+
archiveModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setArchiveModalOpen(false) },
|
|
2984
|
+
React.createElement('div', {
|
|
2985
|
+
className: 'dsh-cron-modal',
|
|
2986
|
+
style: { maxWidth: '700px' },
|
|
2987
|
+
role: 'dialog',
|
|
2988
|
+
'aria-modal': 'true',
|
|
2989
|
+
onClick: (e) => e.stopPropagation()
|
|
2990
|
+
},
|
|
2991
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
2992
|
+
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'archive.title') + (archiveTask ? ' - ' + archiveTask.title : '')),
|
|
2993
|
+
React.createElement('button', {
|
|
2994
|
+
type: 'button',
|
|
2995
|
+
className: 'dsh-cron-icon-btn',
|
|
2996
|
+
onClick: () => setArchiveModalOpen(false)
|
|
2997
|
+
}, '✕')
|
|
2998
|
+
),
|
|
2999
|
+
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #999)', margin: '4px 0 12px 0' } },
|
|
3000
|
+
T_KEY(t, 'archive.totalRuns', { total: archiveTotal })
|
|
3001
|
+
),
|
|
3002
|
+
archiveLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.loading')) :
|
|
3003
|
+
archiveRuns.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'archive.empty')) :
|
|
3004
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '380px', overflowY: 'auto' } },
|
|
3005
|
+
archiveRuns.map((r, idx) => React.createElement('div', {
|
|
3006
|
+
key: idx,
|
|
3007
|
+
style: {
|
|
3008
|
+
background: 'var(--dsw-alias-bg-layer-2, #18181b)',
|
|
3009
|
+
border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
|
|
3010
|
+
borderRadius: '6px',
|
|
3011
|
+
padding: '8px 12px',
|
|
3012
|
+
fontSize: '12px'
|
|
3013
|
+
}
|
|
3014
|
+
},
|
|
3015
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '4px' } },
|
|
3016
|
+
React.createElement('span', { style: { fontWeight: '600', color: r.status === 'success' ? 'var(--dsh-cron-success, #22c55e)' : 'var(--dsh-cron-danger, #ef4444)' } }, (r.status || 'unknown').toUpperCase()),
|
|
3017
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #888)' } }, new Date(r.timestamp || r.startedAt || Date.now()).toLocaleString())
|
|
3018
|
+
),
|
|
3019
|
+
r.durationMs > 0 && React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary, #888)', marginBottom: '4px' } }, 'Duration: ' + r.durationMs + 'ms'),
|
|
3020
|
+
r.error && React.createElement('div', { style: { color: 'var(--dsh-cron-danger, #ef4444)', marginBottom: '4px' } }, 'Error: ' + r.error),
|
|
3021
|
+
r.output && React.createElement('div', {
|
|
3022
|
+
style: {
|
|
3023
|
+
color: 'var(--dsw-alias-label-primary, #ccc)',
|
|
3024
|
+
whiteSpace: 'pre-wrap',
|
|
3025
|
+
wordBreak: 'break-word',
|
|
3026
|
+
maxHeight: '80px',
|
|
3027
|
+
overflowY: 'auto'
|
|
3028
|
+
}
|
|
3029
|
+
}, r.output.slice(0, 300) + (r.output.length > 300 ? '...' : ''))
|
|
3030
|
+
))
|
|
3031
|
+
),
|
|
3032
|
+
React.createElement('div', { className: 'dsh-cron-modal-foot', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '16px' } },
|
|
3033
|
+
archiveRuns.length < archiveTotal ? React.createElement('button', {
|
|
3034
|
+
type: 'button',
|
|
3035
|
+
className: 'dsh-cron-btn-secondary',
|
|
3036
|
+
onClick: () => {
|
|
3037
|
+
const nextOffset = archiveOffset + 15;
|
|
3038
|
+
if (archiveTask) openArchiveModal(archiveTask, nextOffset);
|
|
3039
|
+
}
|
|
3040
|
+
}, T_KEY(t, 'archive.loadMore', { shown: archiveRuns.length, total: archiveTotal })) : React.createElement('div', null),
|
|
3041
|
+
React.createElement('button', {
|
|
3042
|
+
type: 'button',
|
|
3043
|
+
className: 'dsh-cron-btn-primary',
|
|
3044
|
+
onClick: () => setArchiveModalOpen(false)
|
|
3045
|
+
}, T_KEY(t, 'archive.close'))
|
|
3046
|
+
)
|
|
3047
|
+
)
|
|
3048
|
+
),
|
|
3049
|
+
|
|
3050
|
+
// 2. Settings modal
|
|
2479
3051
|
settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
|
|
2480
3052
|
React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
2481
3053
|
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|