@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
package/lib/api-actions.js
CHANGED
|
@@ -1,317 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { captureGitDiff, discardGitChanges, applyGitDiff } from './git-diff.js';
|
|
3
|
-
import { applyDoctorFix, revertDoctorFix, diagnoseFailure } from './doctor.js';
|
|
4
|
-
import { rollbackPromptVersion } from './prompt-evolver.js';
|
|
5
|
-
import { approvalManager } from './approval.js';
|
|
6
|
-
import { MANAGED_BY_CONFIG } from './config-jobs.js';
|
|
7
|
-
import { sendJson, readBody, redactTaskSecrets } from './http-utils.js';
|
|
8
|
-
import { buildDuplicateTask } from './task-transfer.js';
|
|
9
|
-
import { refuseConfigOwned, NOT_FOUND } from './api-collection.js';
|
|
10
|
-
|
|
11
|
-
export async function handleItemPost({ store, scheduler, req, res, url, id, action, subAction }) {
|
|
12
|
-
if (action === 'git-diff' && (subAction === 'discard' || subAction === 'revert')) {
|
|
13
|
-
const task = store.get(id);
|
|
14
|
-
if (!task) {
|
|
15
|
-
sendJson(res, 404, NOT_FOUND);
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
const targetDir = task.cwd;
|
|
19
|
-
if (!targetDir) {
|
|
20
|
-
sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
const result = await discardGitChanges(targetDir);
|
|
24
|
-
sendJson(res, result.ok ? 200 : 500, result);
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (action === 'git-diff' && subAction === 'apply') {
|
|
29
|
-
const task = store.get(id);
|
|
30
|
-
if (!task) {
|
|
31
|
-
sendJson(res, 404, NOT_FOUND);
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
const body = await readBody(req, res);
|
|
35
|
-
const bodyData = (body && body.body) || {};
|
|
36
|
-
let patch = bodyData.diff || bodyData.patch;
|
|
37
|
-
if (!patch) {
|
|
38
|
-
const runs = store.getHistory(id, 5);
|
|
39
|
-
const targetRun = runs.find((r) => r.gitDiff?.diff);
|
|
40
|
-
patch = targetRun?.gitDiff?.diff;
|
|
41
|
-
}
|
|
42
|
-
if (!patch) {
|
|
43
|
-
sendJson(res, 400, { ok: false, error: 'No diff patch provided or found in run history' });
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
const targetDir = task.cwd;
|
|
47
|
-
if (!targetDir) {
|
|
48
|
-
sendJson(res, 400, { ok: false, error: 'Task does not define a working directory (cwd)' });
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
const result = await applyGitDiff(targetDir, patch);
|
|
52
|
-
sendJson(res, result.ok ? 200 : 500, result);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if ((action === 'doctor' && subAction === 'apply') || action === 'doctor-apply') {
|
|
57
|
-
const task = store.get(id);
|
|
58
|
-
if (!task) {
|
|
59
|
-
sendJson(res, 404, NOT_FOUND);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
const body = await readBody(req, res);
|
|
63
|
-
const bodyData = (body && body.body) || {};
|
|
64
|
-
let fix = bodyData.fix || bodyData.doctorReport;
|
|
65
|
-
if (!fix) {
|
|
66
|
-
const runs = store.getHistory(id, 5);
|
|
67
|
-
const reportRun = runs.find((r) => r.doctorReport);
|
|
68
|
-
fix = reportRun?.doctorReport;
|
|
69
|
-
}
|
|
70
|
-
if (!fix || !fix.suggestedFix) {
|
|
71
|
-
sendJson(res, 400, { ok: false, error: 'No doctor fix found to apply' });
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const result = await applyDoctorFix(task, fix, { store, user: bodyData.user || 'operator' });
|
|
76
|
-
if (!result.ok) {
|
|
77
|
-
sendJson(res, 400, { ok: false, error: result.error });
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
sendJson(res, 200, {
|
|
81
|
-
ok: true,
|
|
82
|
-
taskId: id,
|
|
83
|
-
task: redactTaskSecrets(task),
|
|
84
|
-
revision: result.revision,
|
|
85
|
-
});
|
|
86
|
-
} catch (err) {
|
|
87
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
88
|
-
}
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if ((action === 'doctor' && subAction === 'revert') || action === 'doctor-revert') {
|
|
93
|
-
const task = store.get(id);
|
|
94
|
-
if (!task) {
|
|
95
|
-
sendJson(res, 404, NOT_FOUND);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
const body = await readBody(req, res);
|
|
99
|
-
const bodyData = (body && body.body) || {};
|
|
100
|
-
try {
|
|
101
|
-
const result = await revertDoctorFix(task, { store, user: bodyData.user || 'operator' });
|
|
102
|
-
if (!result.ok) {
|
|
103
|
-
sendJson(res, 400, { ok: false, error: result.error });
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
sendJson(res, 200, {
|
|
107
|
-
ok: true,
|
|
108
|
-
taskId: id,
|
|
109
|
-
task: redactTaskSecrets(task),
|
|
110
|
-
revertedCode: result.revertedCode,
|
|
111
|
-
});
|
|
112
|
-
} catch (err) {
|
|
113
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
114
|
-
}
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if ((action === 'doctor' && subAction === 'diagnose') || action === 'doctor-diagnose') {
|
|
119
|
-
const task = store.get(id);
|
|
120
|
-
if (!task) {
|
|
121
|
-
sendJson(res, 404, NOT_FOUND);
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
const body = await readBody(req, res);
|
|
125
|
-
const bodyData = (body && body.body) || {};
|
|
126
|
-
const runId = bodyData.runId;
|
|
127
|
-
const runs = store.getHistory(id, 10);
|
|
128
|
-
const targetRun = runId ? runs.find((r) => r.id === runId) : runs[0];
|
|
129
|
-
if (!targetRun) {
|
|
130
|
-
sendJson(res, 404, { ok: false, error: 'No run history found to diagnose' });
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const doctorReport = await diagnoseFailure({
|
|
135
|
-
task,
|
|
136
|
-
runInfo: targetRun,
|
|
137
|
-
ask: scheduler?.askModel,
|
|
138
|
-
});
|
|
139
|
-
targetRun.doctorReport = doctorReport;
|
|
140
|
-
sendJson(res, 200, { ok: true, taskId: id, doctorReport });
|
|
141
|
-
} catch (err) {
|
|
142
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
if (action === 'prompt-rollback') {
|
|
147
|
-
const task = store.get(id);
|
|
148
|
-
if (!task) {
|
|
149
|
-
sendJson(res, 404, NOT_FOUND);
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
153
|
-
const targetVersion = Number(body.version || url.searchParams.get('version'));
|
|
154
|
-
if (!targetVersion || targetVersion < 1) {
|
|
155
|
-
sendJson(res, 400, { ok: false, error: 'A valid target prompt version is required' });
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
rollbackPromptVersion(task, targetVersion);
|
|
160
|
-
store.set(task);
|
|
161
|
-
sendJson(res, 200, {
|
|
162
|
-
ok: true,
|
|
163
|
-
task: redactTaskSecrets(task),
|
|
164
|
-
promptVersion: task.promptVersion,
|
|
165
|
-
promptHistory: task.promptHistory,
|
|
166
|
-
});
|
|
167
|
-
} catch (err) {
|
|
168
|
-
sendJson(res, 400, { ok: false, error: err.message });
|
|
169
|
-
}
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
if (action === 'retry-delivery') {
|
|
173
|
-
const task = store.get(id);
|
|
174
|
-
if (!task) {
|
|
175
|
-
sendJson(res, 404, NOT_FOUND);
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
179
|
-
const runId = body.runId || url.searchParams.get('runId');
|
|
180
|
-
const runs = store.history.get(id) || [];
|
|
181
|
-
const run = runId ? runs.find(r => r.id === runId) : runs[0];
|
|
182
|
-
if (!run) {
|
|
183
|
-
sendJson(res, 404, { ok: false, error: 'Run not found' });
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
const delivery = await scheduler.deliverNotifications(task, run);
|
|
187
|
-
run.delivery = delivery;
|
|
188
|
-
if (Array.isArray(run.stages)) {
|
|
189
|
-
const deliveryStage = run.stages.find(s => s.name === 'delivery');
|
|
190
|
-
if (deliveryStage) {
|
|
191
|
-
deliveryStage.status = delivery?.status || 'delivered';
|
|
192
|
-
deliveryStage.delivered = delivery?.delivered;
|
|
193
|
-
deliveryStage.failures = delivery?.failures;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
store.scheduleSave();
|
|
197
|
-
sendJson(res, 200, { ok: true, delivery, run });
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
if (action === 'approve' || action === 'reject') {
|
|
201
|
-
const task = store.get(id);
|
|
202
|
-
if (!task) {
|
|
203
|
-
sendJson(res, 404, NOT_FOUND);
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
const body = await readBodyJson(req).catch(() => ({}));
|
|
207
|
-
const proposalId = body.proposalId || url.searchParams.get('proposalId');
|
|
208
|
-
const proposal = proposalId
|
|
209
|
-
? approvalManager.getProposal(proposalId)
|
|
210
|
-
: approvalManager.getPendingProposalForTask(id);
|
|
211
|
-
|
|
212
|
-
if (!proposal) {
|
|
213
|
-
sendJson(res, 404, { ok: false, error: 'No pending approval proposal found for this task' });
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const decision = action === 'approve' ? 'approve' : 'reject';
|
|
218
|
-
const result = approvalManager.decide(proposal.id, decision, {
|
|
219
|
-
decidedBy: body.decidedBy || 'web-user',
|
|
220
|
-
reason: body.reason || '',
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
if (!result.ok) {
|
|
224
|
-
sendJson(res, 400, result);
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (decision === 'approve') {
|
|
229
|
-
scheduler.runTask(id, { approvedBy: body.decidedBy || 'web-user' }).catch(() => {});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
sendJson(res, 200, { ok: true, proposal: result.proposal });
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
if (action === 'resolve-uncertain') {
|
|
236
|
-
const task = store.get(id);
|
|
237
|
-
if (!task) {
|
|
238
|
-
sendJson(res, 404, NOT_FOUND);
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
const body = await readBody(req).then(b => JSON.parse(b || '{}')).catch(() => ({}));
|
|
242
|
-
const runId = body.runId || url.searchParams.get('runId');
|
|
243
|
-
if (!runId) {
|
|
244
|
-
sendJson(res, 400, { ok: false, error: 'runId is required' });
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
const result = store.durableJournal.resolveUncertain(id, runId, {
|
|
248
|
-
decision: body.decision || 'mark_failed',
|
|
249
|
-
note: body.note || '',
|
|
250
|
-
resolvedBy: body.resolvedBy || 'user',
|
|
251
|
-
});
|
|
252
|
-
if (!result.ok) {
|
|
253
|
-
sendJson(res, 400, result);
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
sendJson(res, 200, result);
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
if (action === 'run') {
|
|
260
|
-
if (!store.get(id)) {
|
|
261
|
-
sendJson(res, 404, NOT_FOUND);
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
await scheduler.triggerManualRun(id);
|
|
265
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(store.get(id)) });
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
if (action === 'duplicate') {
|
|
269
|
-
const source = store.get(id);
|
|
270
|
-
if (!source) {
|
|
271
|
-
sendJson(res, 404, NOT_FOUND);
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
const copy = buildDuplicateTask(source, { id: randomUUID() });
|
|
275
|
-
try {
|
|
276
|
-
store.set(copy);
|
|
277
|
-
} catch (err) {
|
|
278
|
-
sendJson(res, 500, { ok: false, error: 'Failed to persist duplicate task: ' + err.message });
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
// A copy starts paused: it must never fire on its own before the user
|
|
282
|
-
// reviews the schedule (a duplicated one-shot may point at a past time).
|
|
283
|
-
scheduler.pauseTask(copy.id);
|
|
284
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(copy) });
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
if (action === 'pause' || action === 'resume') {
|
|
288
|
-
if (store.get(id) && store.get(id).managedBy === MANAGED_BY_CONFIG) {
|
|
289
|
-
refuseConfigOwned(res, id);
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
const task = action === 'pause' ? scheduler.pauseTask(id) : scheduler.resumeTask(id);
|
|
293
|
-
if (!task) {
|
|
294
|
-
sendJson(res, 404, NOT_FOUND);
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
if (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle')) {
|
|
301
|
-
const stored = store.get(id);
|
|
302
|
-
if (!stored) {
|
|
303
|
-
sendJson(res, 404, NOT_FOUND);
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
if (stored.managedBy === MANAGED_BY_CONFIG) {
|
|
307
|
-
refuseConfigOwned(res, id);
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
const task = scheduler.toggleTask(id);
|
|
311
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
1
|
+
export * from './routes/api-actions.js';
|
package/lib/api-collection.js
CHANGED
|
@@ -1,250 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { parseScheduleExpression } from './scheduler.js';
|
|
3
|
-
import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
|
|
4
|
-
import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
|
|
5
|
-
import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
|
|
6
|
-
import { sendJson, rejectCrossOrigin, readBody, SCRIPT_CONFIRM_HEADER, redactTaskSecrets, restoreTaskSecrets } from './http-utils.js';
|
|
7
|
-
import {
|
|
8
|
-
validateTaskType,
|
|
9
|
-
RESERVED_TASK_IDS,
|
|
10
|
-
isCodeExecutingTask,
|
|
11
|
-
} from './task-transfer.js';
|
|
12
|
-
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
13
|
-
import { sanitizeValidators } from './validators.js';
|
|
14
|
-
|
|
15
|
-
export const NOT_FOUND = { ok: false, error: 'Task not found' };
|
|
16
|
-
export const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A task declared in the profile config is owned by that file (#50): the next
|
|
20
|
-
* plugin start would revert any change made here, so every mutating route
|
|
21
|
-
* refuses it and says where the task really lives.
|
|
22
|
-
*/
|
|
23
|
-
export function refuseConfigOwned(res, id) {
|
|
24
|
-
sendJson(res, 409, { ok: false, error: configOwnedMessage(id) });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ------------------------------------------------------------- collection
|
|
28
|
-
|
|
29
|
-
export async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope, apiToken, ctx }) {
|
|
30
|
-
// The alias family only exists for individual tasks.
|
|
31
|
-
if (scope === 'action' || !collection) {
|
|
32
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (req.method === 'GET') {
|
|
36
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
37
|
-
listTasks({ store, scheduler, collection, req, res, url });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (req.method === 'POST') {
|
|
41
|
-
await createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx: undefined });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function listTasks({ store, scheduler, collection, req, res, url }) {
|
|
48
|
-
const status = url.searchParams.get('status') || 'all';
|
|
49
|
-
const query = url.searchParams.get('query') || '';
|
|
50
|
-
const list = store.list({ status, query }).map((t) => ({
|
|
51
|
-
...redactTaskSecrets(t),
|
|
52
|
-
running: scheduler.isRunning(t.id),
|
|
53
|
-
runningSince: scheduler.runningSince(t.id),
|
|
54
|
-
}));
|
|
55
|
-
const stats = store.getAggregatedStats();
|
|
56
|
-
const payload = { ok: true, tasks: list, recommendations: collection.recommendations, stats };
|
|
57
|
-
|
|
58
|
-
// Calculate ETag based on payload JSON representation (#134)
|
|
59
|
-
const bodyString = JSON.stringify(payload);
|
|
60
|
-
const etag = 'W/"' + createHash('sha1').update(bodyString).digest('hex') + '"';
|
|
61
|
-
|
|
62
|
-
if (req && req.headers) {
|
|
63
|
-
const ifNoneMatch = req.headers['if-none-match'];
|
|
64
|
-
if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === etag.slice(2))) {
|
|
65
|
-
res.writeHead(304, { 'ETag': etag, 'Cache-Control': 'no-cache' });
|
|
66
|
-
res.end();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (typeof res.setHeader === 'function') {
|
|
72
|
-
res.setHeader('ETag', etag);
|
|
73
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
74
|
-
}
|
|
75
|
-
sendJson(res, 200, payload);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function createOrUpdateTask({ store, scheduler, req, res, apiToken, ctx }) {
|
|
79
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
80
|
-
const { body: rawBody, error } = await readBody(req, res);
|
|
81
|
-
if (error) return;
|
|
82
|
-
const existing = rawBody.id ? store.get(rawBody.id) : null;
|
|
83
|
-
const body = existing ? restoreTaskSecrets(rawBody, existing) : rawBody;
|
|
84
|
-
if (!body.title || !body.schedule || !body.prompt) {
|
|
85
|
-
sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const unknownChannels = unknownChannelIds(body.channels);
|
|
89
|
-
if (unknownChannels.length) {
|
|
90
|
-
sendJson(res, 400, {
|
|
91
|
-
ok: false,
|
|
92
|
-
error: 'Unknown channel ids: ' + unknownChannels.join(', '),
|
|
93
|
-
unknownChannels,
|
|
94
|
-
});
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const taskType = normalizeTaskType(body.type);
|
|
98
|
-
if ((CODE_EXECUTING_TYPES.includes(taskType) || isCodeExecutingTask(body)) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
99
|
-
sendJson(res, 403, { ok: false, error: `Creating code-executing tasks over HTTP requires the x-dsh-cron-confirm: script header` });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const typeError = validateTaskType(taskType, body);
|
|
103
|
-
if (typeError) {
|
|
104
|
-
sendJson(res, 400, { ok: false, error: typeError });
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
let parsed;
|
|
108
|
-
try {
|
|
109
|
-
parsed = parseScheduleExpression(body.schedule);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
sendJson(res, 400, { ok: false, error: err.message });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// A client-supplied id must not shadow the collection routes under
|
|
115
|
-
// /dsh-cron/tasks, otherwise that task could never be fetched or deleted
|
|
116
|
-
// again (#42 review finding).
|
|
117
|
-
if (body.id && RESERVED_TASK_IDS.includes(String(body.id))) {
|
|
118
|
-
sendJson(res, 400, { ok: false, error: `Task id "${body.id}" is reserved` });
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
// Create-or-update with an existing id is an update too, so the config-owned
|
|
122
|
-
// refusal must apply here as well — otherwise the POST route would be a way
|
|
123
|
-
// around the guard on PATCH (#50 review finding).
|
|
124
|
-
if (body.id) {
|
|
125
|
-
const existing = store.get(body.id);
|
|
126
|
-
if (existing && existing.managedBy === MANAGED_BY_CONFIG) {
|
|
127
|
-
refuseConfigOwned(res, body.id);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
let task;
|
|
132
|
-
try {
|
|
133
|
-
task = store.set(buildTaskRecord(store, body, taskType, parsed));
|
|
134
|
-
} catch (err) {
|
|
135
|
-
sendJson(res, 500, { ok: false, error: 'Failed to persist task: ' + err.message });
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (task.status === 'active') {
|
|
139
|
-
scheduler.scheduleTask(task);
|
|
140
|
-
} else {
|
|
141
|
-
scheduler.pauseTask(task.id);
|
|
142
|
-
}
|
|
143
|
-
sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Merge a create/update payload onto the stored task, if any. */
|
|
147
|
-
export function buildTaskRecord(store, body, taskType, parsed) {
|
|
148
|
-
const current = body.id ? store.get(body.id) : null;
|
|
149
|
-
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
150
|
-
return {
|
|
151
|
-
id: body.id,
|
|
152
|
-
title: body.title,
|
|
153
|
-
schedule: parsed.cronPattern || body.schedule,
|
|
154
|
-
scheduleText: body.scheduleText || parsed.humanText,
|
|
155
|
-
prompt: body.prompt,
|
|
156
|
-
type: taskType,
|
|
157
|
-
delivery: body.delivery || 'current',
|
|
158
|
-
status: resolvedStatus,
|
|
159
|
-
provider: body.provider || undefined,
|
|
160
|
-
model: body.model || undefined,
|
|
161
|
-
fallbackProvider: body.fallbackProvider || undefined,
|
|
162
|
-
fallbackModel: body.fallbackModel || undefined,
|
|
163
|
-
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : undefined),
|
|
164
|
-
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : undefined),
|
|
165
|
-
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
166
|
-
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
167
|
-
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
168
|
-
timezone: body.timezone !== undefined ? String(body.timezone).trim() : (current ? current.timezone : ''),
|
|
169
|
-
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
170
|
-
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
171
|
-
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
172
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
173
|
-
validators: Array.isArray(body.validators) ? sanitizeValidators(body.validators) : (current ? current.validators : undefined),
|
|
174
|
-
...mergeExecutionFields(current, body, parsed),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// export/import logic moved to ./api-import-export.js
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Runtime and execution settings of a task, merged onto the stored one. Split
|
|
182
|
-
* out of buildTaskRecord so both stay readable.
|
|
183
|
-
*/
|
|
184
|
-
export function mergeExecutionFields(current, body, parsed) {
|
|
185
|
-
return {
|
|
186
|
-
env: body.env && typeof body.env === 'object' ? body.env : (current ? current.env : undefined),
|
|
187
|
-
cwd: body.cwd !== undefined ? String(body.cwd).trim() : (current ? current.cwd : ''),
|
|
188
|
-
channels: Array.isArray(body.channels)
|
|
189
|
-
? body.channels.filter((c) => CHANNEL_IDS.includes(c))
|
|
190
|
-
: (current ? current.channels : undefined),
|
|
191
|
-
template: body.template !== undefined ? String(body.template) : (current ? current.template : ''),
|
|
192
|
-
silentRule: body.silentRule !== undefined ? String(body.silentRule) : (current ? current.silentRule : ''),
|
|
193
|
-
inspectOnFailure: body.inspectOnFailure !== undefined ? Boolean(body.inspectOnFailure) : (current ? Boolean(current.inspectOnFailure) : false),
|
|
194
|
-
workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
|
|
195
|
-
worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
|
|
196
|
-
keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
|
|
197
|
-
captureGitDiff: body.captureGitDiff !== undefined ? Boolean(body.captureGitDiff) : (body.captureDiff !== undefined ? Boolean(body.captureDiff) : (current ? current.captureGitDiff : undefined)),
|
|
198
|
-
httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
|
|
199
|
-
httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
|
|
200
|
-
httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
|
|
201
|
-
httpBody: body.httpBody !== undefined ? String(body.httpBody) : (current ? current.httpBody : ''),
|
|
202
|
-
sshProfileId: body.sshProfileId !== undefined ? String(body.sshProfileId).trim() : (current ? current.sshProfileId : ''),
|
|
203
|
-
sshTarget: body.sshTarget !== undefined ? String(body.sshTarget).trim() : (current ? current.sshTarget : ''),
|
|
204
|
-
sshPort: body.sshPort !== undefined ? Number(body.sshPort) || 0 : (current ? current.sshPort : 0),
|
|
205
|
-
sshKeyPath: body.sshKeyPath !== undefined ? String(body.sshKeyPath).trim() : (current ? current.sshKeyPath : ''),
|
|
206
|
-
dockerImage: body.dockerImage !== undefined ? String(body.dockerImage).trim() : (current ? current.dockerImage : ''),
|
|
207
|
-
pythonPath: body.pythonPath !== undefined ? String(body.pythonPath).trim() : (current ? current.pythonPath : ''),
|
|
208
|
-
nodePath: body.nodePath !== undefined ? String(body.nodePath).trim() : (current ? current.nodePath : ''),
|
|
209
|
-
skillName: body.skillName !== undefined ? String(body.skillName).trim() : (current ? current.skillName : ''),
|
|
210
|
-
workflowName: body.workflowName !== undefined ? String(body.workflowName).trim() : (current ? current.workflowName : ''),
|
|
211
|
-
oneShot: Boolean((parsed && parsed.isOneShot) || (body && body.oneShot)),
|
|
212
|
-
targetTimestamp: parsed && parsed.isOneShot
|
|
213
|
-
? (current && current.schedule === (parsed.cronPattern || body.schedule) ? (current.targetTimestamp || parsed.targetTimestamp) : parsed.targetTimestamp)
|
|
214
|
-
: (current ? current.targetTimestamp : undefined),
|
|
215
|
-
costLimitUsd: body.costLimitUsd !== undefined ? (body.costLimitUsd === null ? null : (Number(body.costLimitUsd) > 0 ? Number(body.costLimitUsd) : null)) : (current ? current.costLimitUsd : undefined),
|
|
216
|
-
dailyCostLimitUsd: body.dailyCostLimitUsd !== undefined ? (body.dailyCostLimitUsd === null ? null : (Number(body.dailyCostLimitUsd) > 0 ? Number(body.dailyCostLimitUsd) : null)) : (current ? current.dailyCostLimitUsd : undefined),
|
|
217
|
-
tokenLimit: body.tokenLimit !== undefined ? (body.tokenLimit === null ? null : (Number(body.tokenLimit) > 0 ? Number(body.tokenLimit) : null)) : (current ? current.tokenLimit : undefined),
|
|
218
|
-
pausedReason: body.pausedReason !== undefined ? (body.pausedReason ? String(body.pausedReason) : null) : (current ? current.pausedReason : undefined),
|
|
219
|
-
agentPreset: body.agentPreset !== undefined ? String(body.agentPreset).trim() : (current ? (current.agentPreset || '') : ''),
|
|
220
|
-
targetSessionId: body.targetSessionId !== undefined ? String(body.targetSessionId).trim() : (current ? (current.targetSessionId || '') : ''),
|
|
221
|
-
targetSessionReset: body.targetSessionReset !== undefined ? String(body.targetSessionReset).trim() : (current ? (current.targetSessionReset || 'never') : 'never'),
|
|
222
|
-
sessionTitle: body.sessionTitle !== undefined ? String(body.sessionTitle).trim() : (current ? (current.sessionTitle || '') : ''),
|
|
223
|
-
webhookEnabled: body.webhookEnabled !== undefined
|
|
224
|
-
? Boolean(body.webhookEnabled)
|
|
225
|
-
: (body.webhookTrigger?.enabled !== undefined ? Boolean(body.webhookTrigger.enabled) : (current ? Boolean(current.webhookEnabled || current.webhookTrigger?.enabled) : false)),
|
|
226
|
-
webhookToken: body.webhookToken !== undefined
|
|
227
|
-
? String(body.webhookToken).trim()
|
|
228
|
-
: (body.webhookTrigger?.token ? String(body.webhookTrigger.token).trim() : (current ? (current.webhookToken || current.webhookTrigger?.token || '') : '')),
|
|
229
|
-
webhookSecret: body.webhookSecret !== undefined
|
|
230
|
-
? String(body.webhookSecret).trim()
|
|
231
|
-
: (body.webhookTrigger?.secret ? String(body.webhookTrigger.secret).trim() : (current ? (current.webhookSecret || current.webhookTrigger?.secret || '') : '')),
|
|
232
|
-
webhookDebounceMs: body.webhookDebounceMs !== undefined
|
|
233
|
-
? (Number(body.webhookDebounceMs) || 0)
|
|
234
|
-
: (body.webhookTrigger?.debounceMs !== undefined ? (Number(body.webhookTrigger.debounceMs) || 0) : (current ? (Number(current.webhookDebounceMs || current.webhookTrigger?.debounceMs) || 0) : 0)),
|
|
235
|
-
onSuccess: body.onSuccess !== undefined ? String(body.onSuccess).trim() : (current ? (current.onSuccess || '') : ''),
|
|
236
|
-
onFailure: body.onFailure !== undefined ? String(body.onFailure).trim() : (current ? (current.onFailure || '') : ''),
|
|
237
|
-
heartbeatIntervalSeconds: body.heartbeatIntervalSeconds !== undefined ? Number(body.heartbeatIntervalSeconds) : (current ? (Number(current.heartbeatIntervalSeconds) || 0) : 0),
|
|
238
|
-
gracePeriodSeconds: body.gracePeriodSeconds !== undefined ? Number(body.gracePeriodSeconds) : (current ? (Number(current.gracePeriodSeconds) || 300) : 300),
|
|
239
|
-
preflightType: body.preflightType !== undefined ? String(body.preflightType).trim().toLowerCase() : (current ? (current.preflightType || 'none') : 'none'),
|
|
240
|
-
preflightTarget: body.preflightTarget !== undefined ? String(body.preflightTarget).trim() : (current ? (current.preflightTarget || '') : ''),
|
|
241
|
-
priority: body.priority !== undefined ? (Number(body.priority) || 5) : (current ? (Number(current.priority) || 5) : 5),
|
|
242
|
-
concurrencyGroup: body.concurrencyGroup !== undefined ? String(body.concurrencyGroup).trim() : (current ? (current.concurrencyGroup || 'default') : 'default'),
|
|
243
|
-
selfHealingCommand: body.selfHealingCommand !== undefined ? String(body.selfHealingCommand).trim() : (current ? (current.selfHealingCommand || '') : ''),
|
|
244
|
-
autoDiagnose: body.autoDiagnose !== undefined ? Boolean(body.autoDiagnose) : (current ? Boolean(current.autoDiagnose) : false),
|
|
245
|
-
adaptive: body.adaptive !== undefined ? sanitizeAdaptiveConfig(body.adaptive) : (current ? current.adaptive : undefined),
|
|
246
|
-
adaptiveState: current ? current.adaptiveState : undefined,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
1
|
+
export * from './routes/api-collection.js';
|
package/lib/api-helpers.js
CHANGED
|
@@ -1,100 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { previewSchedule } from './scheduler.js';
|
|
3
|
-
import { sendJson, readBody, rejectCrossOrigin } from './http-utils.js';
|
|
4
|
-
|
|
5
|
-
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
6
|
-
|
|
7
|
-
export function handleHeartbeatPing({ store, req, res, taskId, apiToken: explicitToken, ctx }) {
|
|
8
|
-
if (req.method !== 'POST') {
|
|
9
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
|
|
13
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
14
|
-
|
|
15
|
-
const task = store.get(taskId);
|
|
16
|
-
if (!task) {
|
|
17
|
-
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const result = typeof store.recordHeartbeat === 'function' ? store.recordHeartbeat(taskId) : null;
|
|
21
|
-
if (!result) {
|
|
22
|
-
sendJson(res, 500, { ok: false, error: 'Could not record heartbeat' });
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
sendJson(res, 200, result);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function handleSchedulePreview({ req, res, url, store, apiToken: explicitToken, ctx }) {
|
|
29
|
-
const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
|
|
30
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
31
|
-
|
|
32
|
-
let schedule = '';
|
|
33
|
-
let timezone = '';
|
|
34
|
-
let count = 5;
|
|
35
|
-
|
|
36
|
-
if (req.method === 'POST') {
|
|
37
|
-
const { body, error } = await readBody(req, res);
|
|
38
|
-
if (error) return;
|
|
39
|
-
schedule = body.schedule || '';
|
|
40
|
-
timezone = body.timezone || '';
|
|
41
|
-
count = body.count || 5;
|
|
42
|
-
} else if (req.method === 'GET') {
|
|
43
|
-
schedule = url.searchParams.get('schedule') || '';
|
|
44
|
-
timezone = url.searchParams.get('timezone') || '';
|
|
45
|
-
count = parseInt(url.searchParams.get('count') || '5', 10);
|
|
46
|
-
} else {
|
|
47
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!schedule) {
|
|
52
|
-
sendJson(res, 400, { ok: false, error: 'Field "schedule" is required' });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const preview = previewSchedule(schedule, { timezone, count });
|
|
58
|
-
sendJson(res, 200, { ok: true, ...preview });
|
|
59
|
-
} catch (err) {
|
|
60
|
-
sendJson(res, 400, { ok: false, error: err.message });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function handleDryRunTask({ store, scheduler, req, res, id, apiToken: explicitToken, ctx }) {
|
|
65
|
-
const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
|
|
66
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
67
|
-
const task = store.get(id);
|
|
68
|
-
if (!task) {
|
|
69
|
-
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const outcome = await scheduler.runTask(id, { dryRun: true });
|
|
75
|
-
sendJson(res, 200, outcome || { ok: true, status: 'completed' });
|
|
76
|
-
} catch (err) {
|
|
77
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function handleScheduleTimeline({ req, res, url, store, scheduler, apiToken: explicitToken, ctx }) {
|
|
82
|
-
const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
|
|
83
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
84
|
-
|
|
85
|
-
if (req.method !== 'GET') {
|
|
86
|
-
sendJson(res, 405, NOT_ALLOWED);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const hours = parseInt(url.searchParams.get('hours') || '24', 10);
|
|
91
|
-
const tasks = typeof store?.list === 'function' ? store.list() : [];
|
|
92
|
-
const maxConcurrent = store?.getSettings?.()?.maxConcurrent || 3;
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const timeline = calculate24hTimeline({ tasks, maxConcurrent, hoursCount: hours });
|
|
96
|
-
sendJson(res, 200, { ok: true, timeline });
|
|
97
|
-
} catch (err) {
|
|
98
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
1
|
+
export * from './routes/api-helpers.js';
|