shokupan 0.6.1 → 0.9.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 (81) hide show
  1. package/README.md +55 -2
  2. package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
  3. package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
  4. package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
  5. package/dist/analyzer-Ce_7JxZh.js.map +1 -0
  6. package/dist/cli.cjs +2 -2
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/context.d.ts +58 -23
  11. package/dist/{server-adapter-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  13. package/dist/{server-adapter-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  14. package/dist/http-server-CCeagTyU.js.map +1 -0
  15. package/dist/index.cjs +1940 -917
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +18 -17
  18. package/dist/index.js +1948 -925
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware.d.ts +1 -1
  21. package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
  22. package/dist/plugins/application/cluster.d.ts +33 -0
  23. package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
  24. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  25. package/dist/plugins/application/dashboard/plugin.d.ts +42 -0
  26. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  27. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  28. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  29. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  30. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  31. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  32. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  33. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  34. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  35. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  36. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  37. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  38. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  39. package/dist/plugins/application/dashboard/template.eta +246 -0
  40. package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
  41. package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
  42. package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
  43. package/dist/plugins/application/scalar.d.ts +36 -0
  44. package/dist/plugins/application/socket-io.d.ts +14 -0
  45. package/dist/plugins/middleware/compression.d.ts +17 -0
  46. package/dist/plugins/middleware/cors.d.ts +34 -0
  47. package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
  48. package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
  49. package/dist/plugins/middleware/proxy.d.ts +37 -0
  50. package/dist/plugins/middleware/rate-limit.d.ts +58 -0
  51. package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
  52. package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
  53. package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
  54. package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
  55. package/dist/router.d.ts +17 -5
  56. package/dist/shokupan.d.ts +31 -5
  57. package/dist/util/async-hooks.d.ts +8 -2
  58. package/dist/util/datastore.d.ts +4 -3
  59. package/dist/{decorators.d.ts → util/decorators.d.ts} +6 -1
  60. package/dist/util/http-error.d.ts +38 -0
  61. package/dist/util/http-status.d.ts +32 -0
  62. package/dist/util/instrumentation.d.ts +1 -1
  63. package/dist/{request.d.ts → util/request.d.ts} +1 -1
  64. package/dist/util/symbol.d.ts +34 -0
  65. package/dist/{router → util}/trie.d.ts +1 -1
  66. package/dist/{types.d.ts → util/types.d.ts} +38 -2
  67. package/package.json +9 -6
  68. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
  69. package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
  70. package/dist/plugins/compression.d.ts +0 -5
  71. package/dist/plugins/cors.d.ts +0 -11
  72. package/dist/plugins/debugview/plugin.d.ts +0 -29
  73. package/dist/plugins/proxy.d.ts +0 -11
  74. package/dist/plugins/rate-limit.d.ts +0 -15
  75. package/dist/plugins/scalar.d.ts +0 -15
  76. package/dist/server-adapter-0xH174zz.js.map +0 -1
  77. package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
  78. package/dist/symbol.d.ts +0 -15
  79. /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
  80. /package/dist/{di.d.ts → util/di.d.ts} +0 -0
  81. /package/dist/{response.d.ts → util/response.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { ShokupanContext } from './context';
2
- import { Middleware, NextFn } from './types';
2
+ import { Middleware, NextFn } from './util/types';
3
3
  /**
4
4
  * Composes a list of middleware into a single function.
5
5
  * This is the onion model (Koa-style).
@@ -1,5 +1,7 @@
1
- import { ShokupanContext } from '../context';
2
- import { ShokupanRouter } from '../router';
1
+ import { ShokupanContext } from '../../context';
2
+ import { ShokupanRouter } from '../../router';
3
+ import { Shokupan } from '../../shokupan';
4
+ import { ShokupanPlugin, ShokupanPluginOptions } from '../../util/types';
3
5
  export interface AuthUser {
4
6
  id: string;
5
7
  email?: string;
@@ -9,29 +11,92 @@ export interface AuthUser {
9
11
  raw?: any;
10
12
  }
11
13
  export interface ProviderConfig {
14
+ /**
15
+ * Client ID
16
+ */
12
17
  clientId: string;
18
+ /**
19
+ * Client secret
20
+ */
13
21
  clientSecret: string;
22
+ /**
23
+ * Redirect URI
24
+ */
14
25
  redirectUri: string;
26
+ /**
27
+ * Scopes
28
+ */
15
29
  scopes?: string[];
30
+ /**
31
+ * Tenant ID (MSFT AD)
32
+ */
16
33
  tenantId?: string;
34
+ /**
35
+ * Domain (Auth0, Okta)
36
+ */
17
37
  domain?: string;
38
+ /**
39
+ * Team ID (Apple)
40
+ */
18
41
  teamId?: string;
42
+ /**
43
+ * Key ID (Apple)
44
+ */
19
45
  keyId?: string;
46
+ /**
47
+ * Auth URL (Generic OAuth2)
48
+ */
20
49
  authUrl?: string;
50
+ /**
51
+ * Token URL (Generic OAuth2)
52
+ */
21
53
  tokenUrl?: string;
54
+ /**
55
+ * User info URL (Generic OAuth2)
56
+ */
22
57
  userInfoUrl?: string;
23
58
  }
24
59
  export interface AuthConfig {
60
+ /**
61
+ * JWT secret
62
+ */
25
63
  jwtSecret: string | Uint8Array;
64
+ /**
65
+ * JWT expiration
66
+ */
26
67
  jwtExpiration?: string;
68
+ /**
69
+ * Cookie options
70
+ */
27
71
  cookieOptions?: {
72
+ /**
73
+ * HTTP only
74
+ */
28
75
  httpOnly?: boolean;
76
+ /**
77
+ * Secure
78
+ */
29
79
  secure?: boolean;
80
+ /**
81
+ * Same site
82
+ */
30
83
  sameSite?: "Strict" | "Lax" | "None";
84
+ /**
85
+ * Path
86
+ */
31
87
  path?: string;
88
+ /**
89
+ * Max age
90
+ */
32
91
  maxAge?: number;
33
92
  };
93
+ /**
94
+ * Success callback
95
+ */
34
96
  onSuccess?: (user: AuthUser, ctx: ShokupanContext) => Promise<any> | any;
97
+ /**
98
+ * Providers
99
+ */
35
100
  providers: {
36
101
  github?: ProviderConfig;
37
102
  google?: ProviderConfig;
@@ -43,10 +108,14 @@ export interface AuthConfig {
43
108
  [key: string]: ProviderConfig | undefined;
44
109
  };
45
110
  }
46
- export declare class AuthPlugin extends ShokupanRouter<any> {
111
+ /**
112
+ * Authentication plugin
113
+ */
114
+ export declare class AuthPlugin extends ShokupanRouter<any> implements ShokupanPlugin {
47
115
  private authConfig;
48
116
  private secret;
49
117
  constructor(authConfig: AuthConfig);
118
+ onInit(app: Shokupan, options?: ShokupanPluginOptions): void;
50
119
  private getProviderInstance;
51
120
  private createSession;
52
121
  private init;
@@ -0,0 +1,33 @@
1
+ import { Shokupan } from '../../shokupan';
2
+ import { ShokupanPlugin } from '../../util/types';
3
+ export interface ClusterOptions {
4
+ /**
5
+ * Number of workers to spawn.
6
+ * Set to -1 or 'auto' to spawn one worker per available CPU.
7
+ * @default 'auto'
8
+ */
9
+ workers?: number | 'auto';
10
+ /**
11
+ * Whether to pipe stdout/stderr to the parent process.
12
+ * @default false
13
+ */
14
+ silent?: boolean;
15
+ /**
16
+ * Enable sticky sessions (useful for Socket.io).
17
+ * Currently only supported in Node.js runtime.
18
+ * @default false
19
+ */
20
+ sticky?: boolean;
21
+ }
22
+ /**
23
+ * Cluster Plugin
24
+ *
25
+ * Automatically manages clustering for Node.js and Bun.
26
+ */
27
+ export declare class ClusterPlugin implements ShokupanPlugin {
28
+ private options;
29
+ constructor(options?: ClusterOptions);
30
+ onInit(app: Shokupan): void;
31
+ private handleBun;
32
+ private handleNode;
33
+ }
@@ -1,4 +1,4 @@
1
- import { Middleware } from '../types';
1
+ import { Middleware } from '../../../util/types';
2
2
  export interface FailedRequestRecorderOptions {
3
3
  /**
4
4
  * Maximum number of failed requests to keep.
@@ -0,0 +1,12 @@
1
+ export declare class MetricsCollector {
2
+ private currentIntervalStart;
3
+ private pendingDetails;
4
+ private eventLoopHistogram;
5
+ private timer;
6
+ constructor();
7
+ recordRequest(duration: number, isError: boolean): void;
8
+ private alignTimestamp;
9
+ private collect;
10
+ private flushInterval;
11
+ stop(): void;
12
+ }
@@ -0,0 +1,42 @@
1
+ import { HeadersInit } from 'bun';
2
+ import { ShokupanHooks, ShokupanPlugin } from '../../../util/types';
3
+ export interface RequestLog {
4
+ method: string;
5
+ url: string;
6
+ status: number;
7
+ duration: number;
8
+ timestamp: number;
9
+ handlerStack?: any[];
10
+ }
11
+ export interface DashboardConfig {
12
+ getRequestHeaders?: () => HeadersInit;
13
+ path?: string;
14
+ /**
15
+ * Retention time in milliseconds
16
+ */
17
+ retentionMs?: number;
18
+ }
19
+ export declare class Dashboard implements ShokupanPlugin {
20
+ private readonly dashboardConfig;
21
+ private static __dirname;
22
+ private static getBasePath;
23
+ private router;
24
+ private metrics;
25
+ private eta;
26
+ private startTime;
27
+ private instrumented;
28
+ private metricsCollector;
29
+ constructor(dashboardConfig?: DashboardConfig);
30
+ onInit(app: any, options?: {
31
+ path?: string;
32
+ }): void;
33
+ private setupRoutes;
34
+ private instrumentApp;
35
+ private assignIdsToRegistry;
36
+ recordNodeMetric(id: string, type: string, duration: number, isError: boolean): void;
37
+ recordEdgeMetric(from: string, to: string): void;
38
+ private getLinkPattern;
39
+ getHooks(): ShokupanHooks;
40
+ private updateTiming;
41
+ }
42
+ export default function DebugDashboard(config?: DashboardConfig): Dashboard;
@@ -0,0 +1,328 @@
1
+
2
+ // Common chart config
3
+ const commonOptions = {
4
+ responsive: true,
5
+ maintainAspectRatio: false,
6
+ plugins: {
7
+ legend: { labels: { color: '#94a3b8' } }
8
+ },
9
+ scales: {
10
+ x: {
11
+ ticks: { color: '#94a3b8' },
12
+ grid: { color: '#334155' }
13
+ },
14
+ y: {
15
+ ticks: { color: '#94a3b8' },
16
+ grid: { color: '#334155' },
17
+ beginAtZero: true
18
+ }
19
+ },
20
+ animation: { duration: 0 },
21
+ interaction: {
22
+ mode: 'index',
23
+ intersect: false,
24
+ },
25
+ };
26
+
27
+ // Get request headers from global function if available
28
+ const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
29
+
30
+ // Determine base path for API requests
31
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
32
+ const url = basePath + '/';
33
+
34
+ // Chart instances
35
+
36
+ // --- Latency Chart ---
37
+ const latencyCtx = document.getElementById('latencyChart').getContext('2d');
38
+ const latencyChart = new Chart(latencyCtx, {
39
+ type: 'line',
40
+ data: {
41
+ labels: [],
42
+ datasets: [
43
+ {
44
+ label: 'Msg (Avg)',
45
+ data: [],
46
+ borderColor: '#3b82f6',
47
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
48
+ borderWidth: 2,
49
+ tension: 0.4,
50
+ fill: false
51
+ },
52
+ {
53
+ label: 'p95',
54
+ data: [],
55
+ borderColor: '#eab308',
56
+ backgroundColor: 'rgba(234, 179, 8, 0.1)',
57
+ borderWidth: 2,
58
+ tension: 0.4,
59
+ fill: false
60
+ },
61
+ {
62
+ label: 'p99',
63
+ data: [],
64
+ borderColor: '#ef4444',
65
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
66
+ borderWidth: 2,
67
+ tension: 0.4,
68
+ fill: false
69
+ }
70
+ ]
71
+ },
72
+ options: commonOptions
73
+ });
74
+
75
+ // --- RPS Chart ---
76
+ const rpsCtx = document.getElementById('rpsChart').getContext('2d');
77
+ const rpsChart = new Chart(rpsCtx, {
78
+ type: 'line',
79
+ data: {
80
+ labels: [],
81
+ datasets: [
82
+ {
83
+ label: 'Total Requests',
84
+ data: [],
85
+ borderColor: '#10b981',
86
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
87
+ borderWidth: 2,
88
+ tension: 0.4,
89
+ fill: true
90
+ },
91
+ {
92
+ label: 'Errors',
93
+ data: [],
94
+ borderColor: '#ef4444',
95
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
96
+ borderWidth: 2,
97
+ tension: 0.4,
98
+ fill: true
99
+ }
100
+ ]
101
+ },
102
+ options: commonOptions
103
+ });
104
+
105
+ // --- CPU Chart ---
106
+ const cpuCtx = document.getElementById('cpuChart').getContext('2d');
107
+ const cpuChart = new Chart(cpuCtx, {
108
+ type: 'line',
109
+ data: {
110
+ labels: [],
111
+ datasets: [
112
+ {
113
+ label: 'CPU Load (1m)',
114
+ data: [],
115
+ borderColor: '#8b5cf6',
116
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
117
+ borderWidth: 2,
118
+ tension: 0.4,
119
+ fill: true
120
+ }
121
+ ]
122
+ },
123
+ options: commonOptions
124
+ });
125
+
126
+ // --- Memory Chart ---
127
+ const memoryCtx = document.getElementById('memoryChart').getContext('2d');
128
+ const memoryChart = new Chart(memoryCtx, {
129
+ type: 'line',
130
+ data: {
131
+ labels: [],
132
+ datasets: [
133
+ {
134
+ label: 'Heap Used (MB)',
135
+ data: [],
136
+ borderColor: '#f97316',
137
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
138
+ borderWidth: 2,
139
+ tension: 0.4,
140
+ fill: true
141
+ },
142
+ {
143
+ label: 'RSS (MB)',
144
+ data: [],
145
+ borderColor: '#06b6d4',
146
+ backgroundColor: 'rgba(6, 182, 212, 0.1)',
147
+ borderWidth: 2,
148
+ tension: 0.4,
149
+ fill: true
150
+ }
151
+ ]
152
+ },
153
+ options: commonOptions
154
+ });
155
+
156
+ // --- Heap Chart ---
157
+ const heapCtx = document.getElementById('heapChart').getContext('2d');
158
+ const heapChart = new Chart(heapCtx, {
159
+ type: 'line',
160
+ data: {
161
+ labels: [],
162
+ datasets: [
163
+ {
164
+ label: 'Heap Used (MB)',
165
+ data: [],
166
+ borderColor: '#f97316',
167
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
168
+ borderWidth: 2,
169
+ tension: 0.4,
170
+ fill: true
171
+ },
172
+ {
173
+ label: 'Heap Total (MB)',
174
+ data: [],
175
+ borderColor: '#fb923c',
176
+ backgroundColor: 'rgba(251, 146, 60, 0.1)',
177
+ borderWidth: 2,
178
+ tension: 0.4,
179
+ fill: false
180
+ }
181
+ ]
182
+ },
183
+ options: commonOptions
184
+ });
185
+
186
+ // --- Event Loop Latency Chart ---
187
+ const eventLoopCtx = document.getElementById('eventLoopChart').getContext('2d');
188
+ const eventLoopChart = new Chart(eventLoopCtx, {
189
+ type: 'line',
190
+ data: {
191
+ labels: [],
192
+ datasets: [
193
+ {
194
+ label: 'Mean (ms)',
195
+ data: [],
196
+ borderColor: '#3b82f6',
197
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
198
+ borderWidth: 2,
199
+ tension: 0.4,
200
+ fill: false
201
+ },
202
+ {
203
+ label: 'p95 (ms)',
204
+ data: [],
205
+ borderColor: '#eab308',
206
+ backgroundColor: 'rgba(234, 179, 8, 0.1)',
207
+ borderWidth: 2,
208
+ tension: 0.4,
209
+ fill: false
210
+ },
211
+ {
212
+ label: 'p99 (ms)',
213
+ data: [],
214
+ borderColor: '#ef4444',
215
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
216
+ borderWidth: 2,
217
+ tension: 0.4,
218
+ fill: false
219
+ }
220
+ ]
221
+ },
222
+ options: commonOptions
223
+ });
224
+
225
+ // --- Error Rate Chart ---
226
+ const errorRateCtx = document.getElementById('errorRateChart').getContext('2d');
227
+ const errorRateChart = new Chart(errorRateCtx, {
228
+ type: 'line',
229
+ data: {
230
+ labels: [],
231
+ datasets: [
232
+ {
233
+ label: 'Success Rate (%)',
234
+ data: [],
235
+ borderColor: '#10b981',
236
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
237
+ borderWidth: 2,
238
+ tension: 0.4,
239
+ fill: true
240
+ },
241
+ {
242
+ label: 'Error Rate (%)',
243
+ data: [],
244
+ borderColor: '#ef4444',
245
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
246
+ borderWidth: 2,
247
+ tension: 0.4,
248
+ fill: true
249
+ }
250
+ ]
251
+ },
252
+ options: commonOptions
253
+ });
254
+
255
+ async function updateCharts() {
256
+ const period = document.getElementById('time-range-selector').value || '1m';
257
+ try {
258
+ const res = await fetch(`${url}metrics/history?interval=${period}`, { headers });
259
+ const data = await res.json();
260
+ const metrics = data.metrics || [];
261
+
262
+ const labels = metrics.map(m => new Date(m.timestamp).toLocaleTimeString());
263
+
264
+ // Latency
265
+ latencyChart.data.labels = labels;
266
+ latencyChart.data.datasets[0].data = metrics.map(m => m.responseTime.avg);
267
+ latencyChart.data.datasets[1].data = metrics.map(m => m.responseTime.p95);
268
+ latencyChart.data.datasets[2].data = metrics.map(m => m.responseTime.p99);
269
+ latencyChart.update();
270
+
271
+ // RPS
272
+ rpsChart.data.labels = labels;
273
+ rpsChart.data.datasets[0].data = metrics.map(m => m.requests.total);
274
+ rpsChart.data.datasets[1].data = metrics.map(m => m.requests.error);
275
+ rpsChart.update();
276
+
277
+ // CPU
278
+ cpuChart.data.labels = labels;
279
+ cpuChart.data.datasets[0].data = metrics.map(m => m.cpu);
280
+ cpuChart.update();
281
+
282
+ // Memory
283
+ memoryChart.data.labels = labels;
284
+ memoryChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
285
+ memoryChart.data.datasets[1].data = metrics.map(m => (m.memory.used / 1024 / 1024).toFixed(2));
286
+ memoryChart.update();
287
+
288
+ // Heap
289
+ heapChart.data.labels = labels;
290
+ heapChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
291
+ heapChart.data.datasets[1].data = metrics.map(m => (m.memory.heapTotal / 1024 / 1024).toFixed(2));
292
+ heapChart.update();
293
+
294
+ // Event Loop Latency
295
+ eventLoopChart.data.labels = labels;
296
+ eventLoopChart.data.datasets[0].data = metrics.map(m => m.eventLoopLatency.mean);
297
+ eventLoopChart.data.datasets[1].data = metrics.map(m => m.eventLoopLatency.p95);
298
+ eventLoopChart.data.datasets[2].data = metrics.map(m => m.eventLoopLatency.p99);
299
+ eventLoopChart.update();
300
+
301
+ // Error Rate
302
+ errorRateChart.data.labels = labels;
303
+ errorRateChart.data.datasets[0].data = metrics.map(m => {
304
+ const total = m.requests.success + m.requests.error;
305
+ return total > 0 ? ((m.requests.success / total) * 100).toFixed(2) : 100;
306
+ });
307
+ errorRateChart.data.datasets[1].data = metrics.map(m => {
308
+ const total = m.requests.success + m.requests.error;
309
+ return total > 0 ? ((m.requests.error / total) * 100).toFixed(2) : 0;
310
+ });
311
+ errorRateChart.update();
312
+
313
+ } catch (e) {
314
+ console.error("Failed to fetch metrics", e);
315
+ }
316
+ }
317
+
318
+ // Initial load
319
+ document.addEventListener("DOMContentLoaded", () => {
320
+ updateCharts();
321
+ // Poll every 10s for short intervals
322
+ setInterval(() => {
323
+ const period = document.getElementById('time-range-selector').value;
324
+ if (['1m', '5m', '30m', '1h', '2h'].includes(period)) {
325
+ updateCharts();
326
+ }
327
+ }, 10000);
328
+ });
@@ -0,0 +1,85 @@
1
+
2
+ // --- Failures Tab Logic ---
3
+ const failuresTable = new Tabulator("#failures-table-container", {
4
+ layout: "fitColumns",
5
+ height: "500px",
6
+ placeholder: "No failed requests found",
7
+ data: [],
8
+ columns: [
9
+ { title: "Time", field: "timestamp", width: 180, formatter: (cell) => new Date(cell.getValue()).toLocaleString() },
10
+ { title: "Method", field: "method", width: 100 },
11
+ { title: "URL", field: "url" },
12
+ { title: "Status", field: "status", width: 90, formatter: (cell) => `<span style="color: #ef4444; font-weight: bold;">${cell.getValue()}</span>` },
13
+ {
14
+ title: "Actions", formatter: (cell) => {
15
+ return `
16
+ <button class="replay-btn" style="background:#3b82f6; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer; margin-right:4px;">Replay</button>
17
+ <button class="export-btn" style="background:#64748b; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer;">Export</button>
18
+ `;
19
+ }, cellClick: (e, cell) => {
20
+ if (e.target.classList.contains('replay-btn')) {
21
+ replayRequest(cell.getRow().getData());
22
+ } else if (e.target.classList.contains('export-btn')) {
23
+ exportFailure(cell.getRow().getData());
24
+ }
25
+ }, width: 140, headerSort: false
26
+ }
27
+ ]
28
+ });
29
+
30
+
31
+ function exportFailure(data) {
32
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
33
+ const url = URL.createObjectURL(blob);
34
+ const a = document.createElement('a');
35
+ a.href = url;
36
+ a.download = `failure-${data.timestamp}.json`;
37
+ a.click();
38
+ URL.revokeObjectURL(url);
39
+ }
40
+
41
+ function importFailure() {
42
+ const input = document.createElement('input');
43
+ input.type = 'file';
44
+ input.accept = '.json';
45
+ input.onchange = e => {
46
+ const file = e.target.files[0];
47
+ if (!file) return;
48
+ const reader = new FileReader();
49
+ reader.onload = ev => {
50
+ try {
51
+ const data = JSON.parse(ev.target.result);
52
+ replayRequest(data);
53
+ } catch (err) { alert("Invalid JSON: " + err); }
54
+ };
55
+ reader.readAsText(file);
56
+ };
57
+ input.click();
58
+ }
59
+
60
+ async function replayRequest(req) {
61
+ if (!confirm(`Replay ${req.method} ${req.url}?`)) return;
62
+
63
+ try {
64
+ const headers = getRequestHeaders ? getRequestHeaders() : {};
65
+ const basePath = window.location.pathname.endsWith('/') ? '' : window.location.pathname;
66
+ const url = basePath + (basePath.endsWith('/') ? 'replay' : '/replay');
67
+
68
+ const res = await fetch(url, {
69
+ method: 'POST',
70
+ headers: { ...headers, 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ method: req.method,
73
+ url: req.url,
74
+ headers: req.headers,
75
+ body: req.body
76
+ })
77
+ });
78
+
79
+ const result = await res.json();
80
+ alert(`Replay Status: ${res.status}\n\nResponse:\n${JSON.stringify(result, null, 2)}`);
81
+ } catch (e) {
82
+ alert("Replay Failed: " + e);
83
+ }
84
+ }
85
+