@zintrust/workers 0.1.27

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