@metricinsights/pp-dev 1.5.0-beta.3 → 1.5.0-beta.31

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 (72) hide show
  1. package/README.md +200 -145
  2. package/dist/cjs/auth.provider-fI0tFsf5.js +2 -0
  3. package/dist/cjs/auth.provider-fI0tFsf5.js.map +1 -0
  4. package/dist/cjs/cli.js +1 -1
  5. package/dist/cjs/cli.js.map +1 -1
  6. package/dist/cjs/index-Ty1d2YCg.js +2 -0
  7. package/dist/cjs/index-Ty1d2YCg.js.map +1 -0
  8. package/dist/cjs/index.js +1 -1
  9. package/dist/cjs/migrate-Gi9k9cLw.js +2 -0
  10. package/dist/cjs/migrate-Gi9k9cLw.js.map +1 -0
  11. package/dist/cjs/package.json +2 -135
  12. package/dist/cjs/plugin.js +1 -1
  13. package/dist/cjs/plugin.js.map +1 -1
  14. package/dist/cjs/request-capture.middleware-6DhFLyIz.js +2 -0
  15. package/dist/cjs/request-capture.middleware-6DhFLyIz.js.map +1 -0
  16. package/dist/cjs/version-manifest-CVBuIKBS.js +2 -0
  17. package/dist/cjs/version-manifest-CVBuIKBS.js.map +1 -0
  18. package/dist/cjs/version-plugin-DBdqn2Rn.js +2 -0
  19. package/dist/cjs/version-plugin-DBdqn2Rn.js.map +1 -0
  20. package/dist/client/client.js +278 -126
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/client/request-inspector.css +3 -0
  23. package/dist/client/request-inspector.css.map +1 -0
  24. package/dist/client/request-inspector.html +87 -0
  25. package/dist/client/request-inspector.js +549 -0
  26. package/dist/client/request-inspector.js.map +1 -0
  27. package/dist/client/variables-editor.css +3 -0
  28. package/dist/client/variables-editor.css.map +1 -0
  29. package/dist/client/variables-editor.html +44 -0
  30. package/dist/client/variables-editor.js +3474 -0
  31. package/dist/client/variables-editor.js.map +1 -0
  32. package/dist/esm/auth.provider-DDUsu1H1.js +2 -0
  33. package/dist/esm/auth.provider-DDUsu1H1.js.map +1 -0
  34. package/dist/esm/cli.js +1 -1
  35. package/dist/esm/cli.js.map +1 -1
  36. package/dist/esm/index-tD-QICGk.js +2 -0
  37. package/dist/esm/index-tD-QICGk.js.map +1 -0
  38. package/dist/esm/index.js +1 -1
  39. package/dist/esm/migrate-CCBRuVfj.js +2 -0
  40. package/dist/esm/{migrate-B2gJ62fm.js.map → migrate-CCBRuVfj.js.map} +1 -1
  41. package/dist/esm/package.json +2 -135
  42. package/dist/esm/plugin.js +1 -1
  43. package/dist/esm/plugin.js.map +1 -1
  44. package/dist/esm/request-capture.middleware-VElwjU1W.js +2 -0
  45. package/dist/esm/request-capture.middleware-VElwjU1W.js.map +1 -0
  46. package/dist/esm/version-manifest-CyzmnFOe.js +2 -0
  47. package/dist/esm/version-manifest-CyzmnFOe.js.map +1 -0
  48. package/dist/esm/version-plugin-6rxIgQ-I.js +2 -0
  49. package/dist/esm/version-plugin-6rxIgQ-I.js.map +1 -0
  50. package/dist/types/helpers.d.ts +29 -0
  51. package/dist/types/index.d.ts +15 -156
  52. package/dist/types/plugin.d.ts +8 -0
  53. package/dist/types/pp-dev-config-D889A-PO.d.ts +189 -0
  54. package/docs/features.md +2 -1
  55. package/package.json +26 -35
  56. package/dist/CHANGELOG.md +0 -442
  57. package/dist/README.md +0 -724
  58. package/dist/cjs/index-CCUrjlJE.js +0 -2
  59. package/dist/cjs/index-CCUrjlJE.js.map +0 -1
  60. package/dist/cjs/migrate-BYno__sx.js +0 -2
  61. package/dist/cjs/migrate-BYno__sx.js.map +0 -1
  62. package/dist/cjs/plugin-D_RXI7ru.js +0 -2
  63. package/dist/cjs/plugin-D_RXI7ru.js.map +0 -1
  64. package/dist/cjs/version-plugin-Bf4jLh17.js +0 -2
  65. package/dist/cjs/version-plugin-Bf4jLh17.js.map +0 -1
  66. package/dist/esm/index-BazQY12s.js +0 -2
  67. package/dist/esm/index-BazQY12s.js.map +0 -1
  68. package/dist/esm/migrate-B2gJ62fm.js +0 -2
  69. package/dist/esm/plugin-NIWxLreC.js +0 -2
  70. package/dist/esm/plugin-NIWxLreC.js.map +0 -1
  71. package/dist/esm/version-plugin-DwuZCHob.js +0 -2
  72. package/dist/esm/version-plugin-DwuZCHob.js.map +0 -1
@@ -0,0 +1,549 @@
1
+ // localStorage helpers guarded against environments where storage is unavailable
2
+ // (e.g. sandboxed iframes). Keys follow the original `pp-dev-info-closed` naming.
3
+ const STORAGE_KEYS = {
4
+ /** Shared with the standalone Inspector/Variables Editor pages — same key, same values. */
5
+ theme: 'pp-dev-info-theme',
6
+ };
7
+ function checkLocalStorage() {
8
+ try {
9
+ localStorage.setItem('test', 'test');
10
+ localStorage.removeItem('test');
11
+ return true;
12
+ }
13
+ catch (e) {
14
+ return false;
15
+ }
16
+ }
17
+ function setStorageItem(key, value) {
18
+ if (checkLocalStorage()) {
19
+ localStorage.setItem(key, value);
20
+ }
21
+ }
22
+ function getStorageItem(key) {
23
+ if (checkLocalStorage()) {
24
+ return localStorage.getItem(key);
25
+ }
26
+ return null;
27
+ }
28
+
29
+ // Theme override (Auto/Dark/Light), shared across the dev panel and the standalone
30
+ // Inspector/Variables Editor pages via the same localStorage key (see STORAGE_KEYS.theme) and
31
+ // the same `data-pp-dev-theme` attribute convention (prefixed to avoid colliding with a host
32
+ // Portal Page's own theme attribute, since the panel is injected into pages pp-dev doesn't own).
33
+ const THEME_ATTR = 'data-pp-dev-theme';
34
+ function getStoredTheme() {
35
+ const value = getStorageItem(STORAGE_KEYS.theme);
36
+ return value === 'dark' || value === 'light' ? value : 'auto';
37
+ }
38
+ function applyTheme(theme) {
39
+ if (theme === 'dark' || theme === 'light') {
40
+ document.documentElement.setAttribute(THEME_ATTR, theme);
41
+ }
42
+ else {
43
+ document.documentElement.removeAttribute(THEME_ATTR);
44
+ }
45
+ }
46
+ function setTheme(theme) {
47
+ setStorageItem(STORAGE_KEYS.theme, theme);
48
+ applyTheme(theme);
49
+ }
50
+
51
+ // Formatting helpers of the Request Inspector page: pure, so the tests import them directly.
52
+ /** Escapes text for HTML content and double-quoted attribute values. */
53
+ function esc(value) {
54
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
55
+ }
56
+ function formatBytes(bytes) {
57
+ const b = bytes || 0;
58
+ if (b < 1024) {
59
+ return b + ' B';
60
+ }
61
+ if (b < 1024 * 1024) {
62
+ return (b / 1024).toFixed(1) + ' KB';
63
+ }
64
+ if (b < 1024 * 1024 * 1024) {
65
+ return (b / (1024 * 1024)).toFixed(1) + ' MB';
66
+ }
67
+ return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
68
+ }
69
+ function formatDur(ms) {
70
+ if (ms < 1000) {
71
+ return ms + 'ms';
72
+ }
73
+ return (ms / 1000).toFixed(2) + 's';
74
+ }
75
+ const METHOD_CLASSES = {
76
+ GET: 'm-GET',
77
+ POST: 'm-POST',
78
+ PUT: 'm-PUT',
79
+ PATCH: 'm-PATCH',
80
+ DELETE: 'm-DELETE',
81
+ HEAD: 'm-HEAD',
82
+ OPTIONS: 'm-OPTIONS',
83
+ };
84
+ function methodClass(method) {
85
+ return METHOD_CLASSES[method] || 'm-other';
86
+ }
87
+ function statusClass(status) {
88
+ if (!status) {
89
+ return 's-pending';
90
+ }
91
+ return ['', '', 's-2xx', 's-3xx', 's-4xx', 's-5xx'][Math.floor(status / 100)] || '';
92
+ }
93
+ function sourceInfo(source) {
94
+ if (source === 'proxy') {
95
+ return { cls: 'src-proxy', title: 'Backend (live)' };
96
+ }
97
+ if (source === 'proxy-cache') {
98
+ return { cls: 'src-proxy-cache', title: 'Backend (cached)' };
99
+ }
100
+ return { cls: 'src-local', title: 'Local (dev server)' };
101
+ }
102
+ function sourceLetter(source) {
103
+ if (source === 'proxy') {
104
+ return 'P';
105
+ }
106
+ if (source === 'proxy-cache') {
107
+ return 'C';
108
+ }
109
+ return 'L';
110
+ }
111
+ const FILE_EXTENSIONS = {
112
+ 'application/json': 'json',
113
+ 'text/html': 'html',
114
+ 'text/css': 'css',
115
+ 'text/javascript': 'js',
116
+ 'application/javascript': 'js',
117
+ 'text/plain': 'txt',
118
+ 'text/xml': 'xml',
119
+ 'application/xml': 'xml',
120
+ 'image/png': 'png',
121
+ 'image/jpeg': 'jpg',
122
+ 'image/gif': 'gif',
123
+ 'image/svg+xml': 'svg',
124
+ 'image/webp': 'webp',
125
+ 'application/pdf': 'pdf',
126
+ 'application/zip': 'zip',
127
+ 'application/x-www-form-urlencoded': 'txt',
128
+ 'application/octet-stream': 'bin',
129
+ };
130
+ /** The file extension a saved body of `contentType` gets. */
131
+ function fileExt(contentType) {
132
+ return FILE_EXTENSIONS[(contentType || '').split(';')[0].trim().toLowerCase()] || 'bin';
133
+ }
134
+ function decodeBase64(b64) {
135
+ return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
136
+ }
137
+ function decodeTextBody(b64) {
138
+ return new TextDecoder('utf-8').decode(decodeBase64(b64));
139
+ }
140
+ /** Pretty-prints JSON with highlighting spans; invalid JSON comes back as escaped plain text. */
141
+ function syntaxHighlightJson(text) {
142
+ let str;
143
+ try {
144
+ str = JSON.stringify(JSON.parse(text), null, 2);
145
+ }
146
+ catch {
147
+ // Invalid JSON can contain arbitrary HTML. It cannot be token-highlighted safely because
148
+ // unmatched text is preserved by the replacement below, so render it as escaped plain text.
149
+ return esc(text);
150
+ }
151
+ return str.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"\s*:?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (m) => {
152
+ if (/^"/.test(m)) {
153
+ if (/:$/.test(m)) {
154
+ return '<span class="body-json-key">' + esc(m) + '</span>';
155
+ }
156
+ return '<span class="body-json-str">' + esc(m) + '</span>';
157
+ }
158
+ if (/true|false/.test(m)) {
159
+ return '<span class="body-json-bool">' + m + '</span>';
160
+ }
161
+ if (/null/.test(m)) {
162
+ return '<span class="body-json-null">' + m + '</span>';
163
+ }
164
+ return '<span class="body-json-num">' + m + '</span>';
165
+ });
166
+ }
167
+
168
+ // HTML of the Request Inspector's list and detail. Buttons say what they do in `data-action`
169
+ // (and which side in `data-type`); main.ts handles the clicks in one place.
170
+ function renderListHtml(requests, selectedId) {
171
+ if (!requests.length) {
172
+ return '<div class="empty-state"><div class="empty-icon">🔍</div><div>No matching requests</div></div>';
173
+ }
174
+ return requests
175
+ .map((r) => {
176
+ const sel = r.id === selectedId ? ' selected' : '';
177
+ const dur = r.duration != null ? formatDur(r.duration) : '…';
178
+ const source = r.source || 'local';
179
+ return `<div class="req-item src-${source}${sel}" data-id="${esc(r.id)}">
180
+ <span class="method-badge ${methodClass(r.method)}">${esc(r.method)}</span>
181
+ ${r.user ? `<span class="user-chip" title="From the tab of user ${esc(r.user)}">${esc(r.user)}</span>` : ''}
182
+ <span class="req-url" title="${esc(r.url)}">${esc(r.url)}</span>
183
+ <span class="req-meta">
184
+ <span class="req-status-row">
185
+ <span class="source-letter src-letter-${source}">${sourceLetter(r.source)}</span>
186
+ <span class="status-badge ${statusClass(r.statusCode)}">${r.statusCode || '…'}</span>
187
+ </span>
188
+ <span>${dur}</span>
189
+ <span>${formatBytes(r.responseSize || 0)}</span>
190
+ </span>
191
+ </div>`;
192
+ })
193
+ .join('');
194
+ }
195
+ function renderDetailInfoHtml(e) {
196
+ const src = sourceInfo(e.source);
197
+ return `
198
+ <div class="info-item"><div class="info-label">Method</div><div class="info-value"><span class="method-badge ${methodClass(e.method)}">${esc(e.method)}</span></div></div>
199
+ <div class="info-item"><div class="info-label">Status</div><div class="info-value"><span class="status-badge ${statusClass(e.statusCode)}">${e.statusCode || '—'}</span></div></div>
200
+ <div class="info-item"><div class="info-label">Source</div><div class="info-value"><span class="source-chip ${src.cls}">${src.title}</span></div></div>
201
+ <div class="info-item"><div class="info-label">User</div><div class="info-value">${e.user ? `<span class="user-chip">${esc(e.user)}</span>` : 'Original'}</div></div>
202
+ <div class="info-item"><div class="info-label">Duration</div><div class="info-value">${e.duration != null ? formatDur(e.duration) : '—'}</div></div>
203
+ <div class="info-item"><div class="info-label">Request</div><div class="info-value">${formatBytes(e.requestBodySize || 0)}</div></div>
204
+ <div class="info-item"><div class="info-label">Response</div><div class="info-value">${formatBytes(e.responseBodySize || 0)}</div></div>
205
+ <div class="info-item"><div class="info-label">Time</div><div class="info-value" style="font-size:11px">${new Date(e.timestamp).toLocaleTimeString()}</div></div>
206
+ `;
207
+ }
208
+ function renderDetailUrlHtml(e) {
209
+ return `<span class="info-url-text" title="${esc(e.url)}">${esc(e.url)}</span><button class="section-btn" data-action="copy-url">Copy link</button>`;
210
+ }
211
+ function sectionHtml(title, badge, actions, body) {
212
+ return `<div class="detail-section">
213
+ <div class="detail-section-hdr" data-action="toggle-section">
214
+ <span class="toggle">▾</span>
215
+ <span class="section-title">${title}</span>
216
+ ${badge}
217
+ ${actions ? `<div class="section-actions">${actions}</div>` : ''}
218
+ </div>
219
+ <div class="detail-section-body">${body}</div>
220
+ </div>`;
221
+ }
222
+ function renderHeadersSectionHtml(title, headers, type) {
223
+ const entries = Object.entries(headers || {}).filter(([, v]) => v != null);
224
+ const rows = entries
225
+ .map(([k, v]) => `<tr><td>${esc(k)}</td><td>${esc(Array.isArray(v) ? v.join(', ') : String(v))}</td></tr>`)
226
+ .join('');
227
+ const copy = entries.length
228
+ ? `<button class="section-btn" data-action="copy-headers" data-type="${type}">Copy</button>`
229
+ : '';
230
+ return sectionHtml(title, `<span class="section-badge">${entries.length}</span>`, copy, `<table class="kv-table">${rows}</table>`);
231
+ }
232
+ /**
233
+ * A body shown and copied as text: JSON, script, XML (an SVG among them) and text/* types. XML
234
+ * is `application/xml` or a `+xml` suffix: the Office types (`application/vnd.openxmlformats-…`)
235
+ * name XML too, and are ZIP archives.
236
+ */
237
+ function isTextType(ct) {
238
+ return (ct.includes('json') ||
239
+ ct.startsWith('text/') ||
240
+ ct.includes('/javascript') ||
241
+ ct === 'application/xml' ||
242
+ ct.endsWith('+xml') ||
243
+ ct === 'application/x-www-form-urlencoded');
244
+ }
245
+ function bodyContentHtml(b64, ct, truncated, size) {
246
+ if (!b64 && !truncated) {
247
+ return '<div class="body-binary">No body</div>';
248
+ }
249
+ if (truncated) {
250
+ return `<div class="body-binary">Body too large to capture (${formatBytes(size)})</div>`;
251
+ }
252
+ if (ct.includes('json') || ct.includes('/javascript') || ct === 'text/plain') {
253
+ const text = decodeTextBody(b64);
254
+ return ct.includes('json')
255
+ ? `<pre class="body-content json">${syntaxHighlightJson(text)}</pre>`
256
+ : `<pre class="body-content">${esc(text)}</pre>`;
257
+ }
258
+ if (ct.startsWith('image/') && size < 1024 * 1024) {
259
+ return `<img class="body-image" src="data:${esc(ct)};base64,${b64}" alt="image"/>`;
260
+ }
261
+ if (ct === 'application/x-www-form-urlencoded') {
262
+ const params = new URLSearchParams(decodeTextBody(b64));
263
+ const rows = [...params.entries()].map(([k, v]) => `<tr><td>${esc(k)}</td><td>${esc(v)}</td></tr>`).join('');
264
+ return `<table class="kv-table">${rows}</table>`;
265
+ }
266
+ // XML was offered for copying as text, and shown as "Binary data".
267
+ if (isTextType(ct)) {
268
+ return `<pre class="body-content">${esc(decodeTextBody(b64))}</pre>`;
269
+ }
270
+ return `<div class="body-binary">Binary data — ${formatBytes(size)} (${esc(ct || 'unknown type')})</div>`;
271
+ }
272
+ function renderBodySectionHtml(title, b64, contentType, truncated, size, type, captureLimit) {
273
+ const ct = (contentType || '').toLowerCase().split(';')[0].trim();
274
+ const copy = b64 && isTextType(ct)
275
+ ? `<button class="section-btn" data-action="copy-body" data-type="${type}">Copy</button>`
276
+ : '';
277
+ const save = b64 ? `<button class="section-btn" data-action="save-body" data-type="${type}">Save</button>` : '';
278
+ const note = truncated
279
+ ? `<div class="truncated-note">⚠ Body was larger than the capture limit (${formatBytes(captureLimit)})</div>`
280
+ : '';
281
+ return sectionHtml(title, size ? `<span class="section-badge">${formatBytes(size)}</span>` : '', copy + save, bodyContentHtml(b64, ct, truncated, size) + note);
282
+ }
283
+ function renderDetailSectionsHtml(e, captureLimit) {
284
+ return [
285
+ renderHeadersSectionHtml('Request Headers', e.requestHeaders, 'request'),
286
+ renderBodySectionHtml('Request Body', e.requestBody, e.requestContentType, e.requestBodyTruncated, e.requestBodySize, 'request', captureLimit),
287
+ renderHeadersSectionHtml('Response Headers', e.responseHeaders, 'response'),
288
+ renderBodySectionHtml('Response Body', e.responseBody, e.responseContentType, e.responseBodyTruncated, e.responseBodySize, 'response', captureLimit),
289
+ ].join('');
290
+ }
291
+
292
+ // The Request Inspector page (`/@pp-dev/inspector`): lists what went through the dev server and
293
+ // shows a request's headers and bodies. main.ts starts it on request-inspector.html.
294
+ /** Value of the user filter for requests sent as the original user (a saved user's name can't start with "@"). */
295
+ const ORIGINAL_USER = '@original';
296
+ const DEFAULT_CAPTURE_LIMIT = 10 * 1024 * 1024;
297
+ /** Starts the page on the current document; returns a function that stops it (polling and listeners). */
298
+ function startInspector({ pollIntervalMs = 2000 } = {}) {
299
+ const byId = (id) => document.getElementById(id);
300
+ const captureLimit = Number(document.body.dataset.captureLimit) || DEFAULT_CAPTURE_LIMIT;
301
+ const listEl = byId('req-list');
302
+ const detailEmpty = byId('detail-empty');
303
+ const detailPanel = byId('detail-panel');
304
+ const detailInfo = byId('detail-info');
305
+ const detailUrl = byId('detail-url');
306
+ const detailScroll = byId('detail-scroll');
307
+ const filterBar = byId('filter-bar');
308
+ const searchInput = byId('search-input');
309
+ const methodSelect = byId('method-select');
310
+ const statusSelect = byId('status-select');
311
+ const sourceSelect = byId('source-select');
312
+ const userSelect = byId('user-select');
313
+ const themeSwitch = byId('theme-switch');
314
+ let allRequests = [];
315
+ let filteredRequests = [];
316
+ let selectedId = null;
317
+ let currentEntry = null;
318
+ // ── Theme (Auto/Dark/Light) ────────────────────────────────────────────────
319
+ // The page's <head> already set the stored theme before first paint; this keeps the buttons in
320
+ // step. Shared with the dev panel and the Variables Editor (see ../theme.ts).
321
+ const showTheme = (theme) => {
322
+ applyTheme(theme);
323
+ for (const button of Array.from(themeSwitch.children)) {
324
+ const isActive = button.dataset.themeChoice === theme;
325
+ button.classList.toggle('active', isActive);
326
+ button.setAttribute('aria-pressed', String(isActive));
327
+ }
328
+ };
329
+ // ── List ───────────────────────────────────────────────────────────────────
330
+ const updateStats = (data) => {
331
+ const pct = data.maxMemory > 0 ? Math.min(100, (data.memoryUsage / data.maxMemory) * 100) : 0;
332
+ byId('req-count').textContent = data.total + ' request' + (data.total !== 1 ? 's' : '');
333
+ byId('mem-fill').style.width = pct + '%';
334
+ byId('mem-text').textContent = formatBytes(data.memoryUsage) + ' / ' + formatBytes(data.maxMemory);
335
+ };
336
+ // The user filter only shows up once a request was sent as a saved user.
337
+ const updateUserOptions = () => {
338
+ const names = Array.from(new Set(allRequests.map((r) => r.user).filter((u) => Boolean(u)))).sort();
339
+ const current = userSelect.value;
340
+ userSelect.style.display = names.length ? '' : 'none';
341
+ userSelect.replaceChildren(new Option('All users', ''), new Option('Original', ORIGINAL_USER), ...names.map((name) => new Option(name, name)));
342
+ userSelect.value = current === ORIGINAL_USER || names.includes(current) ? current : '';
343
+ };
344
+ const renderList = () => {
345
+ const atBottom = listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - 16;
346
+ listEl.innerHTML = renderListHtml(filteredRequests, selectedId);
347
+ if (atBottom && filteredRequests.length) {
348
+ listEl.scrollTop = listEl.scrollHeight;
349
+ }
350
+ };
351
+ const applyFilter = () => {
352
+ const search = searchInput.value.toLowerCase();
353
+ const method = methodSelect.value;
354
+ const status = statusSelect.value;
355
+ const source = sourceSelect.value;
356
+ const user = userSelect.value;
357
+ filteredRequests = allRequests.filter((r) => !(user && (r.user || ORIGINAL_USER) !== user) &&
358
+ !(method && r.method !== method) &&
359
+ !(status && !String(r.statusCode || '').startsWith(status)) &&
360
+ !(source && r.source !== source) &&
361
+ !(search && !r.url.toLowerCase().includes(search)));
362
+ renderList();
363
+ };
364
+ const showDetailEmpty = () => {
365
+ detailPanel.style.display = 'none';
366
+ detailEmpty.style.display = 'flex';
367
+ };
368
+ const fetchList = async () => {
369
+ try {
370
+ const data = (await (await fetch('/@api/requests')).json());
371
+ allRequests = data.requests || [];
372
+ updateStats(data);
373
+ updateUserOptions();
374
+ applyFilter();
375
+ if (selectedId && !allRequests.find((r) => r.id === selectedId)) {
376
+ selectedId = null;
377
+ showDetailEmpty();
378
+ }
379
+ }
380
+ catch {
381
+ // The dev server is restarting; the next poll tries again.
382
+ }
383
+ };
384
+ // ── Detail ─────────────────────────────────────────────────────────────────
385
+ const selectRequest = async (id) => {
386
+ selectedId = id;
387
+ renderList();
388
+ detailEmpty.style.display = 'none';
389
+ detailPanel.style.display = 'flex';
390
+ detailPanel.style.flexDirection = 'column';
391
+ detailPanel.style.overflow = 'hidden';
392
+ detailScroll.innerHTML = '<div style="padding:20px;color:var(--text3)">Loading…</div>';
393
+ try {
394
+ const response = await fetch('/@api/requests/' + encodeURIComponent(id));
395
+ // Another row was clicked meanwhile: this answer, arriving last, was shown under the other
396
+ // row's highlight, and Copy/Save acted on it.
397
+ if (selectedId !== id) {
398
+ return;
399
+ }
400
+ if (!response.ok) {
401
+ showDetailEmpty();
402
+ return;
403
+ }
404
+ const loaded = (await response.json());
405
+ if (selectedId !== id) {
406
+ return;
407
+ }
408
+ currentEntry = loaded;
409
+ detailInfo.innerHTML = renderDetailInfoHtml(currentEntry);
410
+ detailUrl.innerHTML = renderDetailUrlHtml(currentEntry);
411
+ detailScroll.innerHTML = renderDetailSectionsHtml(currentEntry, captureLimit);
412
+ }
413
+ catch {
414
+ if (selectedId === id) {
415
+ showDetailEmpty();
416
+ }
417
+ }
418
+ };
419
+ const toggleSection = (header) => {
420
+ const body = header.nextElementSibling;
421
+ const toggle = header.querySelector('.toggle');
422
+ if (body && toggle) {
423
+ toggle.textContent = body.classList.toggle('collapsed') ? '▸' : '▾';
424
+ }
425
+ };
426
+ const clearAll = async () => {
427
+ await fetch('/@api/requests', { method: 'DELETE' });
428
+ allRequests = [];
429
+ filteredRequests = [];
430
+ selectedId = null;
431
+ showDetailEmpty();
432
+ renderList();
433
+ updateStats({ total: 0, memoryUsage: 0, maxMemory: 1024 * 1024 * 1024 });
434
+ };
435
+ // ── Copy / Save ────────────────────────────────────────────────────────────
436
+ const clipboard = (text, button) => {
437
+ navigator.clipboard
438
+ .writeText(text)
439
+ .then(() => {
440
+ const label = button.textContent;
441
+ button.textContent = '✓';
442
+ button.style.color = 'var(--green)';
443
+ setTimeout(() => {
444
+ button.textContent = label;
445
+ button.style.color = '';
446
+ }, 1500);
447
+ })
448
+ .catch(() => { });
449
+ };
450
+ const copyHeaders = (type, button) => {
451
+ const headers = type === 'request' ? currentEntry?.requestHeaders : currentEntry?.responseHeaders;
452
+ if (!headers) {
453
+ return;
454
+ }
455
+ const text = Object.entries(headers)
456
+ .filter(([, v]) => v != null)
457
+ .map(([k, v]) => k + ': ' + (Array.isArray(v) ? v.join(', ') : String(v)))
458
+ .join('\n');
459
+ clipboard(text, button);
460
+ };
461
+ const bodyOf = (type) => (type === 'request' ? currentEntry?.requestBody : currentEntry?.responseBody);
462
+ const copyBody = (type, button) => {
463
+ const b64 = bodyOf(type);
464
+ if (!b64) {
465
+ return;
466
+ }
467
+ try {
468
+ clipboard(decodeTextBody(b64), button);
469
+ }
470
+ catch {
471
+ // Not text after all.
472
+ }
473
+ };
474
+ const saveBody = (type) => {
475
+ const b64 = bodyOf(type);
476
+ const contentType = type === 'request' ? currentEntry?.requestContentType : currentEntry?.responseContentType;
477
+ if (!b64) {
478
+ return;
479
+ }
480
+ try {
481
+ const url = URL.createObjectURL(new Blob([decodeBase64(b64)]));
482
+ const link = document.createElement('a');
483
+ link.href = url;
484
+ link.download = type + '-body.' + fileExt(contentType);
485
+ document.body.appendChild(link);
486
+ link.click();
487
+ document.body.removeChild(link);
488
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
489
+ }
490
+ catch {
491
+ // Not valid base64.
492
+ }
493
+ };
494
+ // ── Events ─────────────────────────────────────────────────────────────────
495
+ // One handler for the whole page: list items carry `data-id`, buttons `data-action` (and
496
+ // `data-type` for request/response), the theme buttons `data-theme-choice`.
497
+ const onClick = (event) => {
498
+ const target = event.target;
499
+ const themeButton = target.closest('[data-theme-choice]');
500
+ if (themeButton) {
501
+ const theme = themeButton.dataset.themeChoice;
502
+ setTheme(theme);
503
+ showTheme(theme);
504
+ return;
505
+ }
506
+ const actionEl = target.closest('[data-action]');
507
+ const type = actionEl?.dataset.type;
508
+ switch (actionEl?.dataset.action) {
509
+ case 'clear':
510
+ void clearAll();
511
+ return;
512
+ case 'copy-url':
513
+ clipboard(currentEntry?.url ?? '', actionEl);
514
+ return;
515
+ case 'copy-headers':
516
+ copyHeaders(type, actionEl);
517
+ return;
518
+ case 'copy-body':
519
+ copyBody(type, actionEl);
520
+ return;
521
+ case 'save-body':
522
+ saveBody(type);
523
+ return;
524
+ case 'toggle-section':
525
+ toggleSection(actionEl);
526
+ return;
527
+ }
528
+ const item = target.closest('.req-item');
529
+ if (item?.dataset.id) {
530
+ void selectRequest(item.dataset.id);
531
+ }
532
+ };
533
+ document.addEventListener('click', onClick);
534
+ filterBar.addEventListener('input', applyFilter);
535
+ filterBar.addEventListener('change', applyFilter);
536
+ showTheme(getStoredTheme());
537
+ void fetchList();
538
+ const poll = setInterval(() => void fetchList(), pollIntervalMs);
539
+ return () => {
540
+ clearInterval(poll);
541
+ document.removeEventListener('click', onClick);
542
+ filterBar.removeEventListener('input', applyFilter);
543
+ filterBar.removeEventListener('change', applyFilter);
544
+ };
545
+ }
546
+
547
+ // Entry of the Request Inspector bundle: its styles and page, built next to it in dist/client.
548
+ startInspector();
549
+ //# sourceMappingURL=request-inspector.js.map