@zintrust/queue-monitor 0.4.50 → 0.4.63
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/QueueMonitoringService.js +1 -9
- package/dist/build-manifest.json +17 -17
- package/dist/dashboard-ui.js +329 -135
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@ import { Logger } from '@zintrust/core';
|
|
|
2
2
|
export const ALL_QUEUES = '__all__';
|
|
3
3
|
const subscriptions = new Map();
|
|
4
4
|
const isAllQueuesSelection = (queue) => queue === ALL_QUEUES;
|
|
5
|
-
const sortJobsByTimestamp = (jobs) => jobs.
|
|
5
|
+
const sortJobsByTimestamp = (jobs) => jobs.toSorted((left, right) => right.timestamp - left.timestamp);
|
|
6
6
|
export async function getRecentJobsForSelection(queueName, metrics, driver, queueNames) {
|
|
7
7
|
if (!isAllQueuesSelection(queueName)) {
|
|
8
8
|
return getRecentJobsForQueue(queueName, metrics, driver);
|
|
@@ -47,10 +47,6 @@ const pushSnapshot = async (subscription) => {
|
|
|
47
47
|
const startPolling = (subscription) => {
|
|
48
48
|
if (subscription.interval)
|
|
49
49
|
return;
|
|
50
|
-
Logger.debug('Starting QueueMonitoringService polling', {
|
|
51
|
-
queue: subscription.config.queue || null,
|
|
52
|
-
pattern: subscription.config.pattern,
|
|
53
|
-
});
|
|
54
50
|
void pushSnapshot(subscription);
|
|
55
51
|
subscription.interval = setInterval(() => {
|
|
56
52
|
void pushSnapshot(subscription);
|
|
@@ -59,10 +55,6 @@ const startPolling = (subscription) => {
|
|
|
59
55
|
const stopPolling = (subscription) => {
|
|
60
56
|
if (!subscription.interval)
|
|
61
57
|
return;
|
|
62
|
-
Logger.debug('Stopping QueueMonitoringService polling', {
|
|
63
|
-
queue: subscription.config.queue || null,
|
|
64
|
-
pattern: subscription.config.pattern,
|
|
65
|
-
});
|
|
66
58
|
clearInterval(subscription.interval);
|
|
67
59
|
subscription.interval = null;
|
|
68
60
|
};
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.63",
|
|
4
|
+
"buildDate": "2026-04-05T13:12:54.428Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
6
|
"node": "v22.22.1",
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
11
|
+
"commit": "533afc24",
|
|
12
12
|
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": {
|
|
27
27
|
"QueueMonitoringService.d.ts": {
|
|
28
|
-
"size":
|
|
29
|
-
"sha256": "
|
|
28
|
+
"size": 1684,
|
|
29
|
+
"sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
|
|
30
30
|
},
|
|
31
31
|
"QueueMonitoringService.js": {
|
|
32
|
-
"size":
|
|
33
|
-
"sha256": "
|
|
32
|
+
"size": 6679,
|
|
33
|
+
"sha256": "edd069c1229071078223cd23cbb7bdae9cbb2431ae603ab35f0cfcb7cd1f6ffe"
|
|
34
34
|
},
|
|
35
35
|
"api/workerClient.d.ts": {
|
|
36
36
|
"size": 597,
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
|
|
42
42
|
},
|
|
43
43
|
"build-manifest.json": {
|
|
44
|
-
"size":
|
|
45
|
-
"sha256": "
|
|
44
|
+
"size": 3866,
|
|
45
|
+
"sha256": "0419f514ce1ea9da7a111680fc03af009afce9a11c61bb909f53e42a09b61690"
|
|
46
46
|
},
|
|
47
47
|
"config/queueMonitor.d.ts": {
|
|
48
48
|
"size": 407,
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"sha256": "1b487b6e9cedbcb00cde7e920fb128724cb21aa154160e0bd02b02e2a0bd7334"
|
|
74
74
|
},
|
|
75
75
|
"dashboard-ui.js": {
|
|
76
|
-
"size":
|
|
77
|
-
"sha256": "
|
|
76
|
+
"size": 53771,
|
|
77
|
+
"sha256": "530bdfba8deed3b881ab2b7ea6190b9a521bf4bdcc5a08a386a71b4c031ccb56"
|
|
78
78
|
},
|
|
79
79
|
"driver.d.ts": {
|
|
80
80
|
"size": 707,
|
|
@@ -85,16 +85,16 @@
|
|
|
85
85
|
"sha256": "50229d05df9ca8bf5528b923e2799a4719e5a32ad9fa30bf3e2f36ecc91cf00c"
|
|
86
86
|
},
|
|
87
87
|
"index.d.ts": {
|
|
88
|
-
"size":
|
|
89
|
-
"sha256": "
|
|
88
|
+
"size": 2161,
|
|
89
|
+
"sha256": "2556c136641769f4e7cd81abd6ecae8663433aba74b09bc507b8fba01e3fc6f5"
|
|
90
90
|
},
|
|
91
91
|
"index.js": {
|
|
92
|
-
"size":
|
|
93
|
-
"sha256": "
|
|
92
|
+
"size": 12811,
|
|
93
|
+
"sha256": "eed486a0813866e41d75bac10fdbaba27091d6a07b32561c6176663e9a7dc510"
|
|
94
94
|
},
|
|
95
95
|
"metrics.d.ts": {
|
|
96
|
-
"size":
|
|
97
|
-
"sha256": "
|
|
96
|
+
"size": 868,
|
|
97
|
+
"sha256": "cbae21100c770e0960f4ae8c96bfaa2015162042122e04f00812817f04306512"
|
|
98
98
|
},
|
|
99
99
|
"metrics.js": {
|
|
100
100
|
"size": 3448,
|
package/dist/dashboard-ui.js
CHANGED
|
@@ -337,8 +337,6 @@ const getDashboardScriptAutoRefresh = () => `
|
|
|
337
337
|
const getRenderStatsFunction = () => `
|
|
338
338
|
function renderStats(data) {
|
|
339
339
|
const grid = document.getElementById('stats-grid');
|
|
340
|
-
grid.innerHTML = '';
|
|
341
|
-
|
|
342
340
|
const totalActive = data.queues.reduce((acc, q) => acc + q.counts.active, 0);
|
|
343
341
|
const totalFailed = data.queues.reduce((acc, q) => acc + q.counts.failed, 0);
|
|
344
342
|
const totalDelayed = data.queues.reduce((acc, q) => acc + q.counts.delayed, 0);
|
|
@@ -374,144 +372,359 @@ const getRenderStatsFunction = () => `
|
|
|
374
372
|
}
|
|
375
373
|
];
|
|
376
374
|
|
|
377
|
-
cards.forEach(card => {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
'
|
|
389
|
-
|
|
390
|
-
|
|
375
|
+
cards.forEach((card, index) => {
|
|
376
|
+
let div = grid.children[index];
|
|
377
|
+
let labelEl;
|
|
378
|
+
let valueEl;
|
|
379
|
+
let iconEl;
|
|
380
|
+
|
|
381
|
+
if (!div) {
|
|
382
|
+
div = document.createElement('div');
|
|
383
|
+
div.className = 'tile';
|
|
384
|
+
|
|
385
|
+
const header = document.createElement('div');
|
|
386
|
+
header.className = 'stat-header';
|
|
387
|
+
|
|
388
|
+
labelEl = document.createElement('div');
|
|
389
|
+
labelEl.className = 'stat-label';
|
|
390
|
+
|
|
391
|
+
iconEl = document.createElement('span');
|
|
392
|
+
iconEl.className = 'info-icon';
|
|
393
|
+
iconEl.textContent = 'i';
|
|
394
|
+
iconEl.addEventListener('mouseenter', showTooltip);
|
|
395
|
+
iconEl.addEventListener('mouseleave', hideTooltip);
|
|
396
|
+
|
|
397
|
+
valueEl = document.createElement('div');
|
|
398
|
+
valueEl.className = 'stat-value';
|
|
399
|
+
|
|
400
|
+
header.appendChild(labelEl);
|
|
401
|
+
header.appendChild(iconEl);
|
|
402
|
+
div.appendChild(header);
|
|
403
|
+
div.appendChild(valueEl);
|
|
404
|
+
grid.appendChild(div);
|
|
405
|
+
} else {
|
|
406
|
+
labelEl = div.querySelector('.stat-label');
|
|
407
|
+
valueEl = div.querySelector('.stat-value');
|
|
408
|
+
iconEl = div.querySelector('.info-icon');
|
|
409
|
+
}
|
|
391
410
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
411
|
+
if (labelEl) labelEl.textContent = card.label;
|
|
412
|
+
if (valueEl) {
|
|
413
|
+
valueEl.textContent = String(card.value);
|
|
414
|
+
valueEl.style.color = card.color || '';
|
|
415
|
+
}
|
|
416
|
+
if (iconEl) iconEl.setAttribute('data-info', card.info);
|
|
395
417
|
});
|
|
418
|
+
|
|
419
|
+
while (grid.children.length > cards.length) {
|
|
420
|
+
grid.removeChild(grid.lastElementChild);
|
|
421
|
+
}
|
|
396
422
|
}`;
|
|
397
|
-
const
|
|
398
|
-
function
|
|
399
|
-
|
|
423
|
+
const getUpdateQueueSelectHelpersFunction = () => `
|
|
424
|
+
function formatQueueOptionCounts(counts) {
|
|
425
|
+
return counts.waiting + ' waiting, ' + counts.failed + ' failed, ' + counts.completed + ' completed';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getPreferredQueue(select) {
|
|
400
429
|
const storedQueue = localStorage.getItem(QUEUE_KEY);
|
|
401
|
-
|
|
402
|
-
|
|
430
|
+
return currentQueue || select.value || storedQueue || '';
|
|
431
|
+
}
|
|
403
432
|
|
|
404
|
-
|
|
405
|
-
|
|
433
|
+
function getEmptyQueueSelection(select) {
|
|
434
|
+
select.disabled = true;
|
|
435
|
+
if (select.options.length !== 1 || select.options[0].value !== '' || select.options[0].textContent !== 'No queues') {
|
|
406
436
|
select.innerHTML = '<option value="">No queues</option>';
|
|
407
|
-
return '';
|
|
408
437
|
}
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
409
440
|
|
|
410
|
-
|
|
441
|
+
function getDesiredQueueOptions(queues) {
|
|
411
442
|
const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
|
|
412
443
|
const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
444
|
+
const totalCompleted = queues.reduce((acc, queue) => acc + queue.counts.completed, 0);
|
|
445
|
+
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
value: ALL_QUEUES,
|
|
449
|
+
text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed, ' + totalCompleted + ' completed)'
|
|
450
|
+
},
|
|
451
|
+
...queues.map(q => ({
|
|
452
|
+
value: q.name,
|
|
453
|
+
text: q.name + ' (' + formatQueueOptionCounts(q.counts) + ')'
|
|
454
|
+
}))
|
|
455
|
+
];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function syncQueueSelectOptions(select, desiredOptions) {
|
|
459
|
+
const existingOptions = new Map();
|
|
460
|
+
Array.from(select.options).forEach(option => {
|
|
461
|
+
existingOptions.set(option.value, option);
|
|
462
|
+
});
|
|
463
|
+
const existingValues = Array.from(select.options).map(option => option.value);
|
|
464
|
+
const desiredValues = desiredOptions.map(option => option.value);
|
|
465
|
+
const needsStructuralSync = existingValues.length !== desiredValues.length || desiredValues.some((value, index) => existingValues[index] !== value);
|
|
466
|
+
|
|
467
|
+
select.disabled = false;
|
|
416
468
|
|
|
469
|
+
desiredOptions.forEach((item, index) => {
|
|
470
|
+
let option = existingOptions.get(item.value);
|
|
471
|
+
if (!option) {
|
|
472
|
+
option = document.createElement('option');
|
|
473
|
+
option.value = item.value;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (option.textContent !== item.text) {
|
|
477
|
+
option.textContent = item.text;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (needsStructuralSync) {
|
|
481
|
+
const currentAtIndex = select.options[index];
|
|
482
|
+
if (currentAtIndex !== option) {
|
|
483
|
+
select.insertBefore(option, currentAtIndex || null);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (needsStructuralSync) {
|
|
489
|
+
Array.from(select.options).forEach(option => {
|
|
490
|
+
if (!desiredOptions.some(item => item.value === option.value)) {
|
|
491
|
+
option.remove();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
const getUpdateQueueSelectFunction = () => `
|
|
498
|
+
|
|
499
|
+
function updateQueueSelect(queues) {
|
|
500
|
+
const select = document.getElementById('queue-select');
|
|
501
|
+
const preferredQueue = getPreferredQueue(select);
|
|
502
|
+
|
|
503
|
+
if (queues.length === 0) {
|
|
504
|
+
return getEmptyQueueSelection(select);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const queueNames = queues.map(q => q.name);
|
|
417
508
|
const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
|
|
418
509
|
? preferredQueue
|
|
419
510
|
: queueNames[0];
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
select
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const opt = document.createElement('option');
|
|
426
|
-
opt.value = q.name;
|
|
427
|
-
opt.textContent = q.name + ' (' + q.counts.waiting + ' waiting, ' + q.counts.failed + ' failed)';
|
|
428
|
-
opt.selected = q.name === nextQueue;
|
|
429
|
-
select.appendChild(opt);
|
|
430
|
-
});
|
|
511
|
+
const desiredOptions = getDesiredQueueOptions(queues);
|
|
512
|
+
|
|
513
|
+
if (document.activeElement === select) {
|
|
514
|
+
return nextQueue;
|
|
515
|
+
}
|
|
431
516
|
|
|
432
|
-
select
|
|
517
|
+
syncQueueSelectOptions(select, desiredOptions);
|
|
518
|
+
|
|
519
|
+
if (select.value !== nextQueue) {
|
|
520
|
+
select.value = nextQueue;
|
|
521
|
+
}
|
|
433
522
|
return nextQueue;
|
|
434
523
|
}`;
|
|
435
|
-
const
|
|
524
|
+
const getRenderJobsStateFunction = () => `
|
|
436
525
|
// Track expanded job IDs to preserve state during SSE updates
|
|
437
526
|
let expandedJobIds = new Set();
|
|
527
|
+
`;
|
|
528
|
+
const getRenderJobsIdentityHelpersFunction = () => `
|
|
529
|
+
function getJobId(job) {
|
|
530
|
+
return String(job.id);
|
|
531
|
+
}
|
|
438
532
|
|
|
439
|
-
function
|
|
440
|
-
const
|
|
533
|
+
function getAdjacentJobDetailRow(row) {
|
|
534
|
+
const existingDetail = row.nextElementSibling;
|
|
535
|
+
return existingDetail && existingDetail.classList.contains('detail-row')
|
|
536
|
+
? existingDetail
|
|
537
|
+
: null;
|
|
538
|
+
}
|
|
441
539
|
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
540
|
+
function getJobStatusInfo(job) {
|
|
541
|
+
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
542
|
+
const statusMap = {
|
|
543
|
+
failed: { label: 'Failed', cls: 'status-failed' },
|
|
544
|
+
completed: { label: 'Completed', cls: 'status-completed' },
|
|
545
|
+
active: { label: 'Active', cls: 'status-active' },
|
|
546
|
+
waiting: { label: 'Waiting', cls: 'status-waiting' },
|
|
547
|
+
delayed: { label: 'Delayed', cls: 'status-delayed' },
|
|
548
|
+
paused: { label: 'Paused', cls: 'status-paused' }
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return statusMap[status] || statusMap.completed;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getJobRetryMarkup(job) {
|
|
555
|
+
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
556
|
+
if (status === 'failed') {
|
|
557
|
+
return '<button class="retry-btn" onclick="retryJob(' + "'" + job.id + "'" + ', ' + "'" + (job.queue || currentQueue) + "'" + ')" title="Retry this job">↻ Retry</button>';
|
|
558
|
+
}
|
|
559
|
+
return '<span style="color: var(--muted); font-size: 11px;">—</span>';
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
const getRenderJobsDetailRowHelpersFunction = () => `
|
|
563
|
+
|
|
564
|
+
function buildJobDetailMarkup(job) {
|
|
565
|
+
const jobData = {
|
|
566
|
+
id: job.id,
|
|
567
|
+
name: job.name,
|
|
568
|
+
queue: job.queue || currentQueue,
|
|
569
|
+
status: job.status || (job.failedReason ? 'failed' : 'completed'),
|
|
570
|
+
attempts: job.attempts,
|
|
571
|
+
timestamp: new Date(job.timestamp).toISOString(),
|
|
572
|
+
data: job.data || {},
|
|
573
|
+
failedReason: job.failedReason || null,
|
|
574
|
+
processedOn: job.processedOn ? new Date(job.processedOn).toISOString() : null,
|
|
575
|
+
finishedOn: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
576
|
+
returnvalue: job.returnvalue
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
return '<td colspan="7" class="detail-cell">' +
|
|
580
|
+
'<div class="detail-content">' +
|
|
581
|
+
'<strong style="color: var(--accent); display: block; margin-bottom: 8px;">Job Details:</strong>' +
|
|
582
|
+
'<pre>' + JSON.stringify(jobData, null, 2) + '</pre>' +
|
|
583
|
+
'</div>' +
|
|
584
|
+
'</td>';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function removeJobDetailRow(row) {
|
|
588
|
+
const existingDetail = getAdjacentJobDetailRow(row);
|
|
589
|
+
if (existingDetail) {
|
|
590
|
+
existingDetail.remove();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function buildOrUpdateJobDetailRow(job, existingDetail) {
|
|
595
|
+
let detailRow = existingDetail;
|
|
596
|
+
if (!detailRow) {
|
|
597
|
+
detailRow = document.createElement('tr');
|
|
598
|
+
detailRow.className = 'detail-row';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
detailRow.dataset.jobId = getJobId(job);
|
|
602
|
+
detailRow.innerHTML = buildJobDetailMarkup(job);
|
|
603
|
+
return detailRow;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function upsertJobDetailRow(row, job, parent) {
|
|
607
|
+
const detailRow = buildOrUpdateJobDetailRow(job, getAdjacentJobDetailRow(row));
|
|
608
|
+
|
|
609
|
+
if (parent) {
|
|
610
|
+
parent.appendChild(detailRow);
|
|
611
|
+
} else {
|
|
612
|
+
row.parentNode.insertBefore(detailRow, row.nextSibling);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return detailRow;
|
|
616
|
+
}
|
|
617
|
+
`;
|
|
618
|
+
const getRenderJobsEmptyStateFunction = () => `
|
|
619
|
+
|
|
620
|
+
function ensureEmptyJobsState(tbody) {
|
|
621
|
+
if (
|
|
622
|
+
tbody.children.length === 1 &&
|
|
623
|
+
tbody.children[0].textContent.includes('No recent jobs found')
|
|
624
|
+
) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--muted)">No recent jobs found</td></tr>';
|
|
629
|
+
}
|
|
630
|
+
`;
|
|
631
|
+
const getRenderJobsRowHelpersFunction = () => `
|
|
632
|
+
function createJobRow(job, idx) {
|
|
633
|
+
const tr = document.createElement('tr');
|
|
634
|
+
tr.className = 'expandable-row';
|
|
635
|
+
tr.addEventListener('click', (e) => {
|
|
636
|
+
if (e.target.classList.contains('retry-btn')) return;
|
|
637
|
+
toggleJobDetails(tr, tr.__jobData);
|
|
638
|
+
});
|
|
639
|
+
updateExistingJobRow(tr, job, idx);
|
|
640
|
+
return tr;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function updateExistingJobRow(tr, job, idx) {
|
|
644
|
+
const jobId = getJobId(job);
|
|
645
|
+
const statusInfo = getJobStatusInfo(job);
|
|
646
|
+
const isExpanded = expandedJobIds.has(jobId);
|
|
647
|
+
|
|
648
|
+
tr.__jobData = job;
|
|
649
|
+
tr.dataset.jobId = jobId;
|
|
650
|
+
tr.dataset.jobIndex = idx;
|
|
651
|
+
tr.innerHTML =
|
|
652
|
+
'<td><span class="expand-icon' + (isExpanded ? ' expanded' : '') + '">▶</span><code>' + job.id + '</code></td>' +
|
|
653
|
+
'<td>' + job.name + '</td>' +
|
|
654
|
+
'<td>' + (job.queue || currentQueue) + '</td>' +
|
|
655
|
+
'<td><span class="status-badge ' + statusInfo.cls + '">' + statusInfo.label + '</span></td>' +
|
|
656
|
+
'<td>' + job.attempts + '</td>' +
|
|
657
|
+
'<td>' + new Date(job.timestamp).toLocaleTimeString() + '</td>' +
|
|
658
|
+
'<td>' + getJobRetryMarkup(job) + '</td>';
|
|
659
|
+
|
|
660
|
+
if (job.failedReason) {
|
|
661
|
+
tr.title = 'Click to see error details';
|
|
662
|
+
} else {
|
|
663
|
+
tr.removeAttribute('title');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function getExistingJobRows(tbody) {
|
|
668
|
+
const rows = new Map();
|
|
669
|
+
Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
|
|
670
|
+
const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
|
|
671
|
+
if (jobId) rows.set(jobId, row);
|
|
450
672
|
});
|
|
673
|
+
return rows;
|
|
674
|
+
}
|
|
451
675
|
|
|
452
|
-
|
|
676
|
+
function getExistingJobDetailRows(tbody) {
|
|
677
|
+
const rows = new Map();
|
|
678
|
+
Array.from(tbody.querySelectorAll('tr.detail-row')).forEach(row => {
|
|
679
|
+
const jobId = row.dataset.jobId;
|
|
680
|
+
if (jobId) rows.set(jobId, row);
|
|
681
|
+
});
|
|
682
|
+
return rows;
|
|
683
|
+
}
|
|
453
684
|
|
|
454
|
-
|
|
455
|
-
|
|
685
|
+
function removeObsoleteJobRows(tbody, currentJobIds) {
|
|
686
|
+
Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
|
|
687
|
+
const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
|
|
688
|
+
if (!jobId || currentJobIds.has(jobId)) return;
|
|
689
|
+
removeJobDetailRow(row);
|
|
690
|
+
row.remove();
|
|
691
|
+
expandedJobIds.delete(jobId);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
`;
|
|
695
|
+
const getRenderJobsFunction = () => `
|
|
696
|
+
function renderJobs(jobs) {
|
|
697
|
+
const tbody = document.querySelector('#jobs-table tbody');
|
|
698
|
+
const jobList = Array.isArray(jobs) ? jobs : [];
|
|
699
|
+
|
|
700
|
+
if (jobList.length === 0) {
|
|
701
|
+
ensureEmptyJobsState(tbody);
|
|
702
|
+
expandedJobIds.clear();
|
|
456
703
|
return;
|
|
457
704
|
}
|
|
458
705
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
464
|
-
const statusMap = {
|
|
465
|
-
failed: { label: 'Failed', cls: 'status-failed' },
|
|
466
|
-
completed: { label: 'Completed', cls: 'status-completed' },
|
|
467
|
-
active: { label: 'Active', cls: 'status-active' },
|
|
468
|
-
waiting: { label: 'Waiting', cls: 'status-waiting' },
|
|
469
|
-
delayed: { label: 'Delayed', cls: 'status-delayed' },
|
|
470
|
-
paused: { label: 'Paused', cls: 'status-paused' }
|
|
471
|
-
};
|
|
472
|
-
const statusInfo = statusMap[status] || statusMap.completed;
|
|
473
|
-
|
|
474
|
-
const retryBtn = status === 'failed'
|
|
475
|
-
? '<button class="retry-btn" onclick="retryJob(' + "'" + job.id + "'" + ', ' + "'" + (job.queue || currentQueue) + "'" + ')" title="Retry this job">↻ Retry</button>'
|
|
476
|
-
: '<span style="color: var(--muted); font-size: 11px;">—</span>';
|
|
477
|
-
|
|
478
|
-
// Check if this job was previously expanded
|
|
479
|
-
const isExpanded = expandedJobIds.has(job.id);
|
|
480
|
-
|
|
481
|
-
tr.innerHTML =
|
|
482
|
-
'<td><span class="expand-icon' + (isExpanded ? ' expanded' : '') + '">▶</span><code>' + job.id + '</code></td>' +
|
|
483
|
-
'<td>' + job.name + '</td>' +
|
|
484
|
-
'<td>' + (job.queue || currentQueue) + '</td>' +
|
|
485
|
-
'<td><span class="status-badge ' +
|
|
486
|
-
statusInfo.cls +
|
|
487
|
-
'">' +
|
|
488
|
-
statusInfo.label +
|
|
489
|
-
'</span></td>' +
|
|
490
|
-
'<td>' + job.attempts + '</td>' +
|
|
491
|
-
'<td>' + new Date(job.timestamp).toLocaleTimeString() + '</td>' +
|
|
492
|
-
'<td>' + retryBtn + '</td>';
|
|
493
|
-
if (job.failedReason) {
|
|
494
|
-
tr.title = 'Click to see error details';
|
|
495
|
-
}
|
|
706
|
+
const currentJobIds = new Set(jobList.map(job => getJobId(job)));
|
|
707
|
+
const existingRows = getExistingJobRows(tbody);
|
|
708
|
+
const existingDetailRows = getExistingJobDetailRows(tbody);
|
|
709
|
+
const fragment = document.createDocumentFragment();
|
|
496
710
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
711
|
+
jobList.forEach((job, idx) => {
|
|
712
|
+
const jobId = getJobId(job);
|
|
713
|
+
const existingRow = existingRows.get(jobId);
|
|
714
|
+
const row = existingRow || createJobRow(job, idx);
|
|
501
715
|
|
|
502
|
-
|
|
716
|
+
if (existingRow) {
|
|
717
|
+
updateExistingJobRow(row, job, idx);
|
|
718
|
+
}
|
|
503
719
|
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
toggleJobDetails(tr, job);
|
|
508
|
-
}, 10); // Small delay to ensure DOM is ready
|
|
720
|
+
fragment.appendChild(row);
|
|
721
|
+
if (expandedJobIds.has(jobId)) {
|
|
722
|
+
fragment.appendChild(buildOrUpdateJobDetailRow(job, existingDetailRows.get(jobId)));
|
|
509
723
|
}
|
|
510
724
|
});
|
|
511
725
|
|
|
512
|
-
// Clean up expanded job IDs that are no longer in the current jobs list
|
|
513
|
-
const currentJobIds = new Set(jobs.map(job => job.id));
|
|
514
726
|
expandedJobIds = new Set([...expandedJobIds].filter(id => currentJobIds.has(id)));
|
|
727
|
+
tbody.replaceChildren(fragment);
|
|
515
728
|
}`;
|
|
516
729
|
const getRenderLocksFunction = () => `
|
|
517
730
|
// Track expanded lock keys to preserve state during SSE updates
|
|
@@ -698,45 +911,20 @@ const getToggleDetailsFunctions = () => `
|
|
|
698
911
|
function toggleJobDetails(row, job) {
|
|
699
912
|
const expandIcon = row.querySelector('.expand-icon');
|
|
700
913
|
const existingDetail = row.nextElementSibling;
|
|
914
|
+
const jobId = getJobId(job);
|
|
701
915
|
|
|
702
916
|
if (existingDetail && existingDetail.classList.contains('detail-row')) {
|
|
703
917
|
expandIcon.classList.remove('expanded');
|
|
704
918
|
existingDetail.remove();
|
|
705
919
|
// Remove from expanded set
|
|
706
|
-
expandedJobIds.delete(
|
|
920
|
+
expandedJobIds.delete(jobId);
|
|
707
921
|
return;
|
|
708
922
|
}
|
|
709
923
|
|
|
710
924
|
expandIcon.classList.add('expanded');
|
|
711
925
|
// Add to expanded set
|
|
712
|
-
expandedJobIds.add(
|
|
713
|
-
|
|
714
|
-
const detailRow = document.createElement('tr');
|
|
715
|
-
detailRow.className = 'detail-row';
|
|
716
|
-
|
|
717
|
-
const jobData = {
|
|
718
|
-
id: job.id,
|
|
719
|
-
name: job.name,
|
|
720
|
-
queue: job.queue || currentQueue,
|
|
721
|
-
status: job.status || (job.failedReason ? 'failed' : 'completed'),
|
|
722
|
-
attempts: job.attempts,
|
|
723
|
-
timestamp: new Date(job.timestamp).toISOString(),
|
|
724
|
-
data: job.data || {},
|
|
725
|
-
failedReason: job.failedReason || null,
|
|
726
|
-
processedOn: job.processedOn ? new Date(job.processedOn).toISOString() : null,
|
|
727
|
-
finishedOn: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
728
|
-
returnvalue: job.returnvalue
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
detailRow.innerHTML =
|
|
732
|
-
'<td colspan="7" class="detail-cell">' +
|
|
733
|
-
'<div class="detail-content">' +
|
|
734
|
-
'<strong style="color: var(--accent); display: block; margin-bottom: 8px;">Job Details:</strong>' +
|
|
735
|
-
'<pre>' + JSON.stringify(jobData, null, 2) + '</pre>' +
|
|
736
|
-
'</div>' +
|
|
737
|
-
'</td>';
|
|
738
|
-
|
|
739
|
-
row.parentNode.insertBefore(detailRow, row.nextSibling);
|
|
926
|
+
expandedJobIds.add(jobId);
|
|
927
|
+
upsertJobDetailRow(row, job);
|
|
740
928
|
}
|
|
741
929
|
|
|
742
930
|
function toggleLockDetails(row, lock) {
|
|
@@ -1030,7 +1218,13 @@ const getDashboardScriptFetch = () => `
|
|
|
1030
1218
|
`;
|
|
1031
1219
|
const getDashboardScriptRender = () => [
|
|
1032
1220
|
getRenderStatsFunction(),
|
|
1221
|
+
getUpdateQueueSelectHelpersFunction(),
|
|
1033
1222
|
getUpdateQueueSelectFunction(),
|
|
1223
|
+
getRenderJobsStateFunction(),
|
|
1224
|
+
getRenderJobsIdentityHelpersFunction(),
|
|
1225
|
+
getRenderJobsDetailRowHelpersFunction(),
|
|
1226
|
+
getRenderJobsEmptyStateFunction(),
|
|
1227
|
+
getRenderJobsRowHelpersFunction(),
|
|
1034
1228
|
getRenderJobsFunction(),
|
|
1035
1229
|
getRenderLocksFunction(),
|
|
1036
1230
|
getLockHelperFunctions(),
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type RedisConfig } from './connection';
|
|
|
3
3
|
import { type QueueDriver } from './driver';
|
|
4
4
|
import { type Metrics } from './metrics';
|
|
5
5
|
export type { JobPayload } from './driver';
|
|
6
|
+
export { createMetrics, type JobStatus, type JobSummary, type Metrics } from './metrics';
|
|
6
7
|
export { createWorker as createQueueWorker, type QueueWorker } from './worker';
|
|
7
8
|
export type QueueMonitorConfig = {
|
|
8
9
|
enabled?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { isNonEmptyString, Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
|
|
1
|
+
import { isArray, isNonEmptyString, Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
|
|
2
2
|
import { createRedisConnection } from './connection';
|
|
3
3
|
import { getDashboardHtml } from './dashboard-ui';
|
|
4
4
|
import { createBullMQDriver } from './driver';
|
|
5
5
|
import { createMetrics } from './metrics';
|
|
6
6
|
import { getRecentJobsForSelection, QueueMonitoringStream } from './QueueMonitoringService';
|
|
7
|
+
export { createMetrics } from './metrics';
|
|
7
8
|
export { createWorker as createQueueWorker } from './worker';
|
|
8
9
|
const DEFAULTS = {
|
|
9
10
|
enabled: true,
|
|
@@ -46,7 +47,7 @@ async function resolveKnownQueues(knownQueues) {
|
|
|
46
47
|
if (typeof knownQueues === 'function') {
|
|
47
48
|
return normalizeQueueNames(await knownQueues());
|
|
48
49
|
}
|
|
49
|
-
if (
|
|
50
|
+
if (isArray(knownQueues)) {
|
|
50
51
|
return normalizeQueueNames(knownQueues);
|
|
51
52
|
}
|
|
52
53
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.63",
|
|
4
4
|
"description": "Queue monitoring package for ZinTrust with BullMQ and Redis.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20.0.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@zintrust/core": "^0.4.
|
|
23
|
+
"@zintrust/core": "^0.4.63"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
"bullmq": "^5.71.1",
|
|
41
41
|
"ioredis": "^5.10.1"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|