@goodandready/dsh-cron 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +143 -30
- package/lib/store.js +17 -12
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -25,6 +25,7 @@ const React = window.React || require('react');
|
|
|
25
25
|
const ICON_BELL = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
|
|
26
26
|
const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
|
|
27
27
|
const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
|
|
28
|
+
const ICON_EDIT = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
|
|
28
29
|
const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
|
|
29
30
|
|
|
30
31
|
const STYLES = `
|
|
@@ -73,7 +74,10 @@ const React = window.React || require('react');
|
|
|
73
74
|
.dsh-cron-tab { appearance: none; background: transparent; border: none; padding: 6px 14px; border-radius: 9999px; color: #9ca3af; font-size: 13.5px; cursor: pointer; }
|
|
74
75
|
.dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: #fff; font-weight: 500; }
|
|
75
76
|
.dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
|
|
76
|
-
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; }
|
|
77
|
+
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; cursor: pointer; transition: background 0.15s, border-color 0.15s; }
|
|
78
|
+
.dsh-cron-task-item:hover { background: var(--dsw-alias-bg-layer-4, #242424); border-color: #444; }
|
|
79
|
+
.dsh-cron-task-prompt-preview { font-size: 12.5px; color: #888; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
|
|
80
|
+
.dsh-cron-type-tag { display: inline-block; font-size: 10.5px; padding: 1px 6px; border-radius: 4px; text-transform: uppercase; font-weight: 600; margin-left: 8px; vertical-align: middle; }
|
|
77
81
|
.dsh-cron-task-item:hover { border-color: #444; }
|
|
78
82
|
.dsh-cron-task-status-btn { background: none; border: 1px solid #444; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: #888; cursor: pointer; flex-shrink: 0; }
|
|
79
83
|
.dsh-cron-task-status-btn[data-active="true"] { color: #10b981; border-color: #10b981; }
|
|
@@ -205,13 +209,17 @@ const React = window.React || require('react');
|
|
|
205
209
|
const [recs, setRecs] = React.useState([]);
|
|
206
210
|
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
|
207
211
|
|
|
208
|
-
// Manual modal state
|
|
212
|
+
// Manual / Edit modal state
|
|
209
213
|
const [manualModalOpen, setManualModalOpen] = React.useState(false);
|
|
214
|
+
const [formId, setFormId] = React.useState(null);
|
|
210
215
|
const [formTitle, setFormTitle] = React.useState('');
|
|
211
216
|
const [formSchedule, setFormSchedule] = React.useState('');
|
|
217
|
+
const [formType, setFormType] = React.useState('llm');
|
|
212
218
|
const [formPrompt, setFormPrompt] = React.useState('');
|
|
213
219
|
const [formProvider, setFormProvider] = React.useState('');
|
|
214
220
|
const [formModel, setFormModel] = React.useState('');
|
|
221
|
+
const [formDelivery, setFormDelivery] = React.useState('isolated');
|
|
222
|
+
const [selectedTaskMeta, setSelectedTaskMeta] = React.useState(null);
|
|
215
223
|
|
|
216
224
|
// DSH dialog state
|
|
217
225
|
const [dshModalOpen, setDshModalOpen] = React.useState(false);
|
|
@@ -287,6 +295,10 @@ const React = window.React || require('react');
|
|
|
287
295
|
|
|
288
296
|
const openManualModal = (preset) => {
|
|
289
297
|
setDropdownOpen(false);
|
|
298
|
+
setFormId(null);
|
|
299
|
+
setSelectedTaskMeta(null);
|
|
300
|
+
setFormType('llm');
|
|
301
|
+
setFormDelivery('isolated');
|
|
290
302
|
if (preset) {
|
|
291
303
|
setFormTitle(preset.title || '');
|
|
292
304
|
setFormSchedule(preset.schedule || '');
|
|
@@ -305,6 +317,22 @@ const React = window.React || require('react');
|
|
|
305
317
|
setManualModalOpen(true);
|
|
306
318
|
};
|
|
307
319
|
|
|
320
|
+
const openEditModal = (task) => {
|
|
321
|
+
setDropdownOpen(false);
|
|
322
|
+
setFormId(task.id);
|
|
323
|
+
setSelectedTaskMeta(task);
|
|
324
|
+
setFormTitle(task.title || '');
|
|
325
|
+
setFormSchedule(task.schedule || '');
|
|
326
|
+
setFormType(task.type || 'llm');
|
|
327
|
+
setFormPrompt(task.prompt || '');
|
|
328
|
+
setFormProvider(task.provider || '');
|
|
329
|
+
setFormModel(task.model || '');
|
|
330
|
+
setFormDelivery(task.delivery || 'isolated');
|
|
331
|
+
if (task.provider) loadModels(task.provider);
|
|
332
|
+
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
333
|
+
setManualModalOpen(true);
|
|
334
|
+
};
|
|
335
|
+
|
|
308
336
|
const handleProviderChange = (e) => {
|
|
309
337
|
const prov = e.target.value;
|
|
310
338
|
setFormProvider(prov);
|
|
@@ -315,19 +343,23 @@ const React = window.React || require('react');
|
|
|
315
343
|
const handleSaveManual = async (e) => {
|
|
316
344
|
e.preventDefault();
|
|
317
345
|
try {
|
|
346
|
+
const payload = {
|
|
347
|
+
id: formId || undefined,
|
|
348
|
+
title: formTitle,
|
|
349
|
+
schedule: formSchedule,
|
|
350
|
+
prompt: formPrompt,
|
|
351
|
+
type: formType,
|
|
352
|
+
delivery: formDelivery || 'isolated',
|
|
353
|
+
provider: formType === 'script' ? undefined : (formProvider || undefined),
|
|
354
|
+
model: formType === 'script' ? undefined : (formModel || undefined),
|
|
355
|
+
};
|
|
318
356
|
const res = await fetch('/dsh-cron/tasks', {
|
|
319
357
|
method: 'POST',
|
|
320
358
|
headers: { 'Content-Type': 'application/json' },
|
|
321
|
-
body: JSON.stringify(
|
|
322
|
-
title: formTitle,
|
|
323
|
-
schedule: formSchedule,
|
|
324
|
-
prompt: formPrompt,
|
|
325
|
-
provider: formProvider || undefined,
|
|
326
|
-
model: formModel || undefined,
|
|
327
|
-
})
|
|
359
|
+
body: JSON.stringify(payload)
|
|
328
360
|
});
|
|
329
361
|
const data = await res.json();
|
|
330
|
-
if (!data.ok) { alert(data.error || 'Ошибка'); return; }
|
|
362
|
+
if (!data.ok) { alert(data.error || 'Ошибка сохранения'); return; }
|
|
331
363
|
setManualModalOpen(false);
|
|
332
364
|
fetchTasks();
|
|
333
365
|
} catch (err) { alert(err.message); }
|
|
@@ -424,30 +456,57 @@ const React = window.React || require('react');
|
|
|
424
456
|
tasks.length === 0
|
|
425
457
|
? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
|
|
426
458
|
: tasks.map((task) =>
|
|
427
|
-
React.createElement('div', {
|
|
459
|
+
React.createElement('div', {
|
|
460
|
+
key: task.id,
|
|
461
|
+
className: 'dsh-cron-task-item',
|
|
462
|
+
onClick: () => openEditModal(task)
|
|
463
|
+
},
|
|
428
464
|
React.createElement('button', {
|
|
465
|
+
type: 'button',
|
|
429
466
|
className: 'dsh-cron-task-status-btn',
|
|
430
467
|
'data-active': task.status === 'active' ? 'true' : undefined,
|
|
431
468
|
title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
|
|
432
|
-
onClick: () => handleToggleTask(task.id)
|
|
469
|
+
onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
|
|
433
470
|
}, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
|
|
434
471
|
React.createElement('div', { className: 'dsh-cron-task-info' },
|
|
435
472
|
React.createElement('div', { className: 'dsh-cron-task-title' },
|
|
436
473
|
task.title,
|
|
474
|
+
React.createElement('span', {
|
|
475
|
+
className: 'dsh-cron-type-tag',
|
|
476
|
+
style: {
|
|
477
|
+
background: task.type === 'script' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(16, 185, 129, 0.15)',
|
|
478
|
+
color: task.type === 'script' ? '#60a5fa' : '#34d399'
|
|
479
|
+
}
|
|
480
|
+
}, task.type === 'script' ? 'Shell' : 'LLM'),
|
|
437
481
|
task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null
|
|
438
482
|
),
|
|
439
483
|
React.createElement('div', { className: 'dsh-cron-task-sched' },
|
|
440
484
|
task.scheduleText || task.schedule,
|
|
441
485
|
task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
|
|
442
|
-
)
|
|
486
|
+
),
|
|
487
|
+
task.prompt ? React.createElement('div', {
|
|
488
|
+
className: 'dsh-cron-task-prompt-preview',
|
|
489
|
+
title: task.prompt
|
|
490
|
+
}, task.prompt.slice(0, 140) + (task.prompt.length > 140 ? '…' : '')) : null
|
|
443
491
|
),
|
|
444
|
-
React.createElement('div', {
|
|
492
|
+
React.createElement('div', {
|
|
493
|
+
className: 'dsh-cron-task-actions',
|
|
494
|
+
onClick: (e) => e.stopPropagation()
|
|
495
|
+
},
|
|
445
496
|
React.createElement('button', {
|
|
497
|
+
type: 'button',
|
|
446
498
|
className: 'dsh-cron-icon-btn',
|
|
447
499
|
title: 'Запустить немедленно',
|
|
448
500
|
onClick: () => handleRunNow(task.id)
|
|
449
501
|
}, 'Запустить'),
|
|
450
502
|
React.createElement('button', {
|
|
503
|
+
type: 'button',
|
|
504
|
+
className: 'dsh-cron-icon-btn',
|
|
505
|
+
title: 'Редактировать / Подробнее',
|
|
506
|
+
onClick: () => openEditModal(task)
|
|
507
|
+
}, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_EDIT } })),
|
|
508
|
+
React.createElement('button', {
|
|
509
|
+
type: 'button',
|
|
451
510
|
className: 'dsh-cron-icon-btn',
|
|
452
511
|
title: 'Удалить задачу',
|
|
453
512
|
onClick: () => handleDelete(task.id)
|
|
@@ -481,30 +540,57 @@ const React = window.React || require('react');
|
|
|
481
540
|
)
|
|
482
541
|
),
|
|
483
542
|
|
|
484
|
-
// 1. Manual creation modal
|
|
543
|
+
// 1. Manual creation / Edit modal
|
|
485
544
|
manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
|
|
486
|
-
React.createElement('div', {
|
|
487
|
-
|
|
545
|
+
React.createElement('div', {
|
|
546
|
+
className: 'dsh-cron-modal',
|
|
547
|
+
style: { maxWidth: '640px' },
|
|
548
|
+
onClick: (e) => e.stopPropagation()
|
|
549
|
+
},
|
|
550
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
551
|
+
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? 'Редактирование задачи' : 'Новая запланированная задача'),
|
|
552
|
+
selectedTaskMeta && React.createElement('span', {
|
|
553
|
+
style: {
|
|
554
|
+
fontSize: '12px',
|
|
555
|
+
padding: '3px 8px',
|
|
556
|
+
borderRadius: '6px',
|
|
557
|
+
background: selectedTaskMeta.status === 'active' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
|
558
|
+
color: selectedTaskMeta.status === 'active' ? '#10b981' : '#ef4444'
|
|
559
|
+
}
|
|
560
|
+
}, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
|
|
561
|
+
),
|
|
488
562
|
React.createElement('form', { onSubmit: handleSaveManual },
|
|
489
563
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
490
564
|
React.createElement('label', null, 'Название задачи'),
|
|
491
565
|
React.createElement('input', {
|
|
492
566
|
required: true,
|
|
493
567
|
value: formTitle,
|
|
494
|
-
placeholder: 'Например:
|
|
568
|
+
placeholder: 'Например: dsh-tags-watch',
|
|
495
569
|
onChange: (e) => setFormTitle(e.target.value)
|
|
496
570
|
})
|
|
497
571
|
),
|
|
498
|
-
React.createElement('div', { className: 'dsh-cron-form-
|
|
499
|
-
React.createElement('
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
572
|
+
React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
|
|
573
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
574
|
+
React.createElement('label', null, 'Тип выполнения'),
|
|
575
|
+
React.createElement('select', {
|
|
576
|
+
value: formType,
|
|
577
|
+
onChange: (e) => setFormType(e.target.value)
|
|
578
|
+
},
|
|
579
|
+
React.createElement('option', { value: 'llm' }, 'LLM (Агент с нейросетью)'),
|
|
580
|
+
React.createElement('option', { value: 'script' }, 'NO-LLM (Shell-команда/Скрипт)')
|
|
581
|
+
)
|
|
582
|
+
),
|
|
583
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
584
|
+
React.createElement('label', null, 'Расписание (cron или интервал)'),
|
|
585
|
+
React.createElement('input', {
|
|
586
|
+
required: true,
|
|
587
|
+
value: formSchedule,
|
|
588
|
+
placeholder: '0 */6 * * * или every 2h',
|
|
589
|
+
onChange: (e) => setFormSchedule(e.target.value)
|
|
590
|
+
})
|
|
591
|
+
)
|
|
506
592
|
),
|
|
507
|
-
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
593
|
+
formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
|
|
508
594
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
509
595
|
React.createElement('label', null, 'Провайдер LLM'),
|
|
510
596
|
React.createElement('select', {
|
|
@@ -527,14 +613,41 @@ const React = window.React || require('react');
|
|
|
527
613
|
)
|
|
528
614
|
),
|
|
529
615
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
530
|
-
React.createElement('label', null, '
|
|
616
|
+
React.createElement('label', null, formType === 'script' ? 'Shell-команда или скрипт' : 'Инструкция / Промпт для агента'),
|
|
531
617
|
React.createElement('textarea', {
|
|
532
618
|
required: true,
|
|
619
|
+
rows: 9,
|
|
620
|
+
style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
|
|
533
621
|
value: formPrompt,
|
|
534
|
-
placeholder: 'Опишите, что именно агент должен сделать...',
|
|
622
|
+
placeholder: formType === 'script' ? 'curl -fsSL https://... || exit 1' : 'Опишите, что именно агент должен сделать...',
|
|
535
623
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
536
624
|
})
|
|
537
625
|
),
|
|
626
|
+
selectedTaskMeta && React.createElement('div', {
|
|
627
|
+
style: {
|
|
628
|
+
background: 'rgba(255,255,255,0.03)',
|
|
629
|
+
border: '1px solid #2e2e2e',
|
|
630
|
+
borderRadius: '8px',
|
|
631
|
+
padding: '10px 14px',
|
|
632
|
+
fontSize: '12.5px',
|
|
633
|
+
color: '#999',
|
|
634
|
+
marginBottom: '16px',
|
|
635
|
+
display: 'flex',
|
|
636
|
+
justifyContent: 'space-between',
|
|
637
|
+
alignItems: 'center'
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
React.createElement('div', null,
|
|
641
|
+
React.createElement('span', null, 'Следующий запуск: '),
|
|
642
|
+
React.createElement('strong', { style: { color: '#eee' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : 'не запланирован')
|
|
643
|
+
),
|
|
644
|
+
React.createElement('button', {
|
|
645
|
+
type: 'button',
|
|
646
|
+
className: 'dsh-cron-btn-secondary',
|
|
647
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
648
|
+
onClick: () => { handleRunNow(selectedTaskMeta.id); }
|
|
649
|
+
}, 'Запустить сейчас')
|
|
650
|
+
),
|
|
538
651
|
React.createElement('div', { className: 'dsh-cron-modal-foot' },
|
|
539
652
|
React.createElement('button', {
|
|
540
653
|
type: 'button',
|
|
@@ -544,7 +657,7 @@ const React = window.React || require('react');
|
|
|
544
657
|
React.createElement('button', {
|
|
545
658
|
type: 'submit',
|
|
546
659
|
className: 'dsh-cron-btn-primary'
|
|
547
|
-
}, 'Сохранить задачу')
|
|
660
|
+
}, formId ? 'Сохранить изменения' : 'Создать задачу')
|
|
548
661
|
)
|
|
549
662
|
)
|
|
550
663
|
)
|
package/lib/store.js
CHANGED
|
@@ -82,21 +82,26 @@ export class TaskStore {
|
|
|
82
82
|
set(task) {
|
|
83
83
|
const now = Date.now();
|
|
84
84
|
const id = task.id || ('cron_' + Math.random().toString(36).slice(2, 9));
|
|
85
|
+
const prev = this.tasks.get(id) || {};
|
|
85
86
|
const record = {
|
|
87
|
+
...prev,
|
|
88
|
+
...task,
|
|
86
89
|
id,
|
|
87
|
-
title: task.title || 'Новая задача',
|
|
88
|
-
schedule: task.schedule || '0 9 * * *',
|
|
89
|
-
scheduleText: task.scheduleText || task.schedule,
|
|
90
|
-
prompt: task.prompt || '',
|
|
91
|
-
type: task.type || '
|
|
92
|
-
status: task.status || 'active',
|
|
93
|
-
delivery: task.delivery || '
|
|
94
|
-
|
|
90
|
+
title: task.title !== undefined ? task.title : (prev.title || 'Новая задача'),
|
|
91
|
+
schedule: task.schedule !== undefined ? task.schedule : (prev.schedule || '0 9 * * *'),
|
|
92
|
+
scheduleText: task.scheduleText !== undefined ? task.scheduleText : (prev.scheduleText || task.schedule),
|
|
93
|
+
prompt: task.prompt !== undefined ? task.prompt : (prev.prompt || ''),
|
|
94
|
+
type: task.type !== undefined ? task.type : (prev.type || 'llm'),
|
|
95
|
+
status: task.status !== undefined ? task.status : (prev.status || 'active'),
|
|
96
|
+
delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
|
|
97
|
+
provider: task.provider !== undefined ? task.provider : prev.provider,
|
|
98
|
+
model: task.model !== undefined ? task.model : prev.model,
|
|
99
|
+
createdAt: prev.createdAt || task.createdAt || now,
|
|
95
100
|
updatedAt: now,
|
|
96
|
-
lastRunAt: task.lastRunAt || null,
|
|
97
|
-
lastStatus: task.lastStatus || null,
|
|
98
|
-
lastDurationMs: task.lastDurationMs || 0,
|
|
99
|
-
nextRunAt: task.nextRunAt || null,
|
|
101
|
+
lastRunAt: task.lastRunAt !== undefined ? task.lastRunAt : (prev.lastRunAt || null),
|
|
102
|
+
lastStatus: task.lastStatus !== undefined ? task.lastStatus : (prev.lastStatus || null),
|
|
103
|
+
lastDurationMs: task.lastDurationMs !== undefined ? task.lastDurationMs : (prev.lastDurationMs || 0),
|
|
104
|
+
nextRunAt: task.nextRunAt !== undefined ? task.nextRunAt : (prev.nextRunAt || null),
|
|
100
105
|
};
|
|
101
106
|
this.tasks.set(id, record);
|
|
102
107
|
this.save();
|