@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.
- package/README.md +861 -0
- package/dist/AnomalyDetection.d.ts +102 -0
- package/dist/AnomalyDetection.js +321 -0
- package/dist/AutoScaler.d.ts +127 -0
- package/dist/AutoScaler.js +425 -0
- package/dist/BroadcastWorker.d.ts +21 -0
- package/dist/BroadcastWorker.js +24 -0
- package/dist/CanaryController.d.ts +103 -0
- package/dist/CanaryController.js +380 -0
- package/dist/ChaosEngineering.d.ts +79 -0
- package/dist/ChaosEngineering.js +216 -0
- package/dist/CircuitBreaker.d.ts +106 -0
- package/dist/CircuitBreaker.js +374 -0
- package/dist/ClusterLock.d.ts +90 -0
- package/dist/ClusterLock.js +385 -0
- package/dist/ComplianceManager.d.ts +177 -0
- package/dist/ComplianceManager.js +556 -0
- package/dist/DatacenterOrchestrator.d.ts +133 -0
- package/dist/DatacenterOrchestrator.js +404 -0
- package/dist/DeadLetterQueue.d.ts +122 -0
- package/dist/DeadLetterQueue.js +539 -0
- package/dist/HealthMonitor.d.ts +42 -0
- package/dist/HealthMonitor.js +301 -0
- package/dist/MultiQueueWorker.d.ts +89 -0
- package/dist/MultiQueueWorker.js +277 -0
- package/dist/NotificationWorker.d.ts +21 -0
- package/dist/NotificationWorker.js +23 -0
- package/dist/Observability.d.ts +153 -0
- package/dist/Observability.js +530 -0
- package/dist/PluginManager.d.ts +123 -0
- package/dist/PluginManager.js +392 -0
- package/dist/PriorityQueue.d.ts +117 -0
- package/dist/PriorityQueue.js +244 -0
- package/dist/ResourceMonitor.d.ts +164 -0
- package/dist/ResourceMonitor.js +605 -0
- package/dist/SLAMonitor.d.ts +110 -0
- package/dist/SLAMonitor.js +274 -0
- package/dist/WorkerFactory.d.ts +193 -0
- package/dist/WorkerFactory.js +1507 -0
- package/dist/WorkerInit.d.ts +85 -0
- package/dist/WorkerInit.js +223 -0
- package/dist/WorkerMetrics.d.ts +114 -0
- package/dist/WorkerMetrics.js +509 -0
- package/dist/WorkerRegistry.d.ts +145 -0
- package/dist/WorkerRegistry.js +319 -0
- package/dist/WorkerShutdown.d.ts +61 -0
- package/dist/WorkerShutdown.js +159 -0
- package/dist/WorkerVersioning.d.ts +107 -0
- package/dist/WorkerVersioning.js +300 -0
- package/dist/build-manifest.json +462 -0
- package/dist/config/workerConfig.d.ts +3 -0
- package/dist/config/workerConfig.js +19 -0
- package/dist/createQueueWorker.d.ts +23 -0
- package/dist/createQueueWorker.js +113 -0
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/types.d.ts +117 -0
- package/dist/dashboard/types.js +1 -0
- package/dist/dashboard/workers-api.d.ts +4 -0
- package/dist/dashboard/workers-api.js +638 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
- package/dist/dashboard/workers-dashboard-ui.js +1026 -0
- package/dist/dashboard/workers-dashboard.d.ts +4 -0
- package/dist/dashboard/workers-dashboard.js +904 -0
- package/dist/helper/index.d.ts +5 -0
- package/dist/helper/index.js +10 -0
- package/dist/http/WorkerApiController.d.ts +38 -0
- package/dist/http/WorkerApiController.js +312 -0
- package/dist/http/WorkerController.d.ts +374 -0
- package/dist/http/WorkerController.js +1351 -0
- package/dist/http/middleware/CustomValidation.d.ts +92 -0
- package/dist/http/middleware/CustomValidation.js +270 -0
- package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
- package/dist/http/middleware/DatacenterValidator.js +94 -0
- package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
- package/dist/http/middleware/EditWorkerValidation.js +55 -0
- package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
- package/dist/http/middleware/FeaturesValidator.js +60 -0
- package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
- package/dist/http/middleware/InfrastructureValidator.js +226 -0
- package/dist/http/middleware/OptionsValidator.d.ts +3 -0
- package/dist/http/middleware/OptionsValidator.js +112 -0
- package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
- package/dist/http/middleware/PayloadSanitizer.js +42 -0
- package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
- package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
- package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/QueueNameSanitizer.js +45 -0
- package/dist/http/middleware/ValidateDriver.d.ts +7 -0
- package/dist/http/middleware/ValidateDriver.js +20 -0
- package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
- package/dist/http/middleware/VersionSanitizer.js +25 -0
- package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
- package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
- package/dist/http/middleware/WorkerValidationChain.js +185 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/routes/workers.d.ts +12 -0
- package/dist/routes/workers.js +81 -0
- package/dist/storage/WorkerStore.d.ts +45 -0
- package/dist/storage/WorkerStore.js +195 -0
- package/dist/type.d.ts +76 -0
- package/dist/type.js +1 -0
- package/dist/ui/router/ui.d.ts +3 -0
- package/dist/ui/router/ui.js +83 -0
- package/dist/ui/types/worker-ui.d.ts +229 -0
- package/dist/ui/types/worker-ui.js +5 -0
- package/package.json +53 -0
- package/src/AnomalyDetection.ts +434 -0
- package/src/AutoScaler.ts +654 -0
- package/src/BroadcastWorker.ts +34 -0
- package/src/CanaryController.ts +531 -0
- package/src/ChaosEngineering.ts +301 -0
- package/src/CircuitBreaker.ts +495 -0
- package/src/ClusterLock.ts +499 -0
- package/src/ComplianceManager.ts +815 -0
- package/src/DatacenterOrchestrator.ts +561 -0
- package/src/DeadLetterQueue.ts +733 -0
- package/src/HealthMonitor.ts +390 -0
- package/src/MultiQueueWorker.ts +431 -0
- package/src/NotificationWorker.ts +33 -0
- package/src/Observability.ts +696 -0
- package/src/PluginManager.ts +551 -0
- package/src/PriorityQueue.ts +351 -0
- package/src/ResourceMonitor.ts +769 -0
- package/src/SLAMonitor.ts +408 -0
- package/src/WorkerFactory.ts +2108 -0
- package/src/WorkerInit.ts +313 -0
- package/src/WorkerMetrics.ts +709 -0
- package/src/WorkerRegistry.ts +443 -0
- package/src/WorkerShutdown.ts +210 -0
- package/src/WorkerVersioning.ts +422 -0
- package/src/config/workerConfig.ts +25 -0
- package/src/createQueueWorker.ts +174 -0
- package/src/dashboard/index.ts +6 -0
- package/src/dashboard/types.ts +141 -0
- package/src/dashboard/workers-api.ts +785 -0
- package/src/dashboard/zintrust.svg +30 -0
- package/src/helper/index.ts +11 -0
- package/src/http/WorkerApiController.ts +369 -0
- package/src/http/WorkerController.ts +1512 -0
- package/src/http/middleware/CustomValidation.ts +360 -0
- package/src/http/middleware/DatacenterValidator.ts +124 -0
- package/src/http/middleware/EditWorkerValidation.ts +74 -0
- package/src/http/middleware/FeaturesValidator.ts +82 -0
- package/src/http/middleware/InfrastructureValidator.ts +295 -0
- package/src/http/middleware/OptionsValidator.ts +144 -0
- package/src/http/middleware/PayloadSanitizer.ts +52 -0
- package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
- package/src/http/middleware/QueueNameSanitizer.ts +55 -0
- package/src/http/middleware/ValidateDriver.ts +29 -0
- package/src/http/middleware/VersionSanitizer.ts +30 -0
- package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
- package/src/http/middleware/WorkerValidationChain.ts +230 -0
- package/src/index.ts +98 -0
- package/src/routes/workers.ts +154 -0
- package/src/storage/WorkerStore.ts +240 -0
- package/src/type.ts +89 -0
- package/src/types/queue-monitor.d.ts +38 -0
- package/src/types/queue-redis.d.ts +38 -0
- package/src/ui/README.md +13 -0
- package/src/ui/components/JsonEditor.js +670 -0
- package/src/ui/components/JsonViewer.js +387 -0
- package/src/ui/components/WorkerCard.js +178 -0
- package/src/ui/components/WorkerExpandPanel.js +257 -0
- package/src/ui/components/fetcher.js +42 -0
- package/src/ui/components/sla-scorecard.js +32 -0
- package/src/ui/components/styles.css +30 -0
- package/src/ui/components/table-expander.js +34 -0
- package/src/ui/integration/worker-ui-integration.js +565 -0
- package/src/ui/router/ui.ts +99 -0
- package/src/ui/services/workerApi.js +240 -0
- package/src/ui/types/worker-ui.ts +283 -0
- package/src/ui/utils/jsonValidator.js +444 -0
- package/src/ui/workers/index.html +202 -0
- package/src/ui/workers/main.js +1781 -0
- 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
|
+
});
|