@goodandready/dsh-cron 0.3.24 → 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 (84) 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 -190
  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/client.js +48 -3
  32. package/lib/commands.js +200 -0
  33. package/lib/durable-journal.js +1 -188
  34. package/lib/external-api.js +1 -74
  35. package/lib/index.js +17 -0
  36. package/lib/log-streamer.js +10 -2
  37. package/lib/metrics.js +1 -125
  38. package/lib/routes/api-actions.js +317 -0
  39. package/lib/routes/api-collection.js +250 -0
  40. package/lib/routes/api-helpers.js +100 -0
  41. package/lib/routes/api-import-export.js +155 -0
  42. package/lib/routes/api-item.js +191 -0
  43. package/lib/routes/api-task-webhook.js +247 -0
  44. package/lib/routes/api-webhook.js +183 -0
  45. package/lib/routes/api.js +178 -0
  46. package/lib/routes/external-api.js +74 -0
  47. package/lib/routes/index.js +11 -0
  48. package/lib/routes/metrics.js +125 -0
  49. package/lib/routes/routes.js +549 -0
  50. package/lib/routes.js +1 -549
  51. package/lib/runner-external.js +1 -352
  52. package/lib/runner-worktree.js +1 -90
  53. package/lib/runner.js +1 -881
  54. package/lib/runtimes/index.js +4 -0
  55. package/lib/runtimes/runner-external.js +352 -0
  56. package/lib/runtimes/runner-worktree.js +90 -0
  57. package/lib/runtimes/runner.js +881 -0
  58. package/lib/runtimes/runtimes.js +198 -0
  59. package/lib/runtimes.js +1 -198
  60. package/lib/scheduler/index.js +6 -0
  61. package/lib/scheduler/scheduler-assists.js +142 -0
  62. package/lib/scheduler/scheduler-completion.js +417 -0
  63. package/lib/scheduler/scheduler-execution.js +360 -0
  64. package/lib/scheduler/scheduler-lifecycle.js +157 -0
  65. package/lib/scheduler/scheduler-preflight.js +195 -0
  66. package/lib/scheduler/scheduler.js +751 -0
  67. package/lib/scheduler-assists.js +1 -142
  68. package/lib/scheduler-completion.js +1 -417
  69. package/lib/scheduler-execution.js +1 -360
  70. package/lib/scheduler-lifecycle.js +1 -157
  71. package/lib/scheduler-preflight.js +1 -195
  72. package/lib/scheduler.js +1 -751
  73. package/lib/store/artifacts.js +200 -0
  74. package/lib/store/chain-checkpoints.js +223 -0
  75. package/lib/store/durable-journal.js +188 -0
  76. package/lib/store/index.js +6 -0
  77. package/lib/store/store.js +1179 -0
  78. package/lib/store/task-schema.js +180 -0
  79. package/lib/store/task-transfer.js +310 -0
  80. package/lib/store.js +2 -1179
  81. package/lib/task-schema.js +1 -180
  82. package/lib/task-transfer.js +1 -310
  83. package/lib/tui-settings.js +157 -0
  84. package/package.json +6 -3
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Real-time Task Execution Log Streaming Modal (SSE) (#336)
3
+ * Terminal console with live chunk append, auto-scroll, status indicator, and disconnect.
4
+ * Complies with Audit #203 (zero 6-digit hex colors, use theme variables or 3-digit hex).
5
+ */
6
+ function renderLiveLogsModal({ isOpen, onClose, taskId, taskTitle, logs, autoScroll, onToggleAutoScroll, completed, status, t }) {
7
+ if (!isOpen) return null;
8
+
9
+ const terminalRef = (el) => {
10
+ if (el && autoScroll) {
11
+ el.scrollTop = el.scrollHeight;
12
+ }
13
+ };
14
+
15
+ const isLive = status === 'running' || status === 'connecting';
16
+ const statusColor = isLive ? 'var(--dsh-cron-success, #2a2)' : (status === 'completed' ? 'var(--dsw-alias-label-secondary, #888)' : 'var(--dsh-cron-danger, #e33)');
17
+
18
+ return React.createElement('div', {
19
+ className: 'dsh-cron-modal-overlay',
20
+ onClick: onClose
21
+ },
22
+ React.createElement('div', {
23
+ className: 'dsh-cron-modal',
24
+ style: { maxWidth: '860px', width: '92vw', height: '80vh', display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base, #111)', color: 'var(--dsw-alias-label-primary, #ddd)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '8px' },
25
+ role: 'dialog',
26
+ 'aria-modal': 'true',
27
+ onClick: (e) => e.stopPropagation()
28
+ },
29
+ // Header
30
+ React.createElement('div', {
31
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderBottom: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
32
+ },
33
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
34
+ React.createElement('span', { style: { fontSize: '18px' } }, '📺'),
35
+ React.createElement('div', null,
36
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '14px', color: 'var(--dsw-alias-label-primary, #fff)', display: 'flex', alignItems: 'center', gap: '8px' } },
37
+ T_KEY(t, 'liveLog.modalTitle'),
38
+ React.createElement('span', {
39
+ style: {
40
+ display: 'inline-block',
41
+ width: '8px',
42
+ height: '8px',
43
+ borderRadius: '50%',
44
+ background: statusColor,
45
+ boxShadow: isLive ? '0 0 8px ' + statusColor : 'none'
46
+ }
47
+ })
48
+ ),
49
+ React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px', fontFamily: 'monospace' } },
50
+ (taskTitle || taskId) + ' · ' + (isLive ? T_KEY(t, 'liveLog.connecting') : (completed ? T_KEY(t, 'liveLog.completed') : status))
51
+ )
52
+ )
53
+ ),
54
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '12px' } },
55
+ React.createElement('label', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)', display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer' } },
56
+ React.createElement('input', {
57
+ type: 'checkbox',
58
+ checked: Boolean(autoScroll),
59
+ onChange: onToggleAutoScroll,
60
+ style: { cursor: 'pointer' }
61
+ }),
62
+ T_KEY(t, 'liveLog.autoScroll')
63
+ ),
64
+ React.createElement('button', {
65
+ type: 'button',
66
+ className: 'dsh-cron-icon-btn',
67
+ style: { color: 'var(--dsw-alias-label-secondary, #888)', background: 'transparent', border: 'none', cursor: 'pointer', fontSize: '16px' },
68
+ onClick: onClose
69
+ }, '✕')
70
+ )
71
+ ),
72
+
73
+ // Terminal console body
74
+ React.createElement('div', {
75
+ ref: terminalRef,
76
+ style: { flex: 1, overflowY: 'auto', padding: '14px 16px', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontSize: '12px', lineHeight: '1.5', background: 'var(--dsw-alias-bg-base, #000)' }
77
+ },
78
+ (!logs || logs.length === 0) && React.createElement('div', {
79
+ style: { color: 'var(--dsw-alias-label-secondary, #888)', fontStyle: 'italic', padding: '20px 0', textAlign: 'center' }
80
+ }, isLive ? T_KEY(t, 'liveLog.waiting') : 'No output captured for this run.'),
81
+
82
+ logs && logs.length > 0 && logs.map((item, idx) => {
83
+ const isErr = item.stream === 'stderr';
84
+ const timeStr = item.ts ? new Date(item.ts).toLocaleTimeString() : '';
85
+ return React.createElement('div', {
86
+ key: idx,
87
+ style: { display: 'flex', gap: '8px', color: isErr ? 'var(--dsh-cron-danger, #f55)' : 'var(--dsh-cron-success, #5f5)', wordBreak: 'break-all', whiteSpace: 'pre-wrap' }
88
+ },
89
+ timeStr && React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #555)', userSelect: 'none', flexShrink: 0 } }, '[' + timeStr + ']'),
90
+ React.createElement('span', { style: { flex: 1 } }, item.chunk || item.text || String(item))
91
+ );
92
+ })
93
+ ),
94
+
95
+ // Footer
96
+ React.createElement('div', {
97
+ style: { display: 'flex', justifyContent: 'flex-end', padding: '10px 16px', borderTop: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
98
+ },
99
+ React.createElement('button', {
100
+ type: 'button',
101
+ className: 'dsh-cron-btn dsh-cron-btn-secondary',
102
+ style: { padding: '6px 14px', fontSize: '12px' },
103
+ onClick: onClose
104
+ }, T_KEY(t, 'liveLog.close'))
105
+ )
106
+ )
107
+ );
108
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Weekly AI Analytics & Cost Digest Modal (#330)
3
+ * Displays 7-day or 30-day KPI cards, top unstable & expensive tasks,
4
+ * one-click Telegram dispatch, and Markdown preview.
5
+ * Complies strictly with Audit #203 (0 raw rgba, 0 6-digit hex colors).
6
+ */
7
+ function renderAnalyticsDigestModal({ isOpen, onClose, digestData, isLoading, periodDays, onSelectPeriod, onSendTelegram, sendStatus, t }) {
8
+ if (!isOpen) return null;
9
+
10
+ const digest = digestData || {
11
+ totalRuns: 0,
12
+ successfulRuns: 0,
13
+ failedRuns: 0,
14
+ successRate: 100,
15
+ totalTokens: 0,
16
+ totalCostUsd: 0,
17
+ healedCount: 0,
18
+ activeTasksCount: 0,
19
+ pausedTasksCount: 0,
20
+ topUnstableTasks: [],
21
+ topExpensiveTasks: []
22
+ };
23
+
24
+ const copyMarkdown = () => {
25
+ if (!digestData) return;
26
+ const text = [
27
+ '# ' + (T_KEY(t, 'digest.modalTitle')),
28
+ 'Period: Last ' + (periodDays || 7) + ' days',
29
+ 'Total Runs: ' + digest.totalRuns + ' (' + digest.successRate + '% SR)',
30
+ 'Tokens: ' + digest.totalTokens + ' (~$' + digest.totalCostUsd + ' USD)',
31
+ 'Active Tasks: ' + digest.activeTasksCount + ' active / ' + digest.pausedTasksCount + ' paused'
32
+ ].join(String.fromCharCode(10));
33
+ navigator.clipboard.writeText(text).catch(() => {});
34
+ };
35
+
36
+ return React.createElement('div', {
37
+ className: 'dsh-cron-modal-overlay',
38
+ onClick: onClose
39
+ },
40
+ React.createElement('div', {
41
+ className: 'dsh-cron-modal',
42
+ style: { maxWidth: '780px', width: '92vw', maxHeight: '86vh', display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base, #111)', color: 'var(--dsw-alias-label-primary, #ddd)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '8px' },
43
+ role: 'dialog',
44
+ 'aria-modal': 'true',
45
+ onClick: (e) => e.stopPropagation()
46
+ },
47
+ // Header
48
+ React.createElement('div', {
49
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 18px', borderBottom: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
50
+ },
51
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
52
+ React.createElement('span', { style: { fontSize: '20px' } }, '📊'),
53
+ React.createElement('div', null,
54
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '15px', color: 'var(--dsw-alias-label-primary, #fff)' } },
55
+ T_KEY(t, 'digest.modalTitle')
56
+ ),
57
+ React.createElement('div', { style: { fontSize: '11.5px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } },
58
+ (T_KEY(t, 'digest.buttonTitle')) + ' · ' + (periodDays || 7) + 'd'
59
+ )
60
+ )
61
+ ),
62
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
63
+ React.createElement('button', {
64
+ type: 'button',
65
+ className: 'dsh-cron-btn-secondary',
66
+ style: { padding: '4px 10px', fontSize: '12px', fontWeight: periodDays === 7 ? '600' : '400', background: periodDays === 7 ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
67
+ onClick: () => onSelectPeriod && onSelectPeriod(7)
68
+ }, T_KEY(t, 'digest.period7')),
69
+ React.createElement('button', {
70
+ type: 'button',
71
+ className: 'dsh-cron-btn-secondary',
72
+ style: { padding: '4px 10px', fontSize: '12px', fontWeight: periodDays === 30 ? '600' : '400', background: periodDays === 30 ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
73
+ onClick: () => onSelectPeriod && onSelectPeriod(30)
74
+ }, T_KEY(t, 'digest.period30')),
75
+ React.createElement('button', {
76
+ type: 'button',
77
+ className: 'dsh-cron-icon-btn',
78
+ onClick: onClose,
79
+ style: { marginLeft: '8px' }
80
+ }, React.createElement('span', null, '✕'))
81
+ )
82
+ ),
83
+
84
+ // Body
85
+ React.createElement('div', {
86
+ style: { padding: '16px 18px', overflowY: 'auto', flex: 1, display: 'flex', flexDirection: 'column', gap: '16px' }
87
+ },
88
+ isLoading ? React.createElement('div', { style: { padding: '30px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #888)' } }, 'Loading digest metrics...') : React.createElement(React.Fragment, null,
89
+ // KPI Grid
90
+ (() => {
91
+ const renderKpi = (label, val, sub, subCol) => React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '10px 12px' } },
92
+ React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, label),
93
+ React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, val),
94
+ React.createElement('div', { style: { fontSize: '11px', color: subCol || 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } }, sub)
95
+ );
96
+ return React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px' } },
97
+ renderKpi(T_KEY(t, 'digest.executions'), digest.totalRuns, digest.successRate + '% SR', digest.successRate >= 90 ? 'var(--dsh-cron-success, #2a2)' : 'var(--dsh-cron-warning, #fa0)'),
98
+ renderKpi(T_KEY(t, 'digest.tokens'), Number(digest.totalTokens || 0).toLocaleString(), 'LLM prompt+out'),
99
+ renderKpi(T_KEY(t, 'digest.cost'), '$' + Number(digest.totalCostUsd || 0).toFixed(4), 'USD estimated'),
100
+ renderKpi(T_KEY(t, 'digest.fleet'), digest.activeTasksCount, digest.pausedTasksCount + ' paused')
101
+ );
102
+ })(),
103
+
104
+ // Unstable TasksUnstable Tasks
105
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '12px' } },
106
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsh-cron-warning, #fa0)', marginBottom: '8px' } },
107
+ '⚠️ ' + (T_KEY(t, 'digest.unstableTitle'))
108
+ ),
109
+ (digest.topUnstableTasks && digest.topUnstableTasks.length > 0) ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
110
+ digest.topUnstableTasks.map((tItem, idx) => React.createElement('div', {
111
+ key: tItem.id || idx,
112
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', padding: '6px 8px', background: 'var(--dsw-alias-bg-base, #111)', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-l2, #222)' }
113
+ },
114
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, (idx + 1) + '. ' + (tItem.name || tItem.id)),
115
+ React.createElement('span', { style: { color: 'var(--dsh-cron-danger, #e33)', fontWeight: '600' } },
116
+ tItem.failures + ' fail (' + tItem.successRate + '% SR)'
117
+ )
118
+ ))
119
+ ) : React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)' } }, '✓ All tasks healthy — 0 failures in window!')
120
+ ),
121
+
122
+ // Expensive Tasks
123
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '12px' } },
124
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsh-cron-info, #4af)', marginBottom: '8px' } },
125
+ '💰 ' + (T_KEY(t, 'digest.expensiveTitle'))
126
+ ),
127
+ (digest.topExpensiveTasks && digest.topExpensiveTasks.length > 0) ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
128
+ digest.topExpensiveTasks.map((tItem, idx) => React.createElement('div', {
129
+ key: tItem.id || idx,
130
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', padding: '6px 8px', background: 'var(--dsw-alias-bg-base, #111)', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-l2, #222)' }
131
+ },
132
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, (idx + 1) + '. ' + (tItem.name || tItem.id)),
133
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary, #ddd)' } },
134
+ Number(tItem.tokens || 0).toLocaleString() + ' tok ($' + Number(tItem.costUsd || 0).toFixed(4) + ')'
135
+ )
136
+ ))
137
+ ) : React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)' } }, 'No significant token consumption recorded.')
138
+ )
139
+ )
140
+ ),
141
+
142
+ // Footer
143
+ React.createElement('div', {
144
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 18px', borderTop: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
145
+ },
146
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)' } },
147
+ sendStatus || ''
148
+ ),
149
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
150
+ React.createElement('button', {
151
+ type: 'button',
152
+ className: 'dsh-cron-btn-secondary',
153
+ onClick: copyMarkdown,
154
+ style: { padding: '6px 14px', fontSize: '12px' }
155
+ }, T_KEY(t, 'digest.copyMarkdown')),
156
+ React.createElement('button', {
157
+ type: 'button',
158
+ className: 'dsh-cron-btn-primary',
159
+ onClick: onSendTelegram,
160
+ style: { padding: '6px 14px', fontSize: '12px' }
161
+ }, T_KEY(t, 'digest.sendTelegram'))
162
+ )
163
+ )
164
+ )
165
+ );
166
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * 24-Hour Schedule Timeline & Cost Heatmap Modal (#332, #333)
3
+ * Visual schedule collision detection and weekly 7x24 token burn heatmap.
4
+ * Complies with Audit #203 (0 raw rgba, 0 6-digit hex colors).
5
+ */
6
+ function renderTimelineHeatmapModal({
7
+ isOpen,
8
+ onClose,
9
+ activeTab,
10
+ onSelectTab,
11
+ timelineData,
12
+ heatmapData,
13
+ isLoading,
14
+ t
15
+ }) {
16
+ if (!isOpen) return null;
17
+
18
+ const tab = activeTab || 'timeline';
19
+ const timeline = timelineData || { buckets: [], totalRuns: 0, peakHoursCount: 0, maxConcurrent: 3 };
20
+ const heatmap = heatmapData || { matrix: [], dayLabels: [], hourLabels: [], totalCostUsd: 0, totalTokens: 0, maxCostHour: 0 };
21
+
22
+ return React.createElement('div', {
23
+ className: 'dsh-cron-modal-overlay',
24
+ onClick: onClose
25
+ },
26
+ React.createElement('div', {
27
+ className: 'dsh-cron-modal',
28
+ style: { maxWidth: '960px', width: '94vw', maxHeight: '88vh', display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base, #111)', color: 'var(--dsw-alias-label-primary, #ddd)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '8px' },
29
+ role: 'dialog',
30
+ 'aria-modal': 'true',
31
+ onClick: (e) => e.stopPropagation()
32
+ },
33
+ // Header
34
+ React.createElement('div', {
35
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 18px', borderBottom: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
36
+ },
37
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
38
+ React.createElement('span', { style: { fontSize: '20px' } }, tab === 'timeline' ? '⏱' : '🔥'),
39
+ React.createElement('div', null,
40
+ React.createElement('div', { style: { fontWeight: '600', fontSize: '15px', color: 'var(--dsw-alias-label-primary, #fff)' } },
41
+ tab === 'timeline' ? (T_KEY(t, 'timeline.modalTitle')) : (T_KEY(t, 'heatmap.modalTitle'))
42
+ ),
43
+ React.createElement('div', { style: { fontSize: '11.5px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } },
44
+ tab === 'timeline'
45
+ ? (timeline.totalRuns + ' runs projected · ' + (timeline.peakHoursCount ? '⚠️ ' + timeline.peakHoursCount + ' peak hours' : '✓ balanced concurrency'))
46
+ : ('$' + Number(heatmap.totalCostUsd || 0).toFixed(4) + ' USD · ' + Number(heatmap.totalTokens || 0).toLocaleString() + ' tokens')
47
+ )
48
+ )
49
+ ),
50
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
51
+ React.createElement('button', {
52
+ type: 'button',
53
+ className: 'dsh-cron-btn-secondary',
54
+ style: { padding: '4px 12px', fontSize: '12px', fontWeight: tab === 'timeline' ? '600' : '400', background: tab === 'timeline' ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
55
+ onClick: () => onSelectTab && onSelectTab('timeline')
56
+ }, '⏱ ' + (T_KEY(t, 'timeline.tabTimeline'))),
57
+ React.createElement('button', {
58
+ type: 'button',
59
+ className: 'dsh-cron-btn-secondary',
60
+ style: { padding: '4px 12px', fontSize: '12px', fontWeight: tab === 'heatmap' ? '600' : '400', background: tab === 'heatmap' ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
61
+ onClick: () => onSelectTab && onSelectTab('heatmap')
62
+ }, '🔥 ' + (T_KEY(t, 'heatmap.tabHeatmap'))),
63
+ React.createElement('button', {
64
+ type: 'button',
65
+ className: 'dsh-cron-icon-btn',
66
+ onClick: onClose,
67
+ style: { marginLeft: '8px' }
68
+ }, React.createElement('span', null, '✕'))
69
+ )
70
+ ),
71
+
72
+ // Content
73
+ React.createElement('div', {
74
+ style: { padding: '16px 18px', overflowY: 'auto', flex: 1, display: 'flex', flexDirection: 'column', gap: '14px' }
75
+ },
76
+ isLoading ? React.createElement('div', { style: { padding: '40px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #888)' } }, 'Loading visualization data...') : (
77
+ tab === 'timeline' ? (
78
+ // 24H Timeline View
79
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
80
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)', display: 'flex', justifyContent: 'space-between' } },
81
+ React.createElement('span', null, T_KEY(t, 'timeline.legendDesc')),
82
+ React.createElement('span', { style: { color: 'var(--dsh-cron-warning, #fa0)' } }, '⚠️ ' + (T_KEY(t, 'timeline.peakWarning')) + ' (' + timeline.maxConcurrent + ')')
83
+ ),
84
+ React.createElement('div', {
85
+ style: { display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: '8px', marginTop: '6px' }
86
+ },
87
+ (timeline.buckets || []).map((b) => {
88
+ const isPeak = b.isPeak, hasRuns = b.count > 0;
89
+ return React.createElement('div', {
90
+ key: b.index,
91
+ style: {
92
+ background: isPeak ? 'color-mix(in srgb, var(--dsh-cron-danger, #e33) 12%, var(--dsw-alias-bg-layer-2, #111))' : (hasRuns ? 'var(--dsw-alias-bg-layer-2, #111)' : 'var(--dsw-alias-bg-base, #111)'),
93
+ border: isPeak ? '1px solid var(--dsh-cron-danger, #e33)' : (hasRuns ? '1px solid var(--dsw-alias-border-l2, #333)' : '1px dashed var(--dsw-alias-border-l2, #222)'),
94
+ borderRadius: '6px', padding: '8px', minHeight: '75px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between'
95
+ }
96
+ },
97
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '11px' } },
98
+ React.createElement('span', { style: { fontWeight: '600', color: isPeak ? 'var(--dsh-cron-danger, #e33)' : 'var(--dsw-alias-label-primary, #fff)' } }, b.label),
99
+ hasRuns ? React.createElement('span', { style: { fontSize: '10px', padding: '1px 5px', borderRadius: '8px', background: isPeak ? 'var(--dsh-cron-danger, #e33)' : 'var(--dsw-alias-bg-layer-3, #222)', color: '#fff' } }, b.count) : null
100
+ ),
101
+ React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '3px', marginTop: '4px' } },
102
+ (b.runs || []).slice(0, 3).map((r, rIdx) => React.createElement('span', {
103
+ key: rIdx, title: (r.title || r.taskId) + ' (' + r.time + ')',
104
+ style: { fontSize: '9.5px', fontFamily: 'monospace', padding: '1px 4px', borderRadius: '3px', background: 'var(--dsw-alias-bg-base, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
105
+ }, r.title || r.taskId))
106
+ )
107
+ );
108
+ })
109
+ )
110
+ )
111
+ ) : (
112
+ // 7x24 Cost & Token Heatmap View
113
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
114
+ React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)', display: 'flex', justifyContent: 'space-between' } },
115
+ React.createElement('span', null, T_KEY(t, 'heatmap.matrixDesc')),
116
+ React.createElement('span', null, 'Max hour spend: $' + Number(heatmap.maxCostHour || 0).toFixed(4))
117
+ ),
118
+ React.createElement('div', {
119
+ style: { overflowX: 'auto', paddingBottom: '8px' }
120
+ },
121
+ React.createElement('table', {
122
+ style: { borderCollapse: 'collapse', width: '100%', minWidth: '700px', fontSize: '10.5px' }
123
+ },
124
+ React.createElement('thead', null,
125
+ React.createElement('tr', null,
126
+ React.createElement('th', { style: { padding: '4px 6px', textAlign: 'left', color: 'var(--dsw-alias-label-secondary, #888)' } }, 'Day'),
127
+ Array.from({ length: 24 }, (_, hour) => React.createElement('th', {
128
+ key: hour,
129
+ style: { padding: '4px 2px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #888)', fontWeight: '400' }
130
+ }, String(hour).padStart(2, '0')))
131
+ )
132
+ ),
133
+ React.createElement('tbody', null,
134
+ (heatmap.matrix || []).map((row, dIdx) => React.createElement('tr', { key: dIdx },
135
+ React.createElement('td', { style: { padding: '4px 6px', fontWeight: '600', color: 'var(--dsw-alias-label-primary, #ddd)' } }, (heatmap.dayLabels && heatmap.dayLabels[dIdx]) || 'Day'),
136
+ row.map((cell, hIdx) => {
137
+ const intensity = cell.costIntensity || 0;
138
+ const hasSpend = cell.costUsd > 0;
139
+ const bg = hasSpend
140
+ ? 'color-mix(in srgb, var(--dsh-cron-primary, #4af) ' + Math.max(15, Math.round(intensity * 100)) + '%, transparent)'
141
+ : 'transparent';
142
+ return React.createElement('td', {
143
+ key: hIdx,
144
+ title: cell.dayLabel + ' ' + cell.hourLabel + ': $' + cell.costUsd + ' (' + cell.tokens.toLocaleString() + ' tokens, ' + cell.runs + ' runs)',
145
+ style: {
146
+ padding: '3px',
147
+ textAlign: 'center',
148
+ background: bg,
149
+ border: '1px solid var(--dsw-alias-border-l2, #222)',
150
+ cursor: hasSpend ? 'pointer' : 'default'
151
+ }
152
+ }, hasSpend ? React.createElement('span', { style: { fontSize: '9px', color: intensity > 0.5 ? '#fff' : 'var(--dsw-alias-label-primary, #ddd)' } }, '$' + (cell.costUsd >= 0.01 ? cell.costUsd.toFixed(2) : cell.costUsd.toFixed(3).slice(1))) : '·');
153
+ })
154
+ ))
155
+ )
156
+ )
157
+ )
158
+ )
159
+ )
160
+ )
161
+ ),
162
+
163
+ // Footer
164
+ React.createElement('div', {
165
+ style: { display: 'flex', justifyContent: 'flex-end', padding: '12px 18px', borderTop: '1px solid var(--dsw-alias-border-l2, #333)', background: 'var(--dsw-alias-bg-layer-2, #111)' }
166
+ },
167
+ React.createElement('button', {
168
+ type: 'button',
169
+ className: 'dsh-cron-btn-secondary',
170
+ onClick: onClose,
171
+ style: { padding: '6px 16px', fontSize: '12px' }
172
+ }, T_KEY(t, 'chainGraph.close'))
173
+ )
174
+ )
175
+ );
176
+ }