@krishivpb60/aether-ai-cli 1.3.4 → 1.3.6

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.
@@ -0,0 +1,855 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Visual Telemetry Server & Dashboard
3
+ // Zero-dependency local server serving a cyberpunk observability HUD.
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import http from "node:http";
7
+ import { exec } from "node:child_process";
8
+ import { getTelemetryData, clearTelemetryLogs } from "./ai/telemetry.js";
9
+ import { getAIConfig } from "./config.js";
10
+
11
+ const HTML_CONTENT = `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>Aether Core // Visual Telemetry HUD</title>
17
+ <style>
18
+ /* Reset & variables */
19
+ * { box-sizing: border-box; margin: 0; padding: 0; }
20
+ :root {
21
+ --bg: #070a13;
22
+ --panel-bg: rgba(13, 19, 38, 0.7);
23
+ --border: #1f2d5a;
24
+ --text: #a9b2c3;
25
+ --text-bright: #e2e8f0;
26
+ --cyan: #00f0ff;
27
+ --magenta: #ff007f;
28
+ --green: #39ff14;
29
+ --orange: #ffaa00;
30
+ --red: #ff3b30;
31
+ --font-mono: 'Roboto Mono', 'Courier New', monospace;
32
+ }
33
+ body {
34
+ background-color: var(--bg);
35
+ background-image: linear-gradient(rgba(31, 45, 90, 0.1) 1px, transparent 1px),
36
+ linear-gradient(90deg, rgba(31, 45, 90, 0.1) 1px, transparent 1px);
37
+ background-size: 20px 20px;
38
+ color: var(--text);
39
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
40
+ padding: 24px;
41
+ min-height: 100vh;
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 20px;
45
+ }
46
+ /* Scrollbar */
47
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
48
+ ::-webkit-scrollbar-track { background: var(--bg); }
49
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
50
+ ::-webkit-scrollbar-thumb:hover { background: var(--cyan); }
51
+
52
+ /* HUD Header */
53
+ header {
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ border-bottom: 2px solid var(--border);
58
+ padding-bottom: 12px;
59
+ margin-bottom: 10px;
60
+ }
61
+ .hud-title {
62
+ font-family: var(--font-mono);
63
+ font-weight: 800;
64
+ font-size: 1.5rem;
65
+ letter-spacing: 2px;
66
+ color: var(--cyan);
67
+ text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 10px;
71
+ }
72
+ .hud-title::before {
73
+ content: "";
74
+ display: inline-block;
75
+ width: 12px;
76
+ height: 12px;
77
+ background-color: var(--cyan);
78
+ box-shadow: 0 0 8px var(--cyan);
79
+ border-radius: 50%;
80
+ }
81
+ .system-status {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 15px;
85
+ font-family: var(--font-mono);
86
+ font-size: 0.85rem;
87
+ }
88
+ .badge {
89
+ padding: 4px 8px;
90
+ border-radius: 4px;
91
+ font-size: 0.75rem;
92
+ font-weight: bold;
93
+ border: 1px solid transparent;
94
+ }
95
+ .badge-cyan {
96
+ background: rgba(0, 240, 255, 0.1);
97
+ color: var(--cyan);
98
+ border-color: var(--cyan);
99
+ }
100
+ .badge-green {
101
+ background: rgba(57, 255, 20, 0.1);
102
+ color: var(--green);
103
+ border-color: var(--green);
104
+ animation: pulse 2s infinite;
105
+ }
106
+ .badge-danger {
107
+ background: rgba(255, 59, 48, 0.1);
108
+ color: var(--red);
109
+ border-color: var(--red);
110
+ cursor: pointer;
111
+ }
112
+ .badge-danger:hover {
113
+ background: var(--red);
114
+ color: white;
115
+ }
116
+
117
+ @keyframes pulse {
118
+ 0% { opacity: 0.6; box-shadow: 0 0 0 0 rgba(57, 255, 20, 0.4); }
119
+ 70% { opacity: 1; box-shadow: 0 0 0 6px rgba(57, 255, 20, 0); }
120
+ 100% { opacity: 0.6; box-shadow: 0 0 0 0 rgba(57, 255, 20, 0); }
121
+ }
122
+
123
+ /* Grid layout */
124
+ .dashboard-grid {
125
+ display: grid;
126
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
127
+ gap: 16px;
128
+ }
129
+ .card {
130
+ background: var(--panel-bg);
131
+ border: 1px solid var(--border);
132
+ border-radius: 8px;
133
+ padding: 16px;
134
+ position: relative;
135
+ overflow: hidden;
136
+ backdrop-filter: blur(8px);
137
+ transition: all 0.3s ease;
138
+ }
139
+ .card::before {
140
+ content: "";
141
+ position: absolute;
142
+ top: 0; left: 0; width: 4px; height: 100%;
143
+ background: var(--border);
144
+ transition: background 0.3s;
145
+ }
146
+ .card:hover {
147
+ border-color: var(--cyan);
148
+ box-shadow: 0 0 15px rgba(0, 240, 255, 0.15);
149
+ transform: translateY(-2px);
150
+ }
151
+ .card:hover::before {
152
+ background: var(--cyan);
153
+ }
154
+ .card-label {
155
+ font-size: 0.75rem;
156
+ font-weight: 600;
157
+ text-transform: uppercase;
158
+ letter-spacing: 1px;
159
+ color: var(--text);
160
+ margin-bottom: 6px;
161
+ }
162
+ .card-value {
163
+ font-size: 1.8rem;
164
+ font-weight: 700;
165
+ color: var(--text-bright);
166
+ font-family: var(--font-mono);
167
+ }
168
+ .card-subtext {
169
+ font-size: 0.75rem;
170
+ color: #64748b;
171
+ margin-top: 4px;
172
+ }
173
+
174
+ /* Two Columns Panels */
175
+ .panels-row {
176
+ display: grid;
177
+ grid-template-columns: 1fr 1fr;
178
+ gap: 20px;
179
+ }
180
+ @media (max-width: 900px) {
181
+ .panels-row { grid-template-columns: 1fr; }
182
+ }
183
+
184
+ .panel {
185
+ background: var(--panel-bg);
186
+ border: 1px solid var(--border);
187
+ border-radius: 8px;
188
+ padding: 20px;
189
+ backdrop-filter: blur(8px);
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 16px;
193
+ }
194
+ .panel-header {
195
+ display: flex;
196
+ justify-content: space-between;
197
+ align-items: center;
198
+ border-bottom: 1px solid var(--border);
199
+ padding-bottom: 8px;
200
+ }
201
+ .panel-title {
202
+ font-family: var(--font-mono);
203
+ font-size: 0.95rem;
204
+ font-weight: bold;
205
+ color: var(--cyan);
206
+ text-transform: uppercase;
207
+ letter-spacing: 1px;
208
+ }
209
+
210
+ /* Topology styles */
211
+ .topology-list {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 12px;
215
+ max-height: 250px;
216
+ overflow-y: auto;
217
+ padding-right: 4px;
218
+ }
219
+ .topology-item {
220
+ display: flex;
221
+ justify-content: space-between;
222
+ align-items: center;
223
+ padding: 8px 12px;
224
+ background: rgba(31, 45, 90, 0.2);
225
+ border: 1px solid var(--border);
226
+ border-radius: 4px;
227
+ font-size: 0.85rem;
228
+ }
229
+ .topology-name {
230
+ font-weight: 600;
231
+ color: var(--text-bright);
232
+ }
233
+ .topology-model {
234
+ font-family: var(--font-mono);
235
+ font-size: 0.75rem;
236
+ color: #64748b;
237
+ }
238
+ .topology-status {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 6px;
242
+ font-family: var(--font-mono);
243
+ font-size: 0.75rem;
244
+ }
245
+ .dot {
246
+ width: 8px;
247
+ height: 8px;
248
+ border-radius: 50%;
249
+ display: inline-block;
250
+ }
251
+ .dot-online { background-color: var(--green); box-shadow: 0 0 6px var(--green); }
252
+ .dot-offline { background-color: #64748b; }
253
+
254
+ /* Chart / SVG Container */
255
+ .chart-container {
256
+ width: 100%;
257
+ height: 200px;
258
+ position: relative;
259
+ }
260
+ .chart-svg {
261
+ width: 100%;
262
+ height: 100%;
263
+ }
264
+
265
+ /* Progress bars for models */
266
+ .model-list {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: 12px;
270
+ max-height: 250px;
271
+ overflow-y: auto;
272
+ padding-right: 4px;
273
+ }
274
+ .model-item {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 4px;
278
+ }
279
+ .model-meta {
280
+ display: flex;
281
+ justify-content: space-between;
282
+ font-size: 0.8rem;
283
+ font-family: var(--font-mono);
284
+ }
285
+ .model-name-text { color: var(--text-bright); }
286
+ .model-tokens { color: var(--magenta); }
287
+ .progress-bar-bg {
288
+ height: 8px;
289
+ background: rgba(31, 45, 90, 0.4);
290
+ border-radius: 4px;
291
+ overflow: hidden;
292
+ }
293
+ .progress-bar-fill {
294
+ height: 100%;
295
+ background: linear-gradient(90deg, var(--magenta), var(--cyan));
296
+ border-radius: 4px;
297
+ width: 0%;
298
+ transition: width 0.8s ease;
299
+ }
300
+
301
+ /* Terminal Console */
302
+ .console-panel {
303
+ flex: 1;
304
+ display: flex;
305
+ flex-direction: column;
306
+ min-height: 250px;
307
+ }
308
+ .table-container {
309
+ overflow-x: auto;
310
+ flex: 1;
311
+ }
312
+ table {
313
+ width: 100%;
314
+ border-collapse: collapse;
315
+ font-family: var(--font-mono);
316
+ font-size: 0.8rem;
317
+ text-align: left;
318
+ }
319
+ th {
320
+ border-bottom: 2px solid var(--border);
321
+ padding: 8px;
322
+ color: var(--cyan);
323
+ text-transform: uppercase;
324
+ font-weight: bold;
325
+ }
326
+ td {
327
+ border-bottom: 1px solid rgba(31, 45, 90, 0.3);
328
+ padding: 8px;
329
+ }
330
+ tr:hover td {
331
+ background: rgba(0, 240, 255, 0.05);
332
+ color: var(--text-bright);
333
+ }
334
+ .status-ok { color: var(--green); font-weight: bold; }
335
+ .status-fail { color: var(--red); font-weight: bold; }
336
+
337
+ /* Action buttons */
338
+ .btn-container {
339
+ display: flex;
340
+ gap: 10px;
341
+ }
342
+ .btn {
343
+ background: rgba(31, 45, 90, 0.3);
344
+ border: 1px solid var(--border);
345
+ color: var(--text);
346
+ padding: 6px 12px;
347
+ border-radius: 4px;
348
+ cursor: pointer;
349
+ font-family: var(--font-mono);
350
+ font-size: 0.8rem;
351
+ transition: all 0.2s;
352
+ }
353
+ .btn:hover {
354
+ border-color: var(--cyan);
355
+ color: var(--cyan);
356
+ box-shadow: 0 0 8px rgba(0, 240, 255, 0.3);
357
+ }
358
+ .btn-clear:hover {
359
+ border-color: var(--magenta);
360
+ color: var(--magenta);
361
+ box-shadow: 0 0 8px rgba(255, 0, 127, 0.3);
362
+ }
363
+ .btn-refresh {
364
+ background: rgba(0, 240, 255, 0.1);
365
+ border-color: var(--cyan);
366
+ color: var(--cyan);
367
+ }
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <header>
372
+ <div class="hud-title">Aether Core // Telemetry HUD</div>
373
+ <div class="system-status">
374
+ <span class="badge badge-cyan" id="node-count">NODES: 0</span>
375
+ <span class="badge badge-green">LIVE STREAMING</span>
376
+ <span class="badge badge-danger" onclick="shutdownServer()">SHUTDOWN HUD</span>
377
+ </div>
378
+ </header>
379
+
380
+ <div class="dashboard-grid">
381
+ <div class="card">
382
+ <div class="card-label">Avg Mesh Latency</div>
383
+ <div class="card-value" id="stat-latency">--</div>
384
+ <div class="card-subtext">Across success queries</div>
385
+ </div>
386
+ <div class="card">
387
+ <div class="card-label">Success Rate</div>
388
+ <div class="card-value" id="stat-success-rate">--</div>
389
+ <div class="card-subtext" id="stat-success-fraction">0 / 0 runs</div>
390
+ </div>
391
+ <div class="card">
392
+ <div class="card-label">Cumulative Tokens</div>
393
+ <div class="card-value" id="stat-tokens">--</div>
394
+ <div class="card-subtext" id="stat-tokens-breakdown">I: 0 | O: 0</div>
395
+ </div>
396
+ <div class="card">
397
+ <div class="card-label">Active Mesh Nodes</div>
398
+ <div class="card-value" id="stat-mesh-nodes">--</div>
399
+ <div class="card-subtext" id="stat-mesh-ratio">0 configured</div>
400
+ </div>
401
+ </div>
402
+
403
+ <div class="panels-row">
404
+ <!-- Failover Mesh Topology Panel -->
405
+ <div class="panel">
406
+ <div class="panel-header">
407
+ <div class="panel-title">Failover Mesh Nodes</div>
408
+ <div id="mesh-status-indicator" class="topology-status"><span class="dot dot-online"></span>ONLINE</div>
409
+ </div>
410
+ <div class="topology-list" id="topology-list">
411
+ <!-- populated by js -->
412
+ </div>
413
+ </div>
414
+
415
+ <!-- Latency History Panel -->
416
+ <div class="panel">
417
+ <div class="panel-header">
418
+ <div class="panel-title">Latency Timeline (ms)</div>
419
+ <div class="topology-status" id="latency-span">Past queries</div>
420
+ </div>
421
+ <div class="chart-container" id="chart-container">
422
+ <!-- SVG injected by JS -->
423
+ </div>
424
+ </div>
425
+ </div>
426
+
427
+ <div class="panels-row">
428
+ <!-- Model Breakdown Panel -->
429
+ <div class="panel">
430
+ <div class="panel-header">
431
+ <div class="panel-title">Model Token Breakdown</div>
432
+ </div>
433
+ <div class="model-list" id="model-list">
434
+ <!-- populated by js -->
435
+ </div>
436
+ </div>
437
+
438
+ <!-- Quick controls / sessions -->
439
+ <div class="panel">
440
+ <div class="panel-header">
441
+ <div class="panel-title">HUD Observability Control</div>
442
+ </div>
443
+ <div style="display: flex; flex-direction: column; gap: 15px; font-size: 0.85rem; line-height: 1.5;">
444
+ <div>
445
+ This dashboard provides real-time latency diagnostics, mesh failover trace telemetry, and token accounting for Aether Core AI.
446
+ It tracks active providers, network latencies, and offline fallback routes.
447
+ </div>
448
+ <div class="btn-container">
449
+ <button class="btn btn-refresh" onclick="updateData()">Force Poll HUD</button>
450
+ <button class="btn btn-clear" onclick="clearTelemetry()">Format Logs</button>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Terminal Console Panel -->
457
+ <div class="panel console-panel">
458
+ <div class="panel-header">
459
+ <div class="panel-title">Live Telemetry Logs</div>
460
+ <div class="btn-container">
461
+ <input type="text" id="log-search" placeholder="Filter provider/model..." class="btn" style="padding: 4px 8px; cursor: text;" oninput="filterLogs()">
462
+ </div>
463
+ </div>
464
+ <div class="table-container">
465
+ <table>
466
+ <thead>
467
+ <tr>
468
+ <th>Time</th>
469
+ <th>Node / Provider</th>
470
+ <th>Model</th>
471
+ <th>Latency</th>
472
+ <th>Tokens (I/O)</th>
473
+ <th>Status</th>
474
+ </tr>
475
+ </thead>
476
+ <tbody id="logs-tbody">
477
+ <!-- populated by js -->
478
+ </tbody>
479
+ </table>
480
+ </div>
481
+ </div>
482
+
483
+ <script>
484
+ let globalData = null;
485
+
486
+ async function fetchData() {
487
+ try {
488
+ const res = await fetch('/api/telemetry');
489
+ const data = await res.json();
490
+ globalData = data;
491
+ renderDashboard(data);
492
+ } catch (err) {
493
+ console.error("Failed to fetch telemetry data", err);
494
+ }
495
+ }
496
+
497
+ function renderDashboard(data) {
498
+ const logs = data.latencyLogs || [];
499
+ const mesh = data.meshStructure || [];
500
+ const tokenStats = data.tokenStats || { prompt: 0, completion: 0, total: 0, exchanges: 0 };
501
+ const modelBreakdown = data.modelBreakdown || {};
502
+
503
+ // 1. Calculate historical metrics from logs (cumulative across CLI runs)
504
+ const successRuns = logs.filter(l => l.success);
505
+ const totalRuns = logs.length;
506
+ const successRate = totalRuns > 0 ? Math.round((successRuns.length / totalRuns) * 100) : 100;
507
+
508
+ const totalSuccessLatency = successRuns.reduce((sum, l) => sum + l.latencyMs, 0);
509
+ const avgLatency = successRuns.length > 0 ? Math.round(totalSuccessLatency / successRuns.length) : null;
510
+
511
+ // Cumulative tokens in the logs
512
+ const logPromptTokens = logs.reduce((sum, l) => sum + (l.promptTokens || 0), 0);
513
+ const logCompletionTokens = logs.reduce((sum, l) => sum + (l.completionTokens || 0), 0);
514
+ const logTotalTokens = logPromptTokens + logCompletionTokens;
515
+
516
+ // Update stat cards
517
+ document.getElementById('stat-latency').innerText = avgLatency ? avgLatency + ' ms' : '--';
518
+ document.getElementById('stat-success-rate').innerText = successRate + '%';
519
+ document.getElementById('stat-success-fraction').innerText = successRuns.length + ' / ' + totalRuns + ' successful';
520
+
521
+ document.getElementById('stat-tokens').innerText = logTotalTokens.toLocaleString();
522
+ document.getElementById('stat-tokens-breakdown').innerText = 'I: ' + logPromptTokens.toLocaleString() + ' | O: ' + logCompletionTokens.toLocaleString();
523
+
524
+ // Mesh stats
525
+ const activeCount = mesh.filter(m => m.configured).length;
526
+ const totalCount = mesh.length;
527
+ document.getElementById('node-count').innerText = 'NODES: ' + (activeCount + 1); // +1 for local solver / companion
528
+ document.getElementById('stat-mesh-nodes').innerText = (activeCount + 1) + ' Online';
529
+ document.getElementById('stat-mesh-ratio').innerText = activeCount + ' / ' + totalCount + ' config providers';
530
+
531
+ // Update Mesh list
532
+ const topoList = document.getElementById('topology-list');
533
+ topoList.innerHTML = '';
534
+
535
+ // Always add Local Math / Fallback Node 0
536
+ topoList.appendChild(createTopologyElement({
537
+ name: "Local Solver Node",
538
+ configured: true,
539
+ defaultModel: "Offline Math + Krylo Companion",
540
+ tier: "free",
541
+ description: "Zero-latency mathematical reasoning & local assistant fallbacks."
542
+ }, "Node 0 (Local)"));
543
+
544
+ mesh.forEach((provider, idx) => {
545
+ topoList.appendChild(createTopologyElement(provider, "Node " + (idx + 1)));
546
+ });
547
+
548
+ // Update Model list
549
+ // We calculate model token usage from logs to show cumulative token distribution
550
+ const logModels = {};
551
+ logs.forEach(l => {
552
+ if (l.success && l.model) {
553
+ if (!logModels[l.model]) {
554
+ logModels[l.model] = 0;
555
+ }
556
+ logModels[l.model] += (l.promptTokens || 0) + (l.completionTokens || 0);
557
+ }
558
+ });
559
+
560
+ const modelList = document.getElementById('model-list');
561
+ modelList.innerHTML = '';
562
+
563
+ const modelKeys = Object.keys(logModels);
564
+ if (modelKeys.length === 0) {
565
+ modelList.innerHTML = '<div style="font-size: 0.8rem; color: #64748b;">No model data recorded.</div>';
566
+ } else {
567
+ const maxTokens = Math.max(...Object.values(logModels));
568
+ modelKeys.forEach(model => {
569
+ const tokens = logModels[model];
570
+ const percent = maxTokens > 0 ? (tokens / maxTokens) * 100 : 0;
571
+
572
+ const modelDiv = document.createElement('div');
573
+ modelDiv.className = 'model-item';
574
+ modelDiv.innerHTML = \`
575
+ <div class="model-meta">
576
+ <span class="model-name-text">\${model}</span>
577
+ <span class="model-tokens">\${tokens.toLocaleString()} tokens</span>
578
+ </div>
579
+ <div class="progress-bar-bg">
580
+ <div class="progress-bar-fill" style="width: \${percent}%"></div>
581
+ </div>
582
+ \`;
583
+ modelList.appendChild(modelDiv);
584
+ });
585
+ }
586
+
587
+ // Draw SVG chart
588
+ drawSvgChart(logs);
589
+
590
+ // Render logs table
591
+ renderLogsTable(logs);
592
+ }
593
+
594
+ function createTopologyElement(provider, nodeLabel) {
595
+ const el = document.createElement('div');
596
+ el.className = 'topology-item';
597
+ el.innerHTML = \`
598
+ <div>
599
+ <div class="topology-name">\${provider.name} <span style="font-size:0.75rem; color: var(--cyan); margin-left: 5px;">\${nodeLabel}</span></div>
600
+ <div class="topology-model">\${provider.defaultModel}</div>
601
+ </div>
602
+ <div class="topology-status">
603
+ <span class="dot \${provider.configured ? 'dot-online' : 'dot-offline'}"></span>
604
+ \${provider.configured ? 'ONLINE' : 'OFFLINE'}
605
+ </div>
606
+ \`;
607
+ return el;
608
+ }
609
+
610
+ function drawSvgChart(logs) {
611
+ const container = document.getElementById('chart-container');
612
+ container.innerHTML = '';
613
+
614
+ if (!logs || logs.length === 0) {
615
+ container.innerHTML = '<div style="display:flex; justify-content:center; align-items:center; height:100%; font-size:0.85rem; color:#64748b;">Waiting for telemetry trace signals...</div>';
616
+ return;
617
+ }
618
+
619
+ // Take last 20 queries for chart
620
+ const chartLogs = logs.slice(-20);
621
+ const maxLatency = Math.max(...chartLogs.map(l => l.latencyMs), 1000); // at least 1000ms ceiling
622
+
623
+ const width = container.clientWidth || 400;
624
+ const height = 180;
625
+ const padding = 25;
626
+
627
+ const chartW = width - padding * 2;
628
+ const chartH = height - padding * 2;
629
+
630
+ // Draw SVG elements
631
+ let svg = \`<svg class="chart-svg" viewBox="0 0 \${width} \${height}" xmlns="http://www.w3.org/2000/svg">\`;
632
+
633
+ // Grid lines (y)
634
+ const gridLevels = [0, 0.25, 0.5, 0.75, 1];
635
+ gridLevels.forEach(lvl => {
636
+ const y = padding + chartH * (1 - lvl);
637
+ const val = Math.round(maxLatency * lvl);
638
+ svg += \`
639
+ <line x1="\${padding}" y1="\${y}" x2="\${width - padding}" y2="\${y}" stroke="#1f2d5a" stroke-dasharray="3,3" stroke-width="1"/>
640
+ <text x="\${padding - 5}" y="\${y + 4}" fill="#64748b" font-family="monospace" font-size="8" text-anchor="end">\${val}ms</text>
641
+ \`;
642
+ });
643
+
644
+ // Calculate coordinates
645
+ const points = [];
646
+ const stepX = chartLogs.length > 1 ? chartW / (chartLogs.length - 1) : chartW;
647
+
648
+ chartLogs.forEach((log, idx) => {
649
+ const x = padding + (idx * stepX);
650
+ const y = padding + chartH * (1 - (log.latencyMs / maxLatency));
651
+ points.push({ x, y, log });
652
+ });
653
+
654
+ // Draw path (line)
655
+ if (points.length > 1) {
656
+ let pathD = \`M \${points[0].x} \${points[0].y}\`;
657
+ for (let i = 1; i < points.length; i++) {
658
+ pathD += \` L \${points[i].x} \${points[i].y}\`;
659
+ }
660
+ svg += \`<path d="\${pathD}" fill="none" stroke="var(--cyan)" stroke-width="2" style="filter: drop-shadow(0px 0px 4px rgba(0,240,255,0.5));"/>\`;
661
+ }
662
+
663
+ // Draw points
664
+ points.forEach((p, idx) => {
665
+ const color = p.log.success ? 'var(--cyan)' : 'var(--red)';
666
+ const title = \`\${p.log.provider} (\${p.log.latencyMs}ms)\`;
667
+ svg += \`
668
+ <circle cx="\${p.x}" cy="\${p.y}" r="4" fill="\${color}" stroke="var(--bg)" stroke-width="1" class="chart-point">
669
+ <title>\${title}</title>
670
+ </circle>
671
+ \`;
672
+ });
673
+
674
+ // X Axis timestamps (only draw first, middle, last to avoid overlap)
675
+ if (points.length > 0) {
676
+ const timeIndices = [0];
677
+ if (points.length > 2) timeIndices.push(Math.floor(points.length / 2));
678
+ if (points.length > 1) timeIndices.push(points.length - 1);
679
+
680
+ timeIndices.forEach(idx => {
681
+ const p = points[idx];
682
+ const d = new Date(p.log.timestamp);
683
+ const timeStr = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
684
+ svg += \`
685
+ <text x="\${p.x}" y="\${height - 5}" fill="#64748b" font-family="monospace" font-size="8" text-anchor="middle">\${timeStr}</text>
686
+ \`;
687
+ });
688
+ }
689
+
690
+ svg += \`</svg>\`;
691
+ container.innerHTML = svg;
692
+ }
693
+
694
+ function renderLogsTable(logs) {
695
+ const tbody = document.getElementById('logs-tbody');
696
+ tbody.innerHTML = '';
697
+
698
+ if (!logs || logs.length === 0) {
699
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #64748b; padding: 20px;">No telemetry frames captured yet. Run a prompt in another terminal.</td></tr>';
700
+ return;
701
+ }
702
+
703
+ const filterVal = document.getElementById('log-search').value.toLowerCase();
704
+
705
+ // Render logs in reverse order (newest first)
706
+ const reversed = [...logs].reverse();
707
+ let shownCount = 0;
708
+
709
+ reversed.forEach(log => {
710
+ const time = new Date(log.timestamp).toLocaleTimeString();
711
+ const prov = log.provider || 'unknown';
712
+ const model = log.model || 'unknown';
713
+
714
+ if (filterVal && !prov.toLowerCase().includes(filterVal) && !model.toLowerCase().includes(filterVal)) {
715
+ return;
716
+ }
717
+
718
+ shownCount++;
719
+ const row = document.createElement('tr');
720
+ const totalTok = (log.promptTokens || 0) + (log.completionTokens || 0);
721
+ const tokensText = totalTok > 0 ? \`\${totalTok} (\${log.promptTokens}/\${log.completionTokens})\` : '--';
722
+
723
+ row.innerHTML = \`
724
+ <td>\${time}</td>
725
+ <td style="color: var(--text-bright);">\${prov}</td>
726
+ <td>\${model}</td>
727
+ <td>\${log.latencyMs} ms</td>
728
+ <td>\${tokensText}</td>
729
+ <td class="\${log.success ? 'status-ok' : 'status-fail'}">\${log.success ? '[ OK ]' : '[ FAIL ]'}</td>
730
+ \`;
731
+ tbody.appendChild(row);
732
+ });
733
+
734
+ if (shownCount === 0) {
735
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #64748b; padding: 20px;">No logs match your filter.</td></tr>';
736
+ }
737
+ }
738
+
739
+ function filterLogs() {
740
+ if (globalData && globalData.latencyLogs) {
741
+ renderLogsTable(globalData.latencyLogs);
742
+ }
743
+ }
744
+
745
+ async function clearTelemetry() {
746
+ if (confirm("Are you sure you want to format and clear all persisted telemetry database?")) {
747
+ try {
748
+ await fetch('/api/clear', { method: 'POST' });
749
+ fetchData();
750
+ } catch (err) {
751
+ console.error(err);
752
+ }
753
+ }
754
+ }
755
+
756
+ async function shutdownServer() {
757
+ if (confirm("Terminate Visual Telemetry HUD local server?")) {
758
+ try {
759
+ await fetch('/api/shutdown', { method: 'POST' });
760
+ document.body.innerHTML = '<div style="display:flex; flex-direction:column; justify-content:center; align-items:center; height:90vh; gap:20px; font-family:monospace; color:var(--magenta);"><h1 style="font-size:2rem;">HUD TERMINATED</h1><p style="color:var(--text);">Local telemetry server was shut down successfully.</p></div>';
761
+ } catch (err) {
762
+ console.error(err);
763
+ }
764
+ }
765
+ }
766
+
767
+ function updateData() {
768
+ fetchData();
769
+ }
770
+
771
+ // Initial load
772
+ fetchData();
773
+
774
+ // Auto update every 2 seconds
775
+ setInterval(fetchData, 2000);
776
+ </script>
777
+ </body>
778
+ </html>
779
+ `;
780
+
781
+ /**
782
+ * Starts the telemetry server on a free port, starting at `port`.
783
+ * @param {number} port - Preferred port to start on
784
+ * @returns {Promise<{server: object, port: number}>}
785
+ */
786
+ export function startTelemetryServer(port = 5050) {
787
+ return new Promise((resolve, reject) => {
788
+ const server = http.createServer(async (req, res) => {
789
+ if (req.url === "/api/telemetry" && req.method === "GET") {
790
+ try {
791
+ const config = await getAIConfig();
792
+ const telemetryData = getTelemetryData(config);
793
+ res.writeHead(200, { "Content-Type": "application/json" });
794
+ res.end(JSON.stringify(telemetryData));
795
+ } catch (err) {
796
+ res.writeHead(500, { "Content-Type": "text/plain" });
797
+ res.end("Internal Server Error: " + err.message);
798
+ }
799
+ } else if (req.url === "/api/clear" && req.method === "POST") {
800
+ try {
801
+ clearTelemetryLogs();
802
+ res.writeHead(200, { "Content-Type": "application/json" });
803
+ res.end(JSON.stringify({ success: true }));
804
+ } catch (err) {
805
+ res.writeHead(500, { "Content-Type": "text/plain" });
806
+ res.end("Internal Server Error: " + err.message);
807
+ }
808
+ } else if (req.url === "/api/shutdown" && req.method === "POST") {
809
+ res.writeHead(200, { "Content-Type": "application/json" });
810
+ res.end(JSON.stringify({ success: true }));
811
+ setTimeout(() => {
812
+ server.close(() => {
813
+ // In unit tests, we don't want to kill the test runner process
814
+ if (process.env.NODE_ENV !== "test") {
815
+ process.exit(0);
816
+ }
817
+ });
818
+ }, 300);
819
+ } else if (req.url === "/" || req.url === "/index.html") {
820
+ res.writeHead(200, { "Content-Type": "text/html" });
821
+ res.end(HTML_CONTENT);
822
+ } else {
823
+ res.writeHead(404, { "Content-Type": "text/plain" });
824
+ res.end("Not Found");
825
+ }
826
+ });
827
+
828
+ server.on("error", (err) => {
829
+ if (err.code === "EADDRINUSE") {
830
+ resolve(startTelemetryServer(port + 1));
831
+ } else {
832
+ reject(err);
833
+ }
834
+ });
835
+
836
+ server.listen(port, () => {
837
+ resolve({ server, port });
838
+ });
839
+ });
840
+ }
841
+
842
+ /**
843
+ * Opens the telemetry HUD page in the default system browser.
844
+ * @param {string} url - The localhost URL to open
845
+ */
846
+ export function openBrowser(url) {
847
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
848
+ // On Windows, start can require a title argument before the URL
849
+ const runCmd = process.platform === "win32" ? `start "" "${url}"` : `${cmd} "${url}"`;
850
+ exec(runCmd, (err) => {
851
+ if (err) {
852
+ // Fail silently, user can open url manually printed in console
853
+ }
854
+ });
855
+ }