@donkeylabs/server 2.0.7 → 2.0.11

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.
@@ -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;