worker-que 1.0.0

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.
Files changed (48) hide show
  1. package/DASHBOARD-QUICKSTART.md +278 -0
  2. package/DASHBOARD.md +556 -0
  3. package/LICENSE +21 -0
  4. package/README.md +414 -0
  5. package/SSL-QUICK-REFERENCE.md +225 -0
  6. package/SSL.md +516 -0
  7. package/dist/client.d.ts +11 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +64 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/dashboard/index.d.ts +34 -0
  12. package/dist/dashboard/index.d.ts.map +1 -0
  13. package/dist/dashboard/index.js +164 -0
  14. package/dist/dashboard/index.js.map +1 -0
  15. package/dist/dashboard/service.d.ts +66 -0
  16. package/dist/dashboard/service.d.ts.map +1 -0
  17. package/dist/dashboard/service.js +201 -0
  18. package/dist/dashboard/service.js.map +1 -0
  19. package/dist/dashboard/views.d.ts +3 -0
  20. package/dist/dashboard/views.d.ts.map +1 -0
  21. package/dist/dashboard/views.js +786 -0
  22. package/dist/dashboard/views.js.map +1 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +29 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/job.d.ts +19 -0
  28. package/dist/job.d.ts.map +1 -0
  29. package/dist/job.js +36 -0
  30. package/dist/job.js.map +1 -0
  31. package/dist/sql.d.ts +8 -0
  32. package/dist/sql.d.ts.map +1 -0
  33. package/dist/sql.js +57 -0
  34. package/dist/sql.js.map +1 -0
  35. package/dist/types.d.ts +90 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +3 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils.d.ts +6 -0
  40. package/dist/utils.d.ts.map +1 -0
  41. package/dist/utils.js +31 -0
  42. package/dist/utils.js.map +1 -0
  43. package/dist/worker.d.ts +19 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/dist/worker.js +99 -0
  46. package/dist/worker.js.map +1 -0
  47. package/migrations/schema.sql +26 -0
  48. package/package.json +105 -0
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDashboardHTML = getDashboardHTML;
4
+ function getDashboardHTML(options) {
5
+ return `
6
+ <!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>${options.title}</title>
12
+ <style>
13
+ * {
14
+ margin: 0;
15
+ padding: 0;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
21
+ background: #f5f7fa;
22
+ color: #2c3e50;
23
+ line-height: 1.6;
24
+ }
25
+
26
+ .header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ color: white;
29
+ padding: 2rem;
30
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
31
+ }
32
+
33
+ .header h1 {
34
+ font-size: 2rem;
35
+ font-weight: 600;
36
+ margin-bottom: 0.5rem;
37
+ }
38
+
39
+ .header p {
40
+ opacity: 0.9;
41
+ font-size: 0.95rem;
42
+ }
43
+
44
+ .container {
45
+ max-width: 1400px;
46
+ margin: 0 auto;
47
+ padding: 2rem;
48
+ }
49
+
50
+ .stats-grid {
51
+ display: grid;
52
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
53
+ gap: 1.5rem;
54
+ margin-bottom: 2rem;
55
+ }
56
+
57
+ .stat-card {
58
+ background: white;
59
+ border-radius: 12px;
60
+ padding: 1.5rem;
61
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
62
+ transition: transform 0.2s, box-shadow 0.2s;
63
+ }
64
+
65
+ .stat-card:hover {
66
+ transform: translateY(-2px);
67
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
68
+ }
69
+
70
+ .stat-label {
71
+ font-size: 0.875rem;
72
+ color: #7f8c8d;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.5px;
75
+ margin-bottom: 0.5rem;
76
+ }
77
+
78
+ .stat-value {
79
+ font-size: 2.5rem;
80
+ font-weight: 700;
81
+ color: #2c3e50;
82
+ }
83
+
84
+ .stat-card.success .stat-value { color: #27ae60; }
85
+ .stat-card.warning .stat-value { color: #f39c12; }
86
+ .stat-card.danger .stat-value { color: #e74c3c; }
87
+ .stat-card.info .stat-value { color: #3498db; }
88
+
89
+ .section {
90
+ background: white;
91
+ border-radius: 12px;
92
+ padding: 1.5rem;
93
+ margin-bottom: 2rem;
94
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
95
+ }
96
+
97
+ .section-header {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ margin-bottom: 1.5rem;
102
+ padding-bottom: 1rem;
103
+ border-bottom: 2px solid #ecf0f1;
104
+ }
105
+
106
+ .section-title {
107
+ font-size: 1.5rem;
108
+ font-weight: 600;
109
+ color: #2c3e50;
110
+ }
111
+
112
+ .filters {
113
+ display: flex;
114
+ gap: 1rem;
115
+ flex-wrap: wrap;
116
+ margin-bottom: 1.5rem;
117
+ }
118
+
119
+ select, input {
120
+ padding: 0.5rem 1rem;
121
+ border: 1px solid #ddd;
122
+ border-radius: 6px;
123
+ font-size: 0.95rem;
124
+ background: white;
125
+ cursor: pointer;
126
+ transition: border-color 0.2s;
127
+ }
128
+
129
+ select:hover, select:focus, input:hover, input:focus {
130
+ border-color: #667eea;
131
+ outline: none;
132
+ }
133
+
134
+ .btn {
135
+ padding: 0.5rem 1.25rem;
136
+ border: none;
137
+ border-radius: 6px;
138
+ font-size: 0.95rem;
139
+ cursor: pointer;
140
+ transition: all 0.2s;
141
+ font-weight: 500;
142
+ }
143
+
144
+ .btn-primary {
145
+ background: #667eea;
146
+ color: white;
147
+ }
148
+
149
+ .btn-primary:hover {
150
+ background: #5568d3;
151
+ }
152
+
153
+ .btn-danger {
154
+ background: #e74c3c;
155
+ color: white;
156
+ }
157
+
158
+ .btn-danger:hover {
159
+ background: #c0392b;
160
+ }
161
+
162
+ .btn-success {
163
+ background: #27ae60;
164
+ color: white;
165
+ }
166
+
167
+ .btn-success:hover {
168
+ background: #229954;
169
+ }
170
+
171
+ .btn-small {
172
+ padding: 0.375rem 0.75rem;
173
+ font-size: 0.875rem;
174
+ }
175
+
176
+ .table-container {
177
+ overflow-x: auto;
178
+ }
179
+
180
+ table {
181
+ width: 100%;
182
+ border-collapse: collapse;
183
+ }
184
+
185
+ th {
186
+ background: #f8f9fa;
187
+ padding: 1rem;
188
+ text-align: left;
189
+ font-weight: 600;
190
+ color: #495057;
191
+ border-bottom: 2px solid #dee2e6;
192
+ font-size: 0.875rem;
193
+ text-transform: uppercase;
194
+ letter-spacing: 0.5px;
195
+ }
196
+
197
+ td {
198
+ padding: 1rem;
199
+ border-bottom: 1px solid #ecf0f1;
200
+ }
201
+
202
+ tr:hover {
203
+ background: #f8f9fa;
204
+ }
205
+
206
+ .badge {
207
+ display: inline-block;
208
+ padding: 0.25rem 0.75rem;
209
+ border-radius: 12px;
210
+ font-size: 0.75rem;
211
+ font-weight: 600;
212
+ text-transform: uppercase;
213
+ letter-spacing: 0.5px;
214
+ }
215
+
216
+ .badge-success { background: #d4edda; color: #155724; }
217
+ .badge-warning { background: #fff3cd; color: #856404; }
218
+ .badge-danger { background: #f8d7da; color: #721c24; }
219
+ .badge-info { background: #d1ecf1; color: #0c5460; }
220
+
221
+ .chart-container {
222
+ display: grid;
223
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
224
+ gap: 2rem;
225
+ margin-top: 1.5rem;
226
+ }
227
+
228
+ .chart {
229
+ background: #f8f9fa;
230
+ padding: 1.5rem;
231
+ border-radius: 8px;
232
+ }
233
+
234
+ .chart-title {
235
+ font-weight: 600;
236
+ margin-bottom: 1rem;
237
+ color: #495057;
238
+ }
239
+
240
+ .chart-bar {
241
+ display: flex;
242
+ align-items: center;
243
+ margin-bottom: 0.75rem;
244
+ }
245
+
246
+ .chart-label {
247
+ min-width: 120px;
248
+ font-size: 0.875rem;
249
+ color: #6c757d;
250
+ }
251
+
252
+ .chart-bar-bg {
253
+ flex: 1;
254
+ height: 24px;
255
+ background: #e9ecef;
256
+ border-radius: 4px;
257
+ overflow: hidden;
258
+ position: relative;
259
+ }
260
+
261
+ .chart-bar-fill {
262
+ height: 100%;
263
+ background: linear-gradient(90deg, #667eea, #764ba2);
264
+ transition: width 0.3s ease;
265
+ }
266
+
267
+ .chart-value {
268
+ margin-left: 0.75rem;
269
+ font-weight: 600;
270
+ font-size: 0.875rem;
271
+ min-width: 40px;
272
+ }
273
+
274
+ .loading {
275
+ text-align: center;
276
+ padding: 3rem;
277
+ color: #7f8c8d;
278
+ }
279
+
280
+ .spinner {
281
+ border: 3px solid #f3f3f3;
282
+ border-top: 3px solid #667eea;
283
+ border-radius: 50%;
284
+ width: 40px;
285
+ height: 40px;
286
+ animation: spin 1s linear infinite;
287
+ margin: 0 auto 1rem;
288
+ }
289
+
290
+ @keyframes spin {
291
+ 0% { transform: rotate(0deg); }
292
+ 100% { transform: rotate(360deg); }
293
+ }
294
+
295
+ .error {
296
+ background: #f8d7da;
297
+ color: #721c24;
298
+ padding: 1rem;
299
+ border-radius: 8px;
300
+ border-left: 4px solid #e74c3c;
301
+ }
302
+
303
+ .empty-state {
304
+ text-align: center;
305
+ padding: 3rem;
306
+ color: #7f8c8d;
307
+ }
308
+
309
+ .empty-state-icon {
310
+ font-size: 4rem;
311
+ margin-bottom: 1rem;
312
+ opacity: 0.3;
313
+ }
314
+
315
+ .pagination {
316
+ display: flex;
317
+ justify-content: center;
318
+ align-items: center;
319
+ gap: 0.5rem;
320
+ margin-top: 1.5rem;
321
+ }
322
+
323
+ .pagination button {
324
+ padding: 0.5rem 1rem;
325
+ }
326
+
327
+ .pagination span {
328
+ color: #6c757d;
329
+ font-size: 0.95rem;
330
+ }
331
+
332
+ code {
333
+ background: #f8f9fa;
334
+ padding: 0.125rem 0.375rem;
335
+ border-radius: 3px;
336
+ font-family: 'Monaco', 'Courier New', monospace;
337
+ font-size: 0.875rem;
338
+ color: #e83e8c;
339
+ }
340
+
341
+ .job-args {
342
+ max-width: 300px;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ white-space: nowrap;
346
+ font-family: 'Monaco', 'Courier New', monospace;
347
+ font-size: 0.875rem;
348
+ }
349
+
350
+ .error-message {
351
+ max-width: 400px;
352
+ overflow: hidden;
353
+ text-overflow: ellipsis;
354
+ white-space: nowrap;
355
+ color: #e74c3c;
356
+ font-size: 0.875rem;
357
+ }
358
+
359
+ .refresh-indicator {
360
+ display: inline-block;
361
+ width: 8px;
362
+ height: 8px;
363
+ border-radius: 50%;
364
+ background: #27ae60;
365
+ margin-left: 0.5rem;
366
+ animation: pulse 2s infinite;
367
+ }
368
+
369
+ @keyframes pulse {
370
+ 0%, 100% { opacity: 1; }
371
+ 50% { opacity: 0.3; }
372
+ }
373
+
374
+ .actions {
375
+ display: flex;
376
+ gap: 0.5rem;
377
+ }
378
+ </style>
379
+ </head>
380
+ <body>
381
+ <div class="header">
382
+ <h1>${options.title}</h1>
383
+ <p>Real-time job queue monitoring and management <span class="refresh-indicator"></span></p>
384
+ </div>
385
+
386
+ <div class="container">
387
+ <!-- Statistics Cards -->
388
+ <div class="stats-grid">
389
+ <div class="stat-card">
390
+ <div class="stat-label">Total Jobs</div>
391
+ <div class="stat-value" id="stat-total">-</div>
392
+ </div>
393
+ <div class="stat-card success">
394
+ <div class="stat-label">Ready to Process</div>
395
+ <div class="stat-value" id="stat-ready">-</div>
396
+ </div>
397
+ <div class="stat-card info">
398
+ <div class="stat-label">Scheduled</div>
399
+ <div class="stat-value" id="stat-scheduled">-</div>
400
+ </div>
401
+ <div class="stat-card danger">
402
+ <div class="stat-label">Failed</div>
403
+ <div class="stat-value" id="stat-failed">-</div>
404
+ </div>
405
+ </div>
406
+
407
+ <!-- Charts -->
408
+ <div class="section">
409
+ <div class="section-header">
410
+ <h2 class="section-title">Analytics</h2>
411
+ </div>
412
+ <div class="chart-container">
413
+ <div class="chart">
414
+ <div class="chart-title">Jobs by Queue</div>
415
+ <div id="chart-queues"></div>
416
+ </div>
417
+ <div class="chart">
418
+ <div class="chart-title">Jobs by Class</div>
419
+ <div id="chart-classes"></div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+
424
+ <!-- Jobs Table -->
425
+ <div class="section">
426
+ <div class="section-header">
427
+ <h2 class="section-title">Jobs</h2>
428
+ </div>
429
+
430
+ <div class="filters">
431
+ <select id="filter-status" onchange="loadJobs()">
432
+ <option value="all">All Jobs</option>
433
+ <option value="ready">Ready</option>
434
+ <option value="scheduled">Scheduled</option>
435
+ <option value="failed">Failed</option>
436
+ </select>
437
+ <select id="filter-queue" onchange="loadJobs()">
438
+ <option value="">All Queues</option>
439
+ </select>
440
+ <select id="filter-class" onchange="loadJobs()">
441
+ <option value="">All Classes</option>
442
+ </select>
443
+ <button class="btn btn-primary" onclick="loadJobs()">Refresh</button>
444
+ </div>
445
+
446
+ <div id="jobs-container">
447
+ <div class="loading">
448
+ <div class="spinner"></div>
449
+ <p>Loading jobs...</p>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ <!-- Recent Failures -->
455
+ <div class="section">
456
+ <div class="section-header">
457
+ <h2 class="section-title">Recent Failures</h2>
458
+ </div>
459
+ <div id="failures-container">
460
+ <div class="loading">
461
+ <div class="spinner"></div>
462
+ <p>Loading failures...</p>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <script>
469
+ const REFRESH_INTERVAL = ${options.refreshInterval};
470
+ let currentPage = 0;
471
+ const pageSize = 50;
472
+
473
+ // Load initial data
474
+ loadStats();
475
+ loadQueues();
476
+ loadJobClasses();
477
+ loadJobs();
478
+
479
+ // Set up auto-refresh
480
+ setInterval(() => {
481
+ loadStats();
482
+ loadJobs();
483
+ }, REFRESH_INTERVAL);
484
+
485
+ async function loadStats() {
486
+ try {
487
+ const response = await fetch('${options.basePath}/api/stats');
488
+ const stats = await response.json();
489
+
490
+ document.getElementById('stat-total').textContent = stats.total.toLocaleString();
491
+ document.getElementById('stat-ready').textContent = stats.ready.toLocaleString();
492
+ document.getElementById('stat-scheduled').textContent = stats.scheduled.toLocaleString();
493
+ document.getElementById('stat-failed').textContent = stats.failed.toLocaleString();
494
+
495
+ renderChart('chart-queues', stats.totalByQueue, 'queue');
496
+ renderChart('chart-classes', stats.totalByClass, 'jobClass');
497
+ renderFailures(stats.recentFailures);
498
+ } catch (error) {
499
+ console.error('Error loading stats:', error);
500
+ }
501
+ }
502
+
503
+ async function loadQueues() {
504
+ try {
505
+ const response = await fetch('${options.basePath}/api/queues');
506
+ const queues = await response.json();
507
+ const select = document.getElementById('filter-queue');
508
+ queues.forEach(queue => {
509
+ const option = document.createElement('option');
510
+ option.value = queue;
511
+ option.textContent = queue;
512
+ select.appendChild(option);
513
+ });
514
+ } catch (error) {
515
+ console.error('Error loading queues:', error);
516
+ }
517
+ }
518
+
519
+ async function loadJobClasses() {
520
+ try {
521
+ const response = await fetch('${options.basePath}/api/job-classes');
522
+ const classes = await response.json();
523
+ const select = document.getElementById('filter-class');
524
+ classes.forEach(cls => {
525
+ const option = document.createElement('option');
526
+ option.value = cls;
527
+ option.textContent = cls;
528
+ select.appendChild(option);
529
+ });
530
+ } catch (error) {
531
+ console.error('Error loading job classes:', error);
532
+ }
533
+ }
534
+
535
+ async function loadJobs() {
536
+ const status = document.getElementById('filter-status').value;
537
+ const queue = document.getElementById('filter-queue').value;
538
+ const jobClass = document.getElementById('filter-class').value;
539
+
540
+ const params = new URLSearchParams({
541
+ status,
542
+ limit: pageSize.toString(),
543
+ offset: (currentPage * pageSize).toString(),
544
+ });
545
+
546
+ if (queue) params.append('queue', queue);
547
+ if (jobClass) params.append('jobClass', jobClass);
548
+
549
+ try {
550
+ const response = await fetch(\`${options.basePath}/api/jobs?\${params}\`);
551
+ const data = await response.json();
552
+ renderJobs(data.jobs, data.total);
553
+ } catch (error) {
554
+ console.error('Error loading jobs:', error);
555
+ document.getElementById('jobs-container').innerHTML =
556
+ '<div class="error">Failed to load jobs</div>';
557
+ }
558
+ }
559
+
560
+ function renderChart(containerId, data, labelKey) {
561
+ const container = document.getElementById(containerId);
562
+ if (!data || data.length === 0) {
563
+ container.innerHTML = '<div class="empty-state">No data</div>';
564
+ return;
565
+ }
566
+
567
+ const maxValue = Math.max(...data.map(d => d.count));
568
+ const html = data.slice(0, 10).map(item => {
569
+ const percentage = (item.count / maxValue) * 100;
570
+ const label = item[labelKey] || '(default)';
571
+ return \`
572
+ <div class="chart-bar">
573
+ <div class="chart-label">\${escapeHtml(label)}</div>
574
+ <div class="chart-bar-bg">
575
+ <div class="chart-bar-fill" style="width: \${percentage}%"></div>
576
+ </div>
577
+ <div class="chart-value">\${item.count}</div>
578
+ </div>
579
+ \`;
580
+ }).join('');
581
+
582
+ container.innerHTML = html || '<div class="empty-state">No data</div>';
583
+ }
584
+
585
+ function renderJobs(jobs, total) {
586
+ const container = document.getElementById('jobs-container');
587
+
588
+ if (!jobs || jobs.length === 0) {
589
+ container.innerHTML = \`
590
+ <div class="empty-state">
591
+ <div class="empty-state-icon">📭</div>
592
+ <p>No jobs found</p>
593
+ </div>
594
+ \`;
595
+ return;
596
+ }
597
+
598
+ const now = new Date();
599
+ const html = \`
600
+ <div class="table-container">
601
+ <table>
602
+ <thead>
603
+ <tr>
604
+ <th>ID</th>
605
+ <th>Class</th>
606
+ <th>Queue</th>
607
+ <th>Priority</th>
608
+ <th>Status</th>
609
+ <th>Run At</th>
610
+ <th>Errors</th>
611
+ <th>Arguments</th>
612
+ <th>Actions</th>
613
+ </tr>
614
+ </thead>
615
+ <tbody>
616
+ \${jobs.map(job => {
617
+ const runAt = new Date(job.runAt);
618
+ const isReady = runAt <= now;
619
+ const isFailed = job.errorCount > 0;
620
+ const status = isFailed ? 'failed' : (isReady ? 'ready' : 'scheduled');
621
+ const statusBadge = isFailed ? 'danger' : (isReady ? 'success' : 'info');
622
+
623
+ return \`
624
+ <tr>
625
+ <td><code>\${job.id}</code></td>
626
+ <td>\${escapeHtml(job.jobClass)}</td>
627
+ <td>\${escapeHtml(job.queue || '(default)')}</td>
628
+ <td>\${job.priority}</td>
629
+ <td><span class="badge badge-\${statusBadge}">\${status}</span></td>
630
+ <td>\${formatDate(runAt)}</td>
631
+ <td>\${job.errorCount > 0 ? '<span class="badge badge-danger">' + job.errorCount + '</span>' : '-'}</td>
632
+ <td class="job-args">\${escapeHtml(JSON.stringify(job.args))}</td>
633
+ <td>
634
+ <div class="actions">
635
+ \${job.errorCount > 0 ?
636
+ \`<button class="btn btn-success btn-small" onclick="retryJob(\${job.id})">Retry</button>\` :
637
+ ''}
638
+ <button class="btn btn-danger btn-small" onclick="deleteJob(\${job.id})">Delete</button>
639
+ </div>
640
+ </td>
641
+ </tr>
642
+ \`;
643
+ }).join('')}
644
+ </tbody>
645
+ </table>
646
+ </div>
647
+ <div class="pagination">
648
+ <button class="btn btn-primary" onclick="previousPage()" \${currentPage === 0 ? 'disabled' : ''}>Previous</button>
649
+ <span>Page \${currentPage + 1} of \${Math.ceil(total / pageSize)}</span>
650
+ <button class="btn btn-primary" onclick="nextPage()" \${(currentPage + 1) * pageSize >= total ? 'disabled' : ''}>Next</button>
651
+ </div>
652
+ \`;
653
+
654
+ container.innerHTML = html;
655
+ }
656
+
657
+ function renderFailures(failures) {
658
+ const container = document.getElementById('failures-container');
659
+
660
+ if (!failures || failures.length === 0) {
661
+ container.innerHTML = \`
662
+ <div class="empty-state">
663
+ <div class="empty-state-icon">✅</div>
664
+ <p>No recent failures</p>
665
+ </div>
666
+ \`;
667
+ return;
668
+ }
669
+
670
+ const html = \`
671
+ <div class="table-container">
672
+ <table>
673
+ <thead>
674
+ <tr>
675
+ <th>ID</th>
676
+ <th>Class</th>
677
+ <th>Queue</th>
678
+ <th>Errors</th>
679
+ <th>Last Error</th>
680
+ <th>Run At</th>
681
+ <th>Actions</th>
682
+ </tr>
683
+ </thead>
684
+ <tbody>
685
+ \${failures.map(job => \`
686
+ <tr>
687
+ <td><code>\${job.id}</code></td>
688
+ <td>\${escapeHtml(job.jobClass)}</td>
689
+ <td>\${escapeHtml(job.queue)}</td>
690
+ <td><span class="badge badge-danger">\${job.errorCount}</span></td>
691
+ <td class="error-message" title="\${escapeHtml(job.lastError)}">\${escapeHtml(job.lastError)}</td>
692
+ <td>\${formatDate(new Date(job.runAt))}</td>
693
+ <td>
694
+ <div class="actions">
695
+ <button class="btn btn-success btn-small" onclick="retryJob(\${job.id})">Retry</button>
696
+ <button class="btn btn-danger btn-small" onclick="deleteJob(\${job.id})">Delete</button>
697
+ </div>
698
+ </td>
699
+ </tr>
700
+ \`).join('')}
701
+ </tbody>
702
+ </table>
703
+ </div>
704
+ \`;
705
+
706
+ container.innerHTML = html;
707
+ }
708
+
709
+ async function deleteJob(jobId) {
710
+ if (!confirm(\`Are you sure you want to delete job #\${jobId}?\`)) {
711
+ return;
712
+ }
713
+
714
+ try {
715
+ const response = await fetch(\`${options.basePath}/api/jobs/\${jobId}\`, {
716
+ method: 'DELETE'
717
+ });
718
+
719
+ if (response.ok) {
720
+ await loadStats();
721
+ await loadJobs();
722
+ } else {
723
+ alert('Failed to delete job');
724
+ }
725
+ } catch (error) {
726
+ console.error('Error deleting job:', error);
727
+ alert('Failed to delete job');
728
+ }
729
+ }
730
+
731
+ async function retryJob(jobId) {
732
+ try {
733
+ const response = await fetch(\`${options.basePath}/api/jobs/\${jobId}/retry\`, {
734
+ method: 'POST'
735
+ });
736
+
737
+ if (response.ok) {
738
+ await loadStats();
739
+ await loadJobs();
740
+ } else {
741
+ alert('Failed to retry job');
742
+ }
743
+ } catch (error) {
744
+ console.error('Error retrying job:', error);
745
+ alert('Failed to retry job');
746
+ }
747
+ }
748
+
749
+ function nextPage() {
750
+ currentPage++;
751
+ loadJobs();
752
+ }
753
+
754
+ function previousPage() {
755
+ if (currentPage > 0) {
756
+ currentPage--;
757
+ loadJobs();
758
+ }
759
+ }
760
+
761
+ function formatDate(date) {
762
+ const now = new Date();
763
+ const diff = now - date;
764
+ const seconds = Math.floor(diff / 1000);
765
+ const minutes = Math.floor(seconds / 60);
766
+ const hours = Math.floor(minutes / 60);
767
+ const days = Math.floor(hours / 24);
768
+
769
+ if (days > 0) return \`\${days}d ago\`;
770
+ if (hours > 0) return \`\${hours}h ago\`;
771
+ if (minutes > 0) return \`\${minutes}m ago\`;
772
+ if (seconds > 0) return \`\${seconds}s ago\`;
773
+ return 'just now';
774
+ }
775
+
776
+ function escapeHtml(text) {
777
+ const div = document.createElement('div');
778
+ div.textContent = text;
779
+ return div.innerHTML;
780
+ }
781
+ </script>
782
+ </body>
783
+ </html>
784
+ `.trim();
785
+ }
786
+ //# sourceMappingURL=views.js.map