network-terminal 1.0.9 → 1.0.11
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/bin/cli.js +244 -260
- package/dist/index.js +492 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +499 -59
- package/dist/index.mjs.map +1 -1
- package/dist/vite-plugin.d.mts +4 -8
- package/dist/vite-plugin.d.ts +4 -8
- package/dist/vite-plugin.js +492 -59
- package/dist/vite-plugin.js.map +1 -1
- package/dist/vite-plugin.mjs +499 -59
- package/dist/vite-plugin.mjs.map +1 -1
- package/package.json +13 -9
package/bin/cli.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
|
-
const path = require('path');
|
|
5
4
|
|
|
6
5
|
const args = process.argv.slice(2);
|
|
7
|
-
|
|
8
|
-
// Parse arguments
|
|
9
6
|
const portIndex = args.indexOf('--port');
|
|
10
|
-
const
|
|
11
|
-
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3001;
|
|
12
|
-
const targetPort = proxyIndex !== -1 ? parseInt(args[proxyIndex + 1], 10) : 3000;
|
|
13
|
-
const dashboardPort = port + 1000; // Dashboard on separate port
|
|
7
|
+
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 4001;
|
|
14
8
|
|
|
15
9
|
// Store network logs
|
|
16
10
|
const networkLogs = [];
|
|
17
|
-
let logId = 0;
|
|
18
11
|
|
|
19
12
|
// SSE clients for real-time updates
|
|
20
13
|
const sseClients = new Set();
|
|
@@ -26,7 +19,132 @@ function broadcastLog(log) {
|
|
|
26
19
|
});
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
//
|
|
22
|
+
// Monitor script that users add to their app
|
|
23
|
+
const monitorScript = `
|
|
24
|
+
(function() {
|
|
25
|
+
if (window.__networkTerminalActive) return;
|
|
26
|
+
window.__networkTerminalActive = true;
|
|
27
|
+
|
|
28
|
+
const DASHBOARD_URL = 'http://localhost:${port}';
|
|
29
|
+
|
|
30
|
+
function sendLog(log) {
|
|
31
|
+
var xhr = new XMLHttpRequest();
|
|
32
|
+
xhr.open('POST', DASHBOARD_URL + '/log', true);
|
|
33
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
34
|
+
xhr.send(JSON.stringify(log));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function tryParse(data) {
|
|
38
|
+
if (!data) return null;
|
|
39
|
+
if (typeof data === 'object') return data;
|
|
40
|
+
try { return JSON.parse(data); } catch (e) { return data; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Intercept Fetch
|
|
44
|
+
var originalFetch = window.fetch;
|
|
45
|
+
window.fetch = function() {
|
|
46
|
+
var args = arguments;
|
|
47
|
+
var startTime = Date.now();
|
|
48
|
+
var input = args[0];
|
|
49
|
+
var options = args[1] || {};
|
|
50
|
+
var url = typeof input === 'string' ? input : (input.url || input.toString());
|
|
51
|
+
var method = (options.method || 'GET').toUpperCase();
|
|
52
|
+
|
|
53
|
+
// Skip our own requests
|
|
54
|
+
if (url.includes('localhost:${port}')) {
|
|
55
|
+
return originalFetch.apply(this, args);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var log = {
|
|
59
|
+
id: Date.now().toString() + Math.random().toString(36).substr(2, 9),
|
|
60
|
+
method: method,
|
|
61
|
+
url: url,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
requestBody: options.body ? tryParse(options.body) : null,
|
|
64
|
+
status: null,
|
|
65
|
+
responseBody: null,
|
|
66
|
+
duration: null,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
sendLog(log);
|
|
70
|
+
|
|
71
|
+
return originalFetch.apply(this, args).then(function(response) {
|
|
72
|
+
log.status = response.status;
|
|
73
|
+
log.duration = Date.now() - startTime;
|
|
74
|
+
|
|
75
|
+
var cloned = response.clone();
|
|
76
|
+
cloned.text().then(function(text) {
|
|
77
|
+
log.responseBody = tryParse(text);
|
|
78
|
+
sendLog(log);
|
|
79
|
+
}).catch(function() {
|
|
80
|
+
sendLog(log);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return response;
|
|
84
|
+
}).catch(function(error) {
|
|
85
|
+
log.status = 0;
|
|
86
|
+
log.duration = Date.now() - startTime;
|
|
87
|
+
log.error = error.message;
|
|
88
|
+
sendLog(log);
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Intercept XMLHttpRequest
|
|
94
|
+
var OriginalXHR = window.XMLHttpRequest;
|
|
95
|
+
window.XMLHttpRequest = function() {
|
|
96
|
+
var xhr = new OriginalXHR();
|
|
97
|
+
var method, url, requestBody, startTime, logId;
|
|
98
|
+
|
|
99
|
+
var originalOpen = xhr.open;
|
|
100
|
+
xhr.open = function(m, u) {
|
|
101
|
+
method = m;
|
|
102
|
+
url = u;
|
|
103
|
+
return originalOpen.apply(this, arguments);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
var originalSend = xhr.send;
|
|
107
|
+
xhr.send = function(body) {
|
|
108
|
+
// Skip our own requests
|
|
109
|
+
if (url && url.includes('localhost:${port}')) {
|
|
110
|
+
return originalSend.apply(this, arguments);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
startTime = Date.now();
|
|
114
|
+
requestBody = body;
|
|
115
|
+
logId = Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
|
116
|
+
|
|
117
|
+
var log = {
|
|
118
|
+
id: logId,
|
|
119
|
+
method: (method || 'GET').toUpperCase(),
|
|
120
|
+
url: url,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
requestBody: tryParse(requestBody),
|
|
123
|
+
status: null,
|
|
124
|
+
responseBody: null,
|
|
125
|
+
duration: null,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
sendLog(log);
|
|
129
|
+
|
|
130
|
+
xhr.addEventListener('loadend', function() {
|
|
131
|
+
log.status = xhr.status;
|
|
132
|
+
log.duration = Date.now() - startTime;
|
|
133
|
+
log.responseBody = tryParse(xhr.responseText);
|
|
134
|
+
sendLog(log);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return originalSend.apply(this, arguments);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return xhr;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
console.log('[Network Terminal] ✓ Monitoring active - Dashboard: http://localhost:${port}');
|
|
144
|
+
})();
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
// Dashboard HTML
|
|
30
148
|
const dashboardHTML = `
|
|
31
149
|
<!DOCTYPE html>
|
|
32
150
|
<html lang="en">
|
|
@@ -51,19 +169,9 @@ const dashboardHTML = `
|
|
|
51
169
|
justify-content: space-between;
|
|
52
170
|
align-items: center;
|
|
53
171
|
}
|
|
54
|
-
.title {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
font-weight: bold;
|
|
58
|
-
}
|
|
59
|
-
.subtitle {
|
|
60
|
-
color: #64748b;
|
|
61
|
-
font-size: 12px;
|
|
62
|
-
}
|
|
63
|
-
.controls {
|
|
64
|
-
display: flex;
|
|
65
|
-
gap: 12px;
|
|
66
|
-
}
|
|
172
|
+
.title { color: #4ade80; font-size: 18px; font-weight: bold; }
|
|
173
|
+
.subtitle { color: #64748b; font-size: 12px; margin-top: 4px; }
|
|
174
|
+
.controls { display: flex; gap: 12px; align-items: center; }
|
|
67
175
|
.btn {
|
|
68
176
|
background: #1e293b;
|
|
69
177
|
border: 1px solid #334155;
|
|
@@ -74,23 +182,15 @@ const dashboardHTML = `
|
|
|
74
182
|
font-size: 12px;
|
|
75
183
|
font-family: inherit;
|
|
76
184
|
}
|
|
77
|
-
.btn:hover {
|
|
78
|
-
|
|
79
|
-
color: #e2e8f0;
|
|
80
|
-
}
|
|
81
|
-
.container {
|
|
82
|
-
display: flex;
|
|
83
|
-
height: calc(100vh - 60px);
|
|
84
|
-
}
|
|
185
|
+
.btn:hover { background: #334155; color: #e2e8f0; }
|
|
186
|
+
.container { display: flex; height: calc(100vh - 80px); }
|
|
85
187
|
.panel {
|
|
86
188
|
flex: 1;
|
|
87
189
|
display: flex;
|
|
88
190
|
flex-direction: column;
|
|
89
191
|
border-right: 1px solid #1e293b;
|
|
90
192
|
}
|
|
91
|
-
.panel:last-child {
|
|
92
|
-
border-right: none;
|
|
93
|
-
}
|
|
193
|
+
.panel:last-child { border-right: none; }
|
|
94
194
|
.panel-header {
|
|
95
195
|
background: #0f172a;
|
|
96
196
|
padding: 12px 16px;
|
|
@@ -99,10 +199,7 @@ const dashboardHTML = `
|
|
|
99
199
|
justify-content: space-between;
|
|
100
200
|
align-items: center;
|
|
101
201
|
}
|
|
102
|
-
.panel-title {
|
|
103
|
-
font-weight: bold;
|
|
104
|
-
font-size: 14px;
|
|
105
|
-
}
|
|
202
|
+
.panel-title { font-weight: bold; font-size: 14px; }
|
|
106
203
|
.panel-title.request { color: #3b82f6; }
|
|
107
204
|
.panel-title.response { color: #22c55e; }
|
|
108
205
|
.panel-count {
|
|
@@ -112,11 +209,7 @@ const dashboardHTML = `
|
|
|
112
209
|
font-size: 11px;
|
|
113
210
|
color: #64748b;
|
|
114
211
|
}
|
|
115
|
-
.logs {
|
|
116
|
-
flex: 1;
|
|
117
|
-
overflow-y: auto;
|
|
118
|
-
padding: 8px;
|
|
119
|
-
}
|
|
212
|
+
.logs { flex: 1; overflow-y: auto; padding: 8px; }
|
|
120
213
|
.log-entry {
|
|
121
214
|
background: #0f172a;
|
|
122
215
|
border: 1px solid #1e293b;
|
|
@@ -126,12 +219,8 @@ const dashboardHTML = `
|
|
|
126
219
|
cursor: pointer;
|
|
127
220
|
transition: border-color 0.2s;
|
|
128
221
|
}
|
|
129
|
-
.log-entry:hover {
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
.log-entry.selected {
|
|
133
|
-
border-color: #4ade80;
|
|
134
|
-
}
|
|
222
|
+
.log-entry:hover { border-color: #334155; }
|
|
223
|
+
.log-entry.selected { border-color: #4ade80; }
|
|
135
224
|
.log-header {
|
|
136
225
|
display: flex;
|
|
137
226
|
align-items: center;
|
|
@@ -150,11 +239,7 @@ const dashboardHTML = `
|
|
|
150
239
|
.method.PUT { background: #a16207; color: #fcd34d; }
|
|
151
240
|
.method.DELETE { background: #991b1b; color: #fca5a5; }
|
|
152
241
|
.method.PATCH { background: #7c3aed; color: #c4b5fd; }
|
|
153
|
-
.status {
|
|
154
|
-
font-size: 11px;
|
|
155
|
-
padding: 3px 8px;
|
|
156
|
-
border-radius: 4px;
|
|
157
|
-
}
|
|
242
|
+
.status { font-size: 11px; padding: 3px 8px; border-radius: 4px; }
|
|
158
243
|
.status.success { background: #166534; color: #4ade80; }
|
|
159
244
|
.status.error { background: #991b1b; color: #fca5a5; }
|
|
160
245
|
.status.pending { background: #374151; color: #9ca3af; }
|
|
@@ -166,20 +251,9 @@ const dashboardHTML = `
|
|
|
166
251
|
text-overflow: ellipsis;
|
|
167
252
|
white-space: nowrap;
|
|
168
253
|
}
|
|
169
|
-
.duration {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
.timestamp {
|
|
174
|
-
font-size: 10px;
|
|
175
|
-
color: #475569;
|
|
176
|
-
}
|
|
177
|
-
.log-body {
|
|
178
|
-
padding: 12px;
|
|
179
|
-
font-size: 11px;
|
|
180
|
-
max-height: 300px;
|
|
181
|
-
overflow: auto;
|
|
182
|
-
}
|
|
254
|
+
.duration { font-size: 11px; color: #64748b; }
|
|
255
|
+
.timestamp { font-size: 10px; color: #475569; }
|
|
256
|
+
.log-body { padding: 12px; font-size: 11px; max-height: 300px; overflow: auto; }
|
|
183
257
|
.log-body pre {
|
|
184
258
|
margin: 0;
|
|
185
259
|
white-space: pre-wrap;
|
|
@@ -187,10 +261,7 @@ const dashboardHTML = `
|
|
|
187
261
|
color: #cbd5e1;
|
|
188
262
|
line-height: 1.5;
|
|
189
263
|
}
|
|
190
|
-
.log-body.empty {
|
|
191
|
-
color: #475569;
|
|
192
|
-
font-style: italic;
|
|
193
|
-
}
|
|
264
|
+
.log-body.empty { color: #475569; font-style: italic; }
|
|
194
265
|
.empty-state {
|
|
195
266
|
display: flex;
|
|
196
267
|
flex-direction: column;
|
|
@@ -198,27 +269,27 @@ const dashboardHTML = `
|
|
|
198
269
|
justify-content: center;
|
|
199
270
|
height: 100%;
|
|
200
271
|
color: #475569;
|
|
272
|
+
text-align: center;
|
|
273
|
+
padding: 20px;
|
|
201
274
|
}
|
|
202
|
-
.empty-state svg {
|
|
203
|
-
width: 48px;
|
|
204
|
-
height: 48px;
|
|
205
|
-
margin-bottom: 16px;
|
|
206
|
-
opacity: 0.5;
|
|
207
|
-
}
|
|
208
|
-
.connection-status {
|
|
209
|
-
display: flex;
|
|
210
|
-
align-items: center;
|
|
211
|
-
gap: 8px;
|
|
212
|
-
font-size: 12px;
|
|
213
|
-
}
|
|
275
|
+
.empty-state svg { width: 48px; height: 48px; margin-bottom: 16px; opacity: 0.5; }
|
|
214
276
|
.status-dot {
|
|
215
277
|
width: 8px;
|
|
216
278
|
height: 8px;
|
|
217
279
|
border-radius: 50%;
|
|
218
280
|
background: #22c55e;
|
|
281
|
+
animation: pulse 2s infinite;
|
|
219
282
|
}
|
|
220
|
-
.
|
|
221
|
-
|
|
283
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
284
|
+
.status-dot.disconnected { background: #ef4444; animation: none; }
|
|
285
|
+
.code-box {
|
|
286
|
+
background: #1e293b;
|
|
287
|
+
padding: 12px;
|
|
288
|
+
border-radius: 6px;
|
|
289
|
+
margin-top: 12px;
|
|
290
|
+
font-size: 11px;
|
|
291
|
+
color: #4ade80;
|
|
292
|
+
word-break: break-all;
|
|
222
293
|
}
|
|
223
294
|
</style>
|
|
224
295
|
</head>
|
|
@@ -226,13 +297,10 @@ const dashboardHTML = `
|
|
|
226
297
|
<div class="header">
|
|
227
298
|
<div>
|
|
228
299
|
<div class="title">>_ Network Terminal</div>
|
|
229
|
-
<div class="subtitle">
|
|
300
|
+
<div class="subtitle">Add this to your app's HTML: <script src="http://localhost:${port}/monitor.js"></script></div>
|
|
230
301
|
</div>
|
|
231
302
|
<div class="controls">
|
|
232
|
-
<div class="
|
|
233
|
-
<div class="status-dot" id="statusDot"></div>
|
|
234
|
-
<span id="statusText">Connected</span>
|
|
235
|
-
</div>
|
|
303
|
+
<div class="status-dot" id="statusDot"></div>
|
|
236
304
|
<button class="btn" onclick="clearLogs()">Clear All</button>
|
|
237
305
|
</div>
|
|
238
306
|
</div>
|
|
@@ -249,7 +317,8 @@ const dashboardHTML = `
|
|
|
249
317
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
250
318
|
</svg>
|
|
251
319
|
<div>Waiting for requests...</div>
|
|
252
|
-
<div style="margin-top:
|
|
320
|
+
<div style="margin-top: 12px; font-size: 11px;">Add this script tag to your HTML:</div>
|
|
321
|
+
<div class="code-box"><script src="http://localhost:${port}/monitor.js"></script></div>
|
|
253
322
|
</div>
|
|
254
323
|
</div>
|
|
255
324
|
</div>
|
|
@@ -271,17 +340,14 @@ const dashboardHTML = `
|
|
|
271
340
|
</div>
|
|
272
341
|
|
|
273
342
|
<script>
|
|
274
|
-
const logs =
|
|
343
|
+
const logs = new Map();
|
|
275
344
|
let selectedId = null;
|
|
276
345
|
|
|
277
346
|
function formatJson(data) {
|
|
278
347
|
if (!data) return null;
|
|
279
348
|
if (typeof data === 'string') {
|
|
280
|
-
try {
|
|
281
|
-
|
|
282
|
-
} catch (e) {
|
|
283
|
-
return escapeHtml(data);
|
|
284
|
-
}
|
|
349
|
+
try { data = JSON.parse(data); }
|
|
350
|
+
catch (e) { return escapeHtml(data.substring(0, 2000)); }
|
|
285
351
|
}
|
|
286
352
|
return escapeHtml(JSON.stringify(data, null, 2));
|
|
287
353
|
}
|
|
@@ -297,21 +363,29 @@ const dashboardHTML = `
|
|
|
297
363
|
String(date.getMilliseconds()).padStart(3, '0');
|
|
298
364
|
}
|
|
299
365
|
|
|
366
|
+
function getLogsArray() {
|
|
367
|
+
return Array.from(logs.values()).sort((a, b) =>
|
|
368
|
+
new Date(b.timestamp) - new Date(a.timestamp)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
300
372
|
function renderLogs() {
|
|
373
|
+
const logsArray = getLogsArray();
|
|
301
374
|
const requestContainer = document.getElementById('requestLogs');
|
|
302
375
|
const responseContainer = document.getElementById('responseLogs');
|
|
303
376
|
|
|
304
|
-
document.getElementById('requestCount').textContent =
|
|
305
|
-
document.getElementById('responseCount').textContent =
|
|
377
|
+
document.getElementById('requestCount').textContent = logsArray.length;
|
|
378
|
+
document.getElementById('responseCount').textContent = logsArray.filter(l => l.status).length;
|
|
306
379
|
|
|
307
|
-
if (
|
|
380
|
+
if (logsArray.length === 0) {
|
|
308
381
|
requestContainer.innerHTML = \`
|
|
309
382
|
<div class="empty-state">
|
|
310
383
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
311
384
|
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
312
385
|
</svg>
|
|
313
386
|
<div>Waiting for requests...</div>
|
|
314
|
-
<div style="margin-top:
|
|
387
|
+
<div style="margin-top: 12px; font-size: 11px;">Add this script tag to your HTML:</div>
|
|
388
|
+
<div class="code-box"><script src="http://localhost:${port}/monitor.js"></script></div>
|
|
315
389
|
</div>
|
|
316
390
|
\`;
|
|
317
391
|
responseContainer.innerHTML = \`
|
|
@@ -325,45 +399,33 @@ const dashboardHTML = `
|
|
|
325
399
|
return;
|
|
326
400
|
}
|
|
327
401
|
|
|
328
|
-
|
|
329
|
-
requestContainer.innerHTML = logs.map(log => {
|
|
402
|
+
requestContainer.innerHTML = logsArray.map(log => {
|
|
330
403
|
const isSelected = selectedId === log.id;
|
|
331
404
|
const bodyContent = formatJson(log.requestBody);
|
|
332
|
-
|
|
333
405
|
return \`
|
|
334
406
|
<div class="log-entry \${isSelected ? 'selected' : ''}" onclick="selectLog('\${log.id}')">
|
|
335
407
|
<div class="log-header">
|
|
336
408
|
<span class="method \${log.method}">\${log.method}</span>
|
|
337
|
-
<span class="url">\${log.url}</span>
|
|
409
|
+
<span class="url" title="\${escapeHtml(log.url)}">\${log.url}</span>
|
|
338
410
|
<span class="timestamp">\${formatTime(log.timestamp)}</span>
|
|
339
411
|
</div>
|
|
340
|
-
\${isSelected ?
|
|
341
|
-
<div class="log-body \${!bodyContent ? 'empty' : ''}">
|
|
342
|
-
\${bodyContent ? \`<pre>\${bodyContent}</pre>\` : 'No request body'}
|
|
343
|
-
</div>
|
|
344
|
-
\` : ''}
|
|
412
|
+
\${isSelected ? \`<div class="log-body \${!bodyContent ? 'empty' : ''}">\${bodyContent ? '<pre>'+bodyContent+'</pre>' : 'No request body'}</div>\` : ''}
|
|
345
413
|
</div>
|
|
346
414
|
\`;
|
|
347
415
|
}).join('');
|
|
348
416
|
|
|
349
|
-
|
|
350
|
-
responseContainer.innerHTML = logs.map(log => {
|
|
417
|
+
responseContainer.innerHTML = logsArray.map(log => {
|
|
351
418
|
const isSelected = selectedId === log.id;
|
|
352
419
|
const statusClass = !log.status ? 'pending' : log.status < 400 ? 'success' : 'error';
|
|
353
420
|
const bodyContent = formatJson(log.responseBody);
|
|
354
|
-
|
|
355
421
|
return \`
|
|
356
422
|
<div class="log-entry \${isSelected ? 'selected' : ''}" onclick="selectLog('\${log.id}')">
|
|
357
423
|
<div class="log-header">
|
|
358
424
|
<span class="status \${statusClass}">\${log.status || '...'}</span>
|
|
359
|
-
<span class="url">\${log.url}</span>
|
|
425
|
+
<span class="url" title="\${escapeHtml(log.url)}">\${log.url}</span>
|
|
360
426
|
<span class="duration">\${log.duration ? log.duration + 'ms' : ''}</span>
|
|
361
427
|
</div>
|
|
362
|
-
\${isSelected ?
|
|
363
|
-
<div class="log-body \${!bodyContent ? 'empty' : ''}">
|
|
364
|
-
\${bodyContent ? \`<pre>\${bodyContent}</pre>\` : log.status ? 'No response body' : 'Pending...'}
|
|
365
|
-
</div>
|
|
366
|
-
\` : ''}
|
|
428
|
+
\${isSelected ? \`<div class="log-body \${!bodyContent ? 'empty' : ''}">\${bodyContent ? '<pre>'+bodyContent+'</pre>' : (log.status ? 'No response body' : 'Pending...')}</div>\` : ''}
|
|
367
429
|
</div>
|
|
368
430
|
\`;
|
|
369
431
|
}).join('');
|
|
@@ -375,40 +437,32 @@ const dashboardHTML = `
|
|
|
375
437
|
}
|
|
376
438
|
|
|
377
439
|
function clearLogs() {
|
|
378
|
-
logs.
|
|
440
|
+
logs.clear();
|
|
379
441
|
selectedId = null;
|
|
380
442
|
renderLogs();
|
|
381
|
-
fetch('/
|
|
443
|
+
fetch('/clear', { method: 'POST' });
|
|
382
444
|
}
|
|
383
445
|
|
|
384
446
|
function addLog(log) {
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
logs.unshift(log);
|
|
390
|
-
if (logs.length > 100) logs.pop();
|
|
447
|
+
logs.set(log.id, log);
|
|
448
|
+
if (logs.size > 100) {
|
|
449
|
+
const oldest = getLogsArray().pop();
|
|
450
|
+
if (oldest) logs.delete(oldest.id);
|
|
391
451
|
}
|
|
392
452
|
renderLogs();
|
|
393
453
|
}
|
|
394
454
|
|
|
395
|
-
// SSE connection for real-time updates
|
|
396
455
|
function connect() {
|
|
397
|
-
const evtSource = new EventSource('/
|
|
398
|
-
|
|
456
|
+
const evtSource = new EventSource('/events');
|
|
399
457
|
evtSource.onopen = () => {
|
|
400
458
|
document.getElementById('statusDot').classList.remove('disconnected');
|
|
401
|
-
document.getElementById('statusText').textContent = 'Connected';
|
|
402
459
|
};
|
|
403
|
-
|
|
404
460
|
evtSource.onmessage = (event) => {
|
|
405
461
|
const log = JSON.parse(event.data);
|
|
406
462
|
addLog(log);
|
|
407
463
|
};
|
|
408
|
-
|
|
409
464
|
evtSource.onerror = () => {
|
|
410
465
|
document.getElementById('statusDot').classList.add('disconnected');
|
|
411
|
-
document.getElementById('statusText').textContent = 'Reconnecting...';
|
|
412
466
|
evtSource.close();
|
|
413
467
|
setTimeout(connect, 2000);
|
|
414
468
|
};
|
|
@@ -422,161 +476,91 @@ const dashboardHTML = `
|
|
|
422
476
|
`;
|
|
423
477
|
|
|
424
478
|
console.log('\x1b[32m%s\x1b[0m', '\n >_ Network Terminal\n');
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
479
|
+
|
|
480
|
+
const server = http.createServer((req, res) => {
|
|
481
|
+
// CORS headers
|
|
482
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
483
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
484
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
485
|
+
|
|
486
|
+
if (req.method === 'OPTIONS') {
|
|
487
|
+
res.writeHead(204);
|
|
488
|
+
res.end();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Serve monitor script
|
|
493
|
+
if (req.url === '/monitor.js') {
|
|
494
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
495
|
+
res.end(monitorScript);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// SSE endpoint
|
|
500
|
+
if (req.url === '/events') {
|
|
433
501
|
res.writeHead(200, {
|
|
434
502
|
'Content-Type': 'text/event-stream',
|
|
435
503
|
'Cache-Control': 'no-cache',
|
|
436
504
|
'Connection': 'keep-alive',
|
|
437
|
-
'Access-Control-Allow-Origin': '*',
|
|
438
505
|
});
|
|
439
|
-
|
|
440
506
|
sseClients.add(res);
|
|
441
|
-
|
|
442
|
-
// Send existing logs
|
|
443
507
|
networkLogs.forEach(log => {
|
|
444
508
|
res.write(`data: ${JSON.stringify(log)}\n\n`);
|
|
445
509
|
});
|
|
510
|
+
req.on('close', () => sseClients.delete(res));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
446
513
|
|
|
447
|
-
|
|
448
|
-
|
|
514
|
+
// Receive logs
|
|
515
|
+
if (req.url === '/log' && req.method === 'POST') {
|
|
516
|
+
let body = '';
|
|
517
|
+
req.on('data', chunk => body += chunk);
|
|
518
|
+
req.on('end', () => {
|
|
519
|
+
try {
|
|
520
|
+
const log = JSON.parse(body);
|
|
521
|
+
const existingIndex = networkLogs.findIndex(l => l.id === log.id);
|
|
522
|
+
if (existingIndex >= 0) {
|
|
523
|
+
networkLogs[existingIndex] = log;
|
|
524
|
+
} else {
|
|
525
|
+
networkLogs.unshift(log);
|
|
526
|
+
if (networkLogs.length > 100) networkLogs.pop();
|
|
527
|
+
}
|
|
528
|
+
broadcastLog(log);
|
|
529
|
+
} catch (e) {}
|
|
530
|
+
res.writeHead(200);
|
|
531
|
+
res.end('OK');
|
|
449
532
|
});
|
|
450
533
|
return;
|
|
451
534
|
}
|
|
452
535
|
|
|
453
|
-
|
|
536
|
+
// Clear logs
|
|
537
|
+
if (req.url === '/clear' && req.method === 'POST') {
|
|
454
538
|
networkLogs.length = 0;
|
|
455
539
|
res.writeHead(200);
|
|
456
540
|
res.end('OK');
|
|
457
541
|
return;
|
|
458
542
|
}
|
|
459
543
|
|
|
544
|
+
// Serve dashboard
|
|
460
545
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
461
546
|
res.end(dashboardHTML);
|
|
462
547
|
});
|
|
463
548
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// Create log entry for request
|
|
470
|
-
const log = {
|
|
471
|
-
id: String(currentLogId),
|
|
472
|
-
method: req.method,
|
|
473
|
-
url: req.url,
|
|
474
|
-
timestamp: new Date().toISOString(),
|
|
475
|
-
requestBody: null,
|
|
476
|
-
status: null,
|
|
477
|
-
responseBody: null,
|
|
478
|
-
duration: null,
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// Collect request body
|
|
482
|
-
let requestBody = '';
|
|
483
|
-
req.on('data', chunk => requestBody += chunk);
|
|
484
|
-
req.on('end', () => {
|
|
485
|
-
if (requestBody) {
|
|
486
|
-
try {
|
|
487
|
-
log.requestBody = JSON.parse(requestBody);
|
|
488
|
-
} catch (e) {
|
|
489
|
-
log.requestBody = requestBody;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Add to logs and broadcast
|
|
494
|
-
networkLogs.unshift(log);
|
|
495
|
-
if (networkLogs.length > 100) networkLogs.pop();
|
|
496
|
-
broadcastLog(log);
|
|
497
|
-
|
|
498
|
-
// Forward request to target
|
|
499
|
-
const options = {
|
|
500
|
-
hostname: 'localhost',
|
|
501
|
-
port: targetPort,
|
|
502
|
-
path: req.url,
|
|
503
|
-
method: req.method,
|
|
504
|
-
headers: req.headers,
|
|
505
|
-
};
|
|
549
|
+
server.listen(port, () => {
|
|
550
|
+
console.log(` Dashboard: \x1b[36mhttp://localhost:${port}\x1b[0m\n`);
|
|
551
|
+
console.log(' Add this to your app\'s HTML (in <head> or before </body>):\n');
|
|
552
|
+
console.log(` \x1b[33m<script src="http://localhost:${port}/monitor.js"></script>\x1b[0m\n`);
|
|
553
|
+
console.log(' Press \x1b[33mCtrl+C\x1b[0m to stop\n');
|
|
506
554
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// Collect response body
|
|
511
|
-
let responseBody = '';
|
|
512
|
-
proxyRes.on('data', chunk => responseBody += chunk);
|
|
513
|
-
proxyRes.on('end', () => {
|
|
514
|
-
// Update log with response
|
|
515
|
-
log.status = proxyRes.statusCode;
|
|
516
|
-
log.duration = Date.now() - startTime;
|
|
517
|
-
|
|
518
|
-
if (responseBody && contentType.includes('application/json')) {
|
|
519
|
-
try {
|
|
520
|
-
log.responseBody = JSON.parse(responseBody);
|
|
521
|
-
} catch (e) {
|
|
522
|
-
log.responseBody = responseBody;
|
|
523
|
-
}
|
|
524
|
-
} else if (responseBody && !contentType.includes('text/html')) {
|
|
525
|
-
log.responseBody = responseBody.substring(0, 1000);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Broadcast updated log
|
|
529
|
-
broadcastLog(log);
|
|
530
|
-
|
|
531
|
-
// Send response to client
|
|
532
|
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
533
|
-
res.end(responseBody);
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
proxyReq.on('error', (err) => {
|
|
538
|
-
log.status = 502;
|
|
539
|
-
log.duration = Date.now() - startTime;
|
|
540
|
-
log.error = err.message;
|
|
541
|
-
broadcastLog(log);
|
|
542
|
-
|
|
543
|
-
res.writeHead(502);
|
|
544
|
-
res.end(`Proxy Error: ${err.message}`);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
if (requestBody) {
|
|
548
|
-
proxyReq.write(requestBody);
|
|
549
|
-
}
|
|
550
|
-
proxyReq.end();
|
|
551
|
-
});
|
|
555
|
+
// Open dashboard
|
|
556
|
+
const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
557
|
+
require('child_process').exec(`${open} http://localhost:${port}`);
|
|
552
558
|
});
|
|
553
559
|
|
|
554
|
-
|
|
555
|
-
dashboardServer.listen(dashboardPort, () => {
|
|
556
|
-
proxyServer.listen(port, () => {
|
|
557
|
-
console.log('\x1b[32m%s\x1b[0m', ' Servers running!\n');
|
|
558
|
-
console.log(` Your app: \x1b[36mhttp://localhost:${port}\x1b[0m`);
|
|
559
|
-
console.log(` Dashboard: \x1b[36mhttp://localhost:${dashboardPort}\x1b[0m\n`);
|
|
560
|
-
console.log(' Press \x1b[33mCtrl+C\x1b[0m to stop\n');
|
|
561
|
-
|
|
562
|
-
// Open dashboard in browser
|
|
563
|
-
const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
564
|
-
require('child_process').exec(`${open} http://localhost:${dashboardPort}`);
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
proxyServer.on('error', (err) => {
|
|
560
|
+
server.on('error', (err) => {
|
|
569
561
|
if (err.code === 'EADDRINUSE') {
|
|
570
562
|
console.error(`\x1b[31m Error: Port ${port} is already in use\x1b[0m`);
|
|
571
563
|
process.exit(1);
|
|
572
564
|
}
|
|
573
|
-
console.error('
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
dashboardServer.on('error', (err) => {
|
|
577
|
-
if (err.code === 'EADDRINUSE') {
|
|
578
|
-
console.error(`\x1b[31m Error: Port ${dashboardPort} is already in use\x1b[0m`);
|
|
579
|
-
process.exit(1);
|
|
580
|
-
}
|
|
581
|
-
console.error('Dashboard server error:', err);
|
|
565
|
+
console.error('Server error:', err);
|
|
582
566
|
});
|