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