@dangao/bun-server 1.9.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +79 -6
  2. package/dist/cache/cache-module.d.ts +6 -0
  3. package/dist/cache/cache-module.d.ts.map +1 -1
  4. package/dist/client/generator.d.ts +16 -0
  5. package/dist/client/generator.d.ts.map +1 -0
  6. package/dist/client/index.d.ts +4 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/runtime.d.ts +15 -0
  9. package/dist/client/runtime.d.ts.map +1 -0
  10. package/dist/client/types.d.ts +36 -0
  11. package/dist/client/types.d.ts.map +1 -0
  12. package/dist/config/config-module.d.ts +7 -0
  13. package/dist/config/config-module.d.ts.map +1 -1
  14. package/dist/config/index.d.ts +1 -1
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/service.d.ts +13 -0
  17. package/dist/config/service.d.ts.map +1 -1
  18. package/dist/config/types.d.ts +10 -0
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/core/application.d.ts +7 -0
  21. package/dist/core/application.d.ts.map +1 -1
  22. package/dist/core/apply-decorators.d.ts +6 -0
  23. package/dist/core/apply-decorators.d.ts.map +1 -0
  24. package/dist/core/cluster.d.ts +47 -0
  25. package/dist/core/cluster.d.ts.map +1 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/server.d.ts +8 -0
  29. package/dist/core/server.d.ts.map +1 -1
  30. package/dist/dashboard/controller.d.ts +55 -0
  31. package/dist/dashboard/controller.d.ts.map +1 -0
  32. package/dist/dashboard/dashboard-extension.d.ts +20 -0
  33. package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
  34. package/dist/dashboard/dashboard-module.d.ts +13 -0
  35. package/dist/dashboard/dashboard-module.d.ts.map +1 -0
  36. package/dist/dashboard/index.d.ts +4 -0
  37. package/dist/dashboard/index.d.ts.map +1 -0
  38. package/dist/dashboard/types.d.ts +16 -0
  39. package/dist/dashboard/types.d.ts.map +1 -0
  40. package/dist/dashboard/ui.d.ts +7 -0
  41. package/dist/dashboard/ui.d.ts.map +1 -0
  42. package/dist/database/database-module.d.ts +7 -0
  43. package/dist/database/database-module.d.ts.map +1 -1
  44. package/dist/debug/debug-module.d.ts +13 -0
  45. package/dist/debug/debug-module.d.ts.map +1 -0
  46. package/dist/debug/debug-ui-middleware.d.ts +8 -0
  47. package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
  48. package/dist/debug/index.d.ts +5 -0
  49. package/dist/debug/index.d.ts.map +1 -0
  50. package/dist/debug/middleware.d.ts +12 -0
  51. package/dist/debug/middleware.d.ts.map +1 -0
  52. package/dist/debug/recorder.d.ts +61 -0
  53. package/dist/debug/recorder.d.ts.map +1 -0
  54. package/dist/debug/types.d.ts +48 -0
  55. package/dist/debug/types.d.ts.map +1 -0
  56. package/dist/debug/ui.d.ts +6 -0
  57. package/dist/debug/ui.d.ts.map +1 -0
  58. package/dist/di/async-module.d.ts +49 -0
  59. package/dist/di/async-module.d.ts.map +1 -0
  60. package/dist/di/lifecycle.d.ts +49 -0
  61. package/dist/di/lifecycle.d.ts.map +1 -0
  62. package/dist/di/module-registry.d.ts +24 -0
  63. package/dist/di/module-registry.d.ts.map +1 -1
  64. package/dist/index.d.ts +9 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1887 -35
  67. package/dist/router/route.d.ts +5 -7
  68. package/dist/router/route.d.ts.map +1 -1
  69. package/dist/swagger/generator.d.ts +10 -0
  70. package/dist/swagger/generator.d.ts.map +1 -1
  71. package/dist/testing/test-client.d.ts +49 -0
  72. package/dist/testing/test-client.d.ts.map +1 -0
  73. package/dist/testing/testing-module.d.ts +90 -0
  74. package/dist/testing/testing-module.d.ts.map +1 -0
  75. package/dist/websocket/registry.d.ts +1 -6
  76. package/dist/websocket/registry.d.ts.map +1 -1
  77. package/docs/async-module.md +59 -0
  78. package/docs/client-generation.md +100 -0
  79. package/docs/cluster.md +81 -0
  80. package/docs/custom-decorators.md +1 -7
  81. package/docs/dashboard.md +54 -0
  82. package/docs/debug.md +58 -0
  83. package/docs/extensions.md +0 -2
  84. package/docs/guide.md +0 -1
  85. package/docs/lifecycle.md +72 -0
  86. package/docs/testing.md +110 -0
  87. package/docs/zh/async-module.md +98 -0
  88. package/docs/zh/client-generation.md +92 -0
  89. package/docs/zh/cluster.md +74 -0
  90. package/docs/zh/custom-decorators.md +1 -7
  91. package/docs/zh/dashboard.md +69 -0
  92. package/docs/zh/debug.md +81 -0
  93. package/docs/zh/extensions.md +0 -2
  94. package/docs/zh/guide.md +0 -1
  95. package/docs/zh/lifecycle.md +87 -0
  96. package/docs/zh/migration.md +0 -5
  97. package/docs/zh/testing.md +119 -0
  98. package/package.json +4 -4
  99. package/src/cache/cache-module.ts +25 -0
  100. package/src/client/generator.ts +36 -0
  101. package/src/client/index.ts +8 -0
  102. package/src/client/runtime.ts +101 -0
  103. package/src/client/types.ts +38 -0
  104. package/src/config/config-module.ts +44 -4
  105. package/src/config/index.ts +1 -0
  106. package/src/config/service.ts +50 -0
  107. package/src/config/types.ts +12 -0
  108. package/src/core/application.ts +37 -0
  109. package/src/core/apply-decorators.ts +31 -0
  110. package/src/core/cluster.ts +143 -0
  111. package/src/core/index.ts +1 -0
  112. package/src/core/server.ts +14 -1
  113. package/src/dashboard/controller.ts +227 -0
  114. package/src/dashboard/dashboard-extension.ts +26 -0
  115. package/src/dashboard/dashboard-module.ts +38 -0
  116. package/src/dashboard/index.ts +3 -0
  117. package/src/dashboard/types.ts +16 -0
  118. package/src/dashboard/ui.ts +219 -0
  119. package/src/database/database-module.ts +20 -0
  120. package/src/debug/debug-module.ts +70 -0
  121. package/src/debug/debug-ui-middleware.ts +110 -0
  122. package/src/debug/index.ts +9 -0
  123. package/src/debug/middleware.ts +126 -0
  124. package/src/debug/recorder.ts +141 -0
  125. package/src/debug/types.ts +49 -0
  126. package/src/debug/ui.ts +393 -0
  127. package/src/di/async-module.ts +141 -0
  128. package/src/di/lifecycle.ts +117 -0
  129. package/src/di/module-registry.ts +75 -0
  130. package/src/index.ts +35 -0
  131. package/src/router/route.ts +20 -20
  132. package/src/swagger/generator.ts +100 -0
  133. package/src/testing/test-client.ts +112 -0
  134. package/src/testing/testing-module.ts +238 -0
  135. package/src/websocket/registry.ts +3 -16
  136. package/tests/auth/auth-decorators.test.ts +0 -1
  137. package/tests/auth/oauth2-service.test.ts +0 -1
  138. package/tests/cache/cache-decorators-extended.test.ts +0 -1
  139. package/tests/cache/cache-decorators.test.ts +0 -1
  140. package/tests/cache/cache-interceptors.test.ts +0 -1
  141. package/tests/cache/cache-module.test.ts +0 -1
  142. package/tests/cache/cache-service-proxy.test.ts +0 -1
  143. package/tests/client/client-generator.test.ts +142 -0
  144. package/tests/config/config-center-integration.test.ts +0 -1
  145. package/tests/config/config-module-extended.test.ts +0 -1
  146. package/tests/config/config-module.test.ts +0 -1
  147. package/tests/controller/controller.test.ts +0 -1
  148. package/tests/controller/param-binder.test.ts +0 -1
  149. package/tests/controller/path-combination.test.ts +0 -1
  150. package/tests/core/application.test.ts +34 -0
  151. package/tests/core/apply-decorators.test.ts +109 -0
  152. package/tests/core/cluster.test.ts +32 -0
  153. package/tests/dashboard/dashboard-module.test.ts +85 -0
  154. package/tests/database/database-module.test.ts +0 -1
  155. package/tests/database/orm.test.ts +0 -1
  156. package/tests/database/postgres-mysql-integration.test.ts +0 -1
  157. package/tests/database/transaction.test.ts +0 -1
  158. package/tests/debug/debug-module.test.ts +141 -0
  159. package/tests/di/async-module.test.ts +125 -0
  160. package/tests/di/container.test.ts +0 -1
  161. package/tests/di/lifecycle.test.ts +140 -0
  162. package/tests/error/error-handler.test.ts +0 -1
  163. package/tests/events/event-decorators.test.ts +0 -1
  164. package/tests/events/event-listener-scanner.test.ts +0 -1
  165. package/tests/events/event-module.test.ts +0 -1
  166. package/tests/extensions/logger-module.test.ts +0 -1
  167. package/tests/health/health-module.test.ts +0 -1
  168. package/tests/integration/oauth2-e2e.test.ts +0 -1
  169. package/tests/integration/session-e2e.test.ts +0 -1
  170. package/tests/interceptor/base-interceptor.test.ts +0 -1
  171. package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
  172. package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
  173. package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
  174. package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
  175. package/tests/interceptor/interceptor-chain.test.ts +0 -1
  176. package/tests/interceptor/interceptor-integration.test.ts +0 -1
  177. package/tests/interceptor/interceptor-metadata.test.ts +0 -1
  178. package/tests/interceptor/interceptor-registry.test.ts +0 -1
  179. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
  180. package/tests/metrics/metrics-module.test.ts +0 -1
  181. package/tests/microservice/config-center.test.ts +0 -1
  182. package/tests/microservice/service-client-decorators.test.ts +0 -1
  183. package/tests/microservice/service-registry-decorators.test.ts +0 -1
  184. package/tests/microservice/service-registry.test.ts +0 -1
  185. package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
  186. package/tests/middleware/builtin/rate-limit.test.ts +0 -1
  187. package/tests/middleware/middleware-decorators.test.ts +0 -1
  188. package/tests/middleware/middleware-pipeline.test.ts +0 -1
  189. package/tests/middleware/middleware.test.ts +0 -1
  190. package/tests/perf/optimization.test.ts +0 -1
  191. package/tests/queue/queue-decorators.test.ts +0 -1
  192. package/tests/queue/queue-module.test.ts +0 -1
  193. package/tests/queue/queue-service.test.ts +0 -1
  194. package/tests/router/router-decorators.test.ts +0 -1
  195. package/tests/router/router-extended.test.ts +0 -1
  196. package/tests/security/guards/guards-integration.test.ts +0 -1
  197. package/tests/security/guards/guards.test.ts +0 -1
  198. package/tests/security/guards/reflector.test.ts +0 -1
  199. package/tests/security/security-filter.test.ts +0 -1
  200. package/tests/security/security-module-extended.test.ts +0 -1
  201. package/tests/security/security-module.test.ts +0 -1
  202. package/tests/session/session-decorators.test.ts +0 -1
  203. package/tests/session/session-module.test.ts +0 -1
  204. package/tests/swagger/decorators.test.ts +0 -1
  205. package/tests/swagger/swagger-module.test.ts +0 -1
  206. package/tests/swagger/ui.test.ts +0 -1
  207. package/tests/testing/testing-module.test.ts +129 -0
  208. package/tests/validation/class-validator.test.ts +0 -1
  209. package/tests/validation/controller-validation.test.ts +0 -1
@@ -0,0 +1,141 @@
1
+ import type { RequestRecord } from './types';
2
+
3
+ /**
4
+ * 请求录制器
5
+ * 使用环形缓冲区存储 HTTP 请求记录
6
+ */
7
+ export class RequestRecorder {
8
+ private readonly buffer: (RequestRecord | null)[];
9
+ private readonly maxRecords: number;
10
+ private writeIndex: number = 0;
11
+ private count: number = 0;
12
+ private idCounter: number = 0;
13
+ private readonly idMap = new Map<string, number>();
14
+
15
+ /**
16
+ * 创建请求录制器
17
+ * @param maxRecords - 最大录制数量
18
+ */
19
+ public constructor(maxRecords: number = 500) {
20
+ this.maxRecords = maxRecords;
21
+ this.buffer = new Array(maxRecords).fill(null);
22
+ }
23
+
24
+ /**
25
+ * 生成唯一 ID
26
+ */
27
+ private generateId(): string {
28
+ this.idCounter += 1;
29
+ const ts = Date.now().toString(16);
30
+ const counter = this.idCounter.toString(16);
31
+ return `${ts}-${counter}`;
32
+ }
33
+
34
+ /**
35
+ * 录制请求
36
+ * @param record - 请求记录(不含 id,由本方法生成)
37
+ */
38
+ public record(record: Omit<RequestRecord, 'id'>): void {
39
+ const id = this.generateId();
40
+ const fullRecord: RequestRecord = { ...record, id };
41
+
42
+ const oldRecord = this.buffer[this.writeIndex];
43
+ if (oldRecord) {
44
+ this.idMap.delete(oldRecord.id);
45
+ }
46
+
47
+ this.buffer[this.writeIndex] = fullRecord;
48
+ this.idMap.set(id, this.writeIndex);
49
+
50
+ this.writeIndex = (this.writeIndex + 1) % this.maxRecords;
51
+ if (this.count < this.maxRecords) {
52
+ this.count += 1;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 获取所有记录(按时间倒序,最新的在前)
58
+ */
59
+ public getAll(): RequestRecord[] {
60
+ const records: RequestRecord[] = [];
61
+ for (let i = 0; i < this.count; i++) {
62
+ const idx = (this.writeIndex - 1 - i + this.maxRecords) % this.maxRecords;
63
+ const record = this.buffer[idx];
64
+ if (record) {
65
+ records.push(record);
66
+ }
67
+ }
68
+ return records;
69
+ }
70
+
71
+ /**
72
+ * 根据 ID 获取单条记录
73
+ * @param id - 记录 ID
74
+ */
75
+ public getById(id: string): RequestRecord | undefined {
76
+ const index = this.idMap.get(id);
77
+ if (index === undefined) {
78
+ return undefined;
79
+ }
80
+ return this.buffer[index] ?? undefined;
81
+ }
82
+
83
+ /**
84
+ * 清除所有记录
85
+ */
86
+ public clear(): void {
87
+ for (let i = 0; i < this.maxRecords; i++) {
88
+ this.buffer[i] = null;
89
+ }
90
+ this.writeIndex = 0;
91
+ this.count = 0;
92
+ this.idMap.clear();
93
+ }
94
+
95
+ /**
96
+ * 获取当前记录数量
97
+ */
98
+ public getCount(): number {
99
+ return this.count;
100
+ }
101
+
102
+ /**
103
+ * 导出所有记录为 JSONL 格式字符串
104
+ * 每行一条 JSON 记录,按时间倒序(最新在前)
105
+ */
106
+ public exportToJsonl(): string {
107
+ const records = this.getAll();
108
+ return records.map((r) => JSON.stringify(r)).join('\n') + '\n';
109
+ }
110
+
111
+ /**
112
+ * 从 JSONL 内容导入请求记录
113
+ * 利用 Bun 1.3.7+ 原生 Bun.JSONL.parse() 高性能解析
114
+ * @param content - JSONL 格式文本
115
+ */
116
+ public importFromJsonl(content: string): void {
117
+ const records = Bun.JSONL.parse(content) as RequestRecord[];
118
+ for (const record of records) {
119
+ if (record.id) {
120
+ const oldRecord = this.buffer[this.writeIndex];
121
+ if (oldRecord) {
122
+ this.idMap.delete(oldRecord.id);
123
+ }
124
+ this.buffer[this.writeIndex] = record;
125
+ this.idMap.set(record.id, this.writeIndex);
126
+ this.writeIndex = (this.writeIndex + 1) % this.maxRecords;
127
+ if (this.count < this.maxRecords) {
128
+ this.count += 1;
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 解析 JSONL 内容为 RequestRecord 数组(静态工具方法)
136
+ * @param content - JSONL 格式文本
137
+ */
138
+ public static parseJsonl(content: string): RequestRecord[] {
139
+ return Bun.JSONL.parse(content) as RequestRecord[];
140
+ }
141
+ }
@@ -0,0 +1,49 @@
1
+ export interface DebugModuleOptions {
2
+ /**
3
+ * 是否启用调试功能
4
+ * @default true
5
+ */
6
+ enabled?: boolean;
7
+ /**
8
+ * 最大录制请求数(环形缓冲区)
9
+ * @default 500
10
+ */
11
+ maxRecords?: number;
12
+ /**
13
+ * 是否录制请求体
14
+ * @default true
15
+ */
16
+ recordBody?: boolean;
17
+ /**
18
+ * Debug UI 路径
19
+ * @default '/_debug'
20
+ */
21
+ path?: string;
22
+ }
23
+
24
+ export interface RequestRecord {
25
+ id: string;
26
+ timestamp: number;
27
+ request: {
28
+ method: string;
29
+ path: string;
30
+ headers: Record<string, string>;
31
+ body?: unknown;
32
+ };
33
+ response: {
34
+ status: number;
35
+ headers: Record<string, string>;
36
+ bodySize: number;
37
+ };
38
+ timing: {
39
+ total: number;
40
+ };
41
+ metadata: {
42
+ matchedRoute?: string;
43
+ controller?: string;
44
+ methodName?: string;
45
+ };
46
+ }
47
+
48
+ export const DEBUG_OPTIONS_TOKEN = Symbol('@dangao/bun-server:debug:options');
49
+ export const DEBUG_RECORDER_TOKEN = Symbol('@dangao/bun-server:debug:recorder');
@@ -0,0 +1,393 @@
1
+ /**
2
+ * 创建 Debug UI HTML 页面
3
+ * @param basePath - Debug UI 基础路径
4
+ */
5
+ export function createDebugHTML(basePath: string): string {
6
+ const apiBase = `${basePath}/api`;
7
+ const normalizePath = (p: string) => (p.endsWith('/') && p.length > 1 ? p.slice(0, -1) : p);
8
+ const base = normalizePath(basePath);
9
+
10
+ return `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>Debug - Request Replay</title>
16
+ <style>
17
+ :root {
18
+ --bg-primary: #0d1117;
19
+ --bg-secondary: #161b22;
20
+ --bg-tertiary: #21262d;
21
+ --text-primary: #e6edf3;
22
+ --text-secondary: #8b949e;
23
+ --border: #30363d;
24
+ --accent: #58a6ff;
25
+ --accent-hover: #79b8ff;
26
+ --success: #3fb950;
27
+ --warning: #d29922;
28
+ --error: #f85149;
29
+ --get: #3fb950;
30
+ --post: #58a6ff;
31
+ --put: #d29922;
32
+ --delete: #f85149;
33
+ --patch: #a371f7;
34
+ }
35
+ * { box-sizing: border-box; }
36
+ body {
37
+ margin: 0;
38
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
39
+ background: var(--bg-primary);
40
+ color: var(--text-primary);
41
+ line-height: 1.5;
42
+ min-height: 100vh;
43
+ }
44
+ .container {
45
+ max-width: 1400px;
46
+ margin: 0 auto;
47
+ padding: 1.5rem;
48
+ }
49
+ header {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ margin-bottom: 1.5rem;
54
+ padding-bottom: 1rem;
55
+ border-bottom: 1px solid var(--border);
56
+ }
57
+ h1 {
58
+ margin: 0;
59
+ font-size: 1.5rem;
60
+ font-weight: 600;
61
+ }
62
+ .toolbar {
63
+ display: flex;
64
+ gap: 0.75rem;
65
+ align-items: center;
66
+ }
67
+ .btn {
68
+ padding: 0.5rem 1rem;
69
+ border: 1px solid var(--border);
70
+ border-radius: 6px;
71
+ background: var(--bg-secondary);
72
+ color: var(--text-primary);
73
+ cursor: pointer;
74
+ font-size: 0.875rem;
75
+ transition: background 0.15s;
76
+ }
77
+ .btn:hover {
78
+ background: var(--bg-tertiary);
79
+ }
80
+ .btn-primary {
81
+ background: var(--accent);
82
+ border-color: var(--accent);
83
+ color: #fff;
84
+ }
85
+ .btn-primary:hover {
86
+ background: var(--accent-hover);
87
+ }
88
+ .btn-danger {
89
+ background: var(--error);
90
+ border-color: var(--error);
91
+ color: #fff;
92
+ }
93
+ .toggle-label {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.5rem;
97
+ font-size: 0.875rem;
98
+ color: var(--text-secondary);
99
+ }
100
+ .toggle {
101
+ width: 36px;
102
+ height: 20px;
103
+ background: var(--bg-tertiary);
104
+ border-radius: 10px;
105
+ position: relative;
106
+ cursor: pointer;
107
+ transition: background 0.2s;
108
+ }
109
+ .toggle.active { background: var(--accent); }
110
+ .toggle::after {
111
+ content: '';
112
+ position: absolute;
113
+ width: 16px;
114
+ height: 16px;
115
+ background: #fff;
116
+ border-radius: 50%;
117
+ top: 2px;
118
+ left: 2px;
119
+ transition: transform 0.2s;
120
+ }
121
+ .toggle.active::after { transform: translateX(16px); }
122
+ .main-grid {
123
+ display: grid;
124
+ grid-template-columns: 1fr 1fr;
125
+ gap: 1.5rem;
126
+ }
127
+ @media (max-width: 900px) {
128
+ .main-grid { grid-template-columns: 1fr; }
129
+ }
130
+ .panel {
131
+ background: var(--bg-secondary);
132
+ border: 1px solid var(--border);
133
+ border-radius: 8px;
134
+ overflow: hidden;
135
+ }
136
+ .panel-header {
137
+ padding: 0.75rem 1rem;
138
+ background: var(--bg-tertiary);
139
+ font-weight: 600;
140
+ font-size: 0.875rem;
141
+ }
142
+ .record-list {
143
+ max-height: 70vh;
144
+ overflow-y: auto;
145
+ }
146
+ .record-item {
147
+ padding: 0.75rem 1rem;
148
+ border-bottom: 1px solid var(--border);
149
+ cursor: pointer;
150
+ transition: background 0.15s;
151
+ display: grid;
152
+ grid-template-columns: 70px 1fr 60px 70px;
153
+ gap: 0.75rem;
154
+ align-items: center;
155
+ font-size: 0.8125rem;
156
+ }
157
+ .record-item:hover { background: var(--bg-tertiary); }
158
+ .record-item.selected { background: var(--bg-tertiary); border-left: 3px solid var(--accent); }
159
+ .method {
160
+ font-weight: 600;
161
+ font-size: 0.75rem;
162
+ padding: 0.2rem 0.5rem;
163
+ border-radius: 4px;
164
+ }
165
+ .method-GET { background: rgba(63,185,80,0.2); color: var(--get); }
166
+ .method-POST { background: rgba(88,166,255,0.2); color: var(--post); }
167
+ .method-PUT { background: rgba(210,153,34,0.2); color: var(--warning); }
168
+ .method-DELETE { background: rgba(248,81,73,0.2); color: var(--error); }
169
+ .method-PATCH { background: rgba(163,113,247,0.2); color: var(--patch); }
170
+ .path { font-family: monospace; word-break: break-all; }
171
+ .status { font-weight: 500; }
172
+ .status-2xx { color: var(--success); }
173
+ .status-4xx { color: var(--warning); }
174
+ .status-5xx { color: var(--error); }
175
+ .detail-content {
176
+ padding: 1rem;
177
+ font-size: 0.8125rem;
178
+ max-height: 70vh;
179
+ overflow-y: auto;
180
+ }
181
+ .detail-section {
182
+ margin-bottom: 1rem;
183
+ }
184
+ .detail-section h4 {
185
+ margin: 0 0 0.5rem;
186
+ font-size: 0.75rem;
187
+ text-transform: uppercase;
188
+ color: var(--text-secondary);
189
+ }
190
+ .detail-section pre {
191
+ margin: 0;
192
+ padding: 0.75rem;
193
+ background: var(--bg-primary);
194
+ border-radius: 6px;
195
+ overflow-x: auto;
196
+ font-size: 0.75rem;
197
+ white-space: pre-wrap;
198
+ word-break: break-all;
199
+ }
200
+ .empty {
201
+ padding: 2rem;
202
+ text-align: center;
203
+ color: var(--text-secondary);
204
+ }
205
+ .loading { opacity: 0.6; pointer-events: none; }
206
+ </style>
207
+ </head>
208
+ <body>
209
+ <div class="container">
210
+ <header>
211
+ <h1>Debug - Request Replay</h1>
212
+ <div class="toolbar">
213
+ <label class="toggle-label">
214
+ <span>Auto-refresh</span>
215
+ <div class="toggle" id="autoRefresh" title="Toggle auto-refresh"></div>
216
+ </label>
217
+ <button class="btn btn-primary" id="replayBtn" disabled>Replay</button>
218
+ <button class="btn btn-danger" id="clearBtn">Clear</button>
219
+ </div>
220
+ </header>
221
+ <div class="main-grid">
222
+ <div class="panel">
223
+ <div class="panel-header">Recorded Requests (<span id="count">0</span>)</div>
224
+ <div class="record-list" id="recordList"></div>
225
+ </div>
226
+ <div class="panel">
227
+ <div class="panel-header">Request Details</div>
228
+ <div class="detail-content" id="detailContent">
229
+ <div class="empty" id="emptyDetail">Select a request to view details</div>
230
+ <div id="detailBody" style="display:none"></div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ <script>
236
+ (function() {
237
+ const apiBase = '${apiBase}';
238
+ const recordList = document.getElementById('recordList');
239
+ const countEl = document.getElementById('count');
240
+ const emptyDetail = document.getElementById('emptyDetail');
241
+ const detailBody = document.getElementById('detailBody');
242
+ const replayBtn = document.getElementById('replayBtn');
243
+ const clearBtn = document.getElementById('clearBtn');
244
+ const autoRefreshToggle = document.getElementById('autoRefresh');
245
+
246
+ let records = [];
247
+ let selectedId = null;
248
+ let autoRefreshInterval = null;
249
+
250
+ function methodClass(m) {
251
+ return 'method-' + (m || 'GET');
252
+ }
253
+ function statusClass(s) {
254
+ if (s >= 200 && s < 300) return 'status-2xx';
255
+ if (s >= 400 && s < 500) return 'status-4xx';
256
+ if (s >= 500) return 'status-5xx';
257
+ return '';
258
+ }
259
+
260
+ async function fetchRecords() {
261
+ const res = await fetch(apiBase + '/records');
262
+ if (!res.ok) throw new Error('Failed to fetch');
263
+ return res.json();
264
+ }
265
+
266
+ async function fetchRecord(id) {
267
+ const res = await fetch(apiBase + '/records/' + id);
268
+ if (!res.ok) throw new Error('Failed to fetch');
269
+ return res.json();
270
+ }
271
+
272
+ async function loadRecords() {
273
+ try {
274
+ records = await fetchRecords();
275
+ countEl.textContent = records.length;
276
+ renderList();
277
+ } catch (e) {
278
+ recordList.innerHTML = '<div class="empty">Failed to load records</div>';
279
+ }
280
+ }
281
+
282
+ function renderList() {
283
+ if (records.length === 0) {
284
+ recordList.innerHTML = '<div class="empty">No requests recorded yet</div>';
285
+ return;
286
+ }
287
+ recordList.innerHTML = records.map(function(r) {
288
+ return '<div class="record-item' + (r.id === selectedId ? ' selected' : '') + '" data-id="' + r.id + '">' +
289
+ '<span class="method ' + methodClass(r.request.method) + '">' + (r.request.method || 'GET') + '</span>' +
290
+ '<span class="path">' + escapeHtml(r.request.path) + '</span>' +
291
+ '<span class="status ' + statusClass(r.response.status) + '">' + r.response.status + '</span>' +
292
+ '<span>' + r.timing.total + 'ms</span>' +
293
+ '</div>';
294
+ }).join('');
295
+ recordList.querySelectorAll('.record-item').forEach(function(el) {
296
+ el.addEventListener('click', function() {
297
+ selectedId = el.dataset.id;
298
+ loadDetail(selectedId);
299
+ renderList();
300
+ replayBtn.disabled = false;
301
+ });
302
+ });
303
+ }
304
+
305
+ function escapeHtml(s) {
306
+ if (s == null) return '';
307
+ const div = document.createElement('div');
308
+ div.textContent = s;
309
+ return div.innerHTML;
310
+ }
311
+
312
+ async function loadDetail(id) {
313
+ try {
314
+ const record = await fetchRecord(id);
315
+ emptyDetail.style.display = 'none';
316
+ detailBody.style.display = 'block';
317
+ const body = record.request.body !== undefined
318
+ ? JSON.stringify(record.request.body, null, 2)
319
+ : '(no body)';
320
+ const respBody = record.response.bodySize + ' bytes';
321
+ const meta = [];
322
+ if (record.metadata.matchedRoute) meta.push('Route: ' + record.metadata.matchedRoute);
323
+ if (record.metadata.controller) meta.push('Controller: ' + record.metadata.controller);
324
+ if (record.metadata.methodName) meta.push('Method: ' + record.metadata.methodName);
325
+ detailBody.innerHTML =
326
+ '<div class="detail-section"><h4>Request Headers</h4><pre>' + escapeHtml(JSON.stringify(record.request.headers, null, 2)) + '</pre></div>' +
327
+ '<div class="detail-section"><h4>Request Body</h4><pre>' + escapeHtml(body) + '</pre></div>' +
328
+ '<div class="detail-section"><h4>Response</h4><pre>Status: ' + record.response.status + '\\nHeaders: ' + JSON.stringify(record.response.headers, null, 2) + '\\nBody size: ' + respBody + '</pre></div>' +
329
+ (meta.length ? '<div class="detail-section"><h4>Metadata</h4><pre>' + escapeHtml(meta.join('\\n')) + '</pre></div>' : '');
330
+ } catch (e) {
331
+ detailBody.innerHTML = '<div class="empty">Failed to load details</div>';
332
+ }
333
+ }
334
+
335
+ async function replay(id) {
336
+ if (!id) return;
337
+ replayBtn.disabled = true;
338
+ replayBtn.textContent = 'Replaying...';
339
+ try {
340
+ const res = await fetch(apiBase + '/replay/' + id, { method: 'POST' });
341
+ const data = await res.json();
342
+ if (data.ok) {
343
+ await loadRecords();
344
+ if (data.newId) selectedId = data.newId;
345
+ renderList();
346
+ loadDetail(selectedId || id);
347
+ } else {
348
+ alert('Replay failed: ' + (data.error || res.statusText));
349
+ }
350
+ } catch (e) {
351
+ alert('Replay failed: ' + e.message);
352
+ }
353
+ replayBtn.disabled = false;
354
+ replayBtn.textContent = 'Replay';
355
+ }
356
+
357
+ async function clear() {
358
+ if (!confirm('Clear all recorded requests?')) return;
359
+ try {
360
+ await fetch(apiBase + '/records', { method: 'DELETE' });
361
+ records = [];
362
+ selectedId = null;
363
+ emptyDetail.style.display = 'block';
364
+ detailBody.style.display = 'none';
365
+ detailBody.innerHTML = '';
366
+ replayBtn.disabled = true;
367
+ loadRecords();
368
+ } catch (e) {
369
+ alert('Clear failed: ' + e.message);
370
+ }
371
+ }
372
+
373
+ replayBtn.addEventListener('click', function() {
374
+ if (selectedId) replay(selectedId);
375
+ });
376
+ clearBtn.addEventListener('click', clear);
377
+
378
+ autoRefreshToggle.addEventListener('click', function() {
379
+ autoRefreshToggle.classList.toggle('active');
380
+ if (autoRefreshToggle.classList.contains('active')) {
381
+ autoRefreshInterval = setInterval(loadRecords, 2000);
382
+ } else {
383
+ clearInterval(autoRefreshInterval);
384
+ autoRefreshInterval = null;
385
+ }
386
+ });
387
+
388
+ loadRecords();
389
+ })();
390
+ </script>
391
+ </body>
392
+ </html>`;
393
+ }