@goodandready/dsh-cron 0.3.25 → 0.3.26

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.
Files changed (82) hide show
  1. package/lib/api-actions.js +1 -317
  2. package/lib/api-collection.js +1 -250
  3. package/lib/api-helpers.js +1 -100
  4. package/lib/api-import-export.js +1 -155
  5. package/lib/api-item.js +1 -191
  6. package/lib/api-task-webhook.js +1 -247
  7. package/lib/api-webhook.js +1 -183
  8. package/lib/api.js +1 -178
  9. package/lib/artifacts.js +1 -200
  10. package/lib/chain-checkpoints.js +1 -223
  11. package/lib/client-src/00-open.js +62 -0
  12. package/lib/client-src/10-locales.js +484 -0
  13. package/lib/client-src/20-styles.js +64 -0
  14. package/lib/client-src/30-utils.js +248 -0
  15. package/lib/client-src/40-delivery-settings.js +96 -0
  16. package/lib/client-src/45-dag-graph-view.js +287 -0
  17. package/lib/client-src/46-live-logs-modal.js +108 -0
  18. package/lib/client-src/47-analytics-digest-modal.js +166 -0
  19. package/lib/client-src/48-timeline-heatmap-view.js +176 -0
  20. package/lib/client-src/50-cron-screen-state.js +447 -0
  21. package/lib/client-src/52-cron-screen-actions.js +330 -0
  22. package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
  23. package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
  24. package/lib/client-src/55-cron-screen-view.js +378 -0
  25. package/lib/client-src/60-modal-task-form-head.js +580 -0
  26. package/lib/client-src/62-modal-task-form-tail.js +466 -0
  27. package/lib/client-src/65-modal-dialogs.js +350 -0
  28. package/lib/client-src/70-settings-card.js +420 -0
  29. package/lib/client-src/80-sidebar-jobs.js +200 -0
  30. package/lib/client-src/90-close.js +200 -0
  31. package/lib/commands.js +200 -0
  32. package/lib/durable-journal.js +1 -188
  33. package/lib/external-api.js +1 -74
  34. package/lib/index.js +17 -0
  35. package/lib/metrics.js +1 -125
  36. package/lib/routes/api-actions.js +317 -0
  37. package/lib/routes/api-collection.js +250 -0
  38. package/lib/routes/api-helpers.js +100 -0
  39. package/lib/routes/api-import-export.js +155 -0
  40. package/lib/routes/api-item.js +191 -0
  41. package/lib/routes/api-task-webhook.js +247 -0
  42. package/lib/routes/api-webhook.js +183 -0
  43. package/lib/routes/api.js +178 -0
  44. package/lib/routes/external-api.js +74 -0
  45. package/lib/routes/index.js +11 -0
  46. package/lib/routes/metrics.js +125 -0
  47. package/lib/routes/routes.js +549 -0
  48. package/lib/routes.js +1 -549
  49. package/lib/runner-external.js +1 -352
  50. package/lib/runner-worktree.js +1 -90
  51. package/lib/runner.js +1 -881
  52. package/lib/runtimes/index.js +4 -0
  53. package/lib/runtimes/runner-external.js +352 -0
  54. package/lib/runtimes/runner-worktree.js +90 -0
  55. package/lib/runtimes/runner.js +881 -0
  56. package/lib/runtimes/runtimes.js +198 -0
  57. package/lib/runtimes.js +1 -198
  58. package/lib/scheduler/index.js +6 -0
  59. package/lib/scheduler/scheduler-assists.js +142 -0
  60. package/lib/scheduler/scheduler-completion.js +417 -0
  61. package/lib/scheduler/scheduler-execution.js +360 -0
  62. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  63. package/lib/scheduler/scheduler-preflight.js +195 -0
  64. package/lib/scheduler/scheduler.js +751 -0
  65. package/lib/scheduler-assists.js +1 -142
  66. package/lib/scheduler-completion.js +1 -417
  67. package/lib/scheduler-execution.js +1 -360
  68. package/lib/scheduler-lifecycle.js +1 -157
  69. package/lib/scheduler-preflight.js +1 -195
  70. package/lib/scheduler.js +1 -751
  71. package/lib/store/artifacts.js +200 -0
  72. package/lib/store/chain-checkpoints.js +223 -0
  73. package/lib/store/durable-journal.js +188 -0
  74. package/lib/store/index.js +6 -0
  75. package/lib/store/store.js +1179 -0
  76. package/lib/store/task-schema.js +180 -0
  77. package/lib/store/task-transfer.js +310 -0
  78. package/lib/store.js +2 -1179
  79. package/lib/task-schema.js +1 -180
  80. package/lib/task-transfer.js +1 -310
  81. package/lib/tui-settings.js +157 -0
  82. package/package.json +2 -1
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Analytics Digest & Schedule Timeline modal handlers (#330, #332, #333, GH #8)
3
+ */
4
+ const openAnalyticsDigest = async (days = 7) => {
5
+ setDigestOpen(true);
6
+ setDigestLoading(true);
7
+ setPeriodDays(days);
8
+ setSendStatus(null);
9
+ try {
10
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/analytics?days=' + encodeURIComponent(days));
11
+ if (data && data.ok) {
12
+ setDigestData(data.digest || null);
13
+ setDigestMarkdown(data.markdown || '');
14
+ }
15
+ } catch (err) {
16
+ console.debug('[dsh-cron] analytics digest fetch failed', err);
17
+ } finally {
18
+ setDigestLoading(false);
19
+ }
20
+ };
21
+
22
+ const onSelectPeriod = async (days) => {
23
+ setPeriodDays(days);
24
+ await openAnalyticsDigest(days);
25
+ };
26
+
27
+ const onSendTelegram = async () => {
28
+ setSendStatus('⏳ ' + (T_KEY(t, 'digest.sending') || 'Sending...'));
29
+ try {
30
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/analytics', {
31
+ method: 'POST',
32
+ body: JSON.stringify({ days: periodDays })
33
+ });
34
+ if (data && data.ok) {
35
+ setSendStatus('✅ ' + (T_KEY(t, 'digest.sent') || 'Sent!'));
36
+ } else {
37
+ setSendStatus('❌ ' + (data?.error || 'Failed'));
38
+ }
39
+ } catch (err) {
40
+ setSendStatus('❌ ' + (err.message || 'Error'));
41
+ }
42
+ };
43
+
44
+ const openTimelineModal = async (tab = 'timeline') => {
45
+ setTimelineOpen(true);
46
+ setTimelineLoading(true);
47
+ setTimelineTab(tab);
48
+ try {
49
+ const [timelineRes, heatmapRes] = await Promise.all([
50
+ fetchJsonWithTimeout('/dsh-cron/schedule/timeline-24h?hours=24'),
51
+ fetchJsonWithTimeout('/dsh-cron/analytics/heatmap')
52
+ ]);
53
+ if (timelineRes?.data?.ok && timelineRes?.data?.timeline) {
54
+ setTimelineData(timelineRes.data.timeline);
55
+ }
56
+ if (heatmapRes?.data?.ok && heatmapRes?.data?.heatmap) {
57
+ setHeatmapData(heatmapRes.data.heatmap);
58
+ }
59
+ } catch (err) {
60
+ console.debug('[dsh-cron] timeline fetch failed', err);
61
+ } finally {
62
+ setTimelineLoading(false);
63
+ }
64
+ };
65
+
66
+ const onSelectTimelineTab = (tab) => {
67
+ setTimelineTab(tab);
68
+ };
@@ -0,0 +1,378 @@
1
+ return React.createElement('div', { className: 'dsh-cron-overlay' },
2
+ React.createElement('div', { className: 'dsh-cron-container' },
3
+ React.createElement('div', { className: 'dsh-cron-top-bar' },
4
+ React.createElement('button', {
5
+ type: 'button',
6
+ className: 'dsh-cron-back-btn',
7
+ title: T_KEY(t, 'panel.back'),
8
+ onClick: () => { if (onClose) onClose(); else toggle.set(false); }
9
+ },
10
+ React.createElement('span', svgProps(BACK_ICON)),
11
+ React.createElement('span', null, T_KEY(t, 'panel.back'))
12
+ )
13
+ ),
14
+ React.createElement('div', { className: 'dsh-cron-header' },
15
+ React.createElement('div', null,
16
+ React.createElement('div', { className: 'dsh-cron-title' }, T_KEY(t, 'panel.title')),
17
+ React.createElement('div', { className: 'dsh-cron-subtitle' }, T_KEY(t, 'panel.subtitle'))
18
+ ),
19
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
20
+ React.createElement('button', {
21
+ type: 'button',
22
+ className: 'dsh-cron-btn-secondary',
23
+ style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 12px' },
24
+ title: T_KEY(t, 'timeline.buttonTitle'),
25
+ onClick: () => openTimelineModal('timeline')
26
+ },
27
+ React.createElement('span', { style: { fontSize: '14px' } }, '⏱'),
28
+ T_KEY(t, 'timeline.buttonLabel')
29
+ ),
30
+ React.createElement('button', {
31
+ type: 'button',
32
+ className: 'dsh-cron-btn-secondary',
33
+ style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 12px' },
34
+ title: T_KEY(t, 'digest.buttonTitle'),
35
+ onClick: openAnalyticsDigest
36
+ },
37
+ React.createElement('span', { style: { fontSize: '14px' } }, '📊'),
38
+ T_KEY(t, 'digest.buttonLabel')
39
+ ),
40
+ React.createElement('button', {
41
+ type: 'button',
42
+ className: 'dsh-cron-btn-secondary',
43
+ style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 14px' },
44
+ title: T_KEY(t, 'actions.settings'),
45
+ onClick: openSettingsModal
46
+ },
47
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
48
+ T_KEY(t, 'actions.settings')
49
+ ),
50
+ React.createElement('div', { style: { position: 'relative' } },
51
+ React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
52
+ T_KEY(t, 'actions.create'),
53
+ React.createElement('span', iconHtml(ICON_CHEVRON_DOWN))
54
+ ),
55
+ dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
56
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, T_KEY(t, 'actions.createWithDsh')),
57
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, T_KEY(t, 'actions.setupManually'))
58
+ )
59
+ ),
60
+ // #42: export/import as a small pair of icon buttons.
61
+ React.createElement('button', {
62
+ type: 'button',
63
+ className: 'dsh-cron-icon-btn',
64
+ title: T_KEY(t, 'transfer.exportTitle'),
65
+ 'aria-label': T_KEY(t, 'transfer.exportTitle'),
66
+ disabled: transferBusy,
67
+ onClick: handleExport
68
+ }, React.createElement('span', svgProps(ICON_DOWNLOAD))),
69
+ React.createElement('button', {
70
+ type: 'button',
71
+ className: 'dsh-cron-icon-btn',
72
+ title: T_KEY(t, 'transfer.importTitle'),
73
+ 'aria-label': T_KEY(t, 'transfer.importTitle'),
74
+ disabled: transferBusy,
75
+ onClick: () => { if (importFileRef.current) importFileRef.current.click(); }
76
+ }, React.createElement('span', svgProps(ICON_UPLOAD))),
77
+ React.createElement('input', {
78
+ type: 'file',
79
+ accept: '.json,application/json',
80
+ ref: importFileRef,
81
+ style: { display: 'none' },
82
+ onChange: handleImportFile
83
+ })
84
+ )
85
+ ),
86
+ React.createElement('div', { className: 'dsh-cron-search-bar' },
87
+ React.createElement('span', { className: 'dsh-cron-search-icon', ...iconHtml(ICON_SEARCH) }),
88
+ React.createElement('input', {
89
+ className: 'dsh-cron-search-input',
90
+ type: 'search',
91
+ 'aria-label': T_KEY(t, 'search.placeholder'),
92
+ placeholder: T_KEY(t, 'search.placeholder'),
93
+ value: query,
94
+ onChange: (e) => setQuery(e.target.value)
95
+ })
96
+ ),
97
+ stats && React.createElement('div', { className: 'dsh-cron-stats-bar' },
98
+ React.createElement('div', { className: 'dsh-cron-stat-card' },
99
+ React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.activeTasks || 0),
100
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.activeTasks'))
101
+ ),
102
+ React.createElement('div', { className: 'dsh-cron-stat-card' },
103
+ React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.totalRuns || 0),
104
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalRuns'))
105
+ ),
106
+ React.createElement('div', { className: 'dsh-cron-stat-card' },
107
+ React.createElement('div', { className: 'dsh-cron-stat-val' }, (stats.totalTokens || 0).toLocaleString()),
108
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalTokens'))
109
+ ),
110
+ React.createElement('div', { className: 'dsh-cron-stat-card' },
111
+ React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: 'var(--dsh-cron-success)' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
112
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalCost'))
113
+ )
114
+ ),
115
+ React.createElement('div', { className: 'dsh-cron-tabs' },
116
+ ['all', 'active', 'paused', 'completed'].map((tabName) =>
117
+ React.createElement('button', {
118
+ key: tabName,
119
+ className: 'dsh-cron-tab',
120
+ 'data-active': tab === tabName ? 'true' : undefined,
121
+ onClick: () => setTab(tabName)
122
+ },
123
+ tabName === 'all' ? T_KEY(t, 'tabs.all')
124
+ : tabName === 'active' ? T_KEY(t, 'tabs.active')
125
+ : tabName === 'paused' ? T_KEY(t, 'tabs.paused')
126
+ : T_KEY(t, 'tabs.completed'))
127
+ )
128
+ ),
129
+ // #40: facet filters over the loaded list.
130
+ React.createElement('div', { className: 'dsh-cron-filters' },
131
+ React.createElement('button', {
132
+ type: 'button',
133
+ className: 'dsh-cron-filter-toggle',
134
+ 'aria-expanded': filtersOpen ? 'true' : 'false',
135
+ onClick: () => setFiltersOpen(!filtersOpen)
136
+ },
137
+ T_KEY(t, 'filters.title'),
138
+ (filterType || filterModel || filterChannel) ? ' •' : ''
139
+ ),
140
+ filtersOpen && React.createElement('div', { className: 'dsh-cron-filter-panel' },
141
+ React.createElement('label', { className: 'dsh-cron-filter-field' },
142
+ React.createElement('span', null, T_KEY(t, 'filters.type')),
143
+ React.createElement('select', {
144
+ value: filterType,
145
+ onChange: (e) => setFilterType(e.target.value)
146
+ },
147
+ React.createElement('option', { value: '' }, T_KEY(t, 'filters.any')),
148
+ ...filterTypeOptions.map((value) => React.createElement('option', { key: value, value }, value))
149
+ )
150
+ ),
151
+ React.createElement('label', { className: 'dsh-cron-filter-field' },
152
+ React.createElement('span', null, T_KEY(t, 'filters.model')),
153
+ React.createElement('select', {
154
+ value: filterModel,
155
+ onChange: (e) => setFilterModel(e.target.value)
156
+ },
157
+ React.createElement('option', { value: '' }, T_KEY(t, 'filters.any')),
158
+ ...filterModelOptions.map((value) => React.createElement('option', { key: value, value }, value))
159
+ )
160
+ ),
161
+ React.createElement('label', { className: 'dsh-cron-filter-field' },
162
+ React.createElement('span', null, T_KEY(t, 'filters.channel')),
163
+ React.createElement('select', {
164
+ value: filterChannel,
165
+ onChange: (e) => setFilterChannel(e.target.value)
166
+ },
167
+ React.createElement('option', { value: '' }, T_KEY(t, 'filters.any')),
168
+ ...DELIVERY_CHANNELS.map((id) => React.createElement('option', { key: id, value: id }, T_KEY(t, CHANNEL_LABEL_KEYS[id])))
169
+ )
170
+ )
171
+ )
172
+ ),
173
+ fetchError ? React.createElement('div', {
174
+ className: 'dsh-cron-status-banner',
175
+ 'data-type': 'error',
176
+ style: { margin: '8px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
177
+ },
178
+ React.createElement('span', null, T_KEY(t, 'banner.loadError', { error: fetchError })),
179
+ React.createElement('button', {
180
+ type: 'button',
181
+ className: 'dsh-cron-btn-secondary',
182
+ style: { marginLeft: '8px', padding: '2px 8px', fontSize: '12px' },
183
+ onClick: () => fetchTasks()
184
+ }, T_KEY(t, 'banner.retry'))
185
+ ) : null,
186
+ (filterType || filterModel || filterChannel) ? React.createElement('div', {
187
+ className: 'dsh-cron-filter-summary',
188
+ style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary, #888)', marginBottom: '8px', display: 'flex', gap: '8px', alignItems: 'center' }
189
+ },
190
+ React.createElement('span', null, T_KEY(t, 'filters.showing', { shown: visibleTasks.length, total: tasks.length })),
191
+ React.createElement('button', {
192
+ type: 'button',
193
+ className: 'dsh-cron-btn-secondary',
194
+ style: { padding: '2px 8px', fontSize: '11px' },
195
+ onClick: resetFilters
196
+ }, T_KEY(t, 'filters.reset'))
197
+ ) : null,
198
+ React.createElement('div', { className: 'dsh-cron-task-list' },
199
+ loading && tasks.length === 0
200
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.loading'))
201
+ : tasks.length === 0
202
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #777)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.empty'))
203
+ : visibleTasks.length === 0
204
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #777)', padding: '16px 0', fontSize: '14px', display: 'flex', gap: '10px', alignItems: 'center' } },
205
+ React.createElement('span', null, T_KEY(t, 'filters.empty')),
206
+ React.createElement('button', {
207
+ type: 'button',
208
+ className: 'dsh-cron-btn-secondary',
209
+ style: { padding: '2px 8px', fontSize: '12px' },
210
+ onClick: resetFilters
211
+ }, T_KEY(t, 'filters.reset'))
212
+ )
213
+ : visibleTasks.map((task) =>
214
+ React.createElement('div', {
215
+ key: task.id,
216
+ className: 'dsh-cron-task-item',
217
+ 'data-task-id': task.id,
218
+ role: isConfigManaged(task) ? undefined : 'button',
219
+ tabIndex: isConfigManaged(task) ? undefined : 0,
220
+ 'aria-label': task.title,
221
+ style: isConfigManaged(task) ? { cursor: 'default' } : undefined,
222
+ onClick: isConfigManaged(task) ? undefined : () => openEditModal(task),
223
+ onKeyDown: isConfigManaged(task) ? undefined : (e) => {
224
+ if (e.key === 'Enter' || e.key === ' ') {
225
+ e.preventDefault();
226
+ openEditModal(task);
227
+ }
228
+ }
229
+ },
230
+ isConfigManaged(task) ? null : React.createElement('button', {
231
+ type: 'button',
232
+ className: 'dsh-cron-task-status-btn' + (task.running ? ' dsh-cron-pulse' : ''),
233
+ 'data-active': task.status === 'active' ? 'true' : undefined,
234
+ title: task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
235
+ 'aria-label': task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
236
+ onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
237
+ }, React.createElement('span', svgProps(task.status === 'active' ? ICON_PAUSE : ICON_PLAY))),
238
+ React.createElement('div', { className: 'dsh-cron-task-info' },
239
+ React.createElement('div', { className: 'dsh-cron-task-title' },
240
+ task.title,
241
+ React.createElement('span', {
242
+ className: 'dsh-cron-type-tag',
243
+ style: task.type === 'script'
244
+ ? { background: 'var(--dsh-cron-info-bg)', color: 'var(--dsh-cron-info)' }
245
+ : { background: 'var(--dsh-cron-success-bg)', color: 'var(--dsh-cron-success)' }
246
+ }, task.type === 'script' ? T_KEY(t, 'list.typeShell') : T_KEY(t, 'list.typeLlm')),
247
+ task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null,
248
+ task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, T_KEY(t, 'list.oneShot')) : null,
249
+ isConfigManaged(task) ? React.createElement('span', {
250
+ className: 'dsh-cron-config-tag',
251
+ title: T_KEY(t, 'list.configManagedHint')
252
+ }, T_KEY(t, 'list.configManaged')) : null,
253
+ task.lastDurationMs > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag', title: 'Last execution duration' }, task.lastDurationMs + 'ms') : null,
254
+ task.onSuccess ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-success', title: 'On success: triggers ' + task.onSuccess }, '➜ ' + task.onSuccess) : null,
255
+ task.onFailure ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-failure', title: 'On failure: triggers ' + task.onFailure }, '↳ ' + task.onFailure) : null,
256
+ (task.dependsOn && (Array.isArray(task.dependsOn) ? task.dependsOn.length > 0 : Boolean(task.dependsOn))) ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-heartbeat', title: 'Depends on: ' + (Array.isArray(task.dependsOn) ? task.dependsOn.join(', ') : task.dependsOn) }, '⛓️ ' + (Array.isArray(task.dependsOn) ? task.dependsOn.join(', ') : task.dependsOn)) : null,
257
+ task.adaptive?.enabled ? React.createElement('span', {
258
+ className: 'dsh-cron-type-tag' + (task.adaptiveState?.backedOff ? ' dsh-cron-tag-failure' : ' dsh-cron-tag-success'),
259
+ title: 'Adaptive: ' + (task.adaptiveState?.effectiveIntervalStr || task.adaptive.minInterval || '5m') + ' (idle: ' + (task.adaptiveState?.idleCount || 0) + ')'
260
+ }, (task.adaptiveState?.backedOff ? '⏳ ' : '⚡ ') + (task.adaptiveState?.effectiveIntervalStr || task.adaptive.minInterval || '5m')) : null,
261
+ task.heartbeatIntervalSeconds > 0 ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-heartbeat', title: 'Heartbeat: every ' + task.heartbeatIntervalSeconds + 's' }, '💓 ' + task.heartbeatIntervalSeconds + 's') : null,
262
+ task.targetSessionId ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-session', title: 'Target session: ' + task.targetSessionId + (task.targetSessionReset && task.targetSessionReset !== 'never' ? ' (' + task.targetSessionReset + ')' : '') }, '🧵 ' + task.targetSessionId) : null,
263
+ task.preflightType && task.preflightType !== 'none' ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-preflight', title: 'Preflight: ' + task.preflightType }, '🛡️ ' + task.preflightType) : null,
264
+ task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
265
+ task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null,
266
+ task.status === 'paused' && task.pausedReason ? React.createElement('span', { className: 'dsh-cron-type-tag dsh-cron-tag-failure', title: task.pausedReason }, '🛑 ' + task.pausedReason) : null
267
+ ),
268
+ React.createElement('div', { className: 'dsh-cron-task-sched' },
269
+ task.scheduleText || task.schedule,
270
+ task.nextRunAt ? T_KEY(t, 'list.next', { time: new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : '',
271
+ task.running && task.runningSince
272
+ ? ' · ' + T_KEY(t, 'list.runningNow') + ' ' + Math.max(0, Math.round((Date.now() - task.runningSince) / 1000)) + 's'
273
+ : ''
274
+ ),
275
+ task.prompt ? React.createElement('div', {
276
+ className: 'dsh-cron-task-prompt-preview',
277
+ title: task.prompt
278
+ }, task.prompt.slice(0, 140) + (task.prompt.length > 140 ? '…' : '')) : null
279
+ ),
280
+ React.createElement('div', {
281
+ className: 'dsh-cron-task-actions',
282
+ onClick: (e) => e.stopPropagation()
283
+ },
284
+ React.createElement('button', {
285
+ type: 'button',
286
+ className: 'dsh-cron-icon-btn',
287
+ title: T_KEY(t, 'actions.dryRunTitle'),
288
+ onClick: () => handleDryRun(task.id)
289
+ }, '🧪'),
290
+ React.createElement('button', {
291
+ type: 'button',
292
+ className: 'dsh-cron-icon-btn' + (task.running ? ' dsh-cron-pulse' : ''),
293
+ title: T_KEY(t, 'liveLog.viewLog'),
294
+ onClick: () => handleOpenLiveLogs(task)
295
+ }, '📺'),
296
+ React.createElement('button', {
297
+ type: 'button',
298
+ className: 'dsh-cron-icon-btn',
299
+ title: T_KEY(t, 'actions.archiveTitle'),
300
+ onClick: () => openArchiveModal(task, 0)
301
+ }, '📜'),
302
+ (task.onSuccess || task.onFailure || (task.dependsOn && (Array.isArray(task.dependsOn) ? task.dependsOn.length > 0 : Boolean(task.dependsOn)))) ? React.createElement('button', {
303
+ type: 'button',
304
+ className: 'dsh-cron-icon-btn',
305
+ title: T_KEY(t, 'chainGraph.viewGraph'),
306
+ onClick: () => openChainGraphModal(task)
307
+ }, '🕸️') : null,
308
+ React.createElement('button', {
309
+ type: 'button',
310
+ className: 'dsh-cron-icon-btn',
311
+ title: T_KEY(t, 'actions.runNow'),
312
+ onClick: () => handleRunNow(task.id)
313
+ }, T_KEY(t, 'actions.runNowShort')),
314
+ isConfigManaged(task) ? null : React.createElement('button', {
315
+ type: 'button',
316
+ className: 'dsh-cron-icon-btn',
317
+ title: T_KEY(t, 'actions.edit'),
318
+ 'aria-label': T_KEY(t, 'actions.edit'),
319
+ onClick: () => openEditModal(task)
320
+ }, React.createElement('span', svgProps(ICON_EDIT))),
321
+ isConfigManaged(task) ? null : React.createElement('button', {
322
+ type: 'button',
323
+ className: 'dsh-cron-icon-btn',
324
+ title: T_KEY(t, 'actions.tuneWithDsh'),
325
+ 'aria-label': T_KEY(t, 'actions.tuneWithDsh'),
326
+ onClick: () => handleTuneWithDsh(task)
327
+ }, React.createElement('span', svgProps(ICON_TUNE))),
328
+ isConfigManaged(task) ? null : React.createElement('button', {
329
+ type: 'button',
330
+ className: 'dsh-cron-icon-btn',
331
+ title: T_KEY(t, 'actions.duplicate'),
332
+ 'aria-label': T_KEY(t, 'actions.duplicate'),
333
+ onClick: () => handleDuplicate(task)
334
+ }, React.createElement('span', svgProps(ICON_COPY))),
335
+ isConfigManaged(task) ? null : React.createElement('button', {
336
+ type: 'button',
337
+ className: 'dsh-cron-icon-btn dsh-cron-danger',
338
+ title: T_KEY(t, 'actions.delete'),
339
+ 'aria-label': T_KEY(t, 'actions.delete'),
340
+ onClick: () => handleDelete(task.id)
341
+ }, React.createElement('span', svgProps(ICON_TRASH)))
342
+ )
343
+ )
344
+ )
345
+ ),
346
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, T_KEY(t, 'recs.title')),
347
+ React.createElement('div', { className: 'dsh-cron-hint', style: { marginBottom: '8px' } }, T_KEY(t, 'recs.hint')),
348
+ React.createElement('div', { className: 'dsh-cron-recs-list' },
349
+ recs.map((rec) =>
350
+ React.createElement('div', {
351
+ key: rec.id,
352
+ className: 'dsh-cron-rec-card',
353
+ role: 'button',
354
+ tabIndex: 0,
355
+ onClick: () => openManualModal(rec),
356
+ onKeyDown: (e) => {
357
+ if (e.key === 'Enter' || e.key === ' ') {
358
+ e.preventDefault();
359
+ openManualModal(rec);
360
+ }
361
+ }
362
+ },
363
+ React.createElement('div', { className: 'dsh-cron-rec-icon' },
364
+ React.createElement('span', iconHtml(rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY))
365
+ ),
366
+ React.createElement('div', { className: 'dsh-cron-rec-content' },
367
+ React.createElement('div', { className: 'dsh-cron-rec-title' },
368
+ rec.title,
369
+ React.createElement('span', { className: 'dsh-cron-rec-time' }, rec.scheduleText)
370
+ ),
371
+ React.createElement('div', { className: 'dsh-cron-rec-desc' }, rec.description)
372
+ )
373
+ )
374
+ )
375
+ )
376
+ ),
377
+
378
+ // 1. Manual creation / Edit modal