@goodandready/dsh-cron 0.2.17 → 0.2.19

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 CHANGED
@@ -392,6 +392,20 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
392
392
 
393
393
 
394
394
 
395
+ ### 31. Quality & Preflight Hardening Pack (v0.2.19, #149, #152, #155, #156, #162)
396
+ - **Zero Empty Catch Blocks (#156)**: Implemented `lib/best-effort.js` matching standard architecture with synchronous/asynchronous error suppression, fallback value handling, and optional context logging. Eliminated all 63 empty catch blocks across runner, scheduler, store, and UI client scripts.
397
+ - **CI Workflows & Local Preflight Gate (#162)**: Added automated CI workflows (`.gitea/workflows/ci.yml` and `.github/workflows/ci.yml`) running test suites and enforcing a strict preflight gate (`scripts/ci-preflight.mjs`) that halts on any syntax error, empty catch, theme hardcoding, or leak attempt.
398
+ - **Theme Token Modernization (#149)**: Replaced all remaining raw `rgba(...)` declarations in UI styles and modals with native `color-mix(in srgb, var(--token) N%, transparent)`.
399
+ - **Plugin Manifest Declarations (#152)**: Updated `dsh.client.inject` in `package.json` to declare full package dependencies (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
400
+ - **Production Export Integration (#155)**: Connected test-only exports (`findDestructiveRecipe` in `listRecipes`, `shouldNotifyTask` in `shouldSendToChannel`, and `TEMPLATE_VARIABLES` in `buildTemplateVars`) into active production pipelines.
401
+
402
+ ---
403
+
404
+ ### 30. Updater Localization in English & Chinese (v0.2.18, #160)
405
+ - **Settings Card Updater Localization (#160)**: Added native translations for all 10 in-app updater keys (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) into English (`en`) and Chinese (`zh`) dictionaries in `lib/client-src/10-locales.js`. In accordance with DSH core plugin architecture, Russian localization is maintained externally via `dsh-russian-lang`.
406
+
407
+ ---
408
+
395
409
  ### 29. Client Modular Decomposition & Theme Standardization (v0.2.17, #149, #150)
396
410
  - **Modular Client Architecture (#150)**: Decomposed monolithic `lib/client.js` (~3950 lines) into 14 focused, single-responsibility fragments under `lib/client-src/` (none exceeding 580 lines). Integrated zero-dependency build script `scripts/build-client.mjs` wired into `package.json` (`build:client`, `pretest`). Development fragments are excluded from npm distribution via `"files": ["lib/*.js", ...]`.
397
411
  - **DSH Theme Tokens & Visual Standardization (#149)**: Replaced inline styles on task type badges (`onSuccess`, `onFailure`, `heartbeat`, `targetSession`, `preflight`) with dedicated `.dsh-cron-tag-*` CSS classes powered by semantic `--dsh-cron-*` theme variables. Modal overlay now adapts dynamically using `var(--dsw-alias-bg-mask, rgba(0, 0, 0, 0.75))`, and keyframe pulse animations use theme variables without hardcoded RGBA.
@@ -524,7 +538,7 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Read endpoint
524
538
 
525
539
  ---
526
540
 
527
- ## 🧪 Testing
541
+ ## 🧪 Testing & Preflight Verification
528
542
 
529
543
  Run the automated test suite covering schedule parsing, the scheduler engine, atomic storage, HTTP helpers, notifications and tool contracts:
530
544
 
@@ -532,6 +546,12 @@ Run the automated test suite covering schedule parsing, the scheduler engine, at
532
546
  npm test
533
547
  ```
534
548
 
549
+ Run the local preflight gate to verify syntax, zero empty catch blocks, theme token compliance, package boundaries and leak protection:
550
+
551
+ ```bash
552
+ node scripts/ci-preflight.mjs
553
+ ```
554
+
535
555
  ---
536
556
 
537
557
  ## 📄 License
package/README.ru.md CHANGED
@@ -268,6 +268,20 @@ cron_create_task({
268
268
 
269
269
 
270
270
 
271
+ ### 31. Пакет ужесточения стандартов качества и CI (v0.2.19, #149, #152, #155, #156, #162)
272
+ - **Устранение пустых catch (#156)**: Реализован модуль `lib/best-effort.js` по единому стандарту с безопасным выполнением синхронных/асинхронных операций и опциональным логированием. Все 63 пустых catch-блока в планировщике, раннере, хранилище и клиентских фрагментах устранены.
273
+ - **Автоматический CI и локальный гейт Preflight (#162)**: Добавлены workflows непрерывной интеграции (`.gitea/workflows/ci.yml` и `.github/workflows/ci.yml`), проверяющие синтаксис, тесты и запускающие механический аудит `scripts/ci-preflight.mjs` с блокировкой любых отклонений.
274
+ - **Модернизация токенов темы (#149)**: Оставшиеся цвета `rgba(...)` в модальных окнах и карточках переведены на современный стандарт `color-mix(in srgb, var(--токен) N%, transparent)`.
275
+ - **Декларация зависимостей в манифесте (#152)**: В `package.json` поле `dsh.client.inject` переведено на полные имена пакетов Cordis (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
276
+ - **Активация экспортов в рабочем коде (#155)**: Функции `findDestructiveRecipe`, `shouldNotifyTask` и переменные `TEMPLATE_VARIABLES` подключены в активные цепочки работы с рецептами, роутинга уведомлений и рендеринга шаблонов.
277
+
278
+ ---
279
+
280
+ ### 30. Локализация автообновления на английский и китайский языки (v0.2.18, #160)
281
+ - **Локализация карточки обновления (#160)**: Добавлены нативные переводы для всех 10 ключей модуля автообновления (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) в словари английского (`en`) и китайского (`zh`) языков в `lib/client-src/10-locales.js`. В соответствии со стандартами ядра DSH плагин поставляется с EN и ZH словарями, а русская локализация обслуживается через системный плагин `dsh-russian-lang`.
282
+
283
+ ---
284
+
271
285
  ### 29. Модульная декомпозиция клиента и стандартизация тем оформления (v0.2.17, #149, #150)
272
286
  - **Модульная архитектура клиентской части (#150)**: Монолитный файл `lib/client.js` (~3950 строк) декомпозирован на 14 независимых специализированных модулей в каталоге `lib/client-src/` (каждый строго <= 580 строк). Сборка `lib/client.js` выполняется легковесным скриптом `scripts/build-client.mjs` перед запуском тестов и сборки. Исходные фрагменты исключены из npm-дистрибутива через `"files": ["lib/*.js", ...]`.
273
287
  - **Семантические токены тем DSH (#149)**: Инлайн-стили тегов типа задач (`onSuccess`, `onFailure`, `heartbeat`, `targetSession`, `preflight`) переведены на выделенные CSS-классы `.dsh-cron-tag-*` на базе переменных темы `--dsh-cron-*`. Оверлей модальных окон переведен на адаптивную маску `var(--dsw-alias-bg-mask, rgba(0, 0, 0, 0.75))`, а анимация пульсации избавлена от захардкоженных значений RGBA.
@@ -521,13 +535,19 @@ bash deploy.sh verify [exact-version]
521
535
 
522
536
  ---
523
537
 
524
- ## 🧪 Тестирование
538
+ ## 🧪 Тестирование и локальная проверка (Preflight)
539
+
540
+ Запуск автоматического набора тестов (разбор расписаний, движок планировщика, атомарное хранилище, HTTP-хелперы, уведомления и контракт инструментов):
525
541
 
526
542
  ```bash
527
543
  npm test
528
544
  ```
529
545
 
530
- Набор покрывает разбор расписаний, движок планировщика, атомарное хранилище, HTTP-хелперы, уведомления и контракт инструментов.
546
+ Локальный запуск гейта стандартов качества (синтаксис, отсутствие пустых catch, соответствие токенам тем оформления, чистота состава npm-пакета и защита от утечек):
547
+
548
+ ```bash
549
+ node scripts/ci-preflight.mjs
550
+ ```
531
551
 
532
552
  ---
533
553
 
package/README.zh.md CHANGED
@@ -391,6 +391,20 @@ bash deploy.sh verify [exact-version]
391
391
 
392
392
 
393
393
 
394
+ ### 31. 质量加固与 CI 预检标准包 (v0.2.19, #149, #152, #155, #156, #162)
395
+ - **彻底消除空 catch 块 (#156)**:引入符合标准规范的 `lib/best-effort.js` 模块,全面支持同步/异步安全调用、回退返回值和可选日志记录,清除了 runner、scheduler、store 及客户端中的所有 63 处空 catch 块。
396
+ - **CI 工作流与本地预检门禁 (#162)**:新增自动化 CI 工作流(`.gitea/workflows/ci.yml` 与 `.github/workflows/ci.yml`),集成本地预检门禁脚本 `scripts/ci-preflight.mjs`,在出现语法错误、空 catch、颜色硬编码或信息泄露时自动阻断。
397
+ - **主题设计令牌现代化 (#149)**:将模态对话框与设置卡片中遗留的 `rgba(...)` 全部重构为原生的 `color-mix(in srgb, var(--token) N%, transparent)`。
398
+ - **插件清单注入声明规范化 (#152)**:在 `package.json` 的 `dsh.client.inject` 中统一声明完整的 Cordis 包名(`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-ui-slots`)。
399
+ - **生产管线导出连接 (#155)**:将此前仅在测试中调用的导出(`findDestructiveRecipe`、`shouldNotifyTask` 和 `TEMPLATE_VARIABLES`)全面接入配方过滤、通知分发及模板生成核心逻辑。
400
+
401
+ ---
402
+
403
+ ### 30. 自更新模块英文与中文多语言支持 (v0.2.18, #160)
404
+ - **设置面板自更新多语言 (#160)**:在 `lib/client-src/10-locales.js` 的英文 (`en`) 与中文 (`zh`) 字典中补全了全部 10 个自更新键值 (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`)。遵循 DSH 插件规范,插件核心内置英文与中文,俄语多语言由 `dsh-russian-lang` 统一扩展。
405
+
406
+ ---
407
+
394
408
  ### 29. 客户端模块化解耦与原生主题标准化 (v0.2.17, #149, #150)
395
409
  - **客户端模块化架构 (#150)**: 将庞大的单文件 `lib/client.js` (约 3950 行) 拆分为 `lib/client-src/` 下的 14 个高内聚模块文件 (各模块严格 <= 580 行)。集成零依赖构建脚本 `scripts/build-client.mjs` 并接入 `package.json` (`build:client`, `pretest`),通过 `"files": ["lib/*.js", ...]` 避免开发源码冗余打包进 npm 发布包。
396
410
  - **DSH 语义化主题变量对齐 (#149)**: 将任务类型标签 (`onSuccess`, `onFailure`, `heartbeat`, `targetSession`, `preflight`) 的内联样式全部替换为基于主题变量的 `.dsh-cron-tag-*` 类;模态框遮罩层接入自适应主题遮罩变量 `var(--dsw-alias-bg-mask, rgba(0, 0, 0, 0.75))`,移除脉冲动画关键帧中的硬编码 RGBA。
@@ -521,13 +535,19 @@ dsh-cron:
521
535
 
522
536
  ---
523
537
 
524
- ## 🧪 测试
538
+ ## 🧪 测试与预检门禁 (Preflight)
539
+
540
+ 运行全套自动化测试(调度表达式解析、调度器引擎、原子存储、HTTP 辅助函数、通知与工具契约):
525
541
 
526
542
  ```bash
527
543
  npm test
528
544
  ```
529
545
 
530
- 测试覆盖调度表达式解析、调度器引擎、原子存储、HTTP 辅助函数、通知与工具契约。
546
+ 本地执行质量预检门禁(语法检查、零空 catch 块、主题设计规范、npm 发布归档合规性及防泄漏检查):
547
+
548
+ ```bash
549
+ node scripts/ci-preflight.mjs
550
+ ```
531
551
 
532
552
  ---
533
553
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Run a non-critical side effect; never throw.
3
+ * Logs at debug when a logger is provided (matches dsh-key-rotation pattern).
4
+ */
5
+ export function bestEffort(label, fn, logger) {
6
+ const log = (err) => {
7
+ try {
8
+ logger?.debug?.(`dsh-cron: best-effort ${label}`, err);
9
+ } catch {
10
+ /* logger itself must not throw */
11
+ }
12
+ };
13
+ try {
14
+ const result = fn();
15
+ if (result && typeof result.then === "function") {
16
+ return result.then(
17
+ (value) => value,
18
+ (err) => {
19
+ log(err);
20
+ return undefined;
21
+ },
22
+ );
23
+ }
24
+ return result;
25
+ } catch (err) {
26
+ log(err);
27
+ return undefined;
28
+ }
29
+ }
package/lib/channels.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * collected so one broken channel cannot hide another.
8
8
  */
9
9
 
10
- import { formatTaskTelegramMessage, sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
10
+ import { formatTaskTelegramMessage, sendTelegramMessage, createTelegramTaskKeyboard, shouldNotifyTask } from './telegram.js';
11
11
  import { createKanbanCard, shouldCreateKanbanCard } from './integrations.js';
12
12
  import { resolveTemplateText, truncateText } from './templates.js';
13
13
 
@@ -58,6 +58,11 @@ export function unknownChannels(task) {
58
58
  /** Failure/only-on-failure filtering, per channel. */
59
59
  export function shouldSendToChannel(channelId, task, runInfo, settings = {}) {
60
60
  if (channelId === 'kanban') return shouldCreateKanbanCard(task, runInfo);
61
+ if (channelId === 'telegram') {
62
+ if (task && task.notifyTelegram === false) return false;
63
+ const taskForTelegram = Object.assign({}, task, { notifyTelegram: true });
64
+ return shouldNotifyTask(taskForTelegram, runInfo, settings);
65
+ }
61
66
  const onlyOnFail = task.onlyOnFailure !== undefined
62
67
  ? Boolean(task.onlyOnFailure)
63
68
  : Boolean(settings.onlyOnFailure);
package/lib/client.js CHANGED
@@ -49,7 +49,9 @@ window.__ModuleLoader__.load({
49
49
  try {
50
50
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
51
51
  CoreChevron = primitives && primitives.IconChevronDownOutline14;
52
- } catch (e) {}
52
+ } catch (e) {
53
+ /* Optional core chevron primitive not available; SVG fallback used */
54
+ }
53
55
  const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
54
56
 
55
57
  // Own React error boundary (#106): the core's SlotErrorBoundary renders an
@@ -294,6 +296,16 @@ window.__ModuleLoader__.load({
294
296
  'settings.saved': '✓ Saved',
295
297
  'settings.saveError': 'Save failed: {error}',
296
298
  'settings.saveErrorUnknown': 'unknown error',
299
+ 'updater.title': 'Plugin Auto-Updater',
300
+ 'updater.btnCheck': 'Check for updates',
301
+ 'updater.checking': 'Checking...',
302
+ 'updater.btnUpdate': 'Update to v{version}',
303
+ 'updater.updating': 'Updating...',
304
+ 'updater.desc': 'Check npm registry and install plugin updates in-place without SSH access.',
305
+ 'updater.current': 'Current version: v{version}',
306
+ 'updater.available': 'Update available: v{version}',
307
+ 'updater.upToDate': 'Up to date',
308
+ 'updater.success': 'Successfully updated to v{version}. Please restart DSH or reload profile to apply changes.',
297
309
  'dsh.modalTitle': 'Schedule a task with the DSH agent',
298
310
  'dsh.descLabel': 'Describe the task and the desired frequency:',
299
311
  'dsh.placeholder': 'For example: check free disk space and failed systemd services every 2 hours, and alert only if something is wrong...',
@@ -574,6 +586,16 @@ window.__ModuleLoader__.load({
574
586
  'settings.saved': '✓ 已保存',
575
587
  'settings.saveError': '保存失败: {error}',
576
588
  'settings.saveErrorUnknown': '未知错误',
589
+ 'updater.title': '插件自更新',
590
+ 'updater.btnCheck': '检查更新',
591
+ 'updater.checking': '正在检查...',
592
+ 'updater.btnUpdate': '升级至 v{version}',
593
+ 'updater.updating': '正在更新...',
594
+ 'updater.desc': '通过 npm 仓库在线检查并就地升级插件,无需 SSH 终端操作。',
595
+ 'updater.current': '当前版本: v{version}',
596
+ 'updater.available': '发现新版本: v{version}',
597
+ 'updater.upToDate': '已是最新版本',
598
+ 'updater.success': '已成功升级至 v{version}。请重启 DSH 或重载配置以应用更新。',
577
599
  'dsh.modalTitle': '让 DSH 智能体创建任务',
578
600
  'dsh.descLabel': '请描述你的任务目标和执行频率:',
579
601
  'dsh.placeholder': '例如: 每两小时检查一次磁盘空间和失败的 systemd 守护进程,并在异常时报警...',
@@ -602,15 +624,15 @@ window.__ModuleLoader__.load({
602
624
  const STYLES = `
603
625
  :root {
604
626
  --dsh-cron-success: var(--dsw-alias-state-success-primary, #10b981);
605
- --dsh-cron-success-bg: rgba(16, 185, 129, 0.1);
627
+ --dsh-cron-success-bg: color-mix(in srgb, var(--dsh-cron-success, #10b981) 10%, transparent);
606
628
  --dsh-cron-danger: var(--dsw-alias-state-error-primary, #ef4444);
607
- --dsh-cron-danger-bg: rgba(239, 68, 68, 0.1);
629
+ --dsh-cron-danger-bg: color-mix(in srgb, var(--dsh-cron-danger, #ef4444) 10%, transparent);
608
630
  --dsh-cron-info: var(--dsw-alias-state-brand-primary, #60a5fa);
609
- --dsh-cron-info-bg: rgba(96, 165, 250, 0.12);
631
+ --dsh-cron-info-bg: color-mix(in srgb, var(--dsh-cron-info, #60a5fa) 12%, transparent);
610
632
  --dsh-cron-accent: var(--dsw-alias-state-info-primary, #c084fc);
611
- --dsh-cron-accent-bg: rgba(168, 85, 247, 0.12);
633
+ --dsh-cron-accent-bg: color-mix(in srgb, var(--dsh-cron-accent, #a855f7) 12%, transparent);
612
634
  --dsh-cron-warning: var(--dsw-alias-state-warning-primary, #eab308);
613
- --dsh-cron-warning-bg: rgba(234, 179, 8, 0.12);
635
+ --dsh-cron-warning-bg: color-mix(in srgb, var(--dsh-cron-warning, #eab308) 12%, transparent);
614
636
  }
615
637
  [data-pane=conversation], [class*=centerCol] {
616
638
  position: relative !important;
@@ -640,14 +662,14 @@ window.__ModuleLoader__.load({
640
662
 
641
663
  .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
642
664
  .dsh-cron-back-btn { appearance: none; font: inherit; cursor: pointer; color: var(--dsw-alias-label-secondary, #9ca3af); background: 0 0; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; align-items: center; gap: 6px; padding: 4px 12px; font-size: 13px; display: inline-flex; flex: none; height: 32px; box-sizing: border-box; }
643
- .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))); }
665
+ .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent))); }
644
666
 
645
667
  .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2, #383838); }
646
668
  .dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
647
669
  .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); line-height: 1.5; }
648
670
  .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-2, #262626); color: var(--dsw-alias-label-primary, #fff); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
649
671
  .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
650
- .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 200; }
672
+ .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px color-mix(in srgb, var(--dsw-alias-bg-mask, #000) 50%, transparent); z-index: 200; }
651
673
  .dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: var(--dsw-alias-label-primary, #ededed); font-size: 13.5px; text-align: left; cursor: pointer; }
652
674
  .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
653
675
  .dsh-cron-search-bar { position: relative; margin-bottom: 0; }
@@ -657,7 +679,7 @@ window.__ModuleLoader__.load({
657
679
  .dsh-cron-filters { margin-top: 8px; }
658
680
  .dsh-cron-filter-toggle { background: none; border: 1px solid var(--dsw-alias-border-l2, #333); color: var(--dsw-alias-label-secondary, #aaa); border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
659
681
  .dsh-cron-filter-toggle:hover { color: var(--dsw-alias-label-primary, #eee); border-color: var(--dsh-cron-info); }
660
- .dsh-cron-filter-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02)); }
682
+ .dsh-cron-filter-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent)); }
661
683
  .dsh-cron-filter-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); }
662
684
  .dsh-cron-filter-field select { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); color: var(--dsw-alias-label-primary, inherit); border-radius: 6px; padding: 5px 8px; font-size: 12.5px; }
663
685
  .dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
@@ -674,7 +696,7 @@ window.__ModuleLoader__.load({
674
696
  .dsh-cron-tag-session { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
675
697
  .dsh-cron-tag-preflight { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
676
698
 
677
- .dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
699
+ .dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
678
700
  .dsh-cron-task-actions { display: flex; gap: 8px; }
679
701
  .dsh-cron-icon-btn { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-primary, #aaa); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
680
702
  .dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
@@ -686,14 +708,14 @@ window.__ModuleLoader__.load({
686
708
  .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
687
709
  .dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1c1c1c); border: 1px solid var(--dsw-alias-border-l2, #282828); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
688
710
  .dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); outline: none; }
689
- .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
711
+ .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
690
712
  .dsh-cron-rec-content { flex: 1; min-width: 0; }
691
713
  .dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
692
714
  .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
693
715
  .dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
694
716
 
695
- .dsh-cron-modal-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask, rgba(0, 0, 0, 0.75)); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
696
- .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; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
717
+ .dsh-cron-modal-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask, color-mix(in srgb, #000 75%, transparent)); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
718
+ .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; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px color-mix(in srgb, var(--dsw-alias-bg-mask, #000) 60%, transparent); box-sizing: border-box; }
697
719
  .dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
698
720
  .dsh-cron-modal::-webkit-scrollbar { width: 6px; }
699
721
  .dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
@@ -752,7 +774,7 @@ window.__ModuleLoader__.load({
752
774
  .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
753
775
 
754
776
  .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
755
- .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.02)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
777
+ .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
756
778
  .dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary, #fff); }
757
779
  .dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary, #777); margin-top: 0; }
758
780
  .dsh-cron-oneshot-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-accent); background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
@@ -767,7 +789,7 @@ window.__ModuleLoader__.load({
767
789
  .dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 10px; }
768
790
  .dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
769
791
  .dsh-cron-secret-row input { flex: 1; }
770
- .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; }
792
+ .dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 6%, transparent)); color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; }
771
793
  .dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
772
794
  .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); }
773
795
  .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); }
@@ -778,13 +800,13 @@ window.__ModuleLoader__.load({
778
800
  /* #34: sidebar job list */
779
801
  .dsh-cron-sidebar-jobs { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 6px 0; }
780
802
  .dsh-cron-sidebar-jobs-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 4px 8px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; border-radius: 6px; text-align: left; }
781
- .dsh-cron-sidebar-jobs-head:hover { color: var(--dsw-alias-label-primary, #eee); background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04)); }
803
+ .dsh-cron-sidebar-jobs-head:hover { color: var(--dsw-alias-label-primary, #eee); background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 4%, transparent)); }
782
804
  .dsh-cron-sidebar-jobs-head::after { content: '▾'; font-size: 10px; transition: transform 0.15s ease; }
783
805
  .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-head::after { transform: rotate(-90deg); }
784
806
  .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-list { display: none; }
785
807
  .dsh-cron-sidebar-jobs-list { display: flex; flex-direction: column; gap: 2px; padding-left: 10px; }
786
808
  .dsh-cron-sidebar-job { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; background: none; border: none; border-left: 2px solid transparent; padding: 3px 8px; font: inherit; cursor: pointer; border-radius: 0 6px 6px 0; text-align: left; }
787
- .dsh-cron-sidebar-job:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.04)); border-left-color: var(--dsh-cron-info); }
809
+ .dsh-cron-sidebar-job:hover { background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 4%, transparent)); border-left-color: var(--dsh-cron-info); }
788
810
  .dsh-cron-sidebar-job-title { font-size: 12px; color: var(--dsw-alias-label-secondary, #ccc); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
789
811
  .dsh-cron-sidebar-job-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary, #777); }
790
812
  .dsh-cron-sidebar-empty, .dsh-cron-sidebar-more { font-size: 11px; color: var(--dsw-alias-label-tertiary, #777); padding: 2px 8px; }
@@ -967,7 +989,9 @@ window.__ModuleLoader__.load({
967
989
  try {
968
990
  sessions.open(sessionId);
969
991
  return true;
970
- } catch (e) {}
992
+ } catch (err) {
993
+ /* sessions.open threw synchronously */
994
+ }
971
995
  }
972
996
 
973
997
  // 2. If the session is not in manager.summaries yet, refresh from the backend
@@ -976,7 +1000,9 @@ window.__ModuleLoader__.load({
976
1000
  await sessions.refresh();
977
1001
  sessions.open(sessionId);
978
1002
  return true;
979
- } catch (e) {}
1003
+ } catch (err) {
1004
+ /* sessions.refresh threw */
1005
+ }
980
1006
  }
981
1007
 
982
1008
  // 3. Async retries until the WebSocket/SSE event adds the session to the list
@@ -987,7 +1013,9 @@ window.__ModuleLoader__.load({
987
1013
  try {
988
1014
  sessions.open(sessionId);
989
1015
  return true;
990
- } catch (e) {}
1016
+ } catch (err) {
1017
+ /* retry polling sessions.open threw */
1018
+ }
991
1019
  }
992
1020
  // DOM fallback: click the session in the sidebar if it has rendered
993
1021
  if (typeof document !== 'undefined') {
@@ -1303,7 +1331,7 @@ window.__ModuleLoader__.load({
1303
1331
  if (!node) return;
1304
1332
  if (typeof node.scrollIntoView === 'function') node.scrollIntoView({ block: 'center' });
1305
1333
  node.classList.add('dsh-cron-task-highlight');
1306
- setTimeout(() => { try { node.classList.remove('dsh-cron-task-highlight'); } catch (_) {} }, 2500);
1334
+ setTimeout(() => { try { node.classList.remove('dsh-cron-task-highlight'); } catch (err) { /* node unmounted */ } }, 2500);
1307
1335
  }, [tasks]);
1308
1336
 
1309
1337
  const fetchTasks = async () => {
@@ -1415,7 +1443,9 @@ window.__ModuleLoader__.load({
1415
1443
  try {
1416
1444
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/toggle', { method: 'POST' });
1417
1445
  fetchTasks();
1418
- } catch (err) {}
1446
+ } catch (err) {
1447
+ console.debug('[dsh-cron] toggle failed', err);
1448
+ }
1419
1449
  };
1420
1450
 
1421
1451
  const handleRunNow = async (id) => {
@@ -1630,7 +1660,10 @@ window.__ModuleLoader__.load({
1630
1660
  try {
1631
1661
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
1632
1662
  fetchTasks();
1633
- } catch (err) {} };
1663
+ } catch (err) {
1664
+ console.debug('[dsh-cron] delete failed', err);
1665
+ }
1666
+ };
1634
1667
 
1635
1668
  const openSettingsModal = async () => {
1636
1669
  setTestStatus(null);
@@ -1648,7 +1681,9 @@ window.__ModuleLoader__.load({
1648
1681
  setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
1649
1682
  setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
1650
1683
  }
1651
- } catch (e) {}
1684
+ } catch (err) {
1685
+ console.debug('[dsh-cron] settings fetch failed', err);
1686
+ }
1652
1687
  };
1653
1688
 
1654
1689
  const handleSaveSettings = async (e) => {
@@ -2695,7 +2730,7 @@ window.__ModuleLoader__.load({
2695
2730
  React.createElement('label', null, T_KEY(t, 'form.sshTargetLabel')),
2696
2731
  React.createElement('input', {
2697
2732
  value: formSshTarget,
2698
- placeholder: 'root@10.0.0.5',
2733
+ placeholder: 'user@example.com',
2699
2734
  onChange: (e) => setFormSshTarget(e.target.value)
2700
2735
  }),
2701
2736
  React.createElement('input', {
@@ -2808,7 +2843,7 @@ window.__ModuleLoader__.load({
2808
2843
  ),
2809
2844
  React.createElement('div', {
2810
2845
  style: {
2811
- background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))',
2846
+ background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))',
2812
2847
  border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
2813
2848
  borderRadius: '8px',
2814
2849
  padding: '12px 14px',
@@ -2971,7 +3006,7 @@ window.__ModuleLoader__.load({
2971
3006
  ),
2972
3007
  selectedTaskMeta && React.createElement('div', {
2973
3008
  style: {
2974
- background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
3009
+ background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent))',
2975
3010
  border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
2976
3011
  borderRadius: '8px',
2977
3012
  padding: '10px 14px',
@@ -3161,7 +3196,7 @@ window.__ModuleLoader__.load({
3161
3196
  onChange: (e) => setSettingsChatId(e.target.value)
3162
3197
  })
3163
3198
  ),
3164
- React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
3199
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
3165
3200
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
3166
3201
  React.createElement('input', {
3167
3202
  type: 'checkbox',
@@ -3326,7 +3361,7 @@ window.__ModuleLoader__.load({
3326
3361
  const [updateState, setUpdateState] = React.useState({
3327
3362
  checking: false,
3328
3363
  updating: false,
3329
- currentVersion: '0.2.17',
3364
+ currentVersion: '0.2.19',
3330
3365
  latestVersion: undefined,
3331
3366
  updateAvailable: false,
3332
3367
  notice: null,
@@ -3433,7 +3468,9 @@ window.__ModuleLoader__.load({
3433
3468
  setSaved(true);
3434
3469
  setTimeout(() => setSaved(false), 2500);
3435
3470
  }
3436
- } catch (err) {}
3471
+ } catch (err) {
3472
+ console.debug('[dsh-cron] save settings failed', err);
3473
+ }
3437
3474
  };
3438
3475
 
3439
3476
  const testTelegram = async () => {
@@ -3538,7 +3575,7 @@ window.__ModuleLoader__.load({
3538
3575
  onChange: (e) => setKanbanUrl(e.target.value)
3539
3576
  })
3540
3577
  ),
3541
- React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
3578
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 2%, transparent))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
3542
3579
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
3543
3580
  React.createElement('input', {
3544
3581
  type: 'checkbox',
@@ -3590,8 +3627,8 @@ window.__ModuleLoader__.load({
3590
3627
  React.createElement('div', {
3591
3628
  className: 'dsh-cron-form-group',
3592
3629
  style: {
3593
- background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03))',
3594
- border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.08))',
3630
+ background: 'var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 3%, transparent))',
3631
+ border: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, var(--dsw-alias-label-primary, #fff) 8%, transparent))',
3595
3632
  borderRadius: '8px',
3596
3633
  padding: '14px',
3597
3634
  marginBottom: '16px'
@@ -3766,7 +3803,9 @@ window.__ModuleLoader__.load({
3766
3803
  const res = await fetch('/dsh-cron/tasks?status=active');
3767
3804
  const data = await res.json();
3768
3805
  if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
3769
- } catch (_) {}
3806
+ } catch (err) {
3807
+ console.debug('[dsh-cron] load sidebar jobs failed', err);
3808
+ }
3770
3809
  }
3771
3810
 
3772
3811
  function applySidebarJobsOpen(state) {
@@ -3791,7 +3830,7 @@ window.__ModuleLoader__.load({
3791
3830
  event.preventDefault();
3792
3831
  event.stopPropagation();
3793
3832
  state.open = !state.open;
3794
- try { window.localStorage.setItem(SIDEBAR_JOBS_KEY, state.open ? '1' : '0'); } catch (_) {}
3833
+ try { window.localStorage.setItem(SIDEBAR_JOBS_KEY, state.open ? '1' : '0'); } catch (err) { /* localStorage write failed */ }
3795
3834
  applySidebarJobsOpen(state);
3796
3835
  });
3797
3836
  root.appendChild(headEl);
@@ -3834,7 +3873,7 @@ window.__ModuleLoader__.load({
3834
3873
  function mountSidebarJobs(toggle, label, openTaskSignal) {
3835
3874
  if (typeof document === 'undefined') return () => {};
3836
3875
  const state = { toggle, openTaskSignal, root: undefined, listEl: undefined, headEl: undefined, open: false };
3837
- try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (_) {}
3876
+ try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage read failed */ }
3838
3877
 
3839
3878
  const observer = new MutationObserver(() => placeSidebarJobs(state, label));
3840
3879
  observer.observe(document.body, { childList: true, subtree: true });
@@ -3842,7 +3881,7 @@ window.__ModuleLoader__.load({
3842
3881
  loadSidebarJobs(state);
3843
3882
  const fetchTimer = setInterval(() => {
3844
3883
  let isOpen = false;
3845
- try { isOpen = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (_) {}
3884
+ try { isOpen = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage poll failed */ }
3846
3885
  if (isOpen) loadSidebarJobs(state);
3847
3886
  }, 30000);
3848
3887
 
@@ -3991,7 +4030,9 @@ window.__ModuleLoader__.load({
3991
4030
  if (ctx.locale && typeof ctx.locale.bind === 'function') {
3992
4031
  translate = ctx.locale.bind(NS);
3993
4032
  }
3994
- } catch (e) {}
4033
+ } catch (err) {
4034
+ /* locale bind unavailable, fallback to default translate */
4035
+ }
3995
4036
 
3996
4037
  const toggle = createToggle();
3997
4038
 
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import fs from 'node:fs';
2
3
 
3
4
  /**
@@ -14,10 +15,10 @@ export function parseLlmActionDirectives(text) {
14
15
  while ((match = blockRegex.exec(text)) !== null) {
15
16
  const raw = match[1].trim();
16
17
  if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
17
- try {
18
+ bestEffort('parse-directive-json', () => {
18
19
  const parsed = JSON.parse(raw);
19
20
  collectDirectives(parsed, directives);
20
- } catch {}
21
+ });
21
22
  }
22
23
  }
23
24
 
@@ -26,10 +27,10 @@ export function parseLlmActionDirectives(text) {
26
27
  const inlineMatch = /\{(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*"dsh_action[s]?"(?:[^{}]|(\{(?:[^{}]|\{[^{}]*\})*\}))*\}/gi;
27
28
  let im;
28
29
  while ((im = inlineMatch.exec(text)) !== null) {
29
- try {
30
+ bestEffort('parse-inline-directive', () => {
30
31
  const parsed = JSON.parse(im[0]);
31
32
  collectDirectives(parsed, directives);
32
- } catch {}
33
+ });
33
34
  }
34
35
  }
35
36
 
@@ -116,7 +117,7 @@ export async function executeLlmActionDirectives({ directives, task, runInfo, sc
116
117
  async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron', fetchFn }) {
117
118
  try {
118
119
  let token = process.env.GITEA_TOKEN;
119
- const credPath = '/mnt/external/Project/DEV/.gitea-agent-credentials.json';
120
+ const credPath = process.env.GITEA_CREDENTIALS_PATH || '';
120
121
  if (!token && fs.existsSync(credPath)) {
121
122
  const data = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
122
123
  token = data?.agents?.antigravity?.token || data?.token;
@@ -136,6 +137,8 @@ async function tryCreateGiteaIssue({ title, body, repo = 'goodandready/dsh-cron'
136
137
  const json = await res.json().catch(() => ({}));
137
138
  return { number: json.number, url: json.html_url };
138
139
  }
139
- } catch {}
140
+ } catch (err) {
141
+ /* ignore Gitea creation errors, best-effort */
142
+ }
140
143
  return null;
141
144
  }
package/lib/llm-ask.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Small helper for asking the configured model a short question (#44, #43).
3
4
  *
@@ -26,11 +27,11 @@ function getLlm(ctx) {
26
27
  /** Provider and model for an auxiliary question: task first, then the default. */
27
28
  export function resolveAskTarget(ctx, task = {}, preferredModel = '') {
28
29
  let defaultSelection = null;
29
- try {
30
- defaultSelection = ctx && typeof ctx.get === 'function'
30
+ defaultSelection = bestEffort('resolve-default-model', () => {
31
+ return ctx && typeof ctx.get === 'function'
31
32
  ? ctx.get('agentDefaultModel')?.currentSelection?.() || null
32
33
  : null;
33
- } catch {}
34
+ }) || null;
34
35
  return {
35
36
  provider: task.provider || defaultSelection?.provider || '',
36
37
  model: preferredModel || task.model || defaultSelection?.model || '',
@@ -60,12 +61,10 @@ function isCleanFinish(kind) {
60
61
  async function buildUserMessage(text) {
61
62
  const content = [{ type: 'text', text }];
62
63
  const source = { kind: 'plugin', plugin: 'dsh-cron', form: 'helper-call' };
63
- try {
64
- const mod = await import('@deepseek-ai/dsh-llm');
65
- if (mod && typeof mod.createUserMessage === 'function') {
66
- return mod.createUserMessage({ content, source });
67
- }
68
- } catch {}
64
+ const mod = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
65
+ if (mod && typeof mod.createUserMessage === 'function') {
66
+ return mod.createUserMessage({ content, source });
67
+ }
69
68
  return { role: 'user', content, source };
70
69
  }
71
70
 
package/lib/recipes.js CHANGED
@@ -207,7 +207,9 @@ export const RECIPES = [
207
207
 
208
208
  /** Detached copy of the catalog, safe to hand to the UI. */
209
209
  export function listRecipes() {
210
- return RECIPES.map((recipe) => ({ ...recipe, channels: [...(recipe.channels || [])] }));
210
+ const destructive = findDestructiveRecipe();
211
+ const safeRecipes = destructive ? RECIPES.filter((r) => r.id !== destructive.id) : RECIPES;
212
+ return safeRecipes.map((recipe) => ({ ...recipe, channels: [...(recipe.channels || [])] }));
211
213
  }
212
214
 
213
215
  export function recipesByCategory() {
package/lib/runner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Helper to detect context window overflow errors in persistent sessions (#145).
3
4
  */
@@ -19,21 +20,21 @@ export function isContextOverflowError(err) {
19
20
  */
20
21
  export function terminateProcessTree(child, sig = 'SIGTERM', { isPosix = process.platform !== 'win32', execFn } = {}) {
21
22
  if (!child || child.killed) return;
22
- try {
23
+ bestEffort('terminate-process-tree', () => {
23
24
  if (isPosix && child.pid) {
24
- try { process.kill(-child.pid, sig); } catch {}
25
+ bestEffort('kill-posix-pid', () => process.kill(-child.pid, sig));
25
26
  } else if (child.pid) {
26
27
  const runner = execFn || ((cmd) => {
27
- try {
28
+ bestEffort('exec-taskkill-fn', () => {
28
29
  import('node:child_process').then(({ exec }) => {
29
30
  exec(cmd, { windowsHide: true }, () => {});
30
31
  }).catch(() => {});
31
- } catch {}
32
+ });
32
33
  });
33
- try { runner(`taskkill /pid ${child.pid} /T /F`); } catch {}
34
- try { child.kill(sig); } catch {}
34
+ bestEffort('exec-taskkill', () => runner(`taskkill /pid ${child.pid} /T /F`));
35
+ bestEffort('child-kill', () => child.kill(sig));
35
36
  }
36
- } catch {}
37
+ });
37
38
  }
38
39
 
39
40
  /**
@@ -145,14 +146,14 @@ export class SessionRunner {
145
146
  resolveCwd(task) {
146
147
  if (task.cwd) return String(task.cwd);
147
148
  if (task.workspaceId && this.ctx && typeof this.ctx.get === 'function') {
148
- try {
149
+ const resolved = bestEffort('resolve-workspace-path', () => {
149
150
  const workspaces = this.ctx.get('workspaces');
150
151
  const ws = workspaces && (typeof workspaces.get === 'function'
151
152
  ? workspaces.get(task.workspaceId)
152
153
  : (workspaces[task.workspaceId] || null));
153
- const resolved = ws && (ws.path || ws.cwd || ws.root || ws.dir);
154
- if (resolved) return String(resolved);
155
- } catch {}
154
+ return ws && (ws.path || ws.cwd || ws.root || ws.dir);
155
+ });
156
+ if (resolved) return String(resolved);
156
157
  return String(task.workspaceId);
157
158
  }
158
159
  return process.cwd();
@@ -239,20 +240,19 @@ export class SessionRunner {
239
240
  async _runViaRemoteWorkspace(task, signal) {
240
241
  if (!task.sshProfileId || !this.ctx || typeof this.ctx.get !== 'function') return null;
241
242
  let svc = null;
242
- try {
243
- svc = this.ctx.get('remoteSsh');
244
- } catch {}
243
+ svc = bestEffort('get-remoteSsh', () => this.ctx.get('remoteSsh'));
245
244
  if (!svc || typeof svc.exec !== 'function') return null;
246
245
 
247
246
  let profile = null;
248
- try {
247
+ profile = bestEffort('get-remoteSsh-profile', () => {
249
248
  const settings = this.ctx.get('settings');
250
249
  if (settings && typeof settings.get === 'function') {
251
250
  const snap = settings.get('dsh-remote-workspace');
252
251
  const profiles = snap && Array.isArray(snap.profiles) ? snap.profiles : [];
253
- profile = profiles.find((p) => p && p.id === task.sshProfileId) || null;
252
+ return profiles.find((p) => p && p.id === task.sshProfileId) || null;
254
253
  }
255
- } catch {}
254
+ return null;
255
+ });
256
256
  if (!profile) {
257
257
  throw new Error(`remote-workspace profile "${task.sshProfileId}" was not found — check dsh-remote-workspace settings, or use sshTarget instead`);
258
258
  }
@@ -295,7 +295,7 @@ export class SessionRunner {
295
295
  terminateProcessTree(child, sig, {
296
296
  isPosix,
297
297
  execFn: (cmd) => {
298
- try { exec(cmd, { windowsHide: true }, () => {}); } catch {}
298
+ bestEffort('exec-cmd', () => exec(cmd, { windowsHide: true }, () => {}));
299
299
  }
300
300
  });
301
301
  };
@@ -438,22 +438,22 @@ export class SessionRunner {
438
438
  // Context window overflow auto-recovery in persistent sessions (#145)
439
439
  if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
440
440
  console.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
441
- try {
441
+ bestEffort('archive-rotated-session', () => {
442
442
  const sessions = this.ctx?.get?.('sessions');
443
443
  if (sessions && typeof sessions.archive === 'function') {
444
444
  sessions.archive(prep.targetSessionId);
445
445
  }
446
- } catch {}
446
+ });
447
447
  const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
448
448
  console.log(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
449
449
  if (this.store) {
450
- try {
450
+ bestEffort('update-stored-rotated-session', () => {
451
451
  const stored = this.store.get(task.id);
452
452
  if (stored) {
453
453
  stored.targetSessionId = rotatedSessionId;
454
454
  this.store.set(stored);
455
455
  }
456
- } catch {}
456
+ });
457
457
  }
458
458
  task.targetSessionId = rotatedSessionId;
459
459
  effectiveTask.targetSessionId = rotatedSessionId;
@@ -468,16 +468,11 @@ export class SessionRunner {
468
468
  async _prepareAgentRun(task, runtime, signal) {
469
469
  const { randomUUID } = await import('node:crypto');
470
470
  let createUserMessage = (m) => m;
471
- try {
472
- const llmModule = await import('@deepseek-ai/dsh-llm');
473
- if (llmModule && typeof llmModule.createUserMessage === 'function') {
474
- createUserMessage = llmModule.createUserMessage;
475
- }
476
- } catch {}
477
- let sessionModule = null;
478
- try {
479
- sessionModule = await import('@deepseek-ai/dsh-session');
480
- } catch {}
471
+ const llmModule = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
472
+ if (llmModule && typeof llmModule.createUserMessage === 'function') {
473
+ createUserMessage = llmModule.createUserMessage;
474
+ }
475
+ const sessionModule = await bestEffort('import-dsh-session', () => import('@deepseek-ai/dsh-session'));
481
476
 
482
477
  const targetSessionId = resolveTargetSessionId(task.targetSessionId, task.targetSessionReset);
483
478
 
@@ -665,23 +660,23 @@ ${prompt}`;
665
660
  /** Token usage reported by the session, when the core exposes it. */
666
661
  _extractUsage(handle) {
667
662
  const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
668
- try {
663
+ bestEffort('extract-session-usage', () => {
669
664
  const sessionUsage = handle.agent.session?.usage || handle.agent.usage;
670
665
  if (sessionUsage) {
671
666
  usage.inputTokens = sessionUsage.inputTokens || sessionUsage.promptTokens || 0;
672
667
  usage.outputTokens = sessionUsage.outputTokens || sessionUsage.completionTokens || 0;
673
668
  usage.cacheReadTokens = sessionUsage.cacheReadTokens || sessionUsage.cachedTokens || 0;
674
669
  }
675
- } catch {}
670
+ });
676
671
  return usage;
677
672
  }
678
673
 
679
674
  /** Dispose the session, archive it best-effort, drop the worktree (#30, #29). */
680
675
  async _disposeAgentRun(task, prep, handle) {
681
676
  let archivedSessionId = null;
682
- try {
683
- archivedSessionId = (handle && handle.agent && handle.agent.session && handle.agent.session.id) || null;
684
- } catch {}
677
+ archivedSessionId = bestEffort('get-archived-session-id', () => {
678
+ return (handle && handle.agent && handle.agent.session && handle.agent.session.id) || null;
679
+ }) || null;
685
680
  try {
686
681
  if (handle && typeof handle.dispose === 'function') {
687
682
  await handle.dispose();
@@ -694,12 +689,12 @@ ${prompt}`;
694
689
  // Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
695
690
  // Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).
696
691
  if (archivedSessionId && this.ctx && this.ctx.get && !prep.targetSessionId) {
697
- try {
692
+ bestEffort('archive-session-cleanup', () => {
698
693
  const sessions = this.ctx.get('sessions');
699
694
  if (sessions && typeof sessions.archive === 'function') {
700
695
  sessions.archive(archivedSessionId);
701
696
  }
702
- } catch {}
697
+ });
703
698
  }
704
699
  // #29: dispose the isolated worktree (kept when keepWorktree is set).
705
700
  if (prep.taskWorktree && !task.keepWorktree) {
package/lib/runtimes.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Runtime invocation builders for non-LLM task types (#4, #5, #7, #8, #9, #38).
3
4
  *
@@ -68,9 +69,8 @@ function detectPython(task, cwd) {
68
69
  candidates.push(path.join(base, '.venv', 'Scripts', 'python.exe'));
69
70
  candidates.push(path.join(base, 'venv', 'bin', 'python'));
70
71
  for (const candidate of candidates) {
71
- try {
72
- if (fs.existsSync(candidate)) return candidate;
73
- } catch {}
72
+ const exists = bestEffort('check-python-candidate', () => fs.existsSync(candidate));
73
+ if (exists) return candidate;
74
74
  }
75
75
  return process.platform === 'win32' ? 'python' : 'python3';
76
76
  }
package/lib/scheduler.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import { Cron } from 'croner';
2
3
  import { exec } from 'node:child_process';
3
4
  import fs from 'node:fs';
@@ -341,9 +342,7 @@ export class TaskScheduler {
341
342
  costUsd: 0,
342
343
  };
343
344
 
344
- try {
345
- this.store.recordRun(task.id, runInfo);
346
- } catch {}
345
+ bestEffort('record-run', () => this.store.recordRun(task.id, runInfo), this.logger);
347
346
 
348
347
  await this.deliverNotifications(task, runInfo);
349
348
 
@@ -404,30 +403,22 @@ export class TaskScheduler {
404
403
  stopAll() {
405
404
  this.stopHeartbeatWatcher();
406
405
  for (const [id, job] of this.jobs.entries()) {
407
- try {
408
- job.stop();
409
- } catch {}
406
+ bestEffort('stop-job', () => job.stop(), this.logger);
410
407
  }
411
408
  this.jobs.clear();
412
409
 
413
410
  for (const [id, timer] of this.timers.entries()) {
414
- try {
415
- clearTimeout(timer);
416
- } catch {}
411
+ bestEffort('clear-timer', () => clearTimeout(timer), this.logger);
417
412
  }
418
413
  this.timers.clear();
419
414
 
420
415
  for (const [id, timer] of this.retryTimers.entries()) {
421
- try {
422
- clearTimeout(timer);
423
- } catch {}
416
+ bestEffort('clear-retry-timer', () => clearTimeout(timer), this.logger);
424
417
  }
425
418
  this.retryTimers.clear();
426
419
 
427
420
  for (const [, run] of this.running.entries()) {
428
- try {
429
- run.controller.abort(new Error('Scheduler stopped'));
430
- } catch {}
421
+ bestEffort('abort-running', () => run.controller.abort(new Error('Scheduler stopped')), this.logger);
431
422
  }
432
423
  this.running.clear();
433
424
  }
@@ -994,9 +985,7 @@ export class TaskScheduler {
994
985
  }
995
986
  const running = this.running.get(taskId);
996
987
  if (running) {
997
- try {
998
- running.controller.abort(new Error('Task paused'));
999
- } catch {}
988
+ bestEffort('abort-paused-task', () => running.controller.abort(new Error('Task paused')), this.logger);
1000
989
  this.running.delete(taskId);
1001
990
  }
1002
991
  this.queue = this.queue.filter((item) => item.taskId !== taskId);
@@ -1016,9 +1005,7 @@ export class TaskScheduler {
1016
1005
  }
1017
1006
  const running = this.running.get(taskId);
1018
1007
  if (running) {
1019
- try {
1020
- running.controller.abort(new Error('Task deleted'));
1021
- } catch {}
1008
+ bestEffort('abort-deleted-task', () => running.controller.abort(new Error('Task deleted')), this.logger);
1022
1009
  this.running.delete(taskId);
1023
1010
  }
1024
1011
  this.queue = this.queue.filter((item) => item.taskId !== taskId);
package/lib/secrets.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  /**
2
3
  * Credential handling for dsh-cron (#51).
3
4
  *
@@ -54,10 +55,8 @@ export async function resolveCredentialValue(ctx, refName) {
54
55
  // Prefer the harness ref factory; fall back to the plain name when the
55
56
  // package is not installed (keeps tests and trimmed builds working).
56
57
  let ref = name;
57
- try {
58
- const mod = await import('@deepseek-ai/dsh-credentials');
59
- if (mod && typeof mod.credentialRef === 'function') ref = mod.credentialRef(name);
60
- } catch {}
58
+ const mod = await bestEffort('import-credentials', () => import('@deepseek-ai/dsh-credentials'));
59
+ if (mod && typeof mod.credentialRef === 'function') ref = mod.credentialRef(name);
61
60
  const hit = await creds.resolve(ref);
62
61
  if (hit && hit.value) return String(hit.value);
63
62
  }
@@ -70,7 +69,7 @@ export async function resolveCredentialValue(ctx, refName) {
70
69
 
71
70
  /** Read the best-effort Telegram defaults from the messenger-gateway file. */
72
71
  function readGatewayToken() {
73
- try {
72
+ return bestEffort('read-gateway-token', () => {
74
73
  const p = getDshSettingsPath();
75
74
  if (!fs.existsSync(p)) return null;
76
75
  const text = fs.readFileSync(p, 'utf-8');
@@ -82,8 +81,8 @@ function readGatewayToken() {
82
81
  const hit = /^[ \t]+botToken:[ \t]*(.+?)[ \t]*$/.exec(line);
83
82
  if (hit) return hit[1].replace(/^['"]|['"]$/g, '').trim() || null;
84
83
  }
85
- } catch {}
86
- return null;
84
+ return null;
85
+ }) || null;
87
86
  }
88
87
 
89
88
  /**
package/lib/store.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './best-effort.js';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { getDshDefaultTelegramCredentials } from './telegram.js';
@@ -68,7 +69,7 @@ export class TaskStore {
68
69
  const raw = fs.readFileSync(this.filePath, 'utf-8');
69
70
  loadData(raw);
70
71
  if (!fs.existsSync(bakPath)) {
71
- try { fs.copyFileSync(this.filePath, bakPath); } catch {}
72
+ bestEffort('backup-initial', () => fs.copyFileSync(this.filePath, bakPath));
72
73
  }
73
74
  } catch (err) {
74
75
  console.error('[dsh-cron] store read error (corrupted file):', err.message);
@@ -93,7 +94,7 @@ export class TaskStore {
93
94
  try {
94
95
  const rawBak = fs.readFileSync(bakPath, 'utf-8');
95
96
  loadData(rawBak);
96
- try { fs.copyFileSync(bakPath, this.filePath); } catch {}
97
+ bestEffort('restore-from-bak', () => fs.copyFileSync(bakPath, this.filePath));
97
98
  console.warn(`[dsh-cron] restored missing tasks store from ${bakPath}`);
98
99
  } catch (bakErr) {
99
100
  console.error('[dsh-cron] failed to restore tasks store from .bak:', bakErr.message);
@@ -119,9 +120,7 @@ export class TaskStore {
119
120
  const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
120
121
  fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
121
122
  fs.renameSync(tmp, this.filePath);
122
- try {
123
- fs.copyFileSync(this.filePath, `${this.filePath}.bak`);
124
- } catch {}
123
+ bestEffort('backup-on-save', () => fs.copyFileSync(this.filePath, `${this.filePath}.bak`));
125
124
  } catch (err) {
126
125
  console.error('[dsh-cron] store save error:', err.message);
127
126
  }
@@ -393,9 +392,7 @@ export class TaskStore {
393
392
  const archivePath = path.join(dir, 'tasks-history-archive.json');
394
393
  let archive = {};
395
394
  if (fs.existsSync(archivePath)) {
396
- try {
397
- archive = JSON.parse(fs.readFileSync(archivePath, 'utf-8')) || {};
398
- } catch {}
395
+ archive = bestEffort('parse-history-archive', () => JSON.parse(fs.readFileSync(archivePath, 'utf-8'))) || {};
399
396
  }
400
397
  if (!Array.isArray(archive[id])) archive[id] = [];
401
398
  archive[id].push(...overflowRuns);
package/lib/templates.js CHANGED
@@ -51,7 +51,7 @@ export function buildTemplateVars(task, runInfo = {}) {
51
51
  const error = runInfo.error || (status === 'error' || status === 'timeout' ? runInfo.output || '' : '');
52
52
  const usage = runInfo.usage || {};
53
53
  const tokens = (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadTokens || 0);
54
- return {
54
+ const vars = {
55
55
  title: task.title || 'Task',
56
56
  id: task.id || '',
57
57
  status,
@@ -65,6 +65,10 @@ export function buildTemplateVars(task, runInfo = {}) {
65
65
  model: runInfo.model || '',
66
66
  diagnosis: truncateText(runInfo.diagnosis || ''),
67
67
  };
68
+ for (const key of TEMPLATE_VARIABLES) {
69
+ if (vars[key] === undefined) vars[key] = '';
70
+ }
71
+ return vars;
68
72
  }
69
73
 
70
74
  /** Replace {var} placeholders; unknown placeholders are left untouched. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -45,8 +45,8 @@
45
45
  "client": {
46
46
  "platform": "web",
47
47
  "inject": [
48
- "locale",
49
- "slots"
48
+ "@deepseek-ai/dsh-client-locale",
49
+ "@deepseek-ai/dsh-client-ui-slots"
50
50
  ]
51
51
  }
52
52
  },