@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.
@@ -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.sort((left, right) => right.timestamp - left.timestamp);
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);
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "0.4.43",
4
- "buildDate": "2026-04-01T18:15:53.560Z",
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": "57e4d1b5",
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": 1492,
29
- "sha256": "a87d7ab795606e7b238c01a657306bea805f63eb4c155951d31c191bfd8582e1"
28
+ "size": 1684,
29
+ "sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
30
30
  },
31
31
  "QueueMonitoringService.js": {
32
- "size": 6190,
33
- "sha256": "068477c0b545686c9c35d6b42960b5ecf8d19ee4f3f5adec8ddeeb21113258b3"
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": "6b9eece186e37f5ed657a014c799e3783f30d0e16e0c2fb4e9fa8f2ffcbdd673"
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": 42243,
77
- "sha256": "04f5bf91f2f283db1149b3cea7ea74a17147d13df4f5e83ca3ec4ed79cedd03e"
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": 1965,
89
- "sha256": "be25d5e56f1c0ce0387c67ef7ce7544074e0d90698ad93826bb4d557d1a0b26f"
88
+ "size": 2161,
89
+ "sha256": "2556c136641769f4e7cd81abd6ecae8663433aba74b09bc507b8fba01e3fc6f5"
90
90
  },
91
91
  "index.js": {
92
- "size": 11889,
93
- "sha256": "19a7bdc71cec34fe732dd0d7177ad366d271e31462f402911d4a2700d0ea8b2c"
92
+ "size": 12811,
93
+ "sha256": "a3691ff87403fc468c86efec2d907b45c5c8a642e75a6e6ecf29bdaaf3d220ed"
94
94
  },
95
95
  "metrics.d.ts": {
96
- "size": 848,
97
- "sha256": "3ed092d97ff3ce81b43aa85dbded2898479d5c4d46860f22f21415085dce6229"
96
+ "size": 868,
97
+ "sha256": "cbae21100c770e0960f4ae8c96bfaa2015162042122e04f00812817f04306512"
98
98
  },
99
99
  "metrics.js": {
100
100
  "size": 3448,
@@ -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
- const div = document.createElement('div');
379
- div.className = 'tile';
380
- const infoIcon = '<span class="info-icon" data-info="' + card.info + '">i</span>';
381
- div.innerHTML =
382
- '<div class="stat-header">' +
383
- '<div class="stat-label">' + card.label + '</div>' +
384
- infoIcon +
385
- '</div>' +
386
- '<div class="stat-value" style="' + (card.color ? 'color:' + card.color : '') + '">' +
387
- card.value +
388
- '</div>';
389
- grid.appendChild(div);
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
- document.querySelectorAll('.info-icon').forEach(icon => {
393
- icon.addEventListener('mouseenter', showTooltip);
394
- icon.addEventListener('mouseleave', hideTooltip);
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.innerHTML = '<option value="">No queues</option>';
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
- allOption.selected = nextQueue === ALL_QUEUES;
422
- select.appendChild(allOption);
423
-
424
- queues.forEach(q => {
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);
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 getRenderJobsFunction = () => `
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 renderJobs(jobs) {
440
- const tbody = document.querySelector('#jobs-table tbody');
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
- // Store currently expanded job IDs before clearing
443
- const currentExpanded = document.querySelectorAll('.expand-icon.expanded');
444
- currentExpanded.forEach(icon => {
445
- const row = icon.closest('tr');
446
- if (row) {
447
- const jobId = row.querySelector('code')?.textContent;
448
- if (jobId) expandedJobIds.add(jobId);
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
- tbody.innerHTML = '';
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 (!jobs || jobs.length === 0) {
455
- tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; color: var(--muted)">No recent jobs found</td></tr>';
640
+ if (jobList.length === 0) {
641
+ ensureEmptyJobsState(tbody);
642
+ expandedJobIds.clear();
456
643
  return;
457
644
  }
458
645
 
459
- jobs.forEach((job, idx) => {
460
- const tr = document.createElement('tr');
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
- tr.addEventListener('click', (e) => {
498
- if (e.target.classList.contains('retry-btn')) return;
499
- toggleJobDetails(tr, job);
500
- });
649
+ const existingRows = getExistingJobRows(tbody);
501
650
 
502
- tbody.appendChild(tr);
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
- // Auto-expand if this job was previously expanded
505
- if (isExpanded) {
506
- setTimeout(() => {
507
- toggleJobDetails(tr, job);
508
- }, 10); // Small delay to ensure DOM is ready
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(job.id);
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(job.id);
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 (Array.isArray(knownQueues)) {
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.50",
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.50"
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
+ }