@donkeylabs/server 2.0.7 → 2.0.10
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/docs/lifecycle-hooks.md +16 -16
- package/docs/processes.md +93 -0
- package/package.json +13 -3
- package/src/admin/dashboard.ts +717 -0
- package/src/admin/index.ts +85 -0
- package/src/admin/routes.ts +573 -0
- package/src/admin/styles.ts +422 -0
- package/src/core/index.ts +25 -0
- package/src/core/job-adapter-kysely.ts +22 -1
- package/src/core/job-adapter-sqlite.ts +22 -1
- package/src/core/jobs.ts +37 -0
- package/src/core/process-client.ts +121 -0
- package/src/core/processes.ts +67 -0
- package/src/core/storage-adapter-local.ts +403 -0
- package/src/core/storage-adapter-s3.ts +409 -0
- package/src/core/storage.ts +543 -0
- package/src/core/websocket.ts +13 -3
- package/src/core/workflow-adapter-kysely.ts +22 -1
- package/src/core/workflows.ts +37 -0
- package/src/core.ts +10 -1
- package/src/harness.ts +3 -0
- package/src/index.ts +19 -0
- package/src/process-client.ts +7 -0
- package/src/server.ts +71 -31
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Dashboard Styles
|
|
3
|
+
* Dark theme, minimal CSS (no build step)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const adminStyles = `
|
|
7
|
+
:root {
|
|
8
|
+
--bg-primary: #0f0f0f;
|
|
9
|
+
--bg-secondary: #1a1a1a;
|
|
10
|
+
--bg-tertiary: #252525;
|
|
11
|
+
--text-primary: #e0e0e0;
|
|
12
|
+
--text-secondary: #999;
|
|
13
|
+
--text-muted: #666;
|
|
14
|
+
--border-color: #333;
|
|
15
|
+
--accent-blue: #3b82f6;
|
|
16
|
+
--accent-green: #22c55e;
|
|
17
|
+
--accent-yellow: #eab308;
|
|
18
|
+
--accent-red: #ef4444;
|
|
19
|
+
--accent-purple: #a855f7;
|
|
20
|
+
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', monospace;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
color: var(--text-primary);
|
|
34
|
+
background: var(--bg-primary);
|
|
35
|
+
min-height: 100vh;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.admin-container {
|
|
39
|
+
display: flex;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Sidebar */
|
|
44
|
+
.sidebar {
|
|
45
|
+
width: 220px;
|
|
46
|
+
background: var(--bg-secondary);
|
|
47
|
+
border-right: 1px solid var(--border-color);
|
|
48
|
+
padding: 20px 0;
|
|
49
|
+
position: fixed;
|
|
50
|
+
height: 100vh;
|
|
51
|
+
overflow-y: auto;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sidebar-header {
|
|
55
|
+
padding: 0 20px 20px;
|
|
56
|
+
border-bottom: 1px solid var(--border-color);
|
|
57
|
+
margin-bottom: 10px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sidebar-title {
|
|
61
|
+
font-size: 16px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sidebar-title svg {
|
|
69
|
+
width: 20px;
|
|
70
|
+
height: 20px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.nav-section {
|
|
74
|
+
padding: 10px 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.nav-section-title {
|
|
78
|
+
font-size: 11px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
color: var(--text-muted);
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
letter-spacing: 0.5px;
|
|
83
|
+
padding: 8px 20px 6px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.nav-item {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 10px;
|
|
90
|
+
padding: 10px 20px;
|
|
91
|
+
color: var(--text-secondary);
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
transition: all 0.15s;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
border-left: 3px solid transparent;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.nav-item:hover {
|
|
99
|
+
background: var(--bg-tertiary);
|
|
100
|
+
color: var(--text-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.nav-item.active {
|
|
104
|
+
background: rgba(59, 130, 246, 0.1);
|
|
105
|
+
color: var(--accent-blue);
|
|
106
|
+
border-left-color: var(--accent-blue);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.nav-item svg {
|
|
110
|
+
width: 16px;
|
|
111
|
+
height: 16px;
|
|
112
|
+
opacity: 0.7;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Main content */
|
|
116
|
+
.main-content {
|
|
117
|
+
flex: 1;
|
|
118
|
+
margin-left: 220px;
|
|
119
|
+
padding: 24px;
|
|
120
|
+
min-height: 100vh;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.page-header {
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: space-between;
|
|
126
|
+
align-items: center;
|
|
127
|
+
margin-bottom: 24px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.page-title {
|
|
131
|
+
font-size: 24px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Stats grid */
|
|
136
|
+
.stats-grid {
|
|
137
|
+
display: grid;
|
|
138
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
139
|
+
gap: 16px;
|
|
140
|
+
margin-bottom: 24px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.stat-card {
|
|
144
|
+
background: var(--bg-secondary);
|
|
145
|
+
border: 1px solid var(--border-color);
|
|
146
|
+
border-radius: 8px;
|
|
147
|
+
padding: 20px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.stat-label {
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: var(--text-muted);
|
|
153
|
+
text-transform: uppercase;
|
|
154
|
+
letter-spacing: 0.5px;
|
|
155
|
+
margin-bottom: 8px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.stat-value {
|
|
159
|
+
font-size: 28px;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
font-family: var(--font-mono);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.stat-value.green { color: var(--accent-green); }
|
|
165
|
+
.stat-value.yellow { color: var(--accent-yellow); }
|
|
166
|
+
.stat-value.red { color: var(--accent-red); }
|
|
167
|
+
.stat-value.blue { color: var(--accent-blue); }
|
|
168
|
+
.stat-value.purple { color: var(--accent-purple); }
|
|
169
|
+
|
|
170
|
+
/* Cards */
|
|
171
|
+
.card {
|
|
172
|
+
background: var(--bg-secondary);
|
|
173
|
+
border: 1px solid var(--border-color);
|
|
174
|
+
border-radius: 8px;
|
|
175
|
+
margin-bottom: 24px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.card-header {
|
|
179
|
+
display: flex;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
align-items: center;
|
|
182
|
+
padding: 16px 20px;
|
|
183
|
+
border-bottom: 1px solid var(--border-color);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.card-title {
|
|
187
|
+
font-size: 16px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.card-body {
|
|
192
|
+
padding: 16px 20px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Tables */
|
|
196
|
+
.table-container {
|
|
197
|
+
overflow-x: auto;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
table {
|
|
201
|
+
width: 100%;
|
|
202
|
+
border-collapse: collapse;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
th, td {
|
|
206
|
+
text-align: left;
|
|
207
|
+
padding: 12px 16px;
|
|
208
|
+
border-bottom: 1px solid var(--border-color);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
th {
|
|
212
|
+
font-size: 11px;
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
color: var(--text-muted);
|
|
215
|
+
text-transform: uppercase;
|
|
216
|
+
letter-spacing: 0.5px;
|
|
217
|
+
background: var(--bg-tertiary);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tr:hover td {
|
|
221
|
+
background: rgba(255, 255, 255, 0.02);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
td {
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Status badges */
|
|
229
|
+
.badge {
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
padding: 4px 8px;
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
font-size: 11px;
|
|
235
|
+
font-weight: 500;
|
|
236
|
+
text-transform: uppercase;
|
|
237
|
+
letter-spacing: 0.3px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.badge-pending { background: rgba(234, 179, 8, 0.2); color: var(--accent-yellow); }
|
|
241
|
+
.badge-running { background: rgba(59, 130, 246, 0.2); color: var(--accent-blue); }
|
|
242
|
+
.badge-completed { background: rgba(34, 197, 94, 0.2); color: var(--accent-green); }
|
|
243
|
+
.badge-failed { background: rgba(239, 68, 68, 0.2); color: var(--accent-red); }
|
|
244
|
+
.badge-cancelled { background: rgba(156, 163, 175, 0.2); color: var(--text-muted); }
|
|
245
|
+
.badge-scheduled { background: rgba(168, 85, 247, 0.2); color: var(--accent-purple); }
|
|
246
|
+
|
|
247
|
+
/* Buttons */
|
|
248
|
+
.btn {
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 6px;
|
|
252
|
+
padding: 8px 16px;
|
|
253
|
+
border: 1px solid var(--border-color);
|
|
254
|
+
border-radius: 6px;
|
|
255
|
+
background: var(--bg-tertiary);
|
|
256
|
+
color: var(--text-primary);
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
font-weight: 500;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
transition: all 0.15s;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.btn:hover {
|
|
264
|
+
background: var(--bg-secondary);
|
|
265
|
+
border-color: var(--text-muted);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.btn-sm {
|
|
269
|
+
padding: 4px 10px;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.btn-danger {
|
|
274
|
+
background: rgba(239, 68, 68, 0.1);
|
|
275
|
+
border-color: var(--accent-red);
|
|
276
|
+
color: var(--accent-red);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.btn-danger:hover {
|
|
280
|
+
background: rgba(239, 68, 68, 0.2);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.btn-primary {
|
|
284
|
+
background: var(--accent-blue);
|
|
285
|
+
border-color: var(--accent-blue);
|
|
286
|
+
color: white;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.btn-primary:hover {
|
|
290
|
+
background: #2563eb;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Filters */
|
|
294
|
+
.filters {
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 12px;
|
|
297
|
+
margin-bottom: 16px;
|
|
298
|
+
flex-wrap: wrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.filter-select {
|
|
302
|
+
padding: 8px 12px;
|
|
303
|
+
border: 1px solid var(--border-color);
|
|
304
|
+
border-radius: 6px;
|
|
305
|
+
background: var(--bg-tertiary);
|
|
306
|
+
color: var(--text-primary);
|
|
307
|
+
font-size: 13px;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.filter-select:focus {
|
|
312
|
+
outline: none;
|
|
313
|
+
border-color: var(--accent-blue);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Code/mono text */
|
|
317
|
+
.mono {
|
|
318
|
+
font-family: var(--font-mono);
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.code-block {
|
|
323
|
+
background: var(--bg-primary);
|
|
324
|
+
border: 1px solid var(--border-color);
|
|
325
|
+
border-radius: 6px;
|
|
326
|
+
padding: 16px;
|
|
327
|
+
font-family: var(--font-mono);
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
overflow-x: auto;
|
|
330
|
+
white-space: pre-wrap;
|
|
331
|
+
word-break: break-all;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Empty state */
|
|
335
|
+
.empty-state {
|
|
336
|
+
text-align: center;
|
|
337
|
+
padding: 48px;
|
|
338
|
+
color: var(--text-muted);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.empty-state svg {
|
|
342
|
+
width: 48px;
|
|
343
|
+
height: 48px;
|
|
344
|
+
margin-bottom: 16px;
|
|
345
|
+
opacity: 0.5;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Responsive */
|
|
349
|
+
@media (max-width: 768px) {
|
|
350
|
+
.sidebar {
|
|
351
|
+
width: 60px;
|
|
352
|
+
padding: 10px 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.sidebar-header,
|
|
356
|
+
.nav-section-title,
|
|
357
|
+
.nav-item span {
|
|
358
|
+
display: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.nav-item {
|
|
362
|
+
padding: 12px;
|
|
363
|
+
justify-content: center;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.main-content {
|
|
367
|
+
margin-left: 60px;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Loading indicator */
|
|
372
|
+
.htmx-indicator {
|
|
373
|
+
opacity: 0;
|
|
374
|
+
transition: opacity 0.2s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.htmx-request .htmx-indicator {
|
|
378
|
+
opacity: 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.spinner {
|
|
382
|
+
width: 16px;
|
|
383
|
+
height: 16px;
|
|
384
|
+
border: 2px solid var(--border-color);
|
|
385
|
+
border-top-color: var(--accent-blue);
|
|
386
|
+
border-radius: 50%;
|
|
387
|
+
animation: spin 0.8s linear infinite;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@keyframes spin {
|
|
391
|
+
to { transform: rotate(360deg); }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Truncate text */
|
|
395
|
+
.truncate {
|
|
396
|
+
white-space: nowrap;
|
|
397
|
+
overflow: hidden;
|
|
398
|
+
text-overflow: ellipsis;
|
|
399
|
+
max-width: 200px;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* Relative time */
|
|
403
|
+
.relative-time {
|
|
404
|
+
color: var(--text-muted);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Pulse animation for live indicators */
|
|
408
|
+
.pulse {
|
|
409
|
+
animation: pulse 2s ease-in-out infinite;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@keyframes pulse {
|
|
413
|
+
0%, 100% { opacity: 1; }
|
|
414
|
+
50% { opacity: 0.5; }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Action buttons in tables */
|
|
418
|
+
.action-btns {
|
|
419
|
+
display: flex;
|
|
420
|
+
gap: 8px;
|
|
421
|
+
}
|
|
422
|
+
`;
|
package/src/core/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
type JobHandler,
|
|
44
44
|
type JobAdapter,
|
|
45
45
|
type JobsConfig,
|
|
46
|
+
type GetAllJobsOptions,
|
|
46
47
|
MemoryJobAdapter,
|
|
47
48
|
createJobs,
|
|
48
49
|
} from "./jobs";
|
|
@@ -146,6 +147,7 @@ export {
|
|
|
146
147
|
type ChoiceCondition,
|
|
147
148
|
type PassStepDefinition,
|
|
148
149
|
type RetryConfig,
|
|
150
|
+
type GetAllWorkflowsOptions,
|
|
149
151
|
WorkflowBuilder,
|
|
150
152
|
MemoryWorkflowAdapter,
|
|
151
153
|
workflow,
|
|
@@ -216,3 +218,26 @@ export {
|
|
|
216
218
|
type WebSocketConfig,
|
|
217
219
|
createWebSocket,
|
|
218
220
|
} from "./websocket";
|
|
221
|
+
|
|
222
|
+
export {
|
|
223
|
+
type Storage,
|
|
224
|
+
type StorageAdapter,
|
|
225
|
+
type StorageConfig,
|
|
226
|
+
type StorageFile,
|
|
227
|
+
type UploadOptions,
|
|
228
|
+
type UploadResult,
|
|
229
|
+
type DownloadResult,
|
|
230
|
+
type ListOptions,
|
|
231
|
+
type ListResult,
|
|
232
|
+
type GetUrlOptions,
|
|
233
|
+
type CopyOptions,
|
|
234
|
+
type StorageVisibility,
|
|
235
|
+
type S3ProviderConfig,
|
|
236
|
+
type LocalProviderConfig,
|
|
237
|
+
type MemoryProviderConfig,
|
|
238
|
+
MemoryStorageAdapter,
|
|
239
|
+
createStorage,
|
|
240
|
+
} from "./storage";
|
|
241
|
+
|
|
242
|
+
export { LocalStorageAdapter } from "./storage-adapter-local";
|
|
243
|
+
export { S3StorageAdapter } from "./storage-adapter-s3";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Kysely } from "kysely";
|
|
9
|
-
import type { Job, JobAdapter, JobStatus } from "./jobs";
|
|
9
|
+
import type { Job, JobAdapter, JobStatus, GetAllJobsOptions } from "./jobs";
|
|
10
10
|
import type { ExternalJobProcessState } from "./external-jobs";
|
|
11
11
|
|
|
12
12
|
export interface KyselyJobAdapterConfig {
|
|
@@ -231,6 +231,27 @@ export class KyselyJobAdapter implements JobAdapter {
|
|
|
231
231
|
return rows.map((r) => this.rowToJob(r));
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
async getAll(options: GetAllJobsOptions = {}): Promise<Job[]> {
|
|
235
|
+
const { status, name, limit = 100, offset = 0 } = options;
|
|
236
|
+
|
|
237
|
+
let query = this.db.selectFrom("__donkeylabs_jobs__").selectAll();
|
|
238
|
+
|
|
239
|
+
if (status) {
|
|
240
|
+
query = query.where("status", "=", status);
|
|
241
|
+
}
|
|
242
|
+
if (name) {
|
|
243
|
+
query = query.where("name", "=", name);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const rows = await query
|
|
247
|
+
.orderBy("created_at", "desc")
|
|
248
|
+
.limit(limit)
|
|
249
|
+
.offset(offset)
|
|
250
|
+
.execute();
|
|
251
|
+
|
|
252
|
+
return rows.map((r) => this.rowToJob(r));
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
private rowToJob(row: JobsTable): Job {
|
|
235
256
|
return {
|
|
236
257
|
id: row.id,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Database } from "bun:sqlite";
|
|
9
9
|
import { mkdir } from "node:fs/promises";
|
|
10
10
|
import { dirname } from "node:path";
|
|
11
|
-
import type { Job, JobAdapter, JobStatus } from "./jobs";
|
|
11
|
+
import type { Job, JobAdapter, JobStatus, GetAllJobsOptions } from "./jobs";
|
|
12
12
|
import type { ExternalJobProcessState } from "./external-jobs";
|
|
13
13
|
|
|
14
14
|
export interface SqliteJobAdapterConfig {
|
|
@@ -232,6 +232,27 @@ export class SqliteJobAdapter implements JobAdapter {
|
|
|
232
232
|
return rows.map((r) => this.rowToJob(r));
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
async getAll(options: GetAllJobsOptions = {}): Promise<Job[]> {
|
|
236
|
+
const { status, name, limit = 100, offset = 0 } = options;
|
|
237
|
+
let query = `SELECT * FROM jobs WHERE 1=1`;
|
|
238
|
+
const params: any[] = [];
|
|
239
|
+
|
|
240
|
+
if (status) {
|
|
241
|
+
query += ` AND status = ?`;
|
|
242
|
+
params.push(status);
|
|
243
|
+
}
|
|
244
|
+
if (name) {
|
|
245
|
+
query += ` AND name = ?`;
|
|
246
|
+
params.push(name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
250
|
+
params.push(limit, offset);
|
|
251
|
+
|
|
252
|
+
const rows = this.db.query(query).all(...params) as any[];
|
|
253
|
+
return rows.map((r) => this.rowToJob(r));
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
private rowToJob(row: any): Job {
|
|
236
257
|
return {
|
|
237
258
|
id: row.id,
|
package/src/core/jobs.ts
CHANGED
|
@@ -61,6 +61,18 @@ export interface JobHandler<T = any, R = any> {
|
|
|
61
61
|
(data: T): Promise<R>;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Options for listing all jobs */
|
|
65
|
+
export interface GetAllJobsOptions {
|
|
66
|
+
/** Filter by status */
|
|
67
|
+
status?: JobStatus;
|
|
68
|
+
/** Filter by job name */
|
|
69
|
+
name?: string;
|
|
70
|
+
/** Max number of jobs to return (default: 100) */
|
|
71
|
+
limit?: number;
|
|
72
|
+
/** Skip first N jobs (for pagination) */
|
|
73
|
+
offset?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
export interface JobAdapter {
|
|
65
77
|
create(job: Omit<Job, "id">): Promise<Job>;
|
|
66
78
|
get(jobId: string): Promise<Job | null>;
|
|
@@ -73,6 +85,8 @@ export interface JobAdapter {
|
|
|
73
85
|
getRunningExternal(): Promise<Job[]>;
|
|
74
86
|
/** Get external jobs that need reconnection after server restart */
|
|
75
87
|
getOrphanedExternal(): Promise<Job[]>;
|
|
88
|
+
/** Get all jobs with optional filtering (for admin dashboard) */
|
|
89
|
+
getAll(options?: GetAllJobsOptions): Promise<Job[]>;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
export interface JobsConfig {
|
|
@@ -109,6 +123,8 @@ export interface Jobs {
|
|
|
109
123
|
getByName(name: string, status?: JobStatus): Promise<Job[]>;
|
|
110
124
|
/** Get all running external jobs */
|
|
111
125
|
getRunningExternal(): Promise<Job[]>;
|
|
126
|
+
/** Get all jobs with optional filtering (for admin dashboard) */
|
|
127
|
+
getAll(options?: GetAllJobsOptions): Promise<Job[]>;
|
|
112
128
|
/** Start the job processing loop */
|
|
113
129
|
start(): void;
|
|
114
130
|
/** Stop the job processing and cleanup */
|
|
@@ -192,6 +208,23 @@ export class MemoryJobAdapter implements JobAdapter {
|
|
|
192
208
|
}
|
|
193
209
|
return results;
|
|
194
210
|
}
|
|
211
|
+
|
|
212
|
+
async getAll(options: GetAllJobsOptions = {}): Promise<Job[]> {
|
|
213
|
+
const { status, name, limit = 100, offset = 0 } = options;
|
|
214
|
+
const results: Job[] = [];
|
|
215
|
+
|
|
216
|
+
for (const job of this.jobs.values()) {
|
|
217
|
+
if (status && job.status !== status) continue;
|
|
218
|
+
if (name && job.name !== name) continue;
|
|
219
|
+
results.push(job);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Sort by createdAt descending (newest first)
|
|
223
|
+
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
224
|
+
|
|
225
|
+
// Apply pagination
|
|
226
|
+
return results.slice(offset, offset + limit);
|
|
227
|
+
}
|
|
195
228
|
}
|
|
196
229
|
|
|
197
230
|
class JobsImpl implements Jobs {
|
|
@@ -335,6 +368,10 @@ class JobsImpl implements Jobs {
|
|
|
335
368
|
return this.adapter.getRunningExternal();
|
|
336
369
|
}
|
|
337
370
|
|
|
371
|
+
async getAll(options?: GetAllJobsOptions): Promise<Job[]> {
|
|
372
|
+
return this.adapter.getAll(options);
|
|
373
|
+
}
|
|
374
|
+
|
|
338
375
|
start(): void {
|
|
339
376
|
if (this.running) return;
|
|
340
377
|
this.running = true;
|