@zintrust/queue-monitor 0.7.0 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/QueueMonitoringService.js +1 -0
- package/dist/build-manifest.json +21 -65
- package/dist/dashboard-ui.js +4 -1
- package/dist/driver.d.ts +10 -1
- package/dist/driver.js +22 -1
- package/dist/index.js +24 -6
- package/dist/metrics.d.ts +2 -1
- package/dist/metrics.js +1 -0
- 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
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.7.8",
|
|
4
|
+
"buildDate": "2026-04-20T12:28:52.322Z",
|
|
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": "52d4df96",
|
|
12
|
+
"branch": "master"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -37,36 +37,8 @@
|
|
|
37
37
|
"sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
|
|
38
38
|
},
|
|
39
39
|
"QueueMonitoringService.js": {
|
|
40
|
-
"size":
|
|
41
|
-
"sha256": "
|
|
42
|
-
},
|
|
43
|
-
"api/workerClient.d.ts": {
|
|
44
|
-
"size": 597,
|
|
45
|
-
"sha256": "1d712bfa9127aa2df4f1fbd0efbb6d84069e1888ed73aca5542a41eee865d4bb"
|
|
46
|
-
},
|
|
47
|
-
"api/workerClient.js": {
|
|
48
|
-
"size": 1629,
|
|
49
|
-
"sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
|
|
50
|
-
},
|
|
51
|
-
"build-manifest.json": {
|
|
52
|
-
"size": 4134,
|
|
53
|
-
"sha256": "27d3df03ab7a4e59fcf59d62f077fd5b76a431f930881e3d6e31ea878875fc0e"
|
|
54
|
-
},
|
|
55
|
-
"config/queueMonitor.d.ts": {
|
|
56
|
-
"size": 407,
|
|
57
|
-
"sha256": "4541f47e64c8ede1bfd8fc0cb7edb76c4e885311b28b1f51c9be7639e5d87eca"
|
|
58
|
-
},
|
|
59
|
-
"config/queueMonitor.js": {
|
|
60
|
-
"size": 689,
|
|
61
|
-
"sha256": "0b95e6b65d4b6ffdd69788cdfd19e0e76400a39ad69dce018d8827a3b298e419"
|
|
62
|
-
},
|
|
63
|
-
"config/workerConfig.d.ts": {
|
|
64
|
-
"size": 86,
|
|
65
|
-
"sha256": "b669205d50c8844455a2d9b34a54f48a71118eb6ac99bad5372683ab666f5a22"
|
|
66
|
-
},
|
|
67
|
-
"config/workerConfig.js": {
|
|
68
|
-
"size": 628,
|
|
69
|
-
"sha256": "ca1c6dbaa751893f0e6b7c8a7fd41a80f7d5e8fc9aaaa4877ca12821bb25f56f"
|
|
40
|
+
"size": 8928,
|
|
41
|
+
"sha256": "fc408db720d5883821b169eabce66944295068cd6f2338f1884493c299f0a0f1"
|
|
70
42
|
},
|
|
71
43
|
"connection.d.ts": {
|
|
72
44
|
"size": 107,
|
|
@@ -81,40 +53,32 @@
|
|
|
81
53
|
"sha256": "5140191207b5620500ac5e1edacefed65956a1ad88739695d545ea7d2b405243"
|
|
82
54
|
},
|
|
83
55
|
"dashboard-ui.js": {
|
|
84
|
-
"size":
|
|
85
|
-
"sha256": "
|
|
56
|
+
"size": 54576,
|
|
57
|
+
"sha256": "b3ee7f13f8fa2fee8204316ac5fe4c9ee0d424131565a58de17061296c7c8e1b"
|
|
86
58
|
},
|
|
87
59
|
"driver.d.ts": {
|
|
88
|
-
"size":
|
|
89
|
-
"sha256": "
|
|
60
|
+
"size": 1102,
|
|
61
|
+
"sha256": "70b74e7a0489da3a5545dfd8458ab5b4def1f3733c291a782c033fed0afd0bcf"
|
|
90
62
|
},
|
|
91
63
|
"driver.js": {
|
|
92
|
-
"size":
|
|
93
|
-
"sha256": "
|
|
64
|
+
"size": 5165,
|
|
65
|
+
"sha256": "06a4aa4294ed0694146d184e8b0f1677ebe22a3d90bffa30b7c268657035c760"
|
|
94
66
|
},
|
|
95
67
|
"index.d.ts": {
|
|
96
68
|
"size": 2161,
|
|
97
69
|
"sha256": "2556c136641769f4e7cd81abd6ecae8663433aba74b09bc507b8fba01e3fc6f5"
|
|
98
70
|
},
|
|
99
71
|
"index.js": {
|
|
100
|
-
"size":
|
|
101
|
-
"sha256": "
|
|
72
|
+
"size": 13905,
|
|
73
|
+
"sha256": "c7ab600099b0b05c81ac97e98668f6649c60c283a82867b8851ab83250d65c2b"
|
|
102
74
|
},
|
|
103
75
|
"metrics.d.ts": {
|
|
104
|
-
"size":
|
|
105
|
-
"sha256": "
|
|
76
|
+
"size": 910,
|
|
77
|
+
"sha256": "6467795f6de818a1f295c42755005e394f25b145163c7678d6ac9ac1f9477fea"
|
|
106
78
|
},
|
|
107
79
|
"metrics.js": {
|
|
108
|
-
"size":
|
|
109
|
-
"sha256": "
|
|
110
|
-
},
|
|
111
|
-
"routes/workers.d.ts": {
|
|
112
|
-
"size": 477,
|
|
113
|
-
"sha256": "cfcc3527c47d1a796a3ced2d4777b339dd767f7feb3ecc66c5ce0a91d8ff1bc8"
|
|
114
|
-
},
|
|
115
|
-
"routes/workers.js": {
|
|
116
|
-
"size": 901,
|
|
117
|
-
"sha256": "1ec48c0becc8c0628b1780fb400e957c5fe50da79f73112436c33ecde7d7d574"
|
|
80
|
+
"size": 3472,
|
|
81
|
+
"sha256": "fab0e6d62ed694dcf57904a68b683280e4063c2fdcf97c60fdf5d933ae2b8f4f"
|
|
118
82
|
},
|
|
119
83
|
"worker.d.ts": {
|
|
120
84
|
"size": 332,
|
|
@@ -123,14 +87,6 @@
|
|
|
123
87
|
"worker.js": {
|
|
124
88
|
"size": 1233,
|
|
125
89
|
"sha256": "da26c1b80da7473e1644a4e0f8d814097ce0adff14e36037164f9ae5855e656a"
|
|
126
|
-
},
|
|
127
|
-
"workers-ui.d.ts": {
|
|
128
|
-
"size": 214,
|
|
129
|
-
"sha256": "4f5fd3bbd077d0dee2cc6c73d2c736fc52119b0d3a5f4a1878a4f3d3d3edf299"
|
|
130
|
-
},
|
|
131
|
-
"workers-ui.js": {
|
|
132
|
-
"size": 24928,
|
|
133
|
-
"sha256": "ba2c5cbf890fe875cc5336c76bd29b146089bc8ba3a70de4a2c383f395f9be73"
|
|
134
90
|
}
|
|
135
91
|
}
|
|
136
92
|
}
|
package/dist/dashboard-ui.js
CHANGED
|
@@ -983,9 +983,12 @@ const getRetryJobFunction = () => `
|
|
|
983
983
|
const res = await fetch(API_BASE + '/api/retry/' + queueName + '/' + jobId, {
|
|
984
984
|
method: 'POST'
|
|
985
985
|
});
|
|
986
|
+
const payload = await res.json().catch(() => null);
|
|
986
987
|
|
|
987
988
|
if (res.ok) {
|
|
988
|
-
btn.textContent =
|
|
989
|
+
btn.textContent = payload && payload.status === 'requeued_from_snapshot'
|
|
990
|
+
? '✓ Requeued'
|
|
991
|
+
: '✓ Retried';
|
|
989
992
|
setTimeout(() => {
|
|
990
993
|
console.log('HTTP jobs polling disabled - using SSE only');
|
|
991
994
|
// fetchJobs(currentQueue);
|
package/dist/driver.d.ts
CHANGED
|
@@ -2,9 +2,18 @@ import type { Job, JobsOptions } from 'bullmq';
|
|
|
2
2
|
import { type RedisConfig } from './connection';
|
|
3
3
|
export type JobPayload<T = unknown> = T;
|
|
4
4
|
export type JobCounts = Record<string, number>;
|
|
5
|
+
export type RetrySnapshot = {
|
|
6
|
+
name?: string;
|
|
7
|
+
data: unknown;
|
|
8
|
+
opts?: JobsOptions;
|
|
9
|
+
};
|
|
5
10
|
export type RetryJobResult = {
|
|
6
11
|
ok: true;
|
|
7
12
|
status: 'retried';
|
|
13
|
+
} | {
|
|
14
|
+
ok: true;
|
|
15
|
+
status: 'requeued_from_snapshot';
|
|
16
|
+
newJobId?: string;
|
|
8
17
|
} | {
|
|
9
18
|
ok: false;
|
|
10
19
|
status: 'missing';
|
|
@@ -18,7 +27,7 @@ export type QueueDriver = {
|
|
|
18
27
|
getJob(queueName: string, jobId: string): Promise<Job | undefined>;
|
|
19
28
|
getJobCounts(queueName: string): Promise<JobCounts>;
|
|
20
29
|
getRecentJobs(queueName: string, limit?: number): Promise<Job[]>;
|
|
21
|
-
retryJob(queueName: string, jobId: string): Promise<RetryJobResult>;
|
|
30
|
+
retryJob(queueName: string, jobId: string, snapshot?: RetrySnapshot): Promise<RetryJobResult>;
|
|
22
31
|
getQueues(): Promise<string[]>;
|
|
23
32
|
close(): Promise<void>;
|
|
24
33
|
};
|
package/dist/driver.js
CHANGED
|
@@ -41,6 +41,7 @@ async function discoverQueuesFromRedis(redis, inMemoryQueues) {
|
|
|
41
41
|
}
|
|
42
42
|
return Array.from(found.values());
|
|
43
43
|
}
|
|
44
|
+
// eslint-disable-next-line max-lines-per-function
|
|
44
45
|
export const createBullMQDriver = (config) => {
|
|
45
46
|
const queues = new Map();
|
|
46
47
|
const redis = createRedisConnection(config, 3, { subsystem: 'queue-monitor' });
|
|
@@ -77,6 +78,23 @@ export const createBullMQDriver = (config) => {
|
|
|
77
78
|
const queue = getQueue(queueName);
|
|
78
79
|
return queue.getJobCounts();
|
|
79
80
|
};
|
|
81
|
+
const requeueFromSnapshot = async (queue, snapshot) => {
|
|
82
|
+
try {
|
|
83
|
+
const requeued = await queue.add(snapshot.name ?? 'default', snapshot.data, snapshot.opts);
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
status: 'requeued_from_snapshot',
|
|
87
|
+
newJobId: requeued.id === undefined || requeued.id === null ? undefined : String(requeued.id),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: 'not_retryable',
|
|
94
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
80
98
|
const getRecentJobs = async (queueName, limit = 100) => {
|
|
81
99
|
const queue = getQueue(queueName);
|
|
82
100
|
const jobs = await queue.getJobs(['completed', 'failed', 'active', 'waiting', 'delayed', 'paused'], 0, Math.max(0, limit - 1), true);
|
|
@@ -84,9 +102,12 @@ export const createBullMQDriver = (config) => {
|
|
|
84
102
|
await enrichJobsWithState(jobs);
|
|
85
103
|
return jobs;
|
|
86
104
|
};
|
|
87
|
-
const retryJob = async (queueName, jobId) => {
|
|
105
|
+
const retryJob = async (queueName, jobId, snapshot) => {
|
|
106
|
+
const queue = getQueue(queueName);
|
|
88
107
|
const job = await getJob(queueName, jobId);
|
|
89
108
|
if (!job) {
|
|
109
|
+
if (snapshot)
|
|
110
|
+
return requeueFromSnapshot(queue, snapshot);
|
|
90
111
|
return { ok: false, status: 'missing' };
|
|
91
112
|
}
|
|
92
113
|
try {
|
package/dist/index.js
CHANGED
|
@@ -168,16 +168,34 @@ async function handleJobsEndpoint(req, res, metrics, driver) {
|
|
|
168
168
|
const jobs = await getRecentJobsForSelection(queueName, metrics, driver);
|
|
169
169
|
res.json(jobs);
|
|
170
170
|
}
|
|
171
|
-
async function handleRetryEndpoint(req, res, driver) {
|
|
171
|
+
async function handleRetryEndpoint(req, res, driver, metrics) {
|
|
172
172
|
const queueName = extractQueueParam(req);
|
|
173
173
|
const jobId = typeof req.getParam === 'function' ? req.getParam?.('jobId') : req.params?.['jobId'];
|
|
174
174
|
if (!queueName || !jobId) {
|
|
175
175
|
res.status(400).json(fieldError('queue_name,job_id', 'Queue name and job ID must be provided'));
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
|
-
const
|
|
178
|
+
const recentJobs = await getRecentJobsForSelection(queueName, metrics, driver);
|
|
179
|
+
const snapshotCandidate = recentJobs.find((job) => String(job.id) === jobId);
|
|
180
|
+
const retrySnapshot = snapshotCandidate === undefined
|
|
181
|
+
? undefined
|
|
182
|
+
: {
|
|
183
|
+
name: snapshotCandidate.name,
|
|
184
|
+
data: snapshotCandidate.data,
|
|
185
|
+
opts: snapshotCandidate.opts,
|
|
186
|
+
};
|
|
187
|
+
const result = await driver.retryJob(queueName, jobId, retrySnapshot);
|
|
179
188
|
if (result.ok) {
|
|
180
|
-
res.json({
|
|
189
|
+
res.json({
|
|
190
|
+
ok: true,
|
|
191
|
+
status: result.status,
|
|
192
|
+
message: result.status === 'requeued_from_snapshot'
|
|
193
|
+
? `Job ${jobId} re-queued from monitor snapshot`
|
|
194
|
+
: `Job ${jobId} queued for retry`,
|
|
195
|
+
...(result.status === 'requeued_from_snapshot' && result.newJobId
|
|
196
|
+
? { newJobId: result.newJobId }
|
|
197
|
+
: {}),
|
|
198
|
+
});
|
|
181
199
|
return;
|
|
182
200
|
}
|
|
183
201
|
if (result.status === 'missing') {
|
|
@@ -246,7 +264,7 @@ function registerApiRoutes(router, settings, routeOptions, metrics, driver, getS
|
|
|
246
264
|
registerSnapshotApi(router, settings, routeOptions, getSnapshot);
|
|
247
265
|
registerJobsApi(router, settings, routeOptions, metrics, driver);
|
|
248
266
|
registerLocksApi(router, settings, routeOptions, getLocks);
|
|
249
|
-
registerRetryApi(router, settings, routeOptions, driver);
|
|
267
|
+
registerRetryApi(router, settings, routeOptions, driver, metrics);
|
|
250
268
|
registerEventsApi(router, settings, routeOptions, getSnapshot, getLocks, metrics, driver);
|
|
251
269
|
}
|
|
252
270
|
function registerSnapshotApi(router, settings, routeOptions, getSnapshot) {
|
|
@@ -270,9 +288,9 @@ function registerLocksApi(router, settings, routeOptions, getLocks) {
|
|
|
270
288
|
res.json(locks);
|
|
271
289
|
}, routeOptions);
|
|
272
290
|
}
|
|
273
|
-
function registerRetryApi(router, settings, routeOptions, driver) {
|
|
291
|
+
function registerRetryApi(router, settings, routeOptions, driver, metrics) {
|
|
274
292
|
Router.post(router, `${settings.basePath}/api/retry/:queue/:jobId`, async (req, res) => {
|
|
275
|
-
await handleRetryEndpoint(req, res, driver);
|
|
293
|
+
await handleRetryEndpoint(req, res, driver, metrics);
|
|
276
294
|
}, routeOptions);
|
|
277
295
|
}
|
|
278
296
|
function registerEventsApi(router, settings, routeOptions, getSnapshot, getLocks, metrics, driver) {
|
package/dist/metrics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Job } from 'bullmq';
|
|
1
|
+
import { type Job, type JobsOptions } from 'bullmq';
|
|
2
2
|
import { type RedisConfig } from './connection';
|
|
3
3
|
export type JobStatus = 'completed' | 'failed';
|
|
4
4
|
export type JobSummary = {
|
|
@@ -6,6 +6,7 @@ export type JobSummary = {
|
|
|
6
6
|
name: string;
|
|
7
7
|
queue?: string;
|
|
8
8
|
data: unknown;
|
|
9
|
+
opts?: JobsOptions;
|
|
9
10
|
attempts: number;
|
|
10
11
|
status?: string;
|
|
11
12
|
failedReason?: string;
|
package/dist/metrics.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/queue-monitor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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.7.
|
|
23
|
+
"@zintrust/core": "^0.7.8"
|
|
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
|
-
};
|