@dpkrn/nodetunnel 1.0.9 → 1.1.0

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,1012 @@
1
+ /**
2
+ * Traffic inspector UI: WebSocket live feed, history, replay, URL ↔ query params sync.
3
+ */
4
+ (function () {
5
+ 'use strict';
6
+
7
+ // --- State ---
8
+ var entries = Object.create(null);
9
+ /** Chronological order (oldest → newest), matches server /logs order. */
10
+ var entryOrder = [];
11
+ var selectedId = null;
12
+ var ws;
13
+ var lastRespRaw = '';
14
+ var hasResponseContent = false;
15
+ /** When true, skip URL→params blur sync (avoid fighting params→URL updates). */
16
+ var syncingUrlFromParams = false;
17
+
18
+ var el = {
19
+ logList: document.getElementById('logList'),
20
+ sidebar: document.getElementById('sidebar'),
21
+ resizerH: document.getElementById('resizerH'),
22
+ resizerV: document.getElementById('resizerV'),
23
+ paneReq: document.getElementById('paneReq'),
24
+ paneResp: document.getElementById('paneResp'),
25
+ splitVWrap: document.getElementById('splitVWrap'),
26
+ method: document.getElementById('method'),
27
+ url: document.getElementById('url'),
28
+ reqBody: document.getElementById('reqBody'),
29
+ paramsTbody: document.getElementById('params-tbody'),
30
+ headersTbody: document.getElementById('headers-tbody'),
31
+ paramsBadge: document.getElementById('params-badge'),
32
+ headersBadge: document.getElementById('headers-badge'),
33
+ authType: document.getElementById('authType'),
34
+ authToken: document.getElementById('authToken'),
35
+ authBearerBlock: document.getElementById('auth-bearer-block'),
36
+ authBasicBlock: document.getElementById('auth-basic-block'),
37
+ authApikeyBlock: document.getElementById('auth-apikey-block'),
38
+ authBasicUser: document.getElementById('authBasicUser'),
39
+ authBasicPass: document.getElementById('authBasicPass'),
40
+ authApiKeyName: document.getElementById('authApiKeyName'),
41
+ authApiKeyValue: document.getElementById('authApiKeyValue'),
42
+ scriptsPre: document.getElementById('scriptsPre'),
43
+ scriptsPost: document.getElementById('scriptsPost'),
44
+ respBody: document.getElementById('respBody'),
45
+ respMeta: document.getElementById('respMeta'),
46
+ targetBase: document.getElementById('targetBase'),
47
+ wsStatus: document.getElementById('wsStatus'),
48
+ btnReplay: document.getElementById('btnReplay'),
49
+ btnReset: document.getElementById('btnReset'),
50
+ originHint: document.getElementById('origin-hint'),
51
+ themeSelect: document.getElementById('themeSelect'),
52
+ emptyResponse: document.getElementById('emptyResponse'),
53
+ responseStatus: document.getElementById('responseStatus'),
54
+ statusBadge: document.getElementById('statusBadge'),
55
+ respTime: document.getElementById('respTime'),
56
+ respSize: document.getElementById('respSize'),
57
+ respHeadersTable: document.getElementById('respHeadersTable'),
58
+ respCookies: document.getElementById('respCookies'),
59
+ respConsole: document.getElementById('respConsole'),
60
+ fmtPretty: document.getElementById('fmtPretty'),
61
+ fmtRaw: document.getElementById('fmtRaw'),
62
+ btnCopyResp: document.getElementById('btnCopyResp'),
63
+ logReplayToHistory: document.getElementById('logReplayToHistory'),
64
+ sidebarOrderBtn: document.getElementById('sidebarOrderBtn'),
65
+ sidebarSearch: document.getElementById('sidebarSearch')
66
+ };
67
+
68
+ var TARGET_KEY = 'inspectorReplayBase';
69
+ var SIDEBAR_W = 'inspectorSidebarW';
70
+ var SPLIT_RATIO = 'inspectorSplitRatio';
71
+ var THEME_KEY = 'inspectorTheme';
72
+ var LOG_REPLAY_KEY = 'inspectorLogReplayToHistory';
73
+ var HISTORY_ORDER_KEY = 'inspectorHistoryOrder';
74
+ var HEADER_LOG_REPLAY = 'X-Inspector-Log-Replay';
75
+
76
+ /** Digits only; set in inspector.html from the tunnel’s forward port when embedded. */
77
+ function localAppPortForDefault() {
78
+ var p =
79
+ typeof window !== 'undefined' && window.__LOCAL_APP_PORT__
80
+ ? String(window.__LOCAL_APP_PORT__).trim()
81
+ : '';
82
+ if (!/^\d+$/.test(p)) p = '8080';
83
+ return p;
84
+ }
85
+
86
+ /** Used when “Replay base” is empty; same host the tunnel uses for your local app (not the inspector UI port). */
87
+ var DEFAULT_REPLAY_BASE = 'http://localhost:' + localAppPortForDefault();
88
+
89
+ /** Sidebar and URL bar: path + query only, never http://host:port (avoids showing the inspector origin). */
90
+ function requestPathForDisplay(s) {
91
+ if (s == null || s === '') return '/';
92
+ s = String(s).trim();
93
+ if (!s) return '/';
94
+ if (/^https?:\/\//i.test(s)) {
95
+ try {
96
+ var u = new URL(s);
97
+ return u.pathname + u.search + (u.hash || '');
98
+ } catch (e) {
99
+ return s;
100
+ }
101
+ }
102
+ return s.startsWith('/') ? s : '/' + s;
103
+ }
104
+
105
+ function replayBaseURL() {
106
+ var b = (el.targetBase.value || '').trim().replace(/\/$/, '');
107
+ return b || DEFAULT_REPLAY_BASE;
108
+ }
109
+
110
+ /** Full http(s) URL for replay and for URL(). Relative paths like /name?q=1 become http://host:port/name?q=1 */
111
+ function absolutizeForReplay(urlStr) {
112
+ var s = (urlStr || '').trim();
113
+ if (!s) return s;
114
+ if (/^https?:\/\//i.test(s)) return s;
115
+ var base = replayBaseURL();
116
+ return base + (s.startsWith('/') ? s : '/' + s);
117
+ }
118
+
119
+ function parseURLWithReplayBase(urlStr) {
120
+ var s = (urlStr || '').trim();
121
+ if (!s) throw new Error('empty url');
122
+ if (/^https?:\/\//i.test(s)) return new URL(s);
123
+ return new URL(s, replayBaseURL() + '/');
124
+ }
125
+
126
+ function inspectorHTTPBase() {
127
+ var q = new URLSearchParams(location.search);
128
+ var api = q.get('api');
129
+ if (api) return api.replace(/\/$/, '');
130
+ var port = q.get('port');
131
+ if (port) return 'http://127.0.0.1:' + port;
132
+ if (location.host) return location.protocol + '//' + location.host;
133
+ return 'http://127.0.0.1:4040';
134
+ }
135
+
136
+ function initTheme() {
137
+ if (!el.themeSelect) return;
138
+ if (
139
+ typeof window !== 'undefined' &&
140
+ window.__NT_INSPECTOR_THEME_SEED__
141
+ ) {
142
+ var seed = String(window.__NT_INSPECTOR_THEME_SEED__).trim();
143
+ if (
144
+ (seed === 'terminal' || seed === 'postman') &&
145
+ !localStorage.getItem(THEME_KEY)
146
+ ) {
147
+ localStorage.setItem(THEME_KEY, seed);
148
+ }
149
+ }
150
+ var t = localStorage.getItem(THEME_KEY) || 'postman';
151
+ if (t !== 'postman' && t !== 'terminal') t = 'postman';
152
+ document.documentElement.setAttribute('data-theme', t);
153
+ el.themeSelect.value = t;
154
+ el.themeSelect.addEventListener('change', function () {
155
+ var v = el.themeSelect.value;
156
+ document.documentElement.setAttribute('data-theme', v);
157
+ localStorage.setItem(THEME_KEY, v);
158
+ });
159
+ }
160
+
161
+ function bytesToUtf8(body) {
162
+ if (body == null || body === '') return '';
163
+ if (typeof body === 'string') {
164
+ try {
165
+ var bin = atob(body);
166
+ var bytes = new Uint8Array(bin.length);
167
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
168
+ return new TextDecoder().decode(bytes);
169
+ } catch (e) {
170
+ return body;
171
+ }
172
+ }
173
+ return String(body);
174
+ }
175
+
176
+ function prettyJSON(raw) {
177
+ if (raw == null || raw === '') return '';
178
+ try {
179
+ return JSON.stringify(JSON.parse(raw), null, 2);
180
+ } catch (e) {
181
+ return raw;
182
+ }
183
+ }
184
+
185
+ function formatBodySize(bodyField) {
186
+ if (bodyField == null) return '—';
187
+ if (typeof bodyField === 'string') {
188
+ try {
189
+ var bin = atob(bodyField);
190
+ return formatBytes(bin.length);
191
+ } catch (e) {
192
+ return formatBytes(new TextEncoder().encode(bodyField).length);
193
+ }
194
+ }
195
+ return '—';
196
+ }
197
+
198
+ function formatBytes(n) {
199
+ if (n < 1024) return n + ' B';
200
+ if (n < 1024 * 1024) return (n / 1024).toFixed(2) + ' KB';
201
+ return (n / (1024 * 1024)).toFixed(2) + ' MB';
202
+ }
203
+
204
+ function clearTbody(tb) {
205
+ tb.innerHTML = '';
206
+ }
207
+
208
+ function bindTableRow(tr, tbody) {
209
+ var isParams = tbody === el.paramsTbody;
210
+ var del = tr.querySelector('.param-del');
211
+ if (del) {
212
+ del.addEventListener('click', function () {
213
+ tr.remove();
214
+ ensureTrailingEmptyRow(tbody);
215
+ updateBadges();
216
+ if (isParams) syncUrlSearchFromParams();
217
+ });
218
+ }
219
+ tr.querySelectorAll('.param-input, .param-check input').forEach(function (inp) {
220
+ inp.addEventListener('input', function () {
221
+ updateBadges();
222
+ if (isParams) syncUrlSearchFromParams();
223
+ });
224
+ inp.addEventListener('change', function () {
225
+ updateBadges();
226
+ if (isParams) syncUrlSearchFromParams();
227
+ });
228
+ });
229
+ }
230
+
231
+ function makeRow(key, value, checked, desc) {
232
+ var tr = document.createElement('tr');
233
+ tr.innerHTML =
234
+ '<td class="param-check"><input type="checkbox"' + (checked ? ' checked' : '') + '></td>' +
235
+ '<td><input class="param-input" type="text" placeholder="Key" value="' + escapeAttr(key) + '"></td>' +
236
+ '<td><input class="param-input" type="text" placeholder="Value" value="' + escapeAttr(value) + '"></td>' +
237
+ '<td><input class="param-input desc" type="text" placeholder="Description" value="' + escapeAttr(desc) + '"></td>' +
238
+ '<td><button type="button" class="param-del" aria-label="Remove">×</button></td>';
239
+ return tr;
240
+ }
241
+
242
+ function escapeAttr(s) {
243
+ return String(s == null ? '' : s)
244
+ .replace(/&/g, '&amp;')
245
+ .replace(/"/g, '&quot;')
246
+ .replace(/</g, '&lt;');
247
+ }
248
+
249
+ function ensureTrailingEmptyRow(tbody) {
250
+ var rows = tbody.querySelectorAll('tr');
251
+ var last = rows[rows.length - 1];
252
+ if (!last) {
253
+ tbody.appendChild(makeRow('', '', false, ''));
254
+ bindTableRow(tbody.lastElementChild, tbody);
255
+ return;
256
+ }
257
+ var inputs = last.querySelectorAll('.param-input');
258
+ var key = inputs[0] && inputs[0].value.trim();
259
+ var hasMore = rows.length > 1;
260
+ if (key || !hasMore) {
261
+ tbody.appendChild(makeRow('', '', false, ''));
262
+ bindTableRow(tbody.lastElementChild, tbody);
263
+ }
264
+ }
265
+
266
+ function addParamRow(key, value, checked, desc) {
267
+ var tr = makeRow(key, value, checked, desc);
268
+ el.paramsTbody.appendChild(tr);
269
+ bindTableRow(tr, el.paramsTbody);
270
+ updateBadges();
271
+ }
272
+
273
+ function addHeaderRow(key, value, checked, desc) {
274
+ var tr = makeRow(key, value, checked, desc);
275
+ el.headersTbody.appendChild(tr);
276
+ bindTableRow(tr, el.headersTbody);
277
+ updateBadges();
278
+ }
279
+
280
+ /** Rewrite only the query string from the Params table (path + origin unchanged). */
281
+ function syncUrlSearchFromParams() {
282
+ if (syncingUrlFromParams) return;
283
+ var raw = el.url.value.trim();
284
+ if (!raw) return;
285
+ try {
286
+ var u = parseURLWithReplayBase(raw);
287
+ var rows = readKeyValueRows(el.paramsTbody).filter(function (r) {
288
+ return r.enabled && r.key;
289
+ });
290
+ var q = new URLSearchParams();
291
+ rows.forEach(function (r) {
292
+ q.append(r.key, r.value);
293
+ });
294
+ u.search = q.toString();
295
+ syncingUrlFromParams = true;
296
+ el.url.value = u.pathname + u.search + (u.hash || '');
297
+ syncingUrlFromParams = false;
298
+ } catch (e) {}
299
+ }
300
+
301
+ function fillParamsFromURL(urlStr) {
302
+ if (syncingUrlFromParams) return;
303
+ clearTbody(el.paramsTbody);
304
+ try {
305
+ var u = parseURLWithReplayBase(urlStr);
306
+ u.searchParams.forEach(function (val, key) {
307
+ var tr = makeRow(key, val, true, '');
308
+ el.paramsTbody.appendChild(tr);
309
+ bindTableRow(tr, el.paramsTbody);
310
+ });
311
+ } catch (e) {}
312
+ ensureTrailingEmptyRow(el.paramsTbody);
313
+ updateBadges();
314
+ }
315
+
316
+ function fillHeadersTable(h) {
317
+ clearTbody(el.headersTbody);
318
+ if (h && typeof h === 'object') {
319
+ Object.keys(h).forEach(function (k) {
320
+ (h[k] || []).forEach(function (v) {
321
+ var tr = makeRow(k, v, true, '');
322
+ el.headersTbody.appendChild(tr);
323
+ bindTableRow(tr, el.headersTbody);
324
+ });
325
+ });
326
+ }
327
+ ensureTrailingEmptyRow(el.headersTbody);
328
+ updateBadges();
329
+ }
330
+
331
+ function readKeyValueRows(tbody) {
332
+ var out = [];
333
+ tbody.querySelectorAll('tr').forEach(function (tr) {
334
+ var cb = tr.querySelector('.param-check input');
335
+ var inputs = tr.querySelectorAll('.param-input');
336
+ var key = inputs[0] && inputs[0].value.trim();
337
+ var val = inputs[1] ? inputs[1].value : '';
338
+ out.push({ enabled: cb && cb.checked, key: key, value: val });
339
+ });
340
+ return out;
341
+ }
342
+
343
+ function headersFromTable() {
344
+ var h = {};
345
+ readKeyValueRows(el.headersTbody).forEach(function (r) {
346
+ if (!r.enabled || !r.key) return;
347
+ if (!h[r.key]) h[r.key] = [];
348
+ h[r.key].push(r.value);
349
+ });
350
+ return h;
351
+ }
352
+
353
+ function mergeAuthHeaders(headers) {
354
+ var t = el.authType.value;
355
+ var copy = JSON.parse(JSON.stringify(headers));
356
+ if (t === 'bearer' && el.authToken.value.trim()) {
357
+ copy['Authorization'] = ['Bearer ' + el.authToken.value.trim()];
358
+ } else if (t === 'basic' && el.authBasicUser.value) {
359
+ var raw = el.authBasicUser.value + ':' + (el.authBasicPass.value || '');
360
+ copy['Authorization'] = ['Basic ' + btoa(unescape(encodeURIComponent(raw)))];
361
+ } else if (t === 'apikey' && el.authApiKeyName.value.trim()) {
362
+ copy[el.authApiKeyName.value.trim()] = [el.authApiKeyValue.value || ''];
363
+ }
364
+ return copy;
365
+ }
366
+
367
+ function buildUrlWithQueryParams(urlStr) {
368
+ var rows = readKeyValueRows(el.paramsTbody).filter(function (r) {
369
+ return r.enabled && r.key;
370
+ });
371
+ var u;
372
+ try {
373
+ u = new URL(absolutizeForReplay(urlStr));
374
+ } catch (e) {
375
+ return absolutizeForReplay(urlStr);
376
+ }
377
+ var q = new URLSearchParams();
378
+ rows.forEach(function (r) {
379
+ q.append(r.key, r.value);
380
+ });
381
+ u.search = q.toString();
382
+ return u.href;
383
+ }
384
+
385
+ function updateBadges() {
386
+ var p = readKeyValueRows(el.paramsTbody).filter(function (r) {
387
+ return r.enabled && r.key;
388
+ }).length;
389
+ var h = readKeyValueRows(el.headersTbody).filter(function (r) {
390
+ return r.enabled && r.key;
391
+ }).length;
392
+ el.paramsBadge.textContent = String(p);
393
+ el.headersBadge.textContent = String(h);
394
+ }
395
+
396
+ function collectReplayPayload() {
397
+ var headers = mergeAuthHeaders(headersFromTable());
398
+ return {
399
+ method: el.method.value,
400
+ url: buildUrlWithQueryParams(el.url.value.trim()),
401
+ headers: headers,
402
+ body: el.reqBody.value
403
+ };
404
+ }
405
+
406
+ function durationOf(entry) {
407
+ if (entry.durationMs != null) return entry.durationMs;
408
+ if (entry.response && entry.response.durationMs != null) return entry.response.durationMs;
409
+ return '';
410
+ }
411
+
412
+ function methodClass(m) {
413
+ m = (m || 'GET').toUpperCase();
414
+ if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(m) < 0) return 'GET';
415
+ return m;
416
+ }
417
+
418
+ function statusClass(code) {
419
+ var c = parseInt(code, 10);
420
+ if (c >= 200 && c < 300) return 'st-2xx';
421
+ if (c >= 400) return 'st-4xx';
422
+ return '';
423
+ }
424
+
425
+ function entryMatchesSearch(entry, q) {
426
+ if (!q) return true;
427
+ var req = entry.request || {};
428
+ var res = entry.response || {};
429
+ var hay =
430
+ (entry.id || '') +
431
+ ' ' +
432
+ (req.method || '') +
433
+ ' ' +
434
+ requestPathForDisplay(req.path || '/') +
435
+ ' ' +
436
+ String(res.statusCode != null ? res.statusCode : '') +
437
+ ' ' +
438
+ bytesToUtf8(req.body || '').slice(0, 2000) +
439
+ ' ' +
440
+ bytesToUtf8(res.body || '').slice(0, 800);
441
+ return hay.toLowerCase().indexOf(q) !== -1;
442
+ }
443
+
444
+ function renderSidebar() {
445
+ if (!el.logList) return;
446
+ var q = (el.sidebarSearch && el.sidebarSearch.value ? el.sidebarSearch.value : '').trim().toLowerCase();
447
+ var newestFirst = historyOrderNewestFirst();
448
+ var ids = [];
449
+ var i;
450
+ for (i = 0; i < entryOrder.length; i++) {
451
+ var id = entryOrder[i];
452
+ var entry = entries[id];
453
+ if (!entry) continue;
454
+ if (!entryMatchesSearch(entry, q)) continue;
455
+ ids.push(id);
456
+ }
457
+ if (newestFirst) {
458
+ ids.reverse();
459
+ }
460
+ el.logList.innerHTML = '';
461
+ for (i = 0; i < ids.length; i++) {
462
+ el.logList.appendChild(buildListItemElement(entries[ids[i]]));
463
+ }
464
+ Array.prototype.forEach.call(el.logList.querySelectorAll('li'), function (li) {
465
+ li.classList.toggle('active', selectedId && li.getAttribute('data-id') === selectedId);
466
+ });
467
+ }
468
+
469
+ function buildListItemElement(entry) {
470
+ var req = entry.request || {};
471
+ var res = entry.response || {};
472
+ var ms = durationOf(entry);
473
+ var st = res.statusCode != null ? res.statusCode : '—';
474
+ var disp = requestPathForDisplay(req.path || '/');
475
+ var li = document.createElement('li');
476
+ li.setAttribute('data-id', entry.id);
477
+ li.innerHTML =
478
+ '<span class="m ' + methodClass(req.method) + '">' + escapeHtml((req.method || '—').toUpperCase()) + '</span>' +
479
+ '<span class="path" title="' + escapeHtml(disp) + '">' + escapeHtml(disp) + '</span>' +
480
+ '<span class="ms">' + escapeHtml(ms !== '' ? ms + 'ms' : '—') + '</span>' +
481
+ '<span class="st ' + statusClass(st) + '">' + escapeHtml(st) + '</span>';
482
+ var eid = entry.id;
483
+ li.addEventListener('click', function () {
484
+ selectEntry(eid);
485
+ });
486
+ return li;
487
+ }
488
+
489
+ function registerEntry(entry) {
490
+ if (!entry || !entry.id) return;
491
+ if (!entries[entry.id]) entryOrder.push(entry.id);
492
+ entries[entry.id] = entry;
493
+ renderSidebar();
494
+ }
495
+
496
+ function upsertListItem(entry) {
497
+ registerEntry(entry);
498
+ }
499
+
500
+ function escapeHtml(s) {
501
+ if (s == null || s === undefined) return '';
502
+ return String(s)
503
+ .replace(/&/g, '&amp;')
504
+ .replace(/</g, '&lt;')
505
+ .replace(/>/g, '&gt;')
506
+ .replace(/"/g, '&quot;');
507
+ }
508
+
509
+ function selectEntry(id) {
510
+ selectedId = id;
511
+ Array.prototype.forEach.call(el.logList.querySelectorAll('li'), function (li) {
512
+ li.classList.toggle('active', li.getAttribute('data-id') === id);
513
+ });
514
+ loadEntryIntoEditors(id);
515
+ }
516
+
517
+ function loadEntryIntoEditors(id) {
518
+ var entry = entries[id];
519
+ if (!entry) {
520
+ fetch(inspectorHTTPBase() + '/log?id=' + encodeURIComponent(id))
521
+ .then(function (r) { return r.json(); })
522
+ .then(function (data) {
523
+ entries[id] = data;
524
+ applyEntryToEditors(data);
525
+ })
526
+ .catch(function () {});
527
+ return;
528
+ }
529
+ applyEntryToEditors(entry);
530
+ }
531
+
532
+ /** Copy Authorization into auth panel and return headers without Authorization for the table. */
533
+ function authFromHeadersAndStrip(headers) {
534
+ el.authType.value = 'none';
535
+ el.authToken.value = '';
536
+ el.authBasicUser.value = '';
537
+ el.authBasicPass.value = '';
538
+ el.authApiKeyName.value = '';
539
+ el.authApiKeyValue.value = '';
540
+ var h = headers && typeof headers === 'object' ? JSON.parse(JSON.stringify(headers)) : {};
541
+ var raw = (h.Authorization || h.authorization || [])[0] || '';
542
+ delete h.Authorization;
543
+ delete h.authorization;
544
+ if (raw.indexOf('Bearer ') === 0) {
545
+ el.authType.value = 'bearer';
546
+ el.authToken.value = raw.slice(7).trim();
547
+ } else if (raw.indexOf('Basic ') === 0) {
548
+ try {
549
+ var dec = atob(raw.slice(6).trim());
550
+ var ix = dec.indexOf(':');
551
+ el.authType.value = 'basic';
552
+ el.authBasicUser.value = ix >= 0 ? dec.slice(0, ix) : dec;
553
+ el.authBasicPass.value = ix >= 0 ? dec.slice(ix + 1) : '';
554
+ } catch (e) {}
555
+ }
556
+ showAuthBlocks();
557
+ return h;
558
+ }
559
+
560
+ function applyEntryToEditors(entry) {
561
+ var req = entry.request || {};
562
+ var res = entry.response || {};
563
+ el.method.value = (req.method || 'GET').toUpperCase();
564
+ var path = requestPathForDisplay(req.path || '/');
565
+ el.url.value = path;
566
+ fillParamsFromURL(absolutizeForReplay(path));
567
+ fillHeadersTable(authFromHeadersAndStrip(req.headers));
568
+ el.reqBody.value = bytesToUtf8(req.body);
569
+
570
+ var capMs = durationOf(entry);
571
+ el.respMeta.innerHTML =
572
+ 'Captured: <strong>' + escapeHtml(capMs !== '' ? capMs + ' ms' : '—') + '</strong>' +
573
+ ' · status <strong>' + escapeHtml(res.statusCode != null ? res.statusCode : '—') + '</strong>';
574
+
575
+ showResponsePanels(res, capMs, 'captured');
576
+ logConsole('Loaded request ' + escapeHtml(entry.id) + ' from history.');
577
+ }
578
+
579
+ function fillRespHeadersTable(headers) {
580
+ el.respHeadersTable.innerHTML = '';
581
+ if (!headers) return;
582
+ Object.keys(headers).forEach(function (k) {
583
+ (headers[k] || []).forEach(function (v) {
584
+ var tr = document.createElement('tr');
585
+ tr.innerHTML =
586
+ '<td class="kv-key">' + escapeHtml(k) + '</td>' +
587
+ '<td>' + escapeHtml(v) + '</td>';
588
+ el.respHeadersTable.appendChild(tr);
589
+ });
590
+ });
591
+ }
592
+
593
+ function parseSetCookies(headers) {
594
+ if (!headers) return [];
595
+ var raw = headers['Set-Cookie'] || headers['set-cookie'];
596
+ if (!raw) return [];
597
+ return raw;
598
+ }
599
+
600
+ function showResponsePanels(res, durationMs, kind) {
601
+ el.emptyResponse.setAttribute('hidden', '');
602
+ el.responseStatus.hidden = false;
603
+ var code = res.statusCode != null ? res.statusCode : '—';
604
+ el.statusBadge.textContent = String(code);
605
+ el.statusBadge.className = 'status-badge';
606
+ var c = parseInt(code, 10);
607
+ if (!isNaN(c)) {
608
+ if (c >= 200 && c < 300) el.statusBadge.classList.add('st-2xx');
609
+ else if (c >= 400) el.statusBadge.classList.add('st-err');
610
+ }
611
+ el.respTime.textContent = durationMs !== '' && durationMs != null ? durationMs + ' ms' : '—';
612
+ el.respSize.textContent = formatBodySize(res.body);
613
+
614
+ fillRespHeadersTable(res.headers);
615
+ var cookies = parseSetCookies(res.headers);
616
+ if (cookies && cookies.length) {
617
+ el.respCookies.innerHTML = cookies.map(function (c) {
618
+ return '<div class="cookie-line">' + escapeHtml(c) + '</div>';
619
+ }).join('');
620
+ } else {
621
+ el.respCookies.textContent = 'No Set-Cookie headers on this response.';
622
+ }
623
+
624
+ lastRespRaw = bytesToUtf8(res.body);
625
+ setBodyFormatPretty();
626
+ hasResponseContent = true;
627
+ switchRespTab('body');
628
+ }
629
+
630
+ function setBodyFormatPretty() {
631
+ el.fmtPretty.classList.add('active');
632
+ el.fmtRaw.classList.remove('active');
633
+ el.respBody.value = prettyJSON(lastRespRaw);
634
+ }
635
+
636
+ function setBodyFormatRaw() {
637
+ el.fmtRaw.classList.add('active');
638
+ el.fmtPretty.classList.remove('active');
639
+ el.respBody.value = lastRespRaw;
640
+ }
641
+
642
+ function showEmptyResponseState() {
643
+ hasResponseContent = false;
644
+ el.emptyResponse.removeAttribute('hidden');
645
+ el.responseStatus.hidden = true;
646
+ el.respMeta.textContent = '';
647
+ lastRespRaw = '';
648
+ el.respBody.value = '';
649
+ el.respHeadersTable.innerHTML = '';
650
+ el.respCookies.textContent = 'No cookies parsed (Set-Cookie appears in Headers).';
651
+ document.getElementById('resp-panel-body').hidden = true;
652
+ document.getElementById('resp-panel-headers').hidden = true;
653
+ document.getElementById('resp-panel-cookies').hidden = true;
654
+ document.getElementById('resp-panel-console').hidden = true;
655
+ }
656
+
657
+ function reloadSelectionFromCapture() {
658
+ if (!selectedId) return;
659
+ delete entries[selectedId];
660
+ loadEntryIntoEditors(selectedId);
661
+ logConsole('Reset: reloaded captured request from server.');
662
+ }
663
+
664
+ function logConsole(line) {
665
+ var t = new Date().toISOString();
666
+ el.respConsole.textContent += '[' + t + '] ' + line + '\n';
667
+ }
668
+
669
+ function switchConfigPanel(panel) {
670
+ document.querySelectorAll('.config-tab').forEach(function (tab) {
671
+ tab.classList.toggle('active', tab.getAttribute('data-panel') === panel);
672
+ });
673
+ document.querySelectorAll('.config-panel').forEach(function (p) {
674
+ p.hidden = p.id !== 'panel-' + panel;
675
+ });
676
+ }
677
+
678
+ function switchRespTab(name) {
679
+ document.querySelectorAll('.resp-tab').forEach(function (tab) {
680
+ tab.classList.toggle('active', tab.getAttribute('data-resp') === name);
681
+ });
682
+ if (!hasResponseContent) return;
683
+ document.getElementById('resp-panel-body').hidden = name !== 'body';
684
+ document.getElementById('resp-panel-headers').hidden = name !== 'headers';
685
+ document.getElementById('resp-panel-cookies').hidden = name !== 'cookies';
686
+ document.getElementById('resp-panel-console').hidden = name !== 'console';
687
+ }
688
+
689
+ function showAuthBlocks() {
690
+ var t = el.authType.value;
691
+ el.authBearerBlock.hidden = t !== 'bearer';
692
+ el.authBasicBlock.hidden = t !== 'basic';
693
+ el.authApikeyBlock.hidden = t !== 'apikey';
694
+ }
695
+
696
+ function historyOrderNewestFirst() {
697
+ return (localStorage.getItem(HISTORY_ORDER_KEY) || 'newest') !== 'oldest';
698
+ }
699
+
700
+ function syncSidebarOrderButton() {
701
+ if (!el.sidebarOrderBtn) return;
702
+ var newest = historyOrderNewestFirst();
703
+ el.sidebarOrderBtn.setAttribute('data-order', newest ? 'newest' : 'oldest');
704
+ el.sidebarOrderBtn.title = newest
705
+ ? 'Newest first — click for oldest first'
706
+ : 'Oldest first — click for newest first';
707
+ el.sidebarOrderBtn.setAttribute(
708
+ 'aria-label',
709
+ newest ? 'Newest first. Click to show oldest first.' : 'Oldest first. Click to show newest first.'
710
+ );
711
+ }
712
+
713
+ function initLogReplayToggle() {
714
+ if (!el.logReplayToHistory) return;
715
+ el.logReplayToHistory.checked = localStorage.getItem(LOG_REPLAY_KEY) === 'true';
716
+ el.logReplayToHistory.addEventListener('change', function () {
717
+ localStorage.setItem(LOG_REPLAY_KEY, el.logReplayToHistory.checked ? 'true' : 'false');
718
+ });
719
+ }
720
+
721
+ function initSidebarHistoryControls() {
722
+ if (el.sidebarOrderBtn) {
723
+ syncSidebarOrderButton();
724
+ el.sidebarOrderBtn.addEventListener('click', function () {
725
+ var nextNewest = !historyOrderNewestFirst();
726
+ localStorage.setItem(HISTORY_ORDER_KEY, nextNewest ? 'newest' : 'oldest');
727
+ syncSidebarOrderButton();
728
+ renderSidebar();
729
+ });
730
+ }
731
+ if (el.sidebarSearch) {
732
+ el.sidebarSearch.addEventListener('input', function () {
733
+ renderSidebar();
734
+ });
735
+ }
736
+ }
737
+
738
+ function connectWS() {
739
+ var base = inspectorHTTPBase();
740
+ var wsProto = base.indexOf('https:') === 0 ? 'wss:' : 'ws:';
741
+ var host = base.replace(/^https?:\/\//, '');
742
+ ws = new WebSocket(wsProto + '//' + host + '/ws');
743
+
744
+ ws.onopen = function () {
745
+ el.wsStatus.classList.add('live');
746
+ fetch(base + '/logs')
747
+ .then(function (r) { return r.json(); })
748
+ .then(function (logs) {
749
+ entries = Object.create(null);
750
+ entryOrder = [];
751
+ var j;
752
+ for (j = 0; j < logs.length; j++) {
753
+ var log = logs[j];
754
+ if (!log || !log.id) continue;
755
+ entries[log.id] = log;
756
+ entryOrder.push(log.id);
757
+ }
758
+ renderSidebar();
759
+ if (logs.length) {
760
+ selectEntry(logs[logs.length - 1].id);
761
+ } else {
762
+ showEmptyResponseState();
763
+ }
764
+ });
765
+ };
766
+
767
+ ws.onmessage = function (ev) {
768
+ var msg = JSON.parse(ev.data);
769
+ if (msg.eventType === 'request' && msg.payload && msg.payload.id) {
770
+ upsertListItem(msg.payload);
771
+ selectEntry(msg.payload.id);
772
+ }
773
+ };
774
+
775
+ ws.onclose = function () {
776
+ el.wsStatus.classList.remove('live');
777
+ setTimeout(connectWS, 1000);
778
+ };
779
+ ws.onerror = function () {};
780
+ }
781
+
782
+ function setupResizerH() {
783
+ if (!el.resizerH || !el.sidebar) return;
784
+ var w = localStorage.getItem(SIDEBAR_W);
785
+ if (w) el.sidebar.style.width = w + 'px';
786
+ el.resizerH.addEventListener('mousedown', function (e) {
787
+ e.preventDefault();
788
+ el.resizerH.classList.add('is-dragging');
789
+ var prevCursor = document.body.style.cursor;
790
+ document.body.style.cursor = 'ew-resize';
791
+ var startX = e.clientX;
792
+ var startW = el.sidebar.offsetWidth;
793
+ function move(ev) {
794
+ ev.preventDefault();
795
+ var nw = Math.max(180, Math.min(startW + (ev.clientX - startX), window.innerWidth * 0.55));
796
+ el.sidebar.style.width = nw + 'px';
797
+ }
798
+ function up() {
799
+ el.resizerH.classList.remove('is-dragging');
800
+ document.body.style.cursor = prevCursor;
801
+ localStorage.setItem(SIDEBAR_W, String(el.sidebar.offsetWidth));
802
+ document.removeEventListener('mousemove', move);
803
+ document.removeEventListener('mouseup', up);
804
+ }
805
+ document.addEventListener('mousemove', move);
806
+ document.addEventListener('mouseup', up);
807
+ });
808
+ }
809
+
810
+ function setupResizerV() {
811
+ if (!el.resizerV || !el.splitVWrap || !el.paneReq || !el.paneResp) return;
812
+
813
+ function getRatio() {
814
+ var ratio = parseFloat(localStorage.getItem(SPLIT_RATIO) || '0.45', 10);
815
+ if (isNaN(ratio) || ratio < 0.15 || ratio > 0.85) return 0.45;
816
+ return ratio;
817
+ }
818
+
819
+ function applySplitRatio() {
820
+ var ratio = getRatio();
821
+ var h = el.splitVWrap.clientHeight;
822
+ if (h < 80) return;
823
+ var topH = Math.round(h * ratio);
824
+ topH = Math.max(120, Math.min(topH, h - 160));
825
+ el.paneReq.style.flex = '0 0 ' + topH + 'px';
826
+ el.paneResp.style.flex = '1 1 0%';
827
+ el.paneResp.style.minHeight = '0';
828
+ }
829
+
830
+ applySplitRatio();
831
+ window.addEventListener('resize', applySplitRatio);
832
+ window.addEventListener('load', applySplitRatio);
833
+ requestAnimationFrame(function () {
834
+ applySplitRatio();
835
+ requestAnimationFrame(applySplitRatio);
836
+ });
837
+
838
+ el.resizerV.addEventListener('mousedown', function (e) {
839
+ e.preventDefault();
840
+ el.resizerV.classList.add('is-dragging');
841
+ var prevCursor = document.body.style.cursor;
842
+ document.body.style.cursor = 'ns-resize';
843
+ var startY = e.clientY;
844
+ var startTop = el.paneReq.getBoundingClientRect().height;
845
+ function move(ev) {
846
+ ev.preventDefault();
847
+ var wrapH = el.splitVWrap.clientHeight;
848
+ if (wrapH < 80) return;
849
+ var dy = ev.clientY - startY;
850
+ var nh = Math.max(120, Math.min(startTop + dy, wrapH - 160));
851
+ el.paneReq.style.flex = '0 0 ' + nh + 'px';
852
+ }
853
+ function up() {
854
+ el.resizerV.classList.remove('is-dragging');
855
+ document.body.style.cursor = prevCursor;
856
+ var wrapH = el.splitVWrap.clientHeight;
857
+ if (wrapH >= 80) {
858
+ var rect = el.paneReq.getBoundingClientRect();
859
+ var ratio = Math.max(0.15, Math.min(0.85, rect.height / wrapH));
860
+ localStorage.setItem(SPLIT_RATIO, String(ratio));
861
+ }
862
+ document.removeEventListener('mousemove', move);
863
+ document.removeEventListener('mouseup', up);
864
+ }
865
+ document.addEventListener('mousemove', move);
866
+ document.addEventListener('mouseup', up);
867
+ });
868
+ }
869
+
870
+ document.querySelectorAll('.config-tab').forEach(function (tab) {
871
+ tab.addEventListener('click', function () {
872
+ switchConfigPanel(tab.getAttribute('data-panel'));
873
+ });
874
+ });
875
+
876
+ document.querySelectorAll('.resp-tab').forEach(function (tab) {
877
+ tab.addEventListener('click', function () {
878
+ switchRespTab(tab.getAttribute('data-resp'));
879
+ });
880
+ });
881
+
882
+ document.querySelectorAll('.script-subtab').forEach(function (btn) {
883
+ btn.addEventListener('click', function () {
884
+ var which = btn.getAttribute('data-script');
885
+ document.querySelectorAll('.script-subtab').forEach(function (b) {
886
+ b.classList.toggle('active', b === btn);
887
+ });
888
+ el.scriptsPre.hidden = which !== 'pre';
889
+ el.scriptsPost.hidden = which !== 'post';
890
+ });
891
+ });
892
+
893
+ el.authType.addEventListener('change', showAuthBlocks);
894
+
895
+ document.getElementById('add-param-row').addEventListener('click', function () {
896
+ addParamRow('', '', false, '');
897
+ });
898
+ document.getElementById('add-header-row').addEventListener('click', function () {
899
+ addHeaderRow('', '', false, '');
900
+ });
901
+
902
+ el.fmtPretty.addEventListener('click', setBodyFormatPretty);
903
+ el.fmtRaw.addEventListener('click', setBodyFormatRaw);
904
+
905
+ el.btnCopyResp.addEventListener('click', function () {
906
+ var t = el.respBody.value;
907
+ if (navigator.clipboard && navigator.clipboard.writeText) {
908
+ navigator.clipboard.writeText(t).then(function () {
909
+ logConsole('Response body copied to clipboard.');
910
+ });
911
+ }
912
+ });
913
+
914
+ el.btnReplay.addEventListener('click', function () {
915
+ var payload = collectReplayPayload();
916
+ el.respMeta.textContent = 'Replaying…';
917
+ lastRespRaw = '';
918
+ el.respBody.value = '';
919
+ var replayHeaders = { 'Content-Type': 'application/json' };
920
+ replayHeaders[HEADER_LOG_REPLAY] = el.logReplayToHistory && el.logReplayToHistory.checked ? '1' : '0';
921
+ fetch(inspectorHTTPBase() + '/replay', {
922
+ method: 'POST',
923
+ headers: replayHeaders,
924
+ body: JSON.stringify(payload)
925
+ })
926
+ .then(function (r) {
927
+ return r.text().then(function (text) {
928
+ try {
929
+ return { j: JSON.parse(text) };
930
+ } catch (e) {
931
+ return { j: { error: text || 'bad response' } };
932
+ }
933
+ });
934
+ })
935
+ .then(function (_ref) {
936
+ var j = _ref.j;
937
+ if (j.error) {
938
+ hasResponseContent = true;
939
+ el.emptyResponse.setAttribute('hidden', '');
940
+ el.responseStatus.hidden = false;
941
+ el.statusBadge.textContent = 'Err';
942
+ el.statusBadge.className = 'status-badge st-err';
943
+ el.respTime.textContent = j.durationMs != null ? j.durationMs + ' ms' : '—';
944
+ el.respSize.textContent = '—';
945
+ el.respMeta.innerHTML = 'Replay failed: <strong>' + escapeHtml(j.error) + '</strong>';
946
+ el.respBody.value = '';
947
+ fillRespHeadersTable({});
948
+ logConsole('Replay error: ' + j.error);
949
+ switchRespTab('body');
950
+ return;
951
+ }
952
+ el.respMeta.innerHTML =
953
+ 'Live replay: <strong>' + (j.durationMs != null ? j.durationMs + ' ms' : '—') + '</strong>' +
954
+ ' · status <strong>' + escapeHtml(j.statusCode) + '</strong>';
955
+ showResponsePanels(
956
+ { statusCode: j.statusCode, headers: j.headers, body: j.body },
957
+ j.durationMs,
958
+ 'replay'
959
+ );
960
+ logConsole('Replay finished: HTTP ' + j.statusCode);
961
+ switchRespTab('body');
962
+ })
963
+ .catch(function (err) {
964
+ el.respMeta.textContent = 'Replay error: ' + err;
965
+ logConsole(String(err));
966
+ });
967
+ });
968
+
969
+ el.btnReset.addEventListener('click', reloadSelectionFromCapture);
970
+
971
+ el.targetBase.placeholder = DEFAULT_REPLAY_BASE;
972
+ var savedBase = localStorage.getItem(TARGET_KEY);
973
+ if (savedBase) el.targetBase.value = savedBase;
974
+ el.targetBase.addEventListener('change', function () {
975
+ localStorage.setItem(TARGET_KEY, el.targetBase.value);
976
+ });
977
+
978
+ el.url.addEventListener('blur', function () {
979
+ if (syncingUrlFromParams) return;
980
+ var t = el.url.value.trim();
981
+ if (t) el.url.value = requestPathForDisplay(t);
982
+ fillParamsFromURL(absolutizeForReplay(el.url.value.trim()));
983
+ });
984
+
985
+ if (!location.host) {
986
+ el.originHint.style.display = 'block';
987
+ el.originHint.textContent =
988
+ 'Open from the Go inspector (http://127.0.0.1:4040/) or use Live Server with ?port=4040 so API/WebSocket hit the inspector.';
989
+ } else {
990
+ var q = new URLSearchParams(location.search);
991
+ if (!q.get('port') && !q.get('api')) {
992
+ var p = location.port;
993
+ if (p && p !== '4040' && p !== '80' && p !== '443') {
994
+ el.originHint.style.display = 'block';
995
+ el.originHint.textContent =
996
+ 'Static server on port ' + p + ': add ?port=4040 (or ?api=http://127.0.0.1:4040) to use the Go inspector for traffic.';
997
+ }
998
+ }
999
+ }
1000
+
1001
+ ensureTrailingEmptyRow(el.paramsTbody);
1002
+ ensureTrailingEmptyRow(el.headersTbody);
1003
+ showAuthBlocks();
1004
+ showEmptyResponseState();
1005
+
1006
+ initTheme();
1007
+ setupResizerH();
1008
+ setupResizerV();
1009
+ initLogReplayToggle();
1010
+ initSidebarHistoryControls();
1011
+ connectWS();
1012
+ })();