@zintrust/workers 0.4.4 → 0.4.34
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 +26 -0
- package/dist/BroadcastWorker.d.ts +5 -0
- package/dist/NotificationWorker.d.ts +5 -0
- package/dist/WorkerFactory.d.ts +8 -0
- package/dist/WorkerFactory.js +334 -32
- package/dist/WorkerInit.d.ts +17 -0
- package/dist/WorkerInit.js +54 -2
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +557 -0
- package/dist/createQueueWorker.d.ts +5 -0
- package/dist/createQueueWorker.js +26 -10
- package/dist/dashboard/workers-api.js +46 -8
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/register.d.ts +3 -3
- package/dist/register.js +10 -4
- package/dist/ui/workers/index.html +202 -0
- package/dist/ui/workers/main.js +1952 -0
- package/dist/ui/workers/styles.css +1350 -0
- package/dist/ui/workers/zintrust.svg +30 -0
- package/package.json +11 -4
- package/src/WorkerFactory.ts +483 -36
- package/src/WorkerInit.ts +81 -3
- package/src/createQueueWorker.ts +44 -14
- package/src/dashboard/workers-api.ts +60 -13
- package/src/index.ts +9 -3
- package/src/register.ts +13 -8
|
@@ -191,6 +191,9 @@ async function getWorkersFromMixedPersistence(offset, limit, query) {
|
|
|
191
191
|
...transformToWorkerData(dbRecords, 'database'),
|
|
192
192
|
...transformToWorkerData(redisRecords, 'redis'),
|
|
193
193
|
];
|
|
194
|
+
if (workers.length === 0) {
|
|
195
|
+
return getWorkersFromFileFallback(limit, query.includeInactive === true);
|
|
196
|
+
}
|
|
194
197
|
return {
|
|
195
198
|
workers,
|
|
196
199
|
total: dbRecords.length + redisRecords.length >= limit
|
|
@@ -216,6 +219,9 @@ async function getWorkersFromSinglePersistence(persistenceDriver, offset, limit,
|
|
|
216
219
|
try {
|
|
217
220
|
const normalizedDriver = normalizeDriver(persistenceDriver);
|
|
218
221
|
const driverRecords = await WorkerFactory.listPersistedRecords({ driver: normalizedDriver }, { offset, limit, includeInactive: query.includeInactive });
|
|
222
|
+
if (driverRecords.length === 0) {
|
|
223
|
+
return getWorkersFromFileFallback(limit, query.includeInactive === true);
|
|
224
|
+
}
|
|
219
225
|
const workers = transformToWorkerData(driverRecords, normalizedDriver);
|
|
220
226
|
return {
|
|
221
227
|
workers,
|
|
@@ -236,6 +242,31 @@ async function getWorkersFromSinglePersistence(persistenceDriver, offset, limit,
|
|
|
236
242
|
};
|
|
237
243
|
}
|
|
238
244
|
}
|
|
245
|
+
async function getWorkersFromFileFallback(limit, includeInactive) {
|
|
246
|
+
try {
|
|
247
|
+
const discovered = await WorkerFactory.listFileBackedRecords();
|
|
248
|
+
const filtered = includeInactive
|
|
249
|
+
? discovered
|
|
250
|
+
: discovered.filter((record) => record.activeStatus !== false);
|
|
251
|
+
return {
|
|
252
|
+
workers: transformToWorkerData(filtered, 'memory'),
|
|
253
|
+
total: filtered.length,
|
|
254
|
+
drivers: getAvailableDriversFromDrivers(['memory']),
|
|
255
|
+
effectiveLimit: limit,
|
|
256
|
+
prePaginated: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
Logger.debug('File-backed worker fallback failed', error);
|
|
261
|
+
return {
|
|
262
|
+
workers: [],
|
|
263
|
+
total: 0,
|
|
264
|
+
drivers: getAvailableDriversFromDrivers(['memory']),
|
|
265
|
+
effectiveLimit: limit,
|
|
266
|
+
prePaginated: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
239
270
|
const normalizeDriver = (driver) => {
|
|
240
271
|
if (driver === 'db' || driver === 'database')
|
|
241
272
|
return 'database';
|
|
@@ -402,18 +433,18 @@ async function getQueueData() {
|
|
|
402
433
|
// Get queue statistics based on QUEUE_DRIVER
|
|
403
434
|
switch (queueDriver) {
|
|
404
435
|
case 'redis':
|
|
405
|
-
return getRedisQueueData();
|
|
436
|
+
return await getRedisQueueData();
|
|
406
437
|
case 'database':
|
|
407
|
-
return getDatabaseQueueData();
|
|
438
|
+
return await getDatabaseQueueData();
|
|
408
439
|
case 'db':
|
|
409
|
-
return getDatabaseQueueData();
|
|
440
|
+
return await getDatabaseQueueData();
|
|
410
441
|
default:
|
|
411
|
-
return getMemoryQueueData();
|
|
442
|
+
return await getMemoryQueueData();
|
|
412
443
|
}
|
|
413
444
|
}
|
|
414
445
|
catch (error) {
|
|
415
446
|
Logger.error('Error fetching queue data:', error);
|
|
416
|
-
return getMemoryQueueData();
|
|
447
|
+
return await getMemoryQueueData();
|
|
417
448
|
}
|
|
418
449
|
}
|
|
419
450
|
async function getRedisQueueData() {
|
|
@@ -472,13 +503,13 @@ async function getDatabaseQueueData() {
|
|
|
472
503
|
const { useEnsureDbConnected } = await import('@zintrust/core');
|
|
473
504
|
const db = await useEnsureDbConnected();
|
|
474
505
|
// Get queue statistics from actual database tables using proper query builder
|
|
475
|
-
const queueStats =
|
|
506
|
+
const queueStats = await db
|
|
476
507
|
.table('queue_jobs')
|
|
477
508
|
.select('COUNT(DISTINCT queue) as totalQueues')
|
|
478
509
|
.selectAs('COUNT(*)', 'totalJobs')
|
|
479
510
|
.selectAs('SUM(CASE WHEN reserved_at IS NOT NULL AND failed_at IS NULL THEN 1 ELSE 0 END)', 'processingJobs')
|
|
480
511
|
.selectAs('SUM(CASE WHEN failed_at IS NOT NULL THEN 1 ELSE 0 END)', 'failedJobs')
|
|
481
|
-
.first()
|
|
512
|
+
.first();
|
|
482
513
|
const stats = queueStats || {
|
|
483
514
|
totalQueues: 0,
|
|
484
515
|
totalJobs: 0,
|
|
@@ -582,7 +613,8 @@ async function enrichWithDetails(workers) {
|
|
|
582
613
|
async function buildWorkerDetails(worker) {
|
|
583
614
|
try {
|
|
584
615
|
const persistenceOverride = resolvePersistenceOverride(worker.driver);
|
|
585
|
-
const persisted = await WorkerFactory.getPersisted(worker.name, persistenceOverride)
|
|
616
|
+
const persisted = (await WorkerFactory.getPersisted(worker.name, persistenceOverride)) ??
|
|
617
|
+
(await WorkerFactory.getFileBackedRecord(worker.name));
|
|
586
618
|
const health = await getWorkerHealthSnapshot(worker.name, worker.health);
|
|
587
619
|
const metrics = await getWorkerMetricsSnapshot(worker.name, worker);
|
|
588
620
|
const configuration = buildWorkerConfiguration(worker, persisted);
|
|
@@ -730,6 +762,12 @@ export async function getWorkerDetails(name, driver) {
|
|
|
730
762
|
worker = buildWorkerFromRecord(record, normalizedDriver);
|
|
731
763
|
}
|
|
732
764
|
}
|
|
765
|
+
if (!worker) {
|
|
766
|
+
const fileBacked = await WorkerFactory.getFileBackedRecord(name);
|
|
767
|
+
if (fileBacked) {
|
|
768
|
+
worker = buildWorkerFromRecord(fileBacked, 'memory');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
733
771
|
if (!worker) {
|
|
734
772
|
throw ErrorFactory.createWorkerError(`Worker ${name} not found`);
|
|
735
773
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { MultiQueueWorker } from './MultiQueueWorker';
|
|
|
24
24
|
export { WorkerVersioning } from './WorkerVersioning';
|
|
25
25
|
export { WorkerFactory } from './WorkerFactory';
|
|
26
26
|
export type { ProcessorResolver, WorkerFactoryConfig, WorkerPersistenceConfig, } from './WorkerFactory';
|
|
27
|
-
export { WorkerInit } from './WorkerInit';
|
|
27
|
+
export { buildFileBackedAutoStartTasks, selectAutoStartNames, selectAutoStartTasks, WorkerInit, } from './WorkerInit';
|
|
28
28
|
export { WorkerShutdown } from './WorkerShutdown';
|
|
29
29
|
export { WorkerController } from './http/WorkerController';
|
|
30
30
|
export { registerWorkerRoutes } from './routes/workers';
|
|
@@ -32,11 +32,12 @@ export { BroadcastWorker } from './BroadcastWorker';
|
|
|
32
32
|
export { createQueueWorker } from './createQueueWorker';
|
|
33
33
|
export type { CreateQueueWorkerOptions } from './createQueueWorker';
|
|
34
34
|
export { NotificationWorker } from './NotificationWorker';
|
|
35
|
-
export type { RedisConfig, WorkerAutoScalingConfig, WorkerComplianceConfig, WorkerConfig, WorkerCostConfig, WorkerObservabilityConfig,
|
|
35
|
+
export type { RedisConfig, WorkerAutoScalingConfig, WorkerComplianceConfig, WorkerConfig, WorkerCostConfig, WorkerObservabilityConfig, WorkersConfigOverrides, WorkersGlobalConfig, WorkerStatus, WorkerVersioningConfig, } from '@zintrust/core';
|
|
36
36
|
export type { Job, Worker, WorkerOptions } from 'bullmq';
|
|
37
37
|
export type { IAnomaly, IAnomalyConfig, IForecast, IMetric, IPrediction, IRecommendation, IRootCauseAnalysis, } from './AnomalyDetection';
|
|
38
38
|
export type { IChaosComparison, IChaosExperiment, IChaosReport, IChaosStatus, } from './ChaosEngineering';
|
|
39
39
|
export type { ISLAConfig, ISLAReport, ISLAStatus, ISLAViolation, ITimeRange } from './SLAMonitor';
|
|
40
|
+
export type * from './config/workerConfig';
|
|
40
41
|
export type * from './type';
|
|
41
42
|
/**
|
|
42
43
|
* Package version and build metadata
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ export { MultiQueueWorker } from './MultiQueueWorker.js';
|
|
|
31
31
|
export { WorkerVersioning } from './WorkerVersioning.js';
|
|
32
32
|
// Factory & Lifecycle
|
|
33
33
|
export { WorkerFactory } from './WorkerFactory.js';
|
|
34
|
-
export { WorkerInit } from './WorkerInit.js';
|
|
34
|
+
export { buildFileBackedAutoStartTasks, selectAutoStartNames, selectAutoStartTasks, WorkerInit, } from './WorkerInit.js';
|
|
35
35
|
export { WorkerShutdown } from './WorkerShutdown.js';
|
|
36
36
|
// HTTP Controllers & Routes
|
|
37
37
|
export { WorkerController } from './http/WorkerController.js';
|
package/dist/register.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
type Registry = {
|
|
2
|
+
register: (id: string, provider: CliCommandProvider) => void;
|
|
3
|
+
};
|
|
1
4
|
type CliCommandProvider = {
|
|
2
5
|
getCommand: () => unknown;
|
|
3
6
|
name?: string;
|
|
4
7
|
};
|
|
5
|
-
type Registry = {
|
|
6
|
-
register: (id: string, provider: CliCommandProvider) => void;
|
|
7
|
-
};
|
|
8
8
|
export declare function registerWorkerCliCommands(registry: Registry): void;
|
|
9
9
|
export {};
|
package/dist/register.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const commandModule = (await (async () => {
|
|
2
|
-
|
|
2
|
+
const workerCommandsSpecifier = '@zintrust/core/worker-commands';
|
|
3
|
+
try {
|
|
4
|
+
return (await import(workerCommandsSpecifier));
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return (await import('@zintrust/core/cli'));
|
|
8
|
+
}
|
|
3
9
|
})());
|
|
4
10
|
const getWorkerProviders = () => {
|
|
5
11
|
const { WorkerCommands } = commandModule;
|
|
@@ -27,9 +33,9 @@ registerWorkerCliCommands({
|
|
|
27
33
|
},
|
|
28
34
|
});
|
|
29
35
|
try {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
registerWorkerCliCommands(
|
|
36
|
+
const coreCli = (await import('@zintrust/core/cli'));
|
|
37
|
+
if (coreCli.OptionalCliCommandRegistry !== undefined) {
|
|
38
|
+
registerWorkerCliCommands(coreCli.OptionalCliCommandRegistry);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
catch {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ZinTrust Workers Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="workers/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="container">
|
|
11
|
+
<div class="header">
|
|
12
|
+
<div class="header-top">
|
|
13
|
+
<div style="display: flex; align-items: center; gap: 16px">
|
|
14
|
+
<div class="logo-frame">
|
|
15
|
+
<img src="workers/zintrust.svg" alt="ZinTrust" class="logo-img" />
|
|
16
|
+
</div>
|
|
17
|
+
<h1>ZinTrust Workers</h1>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="header-actions">
|
|
20
|
+
<button id="theme-toggle" class="theme-toggle">
|
|
21
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
22
|
+
<circle cx="12" cy="12" r="5" />
|
|
23
|
+
<path
|
|
24
|
+
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
Theme
|
|
28
|
+
</button>
|
|
29
|
+
<button id="auto-refresh-toggle" class="btn" onclick="toggleAutoRefresh()">
|
|
30
|
+
<svg id="auto-refresh-icon" class="icon" viewBox="0 0 24 24">
|
|
31
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
32
|
+
</svg>
|
|
33
|
+
<span id="auto-refresh-label">Auto Refresh</span>
|
|
34
|
+
</button>
|
|
35
|
+
<button class="btn" onclick="fetchData()">
|
|
36
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
37
|
+
<path d="M23 4v6h-6" />
|
|
38
|
+
<path d="M1 20v-6h6" />
|
|
39
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
40
|
+
</svg>
|
|
41
|
+
Refresh
|
|
42
|
+
</button>
|
|
43
|
+
<button class="btn btn-primary" onclick="showAddWorkerModal()">
|
|
44
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
45
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
46
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
47
|
+
</svg>
|
|
48
|
+
Add Worker
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="nav-bar">
|
|
54
|
+
<nav class="nav-links">
|
|
55
|
+
<a href="/queue-monitor" class="nav-link">Queue Monitor</a>
|
|
56
|
+
<a href="/telemetry" class="nav-link">Telemetry</a>
|
|
57
|
+
<a href="/metrics" class="nav-link">Metrics</a>
|
|
58
|
+
</nav>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="filters-bar">
|
|
62
|
+
<div class="filter-group">
|
|
63
|
+
<span>Status:</span>
|
|
64
|
+
<select id="status-filter">
|
|
65
|
+
<option value="">All Status</option>
|
|
66
|
+
<option value="running">Running</option>
|
|
67
|
+
<option value="stopped">Stopped</option>
|
|
68
|
+
<option value="error">Error</option>
|
|
69
|
+
<option value="paused">Paused</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="filter-group">
|
|
73
|
+
<span>Driver:</span>
|
|
74
|
+
<select id="driver-filter">
|
|
75
|
+
<option value="">All Drivers</option>
|
|
76
|
+
</select>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="filter-group">
|
|
79
|
+
<span>Sort:</span>
|
|
80
|
+
<select id="sort-select">
|
|
81
|
+
<option value="name">Sort by Name</option>
|
|
82
|
+
<option value="status" selected>Sort by Status</option>
|
|
83
|
+
<option value="driver">Sort by Driver</option>
|
|
84
|
+
<option value="health">Sort by Health</option>
|
|
85
|
+
<option value="version">Sort by Version</option>
|
|
86
|
+
<option value="processed">Sort by Performance</option>
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
<div style="flex-grow: 1"></div>
|
|
90
|
+
<div class="search-box">
|
|
91
|
+
<svg class="search-icon" viewBox="0 0 24 24">
|
|
92
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
93
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
94
|
+
</svg>
|
|
95
|
+
<input type="text" id="search-input" placeholder="Search workers..." />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div id="loading" style="text-align: center; padding: 40px; color: var(--muted)">
|
|
101
|
+
<div>Loading workers...</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
id="error"
|
|
106
|
+
style="display: none; text-align: center; padding: 40px; color: var(--danger)"
|
|
107
|
+
>
|
|
108
|
+
<div>Failed to load workers data</div>
|
|
109
|
+
<button class="btn" onclick="fetchData()" style="margin-top: 16px">Retry</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div id="workers-content" style="display: none">
|
|
113
|
+
<div class="summary-bar" id="queue-summary">
|
|
114
|
+
<div class="summary-item">
|
|
115
|
+
<span class="summary-label">Queue Driver</span>
|
|
116
|
+
<span class="summary-value" id="queue-driver">-</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="summary-item">
|
|
119
|
+
<span class="summary-label">Queues</span>
|
|
120
|
+
<span class="summary-value" id="queue-total">0</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="summary-item">
|
|
123
|
+
<span class="summary-label">Jobs</span>
|
|
124
|
+
<span class="summary-value" id="queue-jobs">0</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="summary-item">
|
|
127
|
+
<span class="summary-label">Processing</span>
|
|
128
|
+
<span class="summary-value" id="queue-processing">0</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="summary-item">
|
|
131
|
+
<span class="summary-label">Failed</span>
|
|
132
|
+
<span class="summary-value" id="queue-failed">0</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="summary-item">
|
|
135
|
+
<span class="summary-label">Drivers</span>
|
|
136
|
+
<div class="drivers-list" id="drivers-list"></div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="table-container">
|
|
140
|
+
<div class="table-wrapper">
|
|
141
|
+
<table>
|
|
142
|
+
<thead>
|
|
143
|
+
<tr>
|
|
144
|
+
<th style="width: 250px">Worker</th>
|
|
145
|
+
<th style="width: 120px">Status</th>
|
|
146
|
+
<th style="width: 120px">Health</th>
|
|
147
|
+
<th style="width: 100px">Driver</th>
|
|
148
|
+
<th style="width: 100px">Version</th>
|
|
149
|
+
<th style="width: 320px">Performance</th>
|
|
150
|
+
<th style="width: 180px">Actions</th>
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody id="workers-tbody">
|
|
154
|
+
<!-- Workers will be populated here -->
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="pagination">
|
|
160
|
+
<div class="pagination-info" id="pagination-info">Showing 0-0 of 0 workers</div>
|
|
161
|
+
<div class="pagination-controls">
|
|
162
|
+
<button class="page-btn" id="prev-btn" onclick="loadPage('prev')" disabled>
|
|
163
|
+
<svg
|
|
164
|
+
viewBox="0 0 24 24"
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke="currentColor"
|
|
167
|
+
stroke-linecap="round"
|
|
168
|
+
stroke-linejoin="round"
|
|
169
|
+
>
|
|
170
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
<div id="page-numbers" style="display: flex; gap: 8px"></div>
|
|
174
|
+
<button class="page-btn" id="next-btn" onclick="loadPage('next')" disabled>
|
|
175
|
+
<svg
|
|
176
|
+
viewBox="0 0 24 24"
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
stroke-linecap="round"
|
|
180
|
+
stroke-linejoin="round"
|
|
181
|
+
>
|
|
182
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
183
|
+
</svg>
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<div class="page-size-selector">
|
|
187
|
+
<span>Show:</span>
|
|
188
|
+
<select id="limit-select" onchange="changeLimit(this.value)">
|
|
189
|
+
<option value="10">10</option>
|
|
190
|
+
<option value="25">25</option>
|
|
191
|
+
<option value="50">50</option>
|
|
192
|
+
<option value="100">100</option>
|
|
193
|
+
</select>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<script src="workers/main.js"></script>
|
|
201
|
+
</body>
|
|
202
|
+
</html>
|