@nexussdk/tracker 0.0.3 → 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.
- package/README.md +147 -8
- package/dist/dev-server/cli.mjs +1027 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.mts +227 -20
- package/dist/index.d.ts +227 -20
- package/dist/index.global.js +3 -3
- package/dist/index.mjs +2 -2
- package/package.json +30 -6
|
@@ -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">◎</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, '&')
|
|
723
|
+
.replace(/</g, '<')
|
|
724
|
+
.replace(/>/g, '>')
|
|
725
|
+
.replace(/"/g, '"')
|
|
726
|
+
.replace(/'/g, ''');
|
|
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();
|