@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.
- 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 -191
- 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/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/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 +2 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
function CronSettingsCard(props) {
|
|
2
|
+
const { ctx, toggle } = props;
|
|
3
|
+
const t = props.t || translate;
|
|
4
|
+
const [token, setToken] = React.useState('');
|
|
5
|
+
const [chatId, setChatId] = React.useState('');
|
|
6
|
+
const [notify, setNotify] = React.useState(false);
|
|
7
|
+
const [onlyFailure, setOnlyFailure] = React.useState(false);
|
|
8
|
+
const [llmActionsEnabled, setLlmActionsEnabled] = React.useState(false);
|
|
9
|
+
const [enableDynamicCredentials, setEnableDynamicCredentials] = React.useState(false);
|
|
10
|
+
const [kanbanUrl, setKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
11
|
+
const [delivery, setDelivery] = React.useState({});
|
|
12
|
+
const [sections, setSections] = React.useState({ channels: true });
|
|
13
|
+
const [hasDefault, setHasDefault] = React.useState(false);
|
|
14
|
+
const [saved, setSaved] = React.useState(false);
|
|
15
|
+
const [testInfo, setTestInfo] = React.useState(null);
|
|
16
|
+
// Collapsed by default, head is the toggle (canonical card contract, #100)
|
|
17
|
+
const [cardOpen, setCardOpen] = React.useState(false);
|
|
18
|
+
const [updateState, setUpdateState] = React.useState({
|
|
19
|
+
checking: false,
|
|
20
|
+
updating: false,
|
|
21
|
+
currentVersion: '',
|
|
22
|
+
latestVersion: undefined,
|
|
23
|
+
updateAvailable: false,
|
|
24
|
+
notice: null,
|
|
25
|
+
error: null
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const checkUpdates = async () => {
|
|
29
|
+
setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
|
|
30
|
+
try {
|
|
31
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update');
|
|
32
|
+
if (data && data.currentVersion) {
|
|
33
|
+
setUpdateState(prev => ({
|
|
34
|
+
...prev,
|
|
35
|
+
checking: false,
|
|
36
|
+
currentVersion: data.currentVersion,
|
|
37
|
+
latestVersion: data.latestVersion,
|
|
38
|
+
updateAvailable: Boolean(data.updateAvailable),
|
|
39
|
+
error: data.latestCheckFailed ? 'Registry check failed' : null
|
|
40
|
+
}));
|
|
41
|
+
} else {
|
|
42
|
+
setUpdateState(prev => ({ ...prev, checking: false, error: data.error || 'Check failed' }));
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setUpdateState(prev => ({ ...prev, checking: false, error: err.message }));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const runUpdate = async () => {
|
|
50
|
+
setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
|
|
51
|
+
try {
|
|
52
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
|
|
55
|
+
});
|
|
56
|
+
if (data && data.updated) {
|
|
57
|
+
setUpdateState(prev => ({
|
|
58
|
+
...prev,
|
|
59
|
+
updating: false,
|
|
60
|
+
updateAvailable: false,
|
|
61
|
+
currentVersion: data.updatedVersion || data.latestVersion,
|
|
62
|
+
notice: T_KEY(t, 'updater.success', { version: data.updatedVersion || data.latestVersion })
|
|
63
|
+
}));
|
|
64
|
+
} else {
|
|
65
|
+
setUpdateState(prev => ({
|
|
66
|
+
...prev,
|
|
67
|
+
updating: false,
|
|
68
|
+
error: data.error || data.message || 'Update failed'
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setUpdateState(prev => ({ ...prev, updating: false, error: err.message }));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Settings snapshot status (#102): never render phantom inputs before
|
|
77
|
+
// the snapshot arrives; offer a retry when it is unavailable.
|
|
78
|
+
const [loadState, setLoadState] = React.useState('loading');
|
|
79
|
+
const [reloadTick, setReloadTick] = React.useState(0);
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
let active = true;
|
|
83
|
+
setLoadState('loading');
|
|
84
|
+
fetchJsonWithTimeout('/dsh-cron/settings')
|
|
85
|
+
.then(({ data }) => {
|
|
86
|
+
if (!active) return;
|
|
87
|
+
if (data && data.ok && data.settings) {
|
|
88
|
+
setToken(data.settings.botToken || '');
|
|
89
|
+
setChatId(data.settings.chatId || '');
|
|
90
|
+
setNotify(Boolean(data.settings.notifyTelegram));
|
|
91
|
+
setOnlyFailure(Boolean(data.settings.onlyOnFailure));
|
|
92
|
+
setLlmActionsEnabled(Boolean(data.settings.llmActionsEnabled));
|
|
93
|
+
setEnableDynamicCredentials(Boolean(data.settings.enableDynamicCredentials));
|
|
94
|
+
setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
95
|
+
setDelivery(pickDeliverySettings(data.settings));
|
|
96
|
+
setHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
97
|
+
if (data.settings.version) {
|
|
98
|
+
setUpdateState(prev => ({ ...prev, currentVersion: data.settings.version }));
|
|
99
|
+
}
|
|
100
|
+
setLoadState('ready');
|
|
101
|
+
} else {
|
|
102
|
+
setLoadState('error');
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
.catch(() => { if (active) setLoadState('error'); });
|
|
106
|
+
return () => { active = false; };
|
|
107
|
+
}, [reloadTick]);
|
|
108
|
+
|
|
109
|
+
const saveSettings = async (e) => {
|
|
110
|
+
if (e) e.preventDefault();
|
|
111
|
+
try {
|
|
112
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(Object.assign({
|
|
116
|
+
botToken: token,
|
|
117
|
+
chatId,
|
|
118
|
+
notifyTelegram: notify,
|
|
119
|
+
onlyOnFailure: onlyFailure,
|
|
120
|
+
llmActionsEnabled: llmActionsEnabled,
|
|
121
|
+
enableDynamicCredentials: enableDynamicCredentials,
|
|
122
|
+
kanbanBaseUrl: kanbanUrl
|
|
123
|
+
}, delivery))
|
|
124
|
+
});
|
|
125
|
+
if (data && data.ok) {
|
|
126
|
+
setSaved(true);
|
|
127
|
+
setTimeout(() => setSaved(false), 2500);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.debug('[dsh-cron] save settings failed', err);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const testTelegram = async () => {
|
|
135
|
+
setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
136
|
+
try {
|
|
137
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ botToken: token, chatId })
|
|
141
|
+
});
|
|
142
|
+
if (data && data.ok) {
|
|
143
|
+
setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
|
|
144
|
+
} else {
|
|
145
|
+
setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const testKanban = async () => {
|
|
153
|
+
setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
|
|
154
|
+
try {
|
|
155
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/kanban/test', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
|
|
159
|
+
});
|
|
160
|
+
if (data && data.ok) {
|
|
161
|
+
setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
|
|
162
|
+
} else {
|
|
163
|
+
setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: (data && data.error) || '' }) });
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: err.message }) });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (loadState !== 'ready') {
|
|
171
|
+
return React.createElement('div', { className: 'dsh-cron-card' },
|
|
172
|
+
React.createElement('div', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
|
|
173
|
+
React.createElement('div', {
|
|
174
|
+
className: 'dsh-cron-status-banner',
|
|
175
|
+
'data-type': loadState === 'error' ? 'error' : 'info',
|
|
176
|
+
style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px' }
|
|
177
|
+
},
|
|
178
|
+
React.createElement('span', null, loadState === 'error' ? T_KEY(t, 'settings.unavailable') : T_KEY(t, 'settings.loading')),
|
|
179
|
+
loadState === 'error' && React.createElement('button', {
|
|
180
|
+
type: 'button',
|
|
181
|
+
className: 'dsh-cron-btn-secondary',
|
|
182
|
+
onClick: () => setReloadTick(x => x + 1)
|
|
183
|
+
}, T_KEY(t, 'settings.retry'))
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
189
|
+
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
190
|
+
if (props && props.view === 'summary') {
|
|
191
|
+
return React.createElement('span', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc'));
|
|
192
|
+
}
|
|
193
|
+
const page = !!(props && props.view === 'page');
|
|
194
|
+
return React.createElement('div', { className: page ? 'dsh-cron-page' : 'dsh-cron-card' },
|
|
195
|
+
React.createElement('button', {
|
|
196
|
+
type: 'button',
|
|
197
|
+
className: 'dsh-cron-card-head-btn',
|
|
198
|
+
style: page ? { display: 'none' } : undefined,
|
|
199
|
+
'aria-expanded': (page || cardOpen) ? 'true' : 'false',
|
|
200
|
+
onClick: () => setCardOpen(!cardOpen)
|
|
201
|
+
},
|
|
202
|
+
React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
|
|
203
|
+
React.createElement('span', { className: 'dsh-cron-chev' + (cardOpen ? ' dsh-cron-chev-open' : '') }, chevronNode())
|
|
204
|
+
),
|
|
205
|
+
(page || cardOpen) && React.createElement('div', { className: 'dsh-cron-card-body' },
|
|
206
|
+
React.createElement('div', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc')),
|
|
207
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '12px' } },
|
|
208
|
+
React.createElement('button', {
|
|
209
|
+
type: 'button',
|
|
210
|
+
className: 'dsh-cron-btn-primary',
|
|
211
|
+
onClick: () => { if (toggle) toggle.set(true); }
|
|
212
|
+
}, T_KEY(t, 'settings.openPanel'))
|
|
213
|
+
),
|
|
214
|
+
React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
215
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
216
|
+
React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
|
|
217
|
+
React.createElement('input', {
|
|
218
|
+
type: 'password',
|
|
219
|
+
value: token,
|
|
220
|
+
placeholder: hasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
|
|
221
|
+
onChange: (e) => setToken(e.target.value)
|
|
222
|
+
}),
|
|
223
|
+
hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
|
|
224
|
+
),
|
|
225
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
226
|
+
React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
|
|
227
|
+
React.createElement('input', {
|
|
228
|
+
value: chatId,
|
|
229
|
+
placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
|
|
230
|
+
onChange: (e) => setChatId(e.target.value)
|
|
231
|
+
})
|
|
232
|
+
),
|
|
233
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
234
|
+
React.createElement('label', null, T_KEY(t, 'settings.kanbanUrlLabel')),
|
|
235
|
+
React.createElement('input', {
|
|
236
|
+
value: kanbanUrl,
|
|
237
|
+
placeholder: 'http://127.0.0.1:3000',
|
|
238
|
+
onChange: (e) => setKanbanUrl(e.target.value)
|
|
239
|
+
})
|
|
240
|
+
),
|
|
241
|
+
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: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
242
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
243
|
+
React.createElement('input', {
|
|
244
|
+
type: 'checkbox',
|
|
245
|
+
checked: notify,
|
|
246
|
+
onChange: (e) => setNotify(e.target.checked)
|
|
247
|
+
}),
|
|
248
|
+
React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
|
|
249
|
+
),
|
|
250
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
251
|
+
React.createElement('input', {
|
|
252
|
+
type: 'checkbox',
|
|
253
|
+
checked: onlyFailure,
|
|
254
|
+
onChange: (e) => setOnlyFailure(e.target.checked)
|
|
255
|
+
}),
|
|
256
|
+
React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
|
|
257
|
+
),
|
|
258
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
259
|
+
React.createElement('input', {
|
|
260
|
+
type: 'checkbox',
|
|
261
|
+
checked: llmActionsEnabled,
|
|
262
|
+
onChange: (e) => setLlmActionsEnabled(e.target.checked)
|
|
263
|
+
}),
|
|
264
|
+
React.createElement('span', null, 'Enable Structured LLM Actions (trigger tasks, notify, create issues)')
|
|
265
|
+
),
|
|
266
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
267
|
+
React.createElement('input', {
|
|
268
|
+
type: 'checkbox',
|
|
269
|
+
checked: enableDynamicCredentials,
|
|
270
|
+
onChange: (e) => setEnableDynamicCredentials(e.target.checked)
|
|
271
|
+
}),
|
|
272
|
+
React.createElement('span', null, T_KEY(t, 'settings.dynamicCredentials'))
|
|
273
|
+
)
|
|
274
|
+
),
|
|
275
|
+
...renderDeliverySettings({
|
|
276
|
+
T: (key, vars) => T_KEY(t, key, vars),
|
|
277
|
+
values: delivery,
|
|
278
|
+
onChange: (key, value) => setDelivery((prev) => {
|
|
279
|
+
const next = Object.assign({}, prev);
|
|
280
|
+
if (value === undefined) delete next[key];
|
|
281
|
+
else next[key] = value;
|
|
282
|
+
return next;
|
|
283
|
+
}),
|
|
284
|
+
onChannelTemplate: (channelId, value) => setDelivery((prev) => {
|
|
285
|
+
const templates = Object.assign({}, prev.channelTemplates || {});
|
|
286
|
+
if (value && value.trim()) templates[channelId] = value;
|
|
287
|
+
else delete templates[channelId];
|
|
288
|
+
return Object.assign({}, prev, { channelTemplates: templates });
|
|
289
|
+
}),
|
|
290
|
+
sections,
|
|
291
|
+
onToggleSection: (key) => setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] })),
|
|
292
|
+
}),
|
|
293
|
+
testInfo && testInfo.message && React.createElement('div', {
|
|
294
|
+
className: 'dsh-cron-status-banner',
|
|
295
|
+
'data-type': testInfo.success ? 'success' : 'error',
|
|
296
|
+
style: { margin: 0, padding: '8px 12px' }
|
|
297
|
+
}, testInfo.message),
|
|
298
|
+
React.createElement('div', {
|
|
299
|
+
className: 'dsh-cron-form-group',
|
|
300
|
+
style: {
|
|
301
|
+
background: 'var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent))',
|
|
302
|
+
border: '1px solid var(--dsw-alias-border-l2, color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent))',
|
|
303
|
+
borderRadius: '8px',
|
|
304
|
+
padding: '14px',
|
|
305
|
+
marginBottom: '16px'
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
React.createElement('div', {
|
|
309
|
+
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }
|
|
310
|
+
},
|
|
311
|
+
React.createElement('div', { style: { fontWeight: '600', fontSize: '14px' } }, `🚀 ${T_KEY(t, 'updater.title')}`),
|
|
312
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
313
|
+
React.createElement('button', {
|
|
314
|
+
type: 'button',
|
|
315
|
+
className: 'dsh-cron-btn-secondary',
|
|
316
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
317
|
+
disabled: updateState.checking || updateState.updating,
|
|
318
|
+
onClick: checkUpdates
|
|
319
|
+
}, updateState.checking ? T_KEY(t, 'updater.checking') : T_KEY(t, 'updater.btnCheck')),
|
|
320
|
+
updateState.updateAvailable ? React.createElement('button', {
|
|
321
|
+
type: 'button',
|
|
322
|
+
className: 'dsh-cron-btn-primary',
|
|
323
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
324
|
+
disabled: updateState.updating,
|
|
325
|
+
onClick: runUpdate
|
|
326
|
+
}, updateState.updating ? T_KEY(t, 'updater.updating') : T_KEY(t, 'updater.btnUpdate', { version: updateState.latestVersion })) : null
|
|
327
|
+
)
|
|
328
|
+
),
|
|
329
|
+
React.createElement('div', {
|
|
330
|
+
style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #999)', marginBottom: '8px' }
|
|
331
|
+
}, T_KEY(t, 'updater.desc')),
|
|
332
|
+
React.createElement('div', {
|
|
333
|
+
style: { display: 'flex', alignItems: 'center', gap: '12px', fontSize: '13px' }
|
|
334
|
+
},
|
|
335
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #aaa)' } },
|
|
336
|
+
updateState.currentVersion ? T_KEY(t, 'updater.current', { version: updateState.currentVersion }) : ''
|
|
337
|
+
),
|
|
338
|
+
updateState.checking ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-info' }, T_KEY(t, 'updater.checking')) :
|
|
339
|
+
updateState.updateAvailable ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-warn' }, T_KEY(t, 'updater.available', { version: updateState.latestVersion })) :
|
|
340
|
+
updateState.currentVersion && !updateState.error && !updateState.checking ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-ok' }, `✓ ${T_KEY(t, 'updater.upToDate')}`) : null
|
|
341
|
+
),
|
|
342
|
+
updateState.notice ? React.createElement('div', {
|
|
343
|
+
style: { marginTop: '8px', color: 'var(--dsw-alias-success)', fontSize: '12px' }
|
|
344
|
+
}, updateState.notice) : null,
|
|
345
|
+
updateState.error ? React.createElement('div', {
|
|
346
|
+
style: { marginTop: '8px', color: 'var(--dsw-alias-error)', fontSize: '12px' }
|
|
347
|
+
}, updateState.error) : null
|
|
348
|
+
),
|
|
349
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '4px' } },
|
|
350
|
+
React.createElement('button', {
|
|
351
|
+
type: 'submit',
|
|
352
|
+
className: 'dsh-cron-btn-primary',
|
|
353
|
+
style: { minWidth: '140px' }
|
|
354
|
+
}, saved ? T_KEY(t, 'settings.saved') : T_KEY(t, 'settings.save')),
|
|
355
|
+
React.createElement('button', {
|
|
356
|
+
type: 'button',
|
|
357
|
+
className: 'dsh-cron-btn-secondary',
|
|
358
|
+
onClick: testTelegram,
|
|
359
|
+
disabled: testInfo?.loading
|
|
360
|
+
}, T_KEY(t, 'settings.testTelegram')),
|
|
361
|
+
React.createElement('button', {
|
|
362
|
+
type: 'button',
|
|
363
|
+
className: 'dsh-cron-btn-secondary',
|
|
364
|
+
onClick: testKanban,
|
|
365
|
+
disabled: testInfo?.loading
|
|
366
|
+
}, T_KEY(t, 'settings.testKanban'))
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function nativeButton() {
|
|
374
|
+
const nodes = document.querySelectorAll('button[class*="newSession"]');
|
|
375
|
+
for (const node of nodes) {
|
|
376
|
+
if (String(node.className).indexOf('newSessionLabel') === -1) return node;
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function createEntry(target, toggle, label) {
|
|
382
|
+
const entry = target.cloneNode(true);
|
|
383
|
+
entry.className = String(target.className) + ' dsh-cron-entry-clone';
|
|
384
|
+
entry.setAttribute(ENTRY_ATTR, '');
|
|
385
|
+
entry.setAttribute('data-dsh-plugin', 'dsh-cron');
|
|
386
|
+
entry.setAttribute('data-dsh-part', 'sidebar-entry');
|
|
387
|
+
entry.setAttribute('aria-label', label);
|
|
388
|
+
entry.removeAttribute('id');
|
|
389
|
+
entry.innerHTML = '';
|
|
390
|
+
|
|
391
|
+
const icon = document.createElement('span');
|
|
392
|
+
icon.className = 'dsh-cron-entry-icon';
|
|
393
|
+
icon.innerHTML = ICON_TIMER;
|
|
394
|
+
entry.appendChild(icon);
|
|
395
|
+
|
|
396
|
+
const text = document.createElement('span');
|
|
397
|
+
text.className = 'dsh-cron-entry-label';
|
|
398
|
+
text.textContent = label;
|
|
399
|
+
entry.appendChild(text);
|
|
400
|
+
|
|
401
|
+
entry.addEventListener('click', (event) => {
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
event.stopPropagation();
|
|
404
|
+
toggle.toggle();
|
|
405
|
+
});
|
|
406
|
+
return entry;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Collapsible list of active jobs under the sidebar entry (#34).
|
|
411
|
+
*
|
|
412
|
+
* Vanilla DOM like the entry itself: the sidebar is not a React root here.
|
|
413
|
+
* Collapsed by default, state persisted, capped at a few rows with an
|
|
414
|
+
* "and N more" line so a busy schedule cannot push the rest of the sidebar
|
|
415
|
+
* off screen. Clicking a row opens the panel and highlights that task.
|
|
416
|
+
*/
|
|
417
|
+
const SIDEBAR_JOBS_KEY = 'dsh-cron-sidebar-open';
|
|
418
|
+
const SIDEBAR_JOBS_MAX_ROWS = 5;
|
|
419
|
+
|
|
420
|
+
/** Sidebar job row: title plus next run time or the live running state. */
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
function buildSidebarJobRow(state, task) {
|
|
2
|
+
const row = document.createElement('button');
|
|
3
|
+
row.type = 'button';
|
|
4
|
+
row.className = 'dsh-cron-sidebar-job';
|
|
5
|
+
// A distinct attribute: the panel uses data-task-id, and the highlight
|
|
6
|
+
// lookup must never pick a sidebar row instead of a task card.
|
|
7
|
+
row.setAttribute('data-sidebar-task-id', task.id);
|
|
8
|
+
row.setAttribute('aria-label', task.title || task.id);
|
|
9
|
+
|
|
10
|
+
const title = document.createElement('span');
|
|
11
|
+
title.className = 'dsh-cron-sidebar-job-title';
|
|
12
|
+
title.textContent = task.title || task.id;
|
|
13
|
+
row.appendChild(title);
|
|
14
|
+
|
|
15
|
+
const meta = document.createElement('span');
|
|
16
|
+
meta.className = 'dsh-cron-sidebar-job-meta';
|
|
17
|
+
if (task.running) meta.textContent = T('list.runningNow');
|
|
18
|
+
else if (task.nextRunAt) meta.textContent = new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
19
|
+
else meta.textContent = task.scheduleText || task.schedule || '';
|
|
20
|
+
row.appendChild(meta);
|
|
21
|
+
|
|
22
|
+
if (task.running) row.classList.add('dsh-cron-pulse');
|
|
23
|
+
row.addEventListener('click', (event) => {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
state.openTaskSignal(task.id);
|
|
27
|
+
if (!state.toggle.isOpen()) state.toggle.toggle();
|
|
28
|
+
});
|
|
29
|
+
return row;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderSidebarJobs(state, tasks) {
|
|
33
|
+
const listEl = state.listEl;
|
|
34
|
+
if (!listEl) return;
|
|
35
|
+
listEl.innerHTML = '';
|
|
36
|
+
if (!tasks.length) {
|
|
37
|
+
const empty = document.createElement('div');
|
|
38
|
+
empty.className = 'dsh-cron-sidebar-empty';
|
|
39
|
+
empty.textContent = T('sidebar.noActiveJobs');
|
|
40
|
+
listEl.appendChild(empty);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const task of tasks.slice(0, SIDEBAR_JOBS_MAX_ROWS)) listEl.appendChild(buildSidebarJobRow(state, task));
|
|
44
|
+
if (tasks.length > SIDEBAR_JOBS_MAX_ROWS) {
|
|
45
|
+
const more = document.createElement('div');
|
|
46
|
+
more.className = 'dsh-cron-sidebar-more';
|
|
47
|
+
more.textContent = T('sidebar.moreJobs', { count: tasks.length - SIDEBAR_JOBS_MAX_ROWS });
|
|
48
|
+
listEl.appendChild(more);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function loadSidebarJobs(state) {
|
|
53
|
+
try {
|
|
54
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks?status=active');
|
|
55
|
+
if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.debug('[dsh-cron] load sidebar jobs failed', err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applySidebarJobsOpen(state) {
|
|
62
|
+
if (!state.root) return;
|
|
63
|
+
state.root.dataset.open = state.open ? 'true' : 'false';
|
|
64
|
+
if (state.headEl) state.headEl.setAttribute('aria-expanded', state.open ? 'true' : 'false');
|
|
65
|
+
if (state.open) loadSidebarJobs(state);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSidebarJobsRoot(state, label) {
|
|
69
|
+
const root = document.createElement('div');
|
|
70
|
+
root.className = 'dsh-cron-sidebar-jobs';
|
|
71
|
+
root.setAttribute('data-dsh-plugin', 'dsh-cron');
|
|
72
|
+
root.setAttribute('data-dsh-part', 'sidebar-jobs');
|
|
73
|
+
|
|
74
|
+
const headEl = document.createElement('button');
|
|
75
|
+
headEl.type = 'button';
|
|
76
|
+
headEl.className = 'dsh-cron-sidebar-jobs-head';
|
|
77
|
+
headEl.setAttribute('aria-expanded', state.open ? 'true' : 'false');
|
|
78
|
+
headEl.textContent = label;
|
|
79
|
+
headEl.addEventListener('click', (event) => {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
state.open = !state.open;
|
|
83
|
+
try { window.localStorage.setItem(SIDEBAR_JOBS_KEY, state.open ? '1' : '0'); } catch (err) { /* localStorage write failed */ }
|
|
84
|
+
applySidebarJobsOpen(state);
|
|
85
|
+
});
|
|
86
|
+
root.appendChild(headEl);
|
|
87
|
+
|
|
88
|
+
const listEl = document.createElement('div');
|
|
89
|
+
listEl.className = 'dsh-cron-sidebar-jobs-list';
|
|
90
|
+
root.appendChild(listEl);
|
|
91
|
+
|
|
92
|
+
state.root = root;
|
|
93
|
+
state.headEl = headEl;
|
|
94
|
+
state.listEl = listEl;
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Keep the section mounted under the sidebar entry across re-renders. */
|
|
99
|
+
function placeSidebarJobs(state, label) {
|
|
100
|
+
if (state.root !== undefined && state.root.isConnected) return;
|
|
101
|
+
const entry = document.querySelector('[' + ENTRY_ATTR + ']');
|
|
102
|
+
if (entry === null || entry.parentElement === null) return;
|
|
103
|
+
if (state.root === undefined) {
|
|
104
|
+
entry.insertAdjacentElement('afterend', buildSidebarJobsRoot(state, label));
|
|
105
|
+
} else {
|
|
106
|
+
entry.insertAdjacentElement('afterend', state.root);
|
|
107
|
+
// Re-query the refs: the previous nodes may belong to a detached tree.
|
|
108
|
+
state.root = document.querySelector('[data-dsh-part="sidebar-jobs"]') || state.root;
|
|
109
|
+
state.listEl = state.root.querySelector('.dsh-cron-sidebar-jobs-list');
|
|
110
|
+
state.headEl = state.root.querySelector('.dsh-cron-sidebar-jobs-head');
|
|
111
|
+
}
|
|
112
|
+
applySidebarJobsOpen(state);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Collapsible list of active jobs under the sidebar entry (#34).
|
|
117
|
+
*
|
|
118
|
+
* Vanilla DOM like the entry itself: the sidebar is not a React root here.
|
|
119
|
+
* Collapsed by default, state persisted, capped at a few rows with an
|
|
120
|
+
* "and N more" line so a busy schedule cannot push the rest of the sidebar
|
|
121
|
+
* off screen. Clicking a row opens the panel and highlights that task.
|
|
122
|
+
*/
|
|
123
|
+
function mountSidebarJobs(toggle, label, openTaskSignal) {
|
|
124
|
+
if (typeof document === 'undefined') return () => {};
|
|
125
|
+
const state = { toggle, openTaskSignal, root: undefined, listEl: undefined, headEl: undefined, open: false };
|
|
126
|
+
try { state.open = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage read failed */ }
|
|
127
|
+
|
|
128
|
+
let rafPending = false;
|
|
129
|
+
const schedulePlace = () => {
|
|
130
|
+
if (state.root !== undefined && state.root.isConnected) return;
|
|
131
|
+
if (rafPending) return;
|
|
132
|
+
rafPending = true;
|
|
133
|
+
const scheduleFn = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (cb) => setTimeout(cb, 16);
|
|
134
|
+
scheduleFn(() => {
|
|
135
|
+
rafPending = false;
|
|
136
|
+
placeSidebarJobs(state, label);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const observer = new MutationObserver(schedulePlace);
|
|
141
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
142
|
+
placeSidebarJobs(state, label);
|
|
143
|
+
loadSidebarJobs(state);
|
|
144
|
+
const fetchTimer = setInterval(() => {
|
|
145
|
+
let isOpen = false;
|
|
146
|
+
try { isOpen = window.localStorage.getItem(SIDEBAR_JOBS_KEY) === '1'; } catch (err) { /* localStorage poll failed */ }
|
|
147
|
+
if (isOpen) loadSidebarJobs(state);
|
|
148
|
+
}, 30000);
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
clearInterval(fetchTimer);
|
|
153
|
+
if (state.root !== undefined && state.root.isConnected) state.root.remove();
|
|
154
|
+
state.root = undefined;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function placeEntry(target, entry) {
|
|
159
|
+
let anchor = target;
|
|
160
|
+
let next = target.nextElementSibling;
|
|
161
|
+
while (next !== null && next.tagName === 'BUTTON'
|
|
162
|
+
&& String(next.className).indexOf('sidebar-entry') >= 0) {
|
|
163
|
+
anchor = next;
|
|
164
|
+
next = next.nextElementSibling;
|
|
165
|
+
}
|
|
166
|
+
anchor.insertAdjacentElement('afterend', entry);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mountSidebarEntry(toggle, label) {
|
|
170
|
+
if (typeof document === 'undefined') return () => {};
|
|
171
|
+
let entry;
|
|
172
|
+
const tryPlace = () => {
|
|
173
|
+
if (entry !== undefined && entry.isConnected) return;
|
|
174
|
+
const target = nativeButton();
|
|
175
|
+
if (target === undefined || target.parentElement === null) return;
|
|
176
|
+
if (target.parentElement.querySelector('[' + ENTRY_ATTR + ']') !== null) return;
|
|
177
|
+
entry = createEntry(target, toggle, label);
|
|
178
|
+
placeEntry(target, entry);
|
|
179
|
+
syncActive();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const syncActive = () => {
|
|
183
|
+
if (entry === undefined) return;
|
|
184
|
+
if (toggle.isOpen()) entry.dataset.active = 'true';
|
|
185
|
+
else delete entry.dataset.active;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const observer = new MutationObserver(tryPlace);
|
|
189
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
190
|
+
const unsubscribe = toggle.subscribe(syncActive);
|
|
191
|
+
tryPlace();
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
observer.disconnect();
|
|
195
|
+
unsubscribe();
|
|
196
|
+
if (entry !== undefined) entry.remove();
|
|
197
|
+
entry = undefined;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|