@goodandready/dsh-cron 0.2.0 → 0.2.1

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.
@@ -16,7 +16,7 @@
16
16
  - Поисковая строка; сводная статистика (активные задачи, запуски, токены, стоимость).
17
17
  - Карточки задач: статус-переключатель, название, расписание (человекочитаемое + raw cron), действия (запуск, редактирование, удаление).
18
18
  - Блок «Рекомендуемые задачи»: готовые шаблоны (Daily digest, Weekly review, Follow-up monitor) в один клик.
19
- - Карточка настроек в слоте settings.plugin.item, key = namespace `dsh-cron`; запасной путь — settings.section.
19
+ - Карточка настроек в слоте settings.plugin.item, key = namespace `dsh-cron`; отдельный раздел настроек не используется (#102).
20
20
  - LLM Tools: cron_create_task (+ alias cron_schedule_task), cron_list_tasks, cron_pause_task, cron_resume_task, cron_delete_task, cron_run_task.
21
21
  - API: HTTP эндпоинты /dsh-cron/* (tasks, models, chat/start, settings, telegram/test, kanban/test, tasks/:id/actions, legacy action/:id/:action). Мутирующие эндпоинты отклоняют cross-origin запросы; script-задачи по HTTP требуют заголовок x-dsh-cron-confirm.
22
22
  - Chat / Slash Commands: отсутствуют (ранее заявленные /cron-команды не были реализованы и удалены из документации; решение 2026-09-09).
@@ -46,7 +46,7 @@
46
46
  - SettingsModal: настройки Telegram/Kanban с тестами доставки.
47
47
  - TaskItem: строка задачи с переключателем состояния и действиями.
48
48
  - RecommendationCard: плашка с готовым шаблоном.
49
- - CronSettingsCard: карточка параметров плагина в настройках.
49
+ - CronSettingsCard: карточка параметров плагина в настройках; свёрнута по умолчанию, шапка-кнопка с шевроном разворачивает тело (aria-expanded), кнопка «Открыть панель задач» — внутри раскрытого тела (решение 2026-09-10, #100).
50
50
  - States:
51
51
  - Loading: индикатор загрузки списка/истории.
52
52
  - Empty: дружелюбный пустой экран со списком рекомендаций.
@@ -63,7 +63,8 @@
63
63
  ## Locked Design Decisions
64
64
  - 2026-09-09 — Пакет надёжности ядра (v0.1.24): буфер shell-задач 10МБ, атомарное сохранение с PID, аудит пропущенных запусков при рестарте (missed), фоновый поллинг UI (8с).
65
65
  - 2026-09-03 — Публичный скоуп @goodandready/dsh-cron; оверлей через mountSidebarEntry/mountScreen аналогично dsh-kanban; двойная кнопка «Создать ⌄».
66
- - 2026-09-09 — Слот карточки настроек: settings.plugin.item с key/namespace `dsh-cron` (совпадение с серверной регистрацией); settings.section — только запасной путь. Причина: контракт слота настроек (#85).
66
+ - 2026-09-09 — Слот карточки настроек: settings.plugin.item с key/namespace `dsh-cron` (совпадение с серверной регистрацией). Причина: контракт слота настроек (#85). Changed 2026-09-10 (#102): settings.section fallback удалён — карточка только во вкладке плагинов.
67
+ - 2026-09-10 — Значения настроек пишутся через зарегистрированный settings-скоуп (`scope.set` → `watch` → store); REST /dsh-cron/settings остаётся транспортом для карточки и headless-сценариев; карточка проверяет статус снапшота и не рисует поля до его получения (#102).
67
68
  - 2026-09-09 — Английский — канонический язык строк; словарь STRINGS.en регистрируется в ctx.locale; русский — через translation-плагин. Причина: стандарт DSH-плагинов (#87).
68
69
  - 2026-09-09 — Same-origin проверка мутирующих эндпоинтов, лимит тела 1 МБ, confirm-заголовок для script-задач. Причина: закрытие CSRF→RCE поверхности (#86).
69
70
  - 2026-09-09 — Стилевая изоляция: динамические <style> с data-dsh-plugin="dsh-cron"; цвета только через токены темы + единый блок plugin-переменных. Причина: защита от очистки стилей соседями и поддержка светлой темы (#91).
package/lib/client.js CHANGED
@@ -119,7 +119,9 @@ window.__ModuleLoader__.load({
119
119
  'form.notifyOnlyFailure': 'Only on failures (onlyOnFailure / silent on success)',
120
120
  'form.nextRunLabel': 'Next run: ',
121
121
  'form.notScheduled': 'not scheduled',
122
- 'settings.sectionLabel': 'Cron tasks',
122
+ 'settings.loading': 'Loading settings...',
123
+ 'settings.unavailable': 'Settings are unavailable right now.',
124
+ 'settings.retry': 'Retry',
123
125
  'settings.cardTitle': '⏰ Task scheduler (Cron)',
124
126
  'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram/Kanban integration.',
125
127
  'settings.openPanel': 'Open task panel',
@@ -170,6 +172,14 @@ window.__ModuleLoader__.load({
170
172
  };
171
173
  const T = (key, vars) => translate(key, vars);
172
174
 
175
+ // Core chevron primitive when the build ships it; own SVG fallback otherwise.
176
+ let CoreChevron = null;
177
+ try {
178
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
179
+ CoreChevron = primitives && primitives.IconChevronDownOutline14;
180
+ } catch (e) {}
181
+ const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
182
+
173
183
  // Status colors: core semantic tokens first, plugin fallbacks as a single
174
184
  // point of change (Issue #91 — no scattered hardcoded hex).
175
185
  const STYLES = `
@@ -271,9 +281,14 @@ window.__ModuleLoader__.load({
271
281
  .dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
272
282
  .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
273
283
 
274
- .dsh-cron-card { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }
275
- .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
284
+ .dsh-cron-card { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 12px; padding: 14px 20px; margin-bottom: 16px; }
285
+ .dsh-cron-card-head-btn { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 4px 0; }
286
+ .dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
276
287
  .dsh-cron-card-title { font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
288
+ .dsh-cron-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary, #888); transition: transform 0.16s; display: flex; align-items: center; }
289
+ .dsh-cron-chev-open { transform: rotate(180deg); }
290
+ .dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l1, #333); margin-top: 12px; padding-top: 14px; }
291
+ .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
277
292
  .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
278
293
 
279
294
  .dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-bottom: 18px; padding-bottom: 2px; }
@@ -1355,24 +1370,36 @@ window.__ModuleLoader__.load({
1355
1370
  const [hasDefault, setHasDefault] = React.useState(false);
1356
1371
  const [saved, setSaved] = React.useState(false);
1357
1372
  const [testInfo, setTestInfo] = React.useState(null);
1373
+ // Collapsed by default, head is the toggle (canonical card contract, #100)
1374
+ const [cardOpen, setCardOpen] = React.useState(false);
1375
+
1376
+ // Settings snapshot status (#102): never render phantom inputs before
1377
+ // the snapshot arrives; offer a retry when it is unavailable.
1378
+ const [loadState, setLoadState] = React.useState('loading');
1379
+ const [reloadTick, setReloadTick] = React.useState(0);
1358
1380
 
1359
1381
  React.useEffect(() => {
1360
1382
  let active = true;
1383
+ setLoadState('loading');
1361
1384
  fetch('/dsh-cron/settings')
1362
1385
  .then(r => r.json())
1363
1386
  .then(data => {
1364
- if (active && data && data.ok && data.settings) {
1387
+ if (!active) return;
1388
+ if (data && data.ok && data.settings) {
1365
1389
  setToken(data.settings.botToken || '');
1366
1390
  setChatId(data.settings.chatId || '');
1367
1391
  setNotify(Boolean(data.settings.notifyTelegram));
1368
1392
  setOnlyFailure(Boolean(data.settings.onlyOnFailure));
1369
1393
  setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
1370
1394
  setHasDefault(Boolean(data.settings.hasDefaultCredentials));
1395
+ setLoadState('ready');
1396
+ } else {
1397
+ setLoadState('error');
1371
1398
  }
1372
1399
  })
1373
- .catch(() => {});
1400
+ .catch(() => { if (active) setLoadState('error'); });
1374
1401
  return () => { active = false; };
1375
- }, []);
1402
+ }, [reloadTick]);
1376
1403
 
1377
1404
  const saveSettings = async (e) => {
1378
1405
  if (e) e.preventDefault();
@@ -1434,19 +1461,44 @@ window.__ModuleLoader__.load({
1434
1461
  }
1435
1462
  };
1436
1463
 
1464
+ if (loadState !== 'ready') {
1465
+ return React.createElement('div', { className: 'dsh-cron-card' },
1466
+ React.createElement('div', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1467
+ React.createElement('div', {
1468
+ className: 'dsh-cron-status-banner',
1469
+ 'data-type': loadState === 'error' ? 'error' : 'info',
1470
+ style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px' }
1471
+ },
1472
+ React.createElement('span', null, loadState === 'error' ? T_KEY(t, 'settings.unavailable') : T_KEY(t, 'settings.loading')),
1473
+ loadState === 'error' && React.createElement('button', {
1474
+ type: 'button',
1475
+ className: 'dsh-cron-btn-secondary',
1476
+ onClick: () => setReloadTick(x => x + 1)
1477
+ }, T_KEY(t, 'settings.retry'))
1478
+ )
1479
+ );
1480
+ }
1481
+
1437
1482
  return React.createElement('div', { className: 'dsh-cron-card' },
1438
- React.createElement('div', { className: 'dsh-cron-card-head' },
1483
+ React.createElement('button', {
1484
+ type: 'button',
1485
+ className: 'dsh-cron-card-head-btn',
1486
+ 'aria-expanded': cardOpen ? 'true' : 'false',
1487
+ onClick: () => setCardOpen(!cardOpen)
1488
+ },
1439
1489
  React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1440
- React.createElement('button', {
1441
- type: 'button',
1442
- className: 'dsh-cron-btn-primary',
1443
- onClick: () => { if (toggle) toggle.set(true); }
1444
- }, T_KEY(t, 'settings.openPanel'))
1445
- ),
1446
- React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
1447
- T_KEY(t, 'settings.cardDesc')
1490
+ React.createElement('span', { className: 'dsh-cron-chev' + (cardOpen ? ' dsh-cron-chev-open' : '') }, chevronNode())
1448
1491
  ),
1449
- React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1492
+ cardOpen && React.createElement('div', { className: 'dsh-cron-card-body' },
1493
+ React.createElement('div', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc')),
1494
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '12px' } },
1495
+ React.createElement('button', {
1496
+ type: 'button',
1497
+ className: 'dsh-cron-btn-primary',
1498
+ onClick: () => { if (toggle) toggle.set(true); }
1499
+ }, T_KEY(t, 'settings.openPanel'))
1500
+ ),
1501
+ React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1450
1502
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1451
1503
  React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1452
1504
  React.createElement('input', {
@@ -1516,7 +1568,8 @@ window.__ModuleLoader__.load({
1516
1568
  }, T_KEY(t, 'settings.testKanban'))
1517
1569
  )
1518
1570
  )
1519
- );
1571
+ )
1572
+ );
1520
1573
  }
1521
1574
 
1522
1575
  function nativeButton() {
@@ -1696,6 +1749,9 @@ window.__ModuleLoader__.load({
1696
1749
 
1697
1750
  const toggle = createToggle();
1698
1751
 
1752
+ // The settings card lives ONLY in the plugins settings tab (#102): no
1753
+ // top-level section fallback — the flat sidebar list is a shared core
1754
+ // resource, and the card slot is proven on current builds (#85).
1699
1755
  const cardOk = registerIntoMount(ctx, 'settings.plugin.item', {
1700
1756
  name: 'settings.plugin.item',
1701
1757
  key: NS,
@@ -1703,16 +1759,7 @@ window.__ModuleLoader__.load({
1703
1759
  inject: () => ({ ctx, toggle }),
1704
1760
  }, CronSettingsCard);
1705
1761
  if (!cardOk) {
1706
- // Fallback for builds without the settings.plugin.item mount: a
1707
- // dedicated section so the settings are never lost (#85).
1708
- registerIntoMount(ctx, 'settings.section', {
1709
- name: 'settings.section',
1710
- id: PLUGIN_ID,
1711
- order: 34,
1712
- locale: NS,
1713
- label: () => translate('settings.sectionLabel'),
1714
- inject: () => ({ ctx, toggle }),
1715
- }, CronSettingsCard);
1762
+ console.warn('[dsh-cron] settings.plugin.item slot unavailable — settings card not registered');
1716
1763
  }
1717
1764
 
1718
1765
  const chipOk = registerIntoMount(ctx, 'conversation.session.header.utilities', {
@@ -1722,7 +1769,7 @@ window.__ModuleLoader__.load({
1722
1769
  locale: NS,
1723
1770
  inject: () => ({ ctx, toggle }),
1724
1771
  }, CronChip);
1725
- const cardSlot = cardOk ? 'settings.plugin.item' : 'settings.section';
1772
+ const cardSlot = cardOk ? 'settings.plugin.item' : undefined;
1726
1773
  const chipSlot = chipOk ? 'conversation.session.header.utilities' : undefined;
1727
1774
 
1728
1775
  function CronChip(props) {
package/lib/index.js CHANGED
@@ -66,6 +66,25 @@ async function readBody(req, res) {
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * Apply settings writes through the registered settings scope so the core
71
+ * snapshot stays authoritative (#102). Returns null when the settings
72
+ * service is unavailable (headless profiles fall back to store-only writes).
73
+ */
74
+ async function applySettingsToScope(settingsScope, body) {
75
+ if (!settingsScope || typeof settingsScope.set !== 'function') return null;
76
+ const errors = [];
77
+ for (const key of ['botToken', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl']) {
78
+ if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
79
+ try {
80
+ await settingsScope.set(key, body[key]);
81
+ } catch (e) {
82
+ errors.push(key + ': ' + ((e && e.message) || e));
83
+ }
84
+ }
85
+ return errors;
86
+ }
87
+
69
88
  /**
70
89
  * Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
71
90
  * route families — the #66 alias is kept alive by the same code path instead
@@ -303,14 +322,18 @@ export function apply(ctx, config) {
303
322
  const runner = new SessionRunner(ctx);
304
323
  const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts));
305
324
 
306
- // Register settings in Harness settings service with Config schema and initial state from store (#71)
325
+ // Register settings in the Harness settings service (#71, #102): the scope
326
+ // is the source of truth for the core, scope.watch mirrors values into the
327
+ // plugin store, and REST writes go through scope.set so nothing bypasses
328
+ // the settings service.
329
+ let cronSettingsScope = null;
307
330
  ctx.inject(['settings'], (sctx) => {
308
331
  try {
309
- const scope = sctx.settings?.register?.('dsh-cron', Config, {
332
+ cronSettingsScope = sctx.settings.register('dsh-cron', Config, {
310
333
  base: store.getSettings()
311
334
  });
312
- if (scope && typeof scope.watch === 'function') {
313
- scope.watch((updated) => {
335
+ if (cronSettingsScope && typeof cronSettingsScope.watch === 'function') {
336
+ cronSettingsScope.watch((updated) => {
314
337
  if (updated && typeof updated === 'object') {
315
338
  store.saveSettings(updated);
316
339
  }
@@ -407,8 +430,17 @@ export function apply(ctx, config) {
407
430
  if (rejectCrossOrigin(req, res)) return;
408
431
  const { body, error } = await readBody(req, res);
409
432
  if (error) return;
410
- store.saveSettings(body);
411
- sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
433
+ // Masked secrets echoed back by the client must never overwrite the
434
+ // stored token (store.saveSettings guards this too, #86-era).
435
+ const payload = Object.assign({}, body);
436
+ if (typeof payload.botToken === 'string' && (payload.botToken.includes('••') || payload.botToken.includes('****'))) {
437
+ delete payload.botToken;
438
+ }
439
+ const scopeErrors = await applySettingsToScope(cronSettingsScope, payload);
440
+ if (scopeErrors === null) {
441
+ store.saveSettings(payload);
442
+ }
443
+ sendJson(res, 200, { ok: true, settings: store.getClientSettings(), scopeErrors: scopeErrors || undefined });
412
444
  return;
413
445
  }
414
446
  sendJson(res, 405, { ok: false, error: 'Method not allowed' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",