shokupan 0.11.0 → 0.12.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 (48) hide show
  1. package/README.md +46 -1815
  2. package/dist/{analyzer-CnKnQ5KV.js → analyzer-BkNQHWj4.js} +2 -2
  3. package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-BkNQHWj4.js.map} +1 -1
  4. package/dist/{analyzer-BAhvpNY_.cjs → analyzer-DM-OlRq8.cjs} +2 -2
  5. package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
  6. package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CVJ8zfGQ.cjs} +11 -3
  7. package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
  8. package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-CsA1bS_s.js} +11 -3
  9. package/dist/analyzer.impl-CsA1bS_s.js.map +1 -0
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.js +1 -1
  12. package/dist/context.d.ts +40 -8
  13. package/dist/index.cjs +1011 -300
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +1011 -300
  16. package/dist/index.js.map +1 -1
  17. package/dist/plugins/application/api-explorer/static/theme.css +4 -0
  18. package/dist/plugins/application/auth.d.ts +5 -0
  19. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
  20. package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +537 -251
  22. package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
  23. package/dist/plugins/application/dashboard/static/theme.css +4 -0
  24. package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
  25. package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
  26. package/dist/plugins/middleware/compression.d.ts +12 -2
  27. package/dist/plugins/middleware/rate-limit.d.ts +5 -0
  28. package/dist/router.d.ts +6 -5
  29. package/dist/server.d.ts +22 -0
  30. package/dist/shokupan.d.ts +24 -1
  31. package/dist/util/adapter/bun.d.ts +8 -0
  32. package/dist/util/adapter/index.d.ts +4 -0
  33. package/dist/util/adapter/interface.d.ts +12 -0
  34. package/dist/util/adapter/node.d.ts +8 -0
  35. package/dist/util/adapter/wintercg.d.ts +5 -0
  36. package/dist/util/body-parser.d.ts +30 -0
  37. package/dist/util/decorators.d.ts +20 -3
  38. package/dist/util/di.d.ts +3 -8
  39. package/dist/util/metadata.d.ts +18 -0
  40. package/dist/util/request.d.ts +1 -0
  41. package/dist/util/symbol.d.ts +1 -0
  42. package/dist/util/types.d.ts +132 -3
  43. package/package.json +3 -1
  44. package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
  45. package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
  46. package/dist/plugins/application/dashboard/static/failures.js +0 -85
  47. package/dist/plugins/application/http-server.d.ts +0 -13
  48. package/dist/util/adapter/adapters.d.ts +0 -19
@@ -7,6 +7,23 @@ window.requestsTable = null;
7
7
  let filterText = '';
8
8
  let filterType = 'all';
9
9
  let filterDirection = 'all';
10
+ let filterIgnore = true;
11
+ let ignoreRegexes = [];
12
+
13
+ function globToRegex(pattern) {
14
+ // Escape special regex chars except *
15
+ let escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
16
+ // Convert * to .*
17
+ // For ** support, we can just treat * as .* for now, or distinguish.
18
+ // Simple approach: replace * with .*
19
+ // Note: This is a loose approximation of glob
20
+ const re = escaped.replace(/\*/g, '.*');
21
+ return new RegExp(`^${re}$`);
22
+ }
23
+
24
+ // Waterfall State
25
+ let minRequestTime = Infinity;
26
+ let maxRequestTime = 0;
10
27
 
11
28
  function initRequests() {
12
29
  console.log('[requests.js] Initializing...');
@@ -14,8 +31,14 @@ function initRequests() {
14
31
  // Initialize Filter Listeners
15
32
  const txtFilter = document.getElementById('network-filter-text');
16
33
  const typeFilter = document.getElementById('network-filter-type');
34
+ const ignoreFilter = document.getElementById('network-filter-ignore');
17
35
  const directionButtons = document.querySelectorAll('.filter-direction');
18
36
 
37
+ // Compile regexes
38
+ if (window.SHOKUPAN_CONFIG && window.SHOKUPAN_CONFIG.ignorePaths) {
39
+ ignoreRegexes = window.SHOKUPAN_CONFIG.ignorePaths.map(globToRegex);
40
+ }
41
+
19
42
  if (directionButtons) {
20
43
  directionButtons.forEach(btn => {
21
44
  btn.onclick = () => {
@@ -49,160 +72,301 @@ function initRequests() {
49
72
  });
50
73
  }
51
74
 
75
+ if (ignoreFilter) {
76
+ // specific listener
77
+ ignoreFilter.addEventListener('change', (e) => {
78
+ filterIgnore = e.target.checked;
79
+ window.requestsTable.setFilter(customFilter);
80
+ });
81
+ filterIgnore = ignoreFilter.checked;
82
+ }
83
+
52
84
  // specific check for Tabulator
53
85
  if (typeof Tabulator === 'undefined') {
54
86
  console.error('Tabulator is not defined. Ensure it is loaded before requests.js');
55
87
  return;
56
88
  }
57
89
 
90
+ // Load saved column state
91
+ let savedColumns = {};
92
+ try {
93
+ const stored = localStorage.getItem('shokupan_dashboard_columns');
94
+ if (stored) savedColumns = JSON.parse(stored);
95
+ } catch (e) {
96
+ console.error("Failed to load column state", e);
97
+ }
98
+
99
+ function saveColumnState() {
100
+ if (!window.requestsTable) return;
101
+ const cols = window.requestsTable.getColumns();
102
+ const state = {};
103
+ cols.forEach(c => {
104
+ state[c.getField()] = c.isVisible();
105
+ });
106
+ localStorage.setItem('shokupan_dashboard_columns', JSON.stringify(state));
107
+ }
108
+
109
+ const headerMenu = [
110
+ {
111
+ label: "Hide Column",
112
+ action: function (e, column) {
113
+ column.hide();
114
+ saveColumnState();
115
+ }
116
+ },
117
+ {
118
+ separator: true,
119
+ },
120
+ {
121
+ label: "Select Columns",
122
+ menu: []
123
+ }
124
+ ];
125
+
126
+ const columns = [
127
+ {
128
+ title: "Status",
129
+ field: "status",
130
+ width: 100,
131
+ visible: savedColumns['status'] !== undefined ? savedColumns['status'] : true,
132
+ formatter: function (cell) {
133
+ const status = cell.getValue();
134
+ if (!status) return '<span style="color: var(--text-secondary)">Pending</span>';
135
+ const color = status >= 500 ? '#ef4444' : status >= 400 ? '#f59e0b' : '#10b981';
136
+ return `<span style="display: inline-block; width: 10px; height: 10px; background: ${color}; border-radius: 50%; margin-right: 6px;"></span>${status}`;
137
+ },
138
+ headerContextMenu: headerMenu
139
+ },
140
+ {
141
+ title: "Method",
142
+ field: "method",
143
+ width: 90,
144
+ headerSort: false,
145
+ visible: savedColumns['method'] !== undefined ? savedColumns['method'] : true,
146
+ headerContextMenu: headerMenu
147
+ },
148
+ {
149
+ title: "Name",
150
+ field: "url",
151
+ widthGrow: 2, // Take up more space
152
+ visible: savedColumns['url'] !== undefined ? savedColumns['url'] : true,
153
+ formatter: function (cell) {
154
+ const url = cell.getValue();
155
+ // Extract name from URL
156
+ let name = url;
157
+ try {
158
+ const u = new URL(url, 'http://localhost');
159
+ name = u.pathname;
160
+ if (name === '/') name = 'localhost';
161
+ const parts = name.split('/');
162
+ const last = parts[parts.length - 1];
163
+ if (last) name = last;
164
+ } catch (e) { }
165
+
166
+ return `<div style="display: flex; flex-direction: column; line-height: 1.2;">
167
+ <span style="color: var(--text-secondary);">${name}</span>
168
+ </div>`;
169
+ },
170
+ headerContextMenu: headerMenu
171
+ },
172
+ {
173
+ title: "Domain",
174
+ field: "domain",
175
+ width: 80,
176
+ visible: savedColumns['domain'] !== undefined ? savedColumns['domain'] : false,
177
+ headerContextMenu: headerMenu
178
+ },
179
+ {
180
+ title: "Path",
181
+ field: "path",
182
+ width: 80,
183
+ visible: savedColumns['path'] !== undefined ? savedColumns['path'] : true,
184
+ headerContextMenu: headerMenu
185
+ },
186
+ {
187
+ title: "URL",
188
+ field: "url",
189
+ width: 80,
190
+ visible: savedColumns['url'] !== undefined ? savedColumns['url'] : false,
191
+ headerContextMenu: headerMenu
192
+ },
193
+ {
194
+ title: "Protocol",
195
+ field: "protocol",
196
+ width: 80,
197
+ visible: savedColumns['protocol'] !== undefined ? savedColumns['protocol'] : false,
198
+ formatter: function (cell) {
199
+ const row = cell.getData();
200
+ // Prefer explicit protocol version (e.g. 1.1, h2) if available
201
+ if (row.protocol && row.protocol !== 'http' && row.protocol !== 'https') return row.protocol;
202
+ return row.scheme || row.protocol || '-';
203
+ },
204
+ headerContextMenu: headerMenu
205
+ },
206
+ {
207
+ title: "Scheme",
208
+ field: "scheme",
209
+ width: 80,
210
+ visible: savedColumns['scheme'] !== undefined ? savedColumns['scheme'] : false,
211
+ headerContextMenu: headerMenu
212
+ },
213
+ {
214
+ title: "Remote IP",
215
+ field: "remoteIP",
216
+ width: 80,
217
+ visible: savedColumns['remoteIP'] !== undefined ? savedColumns['remoteIP'] : true,
218
+ headerContextMenu: headerMenu
219
+ },
220
+ {
221
+ title: "Initiator",
222
+ field: "direction",
223
+ width: 80,
224
+ visible: savedColumns['direction'] !== undefined ? savedColumns['direction'] : false,
225
+ formatter: (cell) => {
226
+ const dir = cell.getValue();
227
+ return dir === 'outbound' ? 'Server' : 'Client';
228
+ },
229
+ headerContextMenu: headerMenu
230
+ },
231
+ {
232
+ title: "Type",
233
+ field: "type",
234
+ width: 80,
235
+ visible: savedColumns['type'] !== undefined ? savedColumns['type'] : false,
236
+ formatter: (cell) => {
237
+ const r = cell.getData();
238
+ if (r.type === 'fetch') return 'fetch';
239
+ if (r.type === 'xhr') return 'xhr';
240
+ if (r.type === 'ws') return 'ws';
241
+ return r.contentType || 'document';
242
+ },
243
+ headerContextMenu: headerMenu
244
+ },
245
+ {
246
+ title: "Cookies",
247
+ field: "cookies",
248
+ width: 80,
249
+ visible: savedColumns['cookies'] !== undefined ? savedColumns['cookies'] : false,
250
+ headerContextMenu: headerMenu
251
+ },
252
+ {
253
+ title: "Transferred",
254
+ field: "transferred",
255
+ width: 80,
256
+ visible: savedColumns['transferred'] !== undefined ? savedColumns['transferred'] : false,
257
+ headerContextMenu: headerMenu
258
+ },
259
+ {
260
+ title: "Size",
261
+ field: "size",
262
+ width: 110,
263
+ visible: savedColumns['size'] !== undefined ? savedColumns['size'] : true,
264
+ formatter: (cell) => formatBytes(cell.getValue()),
265
+ headerContextMenu: headerMenu
266
+ },
267
+ {
268
+ title: "Time",
269
+ field: "duration",
270
+ width: 90,
271
+ visible: savedColumns['duration'] !== undefined ? savedColumns['duration'] : true,
272
+ formatter: (cell) => cell.getValue() ? Math.round(cell.getValue()) + ' ms' : 'Pending',
273
+ headerContextMenu: headerMenu
274
+ },
275
+ {
276
+ title: "Waterfall",
277
+ field: "timestamp",
278
+ widthGrow: 1,
279
+ visible: savedColumns['timestamp'] !== undefined ? savedColumns['timestamp'] : true,
280
+ formatter: waterfallFormatter,
281
+ headerSort: true,
282
+ headerContextMenu: headerMenu
283
+ }
284
+ ];
285
+
286
+ const checkMark = `<svg fill="currentColor" width="16px" height="16px" style="padding: 2px; margin-right: 2px" viewBox="0 0 1024 1024"><path d="M351.605 663.268l481.761-481.761c28.677-28.677 75.171-28.677 103.847 0s28.677 75.171 0 103.847L455.452 767.115l.539.539-58.592 58.592c-24.994 24.994-65.516 24.994-90.51 0L85.507 604.864c-28.677-28.677-28.677-75.171 0-103.847s75.171-28.677 103.847 0l162.25 162.25z"/></svg>`;
287
+ const uncheckMark = `<span style="width: 18px; display: inline-block"></span>`;
288
+
289
+ const subMenu = [];
290
+ columns.forEach((col, idx) => {
291
+ subMenu.push({
292
+ label: (col.visible ? checkMark : uncheckMark) + " " + col.title,
293
+ action: function (e) {
294
+ // const cols = window.requestsTable.getColumns();
295
+ // cols.forEach((c, i) => {
296
+ // if (idx === i) c.toggle();
297
+ // });
298
+ columns[idx].visible = !columns[idx].visible;
299
+ subMenu[idx].label = (columns[idx].visible ? checkMark : uncheckMark) + " " + columns[idx].title;
300
+ if (window.requestsTable) {
301
+ window.requestsTable.redraw();
302
+ window.requestsTable.setColumns(columns);
303
+ saveColumnState();
304
+ };
305
+ }
306
+ });
307
+ });
308
+ headerMenu[2].menu = subMenu;
309
+
58
310
  window.requestsTable = new Tabulator("#requests-list-container", {
59
311
  layout: "fitColumns",
60
- placeholder: "No requests found",
61
- selectable: 1,
312
+ responsiveLayout: true,
313
+ resizableColumnGuide: true,
62
314
  resizableColumnFit: true,
315
+ placeholder: "No requests found",
316
+ selectableRows: 1,
63
317
  height: "100%", // Fill container
64
318
  index: "id",
65
319
  rowHeight: 32, // Dense rows
66
320
  initialSort: [
67
321
  { column: "timestamp", dir: "desc" }
68
322
  ],
69
- columns: [
70
- {
71
- title: "Status",
72
- field: "status",
73
- width: 80,
74
- formatter: function (cell) {
75
- const status = cell.getValue();
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}`;
79
- }
80
- },
81
- {
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
91
- formatter: function (cell) {
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>`;
107
- }
108
- },
109
- {
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",
124
- width: 80,
125
- visible: true
126
- },
127
- {
128
- title: "Protocol",
129
- field: "protocol",
130
- width: 80,
131
- visible: true,
132
- formatter: function (cell) {
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 || '-';
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
- },
323
+ columns: columns,
324
+ data: [],
325
+ rowContextMenu: [
160
326
  {
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';
327
+ label: "Replay Request",
328
+ action: function (e, row) {
329
+ const data = row.getData();
330
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
331
+
332
+ // Determine direction if not explicit
333
+ const direction = data.direction || 'inbound';
334
+
335
+ fetch(basePath + '/replay', {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({
339
+ method: data.method,
340
+ url: data.url,
341
+ headers: data.requestHeaders,
342
+ body: data.requestBody,
343
+ direction: direction
344
+ })
345
+ })
346
+ .then(res => res.json())
347
+ .then(result => {
348
+ if (result.error) {
349
+ alert('Replay Failed: ' + result.error);
350
+ } else {
351
+ // Show result in a simplified details view or just alert success?
352
+ // User requirement: "presents the response data to the user"
353
+ // Let's create a temporary object mimicking a request log and show it in details view
354
+ const replayLog = {
355
+ ...data,
356
+ id: 'replay-' + Date.now(),
357
+ status: result.status,
358
+ duration: result.duration || 0,
359
+ timestamp: Date.now(),
360
+ responseHeaders: result.headers,
361
+ responseBody: result.data,
362
+ size: result.data ? result.data.length : 0
363
+ };
364
+ showRequestDetails(replayLog);
365
+ }
366
+ })
367
+ .catch(err => console.error("Replay fetch failed", err));
170
368
  }
171
369
  },
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
202
- }
203
- ],
204
- data: [],
205
- rowContextMenu: [
206
370
  {
207
371
  label: "Copy as fetch",
208
372
  action: function (e, row) {
@@ -245,7 +409,7 @@ function initRequests() {
245
409
  function initResizeHandle() {
246
410
  const handle = document.getElementById('details-drag-handle');
247
411
  const container = document.getElementById('request-details-container');
248
- const listContainer = document.getElementById('requests-list-container');
412
+
249
413
  if (!handle || !container) return;
250
414
 
251
415
  let isResizing = false;
@@ -412,6 +576,20 @@ function customFilter(data) {
412
576
  if (filterDirection !== dir) return false;
413
577
  }
414
578
 
579
+ // Ignore Filter
580
+ if (filterIgnore && ignoreRegexes.length > 0) {
581
+ // check against regexes
582
+ // We match against URL or Path?
583
+ // Usually path.
584
+ // data.url might be full URL. data.path is path.
585
+ const path = data.path || data.url; // Fallback
586
+ // Also check full URL just in case glob is absolute?
587
+ // Let's assume glob matches against path.
588
+ for (const re of ignoreRegexes) {
589
+ if (re.test(path)) return false;
590
+ }
591
+ }
592
+
415
593
  // Text Filter (Regex-ish)
416
594
  if (filterText) {
417
595
  const text = (data.url + ' ' + data.method).toLowerCase();
@@ -423,27 +601,96 @@ function customFilter(data) {
423
601
 
424
602
  function waterfallFormatter(cell) {
425
603
  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.
604
+ // Default to duration bar if no range yet
605
+ const duration = data.duration || 0;
606
+
607
+ // Safety check
608
+ if (minRequestTime === Infinity || maxRequestTime === 0) {
609
+ // Just show a simple bar based on some 2s default
610
+ const pct = Math.min(100, (duration / 2000) * 100);
611
+ const color = duration > 1000 ? '#ef4444' : duration > 500 ? '#f59e0b' : '#3b82f6';
612
+ return `<div style="width: 100%; height: 100%; display: flex; align-items: center;">
613
+ <div style="height: calc(100% - 4px); width: ${pct}%; background: ${color}; border-radius: 3px; min-width: 2px;"></div>
614
+ </div>`;
615
+ }
430
616
 
431
- // Simpler: Show duration bar relative to a fixed max (e.g. 1s or 5s).
432
- // Or just a simple bar representing execution time.
617
+ const totalRange = maxRequestTime - minRequestTime;
618
+ // Prevent divide by zero
619
+ const safeRange = totalRange <= 0 ? 1 : totalRange;
433
620
 
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);
621
+ // Calculate start offset relative to minRequestTime
622
+ // We treat minRequestTime as 0%
623
+ // If a request started before minRequestTime (unlikely given logic), clamp to 0
624
+ const startTimeResult = data.timestamp - minRequestTime;
625
+ const startPct = Math.max(0, (startTimeResult / safeRange) * 100);
626
+
627
+ // Calculate width relative to totalRange
628
+ // Use a min width of 0.5% so it's visible
629
+ const widthPct = Math.max(0.5, (duration / safeRange) * 100);
438
630
 
439
- // Color based on duration
631
+ // Color
440
632
  const color = duration > 1000 ? '#ef4444' : duration > 500 ? '#f59e0b' : '#3b82f6';
441
633
 
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>
634
+ return `<div style="width: 100%; height: 100%; display: flex; align-items: center; position: relative;">
635
+ <div style="
636
+ position: absolute;
637
+ left: min(${startPct}%, calc(100% - 2px));
638
+ width: ${widthPct}%;
639
+ height: calc(100% - 4px);
640
+ background: ${color};
641
+ border-radius: 3px;
642
+ min-width: 2px;
643
+ " title="Start: +${Math.round(startTimeResult)}ms, Duration: ${Math.round(duration)}ms"></div>
444
644
  </div>`;
445
645
  }
446
646
 
647
+ // Utility to track time range
648
+ function updateTimestamps(requests) {
649
+ if (!requests || !requests.length) return;
650
+ let changed = false;
651
+ requests.forEach(r => {
652
+ const start = r.timestamp;
653
+ const end = start + (r.duration || 0);
654
+ if (start < minRequestTime) {
655
+ minRequestTime = start;
656
+ changed = true;
657
+ }
658
+ // Also extend max if needed, but generally max is max(end)
659
+ // However, waterfall usually shows relative to session start.
660
+ // If we want "waterfall of current view", we care about min/max of visible.
661
+ // But for simplicity, we track global session range.
662
+ if (end > maxRequestTime) {
663
+ maxRequestTime = end;
664
+ changed = true;
665
+ }
666
+ // Also handle if start > maxRequestTime (e.g. first request)
667
+ if (start > maxRequestTime) {
668
+ maxRequestTime = end; // start + duration
669
+ changed = true;
670
+ }
671
+ });
672
+ return changed;
673
+ }
674
+
675
+ // Global handler for Client.js
676
+ window.updateRequestsList = function (newRequests) {
677
+ if (!window.requestsTable || !newRequests || !newRequests.length) return;
678
+
679
+ // Update Timestamps
680
+ const changed = updateTimestamps(newRequests);
681
+
682
+ // Add or Update data (true = add to top if new)
683
+ // using updateOrAddData to be safe if IDs exist
684
+ window.requestsTable.updateOrAddData(newRequests)
685
+ .then(() => {
686
+ // If range expanded significantly, or just always for safety to update relative bars
687
+ if (changed) {
688
+ // redraw(true) forces full re-render of rows
689
+ window.requestsTable.redraw(true);
690
+ }
691
+ });
692
+ };
693
+
447
694
 
448
695
 
449
696
  function fetchRequests() {
@@ -455,7 +702,13 @@ function fetchRequests() {
455
702
  .then(res => res.json())
456
703
  .then(data => {
457
704
  if (window.requestsTable) {
458
- window.requestsTable.setData(data.requests || []);
705
+ const reqs = data.requests || [];
706
+ // Reset timestamps on full load/reload
707
+ minRequestTime = Infinity;
708
+ maxRequestTime = 0;
709
+ updateTimestamps(reqs);
710
+
711
+ window.requestsTable.setData(reqs);
459
712
  window.requestsTable.setFilter(customFilter);
460
713
  }
461
714
  })
@@ -470,7 +723,7 @@ function showRequestDetails(request) {
470
723
  const container = document.getElementById('request-details-container');
471
724
  const content = document.getElementById('request-details-content');
472
725
 
473
- container.style.display = 'block';
726
+ container.style.display = 'flex';
474
727
  if (window.requestsTable) window.requestsTable.redraw();
475
728
 
476
729
  // Tab Headers
@@ -491,7 +744,7 @@ function showRequestDetails(request) {
491
744
 
492
745
  function renderTabs() {
493
746
  return `
494
- <div class="tabs-header" style="display: flex; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem;">
747
+ <div class="tabs-header" style="display: flex; border-bottom: 1px solid var(--border-color)">
495
748
  ${tabs.map(tab => `
496
749
  <div class="tab-item ${tab.id === activeTab ? 'active' : ''}"
497
750
  data-tab="${tab.id}"
@@ -500,7 +753,7 @@ function showRequestDetails(request) {
500
753
  </div>
501
754
  `).join('')}
502
755
  </div>
503
- <div id="tab-content" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column;">
756
+ <div id="tab-content" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; padding: 1rem">
504
757
  ${renderTabContent(activeTab, request)}
505
758
  </div>
506
759
  `;
@@ -516,13 +769,30 @@ function showRequestDetails(request) {
516
769
  if (newTab !== activeTab) {
517
770
  activeTab = newTab;
518
771
  content.innerHTML = renderTabs();
772
+ document.querySelector("#tab-content").style.padding = "1rem";
773
+
774
+ if (activeTab === "timings") {
775
+ const traceContainer = document.getElementById('middleware-trace-container');
776
+ renderTrace(request, traceContainer);
777
+ }
778
+
519
779
  // Re-initialize editors if needed
520
- if (activeTab === 'response') initResponseEditor(request);
521
- if (activeTab === 'request') initRequestEditor(request);
780
+ if (activeTab === 'response') {
781
+ document.querySelector("#tab-content").style.padding = "0";
782
+ initResponseEditor(request);
783
+ };
784
+ if (activeTab === 'request') {
785
+ document.querySelector("#tab-content").style.padding = "0";
786
+ initRequestEditor(request);
787
+ };
522
788
  }
523
789
  }
524
790
  };
525
791
 
792
+ if (activeTab === "timings") {
793
+ const traceContainer = document.getElementById('middleware-trace-container');
794
+ renderTrace(request, traceContainer);
795
+ }
526
796
  // Initial Editor Load
527
797
  if (activeTab === 'response') initResponseEditor(request);
528
798
  }
@@ -546,12 +816,34 @@ function renderTabContent(tabId, request) {
546
816
  }
547
817
  }
548
818
 
819
+ function renderNameValueTable(items, emptyMessage = 'No items found') {
820
+ if (!items || !items.length) return `<div style="padding: 8px; color: var(--text-secondary);">${emptyMessage}</div>`;
821
+ return `
822
+ <table style="width: 100%; text-align: left; border-collapse: collapse; font-size: 0.9em;">
823
+ <thead>
824
+ <tr style="border-bottom: 1px solid var(--border-color);">
825
+ <th style="padding: 4px 8px;">Name</th>
826
+ <th style="padding: 4px 8px;">Value</th>
827
+ </tr>
828
+ </thead>
829
+ <tbody>
830
+ ${items.map(c => `
831
+ <tr style="border-bottom: 1px solid var(--border-color-dim, #33333333);">
832
+ <td style="padding: 4px 8px; font-weight: 500;">${c.name}</td>
833
+ <td style="padding: 4px 8px; word-break: break-all;">${c.value}</td>
834
+ </tr>
835
+ `).join('')}
836
+ </tbody>
837
+ </table>
838
+ `;
839
+ }
840
+
549
841
  function renderHeadersTab(request) {
550
842
  const formatHeaderSection = (title, headers) => {
551
843
  if (!headers || Object.keys(headers).length === 0) return '';
552
844
  const rows = Object.entries(headers).map(([k, v]) => `
553
845
  <tr>
554
- <td style="font-weight: 500; color: var(--text-secondary); padding: 4px 8px; vertical-align: top;">${k}:</td>
846
+ <td style="font-weight: 500; color: var(--text-flavor); padding: 4px 8px; vertical-align: top;">${k}:</td>
555
847
  <td style="word-break: break-all; padding: 4px 8px;">${v}</td>
556
848
  </tr>
557
849
  `).join('');
@@ -570,11 +862,11 @@ function renderHeadersTab(request) {
570
862
  <details open style="margin-bottom: 1rem;">
571
863
  <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">General</summary>
572
864
  <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>
865
+ <div style="color: var(--text-flavor);">Request URL:</div><div style="word-break: break-all;">${request.url}</div>
866
+ <div style="color: var(--text-flavor);">Request Method:</div><div>${request.method}</div>
867
+ <div style="color: var(--text-flavor);">Status Code:</div><div>${request.status}</div>
868
+ <div style="color: var(--text-flavor);">Remote Address:</div><div>${request.remoteIP || '-'}</div>
869
+ <div style="color: var(--text-flavor);">Referrer Policy:</div><div>${request.requestHeaders?.['referrer-policy'] || 'strict-origin-when-cross-origin'}</div>
578
870
  </div>
579
871
  </details>
580
872
  ${formatHeaderSection('Response Headers', request.responseHeaders)}
@@ -604,48 +896,51 @@ function renderCookiesTab(request) {
604
896
  resCookies = [{ name: 'Set-Cookie', value: request.responseHeaders['set-cookie'] }];
605
897
  }
606
898
 
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
899
  return `
630
900
  <div style="padding: 0 0.5rem; display: flex; flex-direction: column; gap: 1rem;">
631
901
  <div>
632
902
  <div style="font-weight: bold; margin-bottom: 0.5rem;">Request Cookies</div>
633
- ${renderTable(reqCookies)}
903
+ ${renderNameValueTable(reqCookies, 'No cookies found')}
634
904
  </div>
635
905
  <div>
636
906
  <div style="font-weight: bold; margin-bottom: 0.5rem;">Response Cookies</div>
637
- ${renderTable(resCookies)}
907
+ ${renderNameValueTable(resCookies, 'No cookies found')}
638
908
  </div>
639
909
  </div>
640
910
  `;
641
911
  }
642
912
 
643
913
  function renderRequestTab(request) {
644
- if (!request.requestBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No payload</div>';
914
+ let queryParamsHtml = '';
915
+ try {
916
+ const url = new URL(request.url.startsWith('http') ? request.url : `http://${request.domain || 'localhost'}${request.url}`);
917
+ const params = [];
918
+ for (const [key, value] of url.searchParams) {
919
+ params.push({ name: key, value: value });
920
+ }
921
+
922
+ if (params.length > 0) {
923
+ queryParamsHtml = `
924
+ <div style="margin-bottom: 1rem;">
925
+ <div style="font-weight: bold; margin-bottom: 0.5rem; color: var(--text-primary);">Query Parameters</div>
926
+ ${renderNameValueTable(params)}
927
+ </div>
928
+ `;
929
+ }
930
+ } catch (e) {
931
+ console.error("Failed to parse URL for query params", e);
932
+ }
933
+
934
+ const hasBody = request.requestBody || request.body || (typeof request.requestBody === 'string' && request.requestBody.length > 0);
935
+
936
+ if (!hasBody && !queryParamsHtml) return '<div style="padding: 1rem; color: var(--text-secondary);">No payload or query parameters</div>';
937
+
645
938
  return `
646
939
  <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>
940
+ ${queryParamsHtml}
941
+ <div style="display: flex; justify-content: flex-end; padding: 4px; gap: 8px;">
942
+ <div style="font-size: 0.8em; color: var(--text-secondary); display: flex; align-items: center;">${request.requestBody ? formatBytes(request.requestBody.length || 0) : ''}</div>
943
+ <button class="btn-action" id="btn-copy-req-body">Copy</button>
649
944
  </div>
650
945
  <div id="request-body-editor" style="flex: 1; border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; min-height: 200px;"></div>
651
946
  </div>
@@ -656,7 +951,7 @@ function renderResponseTab(request) {
656
951
  if (!request.responseBody && !request.body) return '<div style="padding: 1rem; color: var(--text-secondary);">No content</div>';
657
952
 
658
953
  return `
659
- <div style="display: flex; flex-direction: column; height: 100%;">
954
+ <div style="display: flex; flex-direction: column; height: 100%">
660
955
  <div style="display: flex; justify-content: space-between; align-items: center; padding: 4px; border-bottom: 1px solid var(--border-color);">
661
956
  <div style="font-size: 0.8em; color: var(--text-secondary);">${formatBytes(request.size || 0)}</div>
662
957
  <div style="display: flex; gap: 8px; align-items: center;">
@@ -682,7 +977,8 @@ function renderTimingsTab(request) {
682
977
  <div>Duration:</div><div>${request.duration.toFixed(2)} ms</div>
683
978
  <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
979
  </div>
685
- <!-- Future: detailed breakdown -->
980
+ <div class="card-title" style="margin-top: 1rem; padding: 0">Middleware Trace</div>
981
+ <div id="middleware-trace-container"></div>
686
982
  </div>
687
983
  `;
688
984
  }
@@ -692,8 +988,8 @@ function renderSecurityTab(request) {
692
988
  <div style="padding: 1rem;">
693
989
  <div style="margin-bottom: 1rem; font-weight: bold;">Connection</div>
694
990
  <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>
991
+ <div style="color: var(--text-flavor);">Protocol:</div><div>${request.protocol || request.scheme || 'tls'}</div>
992
+ <div style="color: var(--text-flavor);">Remote Address:</div><div>${request.remoteIP || 'Unknown'}</div>
697
993
  </div>
698
994
  <div style="margin-top: 1rem; color: var(--text-secondary); font-style: italic;">
699
995
  Detailed certificate information is not currently captured by the interceptor.
@@ -702,14 +998,6 @@ function renderSecurityTab(request) {
702
998
  `;
703
999
  }
704
1000
 
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
1001
  function closeRequestDetails() {
714
1002
  document.getElementById('request-details-container').style.display = 'none';
715
1003
  if (window.requestsTable) window.requestsTable.redraw();
@@ -753,13 +1041,26 @@ function getExtension(contentType) {
753
1041
  if (contentType.includes('json')) return 'json';
754
1042
  if (contentType.includes('html')) return 'html';
755
1043
  if (contentType.includes('xml')) return 'xml';
756
- if (contentType.includes('javascript')) return 'js';
1044
+ if (contentType.includes('javascript')) return 'javascript';
757
1045
  if (contentType.includes('css')) return 'css';
1046
+ if (contentType.includes('image/png')) return 'png';
1047
+ if (contentType.includes('image/jpeg')) return 'jpeg';
1048
+ if (contentType.includes('image/gif')) return 'gif';
1049
+ if (contentType.includes('image/svg+xml')) return 'svg';
1050
+ if (contentType.includes('application/pdf')) return 'pdf';
1051
+ if (contentType.includes('application/zip')) return 'zip';
1052
+ if (contentType.includes('application/octet-stream')) return 'bin';
758
1053
  return 'txt';
759
1054
  }
760
1055
 
761
- function getRequestBody(request) {
762
- let value = request.body || '';
1056
+ function getContentType(headers) {
1057
+ if (!headers) return '';
1058
+ const output = headers['content-type'] || headers['Content-Type'] || '';
1059
+ return output.toLowerCase();
1060
+ }
1061
+
1062
+ function getBodyContent(body) {
1063
+ let value = body || '';
763
1064
  if (typeof value === 'object') {
764
1065
  try {
765
1066
  value = JSON.stringify(value, null, 2);
@@ -772,25 +1073,13 @@ function getRequestBody(request) {
772
1073
  return value;
773
1074
  }
774
1075
 
775
- let currentRequestBody = ''; // Global/Module scope tracking for request body
776
-
777
1076
  function initRequestEditor(request) {
778
1077
  const el = document.getElementById('request-body-editor');
779
1078
  if (!el) return;
780
1079
 
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';
1080
+ let content = request.requestBody || '';
1081
+ const contentType = getContentType(request.requestHeaders);
1082
+ let language = getExtension(contentType);
794
1083
 
795
1084
  if (typeof content === 'object') {
796
1085
  content = JSON.stringify(content, null, 2);
@@ -805,31 +1094,27 @@ function initRequestEditor(request) {
805
1094
  }
806
1095
  }
807
1096
 
808
- currentRequestBody = content; // store for copy
1097
+ // Handle binary
1098
+ if (content === '[Binary or Unreadable Body]') {
1099
+ el.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary);">Binary Content</div>';
1100
+ return;
1101
+ }
809
1102
 
810
- renderMonacoEditor(el, content, language, false); // Request body usually not auto-formatted
1103
+ renderMonacoEditor(el, content, language, false);
1104
+
1105
+ const btnCopy = document.getElementById('btn-copy-req-body');
1106
+ if (btnCopy) btnCopy.onclick = () => copyToClipboard(getBodyContent(request.requestBody));
811
1107
  }
812
1108
 
813
1109
  function initResponseEditor(request) {
814
1110
  const el = document.getElementById('response-body-editor');
815
1111
  if (!el) return;
816
1112
 
817
- let content = request.body || request.responseBody;
818
- let contentType = request.contentType || '';
819
-
1113
+ let content = request.body || request.responseBody; // fallback to responseBody property if mapped
820
1114
  if (!content) content = '';
821
1115
 
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';
1116
+ const contentType = getContentType(request.responseHeaders);
1117
+ let language = getExtension(contentType);
833
1118
 
834
1119
  if (typeof content === 'object') {
835
1120
  content = JSON.stringify(content, null, 2);
@@ -845,17 +1130,28 @@ function initResponseEditor(request) {
845
1130
  }
846
1131
  }
847
1132
 
1133
+ // Handle binary
1134
+ if (content === '[Binary or Unreadable Body]') {
1135
+ el.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary);">Binary Content</div>';
1136
+ // Ensure download button works
1137
+ const btnDownload = document.getElementById('btn-download-body');
1138
+ // We can't easily download the *actual* binary if we didn't store it.
1139
+ // But if we have it in memory or if backend serving it via separate endpoint...
1140
+ // For now, download the text placeholder is all we can do unless we fetch raw.
1141
+ if (btnDownload) btnDownload.onclick = () => alert("Original binary content not stored in dashboard history.");
1142
+ return;
1143
+ }
1144
+
848
1145
  renderMonacoEditor(el, content, language, window.autoFormatEnabled !== false);
849
1146
 
850
1147
  // Attach button listeners
851
1148
  const btnCopy = document.getElementById('btn-copy-body');
852
1149
  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)}`);
1150
+ if (btnCopy) btnCopy.onclick = () => copyToClipboard(getBodyContent(content));
1151
+ // TODO: replace with filename
1152
+ if (btnDownload) btnDownload.onclick = () => downloadString(getBodyContent(content), `body-${request.timestamp}.${getExtension(request.contentType)}`);
855
1153
  }
856
1154
 
857
- let currentMonacoEditor = null; // To manage the active editor instance
858
-
859
1155
  function renderMonacoEditor(containerElement, value, language, shouldFormat = false) {
860
1156
  if (!window.monaco) {
861
1157
  require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
@@ -863,6 +1159,8 @@ function renderMonacoEditor(containerElement, value, language, shouldFormat = fa
863
1159
  return;
864
1160
  }
865
1161
 
1162
+ console.log({ language });
1163
+ window.currentEditor?.dispose();
866
1164
  window.currentEditor = monaco.editor.create(containerElement, {
867
1165
  value: value,
868
1166
  language: language,
@@ -913,16 +1211,4 @@ function formatBytes(bytes, decimals = 2) {
913
1211
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
914
1212
  }
915
1213
 
916
- window.updateRequestsList = function (newRequests) {
917
- if (!window.requestsTable || !newRequests || newRequests.length === 0) return;
918
1214
 
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
- };