@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/routes.js
CHANGED
|
@@ -1,549 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { handleTaskWebhookRoute } from './api-task-webhook.js';
|
|
3
|
-
import { calculateCostHeatmap } from './timeline.js';
|
|
4
|
-
import { buildAnalyticsDigest, formatDigestMarkdown, sendAnalyticsDigest } from './analytics-digest.js';
|
|
5
|
-
import { listVerifiedRecipes } from './recipes-catalog.js';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { createCronApiHandler } from './api.js';
|
|
8
|
-
import { createMetricsHandler } from './metrics.js';
|
|
9
|
-
import { createExternalApiHandler, EXTERNAL_API_PREFIX } from './external-api.js';
|
|
10
|
-
import { getModelsHandler } from './models-handler.js';
|
|
11
|
-
import { chatStartHandler } from './chat-start.js';
|
|
12
|
-
import { sendJson, rejectCrossOrigin, readBody, parseJsonBody } from './http-utils.js';
|
|
13
|
-
import { recipesByCategory } from './recipes.js';
|
|
14
|
-
import { sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
|
|
15
|
-
import { createKanbanCard } from './integrations.js';
|
|
16
|
-
import { sanitizeSettingsPayload, applySettingsToScope } from './settings.js';
|
|
17
|
-
import { buildTaskGraphModel } from './dag.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const CLIENT_CSS = readFileSync(new URL('./client.css', import.meta.url), 'utf8');
|
|
21
|
-
const LOCALES_JSON = readFileSync(new URL('./locales.json', import.meta.url), 'utf8');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
25
|
-
// Monthly history archives collection route (#391)
|
|
26
|
-
ctx.effect(() => ctx.webServer.register({
|
|
27
|
-
kind: 'exact',
|
|
28
|
-
path: '/dsh-cron/history/archives',
|
|
29
|
-
handler: (req, res) => {
|
|
30
|
-
if (req.method !== 'GET') {
|
|
31
|
-
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
32
|
-
res.end('GET only');
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
res.writeHead(200, {
|
|
36
|
-
'content-type': 'application/json; charset=utf-8',
|
|
37
|
-
'cache-control': 'no-store',
|
|
38
|
-
});
|
|
39
|
-
const archives = typeof store?.listMonthlyArchives === 'function' ? store.listMonthlyArchives() : [];
|
|
40
|
-
res.end(JSON.stringify({ ok: true, archives }));
|
|
41
|
-
}
|
|
42
|
-
}), 'dsh-cron: /history/archives');
|
|
43
|
-
|
|
44
|
-
// Specific monthly history archive route (#391)
|
|
45
|
-
ctx.effect(() => ctx.webServer.register({
|
|
46
|
-
kind: 'prefix',
|
|
47
|
-
path: '/dsh-cron/history/archive/',
|
|
48
|
-
handler: (req, res) => {
|
|
49
|
-
if (req.method !== 'GET') {
|
|
50
|
-
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
51
|
-
res.end('GET only');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
55
|
-
const month = url.pathname.replace(/^\/dsh-cron\/history\/archive\//, '').replace(/\/$/, '');
|
|
56
|
-
if (!/^\d{4}-\d{2}$/.test(month)) {
|
|
57
|
-
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
|
|
58
|
-
res.end(JSON.stringify({ ok: false, error: 'Invalid month format, expected YYYY-MM' }));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const archive = typeof store?.getMonthlyArchive === 'function' ? store.getMonthlyArchive(month) : null;
|
|
62
|
-
if (!archive) {
|
|
63
|
-
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
|
|
64
|
-
res.end(JSON.stringify({ ok: false, error: `Archive for month "${month}" not found` }));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
res.writeHead(200, {
|
|
68
|
-
'content-type': 'application/json; charset=utf-8',
|
|
69
|
-
'cache-control': 'public, max-age=3600',
|
|
70
|
-
});
|
|
71
|
-
res.end(JSON.stringify({ ok: true, month, archive }));
|
|
72
|
-
}
|
|
73
|
-
}), 'dsh-cron: /history/archive/:month');
|
|
74
|
-
|
|
75
|
-
// Health probe route (#384)
|
|
76
|
-
ctx.effect(() => ctx.webServer.register({
|
|
77
|
-
kind: 'exact',
|
|
78
|
-
path: '/dsh-cron/health',
|
|
79
|
-
handler: (req, res) => {
|
|
80
|
-
if (req.method !== 'GET') {
|
|
81
|
-
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
82
|
-
res.end('GET only');
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
res.writeHead(200, {
|
|
86
|
-
'content-type': 'application/json; charset=utf-8',
|
|
87
|
-
'cache-control': 'no-store'
|
|
88
|
-
});
|
|
89
|
-
const tasks = typeof store?.list === 'function' ? store.list().length : 0;
|
|
90
|
-
res.end(JSON.stringify({
|
|
91
|
-
status: 'ok',
|
|
92
|
-
plugin: '@goodandready/dsh-cron',
|
|
93
|
-
tasks,
|
|
94
|
-
uptimeSec: Math.floor(process.uptime()),
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
}), 'dsh-cron: /health');
|
|
98
|
-
|
|
99
|
-
// Client locales route (#374)
|
|
100
|
-
ctx.effect(() => ctx.webServer.register({
|
|
101
|
-
kind: 'exact',
|
|
102
|
-
path: '/dsh-cron/locales.json',
|
|
103
|
-
handler: (req, res) => {
|
|
104
|
-
if (req.method !== 'GET') {
|
|
105
|
-
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
106
|
-
res.end('GET only');
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
res.writeHead(200, {
|
|
110
|
-
'content-type': 'application/json; charset=utf-8',
|
|
111
|
-
'cache-control': 'public, max-age=300'
|
|
112
|
-
});
|
|
113
|
-
res.end(LOCALES_JSON);
|
|
114
|
-
}
|
|
115
|
-
}), 'dsh-cron: /locales.json');
|
|
116
|
-
|
|
117
|
-
// Client CSS stylesheet route (#374)
|
|
118
|
-
ctx.effect(() => ctx.webServer.register({
|
|
119
|
-
kind: 'exact',
|
|
120
|
-
path: '/dsh-cron/client.css',
|
|
121
|
-
handler: (req, res) => {
|
|
122
|
-
if (req.method !== 'GET') {
|
|
123
|
-
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
|
|
124
|
-
res.end('GET only');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
res.writeHead(200, {
|
|
128
|
-
'content-type': 'text/css; charset=utf-8',
|
|
129
|
-
'cache-control': 'public, max-age=300'
|
|
130
|
-
});
|
|
131
|
-
res.end(CLIENT_CSS);
|
|
132
|
-
}
|
|
133
|
-
}), 'dsh-cron: /client.css');
|
|
134
|
-
|
|
135
|
-
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
136
|
-
ctx.effect(() => ctx.webServer.register({
|
|
137
|
-
kind: 'prefix',
|
|
138
|
-
path: '/dsh-cron/tasks',
|
|
139
|
-
handler: createCronApiHandler(store, scheduler, { recommendations, ctx })
|
|
140
|
-
}), 'dsh-cron: /tasks[...]');
|
|
141
|
-
|
|
142
|
-
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
143
|
-
ctx.effect(() => ctx.webServer.register({
|
|
144
|
-
kind: 'prefix',
|
|
145
|
-
path: '/dsh-cron/action',
|
|
146
|
-
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
147
|
-
}), 'dsh-cron: /action/:id/:action');
|
|
148
|
-
|
|
149
|
-
ctx.effect(() => ctx.webServer.register({
|
|
150
|
-
kind: 'prefix',
|
|
151
|
-
path: '/dsh-cron/telegram',
|
|
152
|
-
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
153
|
-
}), 'dsh-cron: /telegram[...]');
|
|
154
|
-
|
|
155
|
-
// Incoming HTTP Webhook triggers (#348)
|
|
156
|
-
ctx.effect(() => ctx.webServer.register({
|
|
157
|
-
kind: 'prefix',
|
|
158
|
-
path: '/dsh-cron/webhook',
|
|
159
|
-
handler: (req, res) => handleTaskWebhookRoute({ store, scheduler, req, res })
|
|
160
|
-
}), 'dsh-cron: /webhook[...]');
|
|
161
|
-
|
|
162
|
-
ctx.effect(() => ctx.webServer.register({
|
|
163
|
-
kind: 'prefix',
|
|
164
|
-
path: '/dsh-cron/heartbeat-ping',
|
|
165
|
-
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
166
|
-
}), 'dsh-cron: /heartbeat-ping[...]');
|
|
167
|
-
|
|
168
|
-
ctx.effect(() => ctx.webServer.register({
|
|
169
|
-
kind: 'prefix',
|
|
170
|
-
path: '/dsh-cron/schedule',
|
|
171
|
-
handler: createCronApiHandler(store, scheduler, { ctx })
|
|
172
|
-
}), 'dsh-cron: /schedule[...]');
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Chain retry from failed step (#319)
|
|
176
|
-
ctx.effect(() => ctx.webServer.register({
|
|
177
|
-
kind: 'prefix',
|
|
178
|
-
path: '/dsh-cron/chains',
|
|
179
|
-
handler: (req, res) => handleChainRoutes(store, scheduler, req, res, ctx)
|
|
180
|
-
}), 'dsh-cron: /chains[...]');
|
|
181
|
-
|
|
182
|
-
// Run artifacts listing, download and safe preview (#321)
|
|
183
|
-
ctx.effect(() => ctx.webServer.register({
|
|
184
|
-
kind: 'prefix',
|
|
185
|
-
path: '/dsh-cron/runs',
|
|
186
|
-
handler: (req, res) => handleRunArtifactRoutes(store, req, res, ctx)
|
|
187
|
-
}), 'dsh-cron: /runs[...]');
|
|
188
|
-
|
|
189
|
-
// GET /dsh-cron/metrics (#53) — Prometheus text exposition
|
|
190
|
-
ctx.effect(() => ctx.webServer.register({
|
|
191
|
-
kind: 'exact',
|
|
192
|
-
path: '/dsh-cron/metrics',
|
|
193
|
-
handler: createMetricsHandler({ store, scheduler })
|
|
194
|
-
}), 'dsh-cron: /metrics');
|
|
195
|
-
|
|
196
|
-
// External REST API under /dsh-cron/api/* (#54, ADR-0001): the only surface
|
|
197
|
-
// guarded by a bearer token, so CI and host automation can drive the
|
|
198
|
-
// scheduler without a browser session. The token is read per request, so a
|
|
199
|
-
// settings change applies without a restart.
|
|
200
|
-
ctx.effect(() => ctx.webServer.register({
|
|
201
|
-
kind: 'prefix',
|
|
202
|
-
path: EXTERNAL_API_PREFIX,
|
|
203
|
-
handler: createExternalApiHandler({
|
|
204
|
-
store,
|
|
205
|
-
scheduler,
|
|
206
|
-
getToken: () => store.getSettings().apiToken || ''
|
|
207
|
-
})
|
|
208
|
-
}), 'dsh-cron: /api[...]');
|
|
209
|
-
|
|
210
|
-
// GET /dsh-cron/models
|
|
211
|
-
ctx.effect(() => ctx.webServer.register({
|
|
212
|
-
kind: 'exact',
|
|
213
|
-
path: '/dsh-cron/models',
|
|
214
|
-
handler: async (req, res) => {
|
|
215
|
-
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
216
|
-
const apiToken = settings.apiToken;
|
|
217
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
218
|
-
await getModelsHandler(ctx, req, res, sendJson);
|
|
219
|
-
}
|
|
220
|
-
}), 'dsh-cron: /models');
|
|
221
|
-
|
|
222
|
-
// POST /dsh-cron/chat/start
|
|
223
|
-
ctx.effect(() => ctx.webServer.register({
|
|
224
|
-
kind: 'exact',
|
|
225
|
-
path: '/dsh-cron/chat/start',
|
|
226
|
-
handler: async (req, res) => {
|
|
227
|
-
if (req.method === 'POST' && rejectCrossOrigin(req, res, { ctx })) return;
|
|
228
|
-
await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
|
|
229
|
-
}
|
|
230
|
-
}), 'dsh-cron: /chat/start');
|
|
231
|
-
|
|
232
|
-
// Analytics digest routes (#330)
|
|
233
|
-
ctx.effect(() => ctx.webServer.register({
|
|
234
|
-
kind: 'prefix',
|
|
235
|
-
path: '/dsh-cron/analytics',
|
|
236
|
-
handler: async (req, res) => {
|
|
237
|
-
try {
|
|
238
|
-
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
239
|
-
const apiToken = settings.apiToken;
|
|
240
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
241
|
-
|
|
242
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
243
|
-
const path = url.pathname;
|
|
244
|
-
|
|
245
|
-
if (req.method === 'GET') {
|
|
246
|
-
if (path === '/dsh-cron/analytics/heatmap') {
|
|
247
|
-
const modelFilter = url.searchParams.get('model') || null;
|
|
248
|
-
const providerFilter = url.searchParams.get('provider') || null;
|
|
249
|
-
const costLedger = store.costLedger || [];
|
|
250
|
-
let history = [];
|
|
251
|
-
if (store.history) {
|
|
252
|
-
if (typeof store.history.values === 'function') {
|
|
253
|
-
for (const runs of store.history.values()) {
|
|
254
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
255
|
-
}
|
|
256
|
-
} else if (typeof store.history === 'object') {
|
|
257
|
-
for (const runs of Object.values(store.history)) {
|
|
258
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const heatmap = calculateCostHeatmap({ costLedger, history, modelFilter, providerFilter });
|
|
263
|
-
sendJson(res, 200, { ok: true, heatmap });
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const days = parseInt(url.searchParams.get('days') || '7', 10);
|
|
267
|
-
const tasks = store.list ? store.list() : Object.values(store.tasks || {});
|
|
268
|
-
let history = [];
|
|
269
|
-
if (store.history) {
|
|
270
|
-
if (typeof store.history.values === 'function') {
|
|
271
|
-
for (const runs of store.history.values()) {
|
|
272
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
273
|
-
}
|
|
274
|
-
} else if (typeof store.history === 'object') {
|
|
275
|
-
for (const runs of Object.values(store.history)) {
|
|
276
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const costLedger = store.costLedger || [];
|
|
281
|
-
const digest = buildAnalyticsDigest({ tasks, history, costLedger, periodDays: days });
|
|
282
|
-
const markdown = formatDigestMarkdown(digest);
|
|
283
|
-
sendJson(res, 200, { ok: true, digest, markdown });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (req.method === 'POST') {
|
|
288
|
-
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
289
|
-
const { body } = await readBody(req, res);
|
|
290
|
-
const days = parseInt(body?.days || url.searchParams.get('days') || '7', 10);
|
|
291
|
-
const tasks = store.list ? store.list() : Object.values(store.tasks || {});
|
|
292
|
-
let history = [];
|
|
293
|
-
if (store.history) {
|
|
294
|
-
if (typeof store.history.values === 'function') {
|
|
295
|
-
for (const runs of store.history.values()) {
|
|
296
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
297
|
-
}
|
|
298
|
-
} else if (typeof store.history === 'object') {
|
|
299
|
-
for (const runs of Object.values(store.history)) {
|
|
300
|
-
if (Array.isArray(runs)) history.push(...runs);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const costLedger = store.costLedger || [];
|
|
305
|
-
const digest = buildAnalyticsDigest({ tasks, history, costLedger, periodDays: days });
|
|
306
|
-
const settings = store.getSettings ? store.getSettings() : {};
|
|
307
|
-
const deliveryConfig = {
|
|
308
|
-
telegram: body?.telegram !== false && Boolean(settings.botToken && settings.chatId),
|
|
309
|
-
webhookUrl: body?.webhookUrl || settings.webhookUrl,
|
|
310
|
-
};
|
|
311
|
-
const channels = {
|
|
312
|
-
sendTelegram: async (text) => sendTelegramMessage({ botToken: settings.botToken, chatId: settings.chatId, text }),
|
|
313
|
-
sendWebhook: async (targetUrl, payload) => {
|
|
314
|
-
const r = await fetch(targetUrl, {
|
|
315
|
-
method: 'POST',
|
|
316
|
-
headers: { 'Content-Type': 'application/json' },
|
|
317
|
-
body: JSON.stringify(payload),
|
|
318
|
-
});
|
|
319
|
-
return { ok: r.ok, status: r.status };
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
const delivery = await sendAnalyticsDigest({ digest, channels, deliveryConfig });
|
|
323
|
-
sendJson(res, 200, { ok: true, digest, delivery });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
328
|
-
} catch (err) {
|
|
329
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}), 'dsh-cron: /analytics');
|
|
333
|
-
|
|
334
|
-
// GET /dsh-cron/recipes (#48): the built-in recipe catalog.
|
|
335
|
-
ctx.effect(() => ctx.webServer.register({
|
|
336
|
-
kind: 'exact',
|
|
337
|
-
path: '/dsh-cron/recipes',
|
|
338
|
-
handler: async (req, res) => {
|
|
339
|
-
try {
|
|
340
|
-
if ((req.method || 'GET') !== 'GET') {
|
|
341
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
const settings = store.getSettings ? store.getSettings() : {};
|
|
345
|
-
const apiToken = settings.apiToken;
|
|
346
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
347
|
-
const categories = recipesByCategory();
|
|
348
|
-
const recipes = listVerifiedRecipes(settings);
|
|
349
|
-
sendJson(res, 200, { ok: true, categories, recipes });
|
|
350
|
-
} catch (err) {
|
|
351
|
-
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}), 'dsh-cron: /recipes');
|
|
355
|
-
|
|
356
|
-
// GET /dsh-cron/heartbeat (#16): liveness probe for external monitors.
|
|
357
|
-
ctx.effect(() => ctx.webServer.register({
|
|
358
|
-
kind: 'exact',
|
|
359
|
-
path: '/dsh-cron/heartbeat',
|
|
360
|
-
handler: async (req, res) => {
|
|
361
|
-
try {
|
|
362
|
-
if ((req.method || 'GET') !== 'GET') {
|
|
363
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const settings = store.getSettings ? store.getSettings() : {};
|
|
367
|
-
const apiToken = settings.apiToken;
|
|
368
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
369
|
-
|
|
370
|
-
const tasks = store.list({ status: 'active' });
|
|
371
|
-
const lastRuns = tasks.map((t) => t.lastRunAt || 0);
|
|
372
|
-
sendJson(res, 200, {
|
|
373
|
-
ok: true,
|
|
374
|
-
activeTasks: tasks.length,
|
|
375
|
-
running: scheduler.runningCount(),
|
|
376
|
-
lastRunAt: lastRuns.length ? Math.max(...lastRuns) : null,
|
|
377
|
-
serverTime: Date.now(),
|
|
378
|
-
});
|
|
379
|
-
} catch (err) {
|
|
380
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}), 'dsh-cron: /heartbeat');
|
|
384
|
-
|
|
385
|
-
// POST /dsh-cron/telegram/test
|
|
386
|
-
ctx.effect(() => ctx.webServer.register({
|
|
387
|
-
kind: 'exact',
|
|
388
|
-
path: '/dsh-cron/telegram/test',
|
|
389
|
-
handler: async (req, res) => {
|
|
390
|
-
try {
|
|
391
|
-
if (req.method !== 'POST') {
|
|
392
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
396
|
-
const { body, error } = await readBody(req, res);
|
|
397
|
-
if (error) return;
|
|
398
|
-
const settings = store.getSettings();
|
|
399
|
-
const botToken = body.botToken || settings.botToken;
|
|
400
|
-
const chatId = body.chatId || settings.chatId;
|
|
401
|
-
|
|
402
|
-
if (!botToken || !chatId) {
|
|
403
|
-
sendJson(res, 400, { ok: false, error: 'botToken or chatId is not configured' });
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const text = `🔔 *dsh-cron test notification*\n\nConnection between DSH Cron and Telegram is working! Checked at: \`${new Date().toISOString()}\``;
|
|
408
|
-
const replyMarkup = createTelegramTaskKeyboard(undefined, undefined);
|
|
409
|
-
await sendTelegramMessage({ botToken, chatId, text, replyMarkup });
|
|
410
|
-
sendJson(res, 200, { ok: true, message: 'Test message sent successfully' });
|
|
411
|
-
} catch (err) {
|
|
412
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}), 'dsh-cron: /telegram/test');
|
|
416
|
-
|
|
417
|
-
// POST /dsh-cron/kanban/test
|
|
418
|
-
ctx.effect(() => ctx.webServer.register({
|
|
419
|
-
kind: 'exact',
|
|
420
|
-
path: '/dsh-cron/kanban/test',
|
|
421
|
-
handler: async (req, res) => {
|
|
422
|
-
try {
|
|
423
|
-
if (req.method !== 'POST') {
|
|
424
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (rejectCrossOrigin(req, res, { ctx })) return;
|
|
428
|
-
const { body, error } = await readBody(req, res);
|
|
429
|
-
if (error) return;
|
|
430
|
-
const settings = store.getSettings();
|
|
431
|
-
const kanbanBaseUrl = body.kanbanBaseUrl || settings.kanbanBaseUrl || 'http://127.0.0.1:3000';
|
|
432
|
-
|
|
433
|
-
const result = await createKanbanCard({
|
|
434
|
-
title: '[Test] dsh-cron -> Kanban connectivity check',
|
|
435
|
-
body: `Test card created automatically by the \`dsh-cron\` module at ${new Date().toISOString()}.`,
|
|
436
|
-
board: 'main',
|
|
437
|
-
column: 'backlog',
|
|
438
|
-
labels: ['cron', 'test'],
|
|
439
|
-
kanbanBaseUrl
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
if (result.success) {
|
|
443
|
-
sendJson(res, 200, { ok: true, task: result.task });
|
|
444
|
-
} else {
|
|
445
|
-
sendJson(res, 500, { ok: false, error: result.error });
|
|
446
|
-
}
|
|
447
|
-
} catch (err) {
|
|
448
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}), 'dsh-cron: /kanban/test');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Handler for chain step retries (#319)
|
|
457
|
-
*/
|
|
458
|
-
export async function handleChainRoutes(store, scheduler, req, res, ctx) {
|
|
459
|
-
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
460
|
-
const apiToken = settings.apiToken;
|
|
461
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
462
|
-
const url = new URL(req.url, 'http://localhost');
|
|
463
|
-
|
|
464
|
-
// GET /dsh-cron/chains/:chainRunId (#334)
|
|
465
|
-
const getMatch = url.pathname.match(/^\/dsh-cron\/chains\/([^\/]+)$/);
|
|
466
|
-
if (getMatch && req.method === 'GET') {
|
|
467
|
-
const chainRunId = getMatch[1];
|
|
468
|
-
const snapshots = store.chainCheckpointManager ? store.chainCheckpointManager.getChainSnapshots(chainRunId) : [];
|
|
469
|
-
const relevantTasks = [];
|
|
470
|
-
const stepIds = new Set(snapshots.map((s) => s.stepTaskId));
|
|
471
|
-
if (store.tasks) {
|
|
472
|
-
for (const t of Object.values(store.tasks)) {
|
|
473
|
-
if (t && (stepIds.has(t.id) || t.onSuccess || t.onFailure || t.dependsOn)) {
|
|
474
|
-
relevantTasks.push(t);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
const graph = buildTaskGraphModel(relevantTasks, snapshots);
|
|
479
|
-
sendJson(res, 200, { ok: true, chainRunId, snapshots, graph });
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// POST /dsh-cron/chains/:chainRunId/retry-from/:stepTaskId (#319, #328)
|
|
484
|
-
const m = url.pathname.match(/^\/dsh-cron\/chains\/([^\/]+)\/retry-from\/([^\/]+)$/);
|
|
485
|
-
if (!m) {
|
|
486
|
-
sendJson(res, 404, { ok: false, error: 'Route not found' });
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (req.method !== 'POST') {
|
|
490
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const [, chainRunId, stepTaskId] = m;
|
|
494
|
-
const result = await store.chainCheckpointManager.retryChainFromStep(scheduler, chainRunId, stepTaskId);
|
|
495
|
-
if (!result.ok) {
|
|
496
|
-
sendJson(res, 400, result);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
sendJson(res, 200, result);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Handler for run artifacts (#321)
|
|
504
|
-
*/
|
|
505
|
-
export async function handleRunArtifactRoutes(store, req, res, ctx) {
|
|
506
|
-
const url = new URL(req.url, 'http://localhost');
|
|
507
|
-
const m = url.pathname.match(/^\/dsh-cron\/runs\/([^\/]+)\/artifacts(?:\/([^\/]+)(?:\/(preview))?)?$/);
|
|
508
|
-
if (!m) {
|
|
509
|
-
sendJson(res, 404, { ok: false, error: 'Route not found' });
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (req.method !== 'GET') {
|
|
513
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
517
|
-
const apiToken = settings.apiToken;
|
|
518
|
-
if (rejectCrossOrigin(req, res, { apiToken, ctx })) return;
|
|
519
|
-
|
|
520
|
-
const [, runId, artifactId, action] = m;
|
|
521
|
-
if (!artifactId) {
|
|
522
|
-
const artifacts = store.getArtifactsForRun(runId);
|
|
523
|
-
sendJson(res, 200, { ok: true, runId, artifacts });
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const artifacts = store.getArtifactsForRun(runId);
|
|
527
|
-
const record = artifacts.find((a) => a.id === artifactId);
|
|
528
|
-
if (!record) {
|
|
529
|
-
sendJson(res, 404, { ok: false, error: 'Artifact record not found' });
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (action === 'preview') {
|
|
533
|
-
const preview = store.artifactManager.generatePreview(artifactId, record.name);
|
|
534
|
-
sendJson(res, preview.ok ? 200 : 404, preview);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const buf = store.artifactManager.readArtifact(artifactId, record.name);
|
|
538
|
-
if (!buf) {
|
|
539
|
-
sendJson(res, 404, { ok: false, error: 'Artifact file missing from disk (expired or removed)' });
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
res.writeHead(200, {
|
|
543
|
-
'Content-Type': record.mimeType || 'application/octet-stream',
|
|
544
|
-
'Content-Disposition': `attachment; filename="${encodeURIComponent(record.name)}"`,
|
|
545
|
-
'Content-Length': buf.length,
|
|
546
|
-
'X-Content-Type-Options': 'nosniff',
|
|
547
|
-
});
|
|
548
|
-
res.end(buf);
|
|
549
|
-
}
|
|
1
|
+
export * from './routes/routes.js';
|