@zintrust/workers 0.4.63 → 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.
@@ -1,1952 +0,0 @@
1
- /* eslint-disable no-restricted-syntax */
2
- /* eslint-disable @typescript-eslint/explicit-function-return-type */
3
- /* global document, alert, confirm */
4
- /* eslint-disable no-console */
5
- // Configuration
6
- const API_BASE = '';
7
-
8
- const THEME_KEY = 'zintrust-workers-dashboard-theme';
9
- const AUTO_REFRESH_KEY = 'zintrust-workers-dashboard-auto-refresh';
10
- const PAGE_SIZE_KEY = 'zintrust-workers-dashboard-page-size';
11
- const _BULK_AUTO_START_KEY = 'zintrust-workers-dashboard-bulk-auto-start';
12
-
13
- let currentPage = 1;
14
- let totalPages = 1;
15
- let totalWorkers = 0;
16
- let autoRefreshEnabled = true;
17
- let refreshTimer = null;
18
- let currentTheme = null;
19
- let eventSource = null;
20
- let sseActive = false;
21
- let lastSseRefresh = 0;
22
- const _bulkAutoStartEnabled = false;
23
- const _lastWorkers = [];
24
- const detailsCache = new Map();
25
- const MAX_CACHE_SIZE = 50;
26
-
27
- // Theme management
28
- function getPreferredTheme() {
29
- const stored = localStorage.getItem(THEME_KEY);
30
- if (stored === 'light' || stored === 'dark') {
31
- return stored;
32
- }
33
- const prefersLight =
34
- globalThis.window.matchMedia &&
35
- globalThis.window.matchMedia('(prefers-color-scheme: light)').matches;
36
- return prefersLight ? 'light' : 'dark';
37
- }
38
-
39
- function applyTheme(nextTheme) {
40
- currentTheme = nextTheme;
41
- document.documentElement.dataset.theme = nextTheme;
42
- localStorage.setItem(THEME_KEY, nextTheme);
43
- }
44
-
45
- function toggleTheme() {
46
- applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
47
- }
48
-
49
- // Helper function to get DOM elements
50
- function getDomElements() {
51
- return {
52
- loading: document.getElementById('loading'),
53
- error: document.getElementById('error'),
54
- content: document.getElementById('workers-content'),
55
- searchBtn: document.getElementById('search-btn'),
56
- };
57
- }
58
-
59
- // Helper function to show loading state
60
- function showLoadingState(elements) {
61
- if (elements.content.style.display === 'none') {
62
- elements.loading.style.display = 'block';
63
- } else {
64
- elements.content.style.opacity = '0.5';
65
- }
66
- elements.error.style.display = 'none';
67
- if (elements.searchBtn) elements.searchBtn.disabled = true;
68
- }
69
-
70
- // Helper function to hide loading state
71
- function hideLoadingState(elements) {
72
- elements.loading.style.display = 'none';
73
- elements.content.style.display = 'block';
74
- elements.content.style.opacity = '1';
75
- if (elements.searchBtn) elements.searchBtn.disabled = false;
76
- }
77
-
78
- // Helper function to handle fetch error
79
- function handleFetchError(elements, error) {
80
- console.error('Error fetching workers:', error);
81
- hideLoadingState(elements);
82
- elements.error.style.display = 'block';
83
- }
84
-
85
- // Helper function to validate worker data
86
- function validateWorkerData(data) {
87
- if (!data || !data.workers || !Array.isArray(data.workers)) {
88
- console.error('Invalid worker data structure:', data);
89
- return false;
90
- }
91
- return true;
92
- }
93
-
94
- // Data fetching - only for search and pagination, SSE handles regular updates
95
- async function fetchData() {
96
- const elements = getDomElements();
97
-
98
- showLoadingState(elements);
99
-
100
- try {
101
- const query = elements.searchBtn
102
- ? elements.searchBtn.parentElement?.parentElement?.querySelector('input')?.value
103
- : '';
104
- const limit = localStorage.getItem(PAGE_SIZE_KEY) || '100';
105
-
106
- const params = new URLSearchParams({
107
- page: currentPage.toString(),
108
- limit: limit,
109
- status: document.getElementById('status-filter')?.value || '',
110
- driver: document.getElementById('driver-filter')?.value || '',
111
- sortBy: document.getElementById('sort-select')?.value || 'status',
112
- sortOrder: 'asc',
113
- search: query,
114
- });
115
-
116
- const response = await fetch(API_BASE + '/api/workers?' + params.toString());
117
- if (!response.ok) {
118
- console.error('Failed to fetch workers:', response.statusText);
119
- hideLoadingState(elements);
120
- elements.error.style.display = 'block';
121
- return;
122
- }
123
-
124
- const data = await response.json();
125
- console.log('Worker data received:', data);
126
-
127
- if (!validateWorkerData(data)) {
128
- elements.error.style.display = 'block';
129
- hideLoadingState(elements);
130
- return;
131
- }
132
-
133
- console.log('Rendering', data.workers.length, 'workers');
134
- renderWorkers(data);
135
- hideLoadingState(elements);
136
- } catch (err) {
137
- handleFetchError(elements, err);
138
- }
139
- }
140
-
141
- function changeLimit(_newLimit) {
142
- localStorage.setItem(PAGE_SIZE_KEY, _newLimit);
143
- currentPage = 1;
144
- fetchData(); // Enable for pagination
145
- }
146
-
147
- // Make functions globally available for HTML onclick/onchange handlers
148
- globalThis.changeLimit = changeLimit;
149
- globalThis.toggleAutoRefresh = toggleAutoRefresh;
150
- globalThis.fetchData = fetchData;
151
- globalThis.showAddWorkerModal = showAddWorkerModal;
152
- globalThis.loadPage = loadPage;
153
- globalThis.startWorker = startWorker;
154
- globalThis.stopWorker = stopWorker;
155
- globalThis.restartWorker = restartWorker;
156
- globalThis.deleteWorker = deleteWorker;
157
- globalThis.toggleAutoStart = toggleAutoStart;
158
- globalThis.toggleDetails = toggleDetails;
159
-
160
- // Helper functions to reduce complexity
161
- function validateDriver(driver) {
162
- return !driver || ['database', 'redis', 'memory'].includes(driver);
163
- }
164
-
165
- async function fetchWorkerData(workerName, driver) {
166
- const [detailsRes, historyRes, trendRes, slaRes] = await Promise.all([
167
- fetch(API_BASE + '/api/workers/' + workerName + '/details?driver=' + driver),
168
- fetch(API_BASE + '/api/workers/' + workerName + '/monitoring/history?limit=50'),
169
- fetch(API_BASE + '/api/workers/' + workerName + '/monitoring/trend'),
170
- fetch(API_BASE + '/api/workers/' + workerName + '/sla/status'),
171
- ]);
172
-
173
- return { detailsRes, historyRes, trendRes, slaRes };
174
- }
175
-
176
- async function processDetailsResponse(detailsRes, data) {
177
- if (!detailsRes.ok) return;
178
- const json = await detailsRes.json();
179
- Object.assign(data, json);
180
- }
181
-
182
- async function processSlaResponse(slaRes, data) {
183
- if (!slaRes.ok) return;
184
- const slaJson = await slaRes.json();
185
- if (slaJson.status) {
186
- if (!data.details) data.details = {};
187
- data.details.sla = slaJson.status;
188
- }
189
- }
190
-
191
- async function processHistoryResponse(historyRes, data) {
192
- if (!historyRes.ok) return;
193
- const historyJson = await historyRes.json();
194
- if (!historyJson.history || !Array.isArray(historyJson.history)) return;
195
-
196
- const formattedLogs = historyJson.history.map((h) => {
197
- const time = new Date(h.timestamp).toLocaleTimeString();
198
- const msg = h.message ? ` - ${h.message}` : '';
199
- return `[${time}] ${h.status.toUpperCase()} (${h.latency}ms)${msg}`;
200
- });
201
-
202
- if (!data.details) data.details = {};
203
- data.details.recentLogs = formattedLogs;
204
- }
205
-
206
- async function processTrendResponse(trendRes, data) {
207
- if (!trendRes.ok) return;
208
- const trendJson = await trendRes.json();
209
- if (!trendJson.trend) return;
210
-
211
- if (!data.details) data.details = {};
212
- if (!data.details.metrics) data.details.metrics = {};
213
- data.details.metrics.uptimeTrend = (trendJson.trend.uptime * 100).toFixed(1) + '%';
214
- if (trendJson.trend.samples) data.details.metrics.samples = trendJson.trend.samples;
215
- }
216
-
217
- function manageCacheSize() {
218
- if (detailsCache.size >= MAX_CACHE_SIZE) {
219
- const firstKey = detailsCache.keys().next().value;
220
- detailsCache.delete(firstKey);
221
- }
222
- }
223
-
224
- async function ensureWorkerDetails(workerName, detailRow, driver) {
225
- if (!workerName || !detailRow) return;
226
-
227
- if (!detailsCache.has(workerName)) {
228
- try {
229
- if (!validateDriver(driver)) {
230
- console.error('Invalid driver specified');
231
- return;
232
- }
233
-
234
- const responses = await fetchWorkerData(workerName, driver);
235
- const data = {};
236
-
237
- await processDetailsResponse(responses.detailsRes, data);
238
- await processSlaResponse(responses.slaRes, data);
239
- await processHistoryResponse(responses.historyRes, data);
240
- await processTrendResponse(responses.trendRes, data);
241
-
242
- manageCacheSize();
243
- detailsCache.set(workerName, data);
244
- } catch (err) {
245
- console.error('Failed to load worker details:', err);
246
- }
247
- }
248
-
249
- const cached = detailsCache.get(workerName);
250
- const detailsData = cached?.details ?? cached;
251
- updateDetailViews(detailRow, detailsData);
252
- }
253
-
254
- // Helper to safe access nested properties (shared across functions)
255
- const get = (obj, path) => path.split('.').reduce((o, i) => (o ? o[i] : null), obj);
256
-
257
- // Helper function to resolve metric value from different data sources
258
- function resolveMetricValue(details, key, originalValue) {
259
- if (!key.startsWith('metrics.')) return originalValue;
260
-
261
- const metricKey = key.replace('metrics.', '');
262
- // Try to get from details.metrics first, then from details directly, then from worker
263
- return (
264
- get(details, `metrics.${metricKey}`) ||
265
- get(details, metricKey) ||
266
- get(details, `details.metrics.${metricKey}`) ||
267
- originalValue
268
- );
269
- }
270
-
271
- // Helper function to format metric values
272
- function formatMetricValue(key, value) {
273
- if (value === null || value === undefined) return value;
274
-
275
- switch (key) {
276
- case 'metrics.processed':
277
- return Number(value).toLocaleString();
278
- case 'metrics.avgTime':
279
- return value + 'ms';
280
- case 'metrics.memory':
281
- return value + 'MB';
282
- case 'metrics.cpu':
283
- return value + '%';
284
- case 'metrics.uptime':
285
- return formatUptime(value);
286
- case 'health.lastCheck':
287
- return formatTimeAgo(value);
288
- default:
289
- return value;
290
- }
291
- }
292
-
293
- // Helper function to update a single element
294
- function updateDetailElement(el, details) {
295
- const key = el.dataset.key;
296
- let value = get(details, key);
297
-
298
- // Resolve metric values from different sources
299
- value = resolveMetricValue(details, key, value);
300
-
301
- // Format the value
302
- value = formatMetricValue(key, value);
303
-
304
- // Update element if value is valid
305
- if (value !== null && value !== undefined && value !== '') {
306
- el.textContent = value;
307
- }
308
- }
309
-
310
- function updateDetailViews(detailRow, details) {
311
- if (!details) return;
312
-
313
- // Update all data-key elements
314
- detailRow.querySelectorAll('[data-key]').forEach((element) => {
315
- updateDetailElement(element);
316
- });
317
-
318
- // Delegate to specialized functions
319
- updateLogsContainer(detailRow, details);
320
- updateSLAContainer(detailRow, details);
321
- }
322
-
323
- // Helper function to format uptime
324
- function formatUptime(seconds) {
325
- if (!seconds || seconds === 'N/A') return 'N/A';
326
-
327
- const days = Math.floor(seconds / 86400);
328
- const hours = Math.floor((seconds % 86400) / 3600);
329
- const minutes = Math.floor((seconds % 3600) / 60);
330
-
331
- if (days > 0) {
332
- return `${days}d ${hours}h ${minutes}m`;
333
- } else if (hours > 0) {
334
- return `${hours}h ${minutes}m`;
335
- } else {
336
- return `${minutes}m`;
337
- }
338
- }
339
-
340
- function updateLogsContainer(detailRow, details) {
341
- // Handle logs if present
342
- const logsContainer = detailRow.querySelector('.logs-content');
343
- if (logsContainer && details.recentLogs && Array.isArray(details.recentLogs)) {
344
- // Clear existing content safely
345
- while (logsContainer.firstChild) {
346
- logsContainer.firstChild.remove();
347
- }
348
-
349
- if (details.recentLogs.length === 0) {
350
- const noLogsMsg = document.createElement('div');
351
- noLogsMsg.style.color = 'var(--muted)';
352
- noLogsMsg.textContent = 'No recent logs';
353
- logsContainer.appendChild(noLogsMsg);
354
- } else {
355
- details.recentLogs.forEach((log) => {
356
- let color = 'var(--text)';
357
- if (
358
- log.toLowerCase().includes('failed') ||
359
- log.toLowerCase().includes('error') ||
360
- log.toLowerCase().includes('down') ||
361
- log.toLowerCase().includes('unhealthy')
362
- )
363
- color = 'var(--danger)';
364
- else if (log.toLowerCase().includes('success') || log.toLowerCase().includes('healthy'))
365
- color = 'var(--success)';
366
- else if (log.toLowerCase().includes('processing')) color = 'var(--info)';
367
-
368
- const logElement = document.createElement('div');
369
- logElement.style.color = color;
370
- logElement.textContent = log; // Safe: textContent doesn't execute HTML
371
- logsContainer.appendChild(logElement);
372
- });
373
- }
374
- } else if (logsContainer) {
375
- // Clear existing content safely
376
- while (logsContainer.firstChild) {
377
- logsContainer.firstChild.remove();
378
- }
379
- const noLogsMsg = document.createElement('div');
380
- noLogsMsg.style.color = 'var(--muted)';
381
- noLogsMsg.textContent = 'No logs available';
382
- logsContainer.appendChild(noLogsMsg);
383
- }
384
- }
385
-
386
- function updateSLAContainer(detailRow, details) {
387
- // Render SLA Scorecard if container/data exists
388
- const slaContainer = detailRow.querySelector('.sla-scorecard-container');
389
- if (slaContainer && details.sla) {
390
- const s = details.sla;
391
-
392
- // Clear existing content safely
393
- while (slaContainer.firstChild) {
394
- slaContainer.firstChild.remove();
395
- }
396
-
397
- // Create main container
398
- const mainDiv = document.createElement('div');
399
- mainDiv.style.border = '1px solid var(--border)';
400
- mainDiv.style.borderRadius = '6px';
401
- mainDiv.style.padding = '10px';
402
- mainDiv.style.background = 'var(--input-bg)';
403
-
404
- // Create header
405
- const headerDiv = document.createElement('div');
406
- headerDiv.style.display = 'flex';
407
- headerDiv.style.justifyContent = 'space-between';
408
- headerDiv.style.marginBottom = '8px';
409
- headerDiv.style.borderBottom = '1px solid var(--border)';
410
- headerDiv.style.paddingBottom = '4px';
411
-
412
- const titleStrong = document.createElement('strong');
413
- titleStrong.style.fontSize = '13px';
414
- titleStrong.textContent = 'SLA Status';
415
-
416
- const statusSpan = document.createElement('span');
417
- // Extract nested ternary for better readability
418
- let statusClass;
419
- if (s.status === 'pass') {
420
- statusClass = 'active';
421
- } else if (s.status === 'fail') {
422
- statusClass = 'error';
423
- } else {
424
- statusClass = 'warning';
425
- }
426
- statusSpan.className = `status-badge status-${statusClass}`;
427
- statusSpan.textContent = s.status.toUpperCase();
428
-
429
- headerDiv.appendChild(titleStrong);
430
- headerDiv.appendChild(statusSpan);
431
- mainDiv.appendChild(headerDiv);
432
-
433
- // Create checks container
434
- const checksDiv = document.createElement('div');
435
- checksDiv.className = 'sla-checks';
436
-
437
- if (s.checks && Object.keys(s.checks).length > 0) {
438
- Object.entries(s.checks).forEach(([key, val]) => {
439
- // Extract nested ternary for better readability
440
- let color;
441
- if (val.status === 'pass') {
442
- color = 'var(--success)';
443
- } else if (val.status === 'fail') {
444
- color = 'var(--danger)';
445
- } else {
446
- color = 'var(--warning)';
447
- }
448
-
449
- const checkDiv = document.createElement('div');
450
- checkDiv.style.display = 'flex';
451
- checkDiv.style.justifyContent = 'space-between';
452
- checkDiv.style.fontSize = '12px';
453
- checkDiv.style.marginBottom = '4px';
454
-
455
- const keySpan = document.createElement('span');
456
- keySpan.textContent = key; // Safe: textContent
457
-
458
- const valueSpan = document.createElement('span');
459
- valueSpan.style.color = color;
460
- valueSpan.textContent = `${val.value} (msg: ${val.status})`; // Safe: textContent
461
-
462
- checkDiv.appendChild(keySpan);
463
- checkDiv.appendChild(valueSpan);
464
- checksDiv.appendChild(checkDiv);
465
- });
466
- } else {
467
- const noChecksDiv = document.createElement('div');
468
- noChecksDiv.className = 'text-muted';
469
- noChecksDiv.textContent = 'No checks';
470
- checksDiv.appendChild(noChecksDiv);
471
- }
472
-
473
- mainDiv.appendChild(checksDiv);
474
-
475
- // Create footer
476
- const footerDiv = document.createElement('div');
477
- footerDiv.style.marginTop = '6px';
478
- footerDiv.style.fontSize = '10px';
479
- footerDiv.style.color = 'var(--muted)';
480
- footerDiv.style.textAlign = 'right';
481
- footerDiv.textContent = `Evaluated: ${new Date(s.evaluatedAt).toLocaleTimeString()}`;
482
-
483
- mainDiv.appendChild(footerDiv);
484
- slaContainer.appendChild(mainDiv);
485
- }
486
- }
487
-
488
- function updateQueueSummary(queueData) {
489
- if (!queueData) return;
490
- const driverEl = document.getElementById('queue-driver');
491
- const totalEl = document.getElementById('queue-total');
492
- const jobsEl = document.getElementById('queue-jobs');
493
- const processingEl = document.getElementById('queue-processing');
494
- const failedEl = document.getElementById('queue-failed');
495
-
496
- if (driverEl) driverEl.textContent = queueData.driver || '-';
497
- if (totalEl) totalEl.textContent = String(queueData.totalQueues ?? 0);
498
- if (jobsEl) jobsEl.textContent = String(queueData.totalJobs ?? 0);
499
- if (processingEl) processingEl.textContent = String(queueData.processingJobs ?? 0);
500
- if (failedEl) failedEl.textContent = String(queueData.failedJobs ?? 0);
501
- }
502
-
503
- function updateDriverFilter(drivers) {
504
- const select = document.getElementById('driver-filter');
505
- if (!select || !Array.isArray(drivers)) return;
506
- const currentValue = select.value;
507
-
508
- // Clear existing options safely
509
- while (select.firstChild) {
510
- select.firstChild.remove();
511
- }
512
-
513
- // Add "All Drivers" option
514
- const allOption = document.createElement('option');
515
- allOption.value = '';
516
- allOption.textContent = 'All Drivers';
517
- select.appendChild(allOption);
518
- drivers.forEach((driver) => {
519
- const option = document.createElement('option');
520
- option.value = driver;
521
- option.textContent = driver.charAt(0).toUpperCase() + driver.slice(1);
522
- select.appendChild(option);
523
- });
524
- if (drivers.includes(currentValue)) {
525
- select.value = currentValue;
526
- }
527
- }
528
-
529
- function updateDriversList(drivers) {
530
- const list = document.getElementById('drivers-list');
531
- if (!list) return;
532
-
533
- // Clear existing content safely
534
- while (list.firstChild) {
535
- list.firstChild.remove();
536
- }
537
- if (!Array.isArray(drivers) || drivers.length === 0) {
538
- return;
539
- }
540
- drivers.forEach((driver) => {
541
- const chip = document.createElement('span');
542
- chip.className = 'driver-chip';
543
- chip.textContent = driver;
544
- list.appendChild(chip);
545
- });
546
- }
547
-
548
- function createWorkerRow(worker) {
549
- const detailsId = `details-${worker.name.replaceAll(/[^a-z0-9]/gi, '-')}`;
550
-
551
- const row = document.createElement('tr');
552
- row.className = 'expander';
553
- row.dataset.workerName = worker.name;
554
- row.dataset.workerDriver = worker.driver;
555
-
556
- // Create cells using helper functions
557
- const nameCell = createNameCell(worker, detailsId);
558
- const statusCell = createStatusCell(worker);
559
- const healthCell = createHealthCell(worker);
560
- const driverCell = createDriverCell(worker);
561
- const versionCell = createVersionCell(worker);
562
- const perfCell = createPerformanceCell();
563
- const actionCell = createActionCell(worker);
564
-
565
- // Append all cells to row
566
- row.appendChild(nameCell);
567
- row.appendChild(statusCell);
568
- row.appendChild(healthCell);
569
- row.appendChild(driverCell);
570
- row.appendChild(versionCell);
571
- row.appendChild(perfCell);
572
- row.appendChild(actionCell);
573
-
574
- return { row, detailsId };
575
- }
576
-
577
- function createNameCell(worker, detailsId) {
578
- const nameCell = document.createElement('td');
579
-
580
- // Create expandable container
581
- const nameContainer = document.createElement('div');
582
- nameContainer.style.cssText = `
583
- display: flex;
584
- align-items: center;
585
- gap: 8px;
586
- cursor: pointer;
587
- `;
588
- nameContainer.setAttribute('onclick', `toggleDetails('${detailsId}')`);
589
- nameContainer.setAttribute('title', 'Click to expand worker details');
590
-
591
- // Add expand/collapse icon
592
- const expandIcon = document.createElement('div');
593
- expandIcon.className = 'expand-icon';
594
- expandIcon.id = `expand-icon-${detailsId}`;
595
- expandIcon.innerHTML = `
596
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
597
- <polyline points="9 18 15 12 9 6"></polyline>
598
- </svg>
599
- `;
600
- expandIcon.style.cssText = `
601
- transition: transform 0.2s ease;
602
- color: var(--muted);
603
- flex-shrink: 0;
604
- `;
605
-
606
- // Add worker name
607
- const nameDiv = document.createElement('div');
608
- nameDiv.className = 'worker-name';
609
- nameDiv.textContent = worker.name; // Safe: textContent
610
-
611
- // Add queue name
612
- const queueDiv = document.createElement('div');
613
- queueDiv.className = 'worker-queue';
614
- queueDiv.textContent = worker.queueName; // Safe: textContent
615
- queueDiv.style.marginLeft = '24px'; // Align with worker name
616
-
617
- // Assemble the structure
618
- nameContainer.appendChild(expandIcon);
619
- nameContainer.appendChild(nameDiv);
620
-
621
- nameCell.appendChild(nameContainer);
622
- nameCell.appendChild(queueDiv);
623
- return nameCell;
624
- }
625
-
626
- function createStatusCell(worker) {
627
- const statusCell = document.createElement('td');
628
- const statusSpan = document.createElement('span');
629
- statusSpan.className = `status-badge status-${worker.status}`;
630
- const statusDot = document.createElement('span');
631
- statusDot.className = 'status-dot';
632
- const statusText = document.createTextNode(
633
- worker.status.charAt(0).toUpperCase() + worker.status.slice(1)
634
- );
635
- statusSpan.appendChild(statusDot);
636
- statusSpan.appendChild(statusText);
637
- statusCell.appendChild(statusSpan);
638
- return statusCell;
639
- }
640
-
641
- function createHealthCell(worker) {
642
- const healthCell = document.createElement('td');
643
- const healthDiv = document.createElement('div');
644
- healthDiv.className = 'health-indicator';
645
- const healthDot = document.createElement('span');
646
- healthDot.className = `health-dot health-${worker.health.status}`;
647
- const healthText = document.createTextNode(
648
- worker.health.status.charAt(0).toUpperCase() + worker.health.status.slice(1)
649
- );
650
- healthDiv.appendChild(healthDot);
651
- healthDiv.appendChild(healthText);
652
- healthCell.appendChild(healthDiv);
653
- return healthCell;
654
- }
655
-
656
- function createDriverCell(worker) {
657
- const driverCell = document.createElement('td');
658
- const driverSpan = document.createElement('span');
659
- driverSpan.className = 'driver-badge';
660
- driverSpan.textContent = worker.driver; // Safe: textContent
661
- driverCell.appendChild(driverSpan);
662
- return driverCell;
663
- }
664
-
665
- function createVersionCell(worker) {
666
- const versionCell = document.createElement('td');
667
- const versionSpan = document.createElement('span');
668
- versionSpan.className = 'version-badge';
669
- versionSpan.textContent = `v${worker.version}`; // Safe: textContent
670
- versionCell.appendChild(versionSpan);
671
- return versionCell;
672
- }
673
-
674
- // Helper function to safely extract performance metrics
675
- function getPerformanceMetrics(worker) {
676
- if (!worker) return { processed: 0, avgTime: 0, memory: 0 };
677
-
678
- const metrics = worker.metrics || {};
679
- return {
680
- processed: metrics.processed || worker.processed || 0,
681
- avgTime: metrics.avgTime || worker.avgTime || 0,
682
- memory: metrics.memory || worker.memory || 0,
683
- };
684
- }
685
-
686
- // Helper function to create performance icon HTML
687
- function createPerformanceIconHtml(type, value, unit) {
688
- const icons = {
689
- processed: `
690
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
691
- <line x1="12" y1="20" x2="12" y2="10" />
692
- <line x1="18" y1="20" x2="18" y2="4" />
693
- <line x1="6" y1="20" x2="6" y2="16" />
694
- </svg>
695
- `,
696
- time: `
697
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
698
- <circle cx="12" cy="12" r="10" />
699
- <polyline points="12 6 12 12 16 14" />
700
- </svg>
701
- `,
702
- memory: `
703
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
704
- <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
705
- <rect x="9" y="9" width="6" height="6" />
706
- <line x1="9" y1="1" x2="9" y2="4" />
707
- <line x1="15" y1="1" x2="15" y2="4" />
708
- <line x1="9" y1="20" x2="9" y2="23" />
709
- <line x1="15" y1="20" x2="15" y2="23" />
710
- <line x1="20" y1="9" x2="23" y2="9" />
711
- <line x1="20" y1="14" x2="23" y2="14" />
712
- <line x1="1" y1="9" x2="4" y2="9" />
713
- <line x1="1" y1="14" x2="4" y2="14" />
714
- </svg>
715
- `,
716
- };
717
-
718
- const titles = {
719
- processed: 'Processed Jobs',
720
- time: 'Avg Time',
721
- memory: 'Memory Usage',
722
- };
723
-
724
- return `
725
- <div class="perf-icon ${type}" title="${titles[type]}">
726
- ${icons[type]}
727
- <span>${value}${unit}</span>
728
- </div>
729
- `;
730
- }
731
-
732
- function createPerformanceCell(worker) {
733
- const perfCell = document.createElement('td');
734
- const perfDiv = document.createElement('div');
735
- perfDiv.className = 'performance-icons';
736
-
737
- const metrics = getPerformanceMetrics(worker);
738
- const processedValue = metrics.processed ? metrics.processed.toLocaleString() : '0';
739
-
740
- perfDiv.innerHTML = `
741
- ${createPerformanceIconHtml('processed', processedValue, '')}
742
- ${createPerformanceIconHtml('time', metrics.avgTime, 'ms')}
743
- ${createPerformanceIconHtml('memory', metrics.memory, 'MB')}
744
- `;
745
-
746
- perfCell.appendChild(perfDiv);
747
- return perfCell;
748
- }
749
-
750
- function createActionCell(worker) {
751
- const actionCell = document.createElement('td');
752
- const actionDiv = document.createElement('div');
753
- actionDiv.className = 'actions-cell';
754
-
755
- // Toggle visibility based on status
756
- if (worker.status === 'running') {
757
- const stopBtn = createActionButton('stop', 'Stop', () =>
758
- stopWorker(worker.name, worker.driver)
759
- );
760
- actionDiv.appendChild(stopBtn);
761
- } else {
762
- const startBtn = createActionButton('start', 'Start', () =>
763
- startWorker(worker.name, worker.driver)
764
- );
765
- actionDiv.appendChild(startBtn);
766
- }
767
-
768
- const restartBtn = createActionButton('restart', 'Restart', () =>
769
- restartWorker(worker.name, worker.driver)
770
- );
771
- const deleteBtn = createActionButton('delete', 'Delete', () =>
772
- deleteWorker(worker.name, worker.driver)
773
- );
774
- const viewJsonBtn = createActionButton('view', 'View JSON', () =>
775
- viewWorkerJson(worker.name, worker.driver)
776
- );
777
- const editJsonBtn = createActionButton('edit', 'Edit JSON', () =>
778
- editWorkerJson(worker.name, worker.driver)
779
- );
780
-
781
- actionDiv.appendChild(restartBtn);
782
- actionDiv.appendChild(deleteBtn);
783
- actionDiv.appendChild(viewJsonBtn);
784
- actionDiv.appendChild(editJsonBtn);
785
- actionCell.appendChild(actionDiv);
786
-
787
- return actionCell;
788
- }
789
-
790
- function createActionButton(type, title, onClickHandler) {
791
- const button = document.createElement('button');
792
- button.className = `action-btn ${type}`;
793
- button.title = title;
794
- button.onclick = function (event) {
795
- event.stopPropagation();
796
- onClickHandler();
797
- };
798
-
799
- // Create SVG icon based on button type
800
- const svg = createButtonIcon(type);
801
- button.appendChild(svg);
802
-
803
- return button;
804
- }
805
-
806
- // Helper functions for creating specific SVG icons
807
- function createStartIcon() {
808
- const svg = createSvgElement();
809
- const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
810
- polygon.setAttribute('points', '5 3 19 12 5 21 5 3');
811
- polygon.setAttribute('fill', 'currentColor');
812
- svg.appendChild(polygon);
813
- return svg;
814
- }
815
-
816
- function createStopIcon() {
817
- const svg = createSvgElement();
818
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
819
- rect.setAttribute('x', '3');
820
- rect.setAttribute('y', '3');
821
- rect.setAttribute('width', '18');
822
- rect.setAttribute('height', '18');
823
- rect.setAttribute('rx', '2');
824
- rect.setAttribute('ry', '2');
825
- rect.setAttribute('fill', 'currentColor');
826
- svg.appendChild(rect);
827
- return svg;
828
- }
829
-
830
- function createRestartIcon() {
831
- const svg = createSvgElement();
832
- const polyline1 = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
833
- polyline1.setAttribute('points', '23 4 23 10 17 10');
834
- const polyline2 = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
835
- polyline2.setAttribute('points', '1 20 1 14 7 14');
836
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
837
- path.setAttribute('d', 'M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15');
838
- svg.appendChild(polyline1);
839
- svg.appendChild(polyline2);
840
- svg.appendChild(path);
841
- return svg;
842
- }
843
-
844
- function createDeleteIcon() {
845
- const svg = createSvgElement();
846
- const deletePolyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
847
- deletePolyline.setAttribute('points', '3 6 5 6 21 6');
848
- const deletePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
849
- deletePath.setAttribute(
850
- 'd',
851
- 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'
852
- );
853
- const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
854
- line1.setAttribute('x1', '10');
855
- line1.setAttribute('y1', '11');
856
- line1.setAttribute('x2', '10');
857
- line1.setAttribute('y2', '17');
858
- const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
859
- line2.setAttribute('x1', '14');
860
- line2.setAttribute('y1', '11');
861
- line2.setAttribute('x2', '14');
862
- line2.setAttribute('y2', '17');
863
- svg.appendChild(deletePolyline);
864
- svg.appendChild(deletePath);
865
- svg.appendChild(line1);
866
- svg.appendChild(line2);
867
- return svg;
868
- }
869
-
870
- function createViewIcon() {
871
- const svg = createSvgElement();
872
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
873
- path.setAttribute('d', 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z');
874
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
875
- circle.setAttribute('cx', '12');
876
- circle.setAttribute('cy', '12');
877
- circle.setAttribute('r', '3');
878
- svg.appendChild(path);
879
- svg.appendChild(circle);
880
- return svg;
881
- }
882
-
883
- function createEditIcon() {
884
- const svg = createSvgElement();
885
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
886
- path.setAttribute('d', 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7');
887
- const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
888
- path2.setAttribute('d', 'M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z');
889
- svg.appendChild(path);
890
- svg.appendChild(path2);
891
- return svg;
892
- }
893
-
894
- // Create base SVG element with common attributes
895
- function createSvgElement() {
896
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
897
- svg.setAttribute('class', 'icon');
898
- svg.setAttribute('viewBox', '0 0 24 24');
899
- svg.setAttribute('fill', 'none');
900
- svg.setAttribute('stroke', 'currentColor');
901
- svg.setAttribute('stroke-width', '2');
902
- svg.setAttribute('stroke-linecap', 'round');
903
- svg.setAttribute('stroke-linejoin', 'round');
904
- return svg;
905
- }
906
-
907
- function createButtonIcon(type) {
908
- switch (type) {
909
- case 'start':
910
- return createStartIcon();
911
- case 'stop':
912
- return createStopIcon();
913
- case 'restart':
914
- return createRestartIcon();
915
- case 'delete':
916
- return createDeleteIcon();
917
- case 'view':
918
- return createViewIcon();
919
- case 'edit':
920
- return createEditIcon();
921
- default:
922
- return createSvgElement();
923
- }
924
- }
925
-
926
- function createDetailRow(worker, detailsId) {
927
- const detailRow = document.createElement('tr');
928
- detailRow.className = 'expandable-row';
929
- detailRow.id = detailsId;
930
- detailRow.dataset.workerName = worker.name;
931
- detailRow.dataset.workerDriver = worker.driver;
932
-
933
- // Delegate HTML creation to specialized function
934
- detailRow.innerHTML = createDetailRowHTML(worker);
935
-
936
- return detailRow;
937
- }
938
-
939
- // Helper function to create Configuration section HTML
940
- function createConfigurationSection(worker) {
941
- return `
942
- <div class="detail-section">
943
- <h4>Configuration</h4>
944
- <div class="detail-item">
945
- <span>Queue Name</span>
946
- <span data-key="configuration.queueName">${worker.queueName}</span>
947
- </div>
948
- <div class="detail-item">
949
- <span>Worker Name</span>
950
- <span data-key="configuration.name">${worker.name}</span>
951
- </div>
952
- <div class="detail-item">
953
- <span>Driver</span>
954
- <span data-key="configuration.driver">${worker.driver}</span>
955
- </div>
956
- <div class="detail-item">
957
- <span>Version</span>
958
- <span data-key="configuration.version">v${worker.version}</span>
959
- </div>
960
- <div class="detail-item">
961
- <span>Auto Start</span>
962
- <label class="auto-start-toggle" onclick="event.stopPropagation()">
963
- <input type="checkbox" ${worker.autoStart ? 'checked' : ''} onchange="toggleAutoStart('${worker.name}', '${worker.driver}', this.checked)">
964
- <span class="toggle-slider"></span>
965
- </label>
966
- </div>
967
- <div class="detail-item">
968
- <span>Status</span>
969
- <span data-key="configuration.status">${worker.status}</span>
970
- </div>
971
- </div>
972
- `;
973
- }
974
-
975
- // Helper function to create Performance Metrics section HTML
976
- function createPerformanceMetricsSection(worker) {
977
- return `
978
- <div class="detail-section">
979
- <h4>Performance Metrics</h4>
980
- <div class="detail-item">
981
- <span>Processed Jobs</span>
982
- <span data-key="metrics.processed">${worker.processed.toLocaleString()}</span>
983
- </div>
984
- <div class="detail-item">
985
- <span>Failed Jobs</span>
986
- <span data-key="metrics.failed">${worker.failed || 0}</span>
987
- </div>
988
- <div class="detail-item">
989
- <span>Average Time</span>
990
- <span data-key="metrics.avgTime">${worker.avgTime}ms</span>
991
- </div>
992
- <div class="detail-item">
993
- <span>Memory Usage</span>
994
- <span data-key="metrics.memory">${worker.memory}MB</span>
995
- </div>
996
- <div class="detail-item">
997
- <span>CPU Usage</span>
998
- <span data-key="metrics.cpu">${worker.cpu || 'N/A'}</span>
999
- </div>
1000
- <div class="detail-item">
1001
- <span>Uptime</span>
1002
- <span data-key="metrics.uptime">${worker.uptime || 'N/A'}</span>
1003
- </div>
1004
- </div>
1005
- `;
1006
- }
1007
-
1008
- // Helper function to create Health & Status section HTML
1009
- function createHealthStatusSection(worker) {
1010
- return `
1011
- <div class="detail-section">
1012
- <h4>Health & Status</h4>
1013
- <div class="detail-item">
1014
- <span>Health Status</span>
1015
- <span data-key="health.status">${worker.health?.status || 'unknown'}</span>
1016
- </div>
1017
- <div class="detail-item">
1018
- <span>Last Check</span>
1019
- <span data-key="health.lastCheck" class="last-check">${formatTimeAgo(worker.health?.lastCheck)}</span>
1020
- </div>
1021
- <div class="detail-item">
1022
- <span>Health Checks</span>
1023
- <div class="health-checks">
1024
- ${renderHealthChecks(worker.health?.checks)}
1025
- </div>
1026
- </div>
1027
- <div class="detail-item">
1028
- <span>Worker Status</span>
1029
- <span data-key="status" class="status-badge status-${worker.status}">${worker.status}</span>
1030
- </div>
1031
- </div>
1032
- `;
1033
- }
1034
-
1035
- // Helper function to render health checks
1036
- function renderHealthChecks(checks) {
1037
- if (!checks || !Array.isArray(checks) || checks.length === 0) {
1038
- return '<span class="no-checks">No health checks available</span>';
1039
- }
1040
-
1041
- return checks
1042
- .map(
1043
- (check) => `
1044
- <div class="health-check">
1045
- <span class="check-name">${check.name}</span>
1046
- <span class="check-status status-${check.status}">${check.status}</span>
1047
- ${check.message ? `<span class="check-message">${check.message}</span>` : ''}
1048
- </div>
1049
- `
1050
- )
1051
- .join('');
1052
- }
1053
-
1054
- // Helper function to format time as "ago"
1055
- function formatTimeAgo(timestamp) {
1056
- if (!timestamp) return 'Never';
1057
-
1058
- const now = new Date();
1059
- const checkTime = new Date(timestamp);
1060
- const diffMs = now - checkTime;
1061
-
1062
- if (diffMs < 0) return 'Just now';
1063
-
1064
- const diffSeconds = Math.floor(diffMs / 1000);
1065
- const diffMinutes = Math.floor(diffSeconds / 60);
1066
- const diffHours = Math.floor(diffMinutes / 60);
1067
- const diffDays = Math.floor(diffHours / 24);
1068
-
1069
- if (diffSeconds < 60) {
1070
- return diffSeconds <= 1 ? 'Just now' : `${diffSeconds}s ago`;
1071
- } else if (diffMinutes < 60) {
1072
- return `${diffMinutes}m ago`;
1073
- } else if (diffHours < 24) {
1074
- return `${diffHours}h ago`;
1075
- } else if (diffDays < 7) {
1076
- return `${diffDays}d ago`;
1077
- } else {
1078
- return checkTime.toLocaleDateString();
1079
- }
1080
- }
1081
-
1082
- // Helper function to create Recent Logs section HTML
1083
- function createRecentLogsSection(worker) {
1084
- return `
1085
- <div class="detail-section">
1086
- <h4>Recent Logs (History)</h4>
1087
- <div class="recent-logs-container" style="
1088
- font-family: monospace;
1089
- font-size: 11px;
1090
- line-height: 1.6;
1091
- color: var(--text);
1092
- background: var(--input-bg);
1093
- padding: 12px;
1094
- border-radius: 8px;
1095
- border: 1px solid var(--border);
1096
- max-height: 200px;
1097
- overflow-y: auto;
1098
- ">
1099
- <div class="logs-content">
1100
- ${
1101
- worker.details?.recentLogs
1102
- ?.map(
1103
- (log) => `
1104
- <div class="log-entry log-${log.level.toLowerCase()}">
1105
- <span class="log-timestamp">[${log.timestamp}]</span>
1106
- <span class="log-level">${log.level.toUpperCase()}</span>
1107
- <span class="log-message">${log.message}</span>
1108
- </div>
1109
- `
1110
- )
1111
- .join('') || '<div class="no-logs">No recent logs available</div>'
1112
- }
1113
- </div>
1114
- </div>
1115
- </div>
1116
- `;
1117
- }
1118
-
1119
- function createDetailRowHTML(worker) {
1120
- return `
1121
- <td colspan="7" class="details-cell">
1122
- <div class="details-content">
1123
- <div class="details-grid">
1124
- ${createConfigurationSection(worker)}
1125
- ${createPerformanceMetricsSection(worker)}
1126
- ${createHealthStatusSection(worker)}
1127
- ${createRecentLogsSection(worker)}
1128
- </div>
1129
- </div>
1130
- </td>
1131
- `;
1132
- }
1133
-
1134
- function renderWorkers(data) {
1135
- const tbody = document.getElementById('workers-tbody');
1136
- if (!tbody) return;
1137
-
1138
- const expandedWorkers = new Set(
1139
- Array.from(tbody.querySelectorAll('.expandable-row.open'))
1140
- .map((row) => row.getAttribute('id')?.replace('details-', ''))
1141
- .filter(Boolean)
1142
- );
1143
-
1144
- // Clear existing content safely
1145
- while (tbody.firstChild) {
1146
- tbody.firstChild.remove();
1147
- }
1148
-
1149
- if (!data.workers || data.workers.length === 0) {
1150
- const noWorkersRow = document.createElement('tr');
1151
- const noWorkersCell = document.createElement('td');
1152
- noWorkersCell.colSpan = '7';
1153
- noWorkersCell.className = 'text-center p-4';
1154
- noWorkersCell.textContent = 'No workers found';
1155
- noWorkersRow.appendChild(noWorkersCell);
1156
- tbody.appendChild(noWorkersRow);
1157
-
1158
- updateQueueSummary(data.queueData);
1159
- updateDriverFilter(data.drivers);
1160
- updateDriversList(data.drivers);
1161
- updatePagination(data.pagination);
1162
- return;
1163
- }
1164
-
1165
- data.workers.forEach((worker) => {
1166
- const { row, detailsId } = createWorkerRow(worker);
1167
- const detailRow = createDetailRow(worker, detailsId);
1168
-
1169
- const normalizedName = worker.name.replaceAll(/[^a-z0-9]/gi, '-');
1170
- if (expandedWorkers.has(normalizedName)) {
1171
- detailRow.classList.add('open');
1172
- ensureWorkerDetails(worker.name, detailRow, worker.driver);
1173
- }
1174
-
1175
- tbody.appendChild(row);
1176
- tbody.appendChild(detailRow);
1177
- });
1178
-
1179
- updateQueueSummary(data.queueData);
1180
- updateDriverFilter(data.drivers);
1181
- updateDriversList(data.drivers);
1182
- updatePagination(data.pagination);
1183
- }
1184
-
1185
- function toggleDetails(rowId) {
1186
- const row = document.getElementById(rowId);
1187
- if (row) {
1188
- const isOpen = row.classList.toggle('open');
1189
-
1190
- // Rotate expand icon
1191
- const expandIcon = document.getElementById(`expand-icon-${rowId}`);
1192
- if (expandIcon) {
1193
- expandIcon.style.transform = isOpen ? 'rotate(90deg)' : 'rotate(0deg)';
1194
- }
1195
-
1196
- if (isOpen) {
1197
- const workerName = row.dataset.workerName || rowId.replace('details-', '');
1198
- const workerDriver = row.dataset.workerDriver;
1199
- ensureWorkerDetails(workerName, row, workerDriver);
1200
- }
1201
- }
1202
- }
1203
-
1204
- function updatePagination(pagination) {
1205
- currentPage = pagination.page;
1206
- totalPages = pagination.totalPages;
1207
- totalWorkers = pagination.total;
1208
-
1209
- const start = (pagination.page - 1) * pagination.limit + 1;
1210
- const end = Math.min(pagination.page * pagination.limit, pagination.total);
1211
-
1212
- document.getElementById('pagination-info').textContent =
1213
- `Showing ${totalWorkers === 0 ? 0 : start}-${end} of ${totalWorkers} workers`;
1214
-
1215
- document.getElementById('prev-btn').disabled = !pagination.hasPrev;
1216
- document.getElementById('next-btn').disabled = !pagination.hasNext;
1217
-
1218
- // Update page numbers
1219
- const pageNumbers = document.getElementById('page-numbers');
1220
-
1221
- // Clear existing content safely
1222
- while (pageNumbers.firstChild) {
1223
- pageNumbers.firstChild.remove();
1224
- }
1225
-
1226
- const startPage = Math.max(1, currentPage - 2);
1227
- const endPage = Math.min(totalPages, currentPage + 2);
1228
-
1229
- for (let i = startPage; i <= endPage; i++) {
1230
- const btn = document.createElement('button');
1231
- btn.className = `page-btn ${i === currentPage ? 'active' : ''}`;
1232
- btn.textContent = i.toString();
1233
- btn.onclick = () => goToPage(i);
1234
- pageNumbers.appendChild(btn);
1235
- }
1236
- }
1237
-
1238
- function loadPage(direction) {
1239
- if (direction === 'prev' && currentPage > 1) {
1240
- currentPage--;
1241
- } else if (direction === 'next' && currentPage < totalPages) {
1242
- currentPage++;
1243
- }
1244
- fetchData(); // Enable for pagination
1245
- }
1246
-
1247
- function goToPage(page) {
1248
- currentPage = page;
1249
- fetchData(); // Enable for pagination
1250
- }
1251
-
1252
- // Worker actions
1253
- async function startWorker(name, driver) {
1254
- // Find and disable the start button to prevent multiple clicks
1255
- const startBtn = document.querySelector(`button[onclick="startWorker('${name}', '${driver}')"]`);
1256
- if (startBtn) {
1257
- startBtn.disabled = true;
1258
- startBtn.textContent = 'Starting...';
1259
- }
1260
-
1261
- try {
1262
- await fetch(`${API_BASE}/api/workers/${name}/start?driver=${driver}`, { method: 'POST' });
1263
- fetchData(); // Refresh data after action
1264
- } catch (err) {
1265
- console.error('Failed to start worker:', err);
1266
- // Re-enable button on error
1267
- if (startBtn) {
1268
- startBtn.disabled = false;
1269
- startBtn.textContent = 'Start';
1270
- }
1271
- }
1272
- }
1273
-
1274
- async function stopWorker(name, driver) {
1275
- // Find and disable the stop button to prevent multiple clicks
1276
- const stopBtn = document.querySelector(`button[onclick="stopWorker('${name}', '${driver}')"]`);
1277
- if (stopBtn) {
1278
- stopBtn.disabled = true;
1279
- stopBtn.textContent = 'Stopping...';
1280
- }
1281
-
1282
- try {
1283
- await fetch(`${API_BASE}/api/workers/${name}/stop?driver=${driver}`, { method: 'POST' });
1284
- fetchData(); // Refresh data after action
1285
- } catch (err) {
1286
- console.error('Failed to stop worker:', err);
1287
- // Re-enable button on error
1288
- if (stopBtn) {
1289
- stopBtn.disabled = false;
1290
- stopBtn.textContent = 'Stop';
1291
- }
1292
- }
1293
- }
1294
-
1295
- async function restartWorker(name, driver) {
1296
- // Find and disable the restart button to prevent multiple clicks
1297
- const restartBtn = document.querySelector(
1298
- `button[onclick="restartWorker('${name}', '${driver}')"]`
1299
- );
1300
- if (restartBtn) {
1301
- restartBtn.disabled = true;
1302
- restartBtn.textContent = 'Restarting...';
1303
- }
1304
-
1305
- try {
1306
- await fetch(`${API_BASE}/api/workers/${name}/restart?driver=${driver}`, {
1307
- method: 'POST',
1308
- });
1309
- fetchData(); // Refresh data after action
1310
- } catch (err) {
1311
- console.error('Failed to restart worker:', err);
1312
- // Re-enable button on error
1313
- if (restartBtn) {
1314
- restartBtn.disabled = false;
1315
- restartBtn.textContent = 'Restart';
1316
- }
1317
- }
1318
- }
1319
-
1320
- async function deleteWorker(name, driver) {
1321
- if (!confirm(`Are you sure you want to delete worker "${name}"? This action cannot be undone.`)) {
1322
- return;
1323
- }
1324
- try {
1325
- const response = await fetch(`${API_BASE}/api/workers/${name}?driver=${driver}`, {
1326
- method: 'DELETE',
1327
- });
1328
- if (!response.ok) throw new Error('Failed to delete worker');
1329
- fetchData(); // Refresh data after action
1330
- } catch (err) {
1331
- console.error('Failed to delete worker:', err);
1332
- alert('Failed to delete worker: ' + err.message);
1333
- }
1334
- }
1335
-
1336
- async function toggleAutoStart(name, driver, enabled) {
1337
- try {
1338
- await fetch(`${API_BASE}/api/workers/${name}/auto-start?driver=${driver}`, {
1339
- method: 'POST',
1340
- headers: { 'Content-Type': 'application/json' },
1341
- body: JSON.stringify({ enabled }),
1342
- });
1343
- } catch (err) {
1344
- console.error('Failed to toggle auto-start:', err);
1345
- }
1346
- }
1347
-
1348
- function showAddWorkerModal() {
1349
- // TODO: Implement add worker modal
1350
- alert('Add Worker functionality coming soon!');
1351
- }
1352
-
1353
- // Helper function to create modal overlay
1354
- function createModalOverlay() {
1355
- const modal = document.createElement('div');
1356
- modal.className = 'modal-overlay';
1357
- modal.style.cssText = `
1358
- position: fixed;
1359
- top: 0;
1360
- left: 0;
1361
- width: 100%;
1362
- height: 100%;
1363
- background: rgba(0,0,0,0.6);
1364
- display: flex;
1365
- align-items: center;
1366
- justify-content: center;
1367
- z-index: 1000;
1368
- backdrop-filter: blur(4px);
1369
- `;
1370
- return modal;
1371
- }
1372
-
1373
- // Helper function to create modal content
1374
- function createModalContent() {
1375
- const content = document.createElement('div');
1376
- content.className = 'modal-content json-modal';
1377
- content.style.cssText = `
1378
- background: var(--card);
1379
- padding: 24px;
1380
- border-radius: 12px;
1381
- max-width: 90%;
1382
- max-height: 90%;
1383
- overflow: auto;
1384
- box-shadow: var(--shadow-lg);
1385
- border: 1px solid var(--border);
1386
- min-width: 400px;
1387
- `;
1388
- return content;
1389
- }
1390
-
1391
- // Helper function to create modal header
1392
- function createModalHeader(name, onClose) {
1393
- const header = document.createElement('div');
1394
- header.style.cssText = `
1395
- display: flex;
1396
- justify-content: space-between;
1397
- align-items: center;
1398
- margin-bottom: 20px;
1399
- padding-bottom: 16px;
1400
- border-bottom: 1px solid var(--border);
1401
- `;
1402
-
1403
- const title = document.createElement('h3');
1404
- title.textContent = `Worker JSON: ${name}`;
1405
- title.style.cssText = `
1406
- margin: 0;
1407
- color: var(--text);
1408
- font-size: 18px;
1409
- font-weight: 600;
1410
- `;
1411
-
1412
- const closeBtn = document.createElement('button');
1413
- closeBtn.innerHTML = `
1414
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1415
- <line x1="18" y1="6" x2="6" y2="18"></line>
1416
- <line x1="6" y1="6" x2="18" y2="18"></line>
1417
- </svg>
1418
- `;
1419
- closeBtn.className = 'btn-close';
1420
- closeBtn.style.cssText = `
1421
- background: none;
1422
- border: none;
1423
- color: var(--muted);
1424
- cursor: pointer;
1425
- padding: 4px;
1426
- border-radius: 4px;
1427
- display: flex;
1428
- align-items: center;
1429
- justify-content: center;
1430
- transition: color 0.2s;
1431
- `;
1432
- closeBtn.onmouseover = function () {
1433
- closeBtn.style.color = 'var(--text)';
1434
- };
1435
- closeBtn.onmouseout = function () {
1436
- closeBtn.style.color = 'var(--muted)';
1437
- };
1438
- closeBtn.onclick = onClose;
1439
-
1440
- header.appendChild(title);
1441
- header.appendChild(closeBtn);
1442
- return header;
1443
- }
1444
-
1445
- // Helper function to create JSON display
1446
- function createJsonDisplay(jsonContent) {
1447
- const pre = document.createElement('pre');
1448
- pre.textContent = jsonContent;
1449
- pre.style.cssText = `
1450
- background: var(--input-bg);
1451
- color: var(--text);
1452
- padding: 16px;
1453
- border-radius: 8px;
1454
- overflow: auto;
1455
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
1456
- font-size: 13px;
1457
- line-height: 1.5;
1458
- max-height: 500px;
1459
- border: 1px solid var(--border);
1460
- white-space: pre-wrap;
1461
- word-wrap: break-word;
1462
- `;
1463
- return pre;
1464
- }
1465
-
1466
- // Helper function to create copy button
1467
- function createCopyButton(jsonContent) {
1468
- const actions = document.createElement('div');
1469
- actions.style.cssText = `
1470
- display: flex;
1471
- gap: 12px;
1472
- margin-top: 20px;
1473
- padding-top: 16px;
1474
- border-top: 1px solid var(--border);
1475
- `;
1476
-
1477
- const copyBtn = document.createElement('button');
1478
- copyBtn.innerHTML = `
1479
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
1480
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1481
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1482
- </svg>
1483
- Copy JSON
1484
- `;
1485
- copyBtn.className = 'btn';
1486
- copyBtn.style.cssText = `
1487
- display: flex;
1488
- align-items: center;
1489
- padding: 8px 16px;
1490
- background: var(--accent);
1491
- color: white;
1492
- border: none;
1493
- border-radius: 6px;
1494
- cursor: pointer;
1495
- font-size: 14px;
1496
- transition: background 0.2s;
1497
- `;
1498
- copyBtn.onmouseover = function () {
1499
- copyBtn.style.background = 'var(--accent-hover)';
1500
- };
1501
- copyBtn.onmouseout = function () {
1502
- copyBtn.style.background = 'var(--accent)';
1503
- };
1504
- copyBtn.onclick = async () => {
1505
- try {
1506
- await navigator.clipboard.writeText(jsonContent);
1507
- const originalText = copyBtn.innerHTML;
1508
- copyBtn.innerHTML = `
1509
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
1510
- <polyline points="20 6 9 17 4 12"></polyline>
1511
- </svg>
1512
- Copied!
1513
- `;
1514
- setTimeout(() => {
1515
- copyBtn.innerHTML = originalText;
1516
- }, 2000);
1517
- } catch (err) {
1518
- console.error('Failed to copy:', err);
1519
- }
1520
- };
1521
-
1522
- actions.appendChild(copyBtn);
1523
- return actions;
1524
- }
1525
-
1526
- // Helper function to setup modal event handlers
1527
- function setupModalHandlers(modal) {
1528
- // Only close on explicit close button click - not on backdrop click or ESC
1529
- // This ensures developers intentionally close the modal
1530
-
1531
- // Prevent body scroll when modal is open
1532
- document.body.style.overflow = 'hidden';
1533
- modal.addEventListener('remove', () => {
1534
- document.body.style.overflow = '';
1535
- });
1536
- }
1537
-
1538
- // View worker JSON data
1539
- async function viewWorkerJson(name, driver) {
1540
- try {
1541
- const response = await fetch(`${API_BASE}/api/workers/${name}/details?driver=${driver}`);
1542
- if (!response.ok) {
1543
- throw new Error('Failed to fetch worker data');
1544
- }
1545
-
1546
- const data = await response.json();
1547
- const jsonContent = JSON.stringify(data, null, 2);
1548
-
1549
- // Create modal components
1550
- const modal = createModalOverlay();
1551
- const content = createModalContent();
1552
- const header = createModalHeader(name, () => modal.remove());
1553
- const jsonDisplay = createJsonDisplay(jsonContent);
1554
- const copyButton = createCopyButton(jsonContent);
1555
-
1556
- // Assemble modal
1557
- content.appendChild(header);
1558
- content.appendChild(jsonDisplay);
1559
- content.appendChild(copyButton);
1560
- modal.appendChild(content);
1561
- document.body.appendChild(modal);
1562
-
1563
- // Setup event handlers
1564
- setupModalHandlers(modal);
1565
- } catch (err) {
1566
- console.error('Failed to view worker JSON:', err);
1567
- alert('Failed to load worker JSON: ' + err.message);
1568
- }
1569
- }
1570
-
1571
- // Create modal element with basic styling
1572
- function createModal() {
1573
- const modal = document.createElement('div');
1574
- modal.style.cssText = `
1575
- position: fixed;
1576
- top: 0;
1577
- left: 0;
1578
- width: 100%;
1579
- height: 100%;
1580
- background: rgba(0,0,0,0.5);
1581
- display: flex;
1582
- align-items: center;
1583
- justify-content: center;
1584
- z-index: 1000;
1585
- `;
1586
- return modal;
1587
- }
1588
-
1589
- // Create JSON textarea for editing
1590
- function createJsonTextarea(jsonContent) {
1591
- const textarea = document.createElement('textarea');
1592
- textarea.value = jsonContent;
1593
- textarea.style.cssText = `
1594
- width: 100%;
1595
- height: 400px;
1596
- padding: 10px;
1597
- border: 1px solid #ddd;
1598
- border-radius: 4px;
1599
- font-family: monospace;
1600
- font-size: 12px;
1601
- resize: vertical;
1602
- `;
1603
- return textarea;
1604
- }
1605
-
1606
- // Create save and cancel buttons
1607
- function createEditButtons(modal, textarea, name, driver) {
1608
- const buttonDiv = document.createElement('div');
1609
- buttonDiv.style.cssText = `
1610
- margin-top: 15px;
1611
- display: flex;
1612
- gap: 10px;
1613
- `;
1614
-
1615
- const saveBtn = document.createElement('button');
1616
- saveBtn.textContent = 'Save';
1617
- saveBtn.style.cssText = `
1618
- padding: 8px 16px;
1619
- background: #28a745;
1620
- color: white;
1621
- border: none;
1622
- border-radius: 4px;
1623
- cursor: pointer;
1624
- `;
1625
-
1626
- const cancelBtn = document.createElement('button');
1627
- cancelBtn.textContent = 'Cancel';
1628
- cancelBtn.style.cssText = `
1629
- padding: 8px 16px;
1630
- background: #6c757d;
1631
- color: white;
1632
- border: none;
1633
- border-radius: 4px;
1634
- cursor: pointer;
1635
- `;
1636
- cancelBtn.onclick = () => modal.remove();
1637
-
1638
- saveBtn.onclick = async () => {
1639
- try {
1640
- const updatedData = JSON.parse(textarea.value);
1641
- // Use the new edit endpoint that has withCreateWorkerValidation
1642
- const updateResponse = await fetch(`${API_BASE}/api/workers/${name}/edit?driver=${driver}`, {
1643
- method: 'PUT',
1644
- headers: { 'Content-Type': 'application/json' },
1645
- body: JSON.stringify(updatedData),
1646
- });
1647
-
1648
- if (!updateResponse.ok) {
1649
- throw new Error('Failed to update worker');
1650
- }
1651
-
1652
- alert('Worker updated successfully!');
1653
- modal.remove();
1654
- fetchData(); // Refresh data after action
1655
- } catch (error) {
1656
- alert('Invalid JSON: ' + error.message);
1657
- }
1658
- };
1659
-
1660
- buttonDiv.appendChild(saveBtn);
1661
- buttonDiv.appendChild(cancelBtn);
1662
- return buttonDiv;
1663
- }
1664
-
1665
- // Edit worker JSON data
1666
- async function editWorkerJson(name, driver) {
1667
- try {
1668
- // Get direct driver data for editing (raw persisted data without enrichment)
1669
- const response = await fetch(`${API_BASE}/api/workers/${name}/driver-data?driver=${driver}`);
1670
- if (!response.ok) {
1671
- throw new Error('Failed to fetch worker driver data');
1672
- }
1673
-
1674
- const data = await response.json();
1675
- const jsonContent = JSON.stringify(data.data, null, 2);
1676
-
1677
- // Create modal components
1678
- const modal = createModal();
1679
- const content = createModalContent();
1680
- const title = document.createElement('h3');
1681
- title.textContent = `Edit Worker JSON: ${name}`;
1682
- title.style.marginBottom = '15px';
1683
-
1684
- // Add warning about immutable fields
1685
- const warning = document.createElement('div');
1686
- warning.style.cssText = `
1687
- background-color: var(--warning-bg, #fff3cd);
1688
- border: 1px solid var(--warning-border, #ffeaa7);
1689
- color: var(--warning-text, #856404);
1690
- padding: 10px;
1691
- margin-bottom: 15px;
1692
- border-radius: 4px;
1693
- font-size: 14px;
1694
- `;
1695
- warning.innerHTML = `
1696
- <strong>⚠️ Note:</strong> Worker name (<code>${name}</code>) and driver (<code>${driver}</code>) cannot be changed.
1697
- Only other configuration fields can be modified.
1698
- `;
1699
-
1700
- const textarea = createJsonTextarea(jsonContent);
1701
- const buttonDiv = createEditButtons(modal, textarea, name, driver);
1702
-
1703
- // Assemble modal
1704
- content.appendChild(title);
1705
- content.appendChild(warning);
1706
- content.appendChild(textarea);
1707
- content.appendChild(buttonDiv);
1708
- modal.appendChild(content);
1709
- document.body.appendChild(modal);
1710
-
1711
- // Only close on explicit close button click - not on backdrop click or ESC
1712
- // This ensures developers intentionally close the modal
1713
-
1714
- // Prevent body scroll when modal is open
1715
- document.body.style.overflow = 'hidden';
1716
- modal.addEventListener('remove', () => {
1717
- document.body.style.overflow = '';
1718
- });
1719
- } catch (err) {
1720
- console.error('Failed to edit worker JSON:', err);
1721
- alert('Failed to load worker JSON: ' + err.message);
1722
- }
1723
- }
1724
-
1725
- function toggleAutoRefresh() {
1726
- setAutoRefresh(!autoRefreshEnabled);
1727
- }
1728
-
1729
- function setupEventStream() {
1730
- if (!globalThis.window.EventSource) return;
1731
-
1732
- if (eventSource) {
1733
- eventSource.close();
1734
- eventSource = null;
1735
- }
1736
-
1737
- // Get current sort parameters to match fetchData
1738
- const sortBy = document.getElementById('sort-select')?.value || 'status';
1739
- const sortOrder = 'asc';
1740
- const status = document.getElementById('status-filter')?.value || '';
1741
- const driver = document.getElementById('driver-filter')?.value || '';
1742
- const search = document.getElementById('search-input')?.value || '';
1743
-
1744
- const params = new URLSearchParams({
1745
- sortBy,
1746
- sortOrder,
1747
- status,
1748
- driver,
1749
- search,
1750
- });
1751
-
1752
- eventSource = new globalThis.window.EventSource(
1753
- API_BASE + '/api/workers/events?' + params.toString()
1754
- );
1755
-
1756
- eventSource.onopen = () => {
1757
- sseActive = true;
1758
- if (refreshTimer) {
1759
- clearInterval(refreshTimer);
1760
- refreshTimer = null;
1761
- }
1762
- };
1763
-
1764
- eventSource.onmessage = (evt) => {
1765
- const elements = getDomElements();
1766
-
1767
- try {
1768
- const payload = JSON.parse(evt.data);
1769
- if (payload && payload.type === 'snapshot') {
1770
- const now = Date.now();
1771
- if (now - lastSseRefresh < 4000) return;
1772
- lastSseRefresh = now;
1773
- // Use SSE data to update the page
1774
- if (payload.workers) {
1775
- renderWorkers(payload.workers);
1776
- // Hide loading state if it's showing
1777
- } else if (payload.snapshot) {
1778
- // Handle queue snapshot events (different format)
1779
- // console.log('Queue snapshot received, not updating workers UI');
1780
- }
1781
- if (payload.monitoring) {
1782
- // Handle queue monitoring events (different format)
1783
- }
1784
- }
1785
- hideLoadingState(elements);
1786
- } catch (err) {
1787
- hideLoadingState(elements);
1788
- console.error('Failed to parse SSE payload', err);
1789
- }
1790
- };
1791
-
1792
- eventSource.onerror = () => {
1793
- if (eventSource) {
1794
- eventSource.close();
1795
- eventSource = null;
1796
- }
1797
- sseActive = false;
1798
- };
1799
- }
1800
-
1801
- // Helper functions to reduce complexity
1802
- function clearRefreshTimer() {
1803
- if (refreshTimer) {
1804
- clearInterval(refreshTimer);
1805
- refreshTimer = null;
1806
- }
1807
- }
1808
-
1809
- function disableEventStream() {
1810
- if (eventSource) {
1811
- eventSource.close();
1812
- eventSource = null;
1813
- }
1814
- sseActive = false;
1815
- }
1816
-
1817
- function enableEventStreamOrPolling() {
1818
- if (globalThis.window.EventSource) {
1819
- setupEventStream();
1820
- } else if (!sseActive && autoRefreshEnabled) {
1821
- refreshTimer = setInterval(fetchData, 30000); // Commented out - SSE should be primary
1822
- }
1823
- }
1824
-
1825
- function createPauseIcon(icon) {
1826
- // Clear existing content
1827
- while (icon.firstChild) {
1828
- icon.firstChild.remove();
1829
- }
1830
- // Create pause icon
1831
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1832
- svg.setAttribute('viewBox', '0 0 24 24');
1833
- const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1834
- rect1.setAttribute('x', '6');
1835
- rect1.setAttribute('y', '4');
1836
- rect1.setAttribute('width', '4');
1837
- rect1.setAttribute('height', '16');
1838
- const rect2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1839
- rect2.setAttribute('x', '14');
1840
- rect2.setAttribute('y', '4');
1841
- rect2.setAttribute('width', '4');
1842
- rect2.setAttribute('height', '16');
1843
- svg.appendChild(rect1);
1844
- svg.appendChild(rect2);
1845
- icon.appendChild(svg);
1846
- }
1847
-
1848
- function createPlayIcon(icon) {
1849
- // Clear existing content
1850
- while (icon.firstChild) {
1851
- icon.firstChild.remove();
1852
- }
1853
- // Create play icon
1854
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1855
- svg.setAttribute('viewBox', '0 0 24 24');
1856
- const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
1857
- polygon.setAttribute('points', '5 3 19 12 5 21 5 3');
1858
- svg.appendChild(polygon);
1859
- icon.appendChild(svg);
1860
- }
1861
-
1862
- function updateRefreshButton(enabled) {
1863
- const btn = document.getElementById('auto-refresh-toggle');
1864
- const icon = document.getElementById('auto-refresh-icon');
1865
- const label = document.getElementById('auto-refresh-label');
1866
-
1867
- if (!btn || !icon || !label) return;
1868
-
1869
- if (enabled) {
1870
- label.textContent = 'Pause Refresh';
1871
- createPauseIcon(icon);
1872
- } else {
1873
- label.textContent = 'Auto Refresh';
1874
- createPlayIcon(icon);
1875
- }
1876
- }
1877
-
1878
- // Auto-refresh - Refactored to reduce complexity
1879
- function setAutoRefresh(enabled) {
1880
- autoRefreshEnabled = enabled;
1881
- localStorage.setItem(AUTO_REFRESH_KEY, enabled.toString());
1882
-
1883
- clearRefreshTimer();
1884
-
1885
- if (!enabled) {
1886
- disableEventStream();
1887
- }
1888
-
1889
- if (enabled) {
1890
- enableEventStreamOrPolling();
1891
- }
1892
-
1893
- updateRefreshButton(enabled);
1894
- }
1895
-
1896
- // Event listeners
1897
- document.addEventListener('DOMContentLoaded', () => {
1898
- // Initialize theme
1899
- currentTheme = getPreferredTheme();
1900
- applyTheme(currentTheme);
1901
- document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
1902
-
1903
- // Set up event listeners
1904
- document.getElementById('status-filter').addEventListener('change', () => {
1905
- currentPage = 1;
1906
- setupEventStream(); // Reconnect SSE with new filters
1907
- fetchData(); // Enable for search/filter
1908
- });
1909
- document.getElementById('driver-filter').addEventListener('change', () => {
1910
- currentPage = 1;
1911
- setupEventStream(); // Reconnect SSE with new filters
1912
- fetchData(); // Enable for search/filter
1913
- });
1914
- document.getElementById('sort-select').addEventListener('change', () => {
1915
- currentPage = 1;
1916
- setupEventStream(); // Reconnect SSE with new sort
1917
- fetchData(); // Enable for sorting
1918
- });
1919
-
1920
- const searchBtn = document.getElementById('search-btn');
1921
- if (searchBtn) {
1922
- searchBtn.addEventListener('click', () => {
1923
- currentPage = 1;
1924
- setupEventStream(); // Reconnect SSE with new search
1925
- fetchData(); // Enable for search
1926
- });
1927
- }
1928
- document.getElementById('search-input').addEventListener('keypress', (e) => {
1929
- if (e.key === 'Enter') {
1930
- currentPage = 1;
1931
- setupEventStream(); // Reconnect SSE with new search
1932
- fetchData(); // Enable for search
1933
- }
1934
- });
1935
-
1936
- // Initialize auto-refresh
1937
- const storedAutoRefresh = localStorage.getItem(AUTO_REFRESH_KEY);
1938
- if (storedAutoRefresh === null) {
1939
- // Only use default if no value is stored
1940
- setAutoRefresh(true);
1941
- } else {
1942
- // Use stored value
1943
- setAutoRefresh(storedAutoRefresh === 'true');
1944
- }
1945
- setupEventStream();
1946
- // SSE should handle initial data loading
1947
- globalThis.window.addEventListener('beforeunload', () => {
1948
- if (eventSource) {
1949
- eventSource.close();
1950
- }
1951
- });
1952
- });