@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.
- package/lib/api-actions.js +1 -317
- package/lib/api-collection.js +1 -250
- package/lib/api-helpers.js +1 -100
- package/lib/api-import-export.js +1 -155
- package/lib/api-item.js +1 -190
- package/lib/api-task-webhook.js +1 -247
- package/lib/api-webhook.js +1 -183
- package/lib/api.js +1 -178
- package/lib/artifacts.js +1 -200
- package/lib/chain-checkpoints.js +1 -223
- package/lib/client-src/00-open.js +62 -0
- package/lib/client-src/10-locales.js +484 -0
- package/lib/client-src/20-styles.js +64 -0
- package/lib/client-src/30-utils.js +248 -0
- package/lib/client-src/40-delivery-settings.js +96 -0
- package/lib/client-src/45-dag-graph-view.js +287 -0
- package/lib/client-src/46-live-logs-modal.js +108 -0
- package/lib/client-src/47-analytics-digest-modal.js +166 -0
- package/lib/client-src/48-timeline-heatmap-view.js +176 -0
- package/lib/client-src/50-cron-screen-state.js +447 -0
- package/lib/client-src/52-cron-screen-actions.js +330 -0
- package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
- package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
- package/lib/client-src/55-cron-screen-view.js +378 -0
- package/lib/client-src/60-modal-task-form-head.js +580 -0
- package/lib/client-src/62-modal-task-form-tail.js +466 -0
- package/lib/client-src/65-modal-dialogs.js +350 -0
- package/lib/client-src/70-settings-card.js +420 -0
- package/lib/client-src/80-sidebar-jobs.js +200 -0
- package/lib/client-src/90-close.js +200 -0
- package/lib/client.js +48 -3
- package/lib/commands.js +200 -0
- package/lib/durable-journal.js +1 -188
- package/lib/external-api.js +1 -74
- package/lib/index.js +17 -0
- package/lib/log-streamer.js +10 -2
- package/lib/metrics.js +1 -125
- package/lib/routes/api-actions.js +317 -0
- package/lib/routes/api-collection.js +250 -0
- package/lib/routes/api-helpers.js +100 -0
- package/lib/routes/api-import-export.js +155 -0
- package/lib/routes/api-item.js +191 -0
- package/lib/routes/api-task-webhook.js +247 -0
- package/lib/routes/api-webhook.js +183 -0
- package/lib/routes/api.js +178 -0
- package/lib/routes/external-api.js +74 -0
- package/lib/routes/index.js +11 -0
- package/lib/routes/metrics.js +125 -0
- package/lib/routes/routes.js +549 -0
- package/lib/routes.js +1 -549
- package/lib/runner-external.js +1 -352
- package/lib/runner-worktree.js +1 -90
- package/lib/runner.js +1 -881
- package/lib/runtimes/index.js +4 -0
- package/lib/runtimes/runner-external.js +352 -0
- package/lib/runtimes/runner-worktree.js +90 -0
- package/lib/runtimes/runner.js +881 -0
- package/lib/runtimes/runtimes.js +198 -0
- package/lib/runtimes.js +1 -198
- package/lib/scheduler/index.js +6 -0
- package/lib/scheduler/scheduler-assists.js +142 -0
- package/lib/scheduler/scheduler-completion.js +417 -0
- package/lib/scheduler/scheduler-execution.js +360 -0
- package/lib/scheduler/scheduler-lifecycle.js +157 -0
- package/lib/scheduler/scheduler-preflight.js +195 -0
- package/lib/scheduler/scheduler.js +751 -0
- package/lib/scheduler-assists.js +1 -142
- package/lib/scheduler-completion.js +1 -417
- package/lib/scheduler-execution.js +1 -360
- package/lib/scheduler-lifecycle.js +1 -157
- package/lib/scheduler-preflight.js +1 -195
- package/lib/scheduler.js +1 -751
- package/lib/store/artifacts.js +200 -0
- package/lib/store/chain-checkpoints.js +223 -0
- package/lib/store/durable-journal.js +188 -0
- package/lib/store/index.js +6 -0
- package/lib/store/store.js +1179 -0
- package/lib/store/task-schema.js +180 -0
- package/lib/store/task-transfer.js +310 -0
- package/lib/store.js +2 -1179
- package/lib/task-schema.js +1 -180
- package/lib/task-transfer.js +1 -310
- package/lib/tui-settings.js +157 -0
- 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
|
+
}
|