@zintrust/queue-monitor 0.4.50 → 0.4.60
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 -1
- package/dist/build-manifest.json +16 -16
- package/dist/dashboard-ui.js +257 -125
- 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);
|
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.60",
|
|
4
|
+
"buildDate": "2026-04-05T07:02:46.309Z",
|
|
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": "be058c1d",
|
|
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": 7011,
|
|
33
|
+
"sha256": "23cfc989a6e6926224e3507cb2fad5e6e884070406ca9cf58644536b63a52eb7"
|
|
34
34
|
},
|
|
35
35
|
"api/workerClient.d.ts": {
|
|
36
36
|
"size": 597,
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"build-manifest.json": {
|
|
44
44
|
"size": 3865,
|
|
45
|
-
"sha256": "
|
|
45
|
+
"sha256": "c2769a76a31b33d2de31cb3281ada6e5e41c31c26e4477c05617d02bebaa64af"
|
|
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": 51371,
|
|
77
|
+
"sha256": "955df86422fd6cda793ade860394744cfdb9217ecc7a841ac89dc7027433b834"
|
|
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": "a3691ff87403fc468c86efec2d907b45c5c8a642e75a6e6ecf29bdaaf3d220ed"
|
|
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,143 +372,299 @@ 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';
|
|
391
399
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|
|
410
|
+
|
|
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
423
|
const getUpdateQueueSelectFunction = () => `
|
|
398
424
|
function updateQueueSelect(queues) {
|
|
399
425
|
const select = document.getElementById('queue-select');
|
|
400
426
|
const storedQueue = localStorage.getItem(QUEUE_KEY);
|
|
401
427
|
const preferredQueue = currentQueue || select.value || storedQueue || '';
|
|
402
|
-
select.innerHTML = '';
|
|
403
428
|
|
|
404
429
|
if (queues.length === 0) {
|
|
405
430
|
select.disabled = true;
|
|
406
|
-
select.
|
|
431
|
+
if (select.options.length !== 1 || select.options[0].value !== '' || select.options[0].textContent !== 'No queues') {
|
|
432
|
+
select.innerHTML = '<option value="">No queues</option>';
|
|
433
|
+
}
|
|
407
434
|
return '';
|
|
408
435
|
}
|
|
409
436
|
|
|
410
437
|
const queueNames = queues.map(q => q.name);
|
|
411
438
|
const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
|
|
412
439
|
const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
|
|
413
|
-
const allOption = document.createElement('option');
|
|
414
|
-
allOption.value = ALL_QUEUES;
|
|
415
|
-
allOption.textContent = 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed)';
|
|
416
440
|
|
|
417
441
|
const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
|
|
418
442
|
? preferredQueue
|
|
419
443
|
: queueNames[0];
|
|
444
|
+
const desiredOptions = [
|
|
445
|
+
{
|
|
446
|
+
value: ALL_QUEUES,
|
|
447
|
+
text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed)'
|
|
448
|
+
},
|
|
449
|
+
...queues.map(q => ({
|
|
450
|
+
value: q.name,
|
|
451
|
+
text: q.name + ' (' + q.counts.waiting + ' waiting, ' + q.counts.failed + ' failed)'
|
|
452
|
+
}))
|
|
453
|
+
];
|
|
454
|
+
|
|
455
|
+
const existingOptions = new Map();
|
|
456
|
+
Array.from(select.options).forEach(option => {
|
|
457
|
+
existingOptions.set(option.value, option);
|
|
458
|
+
});
|
|
459
|
+
|
|
420
460
|
select.disabled = false;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
461
|
+
|
|
462
|
+
desiredOptions.forEach((item, index) => {
|
|
463
|
+
let option = existingOptions.get(item.value);
|
|
464
|
+
if (!option) {
|
|
465
|
+
option = document.createElement('option');
|
|
466
|
+
option.value = item.value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
option.textContent = item.text;
|
|
470
|
+
option.selected = item.value === nextQueue;
|
|
471
|
+
|
|
472
|
+
const currentAtIndex = select.options[index];
|
|
473
|
+
if (currentAtIndex !== option) {
|
|
474
|
+
select.appendChild(option);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
Array.from(select.options).forEach(option => {
|
|
479
|
+
if (!desiredOptions.some(item => item.value === option.value)) {
|
|
480
|
+
option.remove();
|
|
481
|
+
}
|
|
430
482
|
});
|
|
431
483
|
|
|
432
484
|
select.value = nextQueue;
|
|
433
485
|
return nextQueue;
|
|
434
486
|
}`;
|
|
435
|
-
const
|
|
487
|
+
const getRenderJobsStateFunction = () => `
|
|
436
488
|
// Track expanded job IDs to preserve state during SSE updates
|
|
437
489
|
let expandedJobIds = new Set();
|
|
490
|
+
`;
|
|
491
|
+
const getRenderJobsDetailHelpersFunction = () => `
|
|
492
|
+
function getJobId(job) {
|
|
493
|
+
return String(job.id);
|
|
494
|
+
}
|
|
438
495
|
|
|
439
|
-
function
|
|
440
|
-
const
|
|
496
|
+
function getJobStatusInfo(job) {
|
|
497
|
+
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
498
|
+
const statusMap = {
|
|
499
|
+
failed: { label: 'Failed', cls: 'status-failed' },
|
|
500
|
+
completed: { label: 'Completed', cls: 'status-completed' },
|
|
501
|
+
active: { label: 'Active', cls: 'status-active' },
|
|
502
|
+
waiting: { label: 'Waiting', cls: 'status-waiting' },
|
|
503
|
+
delayed: { label: 'Delayed', cls: 'status-delayed' },
|
|
504
|
+
paused: { label: 'Paused', cls: 'status-paused' }
|
|
505
|
+
};
|
|
441
506
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
507
|
+
return statusMap[status] || statusMap.completed;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getJobRetryMarkup(job) {
|
|
511
|
+
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
512
|
+
if (status === 'failed') {
|
|
513
|
+
return '<button class="retry-btn" onclick="retryJob(' + "'" + job.id + "'" + ', ' + "'" + (job.queue || currentQueue) + "'" + ')" title="Retry this job">↻ Retry</button>';
|
|
514
|
+
}
|
|
515
|
+
return '<span style="color: var(--muted); font-size: 11px;">—</span>';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildJobDetailMarkup(job) {
|
|
519
|
+
const jobData = {
|
|
520
|
+
id: job.id,
|
|
521
|
+
name: job.name,
|
|
522
|
+
queue: job.queue || currentQueue,
|
|
523
|
+
status: job.status || (job.failedReason ? 'failed' : 'completed'),
|
|
524
|
+
attempts: job.attempts,
|
|
525
|
+
timestamp: new Date(job.timestamp).toISOString(),
|
|
526
|
+
data: job.data || {},
|
|
527
|
+
failedReason: job.failedReason || null,
|
|
528
|
+
processedOn: job.processedOn ? new Date(job.processedOn).toISOString() : null,
|
|
529
|
+
finishedOn: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
530
|
+
returnvalue: job.returnvalue
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
return '<td colspan="7" class="detail-cell">' +
|
|
534
|
+
'<div class="detail-content">' +
|
|
535
|
+
'<strong style="color: var(--accent); display: block; margin-bottom: 8px;">Job Details:</strong>' +
|
|
536
|
+
'<pre>' + JSON.stringify(jobData, null, 2) + '</pre>' +
|
|
537
|
+
'</div>' +
|
|
538
|
+
'</td>';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function removeJobDetailRow(row) {
|
|
542
|
+
const existingDetail = row.nextElementSibling;
|
|
543
|
+
if (existingDetail && existingDetail.classList.contains('detail-row')) {
|
|
544
|
+
existingDetail.remove();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function upsertJobDetailRow(row, job, parent) {
|
|
549
|
+
const existingDetail = row.nextElementSibling && row.nextElementSibling.classList.contains('detail-row')
|
|
550
|
+
? row.nextElementSibling
|
|
551
|
+
: null;
|
|
552
|
+
|
|
553
|
+
let detailRow = existingDetail;
|
|
554
|
+
if (!detailRow) {
|
|
555
|
+
detailRow = document.createElement('tr');
|
|
556
|
+
detailRow.className = 'detail-row';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
detailRow.dataset.jobId = getJobId(job);
|
|
560
|
+
detailRow.innerHTML = buildJobDetailMarkup(job);
|
|
561
|
+
|
|
562
|
+
if (parent) {
|
|
563
|
+
parent.appendChild(detailRow);
|
|
564
|
+
} else {
|
|
565
|
+
row.parentNode.insertBefore(detailRow, row.nextSibling);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function ensureEmptyJobsState(tbody) {
|
|
570
|
+
if (
|
|
571
|
+
tbody.children.length === 1 &&
|
|
572
|
+
tbody.children[0].textContent.includes('No recent jobs found')
|
|
573
|
+
) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--muted)">No recent jobs found</td></tr>';
|
|
578
|
+
}
|
|
579
|
+
`;
|
|
580
|
+
const getRenderJobsRowHelpersFunction = () => `
|
|
581
|
+
function createJobRow(job, idx) {
|
|
582
|
+
const tr = document.createElement('tr');
|
|
583
|
+
tr.className = 'expandable-row';
|
|
584
|
+
tr.addEventListener('click', (e) => {
|
|
585
|
+
if (e.target.classList.contains('retry-btn')) return;
|
|
586
|
+
toggleJobDetails(tr, tr.__jobData);
|
|
587
|
+
});
|
|
588
|
+
updateExistingJobRow(tr, job, idx);
|
|
589
|
+
return tr;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function updateExistingJobRow(tr, job, idx) {
|
|
593
|
+
const jobId = getJobId(job);
|
|
594
|
+
const statusInfo = getJobStatusInfo(job);
|
|
595
|
+
const isExpanded = expandedJobIds.has(jobId);
|
|
596
|
+
|
|
597
|
+
tr.__jobData = job;
|
|
598
|
+
tr.dataset.jobId = jobId;
|
|
599
|
+
tr.dataset.jobIndex = idx;
|
|
600
|
+
tr.innerHTML =
|
|
601
|
+
'<td><span class="expand-icon' + (isExpanded ? ' expanded' : '') + '">▶</span><code>' + job.id + '</code></td>' +
|
|
602
|
+
'<td>' + job.name + '</td>' +
|
|
603
|
+
'<td>' + (job.queue || currentQueue) + '</td>' +
|
|
604
|
+
'<td><span class="status-badge ' + statusInfo.cls + '">' + statusInfo.label + '</span></td>' +
|
|
605
|
+
'<td>' + job.attempts + '</td>' +
|
|
606
|
+
'<td>' + new Date(job.timestamp).toLocaleTimeString() + '</td>' +
|
|
607
|
+
'<td>' + getJobRetryMarkup(job) + '</td>';
|
|
608
|
+
|
|
609
|
+
if (job.failedReason) {
|
|
610
|
+
tr.title = 'Click to see error details';
|
|
611
|
+
} else {
|
|
612
|
+
tr.removeAttribute('title');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getExistingJobRows(tbody) {
|
|
617
|
+
const rows = new Map();
|
|
618
|
+
Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
|
|
619
|
+
const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
|
|
620
|
+
if (jobId) rows.set(jobId, row);
|
|
450
621
|
});
|
|
622
|
+
return rows;
|
|
623
|
+
}
|
|
451
624
|
|
|
452
|
-
|
|
625
|
+
function removeObsoleteJobRows(tbody, currentJobIds) {
|
|
626
|
+
Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
|
|
627
|
+
const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
|
|
628
|
+
if (!jobId || currentJobIds.has(jobId)) return;
|
|
629
|
+
removeJobDetailRow(row);
|
|
630
|
+
row.remove();
|
|
631
|
+
expandedJobIds.delete(jobId);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
`;
|
|
635
|
+
const getRenderJobsFunction = () => `
|
|
636
|
+
function renderJobs(jobs) {
|
|
637
|
+
const tbody = document.querySelector('#jobs-table tbody');
|
|
638
|
+
const jobList = Array.isArray(jobs) ? jobs : [];
|
|
453
639
|
|
|
454
|
-
if (
|
|
455
|
-
tbody
|
|
640
|
+
if (jobList.length === 0) {
|
|
641
|
+
ensureEmptyJobsState(tbody);
|
|
642
|
+
expandedJobIds.clear();
|
|
456
643
|
return;
|
|
457
644
|
}
|
|
458
645
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
tr.className = 'expandable-row';
|
|
462
|
-
tr.dataset.jobIndex = idx;
|
|
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
|
-
}
|
|
646
|
+
const currentJobIds = new Set(jobList.map(job => getJobId(job)));
|
|
647
|
+
removeObsoleteJobRows(tbody, currentJobIds);
|
|
496
648
|
|
|
497
|
-
|
|
498
|
-
if (e.target.classList.contains('retry-btn')) return;
|
|
499
|
-
toggleJobDetails(tr, job);
|
|
500
|
-
});
|
|
649
|
+
const existingRows = getExistingJobRows(tbody);
|
|
501
650
|
|
|
502
|
-
|
|
651
|
+
jobList.forEach((job, idx) => {
|
|
652
|
+
const jobId = getJobId(job);
|
|
653
|
+
const existingRow = existingRows.get(jobId);
|
|
654
|
+
const row = existingRow || createJobRow(job, idx);
|
|
503
655
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
656
|
+
if (existingRow) {
|
|
657
|
+
updateExistingJobRow(row, job, idx);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
tbody.appendChild(row);
|
|
661
|
+
if (expandedJobIds.has(jobId)) {
|
|
662
|
+
upsertJobDetailRow(row, job, tbody);
|
|
663
|
+
} else {
|
|
664
|
+
removeJobDetailRow(row);
|
|
509
665
|
}
|
|
510
666
|
});
|
|
511
667
|
|
|
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
668
|
expandedJobIds = new Set([...expandedJobIds].filter(id => currentJobIds.has(id)));
|
|
515
669
|
}`;
|
|
516
670
|
const getRenderLocksFunction = () => `
|
|
@@ -698,45 +852,20 @@ const getToggleDetailsFunctions = () => `
|
|
|
698
852
|
function toggleJobDetails(row, job) {
|
|
699
853
|
const expandIcon = row.querySelector('.expand-icon');
|
|
700
854
|
const existingDetail = row.nextElementSibling;
|
|
855
|
+
const jobId = getJobId(job);
|
|
701
856
|
|
|
702
857
|
if (existingDetail && existingDetail.classList.contains('detail-row')) {
|
|
703
858
|
expandIcon.classList.remove('expanded');
|
|
704
859
|
existingDetail.remove();
|
|
705
860
|
// Remove from expanded set
|
|
706
|
-
expandedJobIds.delete(
|
|
861
|
+
expandedJobIds.delete(jobId);
|
|
707
862
|
return;
|
|
708
863
|
}
|
|
709
864
|
|
|
710
865
|
expandIcon.classList.add('expanded');
|
|
711
866
|
// 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);
|
|
867
|
+
expandedJobIds.add(jobId);
|
|
868
|
+
upsertJobDetailRow(row, job);
|
|
740
869
|
}
|
|
741
870
|
|
|
742
871
|
function toggleLockDetails(row, lock) {
|
|
@@ -1031,6 +1160,9 @@ const getDashboardScriptFetch = () => `
|
|
|
1031
1160
|
const getDashboardScriptRender = () => [
|
|
1032
1161
|
getRenderStatsFunction(),
|
|
1033
1162
|
getUpdateQueueSelectFunction(),
|
|
1163
|
+
getRenderJobsStateFunction(),
|
|
1164
|
+
getRenderJobsDetailHelpersFunction(),
|
|
1165
|
+
getRenderJobsRowHelpersFunction(),
|
|
1034
1166
|
getRenderJobsFunction(),
|
|
1035
1167
|
getRenderLocksFunction(),
|
|
1036
1168
|
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.60",
|
|
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.60"
|
|
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
|
+
}
|