@zintrust/queue-monitor 0.4.49 → 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.d.ts +5 -4
- package/dist/QueueMonitoringService.js +84 -72
- package/dist/api/workerClient.d.ts +20 -0
- package/dist/api/workerClient.js +45 -0
- package/dist/build-manifest.json +63 -19
- package/dist/config/queueMonitor.d.ts +18 -0
- package/dist/config/queueMonitor.js +21 -0
- package/dist/config/workerConfig.d.ts +3 -0
- package/dist/config/workerConfig.js +19 -0
- package/dist/dashboard-ui.js +391 -132
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -6
- package/dist/metrics.d.ts +1 -0
- package/dist/routes/workers.d.ts +10 -0
- package/dist/routes/workers.js +20 -0
- package/dist/workers-ui.d.ts +7 -0
- package/dist/workers-ui.js +655 -0
- package/package.json +2 -2
package/dist/dashboard-ui.js
CHANGED
|
@@ -239,7 +239,9 @@ const getDashboardBody = () => `
|
|
|
239
239
|
const getDashboardScriptState = (options) => String.raw `
|
|
240
240
|
const AUTO_REFRESH = ${options.autoRefresh ? 'true' : 'false'};
|
|
241
241
|
const REFRESH_INTERVAL = ${Math.max(1000, Math.floor(options.refreshIntervalMs || 0))};
|
|
242
|
+
const STREAM_RESET_MS = Math.max(15000, REFRESH_INTERVAL * 4);
|
|
242
243
|
const API_BASE = ${JSON.stringify(options.basePath)};
|
|
244
|
+
const ALL_QUEUES = '__all__';
|
|
243
245
|
const THEME_KEY = 'zintrust-queue-monitor-theme';
|
|
244
246
|
const AUTO_REFRESH_KEY = 'zintrust-queue-monitor-auto-refresh';
|
|
245
247
|
const QUEUE_KEY = 'zintrust-queue-monitor-selected-queue';
|
|
@@ -250,6 +252,10 @@ const getDashboardScriptState = (options) => String.raw `
|
|
|
250
252
|
let sseActive = false;
|
|
251
253
|
let lastSseQueue = null;
|
|
252
254
|
let lastSsePattern = null;
|
|
255
|
+
let reconnectTimer = null;
|
|
256
|
+
let streamWatchdogTimer = null;
|
|
257
|
+
let dashboardResetTimer = null;
|
|
258
|
+
let reconnectAttempts = 0;
|
|
253
259
|
let currentTheme = null;
|
|
254
260
|
`;
|
|
255
261
|
const getDashboardScriptTheme = () => `
|
|
@@ -308,6 +314,8 @@ const getDashboardScriptAutoRefresh = () => `
|
|
|
308
314
|
eventSource = null;
|
|
309
315
|
}
|
|
310
316
|
sseActive = false;
|
|
317
|
+
clearSseTimers();
|
|
318
|
+
clearError();
|
|
311
319
|
}
|
|
312
320
|
|
|
313
321
|
if (autoRefreshEnabled && !sseActive) {
|
|
@@ -329,8 +337,6 @@ const getDashboardScriptAutoRefresh = () => `
|
|
|
329
337
|
const getRenderStatsFunction = () => `
|
|
330
338
|
function renderStats(data) {
|
|
331
339
|
const grid = document.getElementById('stats-grid');
|
|
332
|
-
grid.innerHTML = '';
|
|
333
|
-
|
|
334
340
|
const totalActive = data.queues.reduce((acc, q) => acc + q.counts.active, 0);
|
|
335
341
|
const totalFailed = data.queues.reduce((acc, q) => acc + q.counts.failed, 0);
|
|
336
342
|
const totalDelayed = data.queues.reduce((acc, q) => acc + q.counts.delayed, 0);
|
|
@@ -366,121 +372,299 @@ const getRenderStatsFunction = () => `
|
|
|
366
372
|
}
|
|
367
373
|
];
|
|
368
374
|
|
|
369
|
-
cards.forEach(card => {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
'
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
410
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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);
|
|
387
417
|
});
|
|
418
|
+
|
|
419
|
+
while (grid.children.length > cards.length) {
|
|
420
|
+
grid.removeChild(grid.lastElementChild);
|
|
421
|
+
}
|
|
388
422
|
}`;
|
|
389
423
|
const getUpdateQueueSelectFunction = () => `
|
|
390
424
|
function updateQueueSelect(queues) {
|
|
391
425
|
const select = document.getElementById('queue-select');
|
|
392
|
-
const
|
|
393
|
-
select.
|
|
426
|
+
const storedQueue = localStorage.getItem(QUEUE_KEY);
|
|
427
|
+
const preferredQueue = currentQueue || select.value || storedQueue || '';
|
|
428
|
+
|
|
429
|
+
if (queues.length === 0) {
|
|
430
|
+
select.disabled = true;
|
|
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
|
+
}
|
|
434
|
+
return '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const queueNames = queues.map(q => q.name);
|
|
438
|
+
const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
|
|
439
|
+
const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
|
|
440
|
+
|
|
441
|
+
const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
|
|
442
|
+
? preferredQueue
|
|
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
|
+
|
|
460
|
+
select.disabled = false;
|
|
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
|
+
}
|
|
394
468
|
|
|
395
|
-
|
|
469
|
+
option.textContent = item.text;
|
|
470
|
+
option.selected = item.value === nextQueue;
|
|
396
471
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
opt.selected = q.name === currentSelection;
|
|
402
|
-
select.appendChild(opt);
|
|
472
|
+
const currentAtIndex = select.options[index];
|
|
473
|
+
if (currentAtIndex !== option) {
|
|
474
|
+
select.appendChild(option);
|
|
475
|
+
}
|
|
403
476
|
});
|
|
477
|
+
|
|
478
|
+
Array.from(select.options).forEach(option => {
|
|
479
|
+
if (!desiredOptions.some(item => item.value === option.value)) {
|
|
480
|
+
option.remove();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
select.value = nextQueue;
|
|
485
|
+
return nextQueue;
|
|
404
486
|
}`;
|
|
405
|
-
const
|
|
487
|
+
const getRenderJobsStateFunction = () => `
|
|
406
488
|
// Track expanded job IDs to preserve state during SSE updates
|
|
407
489
|
let expandedJobIds = new Set();
|
|
490
|
+
`;
|
|
491
|
+
const getRenderJobsDetailHelpersFunction = () => `
|
|
492
|
+
function getJobId(job) {
|
|
493
|
+
return String(job.id);
|
|
494
|
+
}
|
|
408
495
|
|
|
409
|
-
function
|
|
410
|
-
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
|
+
};
|
|
411
506
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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);
|
|
420
587
|
});
|
|
588
|
+
updateExistingJobRow(tr, job, idx);
|
|
589
|
+
return tr;
|
|
590
|
+
}
|
|
421
591
|
|
|
422
|
-
|
|
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
|
+
}
|
|
423
615
|
|
|
424
|
-
|
|
425
|
-
|
|
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);
|
|
621
|
+
});
|
|
622
|
+
return rows;
|
|
623
|
+
}
|
|
624
|
+
|
|
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 : [];
|
|
639
|
+
|
|
640
|
+
if (jobList.length === 0) {
|
|
641
|
+
ensureEmptyJobsState(tbody);
|
|
642
|
+
expandedJobIds.clear();
|
|
426
643
|
return;
|
|
427
644
|
}
|
|
428
645
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
tr.className = 'expandable-row';
|
|
432
|
-
tr.dataset.jobIndex = idx;
|
|
433
|
-
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
434
|
-
const statusMap = {
|
|
435
|
-
failed: { label: 'Failed', cls: 'status-failed' },
|
|
436
|
-
completed: { label: 'Completed', cls: 'status-completed' },
|
|
437
|
-
active: { label: 'Active', cls: 'status-active' },
|
|
438
|
-
waiting: { label: 'Waiting', cls: 'status-waiting' },
|
|
439
|
-
delayed: { label: 'Delayed', cls: 'status-delayed' },
|
|
440
|
-
paused: { label: 'Paused', cls: 'status-paused' }
|
|
441
|
-
};
|
|
442
|
-
const statusInfo = statusMap[status] || statusMap.completed;
|
|
443
|
-
|
|
444
|
-
const retryBtn = status === 'failed'
|
|
445
|
-
? '<button class="retry-btn" onclick="retryJob(' + "'" + job.id + "'" + ', ' + "'" + (job.queue || currentQueue) + "'" + ')" title="Retry this job">↻ Retry</button>'
|
|
446
|
-
: '<span style="color: var(--muted); font-size: 11px;">—</span>';
|
|
447
|
-
|
|
448
|
-
// Check if this job was previously expanded
|
|
449
|
-
const isExpanded = expandedJobIds.has(job.id);
|
|
450
|
-
|
|
451
|
-
tr.innerHTML =
|
|
452
|
-
'<td><span class="expand-icon' + (isExpanded ? ' expanded' : '') + '">▶</span><code>' + job.id + '</code></td>' +
|
|
453
|
-
'<td>' + job.name + '</td>' +
|
|
454
|
-
'<td>' + (job.queue || currentQueue) + '</td>' +
|
|
455
|
-
'<td><span class="status-badge ' +
|
|
456
|
-
statusInfo.cls +
|
|
457
|
-
'">' +
|
|
458
|
-
statusInfo.label +
|
|
459
|
-
'</span></td>' +
|
|
460
|
-
'<td>' + job.attempts + '</td>' +
|
|
461
|
-
'<td>' + new Date(job.timestamp).toLocaleTimeString() + '</td>' +
|
|
462
|
-
'<td>' + retryBtn + '</td>';
|
|
463
|
-
if (job.failedReason) {
|
|
464
|
-
tr.title = 'Click to see error details';
|
|
465
|
-
}
|
|
646
|
+
const currentJobIds = new Set(jobList.map(job => getJobId(job)));
|
|
647
|
+
removeObsoleteJobRows(tbody, currentJobIds);
|
|
466
648
|
|
|
467
|
-
|
|
468
|
-
if (e.target.classList.contains('retry-btn')) return;
|
|
469
|
-
toggleJobDetails(tr, job);
|
|
470
|
-
});
|
|
649
|
+
const existingRows = getExistingJobRows(tbody);
|
|
471
650
|
|
|
472
|
-
|
|
651
|
+
jobList.forEach((job, idx) => {
|
|
652
|
+
const jobId = getJobId(job);
|
|
653
|
+
const existingRow = existingRows.get(jobId);
|
|
654
|
+
const row = existingRow || createJobRow(job, idx);
|
|
473
655
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
479
665
|
}
|
|
480
666
|
});
|
|
481
667
|
|
|
482
|
-
// Clean up expanded job IDs that are no longer in the current jobs list
|
|
483
|
-
const currentJobIds = new Set(jobs.map(job => job.id));
|
|
484
668
|
expandedJobIds = new Set([...expandedJobIds].filter(id => currentJobIds.has(id)));
|
|
485
669
|
}`;
|
|
486
670
|
const getRenderLocksFunction = () => `
|
|
@@ -634,6 +818,12 @@ const getErrorAndTooltipFunctions = () => `
|
|
|
634
818
|
el.style.display = 'block';
|
|
635
819
|
}
|
|
636
820
|
|
|
821
|
+
function clearError() {
|
|
822
|
+
const el = document.getElementById('error-container');
|
|
823
|
+
el.textContent = '';
|
|
824
|
+
el.style.display = 'none';
|
|
825
|
+
}
|
|
826
|
+
|
|
637
827
|
let tooltipEl = null;
|
|
638
828
|
function showTooltip(e) {
|
|
639
829
|
const info = e.target.getAttribute('data-info');
|
|
@@ -662,45 +852,20 @@ const getToggleDetailsFunctions = () => `
|
|
|
662
852
|
function toggleJobDetails(row, job) {
|
|
663
853
|
const expandIcon = row.querySelector('.expand-icon');
|
|
664
854
|
const existingDetail = row.nextElementSibling;
|
|
855
|
+
const jobId = getJobId(job);
|
|
665
856
|
|
|
666
857
|
if (existingDetail && existingDetail.classList.contains('detail-row')) {
|
|
667
858
|
expandIcon.classList.remove('expanded');
|
|
668
859
|
existingDetail.remove();
|
|
669
860
|
// Remove from expanded set
|
|
670
|
-
expandedJobIds.delete(
|
|
861
|
+
expandedJobIds.delete(jobId);
|
|
671
862
|
return;
|
|
672
863
|
}
|
|
673
864
|
|
|
674
865
|
expandIcon.classList.add('expanded');
|
|
675
866
|
// Add to expanded set
|
|
676
|
-
expandedJobIds.add(
|
|
677
|
-
|
|
678
|
-
const detailRow = document.createElement('tr');
|
|
679
|
-
detailRow.className = 'detail-row';
|
|
680
|
-
|
|
681
|
-
const jobData = {
|
|
682
|
-
id: job.id,
|
|
683
|
-
name: job.name,
|
|
684
|
-
queue: currentQueue,
|
|
685
|
-
status: job.status || (job.failedReason ? 'failed' : 'completed'),
|
|
686
|
-
attempts: job.attempts,
|
|
687
|
-
timestamp: new Date(job.timestamp).toISOString(),
|
|
688
|
-
data: job.data || {},
|
|
689
|
-
failedReason: job.failedReason || null,
|
|
690
|
-
processedOn: job.processedOn ? new Date(job.processedOn).toISOString() : null,
|
|
691
|
-
finishedOn: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
692
|
-
returnvalue: job.returnvalue
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
detailRow.innerHTML =
|
|
696
|
-
'<td colspan="7" class="detail-cell">' +
|
|
697
|
-
'<div class="detail-content">' +
|
|
698
|
-
'<strong style="color: var(--accent); display: block; margin-bottom: 8px;">Job Details:</strong>' +
|
|
699
|
-
'<pre>' + JSON.stringify(jobData, null, 2) + '</pre>' +
|
|
700
|
-
'</div>' +
|
|
701
|
-
'</td>';
|
|
702
|
-
|
|
703
|
-
row.parentNode.insertBefore(detailRow, row.nextSibling);
|
|
867
|
+
expandedJobIds.add(jobId);
|
|
868
|
+
upsertJobDetailRow(row, job);
|
|
704
869
|
}
|
|
705
870
|
|
|
706
871
|
function toggleLockDetails(row, lock) {
|
|
@@ -776,6 +941,77 @@ const getDashboardScriptHelpers = () => `
|
|
|
776
941
|
return patternInput && patternInput.value ? patternInput.value : '*';
|
|
777
942
|
}
|
|
778
943
|
|
|
944
|
+
function clearSseTimers() {
|
|
945
|
+
if (reconnectTimer !== null) {
|
|
946
|
+
clearTimeout(reconnectTimer);
|
|
947
|
+
reconnectTimer = null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (streamWatchdogTimer !== null) {
|
|
951
|
+
clearTimeout(streamWatchdogTimer);
|
|
952
|
+
streamWatchdogTimer = null;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (dashboardResetTimer !== null) {
|
|
956
|
+
clearTimeout(dashboardResetTimer);
|
|
957
|
+
dashboardResetTimer = null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function scheduleDashboardReset(message) {
|
|
962
|
+
if (dashboardResetTimer !== null) return;
|
|
963
|
+
|
|
964
|
+
showError(message || 'Live updates stalled. Resetting dashboard...');
|
|
965
|
+
dashboardResetTimer = window.setTimeout(() => {
|
|
966
|
+
window.location.reload();
|
|
967
|
+
}, STREAM_RESET_MS);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function armStreamWatchdog() {
|
|
971
|
+
if (!autoRefreshEnabled) return;
|
|
972
|
+
|
|
973
|
+
if (streamWatchdogTimer !== null) {
|
|
974
|
+
clearTimeout(streamWatchdogTimer);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
streamWatchdogTimer = window.setTimeout(() => {
|
|
978
|
+
if (eventSource) {
|
|
979
|
+
eventSource.close();
|
|
980
|
+
eventSource = null;
|
|
981
|
+
}
|
|
982
|
+
sseActive = false;
|
|
983
|
+
streamWatchdogTimer = null;
|
|
984
|
+
scheduleDashboardReset('Live updates stalled. Resetting dashboard...');
|
|
985
|
+
}, STREAM_RESET_MS);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function markStreamHealthy() {
|
|
989
|
+
reconnectAttempts = 0;
|
|
990
|
+
if (reconnectTimer !== null) {
|
|
991
|
+
clearTimeout(reconnectTimer);
|
|
992
|
+
reconnectTimer = null;
|
|
993
|
+
}
|
|
994
|
+
clearError();
|
|
995
|
+
armStreamWatchdog();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function scheduleReconnect() {
|
|
999
|
+
if (!autoRefreshEnabled || reconnectTimer !== null) return;
|
|
1000
|
+
|
|
1001
|
+
reconnectAttempts += 1;
|
|
1002
|
+
const delay = Math.min(1000 * reconnectAttempts, 5000);
|
|
1003
|
+
showError('Live updates disconnected. Reconnecting...');
|
|
1004
|
+
|
|
1005
|
+
reconnectTimer = window.setTimeout(() => {
|
|
1006
|
+
reconnectTimer = null;
|
|
1007
|
+
setupEventStream(currentQueue);
|
|
1008
|
+
}, delay);
|
|
1009
|
+
|
|
1010
|
+
if (reconnectAttempts >= 4) {
|
|
1011
|
+
scheduleDashboardReset('Live updates could not reconnect. Resetting dashboard...');
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
779
1015
|
function buildEventsUrl(queue, pattern) {
|
|
780
1016
|
const q = queue || '';
|
|
781
1017
|
const p = pattern || '*';
|
|
@@ -785,11 +1021,12 @@ const getDashboardScriptHelpers = () => `
|
|
|
785
1021
|
const getDashboardScriptEventStream = () => `
|
|
786
1022
|
function setupEventStream(queueOverride) {
|
|
787
1023
|
if (!window.EventSource) return;
|
|
1024
|
+
if (!autoRefreshEnabled) return;
|
|
788
1025
|
|
|
789
|
-
const queue = queueOverride
|
|
1026
|
+
const queue = queueOverride === undefined ? currentQueue : queueOverride;
|
|
790
1027
|
const pattern = getLockPattern();
|
|
791
1028
|
|
|
792
|
-
if (eventSource && queue === lastSseQueue && pattern === lastSsePattern) return;
|
|
1029
|
+
if (eventSource && sseActive && queue === lastSseQueue && pattern === lastSsePattern) return;
|
|
793
1030
|
|
|
794
1031
|
if (eventSource) {
|
|
795
1032
|
eventSource.close();
|
|
@@ -799,10 +1036,12 @@ const getDashboardScriptEventStream = () => `
|
|
|
799
1036
|
lastSseQueue = queue;
|
|
800
1037
|
lastSsePattern = pattern;
|
|
801
1038
|
eventSource = new EventSource(buildEventsUrl(queue, pattern));
|
|
1039
|
+
armStreamWatchdog();
|
|
802
1040
|
|
|
803
1041
|
eventSource.onopen = () => {
|
|
804
1042
|
sseActive = true;
|
|
805
1043
|
stopAutoRefresh();
|
|
1044
|
+
markStreamHealthy();
|
|
806
1045
|
};
|
|
807
1046
|
|
|
808
1047
|
eventSource.onmessage = (evt) => {
|
|
@@ -813,20 +1052,31 @@ const getDashboardScriptEventStream = () => `
|
|
|
813
1052
|
if (payload.type === 'snapshot') {
|
|
814
1053
|
if (payload.snapshot) {
|
|
815
1054
|
renderStats(payload.snapshot);
|
|
816
|
-
updateQueueSelect(payload.snapshot.queues || []);
|
|
1055
|
+
const nextQueue = updateQueueSelect(payload.snapshot.queues || []);
|
|
1056
|
+
if (nextQueue !== currentQueue) {
|
|
1057
|
+
currentQueue = nextQueue;
|
|
1058
|
+
if (currentQueue) {
|
|
1059
|
+
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
1060
|
+
} else {
|
|
1061
|
+
localStorage.removeItem(QUEUE_KEY);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
817
1064
|
}
|
|
818
1065
|
|
|
819
|
-
if (
|
|
820
|
-
|
|
821
|
-
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
822
|
-
const select = document.getElementById('queue-select');
|
|
823
|
-
if (select) select.value = currentQueue;
|
|
1066
|
+
if (!currentQueue) {
|
|
1067
|
+
renderJobs([]);
|
|
824
1068
|
}
|
|
825
1069
|
|
|
826
|
-
if (payload.jobs
|
|
1070
|
+
if (payload.jobs && (!payload.queue || payload.queue === currentQueue)) {
|
|
1071
|
+
renderJobs(payload.jobs);
|
|
1072
|
+
}
|
|
827
1073
|
if (payload.locks) renderLocks(payload.locks);
|
|
828
1074
|
|
|
829
|
-
document.getElementById('last-updated')
|
|
1075
|
+
const lastUpdated = document.getElementById('last-updated');
|
|
1076
|
+
if (lastUpdated) {
|
|
1077
|
+
lastUpdated.textContent = new Date().toLocaleTimeString();
|
|
1078
|
+
}
|
|
1079
|
+
markStreamHealthy();
|
|
830
1080
|
}
|
|
831
1081
|
} catch (err) {
|
|
832
1082
|
console.error('Failed to parse SSE payload', err);
|
|
@@ -839,7 +1089,7 @@ const getDashboardScriptEventStream = () => `
|
|
|
839
1089
|
eventSource = null;
|
|
840
1090
|
}
|
|
841
1091
|
sseActive = false;
|
|
842
|
-
|
|
1092
|
+
scheduleReconnect();
|
|
843
1093
|
};
|
|
844
1094
|
}
|
|
845
1095
|
`;
|
|
@@ -910,6 +1160,9 @@ const getDashboardScriptFetch = () => `
|
|
|
910
1160
|
const getDashboardScriptRender = () => [
|
|
911
1161
|
getRenderStatsFunction(),
|
|
912
1162
|
getUpdateQueueSelectFunction(),
|
|
1163
|
+
getRenderJobsStateFunction(),
|
|
1164
|
+
getRenderJobsDetailHelpersFunction(),
|
|
1165
|
+
getRenderJobsRowHelpersFunction(),
|
|
913
1166
|
getRenderJobsFunction(),
|
|
914
1167
|
getRenderLocksFunction(),
|
|
915
1168
|
getLockHelperFunctions(),
|
|
@@ -932,8 +1185,13 @@ const getDashboardScriptBootstrap = () => `
|
|
|
932
1185
|
if (queueSelect) {
|
|
933
1186
|
queueSelect.addEventListener('change', (e) => {
|
|
934
1187
|
currentQueue = e.target.value;
|
|
935
|
-
|
|
1188
|
+
if (currentQueue) {
|
|
1189
|
+
localStorage.setItem(QUEUE_KEY, currentQueue);
|
|
1190
|
+
} else {
|
|
1191
|
+
localStorage.removeItem(QUEUE_KEY);
|
|
1192
|
+
}
|
|
936
1193
|
console.log('Queue changed - SSE will update automatically');
|
|
1194
|
+
clearError();
|
|
937
1195
|
|
|
938
1196
|
setupEventStream(currentQueue);
|
|
939
1197
|
});
|
|
@@ -961,6 +1219,7 @@ const getDashboardScriptBootstrap = () => `
|
|
|
961
1219
|
setupEventStream(currentQueue);
|
|
962
1220
|
|
|
963
1221
|
window.addEventListener('beforeunload', () => {
|
|
1222
|
+
clearSseTimers();
|
|
964
1223
|
if (eventSource) {
|
|
965
1224
|
eventSource.close();
|
|
966
1225
|
}
|