express-performance-toolkit 1.0.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 (51) hide show
  1. package/README.md +217 -0
  2. package/dist/cache.d.ts +25 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/cache.js +182 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/compression.d.ts +7 -0
  7. package/dist/compression.d.ts.map +1 -0
  8. package/dist/compression.js +26 -0
  9. package/dist/compression.js.map +1 -0
  10. package/dist/dashboard/dashboard.html +756 -0
  11. package/dist/dashboard/dashboardRouter.d.ts +9 -0
  12. package/dist/dashboard/dashboardRouter.d.ts.map +1 -0
  13. package/dist/dashboard/dashboardRouter.js +71 -0
  14. package/dist/dashboard/dashboardRouter.js.map +1 -0
  15. package/dist/index.d.ts +45 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +130 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/logger.d.ts +8 -0
  20. package/dist/logger.d.ts.map +1 -0
  21. package/dist/logger.js +70 -0
  22. package/dist/logger.js.map +1 -0
  23. package/dist/queryHelper.d.ts +8 -0
  24. package/dist/queryHelper.d.ts.map +1 -0
  25. package/dist/queryHelper.js +39 -0
  26. package/dist/queryHelper.js.map +1 -0
  27. package/dist/store.d.ts +24 -0
  28. package/dist/store.d.ts.map +1 -0
  29. package/dist/store.js +108 -0
  30. package/dist/store.js.map +1 -0
  31. package/dist/types.d.ts +135 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +3 -0
  34. package/dist/types.js.map +1 -0
  35. package/example/server.ts +126 -0
  36. package/example/tsconfig.json +17 -0
  37. package/jest.config.js +10 -0
  38. package/package.json +57 -0
  39. package/src/cache.ts +228 -0
  40. package/src/compression.ts +25 -0
  41. package/src/dashboard/dashboard.html +756 -0
  42. package/src/dashboard/dashboardRouter.ts +45 -0
  43. package/src/index.ts +141 -0
  44. package/src/logger.ts +83 -0
  45. package/src/queryHelper.ts +49 -0
  46. package/src/store.ts +134 -0
  47. package/src/types.ts +155 -0
  48. package/tests/cache.test.ts +76 -0
  49. package/tests/integration.test.ts +124 -0
  50. package/tests/store.test.ts +103 -0
  51. package/tsconfig.json +21 -0
@@ -0,0 +1,756 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>⚡ Express Performance Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg-primary: #0a0e1a;
10
+ --bg-secondary: #111827;
11
+ --bg-card: #1a2236;
12
+ --bg-card-hover: #1f2a42;
13
+ --border: #2a3452;
14
+ --text-primary: #e2e8f0;
15
+ --text-secondary: #94a3b8;
16
+ --text-muted: #64748b;
17
+ --accent-blue: #3b82f6;
18
+ --accent-cyan: #06b6d4;
19
+ --accent-green: #10b981;
20
+ --accent-yellow: #f59e0b;
21
+ --accent-red: #ef4444;
22
+ --accent-purple: #8b5cf6;
23
+ --gradient-1: linear-gradient(135deg, #3b82f6, #06b6d4);
24
+ --gradient-2: linear-gradient(135deg, #10b981, #06b6d4);
25
+ --gradient-3: linear-gradient(135deg, #f59e0b, #ef4444);
26
+ --gradient-4: linear-gradient(135deg, #8b5cf6, #3b82f6);
27
+ --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
28
+ --shadow-glow: 0 0 30px rgba(59, 130, 246, 0.15);
29
+ }
30
+
31
+ * { margin: 0; padding: 0; box-sizing: border-box; }
32
+
33
+ body {
34
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
35
+ background: var(--bg-primary);
36
+ color: var(--text-primary);
37
+ min-height: 100vh;
38
+ }
39
+
40
+ /* ── Header ─────────────────────── */
41
+ .header {
42
+ background: var(--bg-secondary);
43
+ border-bottom: 1px solid var(--border);
44
+ padding: 16px 32px;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ position: sticky;
49
+ top: 0;
50
+ z-index: 100;
51
+ backdrop-filter: blur(12px);
52
+ }
53
+ .header-left {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 12px;
57
+ }
58
+ .header h1 {
59
+ font-size: 20px;
60
+ font-weight: 700;
61
+ background: var(--gradient-1);
62
+ -webkit-background-clip: text;
63
+ -webkit-text-fill-color: transparent;
64
+ }
65
+ .header-badge {
66
+ font-size: 11px;
67
+ padding: 3px 10px;
68
+ border-radius: 20px;
69
+ background: rgba(59, 130, 246, 0.15);
70
+ color: var(--accent-blue);
71
+ border: 1px solid rgba(59, 130, 246, 0.3);
72
+ font-weight: 600;
73
+ }
74
+ .pulse-dot {
75
+ width: 8px; height: 8px;
76
+ background: var(--accent-green);
77
+ border-radius: 50%;
78
+ animation: pulse 2s infinite;
79
+ }
80
+ @keyframes pulse {
81
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); }
82
+ 50% { opacity: 0.8; box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
83
+ }
84
+ .header-right {
85
+ display: flex; align-items: center; gap: 12px;
86
+ }
87
+ .uptime {
88
+ font-size: 13px;
89
+ color: var(--text-secondary);
90
+ }
91
+ .refresh-btn {
92
+ background: var(--bg-card);
93
+ border: 1px solid var(--border);
94
+ color: var(--text-secondary);
95
+ padding: 6px 14px;
96
+ border-radius: 8px;
97
+ cursor: pointer;
98
+ font-size: 13px;
99
+ transition: all 0.2s;
100
+ }
101
+ .refresh-btn:hover {
102
+ background: var(--bg-card-hover);
103
+ color: var(--text-primary);
104
+ border-color: var(--accent-blue);
105
+ }
106
+
107
+ /* ── Main Layout ────────────────── */
108
+ .container {
109
+ max-width: 1400px;
110
+ margin: 0 auto;
111
+ padding: 24px 32px;
112
+ }
113
+
114
+ /* ── Stats Cards ────────────────── */
115
+ .stats-grid {
116
+ display: grid;
117
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
118
+ gap: 16px;
119
+ margin-bottom: 28px;
120
+ }
121
+ .stat-card {
122
+ background: var(--bg-card);
123
+ border: 1px solid var(--border);
124
+ border-radius: 14px;
125
+ padding: 20px;
126
+ transition: all 0.3s;
127
+ position: relative;
128
+ overflow: hidden;
129
+ }
130
+ .stat-card::before {
131
+ content: '';
132
+ position: absolute;
133
+ top: 0; left: 0; right: 0;
134
+ height: 3px;
135
+ border-radius: 14px 14px 0 0;
136
+ }
137
+ .stat-card:nth-child(1)::before { background: var(--gradient-1); }
138
+ .stat-card:nth-child(2)::before { background: var(--gradient-2); }
139
+ .stat-card:nth-child(3)::before { background: var(--gradient-3); }
140
+ .stat-card:nth-child(4)::before { background: var(--gradient-4); }
141
+ .stat-card:nth-child(5)::before { background: linear-gradient(135deg, #06b6d4, #10b981); }
142
+ .stat-card:hover {
143
+ border-color: var(--accent-blue);
144
+ box-shadow: var(--shadow-glow);
145
+ transform: translateY(-2px);
146
+ }
147
+ .stat-label {
148
+ font-size: 12px;
149
+ color: var(--text-muted);
150
+ text-transform: uppercase;
151
+ letter-spacing: 1px;
152
+ font-weight: 600;
153
+ margin-bottom: 8px;
154
+ }
155
+ .stat-value {
156
+ font-size: 32px;
157
+ font-weight: 800;
158
+ letter-spacing: -1px;
159
+ }
160
+ .stat-sub {
161
+ font-size: 12px;
162
+ color: var(--text-secondary);
163
+ margin-top: 4px;
164
+ }
165
+
166
+ /* ── Panels ──────────────────────── */
167
+ .panels {
168
+ display: grid;
169
+ grid-template-columns: 1fr 1fr;
170
+ gap: 20px;
171
+ margin-bottom: 28px;
172
+ }
173
+ @media (max-width: 900px) {
174
+ .panels { grid-template-columns: 1fr; }
175
+ }
176
+ .panel {
177
+ background: var(--bg-card);
178
+ border: 1px solid var(--border);
179
+ border-radius: 14px;
180
+ overflow: hidden;
181
+ }
182
+ .panel-header {
183
+ padding: 16px 20px;
184
+ border-bottom: 1px solid var(--border);
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ }
189
+ .panel-title {
190
+ font-size: 14px;
191
+ font-weight: 700;
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 8px;
195
+ }
196
+ .panel-body {
197
+ padding: 16px 20px;
198
+ max-height: 350px;
199
+ overflow-y: auto;
200
+ }
201
+ .panel-body::-webkit-scrollbar { width: 6px; }
202
+ .panel-body::-webkit-scrollbar-track { background: transparent; }
203
+ .panel-body::-webkit-scrollbar-thumb {
204
+ background: var(--border);
205
+ border-radius: 3px;
206
+ }
207
+
208
+ /* ── Cache Donut Chart ───────────── */
209
+ .cache-chart {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ gap: 32px;
214
+ padding: 12px 0;
215
+ }
216
+ .donut-container {
217
+ position: relative;
218
+ width: 130px; height: 130px;
219
+ }
220
+ .donut-center {
221
+ position: absolute;
222
+ top: 50%; left: 50%;
223
+ transform: translate(-50%, -50%);
224
+ text-align: center;
225
+ }
226
+ .donut-center .pct {
227
+ font-size: 28px;
228
+ font-weight: 800;
229
+ }
230
+ .donut-center .lbl {
231
+ font-size: 10px;
232
+ color: var(--text-muted);
233
+ text-transform: uppercase;
234
+ letter-spacing: 1px;
235
+ }
236
+ .cache-legend {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 10px;
240
+ }
241
+ .legend-item {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 8px;
245
+ font-size: 13px;
246
+ }
247
+ .legend-dot {
248
+ width: 10px; height: 10px;
249
+ border-radius: 50%;
250
+ }
251
+
252
+ /* ── Status Codes Bar ────────────── */
253
+ .status-bars {
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: 10px;
257
+ }
258
+ .status-bar-row {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 10px;
262
+ }
263
+ .status-code {
264
+ font-size: 13px;
265
+ font-weight: 700;
266
+ min-width: 36px;
267
+ font-family: 'SF Mono', 'Fira Code', monospace;
268
+ }
269
+ .status-code.s2xx { color: var(--accent-green); }
270
+ .status-code.s3xx { color: var(--accent-blue); }
271
+ .status-code.s4xx { color: var(--accent-yellow); }
272
+ .status-code.s5xx { color: var(--accent-red); }
273
+ .bar-track {
274
+ flex: 1;
275
+ height: 8px;
276
+ background: var(--bg-primary);
277
+ border-radius: 4px;
278
+ overflow: hidden;
279
+ }
280
+ .bar-fill {
281
+ height: 100%;
282
+ border-radius: 4px;
283
+ transition: width 0.5s ease;
284
+ }
285
+ .bar-fill.s2xx { background: var(--gradient-2); }
286
+ .bar-fill.s3xx { background: var(--gradient-4); }
287
+ .bar-fill.s4xx { background: linear-gradient(135deg, #f59e0b, #fbbf24); }
288
+ .bar-fill.s5xx { background: var(--gradient-3); }
289
+ .bar-count {
290
+ font-size: 12px;
291
+ color: var(--text-secondary);
292
+ min-width: 40px;
293
+ text-align: right;
294
+ }
295
+
296
+ /* ── Request Logs Table ──────────── */
297
+ .logs-section {
298
+ background: var(--bg-card);
299
+ border: 1px solid var(--border);
300
+ border-radius: 14px;
301
+ overflow: hidden;
302
+ }
303
+ .logs-header {
304
+ padding: 16px 20px;
305
+ border-bottom: 1px solid var(--border);
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: space-between;
309
+ }
310
+ .logs-title {
311
+ font-size: 14px;
312
+ font-weight: 700;
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 8px;
316
+ }
317
+ .log-filters {
318
+ display: flex;
319
+ gap: 6px;
320
+ }
321
+ .filter-btn {
322
+ background: var(--bg-primary);
323
+ border: 1px solid var(--border);
324
+ color: var(--text-secondary);
325
+ padding: 4px 12px;
326
+ border-radius: 6px;
327
+ cursor: pointer;
328
+ font-size: 12px;
329
+ transition: all 0.2s;
330
+ }
331
+ .filter-btn:hover, .filter-btn.active {
332
+ background: rgba(59, 130, 246, 0.15);
333
+ color: var(--accent-blue);
334
+ border-color: rgba(59, 130, 246, 0.3);
335
+ }
336
+ .logs-table-wrap {
337
+ overflow-x: auto;
338
+ max-height: 450px;
339
+ overflow-y: auto;
340
+ }
341
+ table {
342
+ width: 100%;
343
+ border-collapse: collapse;
344
+ }
345
+ th {
346
+ padding: 10px 16px;
347
+ text-align: left;
348
+ font-size: 11px;
349
+ text-transform: uppercase;
350
+ letter-spacing: 1px;
351
+ color: var(--text-muted);
352
+ font-weight: 600;
353
+ background: var(--bg-secondary);
354
+ border-bottom: 1px solid var(--border);
355
+ position: sticky;
356
+ top: 0;
357
+ z-index: 10;
358
+ }
359
+ td {
360
+ padding: 10px 16px;
361
+ font-size: 13px;
362
+ border-bottom: 1px solid rgba(42, 52, 82, 0.5);
363
+ font-family: 'SF Mono', 'Fira Code', monospace;
364
+ }
365
+ tr:hover td { background: rgba(59, 130, 246, 0.04); }
366
+ tr.slow-row td { background: rgba(239, 68, 68, 0.06); }
367
+ .method-badge {
368
+ display: inline-block;
369
+ padding: 2px 8px;
370
+ border-radius: 4px;
371
+ font-size: 11px;
372
+ font-weight: 700;
373
+ }
374
+ .method-GET { background: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
375
+ .method-POST { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
376
+ .method-PUT { background: rgba(245, 158, 11, 0.15); color: var(--accent-yellow); }
377
+ .method-DELETE { background: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
378
+ .method-PATCH { background: rgba(139, 92, 246, 0.15); color: var(--accent-purple); }
379
+ .time-badge {
380
+ padding: 2px 8px;
381
+ border-radius: 4px;
382
+ font-size: 12px;
383
+ font-weight: 600;
384
+ }
385
+ .time-fast { background: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
386
+ .time-medium { background: rgba(245, 158, 11, 0.15); color: var(--accent-yellow); }
387
+ .time-slow { background: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
388
+ .cache-badge {
389
+ font-size: 11px;
390
+ padding: 2px 8px;
391
+ border-radius: 4px;
392
+ }
393
+ .cache-hit { background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); }
394
+ .cache-miss { color: var(--text-muted); }
395
+ .fire { font-size: 14px; }
396
+ .empty-state {
397
+ padding: 48px 20px;
398
+ text-align: center;
399
+ color: var(--text-muted);
400
+ }
401
+ .empty-state .icon { font-size: 40px; margin-bottom: 12px; }
402
+ .empty-state p { font-size: 14px; }
403
+
404
+ /* ── Routes Table ────────────────── */
405
+ .routes-table td {
406
+ font-family: 'SF Mono', 'Fira Code', monospace;
407
+ font-size: 12px;
408
+ }
409
+ .route-method {
410
+ font-weight: 700;
411
+ min-width: 50px;
412
+ }
413
+
414
+ /* ── Animations ───────────────────── */
415
+ @keyframes fadeIn {
416
+ from { opacity: 0; transform: translateY(8px); }
417
+ to { opacity: 1; transform: translateY(0); }
418
+ }
419
+ .stat-card, .panel, .logs-section {
420
+ animation: fadeIn 0.4s ease both;
421
+ }
422
+ .stat-card:nth-child(2) { animation-delay: 0.05s; }
423
+ .stat-card:nth-child(3) { animation-delay: 0.1s; }
424
+ .stat-card:nth-child(4) { animation-delay: 0.15s; }
425
+ .stat-card:nth-child(5) { animation-delay: 0.2s; }
426
+ </style>
427
+ </head>
428
+ <body>
429
+ <!-- Header -->
430
+ <div class="header">
431
+ <div class="header-left">
432
+ <span style="font-size:22px">⚡</span>
433
+ <h1>Express Performance Dashboard</h1>
434
+ <span class="header-badge">v1.0</span>
435
+ <div class="pulse-dot"></div>
436
+ </div>
437
+ <div class="header-right">
438
+ <span class="uptime" id="uptime">Uptime: --</span>
439
+ <button class="refresh-btn" onclick="fetchMetrics()">↻ Refresh</button>
440
+ </div>
441
+ </div>
442
+
443
+ <div class="container">
444
+ <!-- Stats Cards -->
445
+ <div class="stats-grid">
446
+ <div class="stat-card">
447
+ <div class="stat-label">Total Requests</div>
448
+ <div class="stat-value" id="totalRequests">0</div>
449
+ <div class="stat-sub" id="reqsPerSec">0 req/s</div>
450
+ </div>
451
+ <div class="stat-card">
452
+ <div class="stat-label">Avg Response Time</div>
453
+ <div class="stat-value" id="avgResponseTime">0<span style="font-size:16px;color:var(--text-muted)">ms</span></div>
454
+ </div>
455
+ <div class="stat-card">
456
+ <div class="stat-label">Slow Requests</div>
457
+ <div class="stat-value" id="slowRequests">0</div>
458
+ <div class="stat-sub" id="slowPct">0% of total</div>
459
+ </div>
460
+ <div class="stat-card">
461
+ <div class="stat-label">Cache Hit Rate</div>
462
+ <div class="stat-value" id="cacheHitRate">0<span style="font-size:16px;color:var(--text-muted)">%</span></div>
463
+ <div class="stat-sub" id="cacheEntries">0 entries</div>
464
+ </div>
465
+ <div class="stat-card">
466
+ <div class="stat-label">Status 2xx</div>
467
+ <div class="stat-value" id="status2xx" style="color:var(--accent-green)">0</div>
468
+ <div class="stat-sub" id="errorRate">0% error rate</div>
469
+ </div>
470
+ </div>
471
+
472
+ <!-- Panels Row -->
473
+ <div class="panels">
474
+ <!-- Cache Chart -->
475
+ <div class="panel">
476
+ <div class="panel-header">
477
+ <span class="panel-title">📊 Cache Performance</span>
478
+ </div>
479
+ <div class="panel-body">
480
+ <div class="cache-chart">
481
+ <div class="donut-container">
482
+ <svg viewBox="0 0 42 42" width="130" height="130">
483
+ <circle cx="21" cy="21" r="16" fill="none" stroke="var(--bg-primary)" stroke-width="5"/>
484
+ <circle id="donutHit" cx="21" cy="21" r="16" fill="none" stroke="url(#gradHit)"
485
+ stroke-width="5" stroke-dasharray="0 100" stroke-dashoffset="25"
486
+ stroke-linecap="round" style="transition: stroke-dasharray 0.8s ease"/>
487
+ <circle id="donutMiss" cx="21" cy="21" r="16" fill="none" stroke="#ef4444"
488
+ stroke-width="5" stroke-dasharray="0 100" stroke-dashoffset="25"
489
+ stroke-linecap="round" style="transition: all 0.8s ease"/>
490
+ <defs>
491
+ <linearGradient id="gradHit" x1="0" y1="0" x2="1" y2="1">
492
+ <stop offset="0%" stop-color="#06b6d4"/>
493
+ <stop offset="100%" stop-color="#10b981"/>
494
+ </linearGradient>
495
+ </defs>
496
+ </svg>
497
+ <div class="donut-center">
498
+ <div class="pct" id="donutPct">0%</div>
499
+ <div class="lbl">Hit Rate</div>
500
+ </div>
501
+ </div>
502
+ <div class="cache-legend">
503
+ <div class="legend-item">
504
+ <div class="legend-dot" style="background:var(--accent-cyan)"></div>
505
+ <span>Hits: <strong id="cacheHitsLeg">0</strong></span>
506
+ </div>
507
+ <div class="legend-item">
508
+ <div class="legend-dot" style="background:var(--accent-red)"></div>
509
+ <span>Misses: <strong id="cacheMissesLeg">0</strong></span>
510
+ </div>
511
+ <div class="legend-item">
512
+ <div class="legend-dot" style="background:var(--text-muted)"></div>
513
+ <span>Cached: <strong id="cacheSizeLeg">0</strong> entries</span>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <!-- Status Codes -->
521
+ <div class="panel">
522
+ <div class="panel-header">
523
+ <span class="panel-title">📈 Status Codes</span>
524
+ </div>
525
+ <div class="panel-body">
526
+ <div class="status-bars" id="statusBars">
527
+ <div class="empty-state"><p style="color:var(--text-muted)">No data yet</p></div>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ </div>
532
+
533
+ <!-- Slow Routes Panel -->
534
+ <div class="panels" style="margin-bottom:28px">
535
+ <div class="panel" style="grid-column: 1 / -1">
536
+ <div class="panel-header">
537
+ <span class="panel-title">🔥 Slowest Routes</span>
538
+ </div>
539
+ <div class="panel-body">
540
+ <table class="routes-table">
541
+ <thead>
542
+ <tr>
543
+ <th>Route</th>
544
+ <th>Requests</th>
545
+ <th>Avg Time</th>
546
+ <th>Slow Count</th>
547
+ </tr>
548
+ </thead>
549
+ <tbody id="routesBody">
550
+ <tr><td colspan="4" class="empty-state"><p>No data yet</p></td></tr>
551
+ </tbody>
552
+ </table>
553
+ </div>
554
+ </div>
555
+ </div>
556
+
557
+ <!-- Request Logs -->
558
+ <div class="logs-section">
559
+ <div class="logs-header">
560
+ <span class="logs-title">📋 Request Log</span>
561
+ <div class="log-filters">
562
+ <button class="filter-btn active" onclick="setFilter('all')">All</button>
563
+ <button class="filter-btn" onclick="setFilter('slow')">🔥 Slow</button>
564
+ <button class="filter-btn" onclick="setFilter('cached')">Cached</button>
565
+ <button class="filter-btn" onclick="setFilter('errors')">Errors</button>
566
+ </div>
567
+ </div>
568
+ <div class="logs-table-wrap">
569
+ <table>
570
+ <thead>
571
+ <tr>
572
+ <th>Time</th>
573
+ <th>Method</th>
574
+ <th>Path</th>
575
+ <th>Status</th>
576
+ <th>Duration</th>
577
+ <th>Cache</th>
578
+ <th>Flag</th>
579
+ </tr>
580
+ </thead>
581
+ <tbody id="logsBody">
582
+ <tr><td colspan="7" class="empty-state">
583
+ <div class="icon">📡</div>
584
+ <p>Waiting for requests...</p>
585
+ </td></tr>
586
+ </tbody>
587
+ </table>
588
+ </div>
589
+ </div>
590
+ </div>
591
+
592
+ <script>
593
+ let currentFilter = 'all';
594
+ let metricsData = null;
595
+ const API_URL = window.location.pathname.replace(/\/$/, '') + '/api/metrics';
596
+
597
+ async function fetchMetrics() {
598
+ try {
599
+ const res = await fetch(API_URL);
600
+ metricsData = await res.json();
601
+ render(metricsData);
602
+ } catch (e) {
603
+ console.error('Failed to fetch metrics:', e);
604
+ }
605
+ }
606
+
607
+ function formatUptime(ms) {
608
+ const s = Math.floor(ms / 1000);
609
+ const m = Math.floor(s / 60);
610
+ const h = Math.floor(m / 60);
611
+ const d = Math.floor(h / 24);
612
+ if (d > 0) return d + 'd ' + (h % 24) + 'h';
613
+ if (h > 0) return h + 'h ' + (m % 60) + 'm';
614
+ if (m > 0) return m + 'm ' + (s % 60) + 's';
615
+ return s + 's';
616
+ }
617
+
618
+ function formatTime(ts) {
619
+ return new Date(ts).toLocaleTimeString();
620
+ }
621
+
622
+ function getTimeClass(ms) {
623
+ if (ms < 200) return 'time-fast';
624
+ if (ms < 1000) return 'time-medium';
625
+ return 'time-slow';
626
+ }
627
+
628
+ function getStatusClass(code) {
629
+ if (code < 300) return 's2xx';
630
+ if (code < 400) return 's3xx';
631
+ if (code < 500) return 's4xx';
632
+ return 's5xx';
633
+ }
634
+
635
+ function render(m) {
636
+ // Stats
637
+ document.getElementById('totalRequests').textContent = m.totalRequests.toLocaleString();
638
+ const rps = m.uptime > 0 ? (m.totalRequests / (m.uptime / 1000)).toFixed(1) : '0';
639
+ document.getElementById('reqsPerSec').textContent = rps + ' req/s';
640
+ document.getElementById('avgResponseTime').innerHTML = m.avgResponseTime + '<span style="font-size:16px;color:var(--text-muted)">ms</span>';
641
+ document.getElementById('slowRequests').textContent = m.slowRequests;
642
+ const slowPct = m.totalRequests > 0 ? ((m.slowRequests / m.totalRequests) * 100).toFixed(1) : '0';
643
+ document.getElementById('slowPct').textContent = slowPct + '% of total';
644
+ document.getElementById('cacheHitRate').innerHTML = m.cacheHitRate + '<span style="font-size:16px;color:var(--text-muted)">%</span>';
645
+ document.getElementById('cacheEntries').textContent = m.cacheSize + ' entries';
646
+ document.getElementById('uptime').textContent = 'Uptime: ' + formatUptime(m.uptime);
647
+
648
+ // 2xx count
649
+ let s2 = 0, sErr = 0;
650
+ Object.entries(m.statusCodes).forEach(([code, count]) => {
651
+ if (code < 300) s2 += count;
652
+ if (code >= 400) sErr += count;
653
+ });
654
+ document.getElementById('status2xx').textContent = s2;
655
+ const errRate = m.totalRequests > 0 ? ((sErr / m.totalRequests) * 100).toFixed(1) : '0';
656
+ document.getElementById('errorRate').textContent = errRate + '% error rate';
657
+
658
+ // Cache donut
659
+ const hitPct = m.cacheHitRate;
660
+ const missPct = 100 - hitPct;
661
+ const circ = 2 * Math.PI * 16;
662
+ const hitLen = (hitPct / 100) * 100.53;
663
+ const missLen = (missPct / 100) * 100.53;
664
+ const donutHit = document.getElementById('donutHit');
665
+ const donutMiss = document.getElementById('donutMiss');
666
+ donutHit.setAttribute('stroke-dasharray', hitLen + ' ' + (100.53 - hitLen));
667
+ const missOffset = 25 - hitLen;
668
+ donutMiss.setAttribute('stroke-dasharray', missLen + ' ' + (100.53 - missLen));
669
+ donutMiss.setAttribute('stroke-dashoffset', missOffset);
670
+ document.getElementById('donutPct').textContent = hitPct + '%';
671
+ document.getElementById('cacheHitsLeg').textContent = m.cacheHits;
672
+ document.getElementById('cacheMissesLeg').textContent = m.cacheMisses;
673
+ document.getElementById('cacheSizeLeg').textContent = m.cacheSize;
674
+
675
+ // Status codes bars
676
+ const statusEntries = Object.entries(m.statusCodes).sort(([a], [b]) => a - b);
677
+ const maxCount = Math.max(...statusEntries.map(([, c]) => c), 1);
678
+ if (statusEntries.length > 0) {
679
+ document.getElementById('statusBars').innerHTML = statusEntries.map(([code, count]) => {
680
+ const cls = getStatusClass(parseInt(code));
681
+ const pct = (count / maxCount * 100).toFixed(0);
682
+ return `<div class="status-bar-row">
683
+ <span class="status-code ${cls}">${code}</span>
684
+ <div class="bar-track"><div class="bar-fill ${cls}" style="width:${pct}%"></div></div>
685
+ <span class="bar-count">${count}</span>
686
+ </div>`;
687
+ }).join('');
688
+ }
689
+
690
+ // Routes
691
+ const routeEntries = Object.entries(m.routes)
692
+ .sort(([,a], [,b]) => b.avgTime - a.avgTime)
693
+ .slice(0, 10);
694
+ if (routeEntries.length > 0) {
695
+ document.getElementById('routesBody').innerHTML = routeEntries.map(([route, stats]) => {
696
+ const slow = stats.slowCount > 0 ? '🔥' : '';
697
+ const timeClass = getTimeClass(stats.avgTime);
698
+ return `<tr>
699
+ <td>${route}</td>
700
+ <td>${stats.count}</td>
701
+ <td><span class="time-badge ${timeClass}">${stats.avgTime}ms</span></td>
702
+ <td>${slow} ${stats.slowCount}</td>
703
+ </tr>`;
704
+ }).join('');
705
+ }
706
+
707
+ // Logs
708
+ renderLogs(m.recentLogs);
709
+ }
710
+
711
+ function renderLogs(logs) {
712
+ let filtered = logs;
713
+ if (currentFilter === 'slow') filtered = logs.filter(l => l.slow);
714
+ else if (currentFilter === 'cached') filtered = logs.filter(l => l.cached);
715
+ else if (currentFilter === 'errors') filtered = logs.filter(l => l.statusCode >= 400);
716
+
717
+ if (filtered.length === 0) {
718
+ document.getElementById('logsBody').innerHTML = `<tr><td colspan="7" class="empty-state">
719
+ <div class="icon">📡</div><p>No matching requests</p></td></tr>`;
720
+ return;
721
+ }
722
+
723
+ document.getElementById('logsBody').innerHTML = filtered
724
+ .sort((a, b) => b.timestamp - a.timestamp)
725
+ .map(log => {
726
+ const rowClass = log.slow ? 'slow-row' : '';
727
+ const timeClass = getTimeClass(log.responseTime);
728
+ const cacheBadge = log.cached
729
+ ? '<span class="cache-badge cache-hit">HIT</span>'
730
+ : '<span class="cache-badge cache-miss">MISS</span>';
731
+ const flag = log.slow ? '<span class="fire">🔥</span>' : '';
732
+ return `<tr class="${rowClass}">
733
+ <td>${formatTime(log.timestamp)}</td>
734
+ <td><span class="method-badge method-${log.method}">${log.method}</span></td>
735
+ <td>${log.path}</td>
736
+ <td>${log.statusCode}</td>
737
+ <td><span class="time-badge ${timeClass}">${log.responseTime}ms</span></td>
738
+ <td>${cacheBadge}</td>
739
+ <td>${flag}</td>
740
+ </tr>`;
741
+ }).join('');
742
+ }
743
+
744
+ function setFilter(f) {
745
+ currentFilter = f;
746
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
747
+ event.target.classList.add('active');
748
+ if (metricsData) renderLogs(metricsData.recentLogs);
749
+ }
750
+
751
+ // Auto-refresh every 3 seconds
752
+ fetchMetrics();
753
+ setInterval(fetchMetrics, 3000);
754
+ </script>
755
+ </body>
756
+ </html>