shokupan 0.10.4 → 0.11.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.
Files changed (54) hide show
  1. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
  2. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
  3. package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
  4. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
  5. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
  6. package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
  7. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
  8. package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
  9. package/dist/cli.cjs +206 -18
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +206 -18
  12. package/dist/cli.js.map +1 -1
  13. package/dist/context.d.ts +6 -1
  14. package/dist/index.cjs +2405 -1008
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +2402 -1006
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +423 -30
  19. package/dist/plugins/application/api-explorer/static/style.css +351 -10
  20. package/dist/plugins/application/api-explorer/static/theme.css +7 -2
  21. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  22. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  23. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  24. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
  25. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  26. package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
  27. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  28. package/dist/plugins/application/dashboard/static/client.js +160 -0
  29. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  30. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  31. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  32. package/dist/plugins/application/dashboard/static/requests.js +868 -58
  33. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  34. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  35. package/dist/plugins/application/dashboard/static/theme.css +7 -2
  36. package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
  37. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  38. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  39. package/dist/router.d.ts +55 -16
  40. package/dist/shokupan.d.ts +7 -2
  41. package/dist/util/adapter/adapters.d.ts +19 -0
  42. package/dist/util/adapter/filesystem.d.ts +20 -0
  43. package/dist/util/controller-scanner.d.ts +4 -0
  44. package/dist/util/cpu-monitor.d.ts +2 -0
  45. package/dist/util/middleware-tracker.d.ts +10 -0
  46. package/dist/util/types.d.ts +37 -0
  47. package/package.json +5 -5
  48. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  49. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  50. package/dist/http-server-BEMPIs33.cjs +0 -85
  51. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  52. package/dist/http-server-CCeagTyU.js +0 -68
  53. package/dist/http-server-CCeagTyU.js.map +0 -1
  54. package/dist/plugins/application/dashboard/static/poll.js +0 -146
@@ -1,118 +1,928 @@
1
1
 
2
2
  // Initialize Requests Table
3
- let requestsTable;
3
+ window.requestsTable = null;
4
4
 
5
- document.addEventListener('DOMContentLoaded', () => {
6
- requestsTable = new Tabulator("#requests-list-container", {
5
+ // Filter State
6
+ // Initialize Filter State
7
+ let filterText = '';
8
+ let filterType = 'all';
9
+ let filterDirection = 'all';
10
+
11
+ function initRequests() {
12
+ console.log('[requests.js] Initializing...');
13
+
14
+ // Initialize Filter Listeners
15
+ const txtFilter = document.getElementById('network-filter-text');
16
+ const typeFilter = document.getElementById('network-filter-type');
17
+ const directionButtons = document.querySelectorAll('.filter-direction');
18
+
19
+ if (directionButtons) {
20
+ directionButtons.forEach(btn => {
21
+ btn.onclick = () => {
22
+ // Update active state
23
+ directionButtons.forEach(b => {
24
+ b.style.background = 'transparent';
25
+ b.style.color = 'var(--text-secondary)';
26
+ b.classList.remove('active');
27
+ });
28
+ btn.style.background = 'var(--bg-primary)';
29
+ btn.style.color = 'var(--text-primary)';
30
+ btn.classList.add('active');
31
+
32
+ filterDirection = btn.dataset.value;
33
+ if (window.requestsTable) window.requestsTable.setFilter(customFilter);
34
+ };
35
+ });
36
+ }
37
+
38
+ if (txtFilter) {
39
+ txtFilter.addEventListener('keyup', (e) => {
40
+ filterText = e.target.value.toLowerCase();
41
+ window.requestsTable.setFilter(customFilter);
42
+ });
43
+ }
44
+
45
+ if (typeFilter) {
46
+ typeFilter.addEventListener('change', (e) => {
47
+ filterType = e.target.value;
48
+ window.requestsTable.setFilter(customFilter);
49
+ });
50
+ }
51
+
52
+ // specific check for Tabulator
53
+ if (typeof Tabulator === 'undefined') {
54
+ console.error('Tabulator is not defined. Ensure it is loaded before requests.js');
55
+ return;
56
+ }
57
+
58
+ window.requestsTable = new Tabulator("#requests-list-container", {
7
59
  layout: "fitColumns",
8
60
  placeholder: "No requests found",
9
61
  selectable: 1,
62
+ resizableColumnFit: true,
63
+ height: "100%", // Fill container
64
+ index: "id",
65
+ rowHeight: 32, // Dense rows
66
+ initialSort: [
67
+ { column: "timestamp", dir: "desc" }
68
+ ],
10
69
  columns: [
11
- { title: "Method", field: "method", width: 100 },
12
- { title: "URL", field: "url" },
13
70
  {
14
71
  title: "Status",
15
72
  field: "status",
16
- width: 100,
73
+ width: 80,
17
74
  formatter: function (cell) {
18
75
  const status = cell.getValue();
19
- const color = status >= 500 ? 'red' : status >= 400 ? 'orange' : 'green';
20
- return `<span style="color: ${color}; font-weight: bold;">${status}</span>`;
76
+ if (!status) return '<span style="color: var(--text-secondary)">Pending</span>';
77
+ const color = status >= 500 ? '#ef4444' : status >= 400 ? '#f59e0b' : '#10b981';
78
+ return `<span style="display: inline-block; width: 10px; height: 10px; background: ${color}; border-radius: 50%; margin-right: 6px;"></span>${status}`;
21
79
  }
22
80
  },
23
- { title: "Duration (ms)", field: "duration", width: 150, formatter: (cell) => printDuration(cell.getValue()) },
24
81
  {
25
- title: "Time",
26
- field: "timestamp",
27
- width: 200,
82
+ title: "Method",
83
+ field: "method",
84
+ width: 80,
85
+ visible: true
86
+ },
87
+ {
88
+ title: "Name",
89
+ field: "url",
90
+ widthGrow: 2, // Take up more space
28
91
  formatter: function (cell) {
29
- return new Date(cell.getValue()).toLocaleString();
92
+ const url = cell.getValue();
93
+ // Extract name from URL
94
+ let name = url;
95
+ try {
96
+ const u = new URL(url, 'http://localhost');
97
+ name = u.pathname;
98
+ if (name === '/') name = 'localhost';
99
+ const parts = name.split('/');
100
+ const last = parts[parts.length - 1];
101
+ if (last) name = last;
102
+ } catch (e) { }
103
+
104
+ return `<div style="display: flex; flex-direction: column; line-height: 1.2;">
105
+ <span style="color: var(--text-secondary);">${name}</span>
106
+ </div>`;
30
107
  }
31
108
  },
32
109
  {
33
- title: "",
110
+ title: "Domain",
111
+ field: "domain",
112
+ width: 80,
113
+ visible: true
114
+ },
115
+ {
116
+ title: "Path",
117
+ field: "path",
118
+ width: 80,
119
+ visible: true
120
+ },
121
+ {
122
+ title: "URL",
123
+ field: "url",
34
124
  width: 80,
125
+ visible: true
126
+ },
127
+ {
128
+ title: "Protocol",
129
+ field: "protocol",
130
+ width: 80,
131
+ visible: true,
35
132
  formatter: function (cell) {
36
- const el = document.createElement("div");
37
- el.onclick = () => showRequestDetails(cell.getData());
38
- el.innerHTML = "View";
39
- return el;
133
+ const row = cell.getData();
134
+ // Prefer explicit protocol version (e.g. 1.1, h2) if available
135
+ if (row.protocol && row.protocol !== 'http' && row.protocol !== 'https') return row.protocol;
136
+ return row.scheme || row.protocol || '-';
40
137
  }
138
+ },
139
+ {
140
+ title: "Scheme",
141
+ field: "scheme",
142
+ width: 80,
143
+ visible: true
144
+ },
145
+ {
146
+ title: "Remote IP",
147
+ field: "remoteIP",
148
+ width: 80,
149
+ visible: true
150
+ },
151
+ {
152
+ title: "Initiator",
153
+ field: "direction",
154
+ width: 80,
155
+ formatter: (cell) => {
156
+ const dir = cell.getValue();
157
+ return dir === 'outbound' ? 'Server' : 'Client';
158
+ }
159
+ },
160
+ {
161
+ title: "Type",
162
+ field: "type",
163
+ width: 80,
164
+ formatter: (cell) => {
165
+ const r = cell.getData();
166
+ if (r.type === 'fetch') return 'fetch';
167
+ if (r.type === 'xhr') return 'xhr';
168
+ if (r.type === 'ws') return 'ws';
169
+ return r.contentType || 'document';
170
+ }
171
+ },
172
+ {
173
+ title: "Cookies",
174
+ field: "cookies",
175
+ width: 80,
176
+ visible: true
177
+ },
178
+ {
179
+ title: "Transferred",
180
+ field: "transferred",
181
+ width: 80,
182
+ visible: true
183
+ },
184
+ {
185
+ title: "Size",
186
+ field: "size",
187
+ width: 80,
188
+ formatter: (cell) => formatBytes(cell.getValue())
189
+ },
190
+ {
191
+ title: "Time",
192
+ field: "duration",
193
+ width: 80,
194
+ formatter: (cell) => cell.getValue() ? Math.round(cell.getValue()) + ' ms' : 'Pending'
195
+ },
196
+ {
197
+ title: "Waterfall",
198
+ field: "timestamp",
199
+ widthGrow: 1,
200
+ formatter: waterfallFormatter,
201
+ headerSort: false
41
202
  }
42
203
  ],
43
- data: []
204
+ data: [],
205
+ rowContextMenu: [
206
+ {
207
+ label: "Copy as fetch",
208
+ action: function (e, row) {
209
+ const data = row.getData();
210
+ const fetchCode = generateFetchCode(data);
211
+ copyToClipboard(fetchCode);
212
+ }
213
+ },
214
+ {
215
+ label: "Export as HAR",
216
+ action: function (e, row) {
217
+ const data = row.getData();
218
+ const har = generateHAR([data]);
219
+ downloadString(JSON.stringify(har, null, 2), `request-${data.id}.har`);
220
+ }
221
+ },
222
+ {
223
+ label: "Export All as HAR",
224
+ action: function (e, row) {
225
+ const allData = window.requestsTable.getData("active"); // get filtered data
226
+ const har = generateHAR(allData);
227
+ downloadString(JSON.stringify(har, null, 2), `requests-export.har`);
228
+ }
229
+ }
230
+ ]
231
+ });
232
+
233
+ // Row selection handler
234
+ window.requestsTable.on("rowClick", function (e, row) {
235
+ showRequestDetails(row.getData());
44
236
  });
45
237
 
46
- // Auto-fetch on load if tab is active (or just fetch initially)
238
+ // Auto-fetch on load
47
239
  fetchRequests();
48
- });
49
240
 
50
- function fetchRequests() {
241
+ // Resize Logic
242
+ initResizeHandle();
243
+ }
244
+
245
+ function initResizeHandle() {
246
+ const handle = document.getElementById('details-drag-handle');
247
+ const container = document.getElementById('request-details-container');
248
+ const listContainer = document.getElementById('requests-list-container');
249
+ if (!handle || !container) return;
250
+
251
+ let isResizing = false;
252
+ let startX, startWidth;
253
+
254
+ handle.addEventListener('mousedown', (e) => {
255
+ isResizing = true;
256
+ startX = e.clientX;
257
+ startWidth = container.offsetWidth;
258
+ document.body.style.cursor = 'col-resize';
259
+ e.preventDefault();
260
+ });
261
+
262
+ document.addEventListener('mousemove', (e) => {
263
+ if (!isResizing) return;
264
+ // Calculate new width: It's expanding to the left, so moving mouse left increases width
265
+ const dx = startX - e.clientX;
266
+ const newWidth = Math.max(300, Math.min(window.innerWidth - 100, startWidth + dx));
267
+ container.style.width = `${newWidth}px`;
268
+
269
+ // Optional: trigger tabulator redraw if list container size changed significantly (it flexes)
270
+ if (window.requestsTable) window.requestsTable.redraw();
271
+ });
272
+
273
+ document.addEventListener('mouseup', () => {
274
+ if (isResizing) {
275
+ isResizing = false;
276
+ document.body.style.cursor = '';
277
+ // Save width preference?
278
+ if (window.requestsTable) window.requestsTable.redraw();
279
+ }
280
+ });
281
+
282
+ }
283
+
284
+ function generateFetchCode(req) {
285
+ const headers = req.requestHeaders || {};
286
+ let code = `fetch("${req.url}", {\n`;
287
+ code += ` "method": "${req.method}",\n`;
288
+ code += ` "headers": ${JSON.stringify(headers, null, 2).replace(/\n/g, '\n ')},\n`;
289
+
290
+ if (req.requestBody) {
291
+ if (typeof req.requestBody === 'object') {
292
+ code += ` "body": JSON.stringify(${JSON.stringify(req.requestBody)}),\n`;
293
+ } else {
294
+ code += ` "body": ${JSON.stringify(req.requestBody)},\n`;
295
+ }
296
+ }
297
+ code += `});`;
298
+ return code;
299
+ }
300
+
301
+ function generateHAR(requests) {
302
+ return {
303
+ log: {
304
+ version: "1.2",
305
+ creator: { name: "Shokupan Dashboard", version: "1.0" },
306
+ entries: requests.map(req => ({
307
+ startedDateTime: new Date(req.timestamp).toISOString(),
308
+ time: req.duration,
309
+ request: {
310
+ method: req.method,
311
+ url: req.url,
312
+ httpVersion: req.protocol || "HTTP/1.1",
313
+ cookies: [], // Todo parse
314
+ headers: Object.entries(req.requestHeaders || {}).map(([name, value]) => ({ name, value })),
315
+ queryString: [], // Todo parse from url
316
+ postData: req.requestBody ? { mimeType: req.contentType || "application/json", text: JSON.stringify(req.requestBody) } : undefined,
317
+ headersSize: -1,
318
+ bodySize: -1
319
+ },
320
+ response: {
321
+ status: req.status,
322
+ statusText: "",
323
+ httpVersion: req.protocol || "HTTP/1.1",
324
+ cookies: [],
325
+ headers: Object.entries(req.responseHeaders || {}).map(([name, value]) => ({ name, value })),
326
+ content: {
327
+ size: req.size || 0,
328
+ mimeType: req.contentType || "",
329
+ text: typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
330
+ },
331
+ redirectURL: "",
332
+ headersSize: -1,
333
+ bodySize: -1
334
+ },
335
+ cache: {},
336
+ timings: {
337
+ send: 0,
338
+ wait: req.duration,
339
+ receive: 0
340
+ }
341
+ }))
342
+ }
343
+ };
344
+ }
345
+
346
+ function purgeRequests() {
347
+ if (!confirm("Are you sure you want to purge all captured requests?")) return;
348
+
51
349
  const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
350
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
351
+ // Need to handle if we are mounted at /dashboard vs /dashboard/ so stripping slice(-1) might be wrong if it wasn't there
352
+ // Safer:
353
+ let base = window.location.pathname;
354
+ if (base.endsWith('/')) base = base.slice(0, -1);
355
+
356
+ fetch(base + '/requests', {
357
+ method: 'DELETE',
358
+ headers
359
+ })
360
+ .then(res => res.json())
361
+ .then(data => {
362
+ if (data.success) {
363
+ console.log("Purge successful");
364
+ if (window.requestsTable) window.requestsTable.clearData();
365
+ closeRequestDetails();
366
+ }
367
+ })
368
+ .catch(console.error);
369
+ }
370
+
371
+ // Robust initialization
372
+ let initAttempts = 0;
373
+ function tryInit() {
374
+ if (document.getElementById('requests-list-container') && typeof Tabulator !== 'undefined') {
375
+ try {
376
+ initRequests();
377
+ } catch (e) {
378
+ console.error('Failed to initialize requests table:', e);
379
+ const el = document.getElementById('requests-list-container');
380
+ if (el) el.innerHTML = `<div style="padding: 2rem; color: #ef4444;">Failed to initialize: ${e.message}</div>`;
381
+ }
382
+ } else {
383
+ initAttempts++;
384
+ if (initAttempts > 50) { // 5 seconds timeout
385
+ console.error('Request table initialization timed out. Tabulator is:', typeof Tabulator);
386
+ const el = document.getElementById('requests-list-container');
387
+ if (el) el.innerHTML = `<div style="padding: 2rem; color: #ef4444;">
388
+ Failed to load dependencies. <br>
389
+ Tabulator: ${typeof Tabulator}
390
+ </div>`;
391
+ return;
392
+ }
393
+ setTimeout(tryInit, 100);
394
+ }
395
+ }
396
+
397
+ tryInit();
398
+
399
+
400
+ function customFilter(data) {
401
+ // Type Filter
402
+ if (filterType !== 'all') {
403
+ const type = data.type || 'xhr'; // default to xhr if missing
404
+ if (filterType === 'fetch' && type !== 'fetch') return false;
405
+ if (filterType === 'xhr' && type !== 'xhr') return false;
406
+ if (filterType === 'ws' && type !== 'ws') return false;
407
+ }
408
+
409
+ // Direction Filter
410
+ if (filterDirection !== 'all') {
411
+ const dir = data.direction || 'inbound';
412
+ if (filterDirection !== dir) return false;
413
+ }
414
+
415
+ // Text Filter (Regex-ish)
416
+ if (filterText) {
417
+ const text = (data.url + ' ' + data.method).toLowerCase();
418
+ return text.includes(filterText);
419
+ }
52
420
 
53
- // Determine base path for API requests
421
+ return true;
422
+ }
423
+
424
+ function waterfallFormatter(cell) {
425
+ const data = cell.getData();
426
+ // We need a reference start time for the waterfall.
427
+ // For now, let's use the oldest timestamp in the current page/view or relative to 10 seconds ago?
428
+ // A better approach for "live" view is to just show bar width proportional to duration?
429
+ // Or relative to the start of the trace session.
430
+
431
+ // Simpler: Show duration bar relative to a fixed max (e.g. 1s or 5s).
432
+ // Or just a simple bar representing execution time.
433
+
434
+ // Let's do a "Time/Duration" visual.
435
+ const duration = data.duration || 0;
436
+ const maxDuration = 2000; // 2s baseline for full width
437
+ const pct = Math.min(100, (duration / maxDuration) * 100);
438
+
439
+ // Color based on duration
440
+ const color = duration > 1000 ? '#ef4444' : duration > 500 ? '#f59e0b' : '#3b82f6';
441
+
442
+ return `<div style="width: 100%; height: 100%; display: flex; align-items: center;">
443
+ <div style="height: 6px; width: ${pct}%; background: ${color}; border-radius: 3px; min-width: 2px;"></div>
444
+ </div>`;
445
+ }
446
+
447
+
448
+
449
+ function fetchRequests() {
450
+ const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
54
451
  const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
55
- const url = basePath + '/';
452
+ const url = basePath + '/requests';
56
453
 
57
- fetch(url + 'requests', { headers })
454
+ fetch(url, { headers })
58
455
  .then(res => res.json())
59
456
  .then(data => {
60
- if (requestsTable) {
61
- requestsTable.setData(data.requests);
457
+ if (window.requestsTable) {
458
+ window.requestsTable.setData(data.requests || []);
459
+ window.requestsTable.setFilter(customFilter);
62
460
  }
63
461
  })
64
- .catch(err => console.error("Failed to fetch requests", err));
462
+ .catch(err => {
463
+ console.error("Failed to fetch requests", err);
464
+ });
65
465
  }
66
466
 
467
+
468
+
67
469
  function showRequestDetails(request) {
68
470
  const container = document.getElementById('request-details-container');
69
471
  const content = document.getElementById('request-details-content');
70
- const traceContainer = document.getElementById('middleware-trace-container');
71
472
 
72
473
  container.style.display = 'block';
474
+ if (window.requestsTable) window.requestsTable.redraw();
475
+
476
+ // Tab Headers
477
+ const tabs = [
478
+ { id: 'headers', label: 'Headers' },
479
+ { id: 'cookies', label: 'Cookies' },
480
+ { id: 'request', label: 'Request' },
481
+ { id: 'response', label: 'Response' },
482
+ { id: 'timings', label: 'Timings' },
483
+ // { id: 'security', label: 'Security' } // Enable if we have data
484
+ ];
485
+
486
+ if (request.scheme === 'https' || request.scheme === 'wss') {
487
+ tabs.push({ id: 'security', label: 'Security' });
488
+ }
489
+
490
+ let activeTab = 'headers';
491
+
492
+ function renderTabs() {
493
+ return `
494
+ <div class="tabs-header" style="display: flex; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem;">
495
+ ${tabs.map(tab => `
496
+ <div class="tab-item ${tab.id === activeTab ? 'active' : ''}"
497
+ data-tab="${tab.id}"
498
+ style="padding: 8px 16px; cursor: pointer; border-bottom: 2px solid ${tab.id === activeTab ? 'var(--primary-color, #3b82f6)' : 'transparent'}; color: ${tab.id === activeTab ? 'var(--text-primary)' : 'var(--text-secondary)'};">
499
+ ${tab.label}
500
+ </div>
501
+ `).join('')}
502
+ </div>
503
+ <div id="tab-content" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column;">
504
+ ${renderTabContent(activeTab, request)}
505
+ </div>
506
+ `;
507
+ }
508
+
509
+ content.innerHTML = renderTabs();
510
+
511
+ // Event Delegation for Tabs
512
+ content.onclick = (e) => {
513
+ const tabItem = e.target.closest('.tab-item');
514
+ if (tabItem) {
515
+ const newTab = tabItem.dataset.tab;
516
+ if (newTab !== activeTab) {
517
+ activeTab = newTab;
518
+ content.innerHTML = renderTabs();
519
+ // Re-initialize editors if needed
520
+ if (activeTab === 'response') initResponseEditor(request);
521
+ if (activeTab === 'request') initRequestEditor(request);
522
+ }
523
+ }
524
+ };
525
+
526
+ // Initial Editor Load
527
+ if (activeTab === 'response') initResponseEditor(request);
528
+ }
529
+
530
+ function renderTabContent(tabId, request) {
531
+ switch (tabId) {
532
+ case 'headers':
533
+ return renderHeadersTab(request);
534
+ case 'cookies':
535
+ return renderCookiesTab(request);
536
+ case 'request':
537
+ return renderRequestTab(request);
538
+ case 'response':
539
+ return renderResponseTab(request);
540
+ case 'timings':
541
+ return renderTimingsTab(request);
542
+ case 'security':
543
+ return renderSecurityTab(request);
544
+ default:
545
+ return '';
546
+ }
547
+ }
548
+
549
+ function renderHeadersTab(request) {
550
+ const formatHeaderSection = (title, headers) => {
551
+ if (!headers || Object.keys(headers).length === 0) return '';
552
+ const rows = Object.entries(headers).map(([k, v]) => `
553
+ <tr>
554
+ <td style="font-weight: 500; color: var(--text-secondary); padding: 4px 8px; vertical-align: top;">${k}:</td>
555
+ <td style="word-break: break-all; padding: 4px 8px;">${v}</td>
556
+ </tr>
557
+ `).join('');
558
+ return `
559
+ <details open style="margin-bottom: 1rem;">
560
+ <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">${title}</summary>
561
+ <table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
562
+ ${rows}
563
+ </table>
564
+ </details>
565
+ `;
566
+ };
567
+
568
+ return `
569
+ <div style="padding: 0 0.5rem;">
570
+ <details open style="margin-bottom: 1rem;">
571
+ <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">General</summary>
572
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9em; padding-left: 8px;">
573
+ <div style="color: var(--text-secondary);">Request URL:</div><div style="word-break: break-all;">${request.url}</div>
574
+ <div style="color: var(--text-secondary);">Request Method:</div><div>${request.method}</div>
575
+ <div style="color: var(--text-secondary);">Status Code:</div><div>${request.status}</div>
576
+ <div style="color: var(--text-secondary);">Remote Address:</div><div>${request.remoteIP || '-'}</div>
577
+ <div style="color: var(--text-secondary);">Referrer Policy:</div><div>${request.requestHeaders?.['referrer-policy'] || 'strict-origin-when-cross-origin'}</div>
578
+ </div>
579
+ </details>
580
+ ${formatHeaderSection('Response Headers', request.responseHeaders)}
581
+ ${formatHeaderSection('Request Headers', request.requestHeaders)}
582
+ </div>
583
+ `;
584
+ }
585
+
586
+ function renderCookiesTab(request) {
587
+ // Parse Cookies
588
+ const reqCookies = request.requestHeaders?.['cookie']
589
+ ? request.requestHeaders['cookie'].split(';').map(c => {
590
+ const [k, v] = c.trim().split('=');
591
+ return { name: k, value: v };
592
+ })
593
+ : [];
594
+
595
+ // Naive Set-Cookie parsing (often an array, but we might have it merged or as single string depending on collection)
596
+ // If headers are just Record<string, string>, Set-Cookie might be joined by comma, which is bad for automated parsing if values contain commas.
597
+ // For now, let's assume one or basic parsing.
598
+ let resCookies = [];
599
+ if (request.responseHeaders?.['set-cookie']) {
600
+ // This is tricky if multiple set-cookies are merged.
601
+ // Assuming a simple array or single string for now.
602
+ // If generic Record<string,string> was used, multiple set-cookies might be lost or merged.
603
+ // We'll display what we have.
604
+ resCookies = [{ name: 'Set-Cookie', value: request.responseHeaders['set-cookie'] }];
605
+ }
606
+
607
+ const renderTable = (cookies) => {
608
+ if (!cookies.length) return '<div style="padding: 8px; color: var(--text-secondary);">No cookies found</div>';
609
+ return `
610
+ <table style="width: 100%; text-align: left; border-collapse: collapse; font-size: 0.9em;">
611
+ <thead>
612
+ <tr style="border-bottom: 1px solid var(--border-color);">
613
+ <th style="padding: 4px 8px;">Name</th>
614
+ <th style="padding: 4px 8px;">Value</th>
615
+ </tr>
616
+ </thead>
617
+ <tbody>
618
+ ${cookies.map(c => `
619
+ <tr style="border-bottom: 1px solid var(--border-color-dim, #33333333);">
620
+ <td style="padding: 4px 8px; font-weight: 500;">${c.name}</td>
621
+ <td style="padding: 4px 8px; word-break: break-all;">${c.value}</td>
622
+ </tr>
623
+ `).join('')}
624
+ </tbody>
625
+ </table>
626
+ `;
627
+ };
628
+
629
+ return `
630
+ <div style="padding: 0 0.5rem; display: flex; flex-direction: column; gap: 1rem;">
631
+ <div>
632
+ <div style="font-weight: bold; margin-bottom: 0.5rem;">Request Cookies</div>
633
+ ${renderTable(reqCookies)}
634
+ </div>
635
+ <div>
636
+ <div style="font-weight: bold; margin-bottom: 0.5rem;">Response Cookies</div>
637
+ ${renderTable(resCookies)}
638
+ </div>
639
+ </div>
640
+ `;
641
+ }
642
+
643
+ function renderRequestTab(request) {
644
+ if (!request.requestBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No payload</div>';
645
+ return `
646
+ <div style="display: flex; flex-direction: column; height: 100%;">
647
+ <div style="display: flex; justify-content: flex-end; padding: 4px;">
648
+ <button class="btn-action" onclick="copyToClipboard(currentRequestBody)">Copy</button>
649
+ </div>
650
+ <div id="request-body-editor" style="flex: 1; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; min-height: 200px;"></div>
651
+ </div>
652
+ `;
653
+ }
654
+
655
+ function renderResponseTab(request) {
656
+ if (!request.responseBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No content</div>';
657
+
658
+ return `
659
+ <div style="display: flex; flex-direction: column; height: 100%;">
660
+ <div style="display: flex; justify-content: space-between; align-items: center; padding: 4px; border-bottom: 1px solid var(--border-color);">
661
+ <div style="font-size: 0.8em; color: var(--text-secondary);">${formatBytes(request.size || 0)}</div>
662
+ <div style="display: flex; gap: 8px; align-items: center;">
663
+ <label style="display: flex; align-items: center; gap: 4px; font-size: 0.8rem; cursor: pointer; user-select: none;">
664
+ <input type="checkbox" id="auto-format-check" ${window.autoFormatEnabled !== false ? 'checked' : ''}> Format
665
+ </label>
666
+ <div style="width: 1px; height: 16px; background: var(--border-color); margin: 0 4px;"></div>
667
+ <button id="btn-copy-body" class="btn-action" title="Copy Body">Copy</button>
668
+ <button id="btn-download-body" class="btn-action" title="Download Body">Download</button>
669
+ </div>
670
+ </div>
671
+ <div id="response-body-editor" style="flex: 1; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; min-height: 200px;"></div>
672
+ </div>
673
+ `;
674
+ }
675
+
676
+ function renderTimingsTab(request) {
677
+ // Placeholder for timings visualization
678
+ return `
679
+ <div style="padding: 1rem;">
680
+ <div style="display: grid; grid-template-columns: 1fr auto; gap: 8px; max-width: 400px; font-size: 0.9em;">
681
+ <div>Started At:</div><div>${new Date(request.timestamp).toLocaleString()}</div>
682
+ <div>Duration:</div><div>${request.duration.toFixed(2)} ms</div>
683
+ <div style="border-top: 1px solid var(--border-color); margin-top:8px; padding-top:8px; font-weight:bold;">Total Transferred:</div><div style="border-top: 1px solid var(--border-color); margin-top:8px; padding-top:8px; font-weight:bold;">${formatBytes(request.transferred || request.size || 0)}</div>
684
+ </div>
685
+ <!-- Future: detailed breakdown -->
686
+ </div>
687
+ `;
688
+ }
73
689
 
74
- // Render Summary
75
- content.innerHTML = `
76
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
77
- <div><strong>Method:</strong> ${request.method}</div>
78
- <div><strong>URL:</strong> ${request.url}</div>
79
- <div><strong>Status:</strong> ${request.status}</div>
80
- <div><strong>Duration:</strong> ${printDuration(request.duration)} ms</div>
81
- <div><strong>Timestamp:</strong> ${new Date(request.timestamp).toLocaleString()}</div>
690
+ function renderSecurityTab(request) {
691
+ return `
692
+ <div style="padding: 1rem;">
693
+ <div style="margin-bottom: 1rem; font-weight: bold;">Connection</div>
694
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9em;">
695
+ <div style="color: var(--text-secondary);">Protocol:</div><div>${request.protocol || request.scheme || 'tls'}</div>
696
+ <div style="color: var(--text-secondary);">Remote Address:</div><div>${request.remoteIP || 'Unknown'}</div>
697
+ </div>
698
+ <div style="margin-top: 1rem; color: var(--text-secondary); font-style: italic;">
699
+ Detailed certificate information is not currently captured by the interceptor.
700
+ </div>
82
701
  </div>
83
702
  `;
703
+ }
704
+
705
+
706
+ // Attach event listeners
707
+ // document.getElementById('auto-format-check').onchange = (e) => {
708
+ // window.autoFormatEnabled = e.target.checked;
709
+ // renderMonacoEditor(request);
710
+ // };
711
+
712
+
713
+ function closeRequestDetails() {
714
+ document.getElementById('request-details-container').style.display = 'none';
715
+ if (window.requestsTable) window.requestsTable.redraw();
716
+ }
717
+ window.closeRequestDetails = closeRequestDetails;
84
718
 
85
- // Render Trace
719
+ function renderTrace(request, container) {
86
720
  if (request.handlerStack && request.handlerStack.length > 0) {
721
+ const totalDuration = request.duration || 1;
87
722
  let html = '<div style="display: flex; flex-direction: column; gap: 4px;">';
88
723
 
89
724
  request.handlerStack.forEach((item, index) => {
90
- const duration = item.duration || 0;
91
-
92
- if (index !== 0) {
93
- html += `<div style="align-self: center">⬇︎</div>`;
94
- }
725
+ const duration = item.duration > 0 ? item.duration : 0.01;
726
+ const percent = Math.min(100, Math.max(1, (duration / totalDuration) * 100));
727
+ const isSlow = percent > 15;
95
728
 
96
729
  html += `
97
- <div style="padding: 8px; border-radius: 4px; background: var(--bg-secondary);">
98
- <div style="display: flex; justify-content: space-between;">
99
- <span style="font-weight: bold;">${item.name}</span>
100
- <span>${printDuration(duration)}</span>
101
- </div>
102
- <div style="font-size: 0.8rem; color: var(--text-secondary);">
103
- ${item.file}:${item.line}
104
- </div>
105
- ${item.stateChanges ? `<div style="font-size: 0.8rem; margin-top: 4px; color: #aaa;">State Changes: ${Object.keys(item.stateChanges).join(', ')}</div>` : ''}
730
+ <div style="padding: 8px; border-radius: 4px; background: var(--bg-primary); border-left: 3px solid ${isSlow ? 'var(--color-warning)' : 'var(--color-success)'};">
731
+ <div style="display: flex; justify-content: space-between; font-size: 0.9em;">
732
+ <span style="font-weight: 500;">${item.name}</span>
733
+ <span style="font-family: monospace;">${printDuration(duration)}</span>
106
734
  </div>
107
- `;
108
- });
735
+ <div style="height: 3px; background: var(--bg-secondary); margin-top: 4px; border-radius: 2px; overflow: hidden;">
736
+ <div style="height: 100%; width: ${percent}%; background: ${isSlow ? 'var(--color-warning)' : 'var(--color-success)'}; opacity: 0.8;"></div>
737
+ </div>
738
+ </div>`;
109
739
 
740
+ if (index < request.handlerStack.length - 1) {
741
+ html += `<div style="display: flex; justify-content: center; height: 10px;"><div style="width: 1px; background: var(--border-color); opacity: 0.5;"></div></div>`;
742
+ }
743
+ });
110
744
  html += '</div>';
111
- traceContainer.innerHTML = html;
745
+ container.innerHTML = html;
112
746
  } else {
113
- traceContainer.innerHTML = '<div style="color: var(--text-secondary);">No middleware trace available.</div>';
747
+ container.innerHTML = `<div style="padding: 2rem; text-align: center; color: var(--text-secondary);">No trace data</div>`;
114
748
  }
749
+ }
115
750
 
116
- // Scroll to details
117
- container.scrollIntoView({ behavior: 'smooth' });
751
+ function getExtension(contentType) {
752
+ if (!contentType) return 'txt';
753
+ if (contentType.includes('json')) return 'json';
754
+ if (contentType.includes('html')) return 'html';
755
+ if (contentType.includes('xml')) return 'xml';
756
+ if (contentType.includes('javascript')) return 'js';
757
+ if (contentType.includes('css')) return 'css';
758
+ return 'txt';
118
759
  }
760
+
761
+ function getRequestBody(request) {
762
+ let value = request.body || '';
763
+ if (typeof value === 'object') {
764
+ try {
765
+ value = JSON.stringify(value, null, 2);
766
+ } catch (e) {
767
+ value = String(value);
768
+ }
769
+ } else {
770
+ value = String(value);
771
+ }
772
+ return value;
773
+ }
774
+
775
+ let currentRequestBody = ''; // Global/Module scope tracking for request body
776
+
777
+ function initRequestEditor(request) {
778
+ const el = document.getElementById('request-body-editor');
779
+ if (!el) return;
780
+
781
+ let content = request.requestBody || request.body || '';
782
+ let language = 'plaintext';
783
+ const contentType = (request.requestHeaders?.['content-type'] || '').toLowerCase();
784
+
785
+ if (contentType.includes('json')) language = 'json';
786
+ else if (contentType.includes('html')) language = 'html';
787
+ else if (contentType.includes('xml')) language = 'xml';
788
+ else if (contentType.includes('javascript') || contentType.includes('application/x-javascript')) language = 'javascript';
789
+ else if (contentType.includes('css')) language = 'css';
790
+ else if (contentType.includes('typescript')) language = 'typescript';
791
+ else if (contentType.includes('markdown')) language = 'markdown';
792
+ else if (contentType.includes('sql')) language = 'sql';
793
+ else if (contentType.includes('yaml')) language = 'yaml';
794
+
795
+ if (typeof content === 'object') {
796
+ content = JSON.stringify(content, null, 2);
797
+ language = 'json';
798
+ } else if (typeof content === 'string') {
799
+ // Auto-detect JSON if content looks like JSON but header is wrong
800
+ if (language === 'plaintext' && (content.trim().startsWith('{') || content.trim().startsWith('['))) {
801
+ try {
802
+ JSON.parse(content);
803
+ language = 'json';
804
+ } catch (e) { /* not json */ }
805
+ }
806
+ }
807
+
808
+ currentRequestBody = content; // store for copy
809
+
810
+ renderMonacoEditor(el, content, language, false); // Request body usually not auto-formatted
811
+ }
812
+
813
+ function initResponseEditor(request) {
814
+ const el = document.getElementById('response-body-editor');
815
+ if (!el) return;
816
+
817
+ let content = request.body || request.responseBody;
818
+ let contentType = request.contentType || '';
819
+
820
+ if (!content) content = '';
821
+
822
+ // Auto-Format Logic
823
+ let language = 'plaintext';
824
+ if (contentType.includes('json')) language = 'json';
825
+ else if (contentType.includes('html')) language = 'html';
826
+ else if (contentType.includes('xml')) language = 'xml';
827
+ else if (contentType.includes('javascript') || contentType.includes('application/x-javascript')) language = 'javascript';
828
+ else if (contentType.includes('css')) language = 'css';
829
+ else if (contentType.includes('typescript')) language = 'typescript';
830
+ else if (contentType.includes('markdown')) language = 'markdown';
831
+ else if (contentType.includes('sql')) language = 'sql';
832
+ else if (contentType.includes('yaml')) language = 'yaml';
833
+
834
+ if (typeof content === 'object') {
835
+ content = JSON.stringify(content, null, 2);
836
+ language = 'json';
837
+ } else if (window.autoFormatEnabled !== false && typeof content === 'string') {
838
+ // Try auto-detect JSON if string
839
+ if ((content.trim().startsWith('{') || content.trim().startsWith('[')) && content.length < 524288) {
840
+ try {
841
+ const parsed = JSON.parse(content);
842
+ content = JSON.stringify(parsed, null, 2);
843
+ language = 'json';
844
+ } catch (e) { /* not json */ }
845
+ }
846
+ }
847
+
848
+ renderMonacoEditor(el, content, language, window.autoFormatEnabled !== false);
849
+
850
+ // Attach button listeners
851
+ const btnCopy = document.getElementById('btn-copy-body');
852
+ const btnDownload = document.getElementById('btn-download-body');
853
+ if (btnCopy) btnCopy.onclick = () => copyToClipboard(getRequestBody(request));
854
+ if (btnDownload) btnDownload.onclick = () => downloadString(getRequestBody(request), `body-${request.timestamp}.${getExtension(request.contentType)}`);
855
+ }
856
+
857
+ let currentMonacoEditor = null; // To manage the active editor instance
858
+
859
+ function renderMonacoEditor(containerElement, value, language, shouldFormat = false) {
860
+ if (!window.monaco) {
861
+ require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
862
+ require(['vs/editor/editor.main'], function () { renderMonacoEditor(containerElement, value, language, shouldFormat); });
863
+ return;
864
+ }
865
+
866
+ window.currentEditor = monaco.editor.create(containerElement, {
867
+ value: value,
868
+ language: language,
869
+ theme: 'vs-dark',
870
+ readOnly: true,
871
+ minimap: { enabled: false },
872
+ scrollBeyondLastLine: false,
873
+ automaticLayout: true,
874
+ wordWrap: 'on'
875
+ });
876
+
877
+ if (shouldFormat) {
878
+ setTimeout(() => {
879
+ if (window.currentEditor) {
880
+ window.currentEditor.getAction('editor.action.formatDocument').run();
881
+ }
882
+ }, 100);
883
+ }
884
+ }
885
+
886
+ function copyToClipboard(text) {
887
+ navigator.clipboard.writeText(text).then(() => {
888
+ const btn = document.activeElement;
889
+ if (btn && btn.tagName === 'BUTTON') {
890
+ const original = btn.innerText;
891
+ btn.innerText = 'Copied!';
892
+ setTimeout(() => btn.innerText = original, 1500);
893
+ }
894
+ }).catch(err => console.error('Failed to copy', err));
895
+ }
896
+
897
+ function downloadString(text, filename) {
898
+ const blob = new Blob([text], { type: 'text/plain' });
899
+ const url = URL.createObjectURL(blob);
900
+ const a = document.createElement('a');
901
+ a.href = url;
902
+ a.download = filename;
903
+ a.click();
904
+ URL.revokeObjectURL(url);
905
+ }
906
+
907
+ function formatBytes(bytes, decimals = 2) {
908
+ if (!+bytes) return '0 B';
909
+ const k = 1024;
910
+ const dm = decimals < 0 ? 0 : decimals;
911
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
912
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
913
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
914
+ }
915
+
916
+ window.updateRequestsList = function (newRequests) {
917
+ if (!window.requestsTable || !newRequests || newRequests.length === 0) return;
918
+
919
+ // console.log('[requests.js] Adding/Updating', newRequests.length, 'rows');
920
+ window.requestsTable.updateOrAddData(newRequests)
921
+ .then(() => {
922
+ // Force redraw/filter application
923
+ window.requestsTable.recalc();
924
+ window.requestsTable.redraw();
925
+ // console.log('[requests.js] Table updated');
926
+ })
927
+ .catch(err => console.error("Failed to update table data", err));
928
+ };