@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,350 @@
1
+ dryRunResult && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setDryRunResult(null) },
2
+ React.createElement('div', {
3
+ className: 'dsh-cron-modal',
4
+ style: { maxWidth: '580px' },
5
+ role: 'dialog',
6
+ 'aria-modal': 'true',
7
+ onClick: (e) => e.stopPropagation()
8
+ },
9
+ React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
10
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'dryRun.title')),
11
+ React.createElement('button', {
12
+ type: 'button',
13
+ className: 'dsh-cron-icon-btn',
14
+ onClick: () => setDryRunResult(null)
15
+ }, '✕')
16
+ ),
17
+ React.createElement('div', { style: { marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '13px' } },
18
+ React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.status') + ': '), React.createElement('span', { style: { color: dryRunResult.ok ? 'var(--dsh-cron-success)' : 'var(--dsh-cron-danger)' } }, dryRunResult.ok ? ((dryRunResult.result && dryRunResult.result.status) || dryRunResult.status || 'success') : 'failed')),
19
+ ((dryRunResult.result && dryRunResult.result.preflight) || dryRunResult.preflight) && React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.preflight') + ': '), React.createElement('span', null, (((dryRunResult.result && dryRunResult.result.preflight) || dryRunResult.preflight).passed) ? '✓ Passed' : '✗ Failed (' + ((((dryRunResult.result && dryRunResult.result.preflight) || dryRunResult.preflight).reason) || '') + ')')),
20
+ React.createElement('div', null, React.createElement('strong', null, T_KEY(t, 'dryRun.duration') + ': '), React.createElement('span', null, (((dryRunResult.result && dryRunResult.result.durationMs) !== undefined ? dryRunResult.result.durationMs : dryRunResult.durationMs) || 0) + 'ms')),
21
+ React.createElement('div', { style: { marginTop: '6px' } }, React.createElement('strong', null, T_KEY(t, 'dryRun.output') + ':')),
22
+ React.createElement('pre', {
23
+ style: {
24
+ background: 'var(--dsw-alias-bg-layer-2)',
25
+ border: '1px solid var(--dsw-alias-border-l2)',
26
+ borderRadius: '6px',
27
+ padding: '10px',
28
+ maxHeight: '220px',
29
+ overflowY: 'auto',
30
+ fontSize: '12px',
31
+ whiteSpace: 'pre-wrap',
32
+ wordBreak: 'break-word',
33
+ color: 'var(--dsw-alias-label-primary)'
34
+ }
35
+ }, ((dryRunResult.result && dryRunResult.result.output) !== undefined ? dryRunResult.result.output : (dryRunResult.output !== undefined ? dryRunResult.output : ((dryRunResult.result && dryRunResult.result.error) || dryRunResult.error || '(no output)'))))
36
+ ),
37
+ React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '16px' } },
38
+ React.createElement('button', {
39
+ type: 'button',
40
+ className: 'dsh-cron-btn-primary',
41
+ onClick: () => setDryRunResult(null)
42
+ }, T_KEY(t, 'dryRun.close'))
43
+ )
44
+ )
45
+ ),
46
+
47
+ // Archive runs modal
48
+ archiveModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setArchiveModalOpen(false) },
49
+ React.createElement('div', {
50
+ className: 'dsh-cron-modal',
51
+ style: { maxWidth: '700px' },
52
+ role: 'dialog',
53
+ 'aria-modal': 'true',
54
+ onClick: (e) => e.stopPropagation()
55
+ },
56
+ React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
57
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, T_KEY(t, 'archive.title') + (archiveTask ? ' - ' + archiveTask.title : '')),
58
+ React.createElement('button', {
59
+ type: 'button',
60
+ className: 'dsh-cron-icon-btn',
61
+ onClick: () => setArchiveModalOpen(false)
62
+ }, '✕')
63
+ ),
64
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #999)', margin: '4px 0 12px 0' } },
65
+ T_KEY(t, 'archive.totalRuns', { total: archiveTotal })
66
+ ),
67
+ archiveLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.loading')) :
68
+ archiveRuns.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'archive.empty')) :
69
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '380px', overflowY: 'auto' } },
70
+ archiveRuns.map((r, idx) => React.createElement('div', {
71
+ key: idx,
72
+ style: {
73
+ background: 'var(--dsw-alias-bg-layer-2)',
74
+ border: '1px solid var(--dsw-alias-border-l2)',
75
+ borderRadius: '6px',
76
+ padding: '8px 12px',
77
+ fontSize: '12px'
78
+ }
79
+ },
80
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '4px' } },
81
+ React.createElement('span', { style: { fontWeight: '600', color: r.status === 'success' ? 'var(--dsh-cron-success)' : 'var(--dsh-cron-danger)' } }, (r.status || 'unknown').toUpperCase()),
82
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #888)' } }, (r.at || r.timestamp || r.startedAt) ? new Date(r.at || r.timestamp || r.startedAt).toLocaleString() : "")
83
+ ),
84
+ r.durationMs > 0 && React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary, #888)', marginBottom: '4px' } }, 'Duration: ' + r.durationMs + 'ms'),
85
+ r.chainRunId && React.createElement('div', { style: { marginBottom: '4px' } },
86
+ React.createElement('button', {
87
+ type: 'button',
88
+ className: 'dsh-cron-btn-secondary',
89
+ style: { padding: '2px 8px', fontSize: '11px', display: 'inline-flex', alignItems: 'center', gap: '4px' },
90
+ onClick: () => openChainGraphModal(null, r.chainRunId)
91
+ }, '🕸️ ' + T_KEY(t, 'chainGraph.viewGraph'))
92
+ ),
93
+ r.error && React.createElement('div', { style: { color: 'var(--dsh-cron-danger)', marginBottom: '4px' } }, 'Error: ' + r.error),
94
+ r.output && React.createElement('div', {
95
+ style: {
96
+ color: 'var(--dsw-alias-label-primary, #ccc)',
97
+ whiteSpace: 'pre-wrap',
98
+ wordBreak: 'break-word',
99
+ maxHeight: '80px',
100
+ overflowY: 'auto'
101
+ }
102
+ }, r.output.slice(0, 300) + (r.output.length > 300 ? '...' : ''))
103
+ ))
104
+ ),
105
+ React.createElement('div', { className: 'dsh-cron-modal-foot', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '16px' } },
106
+ archiveRuns.length < archiveTotal ? React.createElement('button', {
107
+ type: 'button',
108
+ className: 'dsh-cron-btn-secondary',
109
+ onClick: () => {
110
+ const nextOffset = archiveRuns.length;
111
+ if (archiveTask) openArchiveModal(archiveTask, nextOffset);
112
+ }
113
+ }, T_KEY(t, 'archive.loadMore', { shown: archiveRuns.length, total: archiveTotal })) : React.createElement('div', null),
114
+ React.createElement('button', {
115
+ type: 'button',
116
+ className: 'dsh-cron-btn-primary',
117
+ onClick: () => setArchiveModalOpen(false)
118
+ }, T_KEY(t, 'archive.close'))
119
+ )
120
+ )
121
+ ),
122
+
123
+ // 2. Settings modal
124
+ settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
125
+ React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
126
+ React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
127
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
128
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
129
+ T_KEY(t, 'settings.modalTitle')
130
+ ),
131
+ React.createElement('button', {
132
+ type: 'button',
133
+ style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-tertiary, #888)', cursor: 'pointer', fontSize: '18px' },
134
+ onClick: () => setSettingsModalOpen(false)
135
+ }, '✕')
136
+ ),
137
+ React.createElement('form', { onSubmit: handleSaveSettings },
138
+ React.createElement('div', { className: 'dsh-cron-form-group' },
139
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
140
+ React.createElement('input', {
141
+ type: 'password',
142
+ value: settingsBotToken,
143
+ placeholder: settingsHasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
144
+ onChange: (e) => setSettingsBotToken(e.target.value)
145
+ }),
146
+ settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
147
+ ),
148
+ React.createElement('div', { className: 'dsh-cron-form-group' },
149
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
150
+ React.createElement('input', {
151
+ value: settingsChatId,
152
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
153
+ onChange: (e) => setSettingsChatId(e.target.value)
154
+ })
155
+ ),
156
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent))', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
157
+ React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
158
+ React.createElement('input', {
159
+ type: 'checkbox',
160
+ checked: settingsNotifyGlobal,
161
+ onChange: (e) => setSettingsNotifyGlobal(e.target.checked)
162
+ }),
163
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
164
+ ),
165
+ React.createElement('label', { className: 'dsh-cron-checkbox-row' },
166
+ React.createElement('input', {
167
+ type: 'checkbox',
168
+ checked: settingsOnlyOnFailureGlobal,
169
+ onChange: (e) => setSettingsOnlyOnFailureGlobal(e.target.checked)
170
+ }),
171
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
172
+ )
173
+ ),
174
+ ...renderDeliverySettings({
175
+ T: (key, vars) => T_KEY(t, key, vars),
176
+ values: settingsDelivery,
177
+ onChange: updateDelivery,
178
+ onChannelTemplate: updateChannelTemplate,
179
+ sections: settingsSections,
180
+ onToggleSection: toggleDeliverySection,
181
+ }),
182
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
183
+ React.createElement('button', {
184
+ type: 'button',
185
+ className: 'dsh-cron-btn-secondary',
186
+ style: { display: 'inline-flex', alignItems: 'center', gap: '6px' },
187
+ disabled: testStatus?.loading,
188
+ onClick: handleTestTelegram
189
+ }, testStatus?.loading ? T_KEY(t, 'settings.sending') : T_KEY(t, 'settings.testTelegram'))
190
+ ),
191
+ testStatus && testStatus.message && React.createElement('div', {
192
+ className: 'dsh-cron-status-banner',
193
+ 'data-type': testStatus.success ? 'success' : 'error'
194
+ }, testStatus.message),
195
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
196
+ React.createElement('button', {
197
+ type: 'button',
198
+ className: 'dsh-cron-btn-secondary',
199
+ onClick: () => setSettingsModalOpen(false)
200
+ }, T_KEY(t, 'modal.cancel')),
201
+ React.createElement('button', {
202
+ type: 'submit',
203
+ className: 'dsh-cron-btn-primary'
204
+ }, T_KEY(t, 'settings.save'))
205
+ )
206
+ )
207
+ )
208
+ ),
209
+
210
+ // 3. Import modal (#42) — summary first, then an explicit strategy.
211
+ importDoc && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !transferBusy && setImportDoc(null) },
212
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
213
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'transfer.importTitleModal')),
214
+ React.createElement('div', { className: 'dsh-cron-hint', style: { marginBottom: '10px' } }, T_KEY(t, 'transfer.importHint')),
215
+ importSummary && React.createElement('div', null,
216
+ React.createElement('div', { style: { fontSize: '13px', marginBottom: '12px' } },
217
+ T_KEY(t, 'transfer.summary', {
218
+ total: (importSummary.add || 0) + (importSummary.replace || 0) + (importSummary.skip || 0),
219
+ add: importSummary.add || 0,
220
+ replace: importSummary.replace || 0,
221
+ skip: importSummary.skip || 0
222
+ })
223
+ ),
224
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '14px' } },
225
+ ...['add', 'replace', 'skip'].map((mode) => React.createElement('label', { key: mode, className: 'dsh-cron-checkbox-row' },
226
+ React.createElement('input', {
227
+ type: 'radio',
228
+ name: 'dsh-cron-import-strategy',
229
+ checked: importStrategy === mode,
230
+ onChange: () => setImportStrategy(mode)
231
+ }),
232
+ React.createElement('span', null, T_KEY(t, mode === 'add' ? 'transfer.strategyAdd' : mode === 'replace' ? 'transfer.strategyReplace' : 'transfer.strategySkip'))
233
+ ))
234
+ )
235
+ ),
236
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
237
+ React.createElement('button', {
238
+ type: 'button',
239
+ className: 'dsh-cron-btn-secondary',
240
+ disabled: transferBusy,
241
+ onClick: () => setImportDoc(null)
242
+ }, T_KEY(t, 'modal.cancel')),
243
+ React.createElement('button', {
244
+ type: 'button',
245
+ className: 'dsh-cron-btn-primary',
246
+ disabled: transferBusy || !importSummary,
247
+ onClick: handleImportConfirm
248
+ }, T_KEY(t, 'transfer.confirm'))
249
+ )
250
+ )
251
+ ),
252
+
253
+ // 4. "Create with DSH" modal
254
+ dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
255
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
256
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'dsh.modalTitle')),
257
+ React.createElement('div', { className: 'dsh-cron-form-group' },
258
+ React.createElement('label', null, T_KEY(t, 'dsh.descLabel')),
259
+ React.createElement('div', { className: 'dsh-cron-chat-box' },
260
+ React.createElement('textarea', {
261
+ className: 'dsh-cron-chat-input',
262
+ rows: 4,
263
+ autoFocus: true,
264
+ disabled: dshLoading,
265
+ placeholder: T_KEY(t, 'dsh.placeholder'),
266
+ value: dshPrompt,
267
+ onKeyDown: (e) => {
268
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
269
+ e.preventDefault();
270
+ handleStartWithDSH();
271
+ }
272
+ },
273
+ onChange: (e) => setDshPrompt(e.target.value)
274
+ }),
275
+ React.createElement('div', { className: 'dsh-cron-chat-foot' },
276
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, T_KEY(t, 'dsh.sendHint')),
277
+ React.createElement('button', {
278
+ type: 'button',
279
+ className: 'dsh-cron-send-btn',
280
+ disabled: dshLoading || !dshPrompt.trim(),
281
+ title: T_KEY(t, 'dsh.sendTitle'),
282
+ onClick: handleStartWithDSH
283
+ },
284
+ React.createElement('span', svgProps(ICON_SEND))
285
+ )
286
+ )
287
+ )
288
+ ),
289
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
290
+ React.createElement('button', {
291
+ type: 'button',
292
+ className: 'dsh-cron-btn-secondary',
293
+ disabled: dshLoading,
294
+ onClick: () => setDshModalOpen(false)
295
+ }, T_KEY(t, 'modal.cancel'))
296
+ )
297
+ )
298
+ ),
299
+
300
+ // Visual Chain Graph & Checkpoint Inspector modal (#334)
301
+ renderChainGraphModal({
302
+ isOpen: chainGraphModalOpen,
303
+ onClose: () => setChainGraphModalOpen(false),
304
+ data: chainGraphData,
305
+ loading: chainGraphLoading,
306
+ onRetry: handleRetryChainStep,
307
+ t
308
+ }),
309
+
310
+ // Real-Time Streaming Logs Modal (#336)
311
+ renderLiveLogsModal({
312
+ isOpen: liveLogsModalOpen,
313
+ onClose: handleCloseLiveLogs,
314
+ taskId: liveLogsTaskId,
315
+ taskTitle: liveLogsTaskTitle,
316
+ logs: liveLogsLines,
317
+ autoScroll: liveLogsAutoScroll,
318
+ onToggleAutoScroll: () => setLiveLogsAutoScroll((v) => !v),
319
+ completed: liveLogsCompleted,
320
+ status: liveLogsStatus,
321
+ t
322
+ }),
323
+
324
+ // Weekly AI Analytics Digest Modal (#330, GH #8)
325
+ renderAnalyticsDigestModal({
326
+ isOpen: digestOpen,
327
+ onClose: () => setDigestOpen(false),
328
+ digestData,
329
+ isLoading: digestLoading,
330
+ periodDays,
331
+ onSelectPeriod,
332
+ onSendTelegram,
333
+ sendStatus,
334
+ t
335
+ }),
336
+
337
+ // 24-Hour Schedule Timeline & Cost Heatmap Modal (#332, #333, GH #8)
338
+ renderTimelineHeatmapModal({
339
+ isOpen: timelineOpen,
340
+ onClose: () => setTimelineOpen(false),
341
+ activeTab: timelineTab,
342
+ onSelectTab: onSelectTimelineTab,
343
+ timelineData,
344
+ heatmapData,
345
+ isLoading: timelineLoading,
346
+ t
347
+ })
348
+ );
349
+ }
350
+