@nicnocquee/dataqueue-dashboard 1.0.0 → 1.26.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.
package/dist/core/html.js DELETED
@@ -1,993 +0,0 @@
1
- /**
2
- * Generate the complete dashboard HTML page.
3
- * The page is a self-contained React SPA with embedded CSS and JS.
4
- * React and ReactDOM are loaded from esm.sh CDN.
5
- */
6
- export function generateDashboardHTML(basePath) {
7
- const normalizedBase = basePath.endsWith('/')
8
- ? basePath.slice(0, -1)
9
- : basePath;
10
- return `<!DOCTYPE html>
11
- <html lang="en">
12
- <head>
13
- <meta charset="UTF-8" />
14
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
15
- <title>Dataqueue Dashboard</title>
16
- <style>${CSS}</style>
17
- </head>
18
- <body>
19
- <div id="root"></div>
20
- <script>window.__DQ_BASE_PATH__ = ${JSON.stringify(normalizedBase)};</script>
21
- <script type="importmap">
22
- {
23
- "imports": {
24
- "react": "https://esm.sh/react@19?dev",
25
- "react-dom/client": "https://esm.sh/react-dom@19/client?dev"
26
- }
27
- }
28
- </script>
29
- <script type="module">${CLIENT_JS}</script>
30
- </body>
31
- </html>`;
32
- }
33
- // ---------------------------------------------------------------------------
34
- // CSS
35
- // ---------------------------------------------------------------------------
36
- const CSS = /* css */ `
37
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
38
-
39
- :root {
40
- --bg: #ffffff;
41
- --bg-secondary: #f8f9fa;
42
- --bg-hover: #f1f3f5;
43
- --border: #dee2e6;
44
- --text: #212529;
45
- --text-secondary: #6c757d;
46
- --text-muted: #adb5bd;
47
- --primary: #228be6;
48
- --primary-hover: #1c7ed6;
49
- --primary-light: #e7f5ff;
50
- --success: #40c057;
51
- --success-light: #ebfbee;
52
- --danger: #fa5252;
53
- --danger-light: #fff5f5;
54
- --warning: #fab005;
55
- --warning-light: #fff9db;
56
- --info: #15aabf;
57
- --info-light: #e3fafc;
58
- --radius: 6px;
59
- --radius-lg: 8px;
60
- --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
61
- --shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
62
- --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
63
- --font-mono: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", Menlo, monospace;
64
- --transition: 150ms ease;
65
- }
66
-
67
- @media (prefers-color-scheme: dark) {
68
- :root {
69
- --bg: #1a1b1e;
70
- --bg-secondary: #25262b;
71
- --bg-hover: #2c2e33;
72
- --border: #373a40;
73
- --text: #c1c2c5;
74
- --text-secondary: #909296;
75
- --text-muted: #5c5f66;
76
- --primary: #4dabf7;
77
- --primary-hover: #339af0;
78
- --primary-light: #1b2838;
79
- --success: #51cf66;
80
- --success-light: #1b2e1b;
81
- --danger: #ff6b6b;
82
- --danger-light: #2e1b1b;
83
- --warning: #fcc419;
84
- --warning-light: #2e2a1b;
85
- --info: #22b8cf;
86
- --info-light: #1b2b2e;
87
- --shadow: 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2);
88
- --shadow-md: 0 4px 6px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.2);
89
- }
90
- }
91
-
92
- body {
93
- font-family: var(--font);
94
- background: var(--bg);
95
- color: var(--text);
96
- line-height: 1.5;
97
- -webkit-font-smoothing: antialiased;
98
- }
99
-
100
- .dq-layout {
101
- display: flex;
102
- min-height: 100vh;
103
- }
104
-
105
- .dq-sidebar {
106
- width: 220px;
107
- background: var(--bg-secondary);
108
- border-right: 1px solid var(--border);
109
- padding: 20px 0;
110
- flex-shrink: 0;
111
- }
112
-
113
- .dq-sidebar-title {
114
- font-size: 14px;
115
- font-weight: 700;
116
- padding: 0 16px 16px;
117
- color: var(--text);
118
- letter-spacing: -0.02em;
119
- }
120
-
121
- .dq-sidebar-title span {
122
- color: var(--primary);
123
- }
124
-
125
- .dq-nav-item {
126
- display: block;
127
- padding: 8px 16px;
128
- font-size: 13px;
129
- color: var(--text-secondary);
130
- text-decoration: none;
131
- cursor: pointer;
132
- border: none;
133
- background: none;
134
- width: 100%;
135
- text-align: left;
136
- transition: all var(--transition);
137
- }
138
-
139
- .dq-nav-item:hover {
140
- background: var(--bg-hover);
141
- color: var(--text);
142
- }
143
-
144
- .dq-nav-item.active {
145
- background: var(--primary-light);
146
- color: var(--primary);
147
- font-weight: 500;
148
- }
149
-
150
- .dq-main {
151
- flex: 1;
152
- padding: 24px 32px;
153
- min-width: 0;
154
- overflow-x: auto;
155
- }
156
-
157
- .dq-header {
158
- display: flex;
159
- align-items: center;
160
- justify-content: space-between;
161
- margin-bottom: 24px;
162
- }
163
-
164
- .dq-header h1 {
165
- font-size: 22px;
166
- font-weight: 700;
167
- letter-spacing: -0.02em;
168
- }
169
-
170
- .dq-header-actions {
171
- display: flex;
172
- gap: 8px;
173
- align-items: center;
174
- }
175
-
176
- .dq-btn {
177
- display: inline-flex;
178
- align-items: center;
179
- gap: 6px;
180
- padding: 7px 14px;
181
- font-size: 13px;
182
- font-weight: 500;
183
- font-family: var(--font);
184
- border-radius: var(--radius);
185
- border: 1px solid var(--border);
186
- background: var(--bg);
187
- color: var(--text);
188
- cursor: pointer;
189
- transition: all var(--transition);
190
- white-space: nowrap;
191
- }
192
-
193
- .dq-btn:hover { background: var(--bg-hover); }
194
- .dq-btn:disabled { opacity: 0.5; cursor: not-allowed; }
195
-
196
- .dq-btn-primary {
197
- background: var(--primary);
198
- color: #fff;
199
- border-color: var(--primary);
200
- }
201
- .dq-btn-primary:hover { background: var(--primary-hover); }
202
-
203
- .dq-btn-danger {
204
- color: var(--danger);
205
- border-color: var(--danger);
206
- }
207
- .dq-btn-danger:hover { background: var(--danger-light); }
208
-
209
- .dq-btn-sm {
210
- padding: 4px 10px;
211
- font-size: 12px;
212
- }
213
-
214
- .dq-tabs {
215
- display: flex;
216
- gap: 2px;
217
- margin-bottom: 16px;
218
- border-bottom: 1px solid var(--border);
219
- overflow-x: auto;
220
- }
221
-
222
- .dq-tab {
223
- padding: 8px 14px;
224
- font-size: 13px;
225
- font-weight: 500;
226
- color: var(--text-secondary);
227
- background: none;
228
- border: none;
229
- border-bottom: 2px solid transparent;
230
- cursor: pointer;
231
- transition: all var(--transition);
232
- font-family: var(--font);
233
- white-space: nowrap;
234
- }
235
-
236
- .dq-tab:hover { color: var(--text); }
237
-
238
- .dq-tab.active {
239
- color: var(--primary);
240
- border-bottom-color: var(--primary);
241
- }
242
-
243
- .dq-tab .dq-count {
244
- margin-left: 4px;
245
- font-size: 11px;
246
- font-weight: 600;
247
- padding: 1px 6px;
248
- border-radius: 10px;
249
- background: var(--bg-hover);
250
- color: var(--text-secondary);
251
- }
252
-
253
- .dq-tab.active .dq-count {
254
- background: var(--primary-light);
255
- color: var(--primary);
256
- }
257
-
258
- .dq-card {
259
- background: var(--bg);
260
- border: 1px solid var(--border);
261
- border-radius: var(--radius-lg);
262
- box-shadow: var(--shadow);
263
- overflow: hidden;
264
- }
265
-
266
- .dq-card-header {
267
- padding: 14px 16px;
268
- font-size: 14px;
269
- font-weight: 600;
270
- border-bottom: 1px solid var(--border);
271
- }
272
-
273
- .dq-card-body { padding: 16px; }
274
-
275
- .dq-table-wrap { overflow-x: auto; }
276
-
277
- table.dq-table {
278
- width: 100%;
279
- border-collapse: collapse;
280
- font-size: 13px;
281
- }
282
-
283
- .dq-table th {
284
- text-align: left;
285
- padding: 10px 12px;
286
- font-weight: 600;
287
- font-size: 12px;
288
- text-transform: uppercase;
289
- letter-spacing: 0.04em;
290
- color: var(--text-secondary);
291
- background: var(--bg-secondary);
292
- border-bottom: 1px solid var(--border);
293
- white-space: nowrap;
294
- }
295
-
296
- .dq-table td {
297
- padding: 10px 12px;
298
- border-bottom: 1px solid var(--border);
299
- vertical-align: middle;
300
- }
301
-
302
- .dq-table tr:last-child td { border-bottom: none; }
303
-
304
- .dq-table tr:hover td { background: var(--bg-hover); }
305
-
306
- .dq-table .dq-id {
307
- font-family: var(--font-mono);
308
- font-size: 12px;
309
- color: var(--primary);
310
- cursor: pointer;
311
- font-weight: 500;
312
- }
313
-
314
- .dq-table .dq-id:hover { text-decoration: underline; }
315
-
316
- .dq-table .dq-type {
317
- font-family: var(--font-mono);
318
- font-size: 12px;
319
- background: var(--bg-secondary);
320
- padding: 2px 8px;
321
- border-radius: 4px;
322
- }
323
-
324
- .dq-badge {
325
- display: inline-flex;
326
- align-items: center;
327
- padding: 2px 8px;
328
- font-size: 11px;
329
- font-weight: 600;
330
- border-radius: 10px;
331
- text-transform: capitalize;
332
- white-space: nowrap;
333
- }
334
-
335
- .dq-badge-pending { background: var(--warning-light); color: var(--warning); }
336
- .dq-badge-processing { background: var(--primary-light); color: var(--primary); }
337
- .dq-badge-completed { background: var(--success-light); color: var(--success); }
338
- .dq-badge-failed { background: var(--danger-light); color: var(--danger); }
339
- .dq-badge-cancelled { background: var(--bg-hover); color: var(--text-muted); }
340
- .dq-badge-waiting { background: var(--info-light); color: var(--info); }
341
-
342
- .dq-pagination {
343
- display: flex;
344
- align-items: center;
345
- justify-content: space-between;
346
- padding: 12px 0;
347
- font-size: 13px;
348
- color: var(--text-secondary);
349
- }
350
-
351
- .dq-pagination-btns {
352
- display: flex;
353
- gap: 8px;
354
- }
355
-
356
- .dq-detail-grid {
357
- display: grid;
358
- grid-template-columns: 1fr 1fr;
359
- gap: 16px;
360
- margin-bottom: 16px;
361
- }
362
-
363
- @media (max-width: 900px) {
364
- .dq-detail-grid { grid-template-columns: 1fr; }
365
- .dq-sidebar { width: 180px; }
366
- }
367
-
368
- @media (max-width: 700px) {
369
- .dq-layout { flex-direction: column; }
370
- .dq-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); padding: 12px 0; }
371
- .dq-main { padding: 16px; }
372
- }
373
-
374
- .dq-prop-table { width: 100%; font-size: 13px; }
375
-
376
- .dq-prop-table td {
377
- padding: 6px 0;
378
- vertical-align: top;
379
- }
380
-
381
- .dq-prop-table td:first-child {
382
- color: var(--text-secondary);
383
- width: 160px;
384
- font-weight: 500;
385
- padding-right: 12px;
386
- }
387
-
388
- .dq-code-block {
389
- background: var(--bg-secondary);
390
- border: 1px solid var(--border);
391
- border-radius: var(--radius);
392
- padding: 12px;
393
- font-family: var(--font-mono);
394
- font-size: 12px;
395
- line-height: 1.6;
396
- overflow-x: auto;
397
- white-space: pre-wrap;
398
- word-break: break-all;
399
- }
400
-
401
- .dq-timeline { position: relative; padding-left: 24px; }
402
-
403
- .dq-timeline::before {
404
- content: '';
405
- position: absolute;
406
- left: 7px;
407
- top: 4px;
408
- bottom: 4px;
409
- width: 2px;
410
- background: var(--border);
411
- }
412
-
413
- .dq-timeline-item {
414
- position: relative;
415
- padding-bottom: 16px;
416
- }
417
-
418
- .dq-timeline-item:last-child { padding-bottom: 0; }
419
-
420
- .dq-timeline-dot {
421
- position: absolute;
422
- left: -20px;
423
- top: 4px;
424
- width: 10px;
425
- height: 10px;
426
- border-radius: 50%;
427
- background: var(--primary);
428
- border: 2px solid var(--bg);
429
- box-shadow: 0 0 0 2px var(--border);
430
- }
431
-
432
- .dq-timeline-dot.completed { background: var(--success); }
433
- .dq-timeline-dot.failed { background: var(--danger); }
434
- .dq-timeline-dot.cancelled { background: var(--text-muted); }
435
-
436
- .dq-timeline-time {
437
- font-size: 11px;
438
- color: var(--text-muted);
439
- font-family: var(--font-mono);
440
- }
441
-
442
- .dq-timeline-type {
443
- font-size: 13px;
444
- font-weight: 500;
445
- text-transform: capitalize;
446
- }
447
-
448
- .dq-timeline-meta {
449
- font-size: 12px;
450
- color: var(--text-secondary);
451
- margin-top: 2px;
452
- }
453
-
454
- .dq-back-link {
455
- display: inline-flex;
456
- align-items: center;
457
- gap: 4px;
458
- font-size: 13px;
459
- color: var(--text-secondary);
460
- cursor: pointer;
461
- margin-bottom: 16px;
462
- background: none;
463
- border: none;
464
- font-family: var(--font);
465
- padding: 0;
466
- }
467
- .dq-back-link:hover { color: var(--primary); }
468
-
469
- .dq-empty {
470
- text-align: center;
471
- padding: 48px 16px;
472
- color: var(--text-muted);
473
- font-size: 14px;
474
- }
475
-
476
- .dq-spinner {
477
- display: inline-block;
478
- width: 16px;
479
- height: 16px;
480
- border: 2px solid var(--border);
481
- border-top-color: var(--primary);
482
- border-radius: 50%;
483
- animation: dq-spin 0.6s linear infinite;
484
- }
485
-
486
- @keyframes dq-spin { to { transform: rotate(360deg); } }
487
-
488
- .dq-loading-overlay {
489
- display: flex;
490
- align-items: center;
491
- justify-content: center;
492
- padding: 64px 0;
493
- }
494
-
495
- .dq-tag {
496
- display: inline-block;
497
- padding: 1px 6px;
498
- font-size: 11px;
499
- background: var(--bg-secondary);
500
- border: 1px solid var(--border);
501
- border-radius: 4px;
502
- margin-right: 4px;
503
- font-family: var(--font-mono);
504
- }
505
-
506
- .dq-progress-bar {
507
- width: 100%;
508
- height: 6px;
509
- background: var(--bg-hover);
510
- border-radius: 3px;
511
- overflow: hidden;
512
- }
513
-
514
- .dq-progress-fill {
515
- height: 100%;
516
- background: var(--primary);
517
- border-radius: 3px;
518
- transition: width 300ms ease;
519
- }
520
-
521
- .dq-error-item {
522
- background: var(--danger-light);
523
- border: 1px solid var(--danger);
524
- border-radius: var(--radius);
525
- padding: 10px 12px;
526
- margin-bottom: 8px;
527
- font-size: 12px;
528
- }
529
- .dq-error-item:last-child { margin-bottom: 0; }
530
-
531
- .dq-error-item .dq-error-time {
532
- font-size: 11px;
533
- color: var(--text-muted);
534
- font-family: var(--font-mono);
535
- margin-bottom: 4px;
536
- }
537
-
538
- .dq-error-item .dq-error-msg {
539
- font-family: var(--font-mono);
540
- color: var(--danger);
541
- word-break: break-all;
542
- }
543
-
544
- .dq-select {
545
- padding: 7px 10px;
546
- font-size: 13px;
547
- font-family: var(--font);
548
- border: 1px solid var(--border);
549
- border-radius: var(--radius);
550
- background: var(--bg);
551
- color: var(--text);
552
- cursor: pointer;
553
- }
554
-
555
- .dq-auto-refresh {
556
- display: flex;
557
- align-items: center;
558
- gap: 6px;
559
- font-size: 12px;
560
- color: var(--text-secondary);
561
- }
562
-
563
- .dq-auto-refresh input[type="checkbox"] {
564
- accent-color: var(--primary);
565
- }
566
-
567
- .dq-toast {
568
- position: fixed;
569
- bottom: 20px;
570
- right: 20px;
571
- padding: 10px 16px;
572
- border-radius: var(--radius);
573
- font-size: 13px;
574
- font-weight: 500;
575
- color: #fff;
576
- background: var(--text);
577
- box-shadow: var(--shadow-md);
578
- z-index: 1000;
579
- animation: dq-fade-in 200ms ease;
580
- }
581
-
582
- .dq-toast.success { background: var(--success); }
583
- .dq-toast.error { background: var(--danger); }
584
-
585
- @keyframes dq-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
586
- `;
587
- // ---------------------------------------------------------------------------
588
- // Client JS (React SPA)
589
- // ---------------------------------------------------------------------------
590
- const CLIENT_JS = /* js */ `
591
- import React from 'react';
592
- import { createRoot } from 'react-dom/client';
593
-
594
- const h = React.createElement;
595
- const F = React.Fragment;
596
- const { useState, useEffect, useCallback, useRef } = React;
597
-
598
- const BASE = window.__DQ_BASE_PATH__;
599
-
600
- // --- API Client ---
601
- async function api(path, opts = {}) {
602
- const res = await fetch(BASE + '/api' + path, {
603
- headers: { 'Content-Type': 'application/json' },
604
- ...opts,
605
- });
606
- if (!res.ok) {
607
- const body = await res.json().catch(() => ({}));
608
- throw new Error(body.error || 'Request failed: ' + res.status);
609
- }
610
- return res.json();
611
- }
612
-
613
- // --- Utils ---
614
- function timeAgo(iso) {
615
- if (!iso) return '—';
616
- const d = new Date(iso);
617
- const now = Date.now();
618
- const diff = now - d.getTime();
619
- if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
620
- if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
621
- if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
622
- return Math.floor(diff / 86400000) + 'd ago';
623
- }
624
-
625
- function formatDate(iso) {
626
- if (!iso) return '—';
627
- return new Date(iso).toLocaleString();
628
- }
629
-
630
- function prettyJSON(val) {
631
- try { return JSON.stringify(val, null, 2); }
632
- catch { return String(val); }
633
- }
634
-
635
- // --- Toast ---
636
- let toastTimer;
637
- function Toast({ toast, setToast }) {
638
- useEffect(() => {
639
- if (toast) {
640
- clearTimeout(toastTimer);
641
- toastTimer = setTimeout(() => setToast(null), 3000);
642
- }
643
- }, [toast]);
644
- if (!toast) return null;
645
- return h('div', { className: 'dq-toast ' + (toast.type || '') }, toast.msg);
646
- }
647
-
648
- // --- Status Badge ---
649
- function StatusBadge({ status }) {
650
- return h('span', { className: 'dq-badge dq-badge-' + status }, status);
651
- }
652
-
653
- // --- Pagination ---
654
- function Pagination({ offset, limit, hasMore, onPrev, onNext }) {
655
- const page = Math.floor(offset / limit) + 1;
656
- return h('div', { className: 'dq-pagination' },
657
- h('span', null, 'Page ' + page),
658
- h('div', { className: 'dq-pagination-btns' },
659
- h('button', { className: 'dq-btn dq-btn-sm', disabled: offset === 0, onClick: onPrev }, '← Previous'),
660
- h('button', { className: 'dq-btn dq-btn-sm', disabled: !hasMore, onClick: onNext }, 'Next →'),
661
- ),
662
- );
663
- }
664
-
665
- // --- Event Timeline ---
666
- function EventTimeline({ events }) {
667
- if (!events.length) return h('div', { className: 'dq-empty' }, 'No events recorded');
668
- const sorted = [...events].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
669
- return h('div', { className: 'dq-timeline' },
670
- sorted.map(ev =>
671
- h('div', { key: ev.id, className: 'dq-timeline-item' },
672
- h('div', { className: 'dq-timeline-dot ' + ev.eventType }),
673
- h('div', { className: 'dq-timeline-time' }, formatDate(ev.createdAt)),
674
- h('div', { className: 'dq-timeline-type' }, ev.eventType),
675
- ev.metadata ? h('div', { className: 'dq-timeline-meta' }, prettyJSON(ev.metadata)) : null,
676
- )
677
- ),
678
- );
679
- }
680
-
681
- // --- Job Detail Page ---
682
- function JobDetailPage({ jobId, onBack, showToast }) {
683
- const [job, setJob] = useState(null);
684
- const [events, setEvents] = useState([]);
685
- const [loading, setLoading] = useState(true);
686
- const [acting, setActing] = useState(false);
687
-
688
- const load = useCallback(async () => {
689
- try {
690
- const [jRes, eRes] = await Promise.all([
691
- api('/jobs/' + jobId),
692
- api('/jobs/' + jobId + '/events'),
693
- ]);
694
- setJob(jRes.job);
695
- setEvents(eRes.events);
696
- } catch (e) {
697
- showToast(e.message, 'error');
698
- } finally {
699
- setLoading(false);
700
- }
701
- }, [jobId]);
702
-
703
- useEffect(() => { load(); }, [load]);
704
-
705
- async function handleAction(action) {
706
- setActing(true);
707
- try {
708
- await api('/jobs/' + jobId + '/' + action, { method: 'POST' });
709
- showToast(action === 'cancel' ? 'Job cancelled' : 'Job retried', 'success');
710
- await load();
711
- } catch (e) {
712
- showToast(e.message, 'error');
713
- } finally {
714
- setActing(false);
715
- }
716
- }
717
-
718
- if (loading) return h('div', { className: 'dq-loading-overlay' }, h('div', { className: 'dq-spinner' }));
719
- if (!job) return h('div', { className: 'dq-empty' }, 'Job not found');
720
-
721
- const canCancel = job.status === 'pending' || job.status === 'waiting';
722
- const canRetry = job.status === 'failed' || job.status === 'cancelled';
723
-
724
- return h(F, null,
725
- h('button', { className: 'dq-back-link', onClick: onBack }, '← Back to Jobs'),
726
- h('div', { className: 'dq-header' },
727
- h('h1', null, 'Job #' + job.id, ' ', h(StatusBadge, { status: job.status })),
728
- h('div', { className: 'dq-header-actions' },
729
- canCancel ? h('button', { className: 'dq-btn dq-btn-danger dq-btn-sm', onClick: () => handleAction('cancel'), disabled: acting }, 'Cancel') : null,
730
- canRetry ? h('button', { className: 'dq-btn dq-btn-primary dq-btn-sm', onClick: () => handleAction('retry'), disabled: acting }, 'Retry') : null,
731
- ),
732
- ),
733
-
734
- h('div', { className: 'dq-detail-grid' },
735
- h('div', { className: 'dq-card' },
736
- h('div', { className: 'dq-card-header' }, 'Properties'),
737
- h('div', { className: 'dq-card-body' },
738
- h('table', { className: 'dq-prop-table' },
739
- h('tbody', null,
740
- propRow('Type', h('span', { className: 'dq-type' }, job.jobType)),
741
- propRow('Status', h(StatusBadge, { status: job.status })),
742
- propRow('Priority', job.priority),
743
- propRow('Attempts', job.attempts + ' / ' + job.maxAttempts),
744
- job.progress != null ? propRow('Progress', h(F, null, h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, h('div', { className: 'dq-progress-bar', style: { width: '100px' } }, h('div', { className: 'dq-progress-fill', style: { width: job.progress + '%' } })), job.progress + '%'))) : null,
745
- propRow('Created', formatDate(job.createdAt)),
746
- propRow('Updated', formatDate(job.updatedAt)),
747
- job.startedAt ? propRow('Started', formatDate(job.startedAt)) : null,
748
- job.completedAt ? propRow('Completed', formatDate(job.completedAt)) : null,
749
- job.runAt ? propRow('Run At', formatDate(job.runAt)) : null,
750
- job.timeoutMs ? propRow('Timeout', job.timeoutMs + 'ms') : null,
751
- job.tags && job.tags.length ? propRow('Tags', h(F, null, job.tags.map(t => h('span', { key: t, className: 'dq-tag' }, t)))) : null,
752
- job.idempotencyKey ? propRow('Idempotency Key', h('code', null, job.idempotencyKey)) : null,
753
- job.failureReason ? propRow('Failure Reason', job.failureReason) : null,
754
- job.pendingReason ? propRow('Pending Reason', job.pendingReason) : null,
755
- job.waitTokenId ? propRow('Wait Token', h('code', null, job.waitTokenId)) : null,
756
- ),
757
- ),
758
- ),
759
- ),
760
-
761
- h('div', { className: 'dq-card' },
762
- h('div', { className: 'dq-card-header' }, 'Payload'),
763
- h('div', { className: 'dq-card-body' },
764
- h('pre', { className: 'dq-code-block' }, prettyJSON(job.payload)),
765
- ),
766
- ),
767
- ),
768
-
769
- job.errorHistory && job.errorHistory.length
770
- ? h('div', { className: 'dq-card', style: { marginBottom: '16px' } },
771
- h('div', { className: 'dq-card-header' }, 'Error History'),
772
- h('div', { className: 'dq-card-body' },
773
- job.errorHistory.map((err, i) =>
774
- h('div', { key: i, className: 'dq-error-item' },
775
- h('div', { className: 'dq-error-time' }, err.timestamp),
776
- h('div', { className: 'dq-error-msg' }, err.message),
777
- )
778
- ),
779
- ),
780
- )
781
- : null,
782
-
783
- job.stepData && Object.keys(job.stepData).length
784
- ? h('div', { className: 'dq-card', style: { marginBottom: '16px' } },
785
- h('div', { className: 'dq-card-header' }, 'Step Data'),
786
- h('div', { className: 'dq-card-body' },
787
- h('pre', { className: 'dq-code-block' }, prettyJSON(job.stepData)),
788
- ),
789
- )
790
- : null,
791
-
792
- h('div', { className: 'dq-card' },
793
- h('div', { className: 'dq-card-header' }, 'Events (' + events.length + ')'),
794
- h('div', { className: 'dq-card-body' }, h(EventTimeline, { events })),
795
- ),
796
- );
797
- }
798
-
799
- function propRow(label, value) {
800
- return h('tr', null,
801
- h('td', null, label),
802
- h('td', null, value),
803
- );
804
- }
805
-
806
- // --- Jobs List Page ---
807
- function JobsPage({ onSelectJob, showToast }) {
808
- const [jobs, setJobs] = useState([]);
809
- const [loading, setLoading] = useState(true);
810
- const [hasMore, setHasMore] = useState(false);
811
- const [status, setStatus] = useState('');
812
- const [offset, setOffset] = useState(0);
813
- const [processing, setProcessing] = useState(false);
814
- const [autoRefresh, setAutoRefresh] = useState(false);
815
- const limit = 25;
816
- const intervalRef = useRef(null);
817
-
818
- const load = useCallback(async () => {
819
- try {
820
- const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
821
- if (status) params.set('status', status);
822
- const data = await api('/jobs?' + params.toString());
823
- setJobs(data.jobs);
824
- setHasMore(data.hasMore);
825
- } catch (e) {
826
- showToast(e.message, 'error');
827
- } finally {
828
- setLoading(false);
829
- }
830
- }, [status, offset]);
831
-
832
- useEffect(() => { setLoading(true); load(); }, [load]);
833
-
834
- useEffect(() => {
835
- if (autoRefresh) {
836
- intervalRef.current = setInterval(load, 3000);
837
- } else {
838
- clearInterval(intervalRef.current);
839
- }
840
- return () => clearInterval(intervalRef.current);
841
- }, [autoRefresh, load]);
842
-
843
- async function handleProcess() {
844
- setProcessing(true);
845
- try {
846
- const res = await api('/process', { method: 'POST' });
847
- showToast('Processed ' + res.processed + ' job(s)', 'success');
848
- await load();
849
- } catch (e) {
850
- showToast(e.message, 'error');
851
- } finally {
852
- setProcessing(false);
853
- }
854
- }
855
-
856
- async function handleAction(jobId, action) {
857
- try {
858
- await api('/jobs/' + jobId + '/' + action, { method: 'POST' });
859
- showToast(action === 'cancel' ? 'Job cancelled' : 'Job retried', 'success');
860
- await load();
861
- } catch (e) {
862
- showToast(e.message, 'error');
863
- }
864
- }
865
-
866
- const statuses = ['', 'pending', 'processing', 'completed', 'failed', 'cancelled', 'waiting'];
867
-
868
- return h(F, null,
869
- h('div', { className: 'dq-header' },
870
- h('h1', null, 'Jobs'),
871
- h('div', { className: 'dq-header-actions' },
872
- h('label', { className: 'dq-auto-refresh' },
873
- h('input', { type: 'checkbox', checked: autoRefresh, onChange: e => setAutoRefresh(e.target.checked) }),
874
- 'Auto-refresh',
875
- ),
876
- h('button', { className: 'dq-btn', onClick: () => { setLoading(true); load(); } }, 'Refresh'),
877
- h('button', {
878
- className: 'dq-btn dq-btn-primary',
879
- onClick: handleProcess,
880
- disabled: processing,
881
- }, processing ? h('span', { className: 'dq-spinner' }) : null, processing ? 'Processing...' : 'Process Jobs'),
882
- ),
883
- ),
884
-
885
- h('div', { className: 'dq-tabs' },
886
- statuses.map(s =>
887
- h('button', {
888
- key: s || 'all',
889
- className: 'dq-tab' + (status === s ? ' active' : ''),
890
- onClick: () => { setStatus(s); setOffset(0); },
891
- }, s || 'All')
892
- ),
893
- ),
894
-
895
- h('div', { className: 'dq-card' },
896
- h('div', { className: 'dq-table-wrap' },
897
- loading
898
- ? h('div', { className: 'dq-loading-overlay' }, h('div', { className: 'dq-spinner' }))
899
- : jobs.length === 0
900
- ? h('div', { className: 'dq-empty' }, 'No jobs found')
901
- : h('table', { className: 'dq-table' },
902
- h('thead', null,
903
- h('tr', null,
904
- h('th', null, 'ID'),
905
- h('th', null, 'Type'),
906
- h('th', null, 'Status'),
907
- h('th', null, 'Priority'),
908
- h('th', null, 'Attempts'),
909
- h('th', null, 'Created'),
910
- h('th', null, 'Actions'),
911
- ),
912
- ),
913
- h('tbody', null,
914
- jobs.map(j =>
915
- h('tr', { key: j.id },
916
- h('td', null, h('span', { className: 'dq-id', onClick: () => onSelectJob(j.id) }, '#' + j.id)),
917
- h('td', null, h('span', { className: 'dq-type' }, j.jobType)),
918
- h('td', null, h(StatusBadge, { status: j.status })),
919
- h('td', null, j.priority),
920
- h('td', null, j.attempts + '/' + j.maxAttempts),
921
- h('td', { title: formatDate(j.createdAt) }, timeAgo(j.createdAt)),
922
- h('td', null,
923
- h('div', { style: { display: 'flex', gap: '4px' } },
924
- (j.status === 'pending' || j.status === 'waiting')
925
- ? h('button', { className: 'dq-btn dq-btn-danger dq-btn-sm', onClick: () => handleAction(j.id, 'cancel') }, 'Cancel')
926
- : null,
927
- (j.status === 'failed' || j.status === 'cancelled')
928
- ? h('button', { className: 'dq-btn dq-btn-sm', onClick: () => handleAction(j.id, 'retry') }, 'Retry')
929
- : null,
930
- ),
931
- ),
932
- )
933
- ),
934
- ),
935
- ),
936
- ),
937
- !loading && jobs.length > 0
938
- ? h(Pagination, {
939
- offset,
940
- limit,
941
- hasMore,
942
- onPrev: () => setOffset(Math.max(0, offset - limit)),
943
- onNext: () => setOffset(offset + limit),
944
- })
945
- : null,
946
- ),
947
- );
948
- }
949
-
950
- // --- App ---
951
- function App() {
952
- const [route, setRoute] = useState(parseHash());
953
- const [toast, setToast] = useState(null);
954
-
955
- useEffect(() => {
956
- function onHash() { setRoute(parseHash()); }
957
- window.addEventListener('hashchange', onHash);
958
- return () => window.removeEventListener('hashchange', onHash);
959
- }, []);
960
-
961
- function navigate(hash) { window.location.hash = hash; }
962
- function showToast(msg, type) { setToast({ msg, type }); }
963
-
964
- return h('div', { className: 'dq-layout' },
965
- h('nav', { className: 'dq-sidebar' },
966
- h('div', { className: 'dq-sidebar-title' }, h('span', null, 'dataqueue'), ' Dashboard'),
967
- h('button', {
968
- className: 'dq-nav-item' + (route.page === 'jobs' && !route.jobId ? ' active' : ''),
969
- onClick: () => navigate('#/'),
970
- }, 'Jobs'),
971
- ),
972
- h('main', { className: 'dq-main' },
973
- route.page === 'detail' && route.jobId
974
- ? h(JobDetailPage, { jobId: route.jobId, onBack: () => navigate('#/'), showToast })
975
- : h(JobsPage, { onSelectJob: id => navigate('#/jobs/' + id), showToast }),
976
- ),
977
- h(Toast, { toast, setToast }),
978
- );
979
- }
980
-
981
- function parseHash() {
982
- const hash = window.location.hash.replace(/^#\\/?/, '');
983
- const parts = hash.split('/').filter(Boolean);
984
- if (parts[0] === 'jobs' && parts[1]) {
985
- return { page: 'detail', jobId: parseInt(parts[1], 10) };
986
- }
987
- return { page: 'jobs', jobId: null };
988
- }
989
-
990
- // --- Mount ---
991
- createRoot(document.getElementById('root')).render(h(App, null));
992
- `;
993
- //# sourceMappingURL=html.js.map