shokupan 0.11.0 → 0.13.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.
- package/README.md +47 -1815
- package/dist/{analyzer-CnKnQ5KV.js → analyzer-B0fMzeIo.js} +2 -2
- package/dist/{analyzer-CnKnQ5KV.js.map → analyzer-B0fMzeIo.js.map} +1 -1
- package/dist/{analyzer-BAhvpNY_.cjs → analyzer-BOtveWL-.cjs} +2 -2
- package/dist/{analyzer-BAhvpNY_.cjs.map → analyzer-BOtveWL-.cjs.map} +1 -1
- package/dist/{analyzer.impl-CfpMu4-g.cjs → analyzer.impl-CUDO6vpn.cjs} +82 -7
- package/dist/analyzer.impl-CUDO6vpn.cjs.map +1 -0
- package/dist/{analyzer.impl-DCiqlXI5.js → analyzer.impl-DmHe92Oi.js} +82 -7
- package/dist/analyzer.impl-DmHe92Oi.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +40 -8
- package/dist/index.cjs +2876 -506
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2911 -541
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/theme.css +4 -0
- package/dist/plugins/application/auth.d.ts +5 -0
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +9 -0
- package/dist/plugins/application/dashboard/static/requests.js +537 -251
- package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
- package/dist/plugins/application/dashboard/static/theme.css +4 -0
- package/dist/plugins/application/error-view/index.d.ts +14 -0
- package/dist/plugins/application/error-view/monkeypatch.d.ts +9 -0
- package/dist/plugins/application/error-view/util/source-reader.d.ts +10 -0
- package/dist/plugins/application/error-view/views/error.d.ts +2 -0
- package/dist/plugins/application/error-view/views/status.d.ts +2 -0
- package/dist/plugins/application/htmx/index.d.ts +39 -0
- package/dist/plugins/application/mcp-server/plugin.d.ts +38 -0
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -0
- package/dist/plugins/application/openapi/test-setup.d.ts +1 -0
- package/dist/plugins/application/opentelemetry/index.d.ts +33 -0
- package/dist/plugins/middleware/compression.d.ts +12 -2
- package/dist/plugins/middleware/rate-limit.d.ts +5 -0
- package/dist/plugins/middleware/session.d.ts +4 -4
- package/dist/plugins/resilience/decorators.d.ts +23 -0
- package/dist/plugins/resilience/factory.d.ts +5 -0
- package/dist/plugins/resilience/index.d.ts +2 -0
- package/dist/router.d.ts +25 -9
- package/dist/server.d.ts +22 -0
- package/dist/shokupan.d.ts +24 -1
- package/dist/util/adapter/bun.d.ts +8 -0
- package/dist/util/adapter/index.d.ts +4 -0
- package/dist/util/adapter/interface.d.ts +12 -0
- package/dist/util/adapter/node.d.ts +8 -0
- package/dist/util/adapter/wintercg.d.ts +5 -0
- package/dist/util/body-parser.d.ts +30 -0
- package/dist/util/decorators.d.ts +58 -3
- package/dist/util/di.d.ts +3 -8
- package/dist/util/env-loader.d.ts +99 -0
- package/dist/util/mcp-protocol.d.ts +52 -0
- package/dist/util/metadata.d.ts +18 -0
- package/dist/util/promise.d.ts +16 -0
- package/dist/util/request.d.ts +1 -0
- package/dist/util/symbol.d.ts +5 -0
- package/dist/util/types.d.ts +140 -3
- package/package.json +37 -10
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +0 -1
- package/dist/analyzer.impl-DCiqlXI5.js.map +0 -1
- package/dist/plugins/application/dashboard/static/failures.js +0 -85
- package/dist/plugins/application/http-server.d.ts +0 -13
- package/dist/util/adapter/adapters.d.ts +0 -19
- package/dist/util/instrumentation.d.ts +0 -9
|
@@ -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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
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
|
-
|
|
432
|
-
//
|
|
617
|
+
const totalRange = maxRequestTime - minRequestTime;
|
|
618
|
+
// Prevent divide by zero
|
|
619
|
+
const safeRange = totalRange <= 0 ? 1 : totalRange;
|
|
433
620
|
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
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
|
|
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="
|
|
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
|
-
|
|
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 = '
|
|
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)
|
|
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')
|
|
521
|
-
|
|
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-
|
|
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-
|
|
574
|
-
<div style="color: var(--text-
|
|
575
|
-
<div style="color: var(--text-
|
|
576
|
-
<div style="color: var(--text-
|
|
577
|
-
<div style="color: var(--text-
|
|
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
|
-
${
|
|
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
|
-
${
|
|
907
|
+
${renderNameValueTable(resCookies, 'No cookies found')}
|
|
638
908
|
</div>
|
|
639
909
|
</div>
|
|
640
910
|
`;
|
|
641
911
|
}
|
|
642
912
|
|
|
643
913
|
function renderRequestTab(request) {
|
|
644
|
-
|
|
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
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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-
|
|
696
|
-
<div style="color: var(--text-
|
|
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 '
|
|
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
|
|
762
|
-
|
|
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 ||
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
823
|
-
let language =
|
|
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(
|
|
854
|
-
|
|
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
|
-
};
|