@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.
- package/docs/design/DESIGN.md +4 -3
- package/lib/client.js +75 -28
- package/lib/index.js +38 -6
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -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`;
|
|
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` (совпадение с серверной регистрацией)
|
|
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.
|
|
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:
|
|
275
|
-
.dsh-cron-card-head {
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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' :
|
|
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
|
|
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
|
-
|
|
332
|
+
cronSettingsScope = sctx.settings.register('dsh-cron', Config, {
|
|
310
333
|
base: store.getSettings()
|
|
311
334
|
});
|
|
312
|
-
if (
|
|
313
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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' });
|