@zintrust/queue-monitor 0.4.60 → 0.4.64
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 +0 -8
- package/dist/build-manifest.json +12 -56
- package/dist/dashboard-ui.js +105 -43
- package/package.json +2 -2
- package/dist/api/workerClient.d.ts +0 -20
- package/dist/api/workerClient.js +0 -45
- package/dist/config/queueMonitor.d.ts +0 -18
- package/dist/config/queueMonitor.js +0 -21
- package/dist/config/workerConfig.d.ts +0 -3
- package/dist/config/workerConfig.js +0 -19
- package/dist/routes/workers.d.ts +0 -10
- package/dist/routes/workers.js +0 -20
- package/dist/workers-ui.d.ts +0 -7
- package/dist/workers-ui.js +0 -655
|
@@ -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,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.64",
|
|
4
|
+
"buildDate": "2026-04-05T16:26:08.670Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v20.20.2",
|
|
7
|
+
"platform": "linux",
|
|
8
|
+
"arch": "x64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "018dcfe5",
|
|
12
|
+
"branch": "master"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -29,36 +29,8 @@
|
|
|
29
29
|
"sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
|
|
30
30
|
},
|
|
31
31
|
"QueueMonitoringService.js": {
|
|
32
|
-
"size":
|
|
33
|
-
"sha256": "
|
|
34
|
-
},
|
|
35
|
-
"api/workerClient.d.ts": {
|
|
36
|
-
"size": 597,
|
|
37
|
-
"sha256": "1d712bfa9127aa2df4f1fbd0efbb6d84069e1888ed73aca5542a41eee865d4bb"
|
|
38
|
-
},
|
|
39
|
-
"api/workerClient.js": {
|
|
40
|
-
"size": 1629,
|
|
41
|
-
"sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
|
|
42
|
-
},
|
|
43
|
-
"build-manifest.json": {
|
|
44
|
-
"size": 3865,
|
|
45
|
-
"sha256": "c2769a76a31b33d2de31cb3281ada6e5e41c31c26e4477c05617d02bebaa64af"
|
|
46
|
-
},
|
|
47
|
-
"config/queueMonitor.d.ts": {
|
|
48
|
-
"size": 407,
|
|
49
|
-
"sha256": "4541f47e64c8ede1bfd8fc0cb7edb76c4e885311b28b1f51c9be7639e5d87eca"
|
|
50
|
-
},
|
|
51
|
-
"config/queueMonitor.js": {
|
|
52
|
-
"size": 689,
|
|
53
|
-
"sha256": "0b95e6b65d4b6ffdd69788cdfd19e0e76400a39ad69dce018d8827a3b298e419"
|
|
54
|
-
},
|
|
55
|
-
"config/workerConfig.d.ts": {
|
|
56
|
-
"size": 86,
|
|
57
|
-
"sha256": "b669205d50c8844455a2d9b34a54f48a71118eb6ac99bad5372683ab666f5a22"
|
|
58
|
-
},
|
|
59
|
-
"config/workerConfig.js": {
|
|
60
|
-
"size": 628,
|
|
61
|
-
"sha256": "ca1c6dbaa751893f0e6b7c8a7fd41a80f7d5e8fc9aaaa4877ca12821bb25f56f"
|
|
32
|
+
"size": 6679,
|
|
33
|
+
"sha256": "edd069c1229071078223cd23cbb7bdae9cbb2431ae603ab35f0cfcb7cd1f6ffe"
|
|
62
34
|
},
|
|
63
35
|
"connection.d.ts": {
|
|
64
36
|
"size": 107,
|
|
@@ -73,8 +45,8 @@
|
|
|
73
45
|
"sha256": "1b487b6e9cedbcb00cde7e920fb128724cb21aa154160e0bd02b02e2a0bd7334"
|
|
74
46
|
},
|
|
75
47
|
"dashboard-ui.js": {
|
|
76
|
-
"size":
|
|
77
|
-
"sha256": "
|
|
48
|
+
"size": 53771,
|
|
49
|
+
"sha256": "530bdfba8deed3b881ab2b7ea6190b9a521bf4bdcc5a08a386a71b4c031ccb56"
|
|
78
50
|
},
|
|
79
51
|
"driver.d.ts": {
|
|
80
52
|
"size": 707,
|
|
@@ -90,7 +62,7 @@
|
|
|
90
62
|
},
|
|
91
63
|
"index.js": {
|
|
92
64
|
"size": 12811,
|
|
93
|
-
"sha256": "
|
|
65
|
+
"sha256": "229d93ec84afcdd135b20c13fcbb4d22059efdb9d3d26e94f1ba7926a7d5b07a"
|
|
94
66
|
},
|
|
95
67
|
"metrics.d.ts": {
|
|
96
68
|
"size": 868,
|
|
@@ -100,14 +72,6 @@
|
|
|
100
72
|
"size": 3448,
|
|
101
73
|
"sha256": "022c97865d37933fd7ed92f47b86b0450056caffd629fce6add51c902529bfe5"
|
|
102
74
|
},
|
|
103
|
-
"routes/workers.d.ts": {
|
|
104
|
-
"size": 477,
|
|
105
|
-
"sha256": "cfcc3527c47d1a796a3ced2d4777b339dd767f7feb3ecc66c5ce0a91d8ff1bc8"
|
|
106
|
-
},
|
|
107
|
-
"routes/workers.js": {
|
|
108
|
-
"size": 901,
|
|
109
|
-
"sha256": "1ec48c0becc8c0628b1780fb400e957c5fe50da79f73112436c33ecde7d7d574"
|
|
110
|
-
},
|
|
111
75
|
"worker.d.ts": {
|
|
112
76
|
"size": 332,
|
|
113
77
|
"sha256": "97cddbc991c6e6724950090cd92f06671932ab848c16dd94a030d9fba3fbae26"
|
|
@@ -115,14 +79,6 @@
|
|
|
115
79
|
"worker.js": {
|
|
116
80
|
"size": 1233,
|
|
117
81
|
"sha256": "da26c1b80da7473e1644a4e0f8d814097ce0adff14e36037164f9ae5855e656a"
|
|
118
|
-
},
|
|
119
|
-
"workers-ui.d.ts": {
|
|
120
|
-
"size": 214,
|
|
121
|
-
"sha256": "4f5fd3bbd077d0dee2cc6c73d2c736fc52119b0d3a5f4a1878a4f3d3d3edf299"
|
|
122
|
-
},
|
|
123
|
-
"workers-ui.js": {
|
|
124
|
-
"size": 24928,
|
|
125
|
-
"sha256": "ba2c5cbf890fe875cc5336c76bd29b146089bc8ba3a70de4a2c383f395f9be73"
|
|
126
82
|
}
|
|
127
83
|
}
|
|
128
84
|
}
|
package/dist/dashboard-ui.js
CHANGED
|
@@ -420,42 +420,49 @@ const getRenderStatsFunction = () => `
|
|
|
420
420
|
grid.removeChild(grid.lastElementChild);
|
|
421
421
|
}
|
|
422
422
|
}`;
|
|
423
|
-
const
|
|
424
|
-
function
|
|
425
|
-
|
|
423
|
+
const getUpdateQueueSelectHelpersFunction = () => `
|
|
424
|
+
function formatQueueOptionCounts(counts) {
|
|
425
|
+
return counts.waiting + ' waiting, ' + counts.failed + ' failed, ' + counts.completed + ' completed';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getPreferredQueue(select) {
|
|
426
429
|
const storedQueue = localStorage.getItem(QUEUE_KEY);
|
|
427
|
-
|
|
430
|
+
return currentQueue || select.value || storedQueue || '';
|
|
431
|
+
}
|
|
428
432
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
return '';
|
|
433
|
+
function getEmptyQueueSelection(select) {
|
|
434
|
+
select.disabled = true;
|
|
435
|
+
if (select.options.length !== 1 || select.options[0].value !== '' || select.options[0].textContent !== 'No queues') {
|
|
436
|
+
select.innerHTML = '<option value="">No queues</option>';
|
|
435
437
|
}
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
436
440
|
|
|
437
|
-
|
|
441
|
+
function getDesiredQueueOptions(queues) {
|
|
438
442
|
const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
|
|
439
443
|
const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
|
|
444
|
+
const totalCompleted = queues.reduce((acc, queue) => acc + queue.counts.completed, 0);
|
|
440
445
|
|
|
441
|
-
|
|
442
|
-
? preferredQueue
|
|
443
|
-
: queueNames[0];
|
|
444
|
-
const desiredOptions = [
|
|
446
|
+
return [
|
|
445
447
|
{
|
|
446
448
|
value: ALL_QUEUES,
|
|
447
|
-
text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed)'
|
|
449
|
+
text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed, ' + totalCompleted + ' completed)'
|
|
448
450
|
},
|
|
449
451
|
...queues.map(q => ({
|
|
450
452
|
value: q.name,
|
|
451
|
-
text: q.name + ' (' + q.counts
|
|
453
|
+
text: q.name + ' (' + formatQueueOptionCounts(q.counts) + ')'
|
|
452
454
|
}))
|
|
453
455
|
];
|
|
456
|
+
}
|
|
454
457
|
|
|
458
|
+
function syncQueueSelectOptions(select, desiredOptions) {
|
|
455
459
|
const existingOptions = new Map();
|
|
456
460
|
Array.from(select.options).forEach(option => {
|
|
457
461
|
existingOptions.set(option.value, option);
|
|
458
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);
|
|
459
466
|
|
|
460
467
|
select.disabled = false;
|
|
461
468
|
|
|
@@ -466,33 +473,70 @@ const getUpdateQueueSelectFunction = () => `
|
|
|
466
473
|
option.value = item.value;
|
|
467
474
|
}
|
|
468
475
|
|
|
469
|
-
option.textContent
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const currentAtIndex = select.options[index];
|
|
473
|
-
if (currentAtIndex !== option) {
|
|
474
|
-
select.appendChild(option);
|
|
476
|
+
if (option.textContent !== item.text) {
|
|
477
|
+
option.textContent = item.text;
|
|
475
478
|
}
|
|
476
|
-
});
|
|
477
479
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
option
|
|
480
|
+
if (needsStructuralSync) {
|
|
481
|
+
const currentAtIndex = select.options[index];
|
|
482
|
+
if (currentAtIndex !== option) {
|
|
483
|
+
select.insertBefore(option, currentAtIndex || null);
|
|
484
|
+
}
|
|
481
485
|
}
|
|
482
486
|
});
|
|
483
487
|
|
|
484
|
-
|
|
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);
|
|
508
|
+
const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
|
|
509
|
+
? preferredQueue
|
|
510
|
+
: queueNames[0];
|
|
511
|
+
const desiredOptions = getDesiredQueueOptions(queues);
|
|
512
|
+
|
|
513
|
+
if (document.activeElement === select) {
|
|
514
|
+
return nextQueue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
syncQueueSelectOptions(select, desiredOptions);
|
|
518
|
+
|
|
519
|
+
if (select.value !== nextQueue) {
|
|
520
|
+
select.value = nextQueue;
|
|
521
|
+
}
|
|
485
522
|
return nextQueue;
|
|
486
523
|
}`;
|
|
487
524
|
const getRenderJobsStateFunction = () => `
|
|
488
525
|
// Track expanded job IDs to preserve state during SSE updates
|
|
489
526
|
let expandedJobIds = new Set();
|
|
490
527
|
`;
|
|
491
|
-
const
|
|
528
|
+
const getRenderJobsIdentityHelpersFunction = () => `
|
|
492
529
|
function getJobId(job) {
|
|
493
530
|
return String(job.id);
|
|
494
531
|
}
|
|
495
532
|
|
|
533
|
+
function getAdjacentJobDetailRow(row) {
|
|
534
|
+
const existingDetail = row.nextElementSibling;
|
|
535
|
+
return existingDetail && existingDetail.classList.contains('detail-row')
|
|
536
|
+
? existingDetail
|
|
537
|
+
: null;
|
|
538
|
+
}
|
|
539
|
+
|
|
496
540
|
function getJobStatusInfo(job) {
|
|
497
541
|
const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
|
|
498
542
|
const statusMap = {
|
|
@@ -514,6 +558,8 @@ const getRenderJobsDetailHelpersFunction = () => `
|
|
|
514
558
|
}
|
|
515
559
|
return '<span style="color: var(--muted); font-size: 11px;">—</span>';
|
|
516
560
|
}
|
|
561
|
+
`;
|
|
562
|
+
const getRenderJobsDetailRowHelpersFunction = () => `
|
|
517
563
|
|
|
518
564
|
function buildJobDetailMarkup(job) {
|
|
519
565
|
const jobData = {
|
|
@@ -539,17 +585,13 @@ const getRenderJobsDetailHelpersFunction = () => `
|
|
|
539
585
|
}
|
|
540
586
|
|
|
541
587
|
function removeJobDetailRow(row) {
|
|
542
|
-
const existingDetail = row
|
|
543
|
-
if (existingDetail
|
|
588
|
+
const existingDetail = getAdjacentJobDetailRow(row);
|
|
589
|
+
if (existingDetail) {
|
|
544
590
|
existingDetail.remove();
|
|
545
591
|
}
|
|
546
592
|
}
|
|
547
593
|
|
|
548
|
-
function
|
|
549
|
-
const existingDetail = row.nextElementSibling && row.nextElementSibling.classList.contains('detail-row')
|
|
550
|
-
? row.nextElementSibling
|
|
551
|
-
: null;
|
|
552
|
-
|
|
594
|
+
function buildOrUpdateJobDetailRow(job, existingDetail) {
|
|
553
595
|
let detailRow = existingDetail;
|
|
554
596
|
if (!detailRow) {
|
|
555
597
|
detailRow = document.createElement('tr');
|
|
@@ -558,13 +600,22 @@ const getRenderJobsDetailHelpersFunction = () => `
|
|
|
558
600
|
|
|
559
601
|
detailRow.dataset.jobId = getJobId(job);
|
|
560
602
|
detailRow.innerHTML = buildJobDetailMarkup(job);
|
|
603
|
+
return detailRow;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function upsertJobDetailRow(row, job, parent) {
|
|
607
|
+
const detailRow = buildOrUpdateJobDetailRow(job, getAdjacentJobDetailRow(row));
|
|
561
608
|
|
|
562
609
|
if (parent) {
|
|
563
610
|
parent.appendChild(detailRow);
|
|
564
611
|
} else {
|
|
565
612
|
row.parentNode.insertBefore(detailRow, row.nextSibling);
|
|
566
613
|
}
|
|
614
|
+
|
|
615
|
+
return detailRow;
|
|
567
616
|
}
|
|
617
|
+
`;
|
|
618
|
+
const getRenderJobsEmptyStateFunction = () => `
|
|
568
619
|
|
|
569
620
|
function ensureEmptyJobsState(tbody) {
|
|
570
621
|
if (
|
|
@@ -622,6 +673,15 @@ const getRenderJobsRowHelpersFunction = () => `
|
|
|
622
673
|
return rows;
|
|
623
674
|
}
|
|
624
675
|
|
|
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
|
+
}
|
|
684
|
+
|
|
625
685
|
function removeObsoleteJobRows(tbody, currentJobIds) {
|
|
626
686
|
Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
|
|
627
687
|
const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
|
|
@@ -644,9 +704,9 @@ const getRenderJobsFunction = () => `
|
|
|
644
704
|
}
|
|
645
705
|
|
|
646
706
|
const currentJobIds = new Set(jobList.map(job => getJobId(job)));
|
|
647
|
-
removeObsoleteJobRows(tbody, currentJobIds);
|
|
648
|
-
|
|
649
707
|
const existingRows = getExistingJobRows(tbody);
|
|
708
|
+
const existingDetailRows = getExistingJobDetailRows(tbody);
|
|
709
|
+
const fragment = document.createDocumentFragment();
|
|
650
710
|
|
|
651
711
|
jobList.forEach((job, idx) => {
|
|
652
712
|
const jobId = getJobId(job);
|
|
@@ -657,15 +717,14 @@ const getRenderJobsFunction = () => `
|
|
|
657
717
|
updateExistingJobRow(row, job, idx);
|
|
658
718
|
}
|
|
659
719
|
|
|
660
|
-
|
|
720
|
+
fragment.appendChild(row);
|
|
661
721
|
if (expandedJobIds.has(jobId)) {
|
|
662
|
-
|
|
663
|
-
} else {
|
|
664
|
-
removeJobDetailRow(row);
|
|
722
|
+
fragment.appendChild(buildOrUpdateJobDetailRow(job, existingDetailRows.get(jobId)));
|
|
665
723
|
}
|
|
666
724
|
});
|
|
667
725
|
|
|
668
726
|
expandedJobIds = new Set([...expandedJobIds].filter(id => currentJobIds.has(id)));
|
|
727
|
+
tbody.replaceChildren(fragment);
|
|
669
728
|
}`;
|
|
670
729
|
const getRenderLocksFunction = () => `
|
|
671
730
|
// Track expanded lock keys to preserve state during SSE updates
|
|
@@ -1159,9 +1218,12 @@ const getDashboardScriptFetch = () => `
|
|
|
1159
1218
|
`;
|
|
1160
1219
|
const getDashboardScriptRender = () => [
|
|
1161
1220
|
getRenderStatsFunction(),
|
|
1221
|
+
getUpdateQueueSelectHelpersFunction(),
|
|
1162
1222
|
getUpdateQueueSelectFunction(),
|
|
1163
1223
|
getRenderJobsStateFunction(),
|
|
1164
|
-
|
|
1224
|
+
getRenderJobsIdentityHelpersFunction(),
|
|
1225
|
+
getRenderJobsDetailRowHelpersFunction(),
|
|
1226
|
+
getRenderJobsEmptyStateFunction(),
|
|
1165
1227
|
getRenderJobsRowHelpersFunction(),
|
|
1166
1228
|
getRenderJobsFunction(),
|
|
1167
1229
|
getRenderLocksFunction(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.64",
|
|
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.64"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
type WorkerApiResponse<T> = {
|
|
2
|
-
ok: boolean;
|
|
3
|
-
error?: string;
|
|
4
|
-
} & T;
|
|
5
|
-
export declare const WorkerClient: Readonly<{
|
|
6
|
-
listWorkers(): Promise<string[]>;
|
|
7
|
-
getWorker(name: string): Promise<unknown>;
|
|
8
|
-
getStatus(name: string): Promise<unknown>;
|
|
9
|
-
getHealth(name: string): Promise<unknown>;
|
|
10
|
-
startWorker(name: string): Promise<WorkerApiResponse<{
|
|
11
|
-
message?: string;
|
|
12
|
-
}>>;
|
|
13
|
-
stopWorker(name: string): Promise<WorkerApiResponse<{
|
|
14
|
-
message?: string;
|
|
15
|
-
}>>;
|
|
16
|
-
restartWorker(name: string): Promise<WorkerApiResponse<{
|
|
17
|
-
message?: string;
|
|
18
|
-
}>>;
|
|
19
|
-
}>;
|
|
20
|
-
export {};
|
package/dist/api/workerClient.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { ErrorFactory, Logger } from '@zintrust/core';
|
|
2
|
-
import { WorkerConfig } from '../config/workerConfig.js';
|
|
3
|
-
const requestJson = async (path, options = {}) => {
|
|
4
|
-
const baseUrl = WorkerConfig.getWorkerBaseUrl();
|
|
5
|
-
const url = `${baseUrl}${path}`;
|
|
6
|
-
const response = await fetch(url, {
|
|
7
|
-
...options,
|
|
8
|
-
headers: {
|
|
9
|
-
'Content-Type': 'application/json',
|
|
10
|
-
...options.headers,
|
|
11
|
-
},
|
|
12
|
-
});
|
|
13
|
-
if (!response.ok) {
|
|
14
|
-
Logger.error('Worker API request failed', { url, status: response.status });
|
|
15
|
-
throw ErrorFactory.createWorkerError(`Worker API request failed (${response.status})`);
|
|
16
|
-
}
|
|
17
|
-
return (await response.json());
|
|
18
|
-
};
|
|
19
|
-
export const WorkerClient = Object.freeze({
|
|
20
|
-
async listWorkers() {
|
|
21
|
-
const response = await requestJson('/api/workers');
|
|
22
|
-
return response.workers ?? [];
|
|
23
|
-
},
|
|
24
|
-
async getWorker(name) {
|
|
25
|
-
const response = await requestJson(`/api/workers/${name}`);
|
|
26
|
-
return response.worker;
|
|
27
|
-
},
|
|
28
|
-
async getStatus(name) {
|
|
29
|
-
const response = await requestJson(`/api/workers/${name}/status`);
|
|
30
|
-
return response.status;
|
|
31
|
-
},
|
|
32
|
-
async getHealth(name) {
|
|
33
|
-
const response = await requestJson(`/api/workers/${name}/health`);
|
|
34
|
-
return response.health;
|
|
35
|
-
},
|
|
36
|
-
async startWorker(name) {
|
|
37
|
-
return requestJson(`/api/workers/${name}/start`, { method: 'POST' });
|
|
38
|
-
},
|
|
39
|
-
async stopWorker(name) {
|
|
40
|
-
return requestJson(`/api/workers/${name}/stop`, { method: 'POST' });
|
|
41
|
-
},
|
|
42
|
-
async restartWorker(name) {
|
|
43
|
-
return requestJson(`/api/workers/${name}/restart`, { method: 'POST' });
|
|
44
|
-
},
|
|
45
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Queue Monitor Configuration (default override)
|
|
3
|
-
*
|
|
4
|
-
* Keep this file declarative:
|
|
5
|
-
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override config by editing values below.
|
|
7
|
-
*/
|
|
8
|
-
declare const _default: {
|
|
9
|
-
enabled: boolean;
|
|
10
|
-
basePath: string;
|
|
11
|
-
middleware: string[];
|
|
12
|
-
redis: {
|
|
13
|
-
host: string;
|
|
14
|
-
port: number;
|
|
15
|
-
password: string;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
export default _default;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Env } from '@zintrust/core';
|
|
2
|
-
/**
|
|
3
|
-
* Queue Monitor Configuration (default override)
|
|
4
|
-
*
|
|
5
|
-
* Keep this file declarative:
|
|
6
|
-
* - Core owns env parsing/default logic.
|
|
7
|
-
* - Projects can override config by editing values below.
|
|
8
|
-
*/
|
|
9
|
-
export default {
|
|
10
|
-
enabled: Env.getBool('QUEUE_MONITOR_ENABLED', true),
|
|
11
|
-
basePath: Env.get('QUEUE_MONITOR_BASE_PATH', '/queue-monitor'),
|
|
12
|
-
middleware: Env.get('QUEUE_MONITOR_MIDDLEWARE', 'auth')
|
|
13
|
-
.split(',')
|
|
14
|
-
.map((m) => m.trim())
|
|
15
|
-
.filter((m) => m.length > 0),
|
|
16
|
-
redis: {
|
|
17
|
-
host: Env.get('REDIS_HOST', 'localhost'),
|
|
18
|
-
port: Env.getInt('REDIS_PORT', 6379),
|
|
19
|
-
password: Env.get('REDIS_PASSWORD', ''),
|
|
20
|
-
},
|
|
21
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Env } from '@zintrust/core';
|
|
2
|
-
const normalizeBaseUrl = (value) => {
|
|
3
|
-
let end = value.length;
|
|
4
|
-
while (end > 0 && value.charAt(end - 1) === '/') {
|
|
5
|
-
end--;
|
|
6
|
-
}
|
|
7
|
-
return value.slice(0, end);
|
|
8
|
-
};
|
|
9
|
-
const withHttpScheme = (value) => value.startsWith('http://') || value.startsWith('https://') ? value : `http://${value}`;
|
|
10
|
-
const resolveWorkerApiUrl = () => {
|
|
11
|
-
const workerApiUrl = Env.get('WORKER_API_URL');
|
|
12
|
-
if (workerApiUrl) {
|
|
13
|
-
return normalizeBaseUrl(withHttpScheme(workerApiUrl));
|
|
14
|
-
}
|
|
15
|
-
return '';
|
|
16
|
-
};
|
|
17
|
-
export const WorkerConfig = Object.freeze({
|
|
18
|
-
getWorkerBaseUrl: resolveWorkerApiUrl,
|
|
19
|
-
});
|
package/dist/routes/workers.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type IRouter } from '@zintrust/core';
|
|
2
|
-
import { type WorkerUiOptions } from '../workers-ui';
|
|
3
|
-
type RouteOptions = {
|
|
4
|
-
middleware?: ReadonlyArray<string>;
|
|
5
|
-
} | undefined;
|
|
6
|
-
export declare const registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
|
|
7
|
-
declare const _default: Readonly<{
|
|
8
|
-
registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
|
|
9
|
-
}>;
|
|
10
|
-
export default _default;
|
package/dist/routes/workers.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Router } from '@zintrust/core';
|
|
2
|
-
import { WorkerConfig } from '../config/workerConfig.js';
|
|
3
|
-
import { getWorkersHtml } from '../workers-ui.js';
|
|
4
|
-
const registerWorkerUiPage = (router, options, routeOptions) => {
|
|
5
|
-
const handler = (_req, res) => {
|
|
6
|
-
res.html(getWorkersHtml({
|
|
7
|
-
basePath: options.basePath,
|
|
8
|
-
apiBaseUrl: WorkerConfig.getWorkerBaseUrl(),
|
|
9
|
-
autoRefresh: options.autoRefresh,
|
|
10
|
-
refreshIntervalMs: options.refreshIntervalMs,
|
|
11
|
-
}));
|
|
12
|
-
};
|
|
13
|
-
Router.get(router, `${options.basePath}/workers`, handler, routeOptions);
|
|
14
|
-
Router.get(router, '/workers', handler, routeOptions);
|
|
15
|
-
Router.get(router, '/workers/', handler, routeOptions);
|
|
16
|
-
};
|
|
17
|
-
export const registerWorkerUiRoutes = (router, options, routeOptions) => {
|
|
18
|
-
registerWorkerUiPage(router, options, routeOptions);
|
|
19
|
-
};
|
|
20
|
-
export default Object.freeze({ registerWorkerUiRoutes });
|
package/dist/workers-ui.d.ts
DELETED
package/dist/workers-ui.js
DELETED
|
@@ -1,655 +0,0 @@
|
|
|
1
|
-
const getBaseStyles = () => `
|
|
2
|
-
body {
|
|
3
|
-
margin: 0;
|
|
4
|
-
font-family: 'Inter', ui-sans-serif, system-ui;
|
|
5
|
-
background: #0b1220;
|
|
6
|
-
color: #f1f5f9;
|
|
7
|
-
}
|
|
8
|
-
.hidden { display: none; }
|
|
9
|
-
`;
|
|
10
|
-
const getLayoutStyles = () => `
|
|
11
|
-
.zt-page { min-height: 100vh; padding: 32px 24px; }
|
|
12
|
-
.zt-container { max-width: 72rem; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
|
|
13
|
-
.zt-header { display: flex; flex-direction: column; gap: 16px; }
|
|
14
|
-
.zt-brand { display: flex; align-items: center; gap: 16px; }
|
|
15
|
-
.zt-brand-icon { height: 48px; width: 48px; border-radius: 16px; border: 1px solid #1e293b; background: rgba(15, 23, 42, 0.8); display: flex; align-items: center; justify-content: center; }
|
|
16
|
-
.zt-kicker { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; color: #94a3b8; }
|
|
17
|
-
.zt-title { font-size: 1.5rem; font-weight: 600; color: #f8fafc; margin: 0; }
|
|
18
|
-
.zt-subtitle { font-size: 0.875rem; color: #94a3b8; margin: 0.25rem 0 0; }
|
|
19
|
-
.zt-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
|
|
20
|
-
.zt-nav { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
21
|
-
.zt-nav-link {
|
|
22
|
-
border: 1px solid #1e293b;
|
|
23
|
-
color: #e2e8f0;
|
|
24
|
-
text-decoration: none;
|
|
25
|
-
padding: 0.4rem 0.75rem;
|
|
26
|
-
border-radius: 0.6rem;
|
|
27
|
-
font-size: 0.75rem;
|
|
28
|
-
font-weight: 600;
|
|
29
|
-
transition: border-color 0.2s ease, color 0.2s ease;
|
|
30
|
-
}
|
|
31
|
-
.zt-nav-link:hover { border-color: #38bdf8; color: #38bdf8; }
|
|
32
|
-
.zt-grid { display: grid; gap: 16px; }
|
|
33
|
-
.zt-grid-3 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
34
|
-
`;
|
|
35
|
-
const getCardStyles = () => `
|
|
36
|
-
.zt-card { background: rgba(15, 23, 42, 0.75); border: 1px solid rgba(148, 163, 184, 0.2); border-radius: 1rem; padding: 1.25rem; }
|
|
37
|
-
.zt-card-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; color: #94a3b8; }
|
|
38
|
-
.zt-card-value { margin-top: 0.75rem; font-size: 1.875rem; font-weight: 600; }
|
|
39
|
-
.zt-text-emerald { color: #6ee7b7; }
|
|
40
|
-
.zt-text-amber { color: #fbbf24; }
|
|
41
|
-
`;
|
|
42
|
-
const getAlertStyles = () => `
|
|
43
|
-
.zt-alert {
|
|
44
|
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
45
|
-
background: rgba(239, 68, 68, 0.1);
|
|
46
|
-
color: #fecaca;
|
|
47
|
-
padding: 0.75rem 1rem;
|
|
48
|
-
border-radius: 1rem;
|
|
49
|
-
font-size: 0.875rem;
|
|
50
|
-
}
|
|
51
|
-
`;
|
|
52
|
-
const getButtonStyles = () => `
|
|
53
|
-
.zt-button {
|
|
54
|
-
border: 1px solid #1e293b;
|
|
55
|
-
background: #0f172a;
|
|
56
|
-
color: #f1f5f9;
|
|
57
|
-
padding: 0.5rem 1rem;
|
|
58
|
-
border-radius: 0.75rem;
|
|
59
|
-
font-size: 0.875rem;
|
|
60
|
-
font-weight: 600;
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
transition: border-color 0.2s ease, color 0.2s ease;
|
|
63
|
-
}
|
|
64
|
-
.zt-button:hover { border-color: #38bdf8; color: #38bdf8; }
|
|
65
|
-
.zt-select {
|
|
66
|
-
border: 1px solid #1e293b;
|
|
67
|
-
background: #0f172a;
|
|
68
|
-
color: #f1f5f9;
|
|
69
|
-
padding: 0.5rem 0.75rem;
|
|
70
|
-
border-radius: 0.75rem;
|
|
71
|
-
font-size: 0.75rem;
|
|
72
|
-
font-weight: 600;
|
|
73
|
-
}
|
|
74
|
-
`;
|
|
75
|
-
const getTableStyles = () => `
|
|
76
|
-
.zt-table-card { border-radius: 1.5rem; padding: 1.5rem; }
|
|
77
|
-
.zt-table-header { display: flex; flex-direction: column; gap: 12px; }
|
|
78
|
-
.zt-table-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.75rem; color: #94a3b8; }
|
|
79
|
-
.zt-table-wrap { margin-top: 1.5rem; overflow-x: auto; }
|
|
80
|
-
.zt-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
|
81
|
-
.zt-head-cell { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: #94a3b8; padding: 0.5rem 0.75rem; text-align: left; }
|
|
82
|
-
.zt-cell { padding: 0.75rem; border-top: 1px solid #1f2937; }
|
|
83
|
-
.zt-cell--strong { font-weight: 600; color: #f1f5f9; }
|
|
84
|
-
.zt-cell--muted { color: #94a3b8; }
|
|
85
|
-
.zt-cell--right { text-align: right; }
|
|
86
|
-
.zt-empty { padding: 1.5rem 0.75rem; text-align: center; font-size: 0.875rem; color: #94a3b8; }
|
|
87
|
-
`;
|
|
88
|
-
const getBadgeStyles = () => `
|
|
89
|
-
.zt-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 999px; }
|
|
90
|
-
.zt-dot--emerald { background: #34d399; }
|
|
91
|
-
.zt-dot--amber { background: #fbbf24; }
|
|
92
|
-
.zt-dot--rose { background: #fb7185; }
|
|
93
|
-
.zt-badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 600; border: 1px solid transparent; }
|
|
94
|
-
.zt-badge--success { background: rgba(16, 185, 129, 0.12); color: #6ee7b7; border-color: rgba(16, 185, 129, 0.3); }
|
|
95
|
-
.zt-badge--warn { background: rgba(245, 158, 11, 0.12); color: #fbbf24; border-color: rgba(245, 158, 11, 0.3); }
|
|
96
|
-
.zt-badge--danger { background: rgba(244, 63, 94, 0.12); color: #fda4af; border-color: rgba(244, 63, 94, 0.3); }
|
|
97
|
-
.zt-badge--neutral { background: rgba(148, 163, 184, 0.12); color: #cbd5f5; border-color: rgba(148, 163, 184, 0.3); }
|
|
98
|
-
.status-pill { border-radius: 999px; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 600; }
|
|
99
|
-
`;
|
|
100
|
-
const getActionStyles = () => `
|
|
101
|
-
.zt-row-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }
|
|
102
|
-
.action-btn {
|
|
103
|
-
border: 1px solid #1e293b;
|
|
104
|
-
background: transparent;
|
|
105
|
-
color: #e2e8f0;
|
|
106
|
-
padding: 0.25rem 0.75rem;
|
|
107
|
-
border-radius: 0.5rem;
|
|
108
|
-
font-size: 0.75rem;
|
|
109
|
-
font-weight: 600;
|
|
110
|
-
cursor: pointer;
|
|
111
|
-
transition: border-color 0.2s ease, color 0.2s ease;
|
|
112
|
-
}
|
|
113
|
-
.action-btn.is-disabled {
|
|
114
|
-
opacity: 0.45;
|
|
115
|
-
cursor: not-allowed;
|
|
116
|
-
border-color: #1f2937;
|
|
117
|
-
color: #64748b;
|
|
118
|
-
}
|
|
119
|
-
.action-btn.is-disabled:hover { border-color: #1f2937; color: #64748b; }
|
|
120
|
-
.action-btn.is-active {
|
|
121
|
-
border-color: rgba(16, 185, 129, 0.6);
|
|
122
|
-
color: #6ee7b7;
|
|
123
|
-
}
|
|
124
|
-
.action-btn.is-toggle {
|
|
125
|
-
border-color: rgba(56, 189, 248, 0.4);
|
|
126
|
-
color: #bae6fd;
|
|
127
|
-
}
|
|
128
|
-
.action-btn.is-delete {
|
|
129
|
-
border-color: rgba(244, 63, 94, 0.5);
|
|
130
|
-
color: #fda4af;
|
|
131
|
-
}
|
|
132
|
-
.action-btn.is-delete:hover { border-color: #fb7185; color: #fb7185; }
|
|
133
|
-
.zt-toggle {
|
|
134
|
-
display: inline-flex;
|
|
135
|
-
align-items: center;
|
|
136
|
-
gap: 0.4rem;
|
|
137
|
-
font-size: 0.7rem;
|
|
138
|
-
font-weight: 600;
|
|
139
|
-
color: #cbd5f5;
|
|
140
|
-
}
|
|
141
|
-
.zt-switch {
|
|
142
|
-
appearance: none;
|
|
143
|
-
width: 2.25rem;
|
|
144
|
-
height: 1.2rem;
|
|
145
|
-
border-radius: 999px;
|
|
146
|
-
border: 1px solid rgba(148, 163, 184, 0.4);
|
|
147
|
-
background: rgba(239, 68, 68, 0.2);
|
|
148
|
-
position: relative;
|
|
149
|
-
cursor: pointer;
|
|
150
|
-
transition: background 0.2s ease, border-color 0.2s ease;
|
|
151
|
-
}
|
|
152
|
-
.zt-switch::after {
|
|
153
|
-
content: '';
|
|
154
|
-
position: absolute;
|
|
155
|
-
top: 1px;
|
|
156
|
-
left: 1px;
|
|
157
|
-
width: 0.95rem;
|
|
158
|
-
height: 0.95rem;
|
|
159
|
-
border-radius: 999px;
|
|
160
|
-
background: #f8fafc;
|
|
161
|
-
transition: transform 0.2s ease;
|
|
162
|
-
}
|
|
163
|
-
.zt-switch:checked {
|
|
164
|
-
background: rgba(16, 185, 129, 0.25);
|
|
165
|
-
border-color: rgba(16, 185, 129, 0.5);
|
|
166
|
-
}
|
|
167
|
-
.zt-switch:checked::after {
|
|
168
|
-
transform: translateX(1rem);
|
|
169
|
-
}
|
|
170
|
-
.action-btn:hover { border-color: #38bdf8; color: #38bdf8; }
|
|
171
|
-
.action-btn[data-action="start"]:hover { border-color: #34d399; color: #34d399; }
|
|
172
|
-
.action-btn[data-action="restart"]:hover { border-color: #fbbf24; color: #fbbf24; }
|
|
173
|
-
.action-btn[data-action="stop"]:hover { border-color: #fb7185; color: #fb7185; }
|
|
174
|
-
`;
|
|
175
|
-
const getResponsiveStyles = () => `
|
|
176
|
-
@media (min-width: 640px) {
|
|
177
|
-
.zt-header { flex-direction: row; align-items: center; justify-content: space-between; }
|
|
178
|
-
.zt-table-header { flex-direction: row; align-items: center; justify-content: space-between; }
|
|
179
|
-
}
|
|
180
|
-
@media (min-width: 768px) {
|
|
181
|
-
.zt-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
182
|
-
}
|
|
183
|
-
`;
|
|
184
|
-
const getInlineStyles = () => `
|
|
185
|
-
<style>
|
|
186
|
-
${getBaseStyles()}
|
|
187
|
-
${getLayoutStyles()}
|
|
188
|
-
${getCardStyles()}
|
|
189
|
-
${getAlertStyles()}
|
|
190
|
-
${getButtonStyles()}
|
|
191
|
-
${getTableStyles()}
|
|
192
|
-
${getBadgeStyles()}
|
|
193
|
-
${getActionStyles()}
|
|
194
|
-
${getResponsiveStyles()}
|
|
195
|
-
</style>`;
|
|
196
|
-
const getLogo = () => `
|
|
197
|
-
<svg width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
198
|
-
<defs>
|
|
199
|
-
<linearGradient id="zt-workers" x1="10" y1="50" x2="90" y2="50" gradientUnits="userSpaceOnUse">
|
|
200
|
-
<stop stop-color="#22c55e" />
|
|
201
|
-
<stop offset="1" stop-color="#38bdf8" />
|
|
202
|
-
</linearGradient>
|
|
203
|
-
</defs>
|
|
204
|
-
<circle cx="50" cy="50" r="34" stroke="rgba(255,255,255,0.16)" stroke-width="4" />
|
|
205
|
-
<ellipse cx="50" cy="50" rx="40" ry="18" stroke="url(#zt-workers)" stroke-width="4" />
|
|
206
|
-
<ellipse cx="50" cy="50" rx="18" ry="40" stroke="url(#zt-workers)" stroke-width="4" opacity="0.75" />
|
|
207
|
-
<circle cx="50" cy="50" r="6" fill="url(#zt-workers)" />
|
|
208
|
-
<path d="M40 52C35 52 32 49 32 44C32 39 35 36 40 36H48" stroke="white" stroke-width="6" stroke-linecap="round" />
|
|
209
|
-
<path d="M60 48C65 48 68 51 68 56C68 61 65 64 60 64H52" stroke="white" stroke-width="6" stroke-linecap="round" />
|
|
210
|
-
<path d="M44 50H56" stroke="rgba(255,255,255,0.22)" stroke-width="6" stroke-linecap="round" />
|
|
211
|
-
</svg>`;
|
|
212
|
-
const normalizeBasePath = (value) => {
|
|
213
|
-
let basePath = value;
|
|
214
|
-
while (basePath.endsWith('/')) {
|
|
215
|
-
basePath = basePath.slice(0, -1);
|
|
216
|
-
}
|
|
217
|
-
return basePath;
|
|
218
|
-
};
|
|
219
|
-
const getWorkersHead = () => `
|
|
220
|
-
<head>
|
|
221
|
-
<meta charset="UTF-8" />
|
|
222
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
223
|
-
<title>ZinTrust Workers</title>
|
|
224
|
-
${getInlineStyles()}
|
|
225
|
-
</head>`;
|
|
226
|
-
const getWorkersHeader = () => `
|
|
227
|
-
<header class="zt-header">
|
|
228
|
-
<div class="zt-brand">
|
|
229
|
-
<div class="zt-brand-icon">
|
|
230
|
-
${getLogo()}
|
|
231
|
-
</div>
|
|
232
|
-
<div>
|
|
233
|
-
<p class="zt-kicker">ZinTrust</p>
|
|
234
|
-
<h1 class="zt-title">Worker Command Center</h1>
|
|
235
|
-
<p class="zt-subtitle">Live control plane for worker orchestration</p>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
<div class="zt-actions">
|
|
239
|
-
<span id="last-updated" class="zt-kicker"></span>
|
|
240
|
-
<select id="storage-select" class="zt-select" aria-label="Worker storage">
|
|
241
|
-
<option value="memory">Memory</option>
|
|
242
|
-
<option value="redis">Redis</option>
|
|
243
|
-
<option value="db">Database</option>
|
|
244
|
-
</select>
|
|
245
|
-
<nav class="zt-nav">
|
|
246
|
-
<a class="zt-nav-link" href="/queue-monitor/">Queue monitor</a>
|
|
247
|
-
<a class="zt-nav-link" href="/workers">Workers</a>
|
|
248
|
-
<a class="zt-nav-link" href="/telemetry">Telemetry</a>
|
|
249
|
-
<a class="zt-nav-link" href="/metrics">Metrics</a>
|
|
250
|
-
</nav>
|
|
251
|
-
<button id="refresh-btn" class="zt-button">Refresh</button>
|
|
252
|
-
<button id="auto-refresh-btn" class="zt-button">Pause auto refresh</button>
|
|
253
|
-
</div>
|
|
254
|
-
</header>`;
|
|
255
|
-
const getWorkersStats = () => `
|
|
256
|
-
<section class="zt-grid zt-grid-3">
|
|
257
|
-
<div class="zt-card">
|
|
258
|
-
<p class="zt-card-title">Total Workers</p>
|
|
259
|
-
<p id="total-workers" class="zt-card-value">0</p>
|
|
260
|
-
</div>
|
|
261
|
-
<div class="zt-card">
|
|
262
|
-
<p class="zt-card-title">Active</p>
|
|
263
|
-
<p id="active-workers" class="zt-card-value zt-text-emerald">0</p>
|
|
264
|
-
</div>
|
|
265
|
-
<div class="zt-card">
|
|
266
|
-
<p class="zt-card-title">Attention Needed</p>
|
|
267
|
-
<p id="attention-workers" class="zt-card-value zt-text-amber">0</p>
|
|
268
|
-
</div>
|
|
269
|
-
</section>`;
|
|
270
|
-
const getWorkersTable = () => `
|
|
271
|
-
<section class="zt-card zt-table-card">
|
|
272
|
-
<div class="zt-table-header">
|
|
273
|
-
<div>
|
|
274
|
-
<h2 class="zt-title" style="font-size: 1.125rem;">Workers</h2>
|
|
275
|
-
<p class="zt-subtitle">Start, stop, and monitor health across the fleet.</p>
|
|
276
|
-
</div>
|
|
277
|
-
<div class="zt-table-meta">
|
|
278
|
-
<span class="zt-dot zt-dot--emerald"></span> Healthy
|
|
279
|
-
<span class="zt-dot zt-dot--amber" style="margin-left: 0.75rem;"></span> Degraded
|
|
280
|
-
<span class="zt-dot zt-dot--rose" style="margin-left: 0.75rem;"></span> Critical
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
<div class="zt-table-wrap">
|
|
284
|
-
<table class="zt-table">
|
|
285
|
-
<thead>
|
|
286
|
-
<tr>
|
|
287
|
-
<th class="zt-head-cell">Worker</th>
|
|
288
|
-
<th class="zt-head-cell">Status</th>
|
|
289
|
-
<th class="zt-head-cell">Health</th>
|
|
290
|
-
<th class="zt-head-cell">Version</th>
|
|
291
|
-
<th class="zt-head-cell zt-cell--right">Actions</th>
|
|
292
|
-
</tr>
|
|
293
|
-
</thead>
|
|
294
|
-
<tbody id="workers-body"></tbody>
|
|
295
|
-
</table>
|
|
296
|
-
</div>
|
|
297
|
-
</section>`;
|
|
298
|
-
const getWorkersBody = () => `
|
|
299
|
-
<div class="zt-page">
|
|
300
|
-
<div class="zt-container">
|
|
301
|
-
${getWorkersHeader()}
|
|
302
|
-
|
|
303
|
-
<section id="error" class="hidden zt-alert"></section>
|
|
304
|
-
|
|
305
|
-
${getWorkersStats()}
|
|
306
|
-
|
|
307
|
-
${getWorkersTable()}
|
|
308
|
-
</div>
|
|
309
|
-
</div>`;
|
|
310
|
-
const getWorkersScriptState = (options, apiBaseUrl) => `
|
|
311
|
-
const RAW_API_BASE = '${apiBaseUrl}';
|
|
312
|
-
const API_BASE = RAW_API_BASE
|
|
313
|
-
? (RAW_API_BASE.startsWith('/') ? window.location.origin + RAW_API_BASE : RAW_API_BASE)
|
|
314
|
-
: window.location.origin;
|
|
315
|
-
const STORAGE_KEYS = {
|
|
316
|
-
autoRefresh: 'zintrust.workers.autoRefresh',
|
|
317
|
-
storageMode: 'zintrust.workers.storageMode',
|
|
318
|
-
};
|
|
319
|
-
const AUTO_REFRESH = ${options.autoRefresh ? 'true' : 'false'};
|
|
320
|
-
const REFRESH_INTERVAL = ${Math.max(1000, Math.floor(options.refreshIntervalMs))};
|
|
321
|
-
|
|
322
|
-
const errorEl = document.getElementById('error');
|
|
323
|
-
const totalEl = document.getElementById('total-workers');
|
|
324
|
-
const activeEl = document.getElementById('active-workers');
|
|
325
|
-
const attentionEl = document.getElementById('attention-workers');
|
|
326
|
-
const bodyEl = document.getElementById('workers-body');
|
|
327
|
-
const refreshBtn = document.getElementById('refresh-btn');
|
|
328
|
-
const autoBtn = document.getElementById('auto-refresh-btn');
|
|
329
|
-
const lastUpdated = document.getElementById('last-updated');
|
|
330
|
-
const storageSelect = document.getElementById('storage-select');
|
|
331
|
-
|
|
332
|
-
let autoRefresh = AUTO_REFRESH;
|
|
333
|
-
let storageMode = 'memory';
|
|
334
|
-
let autoTimer = null;
|
|
335
|
-
`;
|
|
336
|
-
const getWorkersScriptStorage = () => `
|
|
337
|
-
const readStorage = (key) => {
|
|
338
|
-
try {
|
|
339
|
-
return localStorage.getItem(key);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const writeStorage = (key, value) => {
|
|
346
|
-
try {
|
|
347
|
-
localStorage.setItem(key, value);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const setAutoLabel = () => {
|
|
354
|
-
autoBtn.textContent = autoRefresh ? 'Pause auto refresh' : 'Resume auto refresh';
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const setStorageValue = (value) => {
|
|
358
|
-
storageMode = value || 'memory';
|
|
359
|
-
if (storageSelect) {
|
|
360
|
-
storageSelect.value = storageMode;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
`;
|
|
364
|
-
const getWorkersScriptError = () => `
|
|
365
|
-
const setError = (message) => {
|
|
366
|
-
if (!message) {
|
|
367
|
-
errorEl.classList.add('hidden');
|
|
368
|
-
errorEl.textContent = '';
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
errorEl.classList.remove('hidden');
|
|
372
|
-
errorEl.textContent = message;
|
|
373
|
-
};
|
|
374
|
-
`;
|
|
375
|
-
const getWorkersScriptBadges = () => `
|
|
376
|
-
const statusBadge = (label, tone) => {
|
|
377
|
-
const tones = {
|
|
378
|
-
success: 'zt-badge--success',
|
|
379
|
-
warn: 'zt-badge--warn',
|
|
380
|
-
danger: 'zt-badge--danger',
|
|
381
|
-
neutral: 'zt-badge--neutral',
|
|
382
|
-
};
|
|
383
|
-
return '<span class="zt-badge ' + (tones[tone] || tones.neutral) + '">' + label + '</span>';
|
|
384
|
-
};
|
|
385
|
-
`;
|
|
386
|
-
const getWorkersScriptTone = () => `
|
|
387
|
-
const toStatusTone = (status) => {
|
|
388
|
-
if (!status) return { label: 'unknown', tone: 'neutral' };
|
|
389
|
-
const normalized = String(status).toLowerCase();
|
|
390
|
-
if (['running', 'active'].includes(normalized)) return { label: normalized, tone: 'success' };
|
|
391
|
-
if (['stopped', 'stopping', 'sleeping', 'paused'].includes(normalized)) return { label: normalized, tone: 'neutral' };
|
|
392
|
-
if (['starting', 'draining'].includes(normalized)) return { label: normalized, tone: 'warn' };
|
|
393
|
-
return { label: normalized, tone: 'danger' };
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const toHealthTone = (health) => {
|
|
397
|
-
if (!health) return { label: 'unknown', tone: 'neutral' };
|
|
398
|
-
const normalized = String(health).toLowerCase();
|
|
399
|
-
if (['healthy', 'green'].includes(normalized)) return { label: normalized, tone: 'success' };
|
|
400
|
-
if (['degraded', 'yellow'].includes(normalized)) return { label: normalized, tone: 'warn' };
|
|
401
|
-
if (['critical', 'unhealthy', 'red'].includes(normalized)) return { label: normalized, tone: 'danger' };
|
|
402
|
-
return { label: normalized, tone: 'neutral' };
|
|
403
|
-
};
|
|
404
|
-
`;
|
|
405
|
-
const getWorkersScriptRenderRowTemplate = () => `
|
|
406
|
-
const renderWorkerRow = (worker) => {
|
|
407
|
-
const resolvedName =
|
|
408
|
-
worker.name || worker.workerName || worker.worker?.name || worker.status?.name || '';
|
|
409
|
-
const statusValue =
|
|
410
|
-
worker.status?.status ||
|
|
411
|
-
worker.status?.state ||
|
|
412
|
-
worker.status ||
|
|
413
|
-
worker.worker?.status ||
|
|
414
|
-
'unknown';
|
|
415
|
-
const statusInfo = toStatusTone(statusValue);
|
|
416
|
-
const healthInfo = toHealthTone(worker.health?.status || worker.health);
|
|
417
|
-
const version =
|
|
418
|
-
worker.version ||
|
|
419
|
-
worker.status?.version ||
|
|
420
|
-
worker.worker?.config?.version ||
|
|
421
|
-
worker.worker?.version ||
|
|
422
|
-
'n/a';
|
|
423
|
-
const autoStart = Boolean(worker.autoStart ?? worker.worker?.config?.autoStart ?? false);
|
|
424
|
-
|
|
425
|
-
const normalizedStatus = String(statusValue || '').toLowerCase();
|
|
426
|
-
const isRunning = ['running', 'active'].includes(normalizedStatus);
|
|
427
|
-
const startClass = isRunning ? 'action-btn is-disabled is-active' : 'action-btn';
|
|
428
|
-
const startAttr = isRunning ? ' disabled' : '';
|
|
429
|
-
|
|
430
|
-
if (!resolvedName) {
|
|
431
|
-
return '';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const rowHtml =
|
|
435
|
-
'<tr>' +
|
|
436
|
-
'<td class="zt-cell zt-cell--strong">' +
|
|
437
|
-
resolvedName +
|
|
438
|
-
'</td>' +
|
|
439
|
-
'<td class="zt-cell">' +
|
|
440
|
-
statusBadge(statusInfo.label, statusInfo.tone) +
|
|
441
|
-
'</td>' +
|
|
442
|
-
'<td class="zt-cell">' +
|
|
443
|
-
statusBadge(healthInfo.label, healthInfo.tone) +
|
|
444
|
-
'</td>' +
|
|
445
|
-
'<td class="zt-cell zt-cell--muted">' +
|
|
446
|
-
version +
|
|
447
|
-
'</td>' +
|
|
448
|
-
'<td class="zt-cell zt-cell--right">' +
|
|
449
|
-
'<div class="zt-row-actions">' +
|
|
450
|
-
'<label class="zt-toggle" title="Auto start">' +
|
|
451
|
-
'<input class="zt-switch" type="checkbox" data-action="auto-start" data-worker="' +
|
|
452
|
-
resolvedName +
|
|
453
|
-
'" data-auto-start="' +
|
|
454
|
-
(autoStart ? 'true' : 'false') +
|
|
455
|
-
'"' +
|
|
456
|
-
(autoStart ? ' checked' : '') +
|
|
457
|
-
' />' +
|
|
458
|
-
'<span>Auto</span>' +
|
|
459
|
-
'</label>' +
|
|
460
|
-
'<button class="' +
|
|
461
|
-
startClass +
|
|
462
|
-
'" data-action="start" data-worker="' +
|
|
463
|
-
resolvedName +
|
|
464
|
-
'"' +
|
|
465
|
-
startAttr +
|
|
466
|
-
'>Start</button>' +
|
|
467
|
-
'<button class="action-btn" data-action="restart" data-worker="' +
|
|
468
|
-
resolvedName +
|
|
469
|
-
'">Restart</button>' +
|
|
470
|
-
'<button class="action-btn" data-action="stop" data-worker="' +
|
|
471
|
-
resolvedName +
|
|
472
|
-
'">Stop</button>' +
|
|
473
|
-
'<button class="action-btn is-delete" data-action="delete" data-worker="' +
|
|
474
|
-
resolvedName +
|
|
475
|
-
'">Delete</button>' +
|
|
476
|
-
'</div>' +
|
|
477
|
-
'</td>' +
|
|
478
|
-
'</tr>';
|
|
479
|
-
|
|
480
|
-
return rowHtml;
|
|
481
|
-
};
|
|
482
|
-
`;
|
|
483
|
-
const getWorkersScriptRenderRows = () => `
|
|
484
|
-
const renderWorkers = (workers) => {
|
|
485
|
-
bodyEl.innerHTML = '';
|
|
486
|
-
if (!workers.length) {
|
|
487
|
-
bodyEl.innerHTML = '<tr><td colspan="5" class="zt-empty">No workers found.</td></tr>';
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
workers.forEach((worker) => {
|
|
492
|
-
const rowHtml = renderWorkerRow(worker);
|
|
493
|
-
if (!rowHtml) {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
bodyEl.insertAdjacentHTML('beforeend', rowHtml);
|
|
497
|
-
});
|
|
498
|
-
};
|
|
499
|
-
`;
|
|
500
|
-
const getWorkersScriptRenderSummary = () => `
|
|
501
|
-
const updateSummary = (workers) => {
|
|
502
|
-
totalEl.textContent = workers.length;
|
|
503
|
-
const activeCount = workers.filter((worker) => {
|
|
504
|
-
const status = String(worker.status?.status || worker.status || '').toLowerCase();
|
|
505
|
-
return status === 'running' || status === 'active';
|
|
506
|
-
}).length;
|
|
507
|
-
const attentionCount = workers.filter((worker) => {
|
|
508
|
-
const health = String(worker.health?.status || worker.health || '').toLowerCase();
|
|
509
|
-
return ['degraded', 'critical', 'unhealthy', 'red', 'yellow'].includes(health);
|
|
510
|
-
}).length;
|
|
511
|
-
|
|
512
|
-
activeEl.textContent = activeCount;
|
|
513
|
-
attentionEl.textContent = attentionCount;
|
|
514
|
-
};
|
|
515
|
-
`;
|
|
516
|
-
const getWorkersScriptFetch = () => `
|
|
517
|
-
const fetchWorkers = async () => {
|
|
518
|
-
setError('');
|
|
519
|
-
try {
|
|
520
|
-
const query = new URLSearchParams({
|
|
521
|
-
detail: 'true',
|
|
522
|
-
storage: storageMode,
|
|
523
|
-
});
|
|
524
|
-
const response = await fetch(API_BASE + '/api/workers?' + query.toString());
|
|
525
|
-
if (!response.ok) throw new Error('Failed to load workers');
|
|
526
|
-
const payload = await response.json();
|
|
527
|
-
const workers = payload.workers || [];
|
|
528
|
-
renderWorkers(workers);
|
|
529
|
-
updateSummary(workers);
|
|
530
|
-
lastUpdated.textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
531
|
-
} catch (error) {
|
|
532
|
-
setError(error.message || 'Failed to load worker data');
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
const handleAction = async (action, workerName, extraParams = {}, methodOverride) => {
|
|
537
|
-
setError('');
|
|
538
|
-
try {
|
|
539
|
-
if (!workerName) {
|
|
540
|
-
throw new Error('Missing worker name');
|
|
541
|
-
}
|
|
542
|
-
const query = new URLSearchParams({ storage: storageMode, ...extraParams });
|
|
543
|
-
const path = action ? '/' + action : '';
|
|
544
|
-
const response = await fetch(
|
|
545
|
-
API_BASE + '/api/workers/' + workerName + path + '?' + query.toString(),
|
|
546
|
-
{
|
|
547
|
-
method: methodOverride || 'POST',
|
|
548
|
-
}
|
|
549
|
-
);
|
|
550
|
-
if (!response.ok) throw new Error('Action failed');
|
|
551
|
-
await fetchWorkers();
|
|
552
|
-
} catch (error) {
|
|
553
|
-
setError(error.message || 'Failed to execute action');
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
`;
|
|
557
|
-
const getWorkersScriptControls = () => `
|
|
558
|
-
bodyEl.addEventListener('click', (event) => {
|
|
559
|
-
const target = event.target;
|
|
560
|
-
if (!target || !target.dataset) return;
|
|
561
|
-
if (target.dataset.action && target.dataset.worker) {
|
|
562
|
-
if (target.dataset.action === 'auto-start') {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (target.dataset.action === 'delete') {
|
|
566
|
-
const first = window.confirm('Delete this worker?');
|
|
567
|
-
if (!first) return;
|
|
568
|
-
const second = window.confirm('This cannot be undone. Delete worker permanently?');
|
|
569
|
-
if (!second) return;
|
|
570
|
-
handleAction('', target.dataset.worker, {}, 'DELETE');
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
handleAction(target.dataset.action, target.dataset.worker);
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
bodyEl.addEventListener('change', (event) => {
|
|
578
|
-
const target = event.target;
|
|
579
|
-
if (!target || !target.dataset) return;
|
|
580
|
-
if (target.dataset.action === 'auto-start' && target.dataset.worker) {
|
|
581
|
-
const nextValue = target.checked === true;
|
|
582
|
-
const confirmMessage = nextValue
|
|
583
|
-
? 'Enable auto start for this worker?'
|
|
584
|
-
: 'Disable auto start for this worker?';
|
|
585
|
-
if (!window.confirm(confirmMessage)) {
|
|
586
|
-
target.checked = !nextValue;
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
handleAction('auto-start', target.dataset.worker, { enabled: String(nextValue) });
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
refreshBtn.addEventListener('click', () => fetchWorkers());
|
|
594
|
-
if (storageSelect) {
|
|
595
|
-
storageSelect.addEventListener('change', (event) => {
|
|
596
|
-
setStorageValue(event.target.value);
|
|
597
|
-
writeStorage(STORAGE_KEYS.storageMode, storageMode);
|
|
598
|
-
fetchWorkers();
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
autoBtn.addEventListener('click', () => {
|
|
602
|
-
autoRefresh = !autoRefresh;
|
|
603
|
-
setAutoLabel();
|
|
604
|
-
writeStorage(STORAGE_KEYS.autoRefresh, String(autoRefresh));
|
|
605
|
-
if (autoRefresh) {
|
|
606
|
-
autoTimer = setInterval(fetchWorkers, REFRESH_INTERVAL);
|
|
607
|
-
} else if (autoTimer) {
|
|
608
|
-
clearInterval(autoTimer);
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
`;
|
|
612
|
-
const getWorkersScriptBootstrap = () => `
|
|
613
|
-
const storedAuto = readStorage(STORAGE_KEYS.autoRefresh);
|
|
614
|
-
if (storedAuto !== null) {
|
|
615
|
-
autoRefresh = storedAuto === 'true';
|
|
616
|
-
}
|
|
617
|
-
const storedStorage = readStorage(STORAGE_KEYS.storageMode);
|
|
618
|
-
if (storedStorage) {
|
|
619
|
-
setStorageValue(storedStorage);
|
|
620
|
-
} else {
|
|
621
|
-
setStorageValue('memory');
|
|
622
|
-
}
|
|
623
|
-
setAutoLabel();
|
|
624
|
-
|
|
625
|
-
if (autoRefresh) {
|
|
626
|
-
autoTimer = setInterval(fetchWorkers, REFRESH_INTERVAL);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
fetchWorkers();
|
|
630
|
-
`;
|
|
631
|
-
const getWorkersScript = (options, apiBaseUrl) => `
|
|
632
|
-
<script>
|
|
633
|
-
${getWorkersScriptState(options, apiBaseUrl)}
|
|
634
|
-
${getWorkersScriptStorage()}
|
|
635
|
-
${getWorkersScriptError()}
|
|
636
|
-
${getWorkersScriptBadges()}
|
|
637
|
-
${getWorkersScriptTone()}
|
|
638
|
-
${getWorkersScriptRenderRowTemplate()}
|
|
639
|
-
${getWorkersScriptRenderRows()}
|
|
640
|
-
${getWorkersScriptRenderSummary()}
|
|
641
|
-
${getWorkersScriptFetch()}
|
|
642
|
-
${getWorkersScriptControls()}
|
|
643
|
-
${getWorkersScriptBootstrap()}
|
|
644
|
-
</script>`;
|
|
645
|
-
export const getWorkersHtml = (options) => {
|
|
646
|
-
const apiBaseUrl = normalizeBasePath(options.apiBaseUrl ?? '');
|
|
647
|
-
return `<!DOCTYPE html>
|
|
648
|
-
<html lang="en">
|
|
649
|
-
${getWorkersHead()}
|
|
650
|
-
<body>
|
|
651
|
-
${getWorkersBody()}
|
|
652
|
-
${getWorkersScript(options, apiBaseUrl)}
|
|
653
|
-
</body>
|
|
654
|
-
</html>`;
|
|
655
|
-
};
|