shokupan 0.13.0 → 0.14.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 (97) hide show
  1. package/dist/{analyzer-BOtveWL-.cjs → analyzer-BZSVGTmP.cjs} +5 -4
  2. package/dist/analyzer-BZSVGTmP.cjs.map +1 -0
  3. package/dist/{analyzer-B0fMzeIo.js → analyzer-Faojwm7c.js} +5 -4
  4. package/dist/analyzer-Faojwm7c.js.map +1 -0
  5. package/dist/{analyzer.impl-CUDO6vpn.cjs → analyzer.impl-5aCqtook.cjs} +28 -11
  6. package/dist/analyzer.impl-5aCqtook.cjs.map +1 -0
  7. package/dist/{analyzer.impl-DmHe92Oi.js → analyzer.impl-COdN69gL.js} +28 -11
  8. package/dist/analyzer.impl-COdN69gL.js.map +1 -0
  9. package/dist/ast-analyzer-worker-C3jrQ8VR.js +184 -0
  10. package/dist/ast-analyzer-worker-C3jrQ8VR.js.map +1 -0
  11. package/dist/ast-analyzer-worker-D_uYkqmY.cjs +184 -0
  12. package/dist/ast-analyzer-worker-D_uYkqmY.cjs.map +1 -0
  13. package/dist/cli.cjs +1 -1
  14. package/dist/cli.js +1 -1
  15. package/dist/context.d.ts +39 -4
  16. package/dist/decorators/di.d.ts +31 -0
  17. package/dist/decorators/hooks.d.ts +28 -0
  18. package/dist/decorators/http.d.ts +60 -0
  19. package/dist/decorators/index.d.ts +8 -0
  20. package/dist/decorators/mcp.d.ts +48 -0
  21. package/dist/decorators/util/container.d.ts +36 -0
  22. package/dist/decorators/websocket.d.ts +172 -0
  23. package/dist/index-BP7v0Hiv.cjs +12216 -0
  24. package/dist/index-BP7v0Hiv.cjs.map +1 -0
  25. package/dist/index-CUNBeZKj.js +12176 -0
  26. package/dist/index-CUNBeZKj.js.map +1 -0
  27. package/dist/index.cjs +137 -10518
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +1 -2
  30. package/dist/index.js +137 -10477
  31. package/dist/index.js.map +1 -1
  32. package/dist/{json-parser-COdZ0fqY.cjs → json-parser-BA0mUgMF.cjs} +3 -3
  33. package/dist/json-parser-BA0mUgMF.cjs.map +1 -0
  34. package/dist/{json-parser-B3dnQmCC.js → json-parser-BFM-SnBR.js} +3 -3
  35. package/dist/json-parser-BFM-SnBR.js.map +1 -0
  36. package/dist/knex-DDPXR-sQ.js +218 -0
  37. package/dist/knex-DDPXR-sQ.js.map +1 -0
  38. package/dist/knex-DghF-jjm.cjs +240 -0
  39. package/dist/knex-DghF-jjm.cjs.map +1 -0
  40. package/dist/level-BU87Jbus.js +184 -0
  41. package/dist/level-BU87Jbus.js.map +1 -0
  42. package/dist/level-DNFl2n-m.cjs +184 -0
  43. package/dist/level-DNFl2n-m.cjs.map +1 -0
  44. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +54 -28
  45. package/dist/plugins/application/asyncapi/plugin.d.ts +1 -0
  46. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +22 -11
  47. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +3 -1
  48. package/dist/plugins/application/dashboard/metrics-collector.d.ts +5 -3
  49. package/dist/plugins/application/dashboard/plugin.d.ts +36 -3
  50. package/dist/plugins/application/dashboard/static/requests.js +517 -53
  51. package/dist/plugins/application/dashboard/static/tabs.js +2 -2
  52. package/dist/plugins/application/error-view/index.d.ts +25 -0
  53. package/dist/plugins/application/error-view/reason-phrases.d.ts +1 -0
  54. package/dist/plugins/application/openapi/analyzer.d.ts +3 -1
  55. package/dist/plugins/application/openapi/analyzer.impl.d.ts +4 -2
  56. package/dist/router.d.ts +56 -21
  57. package/dist/shokupan.d.ts +25 -11
  58. package/dist/sqlite-CLrcTkti.js +180 -0
  59. package/dist/sqlite-CLrcTkti.js.map +1 -0
  60. package/dist/sqlite-n7FQ6Ja6.cjs +180 -0
  61. package/dist/sqlite-n7FQ6Ja6.cjs.map +1 -0
  62. package/dist/surreal-6QONU6xa.cjs +210 -0
  63. package/dist/surreal-6QONU6xa.cjs.map +1 -0
  64. package/dist/surreal-w7DeGVI-.js +188 -0
  65. package/dist/surreal-w7DeGVI-.js.map +1 -0
  66. package/dist/util/adapter/datastore/knex.d.ts +29 -0
  67. package/dist/util/adapter/datastore/level.d.ts +26 -0
  68. package/dist/util/adapter/datastore/sqlite.d.ts +24 -0
  69. package/dist/util/adapter/datastore/surreal.d.ts +29 -0
  70. package/dist/util/adapter/datastore.d.ts +59 -0
  71. package/dist/util/adapter/h3.d.ts +8 -0
  72. package/dist/util/adapter/index.d.ts +1 -0
  73. package/dist/util/ast-analyzer-worker.d.ts +77 -0
  74. package/dist/util/ast-worker-thread.d.ts +1 -0
  75. package/dist/util/cookie-parser.d.ts +6 -0
  76. package/dist/util/env-loader.d.ts +7 -0
  77. package/dist/util/html.d.ts +15 -0
  78. package/dist/util/ide.d.ts +9 -0
  79. package/dist/util/logger.d.ts +25 -0
  80. package/dist/util/query-string.d.ts +8 -0
  81. package/dist/util/response-transformer.d.ts +87 -0
  82. package/dist/util/symbol.d.ts +1 -0
  83. package/dist/util/types.d.ts +116 -42
  84. package/dist/websocket.d.ts +163 -0
  85. package/package.json +27 -1
  86. package/dist/analyzer-B0fMzeIo.js.map +0 -1
  87. package/dist/analyzer-BOtveWL-.cjs.map +0 -1
  88. package/dist/analyzer.impl-CUDO6vpn.cjs.map +0 -1
  89. package/dist/analyzer.impl-DmHe92Oi.js.map +0 -1
  90. package/dist/json-parser-B3dnQmCC.js.map +0 -1
  91. package/dist/json-parser-COdZ0fqY.cjs.map +0 -1
  92. package/dist/plugins/application/error-view/views/error.d.ts +0 -2
  93. package/dist/plugins/application/error-view/views/status.d.ts +0 -2
  94. package/dist/util/decorators.d.ts +0 -134
  95. package/dist/util/di.d.ts +0 -13
  96. /package/dist/{util → decorators/util}/metadata.d.ts +0 -0
  97. /package/dist/{util → decorators/util}/stack.d.ts +0 -0
@@ -28,6 +28,9 @@ let maxRequestTime = 0;
28
28
  function initRequests() {
29
29
  console.log('[requests.js] Initializing...');
30
30
 
31
+ if (window.updateRequestsList) console.log('[requests.js] updateRequestsList is already defined!');
32
+ else console.log('[requests.js] Defining updateRequestsList...');
33
+
31
34
  // Initialize Filter Listeners
32
35
  const txtFilter = document.getElementById('network-filter-text');
33
36
  const typeFilter = document.getElementById('network-filter-type');
@@ -164,7 +167,7 @@ function initRequests() {
164
167
  } catch (e) { }
165
168
 
166
169
  return `<div style="display: flex; flex-direction: column; line-height: 1.2;">
167
- <span style="color: var(--text-secondary);">${name}</span>
170
+ <span style="color: var(--text-secondary);">${escapeHtml(name)}</span>
168
171
  </div>`;
169
172
  },
170
173
  headerContextMenu: headerMenu
@@ -198,8 +201,8 @@ function initRequests() {
198
201
  formatter: function (cell) {
199
202
  const row = cell.getData();
200
203
  // 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 || '-';
204
+ if (row.protocol && row.protocol !== 'http' && row.protocol !== 'https') return escapeHtml(row.protocol);
205
+ return escapeHtml(row.scheme || row.protocol || '-');
203
206
  },
204
207
  headerContextMenu: headerMenu
205
208
  },
@@ -238,7 +241,7 @@ function initRequests() {
238
241
  if (r.type === 'fetch') return 'fetch';
239
242
  if (r.type === 'xhr') return 'xhr';
240
243
  if (r.type === 'ws') return 'ws';
241
- return r.contentType || 'document';
244
+ return escapeHtml(r.contentType || 'document');
242
245
  },
243
246
  headerContextMenu: headerMenu
244
247
  },
@@ -327,44 +330,7 @@ function initRequests() {
327
330
  label: "Replay Request",
328
331
  action: function (e, row) {
329
332
  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));
333
+ openReplayModal(data);
368
334
  }
369
335
  },
370
336
  {
@@ -674,7 +640,12 @@ function updateTimestamps(requests) {
674
640
 
675
641
  // Global handler for Client.js
676
642
  window.updateRequestsList = function (newRequests) {
677
- if (!window.requestsTable || !newRequests || !newRequests.length) return;
643
+ console.log('[requests.js] updateRequestsList called with', newRequests ? newRequests.length : 0, 'items');
644
+ if (!window.requestsTable) {
645
+ console.warn('[requests.js] requestsTable is missing!');
646
+ return;
647
+ }
648
+ if (!newRequests || !newRequests.length) return;
678
649
 
679
650
  // Update Timestamps
680
651
  const changed = updateTimestamps(newRequests);
@@ -736,6 +707,11 @@ function showRequestDetails(request) {
736
707
  // { id: 'security', label: 'Security' } // Enable if we have data
737
708
  ];
738
709
 
710
+ // Add Middleware tab if we have handler stack data with state changes
711
+ if (request.handlerStack && request.handlerStack.some(h => h.stateChanges && Object.keys(h.stateChanges).length > 0)) {
712
+ tabs.splice(5, 0, { id: 'middleware', label: 'Middleware' });
713
+ }
714
+
739
715
  if (request.scheme === 'https' || request.scheme === 'wss') {
740
716
  tabs.push({ id: 'security', label: 'Security' });
741
717
  }
@@ -809,6 +785,8 @@ function renderTabContent(tabId, request) {
809
785
  return renderResponseTab(request);
810
786
  case 'timings':
811
787
  return renderTimingsTab(request);
788
+ case 'middleware':
789
+ return renderMiddlewareTab(request);
812
790
  case 'security':
813
791
  return renderSecurityTab(request);
814
792
  default:
@@ -816,6 +794,17 @@ function renderTabContent(tabId, request) {
816
794
  }
817
795
  }
818
796
 
797
+ // Utility
798
+ function escapeHtml(text) {
799
+ if (!text) return '';
800
+ return String(text)
801
+ .replace(/&/g, "&amp;")
802
+ .replace(/</g, "&lt;")
803
+ .replace(/>/g, "&gt;")
804
+ .replace(/"/g, "&quot;")
805
+ .replace(/'/g, "&#039;");
806
+ }
807
+
819
808
  function renderNameValueTable(items, emptyMessage = 'No items found') {
820
809
  if (!items || !items.length) return `<div style="padding: 8px; color: var(--text-secondary);">${emptyMessage}</div>`;
821
810
  return `
@@ -829,8 +818,8 @@ function renderNameValueTable(items, emptyMessage = 'No items found') {
829
818
  <tbody>
830
819
  ${items.map(c => `
831
820
  <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>
821
+ <td style="padding: 4px 8px; font-weight: 500;">${escapeHtml(c.name)}</td>
822
+ <td style="padding: 4px 8px; word-break: break-all;">${escapeHtml(c.value)}</td>
834
823
  </tr>
835
824
  `).join('')}
836
825
  </tbody>
@@ -843,13 +832,13 @@ function renderHeadersTab(request) {
843
832
  if (!headers || Object.keys(headers).length === 0) return '';
844
833
  const rows = Object.entries(headers).map(([k, v]) => `
845
834
  <tr>
846
- <td style="font-weight: 500; color: var(--text-flavor); padding: 4px 8px; vertical-align: top;">${k}:</td>
847
- <td style="word-break: break-all; padding: 4px 8px;">${v}</td>
835
+ <td style="font-weight: 500; color: var(--text-flavor); padding: 4px 8px; vertical-align: top;">${escapeHtml(k)}:</td>
836
+ <td style="word-break: break-all; padding: 4px 8px;">${escapeHtml(v)}</td>
848
837
  </tr>
849
838
  `).join('');
850
839
  return `
851
840
  <details open style="margin-bottom: 1rem;">
852
- <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">${title}</summary>
841
+ <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">${escapeHtml(title)}</summary>
853
842
  <table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
854
843
  ${rows}
855
844
  </table>
@@ -862,11 +851,11 @@ function renderHeadersTab(request) {
862
851
  <details open style="margin-bottom: 1rem;">
863
852
  <summary style="font-weight: bold; padding: 4px 0; cursor: pointer; color: var(--text-primary);">General</summary>
864
853
  <div style="display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 0.9em; padding-left: 8px;">
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>
854
+ <div style="color: var(--text-flavor);">Request URL:</div><div style="word-break: break-all;">${escapeHtml(request.url)}</div>
855
+ <div style="color: var(--text-flavor);">Request Method:</div><div>${escapeHtml(request.method)}</div>
856
+ <div style="color: var(--text-flavor);">Status Code:</div><div>${escapeHtml(request.status)}</div>
857
+ <div style="color: var(--text-flavor);">Remote Address:</div><div>${escapeHtml(request.remoteIP || '-')}</div>
858
+ <div style="color: var(--text-flavor);">Referrer Policy:</div><div>${escapeHtml(request.requestHeaders?.['referrer-policy'] || 'strict-origin-when-cross-origin')}</div>
870
859
  </div>
871
860
  </details>
872
861
  ${formatHeaderSection('Response Headers', request.responseHeaders)}
@@ -998,6 +987,116 @@ function renderSecurityTab(request) {
998
987
  `;
999
988
  }
1000
989
 
990
+ function renderMiddlewareTab(request) {
991
+ if (!request.handlerStack || request.handlerStack.length === 0) {
992
+ return `
993
+ <div style="padding: 2rem; text-align: center; color: var(--text-secondary);">
994
+ No middleware tracking data available.
995
+ <br><br>
996
+ Enable middleware tracking by setting <code style="background: var(--bg-primary); padding: 2px 6px; border-radius: 3px;">enableMiddlewareTracking: true</code> in your application config.
997
+ </div>
998
+ `;
999
+ }
1000
+
1001
+ const totalDuration = request.duration || 1;
1002
+ const formatValue = (val) => {
1003
+ if (val === undefined) return '<span style="color: var(--text-secondary); font-style: italic;">undefined</span>';
1004
+ if (val === null) return '<span style="color: var(--text-secondary); font-style: italic;">null</span>';
1005
+ if (typeof val === 'string') return `"<span style="color: var(--color-success);">${escapeHtml(val)}</span>"`;
1006
+ if (typeof val === 'number') return `<span style="color: var(--color-info);">${val}</span>`;
1007
+ if (typeof val === 'boolean') return `<span style="color: var(--color-warning);">${val}</span>`;
1008
+ if (typeof val === 'object') {
1009
+ try {
1010
+ return `<span style="color: var(--text-secondary);">${escapeHtml(JSON.stringify(val, null, 2))}</span>`;
1011
+ } catch (e) {
1012
+ return `<span style="color: var(--text-secondary);">[Object]</span>`;
1013
+ }
1014
+ }
1015
+ return escapeHtml(String(val));
1016
+ };
1017
+
1018
+ let html = '<div style="padding: 1rem;">';
1019
+ html += '<div style="margin-bottom: 1rem;">';
1020
+ html += '<div style="font-size: 0.9em; color: var(--text-secondary); margin-bottom: 0.5rem;">';
1021
+ html += 'This tab shows state mutations made by each middleware handler during request processing.';
1022
+ html += '</div>';
1023
+ html += '</div>';
1024
+
1025
+ html += '<div style="display: flex; flex-direction: column; gap: 12px;">';
1026
+
1027
+ request.handlerStack.forEach((item, index) => {
1028
+ const duration = item.duration > 0 ? item.duration : 0.01;
1029
+ const percent = Math.min(100, Math.max(1, (duration / totalDuration) * 100));
1030
+ const isSlow = percent > 15;
1031
+ const hasStateChanges = item.stateChanges && Object.keys(item.stateChanges).length > 0;
1032
+
1033
+ const detailsId = `middleware-${index}`;
1034
+
1035
+ html += `
1036
+ <details ${hasStateChanges ? 'open' : ''} id="${detailsId}" style="border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; background: var(--bg-primary);">
1037
+ <summary style="padding: 12px; cursor: pointer; background: var(--bg-secondary); border-left: 3px solid ${hasStateChanges ? 'var(--primary-color, #3b82f6)' : 'var(--border-color)'}; display: flex; justify-content: space-between; align-items: center;">
1038
+ <div style="flex: 1;">
1039
+ <div style="font-weight: 500; margin-bottom: 4px;">
1040
+ ${hasStateChanges ? '🔹 ' : '⚪ '}${escapeHtml(item.name)}
1041
+ </div>
1042
+ <div style="font-size: 0.85em; color: var(--text-secondary); font-family: monospace;">
1043
+ ${escapeHtml(item.file)}:${item.line}
1044
+ </div>
1045
+ </div>
1046
+ <div style="text-align: right;">
1047
+ <div style="font-family: monospace; font-size: 0.9em; color: ${isSlow ? 'var(--color-warning)' : 'var(--text-secondary)'};">
1048
+ ${printDuration(duration)}
1049
+ </div>
1050
+ ${hasStateChanges ? `<div style="font-size: 0.8em; color: var(--primary-color, #3b82f6); margin-top: 2px;">${Object.keys(item.stateChanges).length} change${Object.keys(item.stateChanges).length === 1 ? '' : 's'}</div>` : '<div style="font-size: 0.8em; color: var(--text-secondary); margin-top: 2px;">No changes</div>'}
1051
+ </div>
1052
+ </summary>
1053
+
1054
+ <div style="padding: 12px; border-top: 1px solid var(--border-color);">`;
1055
+
1056
+ if (hasStateChanges) {
1057
+ html += '<div style="margin-bottom: 8px; font-weight: 500; color: var(--text-primary);">State Changes:</div>';
1058
+ html += '<table style="width: 100%; border-collapse: collapse; font-size: 0.9em; font-family: monospace;">';
1059
+
1060
+ Object.entries(item.stateChanges).forEach(([key, value]) => {
1061
+ html += `
1062
+ <tr style="border-bottom: 1px solid var(--border-color-dim, #33333333);">
1063
+ <td style="padding: 6px 8px; color: var(--text-flavor); font-weight: 500; vertical-align: top; width: 30%;">
1064
+ ${escapeHtml(key)}
1065
+ </td>
1066
+ <td style="padding: 6px 8px; color: var(--text-secondary); vertical-align: top; width: 10%; text-align: center;">
1067
+
1068
+ </td>
1069
+ <td style="padding: 6px 8px; word-break: break-all; vertical-align: top;">
1070
+ ${formatValue(value)}
1071
+ </td>
1072
+ </tr>
1073
+ `;
1074
+ });
1075
+
1076
+ html += '</table>';
1077
+ } else {
1078
+ html += '<div style="color: var(--text-secondary); font-style: italic; text-align: center; padding: 1rem;">';
1079
+ html += 'This middleware did not modify ctx.state';
1080
+ html += '</div>';
1081
+ }
1082
+
1083
+ html += '<div style="margin-top: 12px;">';
1084
+ html += '<div style="font-size: 0.8em; color: var(--text-secondary); margin-bottom: 4px;">Execution Time</div>';
1085
+ html += '<div style="height: 6px; background: var(--bg-secondary); border-radius: 3px; overflow: hidden;">';
1086
+ html += `<div style="height: 100%; width: ${percent}%; background: ${isSlow ? 'var(--color-warning)' : 'var(--color-success)'}; transition: width 0.3s ease;"></div>`;
1087
+ html += '</div>';
1088
+ html += '</div>';
1089
+
1090
+ html += '</div>';
1091
+ html += '</details>';
1092
+ });
1093
+
1094
+ html += '</div>';
1095
+ html += '</div>';
1096
+
1097
+ return html;
1098
+ }
1099
+
1001
1100
  function closeRequestDetails() {
1002
1101
  document.getElementById('request-details-container').style.display = 'none';
1003
1102
  if (window.requestsTable) window.requestsTable.redraw();
@@ -1212,3 +1311,368 @@ function formatBytes(bytes, decimals = 2) {
1212
1311
  }
1213
1312
 
1214
1313
 
1314
+
1315
+ // --- Replay Modal Implementation ---
1316
+
1317
+ function injectReplayStyles() {
1318
+ if (document.getElementById('replay-modal-styles')) return;
1319
+ const style = document.createElement('style');
1320
+ style.id = 'replay-modal-styles';
1321
+ style.textContent = `
1322
+ #replay-modal-overlay {
1323
+ position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000;
1324
+ display: flex; align-items: center; justify-content: center;
1325
+ backdrop-filter: blur(2px);
1326
+ }
1327
+ #replay-modal {
1328
+ background: var(--bg-secondary); width: 800px; max-width: 95vw; height: 80vh;
1329
+ border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
1330
+ display: flex; flex-direction: column; border: 1px solid var(--border-color);
1331
+ }
1332
+ .replay-header {
1333
+ padding: 1rem; border-bottom: 1px solid var(--border-color);
1334
+ display: flex; justify-content: space-between; align-items: center;
1335
+ font-weight: 600; font-size: 1.1rem;
1336
+ }
1337
+ .replay-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
1338
+ .replay-toolbar {
1339
+ padding: 1rem; display: flex; gap: 0.5rem; border-bottom: 1px solid var(--border-color);
1340
+ background: var(--bg-primary);
1341
+ }
1342
+ .replay-input {
1343
+ flex: 1; padding: 0.5rem; border-radius: 4px; border: 1px solid var(--border-color);
1344
+ background: var(--bg-secondary); color: var(--text-primary);
1345
+ }
1346
+ .replay-method {
1347
+ padding: 0.5rem; border-radius: 4px; border: 1px solid var(--border-color);
1348
+ background: var(--bg-secondary); color: var(--text-primary); font-weight: bold;
1349
+ }
1350
+ .replay-btn {
1351
+ padding: 0.5rem 1rem; border-radius: 4px; border: none; cursor: pointer;
1352
+ font-weight: 500; display: flex; align-items: center; gap: 0.5rem;
1353
+ }
1354
+ .btn-primary { background: var(--primary-color, #3b82f6); color: white; }
1355
+ .btn-secondary { background: var(--bg-primary, #e5e7eb); color: var(--text-primary); }
1356
+ .dark .btn-secondary { background: #374151; }
1357
+
1358
+ .replay-tabs { display: flex; border-bottom: 1px solid var(--border-color); background: var(--bg-primary); }
1359
+ .replay-tab {
1360
+ padding: 0.75rem 1rem; cursor: pointer; border-bottom: 2px solid transparent;
1361
+ color: var(--text-secondary);
1362
+ }
1363
+ .replay-tab.active {
1364
+ border-color: var(--primary-color, #3b82f6); color: var(--text-primary);
1365
+ }
1366
+
1367
+ .replay-content { flex: 1; overflow-y: auto; padding: 1rem; position: relative; }
1368
+ .code-editor {
1369
+ width: 100%; height: 100%; font-family: monospace; border: none; resize: none;
1370
+ background: transparent; color: var(--text-primary); outline: none;
1371
+ }
1372
+
1373
+ .kv-editor-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }
1374
+ .kv-key, .kv-val { flex: 1; padding: 0.4rem; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); border-radius: 4px; }
1375
+ .kv-remove { padding: 0.4rem; cursor: pointer; color: #ef4444; }
1376
+
1377
+ .response-status-badge {
1378
+ padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.85rem; font-weight: bold;
1379
+ }
1380
+ .status-2xx { background: rgba(16, 185, 129, 0.2); color: #10b981; }
1381
+ .status-4xx { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
1382
+ .status-5xx { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
1383
+ `;
1384
+ document.head.appendChild(style);
1385
+ }
1386
+
1387
+ let currentReplayState = {
1388
+ method: 'GET',
1389
+ url: '',
1390
+ headers: [],
1391
+ body: '',
1392
+ activeTab: 'body'
1393
+ };
1394
+
1395
+ function openReplayModal(request) {
1396
+ injectReplayStyles();
1397
+
1398
+ // Initialize State
1399
+ currentReplayState = {
1400
+ method: request.method || 'GET',
1401
+ url: request.url || '',
1402
+ headers: Object.entries(request.requestHeaders || {}).map(([k, v]) => ({ key: k, value: v })),
1403
+ body: typeof (request.requestBody) === 'string' ? request.requestBody : (request.requestBody ? JSON.stringify(request.requestBody || {}, null, 2) : ''),
1404
+ direction: request.direction || 'outbound',
1405
+ activeTab: 'body',
1406
+ response: null
1407
+ };
1408
+
1409
+ renderReplayModal();
1410
+ }
1411
+
1412
+ function closeReplayModal() {
1413
+ const el = document.getElementById('replay-modal-overlay');
1414
+ if (el) el.remove();
1415
+ }
1416
+
1417
+ function renderReplayModal() {
1418
+ let el = document.getElementById('replay-modal-overlay');
1419
+ if (!el) {
1420
+ el = document.createElement('div');
1421
+ el.id = 'replay-modal-overlay';
1422
+ document.body.appendChild(el);
1423
+
1424
+ // Close on backdrop click
1425
+ el.addEventListener('click', (e) => {
1426
+ if (e.target === el) closeReplayModal();
1427
+ });
1428
+ }
1429
+
1430
+ const { method, url, headers, body, activeTab, response } = currentReplayState;
1431
+ const isResponse = activeTab === 'response';
1432
+
1433
+ const tabs = ['body', 'headers', 'params', 'response'];
1434
+
1435
+ el.innerHTML = `
1436
+ <div id="replay-modal">
1437
+ <div class="replay-header">
1438
+ <span>Replay Request</span>
1439
+ <div style="display:flex; gap: 0.5rem">
1440
+ <button class="replay-btn btn-secondary" onclick="document.getElementById('replay-import-file').click()">
1441
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1442
+ Import
1443
+ </button>
1444
+ <input type="file" id="replay-import-file" style="display:none" onchange="handleReplayImport(this)">
1445
+ <button class="replay-btn btn-secondary" onclick="copyReplayCurl()">
1446
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1447
+ Copy Curl
1448
+ </button>
1449
+ <button class="replay-btn" style="background: transparent; color: var(--text-secondary)" onclick="closeReplayModal()">✕</button>
1450
+ </div>
1451
+ </div>
1452
+
1453
+ <div class="replay-toolbar">
1454
+ <select class="replay-method" onchange="updateReplayState('method', this.value)">
1455
+ <option value="GET" ${method === 'GET' ? 'selected' : ''}>GET</option>
1456
+ <option value="POST" ${method === 'POST' ? 'selected' : ''}>POST</option>
1457
+ <option value="PUT" ${method === 'PUT' ? 'selected' : ''}>PUT</option>
1458
+ <option value="DELETE" ${method === 'DELETE' ? 'selected' : ''}>DELETE</option>
1459
+ <option value="PATCH" ${method === 'PATCH' ? 'selected' : ''}>PATCH</option>
1460
+ </select>
1461
+ <input class="replay-input" value="${escapeHtml(url)}" oninput="updateReplayState('url', this.value)" placeholder="https://api.example.com/v1/...">
1462
+ <button class="replay-btn btn-primary" onclick="executeReplay()">
1463
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1464
+ Send
1465
+ </button>
1466
+ </div>
1467
+
1468
+ <div class="replay-tabs">
1469
+ <div class="replay-tab ${activeTab === 'body' ? 'active' : ''}" onclick="updateReplayState('activeTab', 'body')">Body</div>
1470
+ <div class="replay-tab ${activeTab === 'headers' ? 'active' : ''}" onclick="updateReplayState('activeTab', 'headers')">Headers</div>
1471
+ <div class="replay-tab ${activeTab === 'response' ? 'active' : ''}" onclick="updateReplayState('activeTab', 'response')">
1472
+ Response ${response ? `<span style="font-size: 0.8em; opacity: 0.8">(${response.status})</span>` : ''}
1473
+ </div>
1474
+ </div>
1475
+
1476
+ <div class="replay-content">
1477
+ ${activeTab === 'body' ? `
1478
+ <textarea class="code-editor" spellcheck="false" oninput="updateReplayState('body', this.value)">${escapeHtml(body)}</textarea>
1479
+ ` : ''}
1480
+
1481
+ ${activeTab === 'headers' ? `
1482
+ <div id="replay-headers-list">
1483
+ ${headers.map((h, i) => `
1484
+ <div class="kv-editor-row">
1485
+ <input class="kv-key" value="${escapeHtml(h.key)}" oninput="updateReplayHeader(${i}, 'key', this.value)" placeholder="Key">
1486
+ <input class="kv-val" value="${escapeHtml(h.value)}" oninput="updateReplayHeader(${i}, 'value', this.value)" placeholder="Value">
1487
+ <div class="kv-remove" onclick="removeReplayHeader(${i})">✕</div>
1488
+ </div>
1489
+ `).join('')}
1490
+ </div>
1491
+ <button class="replay-btn btn-secondary" style="margin-top: 1rem" onclick="addReplayHeader()">+ Add Header</button>
1492
+ ` : ''}
1493
+
1494
+ ${activeTab === 'response' ? renderReplayResponsePlaceholder(response) : ''}
1495
+ </div>
1496
+ </div>
1497
+ `;
1498
+
1499
+ if (activeTab === 'response' && response) {
1500
+ setTimeout(() => {
1501
+ const el = document.getElementById('replay-response-editor');
1502
+ if (el) {
1503
+ let content = response.body || '';
1504
+ if (typeof content === 'object') content = JSON.stringify(content, null, 2);
1505
+
1506
+ let lang = 'json'; // default
1507
+ // try to sniff
1508
+ if (typeof content === 'string' && !content.trim().startsWith('{') && !content.trim().startsWith('[')) {
1509
+ lang = 'plaintext';
1510
+ }
1511
+
1512
+ renderMonacoEditor(el, content, lang, true);
1513
+ }
1514
+ }, 0);
1515
+ }
1516
+ }
1517
+
1518
+ function renderReplayResponsePlaceholder(response) {
1519
+ if (!response) return `<div style="color: var(--text-secondary); text-align: center; margin-top: 2rem;">No response yet. Click Send to replay.</div>`;
1520
+
1521
+ let colorClass = response.status >= 500 ? 'status-5xx' : response.status >= 400 ? 'status-4xx' : 'status-2xx';
1522
+
1523
+ return `
1524
+ <div style="margin-bottom: 1rem; display: flex; gap: 1rem; align-items: center;">
1525
+ <span class="response-status-badge ${colorClass}">${response.status} ${response.statusText || ''}</span>
1526
+ <span style="color: var(--text-secondary)">${formatBytes(response.size || 0)}</span>
1527
+ <span style="color: var(--text-secondary)">${response.duration || 0}ms</span>
1528
+ <div style="flex:1"></div>
1529
+ <button class="replay-btn btn-secondary" onclick="copyToClipboard(currentReplayState.responseBodyStr)">Copy</button>
1530
+ </div>
1531
+ <div style="border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; display: flex; flex-direction: column; height: calc(100% - 40px)">
1532
+ <div id="replay-response-editor" style="flex: 1;"></div>
1533
+ </div>
1534
+ `;
1535
+ }
1536
+
1537
+ function updateReplayState(key, value) {
1538
+ currentReplayState[key] = value;
1539
+ if (key === 'activeTab') renderReplayModal(); // Re-render for tab switch
1540
+ }
1541
+
1542
+ function updateReplayHeader(index, field, value) {
1543
+ currentReplayState.headers[index][field] = value;
1544
+ }
1545
+
1546
+ function addReplayHeader() {
1547
+ currentReplayState.headers.push({ key: '', value: '' });
1548
+ renderReplayModal();
1549
+ }
1550
+
1551
+ function removeReplayHeader(index) {
1552
+ currentReplayState.headers.splice(index, 1);
1553
+ renderReplayModal();
1554
+ }
1555
+
1556
+ function executeReplay() {
1557
+ const { method, url, headers, body, direction } = currentReplayState;
1558
+
1559
+ // Construct headers object
1560
+ const headersObj = {};
1561
+ headers.forEach(h => {
1562
+ if (h.key) headersObj[h.key] = h.value;
1563
+ });
1564
+
1565
+ // Parse body if JSON
1566
+ let bodyData = body;
1567
+ try {
1568
+ bodyData = JSON.parse(body);
1569
+ } catch (e) {
1570
+ // Keep as string
1571
+ }
1572
+
1573
+ // Using dashboard replay endpoint
1574
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
1575
+
1576
+ // Show loading?
1577
+ const btn = document.querySelector('.replay-toolbar .btn-primary');
1578
+ if (btn) btn.innerText = 'Sending...';
1579
+
1580
+ console.log('[Dashboard] Replaying request:', { method, url, direction });
1581
+
1582
+ fetch(basePath + '/replay', {
1583
+ method: 'POST',
1584
+ headers: { 'Content-Type': 'application/json' },
1585
+ body: JSON.stringify({
1586
+ method,
1587
+ url,
1588
+ headers: headersObj,
1589
+ body: bodyData,
1590
+ direction: direction || 'outbound'
1591
+ })
1592
+ })
1593
+ .then(res => res.json())
1594
+ .then(result => {
1595
+ console.log('[Dashboard] Replay result:', result);
1596
+ if (result.error) {
1597
+ alert("Error: " + result.error);
1598
+ } else {
1599
+ let bodyStr = result.data;
1600
+ if (typeof bodyStr === 'object') bodyStr = JSON.stringify(bodyStr, null, 2);
1601
+
1602
+ currentReplayState.response = {
1603
+ status: result.status,
1604
+ headers: result.headers,
1605
+ body: result.data,
1606
+ duration: result.duration,
1607
+ size: bodyStr ? bodyStr.length : 0
1608
+ };
1609
+ currentReplayState.responseBodyStr = bodyStr; // Store for copy
1610
+ currentReplayState.activeTab = 'response';
1611
+ renderReplayModal();
1612
+ }
1613
+ })
1614
+ .catch(err => {
1615
+ console.error('[Dashboard] Replay failed:', err);
1616
+ alert("Replay failed: " + err);
1617
+ })
1618
+ .finally(() => {
1619
+ if (btn) btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> Send`;
1620
+ });
1621
+ }
1622
+
1623
+ function copyReplayCurl() {
1624
+ // Generate Curl
1625
+ const { method, url, headers, body } = currentReplayState;
1626
+ let cmd = `curl -X ${method} "${url}"`;
1627
+ headers.forEach(h => {
1628
+ if (h.key) cmd += ` \\\n -H "${h.key}: ${h.value}"`;
1629
+ });
1630
+ if (body) {
1631
+ // Escape body for shell
1632
+ const escaped = body.replace(/"/g, '\\"');
1633
+ cmd += ` \\\n -d "${escaped}"`;
1634
+ }
1635
+
1636
+ copyToClipboard(cmd);
1637
+ }
1638
+
1639
+ function handleReplayImport(input) {
1640
+ const file = input.files[0];
1641
+ if (!file) return;
1642
+
1643
+ const reader = new FileReader();
1644
+ reader.onload = (e) => {
1645
+ try {
1646
+ const data = JSON.parse(e.target.result);
1647
+ // Try to map HAR or simple JSON
1648
+ if (data.log && data.log.entries) {
1649
+ // HAR
1650
+ const entry = data.log.entries[0];
1651
+ if (entry && entry.request) {
1652
+ currentReplayState.method = entry.request.method;
1653
+ currentReplayState.url = entry.request.url;
1654
+ currentReplayState.headers = entry.request.headers.map(h => ({ key: h.name, value: h.value }));
1655
+ if (entry.request.postData && entry.request.postData.text) {
1656
+ currentReplayState.body = entry.request.postData.text;
1657
+ }
1658
+ }
1659
+ } else {
1660
+ // Simple format
1661
+ currentReplayState.method = data.method || 'GET';
1662
+ currentReplayState.url = data.url || '';
1663
+ if (data.headers) {
1664
+ if (Array.isArray(data.headers)) currentReplayState.headers = data.headers;
1665
+ else currentReplayState.headers = Object.entries(data.headers).map(([k, v]) => ({ key: k, value: v }));
1666
+ }
1667
+ if (data.body) {
1668
+ currentReplayState.body = typeof data.body === 'string' ? data.body : JSON.stringify(data.body, null, 2);
1669
+ }
1670
+ }
1671
+ renderReplayModal();
1672
+ } catch (err) {
1673
+ alert("Failed to parse file: " + err.message);
1674
+ }
1675
+ };
1676
+ reader.readAsText(file);
1677
+ input.value = ''; // Reset
1678
+ }