@zintrust/workers 0.1.29 → 0.1.30
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 +16 -1
- package/dist/AnomalyDetection.d.ts +4 -0
- package/dist/AnomalyDetection.js +8 -0
- package/dist/BroadcastWorker.d.ts +2 -0
- package/dist/CanaryController.js +49 -5
- package/dist/ChaosEngineering.js +13 -0
- package/dist/ClusterLock.js +21 -10
- package/dist/DeadLetterQueue.js +12 -8
- package/dist/MultiQueueWorker.d.ts +1 -1
- package/dist/MultiQueueWorker.js +12 -7
- package/dist/NotificationWorker.d.ts +2 -0
- package/dist/PriorityQueue.d.ts +2 -2
- package/dist/PriorityQueue.js +20 -21
- package/dist/ResourceMonitor.js +65 -38
- package/dist/WorkerFactory.d.ts +23 -3
- package/dist/WorkerFactory.js +420 -40
- package/dist/WorkerInit.js +8 -3
- package/dist/WorkerMetrics.d.ts +2 -1
- package/dist/WorkerMetrics.js +152 -93
- package/dist/WorkerRegistry.d.ts +6 -0
- package/dist/WorkerRegistry.js +70 -1
- package/dist/WorkerShutdown.d.ts +21 -0
- package/dist/WorkerShutdown.js +82 -9
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +171 -99
- package/dist/createQueueWorker.d.ts +2 -0
- package/dist/createQueueWorker.js +42 -27
- package/dist/dashboard/types.d.ts +5 -0
- package/dist/dashboard/workers-api.js +136 -43
- package/dist/http/WorkerApiController.js +1 -0
- package/dist/http/WorkerController.js +133 -85
- package/dist/http/WorkerMonitoringService.d.ts +11 -0
- package/dist/http/WorkerMonitoringService.js +62 -0
- package/dist/http/middleware/CustomValidation.js +1 -1
- package/dist/http/middleware/EditWorkerValidation.d.ts +1 -1
- package/dist/http/middleware/EditWorkerValidation.js +7 -6
- package/dist/http/middleware/ProcessorPathSanitizer.js +101 -35
- package/dist/http/middleware/WorkerValidationChain.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/routes/workers.js +48 -6
- package/dist/storage/WorkerStore.d.ts +4 -1
- package/dist/storage/WorkerStore.js +55 -7
- package/dist/telemetry/api/TelemetryAPI.d.ts +46 -0
- package/dist/telemetry/api/TelemetryAPI.js +219 -0
- package/dist/telemetry/api/TelemetryMonitoringService.d.ts +17 -0
- package/dist/telemetry/api/TelemetryMonitoringService.js +113 -0
- package/dist/telemetry/components/AlertPanel.d.ts +1 -0
- package/dist/telemetry/components/AlertPanel.js +13 -0
- package/dist/telemetry/components/CostTracking.d.ts +1 -0
- package/dist/telemetry/components/CostTracking.js +14 -0
- package/dist/telemetry/components/ResourceUsageChart.d.ts +1 -0
- package/dist/telemetry/components/ResourceUsageChart.js +11 -0
- package/dist/telemetry/components/WorkerHealthChart.d.ts +1 -0
- package/dist/telemetry/components/WorkerHealthChart.js +11 -0
- package/dist/telemetry/index.d.ts +15 -0
- package/dist/telemetry/index.js +60 -0
- package/dist/telemetry/routes/dashboard.d.ts +6 -0
- package/dist/telemetry/routes/dashboard.js +608 -0
- package/dist/ui/router/EmbeddedAssets.d.ts +4 -0
- package/dist/ui/router/EmbeddedAssets.js +13 -0
- package/dist/ui/router/ui.js +100 -4
- package/package.json +10 -6
- package/src/AnomalyDetection.ts +9 -0
- package/src/CanaryController.ts +41 -5
- package/src/ChaosEngineering.ts +14 -0
- package/src/ClusterLock.ts +22 -9
- package/src/DeadLetterQueue.ts +13 -8
- package/src/MultiQueueWorker.ts +15 -8
- package/src/PriorityQueue.ts +21 -22
- package/src/ResourceMonitor.ts +72 -40
- package/src/WorkerFactory.ts +545 -49
- package/src/WorkerInit.ts +8 -3
- package/src/WorkerMetrics.ts +183 -105
- package/src/WorkerRegistry.ts +80 -1
- package/src/WorkerShutdown.ts +115 -9
- package/src/WorkerShutdownDurableObject.ts +64 -0
- package/src/createQueueWorker.ts +73 -30
- package/src/dashboard/types.ts +5 -0
- package/src/dashboard/workers-api.ts +165 -52
- package/src/http/WorkerApiController.ts +1 -0
- package/src/http/WorkerController.ts +167 -90
- package/src/http/WorkerMonitoringService.ts +77 -0
- package/src/http/middleware/CustomValidation.ts +1 -1
- package/src/http/middleware/EditWorkerValidation.ts +7 -6
- package/src/http/middleware/ProcessorPathSanitizer.ts +123 -36
- package/src/http/middleware/WorkerValidationChain.ts +1 -0
- package/src/index.ts +6 -1
- package/src/routes/workers.ts +66 -9
- package/src/storage/WorkerStore.ts +59 -9
- package/src/telemetry/api/TelemetryAPI.ts +292 -0
- package/src/telemetry/api/TelemetryMonitoringService.ts +149 -0
- package/src/telemetry/components/AlertPanel.ts +13 -0
- package/src/telemetry/components/CostTracking.ts +14 -0
- package/src/telemetry/components/ResourceUsageChart.ts +11 -0
- package/src/telemetry/components/WorkerHealthChart.ts +11 -0
- package/src/telemetry/index.ts +121 -0
- package/src/telemetry/public/assets/zintrust-logo.svg +15 -0
- package/src/telemetry/routes/dashboard.ts +638 -0
- package/src/telemetry/styles/tailwind.css +1 -0
- package/src/telemetry/styles/zintrust-theme.css +8 -0
- package/src/ui/router/EmbeddedAssets.ts +13 -0
- package/src/ui/router/ui.ts +112 -5
- package/src/ui/workers/index.html +2 -2
- package/src/ui/workers/main.js +232 -61
- package/src/ui/workers/zintrust.svg +30 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +0 -3
- package/dist/dashboard/workers-dashboard-ui.js +0 -1026
- package/dist/dashboard/workers-dashboard.d.ts +0 -4
- package/dist/dashboard/workers-dashboard.js +0 -904
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { renderAlertPanel } from '../components/AlertPanel';
|
|
2
|
+
import { renderCostTracking } from '../components/CostTracking';
|
|
3
|
+
import { renderResourceUsageChart } from '../components/ResourceUsageChart';
|
|
4
|
+
import { renderWorkerHealthChart } from '../components/WorkerHealthChart';
|
|
5
|
+
const getDashboardColorStyles = () => `
|
|
6
|
+
:root {
|
|
7
|
+
--bg: #0b1220;
|
|
8
|
+
--card: rgba(15, 23, 42, 0.75);
|
|
9
|
+
--border: rgba(148, 163, 184, 0.2);
|
|
10
|
+
--text: #f1f5f9;
|
|
11
|
+
--muted: #94a3b8;
|
|
12
|
+
--accent: #38bdf8;
|
|
13
|
+
--accent-strong: #0ea5e9;
|
|
14
|
+
--success: #6ee7b7;
|
|
15
|
+
--warn: #fbbf24;
|
|
16
|
+
--danger: #fecaca;
|
|
17
|
+
--danger-strong: #ef4444;
|
|
18
|
+
}
|
|
19
|
+
html[data-theme="light"] {
|
|
20
|
+
--bg: #f8fafc;
|
|
21
|
+
--card: #ffffff;
|
|
22
|
+
--border: #e2e8f0;
|
|
23
|
+
--text: #0f172a;
|
|
24
|
+
--muted: #475569;
|
|
25
|
+
--accent: #0284c7;
|
|
26
|
+
--accent-strong: #0369a1;
|
|
27
|
+
--success: #16a34a;
|
|
28
|
+
--warn: #d97706;
|
|
29
|
+
--danger: #dc2626;
|
|
30
|
+
--danger-strong: #dc2626;
|
|
31
|
+
}
|
|
32
|
+
body {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-family: 'Inter', ui-sans-serif, system-ui;
|
|
35
|
+
background: var(--bg);
|
|
36
|
+
color: var(--text);
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
const getDashboardLayoutStyles = () => `
|
|
40
|
+
.zt-page { min-height: 100vh; padding: 32px 24px; }
|
|
41
|
+
.zt-container { max-width: 72rem; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
|
|
42
|
+
.zt-header { display: flex; flex-direction: column; gap: 16px; }
|
|
43
|
+
.zt-brand { display: flex; align-items: center; gap: 16px; }
|
|
44
|
+
.zt-brand-icon {
|
|
45
|
+
height: 34px;
|
|
46
|
+
width: 34px;
|
|
47
|
+
border-radius: 9px;
|
|
48
|
+
border: 1px solid rgba(14, 165, 233, 0.35);
|
|
49
|
+
background: linear-gradient(180deg, rgba(14, 165, 233, 0.18), rgba(2, 132, 199, 0.1));
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
}
|
|
54
|
+
.zt-kicker { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.2em; color: var(--muted); }
|
|
55
|
+
.zt-title { font-size: 1.5rem; font-weight: 600; color: var(--text); margin: 0; }
|
|
56
|
+
.zt-subtitle { font-size: 0.875rem; color: var(--muted); margin: 0.25rem 0 0; }
|
|
57
|
+
.zt-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-size: 0.75rem; color: var(--muted); }
|
|
58
|
+
.zt-nav { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
59
|
+
.zt-nav-link {
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
color: var(--text);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
padding: 0.4rem 0.75rem;
|
|
64
|
+
border-radius: 0.6rem;
|
|
65
|
+
font-size: 0.75rem;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
transition: border-color 0.2s ease, color 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
.zt-nav-link:hover { border-color: var(--accent); color: var(--accent); }
|
|
70
|
+
#last-updated { min-width: 9rem; font-variant-numeric: tabular-nums; }
|
|
71
|
+
.zt-button {
|
|
72
|
+
border: 1px solid var(--border);
|
|
73
|
+
background: var(--card);
|
|
74
|
+
color: var(--text);
|
|
75
|
+
padding: 0.5rem 1rem;
|
|
76
|
+
border-radius: 0.75rem;
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
transition: border-color 0.2s ease, color 0.2s ease;
|
|
81
|
+
}
|
|
82
|
+
.zt-button:hover { border-color: var(--accent); color: var(--accent); }
|
|
83
|
+
.zt-alert {
|
|
84
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
85
|
+
background: rgba(239, 68, 68, 0.1);
|
|
86
|
+
color: var(--danger);
|
|
87
|
+
padding: 0.75rem 1rem;
|
|
88
|
+
border-radius: 1rem;
|
|
89
|
+
font-size: 0.875rem;
|
|
90
|
+
}
|
|
91
|
+
.hidden { display: none; }
|
|
92
|
+
.zt-grid { display: grid; gap: 16px; }
|
|
93
|
+
.zt-grid-3 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
|
94
|
+
.zt-card { background: var(--card); border: 1px solid var(--border); border-radius: 1rem; padding: 1.25rem; }
|
|
95
|
+
.zt-card-header { display: flex; align-items: center; justify-content: space-between; }
|
|
96
|
+
.zt-card-title { font-size: 0.875rem; font-weight: 600; color: var(--text); margin: 0; }
|
|
97
|
+
.zt-card-meta { font-size: 0.75rem; color: var(--muted); }
|
|
98
|
+
.zt-card-value { margin-top: 0.75rem; font-size: 1.875rem; font-weight: 600; }
|
|
99
|
+
.zt-card-subvalue { margin-top: 0.35rem; font-size: 0.875rem; color: var(--muted); }
|
|
100
|
+
.zt-card-body { margin-top: 1rem; }
|
|
101
|
+
.zt-chart { margin-top: 1rem; height: 10rem; width: 100%; }
|
|
102
|
+
.zt-cost-value { font-size: 1.875rem; font-weight: 600; color: var(--success); margin: 0; }
|
|
103
|
+
.zt-alert-list { margin: 1rem 0 0; padding: 0; list-style: none; display: grid; gap: 0.75rem; font-size: 0.875rem; color: var(--text); }
|
|
104
|
+
.zt-alert-item { border-radius: 0.75rem; border: 1px solid var(--border); background: rgba(15, 23, 42, 0.6); padding: 0.5rem 0.75rem; }
|
|
105
|
+
html[data-theme="light"] .zt-alert-item { background: #f8fafc; }
|
|
106
|
+
.zt-text-emerald { color: var(--success); }
|
|
107
|
+
.zt-text-amber { color: var(--warn); }
|
|
108
|
+
`;
|
|
109
|
+
const getDashboardResponsiveStyles = () => `
|
|
110
|
+
@media (min-width: 640px) {
|
|
111
|
+
.zt-header { flex-direction: row; align-items: center; justify-content: space-between; }
|
|
112
|
+
}
|
|
113
|
+
@media (min-width: 768px) {
|
|
114
|
+
.zt-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
const getDashboardStyles = () => `
|
|
118
|
+
<style>
|
|
119
|
+
${getDashboardColorStyles()}
|
|
120
|
+
${getDashboardLayoutStyles()}
|
|
121
|
+
${getDashboardResponsiveStyles()}
|
|
122
|
+
</style>`;
|
|
123
|
+
const getLogo = () => `
|
|
124
|
+
<svg width="26" height="26" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
125
|
+
<defs>
|
|
126
|
+
<linearGradient id="zt-telemetry" x1="10" y1="50" x2="90" y2="50" gradientUnits="userSpaceOnUse">
|
|
127
|
+
<stop stop-color="#22c55e" />
|
|
128
|
+
<stop offset="1" stop-color="#38bdf8" />
|
|
129
|
+
</linearGradient>
|
|
130
|
+
</defs>
|
|
131
|
+
<circle cx="50" cy="50" r="34" stroke="rgba(255,255,255,0.16)" stroke-width="4" />
|
|
132
|
+
<ellipse cx="50" cy="50" rx="40" ry="18" stroke="url(#zt-telemetry)" stroke-width="4" />
|
|
133
|
+
<ellipse cx="50" cy="50" rx="18" ry="40" stroke="url(#zt-telemetry)" stroke-width="4" opacity="0.75" />
|
|
134
|
+
<circle cx="50" cy="50" r="6" fill="url(#zt-telemetry)" />
|
|
135
|
+
<path d="M40 52C35 52 32 49 32 44C32 39 35 36 40 36H48" stroke="white" stroke-width="6" stroke-linecap="round" />
|
|
136
|
+
<path d="M60 48C65 48 68 51 68 56C68 61 65 64 60 64H52" stroke="white" stroke-width="6" stroke-linecap="round" />
|
|
137
|
+
<path d="M44 50H56" stroke="rgba(255,255,255,0.22)" stroke-width="6" stroke-linecap="round" />
|
|
138
|
+
</svg>`;
|
|
139
|
+
const getDashboardHead = () => `
|
|
140
|
+
<head>
|
|
141
|
+
<meta charset="UTF-8" />
|
|
142
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
143
|
+
<title>ZinTrust Telemetry Dashboard</title>
|
|
144
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
145
|
+
${getDashboardStyles()}
|
|
146
|
+
</head>`;
|
|
147
|
+
const getDashboardHeader = () => `
|
|
148
|
+
<header class="zt-header">
|
|
149
|
+
<div class="zt-brand">
|
|
150
|
+
<div class="zt-brand-icon">
|
|
151
|
+
${getLogo()}
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<p class="zt-kicker">ZinTrust</p>
|
|
155
|
+
<h1 class="zt-title">Telemetry Dashboard</h1>
|
|
156
|
+
<p class="zt-subtitle">Unified view of worker health and performance</p>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="zt-actions">
|
|
160
|
+
<span id="last-updated"></span>
|
|
161
|
+
<nav class="zt-nav">
|
|
162
|
+
<a class="zt-nav-link" href="/queue-monitor/">Queue monitor</a>
|
|
163
|
+
<a class="zt-nav-link" href="/workers">Workers</a>
|
|
164
|
+
<a class="zt-nav-link" href="/metrics">Metrics</a>
|
|
165
|
+
</nav>
|
|
166
|
+
<button id="refresh-btn" class="zt-button">Refresh</button>
|
|
167
|
+
<button id="auto-refresh-btn" class="zt-button">Pause auto refresh</button>
|
|
168
|
+
<button id="theme-toggle" class="zt-button" aria-label="Toggle theme">Light mode</button>
|
|
169
|
+
</div>
|
|
170
|
+
</header>`;
|
|
171
|
+
const getDashboardStats = () => `
|
|
172
|
+
<section class="zt-grid zt-grid-3">
|
|
173
|
+
<div class="zt-card">
|
|
174
|
+
<p class="zt-kicker">Total Workers</p>
|
|
175
|
+
<p id="total-workers" class="zt-card-value">0</p>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="zt-card">
|
|
178
|
+
<p class="zt-kicker">Healthy</p>
|
|
179
|
+
<p id="healthy-workers" class="zt-card-value zt-text-emerald">0</p>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="zt-card">
|
|
182
|
+
<p class="zt-kicker">Needs Attention</p>
|
|
183
|
+
<p id="attention-workers" class="zt-card-value zt-text-amber">0</p>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="zt-card">
|
|
186
|
+
<p class="zt-kicker">CPU (System / Process)</p>
|
|
187
|
+
<p id="system-cpu" class="zt-card-value">0%</p>
|
|
188
|
+
<p id="process-cpu" class="zt-card-subvalue">Process: 0% (0 ms)</p>
|
|
189
|
+
</div>
|
|
190
|
+
</section>`;
|
|
191
|
+
const getDashboardCharts = () => `
|
|
192
|
+
<section class="zt-grid zt-grid-3">
|
|
193
|
+
${renderWorkerHealthChart()}
|
|
194
|
+
${renderResourceUsageChart()}
|
|
195
|
+
${renderCostTracking()}
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
${renderAlertPanel()}`;
|
|
199
|
+
const getDashboardBody = () => `
|
|
200
|
+
<div class="zt-page">
|
|
201
|
+
<div class="zt-container">
|
|
202
|
+
${getDashboardHeader()}
|
|
203
|
+
|
|
204
|
+
<section id="error" class="hidden zt-alert"></section>
|
|
205
|
+
|
|
206
|
+
${getDashboardStats()}
|
|
207
|
+
|
|
208
|
+
${getDashboardCharts()}
|
|
209
|
+
</div>
|
|
210
|
+
</div>`;
|
|
211
|
+
const getDashboardScriptHelpers = () => `
|
|
212
|
+
const formatPercent = (value) => {
|
|
213
|
+
if (!Number.isFinite(value)) return '0%';
|
|
214
|
+
return value.toFixed(1) + '%';
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const formatMs = (value) => {
|
|
218
|
+
if (!Number.isFinite(value)) return '0 ms';
|
|
219
|
+
return Math.max(0, Math.round(value)) + ' ms';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const getProcessCpuPercent = (cpuUsage, timestamp, cores) => {
|
|
223
|
+
if (!cpuUsage || !timestamp || !cores) return { percent: 0, deltaMs: 0 };
|
|
224
|
+
|
|
225
|
+
const currentTotal = (cpuUsage.user || 0) + (cpuUsage.system || 0);
|
|
226
|
+
const currentTs = Number.isFinite(timestamp) ? timestamp : Date.parse(timestamp);
|
|
227
|
+
|
|
228
|
+
if (!Number.isFinite(currentTs)) {
|
|
229
|
+
return { percent: 0, deltaMs: 0 };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!lastProcessCpu || !Number.isFinite(lastProcessTs)) {
|
|
233
|
+
lastProcessCpu = currentTotal;
|
|
234
|
+
lastProcessTs = currentTs;
|
|
235
|
+
return { percent: 0, deltaMs: 0 };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const deltaUsage = currentTotal - lastProcessCpu;
|
|
239
|
+
const deltaMs = Math.max(0, currentTs - lastProcessTs);
|
|
240
|
+
const normalized = deltaMs > 0 ? (deltaUsage / 1000) / (deltaMs * Math.max(1, cores)) : 0;
|
|
241
|
+
const percent = Math.max(0, Math.min(100, normalized * 100));
|
|
242
|
+
|
|
243
|
+
lastProcessCpu = currentTotal;
|
|
244
|
+
lastProcessTs = currentTs;
|
|
245
|
+
|
|
246
|
+
return { percent, deltaMs: deltaUsage / 1000 };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const readStorage = (key) => {
|
|
250
|
+
try {
|
|
251
|
+
return localStorage.getItem(key);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const writeStorage = (key, value) => {
|
|
258
|
+
try {
|
|
259
|
+
localStorage.setItem(key, value);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const setAutoLabel = () => {
|
|
266
|
+
autoBtn.textContent = autoRefresh ? 'Pause auto refresh' : 'Resume auto refresh';
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const applyTheme = (theme) => {
|
|
270
|
+
currentTheme = theme;
|
|
271
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
272
|
+
themeBtn.textContent = theme === 'dark' ? 'Light mode' : 'Dark mode';
|
|
273
|
+
writeStorage(STORAGE_KEYS.theme, theme);
|
|
274
|
+
};
|
|
275
|
+
`;
|
|
276
|
+
const getDashboardScriptState = (options) => {
|
|
277
|
+
// Ensure basePath is a valid string with explicit fallback
|
|
278
|
+
let basePath;
|
|
279
|
+
if (options && typeof options.basePath === 'string' && options.basePath.length > 0) {
|
|
280
|
+
basePath = options.basePath;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
basePath = '/telemetry';
|
|
284
|
+
}
|
|
285
|
+
// Strip trailing slashes safely
|
|
286
|
+
if (basePath && typeof basePath === 'string') {
|
|
287
|
+
while (basePath.endsWith('/')) {
|
|
288
|
+
basePath = basePath.slice(0, -1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Ensure values are safe for template interpolation
|
|
292
|
+
const autoRefresh = options && typeof options.autoRefresh === 'boolean' ? options.autoRefresh : false;
|
|
293
|
+
const refreshInterval = options &&
|
|
294
|
+
typeof options.refreshIntervalMs === 'number' &&
|
|
295
|
+
Number.isFinite(options.refreshIntervalMs)
|
|
296
|
+
? Math.max(1000, Math.floor(options.refreshIntervalMs))
|
|
297
|
+
: 10000;
|
|
298
|
+
return `
|
|
299
|
+
const API_BASE = '${basePath}';
|
|
300
|
+
const AUTO_REFRESH = ${autoRefresh ? 'true' : 'false'};
|
|
301
|
+
const REFRESH_INTERVAL = ${refreshInterval};
|
|
302
|
+
const STORAGE_KEYS = {
|
|
303
|
+
autoRefresh: 'zintrust.telemetry.autoRefresh',
|
|
304
|
+
theme: 'zintrust.telemetry.theme',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const errorEl = document.getElementById('error');
|
|
308
|
+
const totalEl = document.getElementById('total-workers');
|
|
309
|
+
const healthyEl = document.getElementById('healthy-workers');
|
|
310
|
+
const attentionEl = document.getElementById('attention-workers');
|
|
311
|
+
const systemCpuEl = document.getElementById('system-cpu');
|
|
312
|
+
const processCpuEl = document.getElementById('process-cpu');
|
|
313
|
+
const costEl = document.getElementById('costTotal');
|
|
314
|
+
const refreshBtn = document.getElementById('refresh-btn');
|
|
315
|
+
const autoBtn = document.getElementById('auto-refresh-btn');
|
|
316
|
+
const themeBtn = document.getElementById('theme-toggle');
|
|
317
|
+
const lastUpdated = document.getElementById('last-updated');
|
|
318
|
+
|
|
319
|
+
let autoRefresh = AUTO_REFRESH;
|
|
320
|
+
let autoTimer = null;
|
|
321
|
+
let currentTheme = 'dark';
|
|
322
|
+
let eventSource = null;
|
|
323
|
+
let sseActive = false;
|
|
324
|
+
let lastProcessCpu = null;
|
|
325
|
+
let lastProcessTs = null;
|
|
326
|
+
|
|
327
|
+
${getDashboardScriptHelpers()}
|
|
328
|
+
`;
|
|
329
|
+
};
|
|
330
|
+
const getDashboardScriptCharts = () => `
|
|
331
|
+
const setError = (message) => {
|
|
332
|
+
if (!message) {
|
|
333
|
+
errorEl.classList.add('hidden');
|
|
334
|
+
errorEl.textContent = '';
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
errorEl.classList.remove('hidden');
|
|
338
|
+
errorEl.textContent = message;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const buildHealthChart = (labels, values) => {
|
|
342
|
+
const ctx = document.getElementById('workerHealthChart');
|
|
343
|
+
if (!ctx || !window.Chart) return;
|
|
344
|
+
return new window.Chart(ctx, {
|
|
345
|
+
type: 'line',
|
|
346
|
+
data: {
|
|
347
|
+
labels,
|
|
348
|
+
datasets: [{
|
|
349
|
+
data: values,
|
|
350
|
+
borderColor: '#38bdf8',
|
|
351
|
+
backgroundColor: 'rgba(56, 189, 248, 0.2)',
|
|
352
|
+
tension: 0.4,
|
|
353
|
+
fill: true,
|
|
354
|
+
}],
|
|
355
|
+
},
|
|
356
|
+
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } },
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const buildResourceChart = (labels, values) => {
|
|
361
|
+
const ctx = document.getElementById('resourceUsageChart');
|
|
362
|
+
if (!ctx || !window.Chart) return;
|
|
363
|
+
return new window.Chart(ctx, {
|
|
364
|
+
type: 'bar',
|
|
365
|
+
data: {
|
|
366
|
+
labels,
|
|
367
|
+
datasets: [{
|
|
368
|
+
data: values,
|
|
369
|
+
backgroundColor: ['#22c55e', '#38bdf8', '#f97316'],
|
|
370
|
+
borderRadius: 12,
|
|
371
|
+
}],
|
|
372
|
+
},
|
|
373
|
+
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } },
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
let healthChart = null;
|
|
378
|
+
let resourceChart = null;
|
|
379
|
+
|
|
380
|
+
const updateCharts = (summary, resources) => {
|
|
381
|
+
const monitoring = summary?.monitoring || { total: 0, healthy: 0, degraded: 0, critical: 0, details: [] };
|
|
382
|
+
const labels = ['Healthy', 'Degraded', 'Critical'];
|
|
383
|
+
const values = [monitoring.healthy || 0, monitoring.degraded || 0, monitoring.critical || 0];
|
|
384
|
+
|
|
385
|
+
if (healthChart) healthChart.destroy();
|
|
386
|
+
healthChart = buildHealthChart(labels, values);
|
|
387
|
+
|
|
388
|
+
const resourceLabels = ['CPU', 'Memory', 'Network'];
|
|
389
|
+
const cpu = resources?.resourceSnapshot?.cpu?.usage || 0;
|
|
390
|
+
const memory = resources?.resourceSnapshot?.memory?.usage || 0;
|
|
391
|
+
const network = resources?.resourceSnapshot?.network?.received || 0;
|
|
392
|
+
const resourceValues = [cpu, memory, Math.min(100, (network / 1024 / 1024) || 0)];
|
|
393
|
+
|
|
394
|
+
if (resourceChart) resourceChart.destroy();
|
|
395
|
+
resourceChart = buildResourceChart(resourceLabels, resourceValues);
|
|
396
|
+
};
|
|
397
|
+
`;
|
|
398
|
+
const getDashboardScriptApplySnapshot = () => `
|
|
399
|
+
const applySnapshot = (payload) => {
|
|
400
|
+
const summary = payload.summary || {};
|
|
401
|
+
const total = summary.workers || 0;
|
|
402
|
+
const monitoring = summary.monitoring || { total: 0, healthy: 0, degraded: 0, critical: 0, details: [] };
|
|
403
|
+
const healthyCount = monitoring.healthy || 0;
|
|
404
|
+
const attentionCount = (monitoring.degraded || 0) + (monitoring.critical || 0);
|
|
405
|
+
const resources = payload.resources || {};
|
|
406
|
+
const snapshot = resources.resourceSnapshot || {};
|
|
407
|
+
const cpuSnapshot = snapshot.cpu || {};
|
|
408
|
+
const processSnapshot = snapshot.process || {};
|
|
409
|
+
|
|
410
|
+
totalEl.textContent = total;
|
|
411
|
+
healthyEl.textContent = healthyCount;
|
|
412
|
+
attentionEl.textContent = attentionCount;
|
|
413
|
+
if (systemCpuEl) {
|
|
414
|
+
systemCpuEl.textContent = formatPercent(cpuSnapshot.usage || 0);
|
|
415
|
+
}
|
|
416
|
+
if (processCpuEl) {
|
|
417
|
+
const processMetrics = getProcessCpuPercent(
|
|
418
|
+
processSnapshot.cpuUsage,
|
|
419
|
+
snapshot.timestamp,
|
|
420
|
+
cpuSnapshot.cores || 1
|
|
421
|
+
);
|
|
422
|
+
processCpuEl.textContent =
|
|
423
|
+
'Process: ' + formatPercent(processMetrics.percent) + ' (' + formatMs(processMetrics.deltaMs) + ')';
|
|
424
|
+
}
|
|
425
|
+
if (costEl && payload.resources?.cost) {
|
|
426
|
+
const cost = payload.resources.cost;
|
|
427
|
+
costEl.textContent = '$' + cost.hourly?.toFixed(4) + '/hr';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
updateAlerts(summary.alerts || []);
|
|
431
|
+
updateCharts(summary, resources);
|
|
432
|
+
lastUpdated.textContent = 'Updated ' + new Date().toLocaleTimeString();
|
|
433
|
+
};
|
|
434
|
+
`;
|
|
435
|
+
const getDashboardScriptUpdateAlerts = () => `
|
|
436
|
+
const updateAlerts = (alerts) => {
|
|
437
|
+
console.log('Alerts found:', alerts.length, alerts); // Debug logging
|
|
438
|
+
const alertList = document.getElementById('alertList');
|
|
439
|
+
if (alertList) {
|
|
440
|
+
if (alerts.length === 0) {
|
|
441
|
+
alertList.innerHTML = '<li class="zt-alert-item">No alerts yet.</li>';
|
|
442
|
+
} else {
|
|
443
|
+
alertList.innerHTML = alerts.map(alert => {
|
|
444
|
+
// Handle both nested alert structure and direct alert structure
|
|
445
|
+
const alertData = alert.alert || alert;
|
|
446
|
+
console.log('Processing alert:', alertData); // Debug logging
|
|
447
|
+
|
|
448
|
+
const severity = alertData.severity || 'info';
|
|
449
|
+
const severityColor =
|
|
450
|
+
severity === 'critical' ? 'var(--danger)' :
|
|
451
|
+
severity === 'warning' ? 'var(--warn)' :
|
|
452
|
+
severity === 'info' ? 'var(--accent)' : 'var(--text)';
|
|
453
|
+
|
|
454
|
+
return '<li class="zt-alert-item">' +
|
|
455
|
+
'<div style="display: flex; justify-content: space-between; align-items: center;">' +
|
|
456
|
+
'<span style="font-weight: 600; color: ' + severityColor + '">' +
|
|
457
|
+
(alertData.message || 'Unknown alert') + '</span>' +
|
|
458
|
+
'<span style="font-size: 0.75rem; opacity: 0.7;">' +
|
|
459
|
+
(alertData.timestamp ? new Date(alertData.timestamp).toLocaleTimeString() : 'No timestamp') + '</span>' +
|
|
460
|
+
'</div>' +
|
|
461
|
+
(alertData.recommendation ? '<div style="font-size: 0.8rem; margin-top: 0.25rem; opacity: 0.8;">' + alertData.recommendation + '</div>' : '') +
|
|
462
|
+
'</li>';
|
|
463
|
+
}).join('');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
`;
|
|
468
|
+
const getDashboardScriptFetch = () => `
|
|
469
|
+
const fetchSummary = async () => {
|
|
470
|
+
setError('');
|
|
471
|
+
try {
|
|
472
|
+
const response = await fetch(API_BASE + '/api/summary');
|
|
473
|
+
if (!response.ok) throw new Error('Failed to load telemetry summary');
|
|
474
|
+
const payload = await response.json();
|
|
475
|
+
applySnapshot(payload);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
setError(error.message || 'Failed to load telemetry data');
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const connectSse = () => {
|
|
482
|
+
if (!globalThis.window.EventSource) return false;
|
|
483
|
+
|
|
484
|
+
if (eventSource) {
|
|
485
|
+
eventSource.close();
|
|
486
|
+
eventSource = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
eventSource = new globalThis.window.EventSource(API_BASE + '/api/events');
|
|
490
|
+
|
|
491
|
+
eventSource.onopen = () => {
|
|
492
|
+
sseActive = true;
|
|
493
|
+
if (autoTimer) {
|
|
494
|
+
clearInterval(autoTimer);
|
|
495
|
+
autoTimer = null;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
eventSource.onmessage = (evt) => {
|
|
500
|
+
try {
|
|
501
|
+
const payload = JSON.parse(evt.data);
|
|
502
|
+
if (payload && payload.type === 'snapshot') {
|
|
503
|
+
applySnapshot(payload);
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error('Failed to parse SSE payload', err);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
eventSource.onerror = () => {
|
|
511
|
+
if (eventSource) {
|
|
512
|
+
eventSource.close();
|
|
513
|
+
eventSource = null;
|
|
514
|
+
}
|
|
515
|
+
sseActive = false;
|
|
516
|
+
// HTTP fallback disabled - 100% SSE reliance
|
|
517
|
+
// if (autoRefresh && !autoTimer) {
|
|
518
|
+
// autoTimer = setInterval(fetchSummary, REFRESH_INTERVAL);
|
|
519
|
+
// }
|
|
520
|
+
console.log('SSE connection lost - please refresh page to reconnect');
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
return true;
|
|
524
|
+
};
|
|
525
|
+
`;
|
|
526
|
+
const getDashboardScriptControls = () => `
|
|
527
|
+
refreshBtn.addEventListener('click', () => {
|
|
528
|
+
console.log('Manual refresh - SSE only mode');
|
|
529
|
+
// SSE handles all updates, no HTTP fallback
|
|
530
|
+
if (!sseActive) {
|
|
531
|
+
connectSse();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
autoBtn.addEventListener('click', () => {
|
|
535
|
+
autoRefresh = !autoRefresh;
|
|
536
|
+
setAutoLabel();
|
|
537
|
+
writeStorage(STORAGE_KEYS.autoRefresh, String(autoRefresh));
|
|
538
|
+
if (!autoRefresh) {
|
|
539
|
+
if (autoTimer) {
|
|
540
|
+
clearInterval(autoTimer);
|
|
541
|
+
autoTimer = null;
|
|
542
|
+
}
|
|
543
|
+
if (eventSource) {
|
|
544
|
+
eventSource.close();
|
|
545
|
+
eventSource = null;
|
|
546
|
+
sseActive = false;
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!connectSse()) {
|
|
552
|
+
// HTTP fallback disabled - 100% SSE reliance
|
|
553
|
+
// autoTimer = setInterval(fetchSummary, REFRESH_INTERVAL);
|
|
554
|
+
console.log('SSE connection failed - please check server');
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
themeBtn.addEventListener('click', () => {
|
|
559
|
+
const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
560
|
+
applyTheme(nextTheme);
|
|
561
|
+
});
|
|
562
|
+
`;
|
|
563
|
+
const getDashboardScriptBootstrap = () => `
|
|
564
|
+
const storedAuto = readStorage(STORAGE_KEYS.autoRefresh);
|
|
565
|
+
if (storedAuto !== null) {
|
|
566
|
+
autoRefresh = storedAuto === 'true';
|
|
567
|
+
}
|
|
568
|
+
setAutoLabel();
|
|
569
|
+
|
|
570
|
+
const storedTheme = readStorage(STORAGE_KEYS.theme);
|
|
571
|
+
const initialTheme = storedTheme === 'light' || storedTheme === 'dark' ? storedTheme : 'dark';
|
|
572
|
+
applyTheme(initialTheme);
|
|
573
|
+
|
|
574
|
+
if (autoRefresh) {
|
|
575
|
+
if (!connectSse()) {
|
|
576
|
+
// HTTP fallback disabled - 100% SSE reliance
|
|
577
|
+
// autoTimer = setInterval(fetchSummary, REFRESH_INTERVAL);
|
|
578
|
+
console.log('SSE connection failed - please check server');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Initial data load via SSE only - no HTTP fallback
|
|
583
|
+
// fetchSummary(); // Disabled - SSE handles initial data
|
|
584
|
+
|
|
585
|
+
window.addEventListener('beforeunload', () => {
|
|
586
|
+
if (eventSource) {
|
|
587
|
+
eventSource.close();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
`;
|
|
591
|
+
const getDashboardScript = (options) => `
|
|
592
|
+
<script>
|
|
593
|
+
${getDashboardScriptState(options)}
|
|
594
|
+
${getDashboardScriptCharts()}
|
|
595
|
+
${getDashboardScriptApplySnapshot()}
|
|
596
|
+
${getDashboardScriptUpdateAlerts()}
|
|
597
|
+
${getDashboardScriptFetch()}
|
|
598
|
+
${getDashboardScriptControls()}
|
|
599
|
+
${getDashboardScriptBootstrap()}
|
|
600
|
+
</script>`;
|
|
601
|
+
export const getDashboardHtml = (options) => `<!DOCTYPE html>
|
|
602
|
+
<html lang="en">
|
|
603
|
+
${getDashboardHead()}
|
|
604
|
+
<body>
|
|
605
|
+
${getDashboardBody()}
|
|
606
|
+
${getDashboardScript(options)}
|
|
607
|
+
</body>
|
|
608
|
+
</html>`;
|