@nexussdk/tracker 0.0.1 → 0.0.4

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,1027 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/dev-server/server.ts
4
+ import { createServer } from "http";
5
+ import { URL } from "url";
6
+
7
+ // src/dev-server/event-store.ts
8
+ var DevServerEventStore = class {
9
+ _capacity;
10
+ _events = [];
11
+ _nextId = 1;
12
+ constructor(capacity = 1e3) {
13
+ this._capacity = Math.max(10, capacity);
14
+ }
15
+ /**
16
+ * Adds an error payload to the ring buffer.
17
+ * Assigns a monotonic ID and ISO timestamp.
18
+ * Evicts the oldest event if capacity is exceeded.
19
+ */
20
+ add(payload) {
21
+ const envelope = {
22
+ id: this._nextId++,
23
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
24
+ payload
25
+ };
26
+ if (this._events.length >= this._capacity) {
27
+ this._events.shift();
28
+ }
29
+ this._events.push(envelope);
30
+ return envelope;
31
+ }
32
+ /**
33
+ * Returns all stored event envelopes (newest first).
34
+ */
35
+ getAll() {
36
+ return [...this._events].reverse();
37
+ }
38
+ /**
39
+ * Finds a specific envelope by its server-assigned ID.
40
+ */
41
+ getById(id) {
42
+ return this._events.find((e) => e.id === id);
43
+ }
44
+ /**
45
+ * Clears all stored events and resets counter.
46
+ */
47
+ clear() {
48
+ this._events = [];
49
+ }
50
+ /**
51
+ * Current number of stored events.
52
+ */
53
+ size() {
54
+ return this._events.length;
55
+ }
56
+ /**
57
+ * Configured maximum buffer capacity.
58
+ */
59
+ get capacity() {
60
+ return this._capacity;
61
+ }
62
+ };
63
+
64
+ // src/dev-server/sse.ts
65
+ var SseBroadcaster = class {
66
+ _clients = /* @__PURE__ */ new Set();
67
+ _heartbeatInterval = null;
68
+ constructor() {
69
+ this._startHeartbeat();
70
+ }
71
+ /**
72
+ * Registers a new HTTP response as an SSE client.
73
+ * Sends appropriate SSE headers and initial handshake comment.
74
+ */
75
+ addClient(res) {
76
+ res.writeHead(200, {
77
+ "Content-Type": "text/event-stream",
78
+ "Cache-Control": "no-cache, no-transform",
79
+ Connection: "keep-alive",
80
+ "Access-Control-Allow-Origin": "*",
81
+ "X-Accel-Buffering": "no"
82
+ });
83
+ res.write(`event: connected
84
+ data: ${JSON.stringify({ timestamp: Date.now() })}
85
+
86
+ `);
87
+ this._clients.add(res);
88
+ res.on("close", () => {
89
+ this._clients.delete(res);
90
+ });
91
+ res.on("error", () => {
92
+ this._clients.delete(res);
93
+ });
94
+ }
95
+ /**
96
+ * Removes a client manually if needed.
97
+ */
98
+ removeClient(res) {
99
+ this._clients.delete(res);
100
+ }
101
+ /**
102
+ * Broadcasts an event to all currently connected clients.
103
+ * @param eventName Name of the SSE event (e.g. 'error_event', 'clear')
104
+ * @param data Payload to serialize as JSON in the data field
105
+ */
106
+ broadcast(eventName, data) {
107
+ const payload = `event: ${eventName}
108
+ data: ${JSON.stringify(data)}
109
+
110
+ `;
111
+ for (const client of this._clients) {
112
+ try {
113
+ client.write(payload);
114
+ } catch {
115
+ this._clients.delete(client);
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Number of currently connected SSE clients.
121
+ */
122
+ get clientCount() {
123
+ return this._clients.size;
124
+ }
125
+ /**
126
+ * Stops heartbeat and closes all connections.
127
+ */
128
+ destroy() {
129
+ if (this._heartbeatInterval) {
130
+ clearInterval(this._heartbeatInterval);
131
+ this._heartbeatInterval = null;
132
+ }
133
+ for (const client of this._clients) {
134
+ try {
135
+ client.end();
136
+ } catch {
137
+ }
138
+ }
139
+ this._clients.clear();
140
+ }
141
+ _startHeartbeat() {
142
+ this._heartbeatInterval = setInterval(() => {
143
+ const ping = `: ping ${Date.now()}
144
+
145
+ `;
146
+ for (const client of this._clients) {
147
+ try {
148
+ client.write(ping);
149
+ } catch {
150
+ this._clients.delete(client);
151
+ }
152
+ }
153
+ }, 15e3);
154
+ if (this._heartbeatInterval.unref) {
155
+ this._heartbeatInterval.unref();
156
+ }
157
+ }
158
+ };
159
+
160
+ // src/dev-server/gui.ts
161
+ function renderDevServerGuiHtml(port) {
162
+ return `<!DOCTYPE html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="UTF-8">
166
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
167
+ <title>Nexus Telemetry Dev Server</title>
168
+ <style>
169
+ :root {
170
+ --bg: #090d16;
171
+ --card-bg: #111827;
172
+ --border: #1f2937;
173
+ --border-focus: #374151;
174
+ --text: #f3f4f6;
175
+ --text-muted: #9ca3af;
176
+ --accent: #3b82f6;
177
+ --accent-hover: #2563eb;
178
+ --error-bg: #450a0a;
179
+ --error-text: #f87171;
180
+ --error-border: #991b1b;
181
+ --warn-bg: #451a03;
182
+ --warn-text: #fbbf24;
183
+ --warn-border: #b45309;
184
+ --info-bg: #082f49;
185
+ --info-text: #38bdf8;
186
+ --info-border: #0369a1;
187
+ --code-bg: #030712;
188
+ --green: #10b981;
189
+ --radius: 8px;
190
+ }
191
+ * { box-sizing: border-box; margin: 0; padding: 0; }
192
+ body {
193
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
194
+ background: var(--bg);
195
+ color: var(--text);
196
+ min-height: 100vh;
197
+ display: flex;
198
+ flex-direction: column;
199
+ }
200
+ header {
201
+ background: rgba(17, 24, 39, 0.85);
202
+ backdrop-filter: blur(8px);
203
+ border-bottom: 1px solid var(--border);
204
+ padding: 14px 24px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: space-between;
208
+ position: sticky;
209
+ top: 0;
210
+ z-index: 50;
211
+ }
212
+ .brand {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 12px;
216
+ }
217
+ .logo-badge {
218
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
219
+ color: white;
220
+ font-weight: 800;
221
+ font-size: 13px;
222
+ padding: 4px 10px;
223
+ border-radius: 6px;
224
+ letter-spacing: 0.5px;
225
+ }
226
+ .title {
227
+ font-size: 16px;
228
+ font-weight: 600;
229
+ color: #fff;
230
+ }
231
+ .status-badge {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 8px;
235
+ font-size: 13px;
236
+ color: var(--text-muted);
237
+ background: rgba(0,0,0,0.3);
238
+ padding: 6px 12px;
239
+ border-radius: 20px;
240
+ border: 1px solid var(--border);
241
+ }
242
+ .pulse-dot {
243
+ width: 8px;
244
+ height: 8px;
245
+ border-radius: 50%;
246
+ background: var(--green);
247
+ box-shadow: 0 0 8px var(--green);
248
+ animation: pulse 2s infinite;
249
+ }
250
+ .pulse-dot.offline {
251
+ background: #ef4444;
252
+ box-shadow: 0 0 8px #ef4444;
253
+ animation: none;
254
+ }
255
+ @keyframes pulse {
256
+ 0% { transform: scale(0.95); opacity: 0.8; }
257
+ 50% { transform: scale(1.15); opacity: 1; }
258
+ 100% { transform: scale(0.95); opacity: 0.8; }
259
+ }
260
+ main {
261
+ flex: 1;
262
+ max-width: 1300px;
263
+ width: 100%;
264
+ margin: 0 auto;
265
+ padding: 24px;
266
+ display: flex;
267
+ flex-direction: column;
268
+ gap: 20px;
269
+ }
270
+ .stats-grid {
271
+ display: grid;
272
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
273
+ gap: 16px;
274
+ }
275
+ .stat-card {
276
+ background: var(--card-bg);
277
+ border: 1px solid var(--border);
278
+ border-radius: var(--radius);
279
+ padding: 16px 20px;
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 6px;
283
+ }
284
+ .stat-label {
285
+ font-size: 12px;
286
+ color: var(--text-muted);
287
+ text-transform: uppercase;
288
+ letter-spacing: 0.5px;
289
+ font-weight: 600;
290
+ }
291
+ .stat-value {
292
+ font-size: 28px;
293
+ font-weight: 700;
294
+ color: #fff;
295
+ }
296
+ .controls {
297
+ display: flex;
298
+ flex-wrap: wrap;
299
+ align-items: center;
300
+ justify-content: space-between;
301
+ gap: 12px;
302
+ background: var(--card-bg);
303
+ border: 1px solid var(--border);
304
+ border-radius: var(--radius);
305
+ padding: 12px 16px;
306
+ }
307
+ .search-box {
308
+ flex: 1;
309
+ min-width: 250px;
310
+ }
311
+ .search-input {
312
+ width: 100%;
313
+ background: var(--code-bg);
314
+ border: 1px solid var(--border);
315
+ color: var(--text);
316
+ padding: 8px 14px;
317
+ border-radius: 6px;
318
+ font-size: 14px;
319
+ outline: none;
320
+ transition: border-color 0.2s;
321
+ }
322
+ .search-input:focus {
323
+ border-color: var(--accent);
324
+ }
325
+ .filter-pills {
326
+ display: flex;
327
+ gap: 6px;
328
+ }
329
+ .pill {
330
+ background: transparent;
331
+ border: 1px solid var(--border);
332
+ color: var(--text-muted);
333
+ padding: 6px 14px;
334
+ border-radius: 6px;
335
+ font-size: 13px;
336
+ cursor: pointer;
337
+ font-weight: 500;
338
+ transition: all 0.15s ease;
339
+ }
340
+ .pill:hover {
341
+ background: rgba(255,255,255,0.05);
342
+ color: var(--text);
343
+ }
344
+ .pill.active {
345
+ background: var(--accent);
346
+ border-color: var(--accent);
347
+ color: #fff;
348
+ }
349
+ .action-btn {
350
+ background: transparent;
351
+ border: 1px solid var(--border);
352
+ color: var(--text-muted);
353
+ padding: 7px 14px;
354
+ border-radius: 6px;
355
+ font-size: 13px;
356
+ cursor: pointer;
357
+ font-weight: 500;
358
+ transition: all 0.15s;
359
+ }
360
+ .action-btn:hover {
361
+ background: rgba(239, 68, 68, 0.15);
362
+ border-color: #ef4444;
363
+ color: #ef4444;
364
+ }
365
+ .feed-container {
366
+ background: var(--card-bg);
367
+ border: 1px solid var(--border);
368
+ border-radius: var(--radius);
369
+ overflow: hidden;
370
+ display: flex;
371
+ flex-direction: column;
372
+ }
373
+ .feed-header {
374
+ padding: 12px 20px;
375
+ border-bottom: 1px solid var(--border);
376
+ font-size: 13px;
377
+ font-weight: 600;
378
+ color: var(--text-muted);
379
+ display: grid;
380
+ grid-template-columns: 80px 90px 1fr 220px 120px;
381
+ gap: 12px;
382
+ background: rgba(0,0,0,0.2);
383
+ }
384
+ .feed-list {
385
+ list-style: none;
386
+ max-height: 600px;
387
+ overflow-y: auto;
388
+ }
389
+ .event-row {
390
+ border-bottom: 1px solid var(--border);
391
+ cursor: pointer;
392
+ transition: background-color 0.15s;
393
+ }
394
+ .event-row:last-child {
395
+ border-bottom: none;
396
+ }
397
+ .event-row:hover {
398
+ background: rgba(255, 255, 255, 0.02);
399
+ }
400
+ .event-row.open {
401
+ background: rgba(59, 130, 246, 0.03);
402
+ }
403
+ .event-summary {
404
+ padding: 12px 20px;
405
+ display: grid;
406
+ grid-template-columns: 80px 90px 1fr 220px 120px;
407
+ gap: 12px;
408
+ align-items: center;
409
+ font-size: 13px;
410
+ }
411
+ .badge {
412
+ display: inline-block;
413
+ padding: 2px 8px;
414
+ border-radius: 4px;
415
+ font-size: 11px;
416
+ font-weight: 700;
417
+ text-transform: uppercase;
418
+ text-align: center;
419
+ }
420
+ .badge-error { background: var(--error-bg); color: var(--error-text); border: 1px solid var(--error-border); }
421
+ .badge-warning { background: var(--warn-bg); color: var(--warn-text); border: 1px solid var(--warn-border); }
422
+ .badge-info { background: var(--info-bg); color: var(--info-text); border: 1px solid var(--info-border); }
423
+ .event-msg {
424
+ font-weight: 500;
425
+ color: #fff;
426
+ overflow: hidden;
427
+ text-overflow: ellipsis;
428
+ white-space: nowrap;
429
+ display: flex;
430
+ align-items: center;
431
+ gap: 8px;
432
+ }
433
+ .event-fp {
434
+ font-family: monospace;
435
+ font-size: 11px;
436
+ color: var(--accent);
437
+ background: rgba(59, 130, 246, 0.1);
438
+ padding: 2px 6px;
439
+ border-radius: 4px;
440
+ }
441
+ .event-time {
442
+ color: var(--text-muted);
443
+ font-size: 12px;
444
+ font-family: monospace;
445
+ }
446
+ .event-details {
447
+ display: none;
448
+ padding: 16px 24px;
449
+ background: var(--code-bg);
450
+ border-top: 1px dashed var(--border);
451
+ font-size: 13px;
452
+ display: flex;
453
+ flex-direction: column;
454
+ gap: 16px;
455
+ }
456
+ .details-grid {
457
+ display: grid;
458
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
459
+ gap: 12px;
460
+ background: rgba(255,255,255,0.02);
461
+ padding: 12px;
462
+ border-radius: 6px;
463
+ border: 1px solid var(--border);
464
+ }
465
+ .meta-item { display: flex; flex-direction: column; gap: 4px; }
466
+ .meta-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; font-weight: 600; }
467
+ .meta-value { font-family: monospace; font-size: 12px; color: #fff; word-break: break-all; }
468
+ .stack-trace {
469
+ background: #000;
470
+ border: 1px solid var(--border);
471
+ border-radius: 6px;
472
+ padding: 12px;
473
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
474
+ font-size: 12px;
475
+ line-height: 1.5;
476
+ color: #f87171;
477
+ max-height: 220px;
478
+ overflow-y: auto;
479
+ white-space: pre-wrap;
480
+ }
481
+ .breadcrumbs-list {
482
+ display: flex;
483
+ flex-direction: column;
484
+ gap: 6px;
485
+ max-height: 200px;
486
+ overflow-y: auto;
487
+ }
488
+ .crumb-item {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 10px;
492
+ padding: 6px 10px;
493
+ background: rgba(255,255,255,0.02);
494
+ border-radius: 4px;
495
+ font-size: 12px;
496
+ }
497
+ .empty-state {
498
+ padding: 60px 20px;
499
+ text-align: center;
500
+ color: var(--text-muted);
501
+ display: flex;
502
+ flex-direction: column;
503
+ align-items: center;
504
+ gap: 10px;
505
+ }
506
+ .empty-icon { font-size: 32px; opacity: 0.5; }
507
+ .copy-btn {
508
+ align-self: flex-start;
509
+ background: rgba(255,255,255,0.05);
510
+ border: 1px solid var(--border);
511
+ color: var(--text);
512
+ padding: 5px 10px;
513
+ border-radius: 4px;
514
+ cursor: pointer;
515
+ font-size: 11px;
516
+ }
517
+ .copy-btn:hover { background: rgba(255,255,255,0.1); }
518
+ </style>
519
+ </head>
520
+ <body>
521
+ <header>
522
+ <div class="brand">
523
+ <span class="logo-badge">NEXUS</span>
524
+ <span class="title">Local Telemetry Dev Server</span>
525
+ </div>
526
+ <div class="status-badge">
527
+ <span id="pulseDot" class="pulse-dot"></span>
528
+ <span id="statusText">Connected to :${port}</span>
529
+ </div>
530
+ </header>
531
+
532
+ <main>
533
+ <div class="stats-grid">
534
+ <div class="stat-card">
535
+ <span class="stat-label">Total Events</span>
536
+ <span id="statTotal" class="stat-value">0</span>
537
+ </div>
538
+ <div class="stat-card">
539
+ <span class="stat-label">Errors</span>
540
+ <span id="statErrors" class="stat-value" style="color: var(--error-text);">0</span>
541
+ </div>
542
+ <div class="stat-card">
543
+ <span class="stat-label">Warnings</span>
544
+ <span id="statWarnings" class="stat-value" style="color: var(--warn-text);">0</span>
545
+ </div>
546
+ <div class="stat-card">
547
+ <span class="stat-label">Info Messages</span>
548
+ <span id="statInfo" class="stat-value" style="color: var(--info-text);">0</span>
549
+ </div>
550
+ </div>
551
+
552
+ <div class="controls">
553
+ <div class="search-box">
554
+ <input type="text" id="searchInput" class="search-input" placeholder="Search message, URL, or fingerprint...">
555
+ </div>
556
+ <div class="filter-pills">
557
+ <button class="pill active" data-filter="all">All</button>
558
+ <button class="pill" data-filter="error">Error</button>
559
+ <button class="pill" data-filter="warning">Warning</button>
560
+ <button class="pill" data-filter="info">Info</button>
561
+ </div>
562
+ <button id="clearBtn" class="action-btn">Clear Buffer</button>
563
+ </div>
564
+
565
+ <div class="feed-container">
566
+ <div class="feed-header">
567
+ <span># ID</span>
568
+ <span>Severity</span>
569
+ <span>Message & Fingerprint</span>
570
+ <span>Timestamp</span>
571
+ <span>Environment</span>
572
+ </div>
573
+ <ul id="eventList" class="feed-list">
574
+ <div id="emptyState" class="empty-state">
575
+ <div class="empty-icon">&#9678;</div>
576
+ <div>Waiting for telemetry events...</div>
577
+ <div style="font-size: 12px; color: #6b7280;">Send error payloads to <code>http://localhost:${port}/api/v1/telemetry/errors</code></div>
578
+ </div>
579
+ </ul>
580
+ </div>
581
+ </main>
582
+
583
+ <script>
584
+ let events = [];
585
+ let currentFilter = 'all';
586
+ let searchQuery = '';
587
+ const openRows = new Set();
588
+
589
+ const eventList = document.getElementById('eventList');
590
+ const emptyState = document.getElementById('emptyState');
591
+ const searchInput = document.getElementById('searchInput');
592
+ const clearBtn = document.getElementById('clearBtn');
593
+ const pulseDot = document.getElementById('pulseDot');
594
+ const statusText = document.getElementById('statusText');
595
+
596
+ const statTotal = document.getElementById('statTotal');
597
+ const statErrors = document.getElementById('statErrors');
598
+ const statWarnings = document.getElementById('statWarnings');
599
+ const statInfo = document.getElementById('statInfo');
600
+
601
+ function updateStats() {
602
+ statTotal.textContent = events.length;
603
+ statErrors.textContent = events.filter(e => (e.payload.severity || 'error') === 'error').length;
604
+ statWarnings.textContent = events.filter(e => e.payload.severity === 'warning').length;
605
+ statInfo.textContent = events.filter(e => e.payload.severity === 'info').length;
606
+ }
607
+
608
+ function renderFeed() {
609
+ const filtered = events.filter(item => {
610
+ const severity = (item.payload.severity || 'error').toLowerCase();
611
+ if (currentFilter !== 'all' && severity !== currentFilter) return false;
612
+ if (!searchQuery) return true;
613
+ const q = searchQuery.toLowerCase();
614
+ return (
615
+ (item.payload.errorMessage || '').toLowerCase().includes(q) ||
616
+ (item.payload.fingerprint || '').toLowerCase().includes(q) ||
617
+ (item.payload.url || '').toLowerCase().includes(q) ||
618
+ (item.payload.errorType || '').toLowerCase().includes(q)
619
+ );
620
+ });
621
+
622
+ if (filtered.length === 0) {
623
+ eventList.innerHTML = '';
624
+ eventList.appendChild(emptyState);
625
+ return;
626
+ }
627
+
628
+ eventList.innerHTML = filtered.map(item => {
629
+ const p = item.payload;
630
+ const sev = p.severity || 'error';
631
+ const isOpen = openRows.has(item.id);
632
+ const timeFormatted = new Date(item.receivedAt).toLocaleTimeString();
633
+ const fullTime = new Date(item.receivedAt).toISOString();
634
+
635
+ const badgeClass = sev === 'warning' ? 'badge-warning' : sev === 'info' ? 'badge-info' : 'badge-error';
636
+ const fpShort = (p.fingerprint || '').slice(0, 8);
637
+
638
+ const breadcrumbsHtml = (p.breadcrumbs && p.breadcrumbs.length > 0)
639
+ ? p.breadcrumbs.map(c => \`
640
+ <div class="crumb-item">
641
+ <span class="badge badge-info">\${escapeHtml(c.category || 'crumb')}</span>
642
+ <span style="color: #fff; flex: 1;">\${escapeHtml(c.message || '')}</span>
643
+ <span style="color: #6b7280; font-family: monospace;">\${c.timestamp ? new Date(c.timestamp).toLocaleTimeString() : ''}</span>
644
+ </div>
645
+ \`).join('')
646
+ : '<div style="color: #6b7280; font-size: 12px;">No breadcrumbs recorded</div>';
647
+
648
+ return \`
649
+ <li class="event-row \${isOpen ? 'open' : ''}" data-id="\${item.id}">
650
+ <div class="event-summary" onclick="toggleDetails(\${item.id})">
651
+ <span style="font-family: monospace; color: #6b7280;">#\${item.id}</span>
652
+ <span><span class="badge \${badgeClass}">\${sev}</span></span>
653
+ <span class="event-msg">
654
+ <span>\${escapeHtml(p.errorMessage || 'Unknown Error')}</span>
655
+ \${fpShort ? \`<span class="event-fp">NX-\${escapeHtml(fpShort)}</span>\` : ''}
656
+ </span>
657
+ <span class="event-time" title="\${fullTime}">\${timeFormatted}</span>
658
+ <span style="color: #9ca3af; font-size: 12px;">\${escapeHtml(p.environment || 'local')}</span>
659
+ </div>
660
+ <div class="event-details" id="details-\${item.id}" style="display: \${isOpen ? 'flex' : 'none'};">
661
+ <div class="details-grid">
662
+ <div class="meta-item">
663
+ <span class="meta-label">Error Type</span>
664
+ <span class="meta-value">\${escapeHtml(p.errorType || 'Error')}</span>
665
+ </div>
666
+ <div class="meta-item">
667
+ <span class="meta-label">Fingerprint</span>
668
+ <span class="meta-value">\${escapeHtml(p.fingerprint || 'N/A')}</span>
669
+ </div>
670
+ <div class="meta-item">
671
+ <span class="meta-label">URL</span>
672
+ <span class="meta-value">\${escapeHtml(p.url || 'N/A')}</span>
673
+ </div>
674
+ <div class="meta-item">
675
+ <span class="meta-label">User Context</span>
676
+ <span class="meta-value">\${p.user ? escapeHtml(p.user.id || p.user.email || 'Anonymous') : 'None'}</span>
677
+ </div>
678
+ </div>
679
+
680
+ \${p.stackTrace ? \`
681
+ <div>
682
+ <div class="meta-label" style="margin-bottom: 6px;">Stack Trace</div>
683
+ <pre class="stack-trace">\${escapeHtml(p.stackTrace)}</pre>
684
+ </div>
685
+ \` : ''}
686
+
687
+ <div>
688
+ <div class="meta-label" style="margin-bottom: 6px;">Breadcrumbs (\${(p.breadcrumbs || []).length})</div>
689
+ <div class="breadcrumbs-list">\${breadcrumbsHtml}</div>
690
+ </div>
691
+
692
+ <button class="copy-btn" onclick="copyJson(\${item.id}, event)">Copy Payload JSON</button>
693
+ </div>
694
+ </li>
695
+ \`;
696
+ }).join('');
697
+ }
698
+
699
+ function toggleDetails(id) {
700
+ if (openRows.has(id)) {
701
+ openRows.delete(id);
702
+ } else {
703
+ openRows.add(id);
704
+ }
705
+ renderFeed();
706
+ }
707
+
708
+ function copyJson(id, evt) {
709
+ evt.stopPropagation();
710
+ const item = events.find(e => e.id === id);
711
+ if (!item) return;
712
+ navigator.clipboard.writeText(JSON.stringify(item.payload, null, 2)).then(() => {
713
+ const btn = evt.target;
714
+ const orig = btn.textContent;
715
+ btn.textContent = 'Copied!';
716
+ setTimeout(() => { btn.textContent = orig; }, 1500);
717
+ });
718
+ }
719
+
720
+ function escapeHtml(str) {
721
+ return String(str)
722
+ .replace(/&/g, '&amp;')
723
+ .replace(/</g, '&lt;')
724
+ .replace(/>/g, '&gt;')
725
+ .replace(/"/g, '&quot;')
726
+ .replace(/'/g, '&#039;');
727
+ }
728
+
729
+ // Filter pill events
730
+ document.querySelectorAll('.filter-pills .pill').forEach(btn => {
731
+ btn.addEventListener('click', () => {
732
+ document.querySelectorAll('.filter-pills .pill').forEach(b => b.classList.remove('active'));
733
+ btn.classList.add('active');
734
+ currentFilter = btn.dataset.filter;
735
+ renderFeed();
736
+ });
737
+ });
738
+
739
+ // Search query
740
+ searchInput.addEventListener('input', (e) => {
741
+ searchQuery = e.target.value.trim();
742
+ renderFeed();
743
+ });
744
+
745
+ // Clear buffer
746
+ clearBtn.addEventListener('click', async () => {
747
+ if (!confirm('Clear all stored telemetry events?')) return;
748
+ try {
749
+ await fetch('/events', { method: 'DELETE' });
750
+ events = [];
751
+ openRows.clear();
752
+ updateStats();
753
+ renderFeed();
754
+ } catch (err) {
755
+ console.error('Failed to clear events:', err);
756
+ }
757
+ });
758
+
759
+ // Load initial events
760
+ async function loadInitialEvents() {
761
+ try {
762
+ const res = await fetch('/events');
763
+ if (res.ok) {
764
+ events = await res.json();
765
+ updateStats();
766
+ renderFeed();
767
+ }
768
+ } catch (e) {
769
+ console.error('Failed to load initial events:', e);
770
+ }
771
+ }
772
+
773
+ // Connect SSE
774
+ function connectSse() {
775
+ const sse = new EventSource('/sse');
776
+
777
+ sse.addEventListener('connected', () => {
778
+ pulseDot.classList.remove('offline');
779
+ statusText.textContent = 'Live Feed Connected';
780
+ });
781
+
782
+ sse.addEventListener('error_event', (e) => {
783
+ try {
784
+ const envelope = JSON.parse(e.data);
785
+ events.unshift(envelope);
786
+ updateStats();
787
+ renderFeed();
788
+ } catch (err) {
789
+ console.error('Failed to parse SSE error_event:', err);
790
+ }
791
+ });
792
+
793
+ sse.addEventListener('clear', () => {
794
+ events = [];
795
+ openRows.clear();
796
+ updateStats();
797
+ renderFeed();
798
+ });
799
+
800
+ sse.onerror = () => {
801
+ pulseDot.classList.add('offline');
802
+ statusText.textContent = 'Disconnected \u2014 Reconnecting...';
803
+ };
804
+ }
805
+
806
+ loadInitialEvents();
807
+ connectSse();
808
+ </script>
809
+ </body>
810
+ </html>`;
811
+ }
812
+
813
+ // src/dev-server/server.ts
814
+ function setCorsHeaders(res) {
815
+ res.setHeader("Access-Control-Allow-Origin", "*");
816
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
817
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
818
+ res.setHeader("Access-Control-Max-Age", "86400");
819
+ }
820
+ function sendJson(res, statusCode, data) {
821
+ const json = JSON.stringify(data);
822
+ res.writeHead(statusCode, {
823
+ "Content-Type": "application/json; charset=utf-8",
824
+ "Content-Length": Buffer.byteLength(json)
825
+ });
826
+ res.end(json);
827
+ }
828
+ function parseJsonBody(req) {
829
+ return new Promise((resolve, reject) => {
830
+ let body = "";
831
+ req.setEncoding("utf8");
832
+ req.on("data", (chunk) => {
833
+ body += chunk;
834
+ if (body.length > 10 * 1024 * 1024) {
835
+ req.destroy();
836
+ reject(new Error("Payload too large"));
837
+ }
838
+ });
839
+ req.on("end", () => {
840
+ if (!body.trim()) {
841
+ resolve({});
842
+ return;
843
+ }
844
+ try {
845
+ resolve(JSON.parse(body));
846
+ } catch (err) {
847
+ reject(new Error("Invalid JSON"));
848
+ }
849
+ });
850
+ req.on("error", reject);
851
+ });
852
+ }
853
+ function startDevServer(options = {}) {
854
+ const port = options.port ?? 4567;
855
+ const host = options.host ?? "localhost";
856
+ const store = new DevServerEventStore(options.bufferCapacity ?? 1e3);
857
+ const sse = new SseBroadcaster();
858
+ return new Promise((resolve, reject) => {
859
+ const server = createServer(async (req, res) => {
860
+ setCorsHeaders(res);
861
+ if (req.method === "OPTIONS") {
862
+ res.writeHead(204);
863
+ res.end();
864
+ return;
865
+ }
866
+ const reqUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
867
+ const pathname = reqUrl.pathname;
868
+ if (req.method === "GET" && pathname === "/health") {
869
+ sendJson(res, 200, {
870
+ status: "ok",
871
+ uptime: process.uptime(),
872
+ eventCount: store.size(),
873
+ connectedClients: sse.clientCount
874
+ });
875
+ return;
876
+ }
877
+ if (req.method === "POST" && (pathname === "/api/v1/telemetry/errors" || pathname === "/telemetry/errors")) {
878
+ try {
879
+ const body = await parseJsonBody(req);
880
+ const envelope = store.add(body);
881
+ sse.broadcast("error_event", envelope);
882
+ sendJson(res, 202, {
883
+ success: true,
884
+ id: envelope.id,
885
+ receivedAt: envelope.receivedAt,
886
+ fingerprint: body.fingerprint
887
+ });
888
+ } catch (err) {
889
+ const msg = err instanceof Error ? err.message : "Bad request";
890
+ sendJson(res, 400, {
891
+ type: "https://nexus.dev/problems/bad-request",
892
+ title: "Bad Request",
893
+ status: 400,
894
+ detail: msg
895
+ });
896
+ }
897
+ return;
898
+ }
899
+ if (req.method === "GET" && (pathname === "/events" || pathname === "/api/v1/telemetry/errors")) {
900
+ sendJson(res, 200, store.getAll());
901
+ return;
902
+ }
903
+ if (req.method === "DELETE" && pathname === "/events") {
904
+ const count = store.size();
905
+ store.clear();
906
+ sse.broadcast("clear", {});
907
+ sendJson(res, 200, { success: true, cleared: count });
908
+ return;
909
+ }
910
+ if (req.method === "GET" && pathname === "/sse") {
911
+ sse.addClient(res);
912
+ return;
913
+ }
914
+ if (req.method === "GET" && (pathname === "/" || pathname === "/gui")) {
915
+ const html = renderDevServerGuiHtml(port);
916
+ res.writeHead(200, {
917
+ "Content-Type": "text/html; charset=utf-8",
918
+ "Content-Length": Buffer.byteLength(html)
919
+ });
920
+ res.end(html);
921
+ return;
922
+ }
923
+ sendJson(res, 404, {
924
+ type: "https://nexus.dev/problems/not-found",
925
+ title: "Not Found",
926
+ status: 404,
927
+ detail: `No handler for ${req.method} ${pathname}`
928
+ });
929
+ });
930
+ server.on("error", (err) => {
931
+ reject(err);
932
+ });
933
+ server.listen(port, host, () => {
934
+ const instance = {
935
+ server,
936
+ port,
937
+ host,
938
+ store,
939
+ sse,
940
+ close: () => {
941
+ return new Promise((resClose) => {
942
+ sse.destroy();
943
+ server.close(() => resClose());
944
+ });
945
+ }
946
+ };
947
+ resolve(instance);
948
+ });
949
+ });
950
+ }
951
+
952
+ // src/dev-server/cli.ts
953
+ function parseArgs(args) {
954
+ let port = 4567;
955
+ let host = "localhost";
956
+ let showHelp = false;
957
+ for (let i = 0; i < args.length; i++) {
958
+ const arg = args[i];
959
+ if (arg === "--help" || arg === "-h") {
960
+ showHelp = true;
961
+ } else if (arg === "--port" || arg === "-p") {
962
+ const nextArg = args[++i];
963
+ const val = nextArg ? parseInt(nextArg, 10) : NaN;
964
+ if (!isNaN(val) && val > 0 && val < 65536) {
965
+ port = val;
966
+ }
967
+ } else if (arg === "--host") {
968
+ const nextArg = args[++i];
969
+ if (nextArg) {
970
+ host = nextArg;
971
+ }
972
+ }
973
+ }
974
+ return { port, host, showHelp };
975
+ }
976
+ async function main() {
977
+ const { port, host, showHelp } = parseArgs(process.argv.slice(2));
978
+ if (showHelp) {
979
+ console.log(`
980
+ Nexus Dev Server \u2014 Local Telemetry & Error Tracking Dashboard
981
+
982
+ Usage:
983
+ nexus-dev [options]
984
+
985
+ Options:
986
+ -p, --port <number> Port to listen on (default: 4567)
987
+ --host <string> Host interface to bind (default: localhost)
988
+ -h, --help Show this help message
989
+
990
+ Endpoints:
991
+ GET / Interactive Web GUI Dashboard
992
+ GET /health Health check endpoint
993
+ POST /api/v1/telemetry/errors Ingest error payload
994
+ GET /events List buffered events (JSON)
995
+ DELETE /events Clear event buffer
996
+ GET /sse Realtime Server-Sent Events stream
997
+ `);
998
+ process.exit(0);
999
+ }
1000
+ try {
1001
+ const instance = await startDevServer({ port, host });
1002
+ const serverUrl = `http://${host}:${port}`;
1003
+ const ingestUrl = `${serverUrl}/api/v1/telemetry/errors`;
1004
+ console.log(`
1005
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1006
+ \u2551 NEXUS DEV INGESTION SERVER \u2551
1007
+ \u2551 Local Telemetry & Error Tracking Dashboard \u2551
1008
+ \u2551 \u2551
1009
+ \u2551 Web Dashboard: ${serverUrl.padEnd(39)} \u2551
1010
+ \u2551 Ingestion URL: ${ingestUrl.padEnd(39)} \u2551
1011
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1012
+
1013
+ [INFO] Server running. Press Ctrl+C to terminate.
1014
+ `);
1015
+ const onExit = async () => {
1016
+ console.log("\n[INFO] Shutting down Nexus Dev Server...");
1017
+ await instance.close();
1018
+ process.exit(0);
1019
+ };
1020
+ process.on("SIGINT", onExit);
1021
+ process.on("SIGTERM", onExit);
1022
+ } catch (err) {
1023
+ console.error("[ERROR] Failed to start Nexus Dev Server:", err);
1024
+ process.exit(1);
1025
+ }
1026
+ }
1027
+ main();