@ngn-net/nestjs-telescope 0.1.6 → 0.1.8

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 (37) hide show
  1. package/README.md +42 -34
  2. package/dist/constants.d.ts +12 -3
  3. package/dist/constants.js +14 -5
  4. package/dist/controllers/telescope.controller.d.ts +5 -2
  5. package/dist/controllers/telescope.controller.js +41 -13
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.js +12 -0
  8. package/dist/interfaces/telescope-options.interface.d.ts +9 -3
  9. package/dist/storage/entities/telescope-entry.entity.js +34 -7
  10. package/dist/storage/telescope-repository.service.d.ts +17 -7
  11. package/dist/storage/telescope-repository.service.js +42 -17
  12. package/dist/telescope.module.d.ts +6 -2
  13. package/dist/telescope.module.js +148 -44
  14. package/dist/telescope.service.d.ts +21 -1
  15. package/dist/telescope.service.js +72 -22
  16. package/dist/ui/index.html +1635 -0
  17. package/dist/watchers/cache.watcher.d.ts +5 -4
  18. package/dist/watchers/cache.watcher.js +50 -34
  19. package/dist/watchers/event.watcher.d.ts +7 -3
  20. package/dist/watchers/event.watcher.js +22 -6
  21. package/dist/watchers/exception.watcher.js +7 -1
  22. package/dist/watchers/http-request.watcher.d.ts +3 -1
  23. package/dist/watchers/http-request.watcher.js +25 -8
  24. package/dist/watchers/log.watcher.d.ts +1 -0
  25. package/dist/watchers/log.watcher.js +22 -17
  26. package/dist/watchers/mail.watcher.d.ts +1 -1
  27. package/dist/watchers/mail.watcher.js +28 -56
  28. package/dist/watchers/query.watcher.d.ts +6 -3
  29. package/dist/watchers/query.watcher.js +36 -8
  30. package/dist/watchers/queue.watcher.d.ts +2 -4
  31. package/dist/watchers/queue.watcher.js +38 -32
  32. package/dist/watchers/redis.watcher.d.ts +3 -1
  33. package/dist/watchers/redis.watcher.js +16 -4
  34. package/dist/watchers/schedule.watcher.d.ts +8 -3
  35. package/dist/watchers/schedule.watcher.js +25 -11
  36. package/package.json +51 -19
  37. package/ui/index.html +602 -235
package/ui/index.html CHANGED
@@ -4,28 +4,31 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>NestJS Telescope Dashboard</title>
7
- <!-- Google Fonts: Inter & JetBrains Mono -->
7
+ <!-- Google Fonts: Outfit (Premium Sans) & JetBrains Mono -->
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
11
 
12
12
  <style>
13
13
  :root {
14
- --bg-main: #0b0f19;
15
- --bg-sidebar: #111827;
16
- --bg-card: rgba(17, 24, 39, 0.7);
17
- --bg-input: #1f2937;
18
- --border-color: rgba(255, 255, 255, 0.08);
19
- --text-main: #f3f4f6;
20
- --text-muted: #9ca3af;
14
+ --bg-main: #060913;
15
+ --bg-sidebar: rgba(10, 15, 30, 0.7);
16
+ --bg-card: rgba(16, 22, 42, 0.65);
17
+ --bg-card-hover: rgba(22, 30, 58, 0.85);
18
+ --bg-input: rgba(30, 41, 73, 0.5);
19
+ --border-color: rgba(255, 255, 255, 0.07);
20
+ --border-focus: rgba(99, 102, 241, 0.4);
21
+ --text-main: #f1f5f9;
22
+ --text-muted: #94a3b8;
21
23
  --primary: #6366f1;
22
24
  --primary-hover: #4f46e5;
25
+ --primary-glow: rgba(99, 102, 241, 0.3);
23
26
  --accent-green: #10b981;
24
- --accent-red: #ef4444;
27
+ --accent-red: #f43f5e;
25
28
  --accent-blue: #3b82f6;
26
29
  --accent-purple: #8b5cf6;
27
30
  --accent-amber: #f59e0b;
28
- --font-sans: 'Inter', sans-serif;
31
+ --font-sans: 'Outfit', sans-serif;
29
32
  --font-mono: 'JetBrains Mono', monospace;
30
33
  }
31
34
 
@@ -42,6 +45,9 @@
42
45
  overflow: hidden;
43
46
  height: 100vh;
44
47
  display: flex;
48
+ background-image:
49
+ radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.05) 0%, transparent 40%),
50
+ radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.05) 0%, transparent 40%);
45
51
  }
46
52
 
47
53
  /* Scrollbars */
@@ -50,7 +56,7 @@
50
56
  height: 6px;
51
57
  }
52
58
  ::-webkit-scrollbar-track {
53
- background: rgba(0, 0, 0, 0.1);
59
+ background: rgba(0, 0, 0, 0.2);
54
60
  }
55
61
  ::-webkit-scrollbar-thumb {
56
62
  background: rgba(255, 255, 255, 0.1);
@@ -66,14 +72,17 @@
66
72
  display: flex;
67
73
  }
68
74
 
69
- /* Sidebar */
75
+ /* Sidebar with Glassmorphism */
70
76
  .sidebar {
71
77
  width: 260px;
72
- background-color: var(--bg-sidebar);
78
+ background: var(--bg-sidebar);
79
+ backdrop-filter: blur(12px);
80
+ -webkit-backdrop-filter: blur(12px);
73
81
  border-right: 1px solid var(--border-color);
74
82
  display: flex;
75
83
  flex-direction: column;
76
84
  flex-shrink: 0;
85
+ z-index: 10;
77
86
  }
78
87
 
79
88
  .sidebar-header {
@@ -85,24 +94,24 @@
85
94
  }
86
95
 
87
96
  .logo {
88
- background: linear-gradient(135deg, var(--primary), #a78bfa);
89
- width: 36px;
90
- height: 36px;
91
- border-radius: 8px;
97
+ background: linear-gradient(135deg, var(--primary), #8b5cf6);
98
+ width: 38px;
99
+ height: 38px;
100
+ border-radius: 10px;
92
101
  display: flex;
93
102
  align-items: center;
94
103
  justify-content: center;
95
104
  font-weight: 700;
96
105
  color: #fff;
97
- font-size: 1.2rem;
98
- box-shadow: 0 0 15px rgba(99, 102, 241, 0.4);
106
+ font-size: 1.3rem;
107
+ box-shadow: 0 0 20px var(--primary-glow);
99
108
  }
100
109
 
101
110
  .sidebar-title {
102
- font-weight: 600;
103
- font-size: 1.1rem;
104
- letter-spacing: -0.025em;
105
- background: linear-gradient(to right, #fff, #9ca3af);
111
+ font-weight: 700;
112
+ font-size: 1.2rem;
113
+ letter-spacing: -0.02em;
114
+ background: linear-gradient(to right, #ffffff, #cbd5e1);
106
115
  -webkit-background-clip: text;
107
116
  -webkit-text-fill-color: transparent;
108
117
  }
@@ -112,7 +121,7 @@
112
121
  padding: 16px 12px;
113
122
  display: flex;
114
123
  flex-direction: column;
115
- gap: 4px;
124
+ gap: 6px;
116
125
  overflow-y: auto;
117
126
  flex-grow: 1;
118
127
  }
@@ -120,38 +129,58 @@
120
129
  .nav-item {
121
130
  display: flex;
122
131
  align-items: center;
123
- gap: 12px;
132
+ justify-content: space-between;
124
133
  padding: 10px 16px;
125
134
  border-radius: 8px;
126
135
  cursor: pointer;
127
136
  color: var(--text-muted);
128
137
  font-weight: 500;
129
- font-size: 0.9rem;
130
- transition: all 0.2s ease;
138
+ font-size: 0.92rem;
139
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
131
140
  text-decoration: none;
132
141
  }
133
142
 
134
143
  .nav-item:hover {
135
144
  color: var(--text-main);
136
- background-color: rgba(255, 255, 255, 0.03);
145
+ background-color: rgba(255, 255, 255, 0.04);
146
+ transform: translateX(4px);
137
147
  }
138
148
 
139
149
  .nav-item.active {
140
150
  color: #fff;
141
- background-color: rgba(99, 102, 241, 0.15);
151
+ background: linear-gradient(90deg, rgba(99, 102, 241, 0.15) 0%, rgba(99, 102, 241, 0.02) 100%);
142
152
  border-left: 3px solid var(--primary);
153
+ box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.05);
143
154
  }
144
155
 
145
- .nav-icon {
146
- width: 18px;
147
- height: 18px;
156
+ .nav-item-left {
148
157
  display: flex;
149
158
  align-items: center;
150
- justify-content: center;
159
+ gap: 12px;
160
+ }
161
+
162
+ .nav-icon {
163
+ font-size: 1.1rem;
164
+ width: 20px;
165
+ text-align: center;
166
+ }
167
+
168
+ .nav-count {
169
+ background-color: rgba(255, 255, 255, 0.07);
170
+ color: var(--text-muted);
171
+ font-size: 0.75rem;
172
+ padding: 2px 6px;
173
+ border-radius: 20px;
174
+ font-weight: 600;
175
+ }
176
+
177
+ .nav-item.active .nav-count {
178
+ background-color: var(--primary);
179
+ color: #fff;
151
180
  }
152
181
 
153
182
  .sidebar-footer {
154
- padding: 16px;
183
+ padding: 16px 20px;
155
184
  border-top: 1px solid var(--border-color);
156
185
  display: flex;
157
186
  align-items: center;
@@ -165,27 +194,30 @@
165
194
  border: none;
166
195
  color: var(--accent-red);
167
196
  cursor: pointer;
168
- font-weight: 500;
197
+ font-weight: 600;
198
+ font-family: var(--font-sans);
199
+ transition: opacity 0.2s;
169
200
  }
170
201
 
171
202
  .logout-btn:hover {
203
+ opacity: 0.8;
172
204
  text-decoration: underline;
173
205
  }
174
206
 
175
- /* Main Content */
207
+ /* Main Container */
176
208
  .main-container {
177
209
  flex-grow: 1;
178
210
  display: flex;
179
211
  flex-direction: column;
180
212
  overflow: hidden;
181
- background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.05), transparent 600px);
213
+ background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.03), transparent 700px);
182
214
  }
183
215
 
184
216
  /* Header */
185
217
  .header {
186
- height: 70px;
218
+ height: 72px;
187
219
  border-bottom: 1px solid var(--border-color);
188
- padding: 0 24px;
220
+ padding: 0 28px;
189
221
  display: flex;
190
222
  align-items: center;
191
223
  justify-content: space-between;
@@ -195,12 +227,13 @@
195
227
  .header-left {
196
228
  display: flex;
197
229
  align-items: center;
198
- gap: 16px;
230
+ gap: 20px;
199
231
  }
200
232
 
201
233
  .view-title {
202
- font-size: 1.25rem;
203
- font-weight: 600;
234
+ font-size: 1.35rem;
235
+ font-weight: 700;
236
+ letter-spacing: -0.01em;
204
237
  }
205
238
 
206
239
  .header-right {
@@ -214,16 +247,17 @@
214
247
  border: 1px solid var(--border-color);
215
248
  border-radius: 8px;
216
249
  color: var(--text-main);
217
- padding: 8px 16px;
250
+ padding: 9px 16px;
218
251
  font-size: 0.875rem;
252
+ font-family: var(--font-sans);
219
253
  outline: none;
220
- width: 240px;
254
+ width: 260px;
221
255
  transition: all 0.2s ease;
222
256
  }
223
257
 
224
258
  .search-input:focus {
225
- border-color: var(--primary);
226
- box-shadow: 0 0 8px rgba(99, 102, 241, 0.2);
259
+ border-color: var(--border-focus);
260
+ box-shadow: 0 0 12px var(--primary-glow);
227
261
  }
228
262
 
229
263
  .btn {
@@ -231,9 +265,10 @@
231
265
  color: #fff;
232
266
  border: none;
233
267
  border-radius: 8px;
234
- padding: 8px 16px;
268
+ padding: 9px 18px;
235
269
  font-size: 0.875rem;
236
- font-weight: 500;
270
+ font-weight: 600;
271
+ font-family: var(--font-sans);
237
272
  cursor: pointer;
238
273
  transition: all 0.2s ease;
239
274
  display: flex;
@@ -243,12 +278,17 @@
243
278
 
244
279
  .btn:hover {
245
280
  background-color: var(--primary-hover);
281
+ transform: translateY(-1px);
282
+ }
283
+
284
+ .btn:active {
285
+ transform: translateY(0);
246
286
  }
247
287
 
248
288
  .btn-danger {
249
- background-color: rgba(239, 68, 68, 0.1);
289
+ background-color: rgba(244, 63, 94, 0.1);
250
290
  color: var(--accent-red);
251
- border: 1px solid rgba(239, 68, 68, 0.2);
291
+ border: 1px solid rgba(244, 63, 94, 0.2);
252
292
  }
253
293
 
254
294
  .btn-danger:hover {
@@ -263,13 +303,14 @@
263
303
  font-size: 0.85rem;
264
304
  color: var(--text-muted);
265
305
  cursor: pointer;
306
+ user-select: none;
266
307
  }
267
308
 
268
309
  .switch {
269
310
  position: relative;
270
311
  display: inline-block;
271
- width: 36px;
272
- height: 20px;
312
+ width: 38px;
313
+ height: 22px;
273
314
  }
274
315
 
275
316
  .switch input {
@@ -286,7 +327,7 @@
286
327
  right: 0;
287
328
  bottom: 0;
288
329
  background-color: var(--bg-input);
289
- transition: .3s;
330
+ transition: .2s;
290
331
  border-radius: 34px;
291
332
  border: 1px solid var(--border-color);
292
333
  }
@@ -294,17 +335,18 @@
294
335
  .slider:before {
295
336
  position: absolute;
296
337
  content: "";
297
- height: 12px;
298
- width: 12px;
338
+ height: 14px;
339
+ width: 14px;
299
340
  left: 3px;
300
341
  bottom: 3px;
301
342
  background-color: var(--text-muted);
302
- transition: .3s;
343
+ transition: .2s;
303
344
  border-radius: 50%;
304
345
  }
305
346
 
306
347
  input:checked + .slider {
307
348
  background-color: var(--primary);
349
+ border-color: transparent;
308
350
  }
309
351
 
310
352
  input:checked + .slider:before {
@@ -316,19 +358,70 @@
316
358
  .content-area {
317
359
  flex-grow: 1;
318
360
  overflow-y: auto;
319
- padding: 24px;
361
+ padding: 28px;
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 24px;
365
+ }
366
+
367
+ /* Top Stats Grid */
368
+ .stats-grid {
369
+ display: grid;
370
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
371
+ gap: 16px;
372
+ margin-bottom: 4px;
373
+ }
374
+
375
+ .stat-card {
376
+ background: var(--bg-card);
377
+ border: 1px solid var(--border-color);
378
+ border-radius: 12px;
379
+ padding: 16px;
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 6px;
383
+ cursor: pointer;
384
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
385
+ }
386
+
387
+ .stat-card:hover {
388
+ background: var(--bg-card-hover);
389
+ border-color: rgba(255, 255, 255, 0.15);
390
+ transform: translateY(-2px);
391
+ }
392
+
393
+ .stat-card.active {
394
+ border-color: var(--primary);
395
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(16, 22, 42, 0.8) 100%);
396
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.05);
397
+ }
398
+
399
+ .stat-header {
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: space-between;
403
+ color: var(--text-muted);
404
+ font-size: 0.8rem;
405
+ font-weight: 600;
406
+ }
407
+
408
+ .stat-value {
409
+ font-size: 1.6rem;
410
+ font-weight: 700;
411
+ color: #fff;
320
412
  }
321
413
 
322
- /* Cards & Glassmorphism */
414
+ /* Card & Table View */
323
415
  .card {
324
- background-color: var(--bg-card);
416
+ background: var(--bg-card);
325
417
  backdrop-filter: blur(10px);
418
+ -webkit-backdrop-filter: blur(10px);
326
419
  border: 1px solid var(--border-color);
327
- border-radius: 12px;
420
+ border-radius: 14px;
328
421
  overflow: hidden;
422
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
329
423
  }
330
424
 
331
- /* List Table */
332
425
  .table-container {
333
426
  overflow-x: auto;
334
427
  }
@@ -340,21 +433,22 @@
340
433
  }
341
434
 
342
435
  th {
343
- padding: 14px 20px;
344
- font-size: 0.75rem;
436
+ padding: 16px 24px;
437
+ font-size: 0.78rem;
345
438
  text-transform: uppercase;
346
- letter-spacing: 0.05em;
439
+ letter-spacing: 0.06em;
347
440
  color: var(--text-muted);
348
441
  border-bottom: 1px solid var(--border-color);
349
- font-weight: 600;
442
+ font-weight: 700;
443
+ background-color: rgba(10, 15, 30, 0.3);
350
444
  }
351
445
 
352
446
  td {
353
- padding: 14px 20px;
354
- font-size: 0.875rem;
447
+ padding: 16px 24px;
448
+ font-size: 0.9rem;
355
449
  border-bottom: 1px solid var(--border-color);
356
450
  color: var(--text-main);
357
- max-width: 400px;
451
+ max-width: 450px;
358
452
  white-space: nowrap;
359
453
  overflow: hidden;
360
454
  text-overflow: ellipsis;
@@ -366,42 +460,59 @@
366
460
 
367
461
  tr {
368
462
  cursor: pointer;
369
- transition: background-color 0.2s ease;
463
+ transition: background-color 0.15s ease;
370
464
  }
371
465
 
372
466
  tr:hover {
373
- background-color: rgba(255, 255, 255, 0.02);
467
+ background-color: rgba(255, 255, 255, 0.015);
468
+ }
469
+
470
+ tr.selected {
471
+ background-color: rgba(99, 102, 241, 0.07);
374
472
  }
375
473
 
376
- /* Tags */
474
+ /* Badges */
377
475
  .badge {
378
476
  display: inline-flex;
379
477
  align-items: center;
380
478
  padding: 4px 8px;
381
479
  border-radius: 6px;
382
- font-size: 0.75rem;
383
- font-weight: 600;
384
- letter-spacing: 0.025em;
480
+ font-size: 0.72rem;
481
+ font-weight: 700;
482
+ letter-spacing: 0.03em;
385
483
  }
386
484
 
387
485
  .badge-method {
388
486
  text-transform: uppercase;
389
487
  }
488
+
489
+ .badge-request { background-color: rgba(59, 130, 246, 0.12); color: var(--accent-blue); }
490
+ .badge-query { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
491
+ .badge-cache { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
492
+ .badge-job { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
493
+ .badge-event { background-color: rgba(59, 130, 246, 0.12); color: var(--accent-blue); }
494
+ .badge-mail { background-color: rgba(16, 185, 129, 0.12); color: var(--accent-green); }
495
+ .badge-log { background-color: rgba(148, 163, 184, 0.15); color: var(--text-main); }
496
+ .badge-exception { background-color: rgba(244, 63, 94, 0.12); color: var(--accent-red); }
497
+ .badge-scheduled_task { background-color: rgba(245, 158, 11, 0.12); color: var(--accent-amber); }
498
+ .badge-redis { background-color: rgba(139, 92, 246, 0.12); color: var(--accent-purple); }
499
+
390
500
  .badge-get { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
391
501
  .badge-post { background-color: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
392
502
  .badge-put { background-color: rgba(245, 158, 11, 0.15); color: var(--accent-amber); }
393
- .badge-delete { background-color: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
503
+ .badge-delete { background-color: rgba(244, 63, 94, 0.15); color: var(--accent-red); }
504
+
394
505
  .badge-status-ok { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
395
- .badge-status-err { background-color: rgba(239, 68, 68, 0.15); color: var(--accent-red); }
506
+ .badge-status-err { background-color: rgba(244, 63, 94, 0.15); color: var(--accent-red); }
396
507
 
397
- /* Login Modal */
508
+ /* Animated Login Screen */
398
509
  .login-overlay {
399
510
  position: fixed;
400
511
  top: 0;
401
512
  left: 0;
402
513
  right: 0;
403
514
  bottom: 0;
404
- background-color: rgba(11, 15, 25, 0.95);
515
+ background: radial-gradient(circle at center, #0c122c 0%, #050714 100%);
405
516
  display: flex;
406
517
  align-items: center;
407
518
  justify-content: center;
@@ -409,16 +520,24 @@
409
520
  }
410
521
 
411
522
  .login-card {
412
- width: 380px;
413
- background-color: var(--bg-sidebar);
414
- border: 1px solid var(--border-color);
415
- border-radius: 16px;
416
- padding: 32px;
417
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
523
+ width: 400px;
524
+ background: rgba(13, 20, 43, 0.7);
525
+ backdrop-filter: blur(20px);
526
+ -webkit-backdrop-filter: blur(20px);
527
+ border: 1px solid rgba(255, 255, 255, 0.08);
528
+ border-radius: 20px;
529
+ padding: 40px;
530
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 40px rgba(99, 102, 241, 0.1);
418
531
  display: flex;
419
532
  flex-direction: column;
420
- gap: 24px;
533
+ gap: 28px;
421
534
  text-align: center;
535
+ animation: loginFloat 6s ease-in-out infinite;
536
+ }
537
+
538
+ @keyframes loginFloat {
539
+ 0%, 100% { transform: translateY(0); }
540
+ 50% { transform: translateY(-8px); }
422
541
  }
423
542
 
424
543
  .login-logo-wrapper {
@@ -426,13 +545,28 @@
426
545
  justify-content: center;
427
546
  }
428
547
 
548
+ .login-logo {
549
+ background: linear-gradient(135deg, var(--primary), #8b5cf6);
550
+ width: 60px;
551
+ height: 60px;
552
+ border-radius: 16px;
553
+ display: flex;
554
+ align-items: center;
555
+ justify-content: center;
556
+ font-weight: 800;
557
+ color: #fff;
558
+ font-size: 2rem;
559
+ box-shadow: 0 0 30px rgba(99, 102, 241, 0.4);
560
+ }
561
+
429
562
  .login-input {
430
- background-color: var(--bg-main);
563
+ background-color: var(--bg-input);
431
564
  border: 1px solid var(--border-color);
432
565
  border-radius: 8px;
433
566
  color: var(--text-main);
434
- padding: 12px 16px;
567
+ padding: 14px;
435
568
  font-size: 1rem;
569
+ font-family: var(--font-sans);
436
570
  outline: none;
437
571
  width: 100%;
438
572
  text-align: center;
@@ -440,60 +574,65 @@
440
574
  }
441
575
 
442
576
  .login-input:focus {
443
- border-color: var(--primary);
577
+ border-color: var(--border-focus);
578
+ box-shadow: 0 0 12px var(--primary-glow);
444
579
  }
445
580
 
446
581
  .error-msg {
447
582
  color: var(--accent-red);
448
583
  font-size: 0.85rem;
584
+ font-weight: 500;
449
585
  }
450
586
 
451
- /* Detail Modal */
587
+ /* Modal Styles with Blur & Entrance animation */
452
588
  .modal-overlay {
453
589
  position: fixed;
454
590
  top: 0;
455
591
  left: 0;
456
592
  right: 0;
457
593
  bottom: 0;
458
- background-color: rgba(0, 0, 0, 0.7);
459
- backdrop-filter: blur(4px);
594
+ background-color: rgba(2, 4, 12, 0.75);
595
+ backdrop-filter: blur(8px);
596
+ -webkit-backdrop-filter: blur(8px);
460
597
  display: flex;
461
598
  align-items: center;
462
599
  justify-content: center;
463
600
  z-index: 999;
464
- padding: 40px;
601
+ padding: 30px;
465
602
  }
466
603
 
467
604
  .modal-card {
468
605
  width: 100%;
469
- max-width: 900px;
470
- height: 90%;
471
- background-color: var(--bg-sidebar);
606
+ max-width: 950px;
607
+ height: 85%;
608
+ background-color: #0b1226;
472
609
  border: 1px solid var(--border-color);
473
- border-radius: 16px;
610
+ border-radius: 18px;
474
611
  display: flex;
475
612
  flex-direction: column;
476
613
  overflow: hidden;
477
- animation: modalFadeIn 0.2s ease-out;
614
+ animation: modalEntrance 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
615
+ box-shadow: 0 25px 60px rgba(0,0,0,0.4), 0 0 2px rgba(255,255,255,0.1);
478
616
  }
479
617
 
480
- @keyframes modalFadeIn {
481
- from { transform: scale(0.95); opacity: 0; }
482
- to { transform: scale(1); opacity: 1; }
618
+ @keyframes modalEntrance {
619
+ from { transform: scale(0.96) translateY(10px); opacity: 0; }
620
+ to { transform: scale(1) translateY(0); opacity: 1; }
483
621
  }
484
622
 
485
623
  .modal-header {
486
- padding: 20px 24px;
624
+ padding: 22px 28px;
487
625
  border-bottom: 1px solid var(--border-color);
488
626
  display: flex;
489
627
  align-items: center;
490
628
  justify-content: space-between;
629
+ background-color: rgba(0, 0, 0, 0.15);
491
630
  }
492
631
 
493
632
  .modal-title-group {
494
633
  display: flex;
495
634
  align-items: center;
496
- gap: 12px;
635
+ gap: 16px;
497
636
  }
498
637
 
499
638
  .modal-close {
@@ -501,12 +640,12 @@
501
640
  border: none;
502
641
  color: var(--text-muted);
503
642
  cursor: pointer;
504
- font-size: 1.5rem;
643
+ font-size: 1.8rem;
505
644
  display: flex;
506
645
  align-items: center;
507
646
  justify-content: center;
508
- width: 32px;
509
- height: 32px;
647
+ width: 36px;
648
+ height: 36px;
510
649
  border-radius: 50%;
511
650
  transition: all 0.2s ease;
512
651
  }
@@ -514,23 +653,24 @@
514
653
  .modal-close:hover {
515
654
  background-color: rgba(255, 255, 255, 0.05);
516
655
  color: #fff;
656
+ transform: rotate(90deg);
517
657
  }
518
658
 
519
659
  .modal-tabs {
520
660
  display: flex;
521
- background-color: rgba(0, 0, 0, 0.2);
661
+ background-color: rgba(0, 0, 0, 0.25);
522
662
  border-bottom: 1px solid var(--border-color);
523
- padding: 0 24px;
663
+ padding: 0 28px;
524
664
  }
525
665
 
526
666
  .modal-tab {
527
- padding: 14px 20px;
667
+ padding: 14px 22px;
528
668
  cursor: pointer;
529
669
  color: var(--text-muted);
530
- font-weight: 500;
670
+ font-weight: 600;
531
671
  font-size: 0.9rem;
532
672
  border-bottom: 2px solid transparent;
533
- transition: all 0.2s ease;
673
+ transition: all 0.2s;
534
674
  }
535
675
 
536
676
  .modal-tab:hover {
@@ -545,35 +685,36 @@
545
685
  .modal-body {
546
686
  flex-grow: 1;
547
687
  overflow-y: auto;
548
- padding: 24px;
688
+ padding: 28px;
689
+ background-color: rgba(10, 15, 30, 0.2);
549
690
  }
550
691
 
551
- /* JSON display */
692
+ /* JSON Display */
552
693
  .json-code {
553
694
  font-family: var(--font-mono);
554
- font-size: 0.85rem;
555
- background-color: rgba(0, 0, 0, 0.3);
556
- padding: 16px;
557
- border-radius: 8px;
695
+ font-size: 0.825rem;
696
+ background-color: rgba(2, 4, 10, 0.6);
697
+ padding: 20px;
698
+ border-radius: 10px;
558
699
  border: 1px solid var(--border-color);
559
700
  white-space: pre-wrap;
560
701
  word-break: break-all;
561
- color: #cbd5e1;
702
+ color: #e2e8f0;
562
703
  overflow-x: auto;
563
704
  }
564
705
 
565
706
  .key-val-list {
566
707
  display: flex;
567
708
  flex-direction: column;
568
- gap: 12px;
709
+ gap: 16px;
569
710
  }
570
711
 
571
712
  .key-val-row {
572
713
  display: grid;
573
- grid-template-columns: 200px 1fr;
574
- gap: 16px;
714
+ grid-template-columns: 220px 1fr;
715
+ gap: 20px;
575
716
  border-bottom: 1px solid var(--border-color);
576
- padding-bottom: 12px;
717
+ padding-bottom: 14px;
577
718
  }
578
719
 
579
720
  .key-val-row:last-child {
@@ -583,22 +724,86 @@
583
724
  .key-val-label {
584
725
  font-weight: 600;
585
726
  color: var(--text-muted);
586
- font-size: 0.875rem;
727
+ font-size: 0.9rem;
587
728
  }
588
729
 
589
730
  .key-val-value {
590
- font-size: 0.875rem;
731
+ font-size: 0.9rem;
591
732
  word-break: break-all;
592
733
  }
593
734
 
594
- /* Loading Spinner */
735
+ /* Pagination controls */
736
+ .pagination-bar {
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: space-between;
740
+ padding: 14px 24px;
741
+ border-top: 1px solid var(--border-color);
742
+ background-color: rgba(10, 15, 30, 0.15);
743
+ font-size: 0.85rem;
744
+ color: var(--text-muted);
745
+ }
746
+
747
+ .pagination-buttons {
748
+ display: flex;
749
+ gap: 8px;
750
+ }
751
+
752
+ .pagination-btn {
753
+ background-color: var(--bg-input);
754
+ border: 1px solid var(--border-color);
755
+ color: var(--text-main);
756
+ padding: 6px 12px;
757
+ border-radius: 6px;
758
+ cursor: pointer;
759
+ font-family: var(--font-sans);
760
+ font-weight: 500;
761
+ transition: all 0.2s;
762
+ }
763
+
764
+ .pagination-btn:hover:not(:disabled) {
765
+ background-color: var(--primary);
766
+ border-color: transparent;
767
+ color: #fff;
768
+ }
769
+
770
+ .pagination-btn:disabled {
771
+ opacity: 0.4;
772
+ cursor: not-allowed;
773
+ }
774
+
775
+ .pagination-info {
776
+ font-weight: 500;
777
+ }
778
+
779
+ /* Keyboard Shortcuts Indicator */
780
+ .shortcuts-help {
781
+ display: flex;
782
+ gap: 12px;
783
+ font-size: 0.75rem;
784
+ color: var(--text-muted);
785
+ align-items: center;
786
+ }
787
+
788
+ .key-cap {
789
+ background: var(--bg-input);
790
+ border: 1px solid var(--border-color);
791
+ border-radius: 4px;
792
+ padding: 1px 5px;
793
+ font-family: var(--font-mono);
794
+ font-weight: 600;
795
+ box-shadow: 0 1px 0 rgba(255,255,255,0.1);
796
+ color: #fff;
797
+ }
798
+
799
+ /* Spinner */
595
800
  .spinner {
596
- border: 3px solid rgba(255, 255, 255, 0.1);
801
+ border: 3px solid rgba(255, 255, 255, 0.05);
597
802
  border-top: 3px solid var(--primary);
598
803
  border-radius: 50%;
599
- width: 24px;
600
- height: 24px;
601
- animation: spin 1s linear infinite;
804
+ width: 28px;
805
+ height: 28px;
806
+ animation: spin 0.8s linear infinite;
602
807
  }
603
808
 
604
809
  @keyframes spin {
@@ -610,19 +815,19 @@
610
815
  display: flex;
611
816
  align-items: center;
612
817
  justify-content: center;
613
- height: 200px;
818
+ height: 250px;
614
819
  }
615
820
 
616
821
  .empty-state {
617
822
  text-align: center;
618
- padding: 48px;
823
+ padding: 64px 32px;
619
824
  color: var(--text-muted);
620
825
  }
621
826
 
622
827
  .empty-icon {
623
- font-size: 2.5rem;
624
- margin-bottom: 12px;
625
- opacity: 0.5;
828
+ font-size: 3rem;
829
+ margin-bottom: 16px;
830
+ opacity: 0.6;
626
831
  }
627
832
  </style>
628
833
  </head>
@@ -657,36 +862,68 @@
657
862
  const [loginError, setLoginError] = useState('');
658
863
  const [activeTab, setActiveTab] = useState('all');
659
864
  const [entries, setEntries] = useState([]);
865
+ const [stats, setStats] = useState({});
660
866
  const [searchTerm, setSearchTerm] = useState('');
661
867
  const [loading, setLoading] = useState(false);
662
868
  const [polling, setPolling] = useState(true);
663
869
  const [selectedEntry, setSelectedEntry] = useState(null);
664
870
  const [modalTab, setModalTab] = useState('details');
665
871
 
872
+ // Pagination state
873
+ const [currentPage, setCurrentPage] = useState(1);
874
+ const [totalPages, setTotalPages] = useState(1);
875
+ const [totalEntries, setTotalEntries] = useState(0);
876
+ const [perPage, setPerPage] = useState(50);
877
+
666
878
  const prefixPath = window.location.pathname.split('/api')[0].split('/')[1] || 'telescope';
667
879
 
880
+ // Load entries when dependencies change
668
881
  useEffect(() => {
669
882
  if (token) {
670
- fetchEntries();
883
+ fetchEntries(1, false); // Reset page to 1 on tab or search change
671
884
  }
672
885
  }, [token, activeTab, searchTerm]);
673
886
 
887
+ // Polling effect
674
888
  useEffect(() => {
675
889
  let interval;
676
890
  if (polling && token) {
677
891
  interval = setInterval(() => {
678
- fetchEntries(true);
892
+ fetchEntries(currentPage, true);
893
+ fetchStats();
679
894
  }, 3000);
680
895
  }
681
896
  return () => clearInterval(interval);
682
- }, [polling, token, activeTab, searchTerm]);
897
+ }, [polling, token, activeTab, searchTerm, currentPage]);
898
+
899
+ // Fetch stats once on boot, and then periodically
900
+ useEffect(() => {
901
+ if (token) {
902
+ fetchStats();
903
+ }
904
+ }, [token]);
905
+
906
+ const fetchStats = async () => {
907
+ try {
908
+ const res = await fetch(`/${prefixPath}/api/stats`, {
909
+ headers: { 'Authorization': `Bearer ${token}` }
910
+ });
911
+ if (res.ok) {
912
+ const data = await res.json();
913
+ setStats(data);
914
+ }
915
+ } catch (e) {
916
+ console.error('Failed to fetch stats', e);
917
+ }
918
+ };
683
919
 
684
- const fetchEntries = async (silent = false) => {
920
+ const fetchEntries = async (page = 1, silent = false) => {
685
921
  if (!silent) setLoading(true);
686
922
  try {
687
923
  const typeQuery = activeTab !== 'all' ? `type=${activeTab}` : '';
688
924
  const searchQuery = searchTerm ? `search=${encodeURIComponent(searchTerm)}` : '';
689
- const query = [typeQuery, searchQuery].filter(Boolean).join('&');
925
+ const paginationQuery = `page=${page}&perPage=${perPage}`;
926
+ const query = [typeQuery, searchQuery, paginationQuery].filter(Boolean).join('&');
690
927
 
691
928
  const res = await fetch(`/${prefixPath}/api/entries?${query}`, {
692
929
  headers: {
@@ -697,8 +934,18 @@
697
934
  handleLogout();
698
935
  return;
699
936
  }
700
- const data = await res.json();
701
- setEntries(data);
937
+ const result = await res.json();
938
+ if (result && typeof result === 'object' && Array.isArray(result.data)) {
939
+ setEntries(result.data);
940
+ setTotalEntries(result.total);
941
+ setTotalPages(result.totalPages);
942
+ setCurrentPage(result.page);
943
+ } else if (Array.isArray(result)) {
944
+ setEntries(result);
945
+ setTotalEntries(result.length);
946
+ setTotalPages(1);
947
+ setCurrentPage(1);
948
+ }
702
949
  } catch (e) {
703
950
  console.error(e);
704
951
  } finally {
@@ -741,6 +988,7 @@
741
988
  }
742
989
  });
743
990
  setEntries([]);
991
+ setStats({});
744
992
  setSelectedEntry(null);
745
993
  } catch (e) {
746
994
  console.error(e);
@@ -764,17 +1012,49 @@
764
1012
  return date.toLocaleDateString();
765
1013
  };
766
1014
 
1015
+ // Keyboard navigation handler
1016
+ useEffect(() => {
1017
+ const handleKeyDown = (e) => {
1018
+ if (!selectedEntry) return;
1019
+ if (e.key === 'Escape') {
1020
+ setSelectedEntry(null);
1021
+ return;
1022
+ }
1023
+
1024
+ const currentIndex = entries.findIndex(item => item.uuid === selectedEntry.uuid);
1025
+ if (currentIndex === -1) return;
1026
+
1027
+ if (e.key === 'ArrowDown') {
1028
+ e.preventDefault();
1029
+ const nextIndex = currentIndex + 1;
1030
+ if (nextIndex < entries.length) {
1031
+ setSelectedEntry(entries[nextIndex]);
1032
+ }
1033
+ } else if (e.key === 'ArrowUp') {
1034
+ e.preventDefault();
1035
+ const prevIndex = currentIndex - 1;
1036
+ if (prevIndex >= 0) {
1037
+ setSelectedEntry(entries[prevIndex]);
1038
+ }
1039
+ }
1040
+ };
1041
+
1042
+ window.addEventListener('keydown', handleKeyDown);
1043
+ return () => window.removeEventListener('keydown', handleKeyDown);
1044
+ }, [selectedEntry, entries]);
1045
+
1046
+ // Login screen
767
1047
  if (!token) {
768
1048
  return (
769
1049
  <div className="login-overlay">
770
1050
  <form className="login-card" onSubmit={handleLogin}>
771
1051
  <div className="login-logo-wrapper">
772
- <div className="logo">T</div>
1052
+ <div className="login-logo">T</div>
773
1053
  </div>
774
1054
  <div>
775
- <h2 style={{ marginBottom: '8px' }}>Telescope Auth</h2>
776
- <p style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}>
777
- Enter the password configured in your environment.
1055
+ <h2 style={{ marginBottom: '8px', fontWeight: '700', letterSpacing: '-0.02em' }}>Telescope Dashboard</h2>
1056
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>
1057
+ Provide the password to unlock monitoring dashboard
778
1058
  </p>
779
1059
  </div>
780
1060
  <input
@@ -787,14 +1067,22 @@
787
1067
  autoFocus
788
1068
  />
789
1069
  {loginError && <div className="error-msg">{loginError}</div>}
790
- <button type="submit" className="btn" style={{ justifyContent: 'center' }}>
791
- Access Dashboard
1070
+ <button type="submit" className="btn" style={{ justifyContent: 'center', padding: '12px' }}>
1071
+ Access Monitor
792
1072
  </button>
793
1073
  </form>
794
1074
  </div>
795
1075
  );
796
1076
  }
797
1077
 
1078
+ // Format navigation count
1079
+ const getTabCount = (tabId) => {
1080
+ if (tabId === 'all') {
1081
+ return Object.values(stats).reduce((a, b) => a + b, 0);
1082
+ }
1083
+ return stats[tabId] || 0;
1084
+ };
1085
+
798
1086
  return (
799
1087
  <React.Fragment>
800
1088
  {/* Sidebar */}
@@ -811,10 +1099,14 @@
811
1099
  onClick={() => {
812
1100
  setActiveTab(tab.id);
813
1101
  setSelectedEntry(null);
1102
+ setCurrentPage(1);
814
1103
  }}
815
1104
  >
816
- <span className="nav-icon">{tab.icon}</span>
817
- {tab.label}
1105
+ <div className="nav-item-left">
1106
+ <span className="nav-icon">{tab.icon}</span>
1107
+ <span>{tab.label}</span>
1108
+ </div>
1109
+ <span className="nav-count">{getTabCount(tab.id)}</span>
818
1110
  </li>
819
1111
  ))}
820
1112
  </ul>
@@ -856,6 +1148,25 @@
856
1148
 
857
1149
  {/* Content Area */}
858
1150
  <div className="content-area">
1151
+ {/* Stats Grid at the top for quick insights */}
1152
+ {activeTab === 'all' && (
1153
+ <div className="stats-grid">
1154
+ {NAVIGATION_TABS.slice(1, 7).map((tab) => (
1155
+ <div
1156
+ key={tab.id}
1157
+ className={`stat-card ${activeTab === tab.id ? 'active' : ''}`}
1158
+ onClick={() => setActiveTab(tab.id)}
1159
+ >
1160
+ <div className="stat-header">
1161
+ <span>{tab.label}</span>
1162
+ <span>{tab.icon}</span>
1163
+ </div>
1164
+ <div className="stat-value">{stats[tab.id] || 0}</div>
1165
+ </div>
1166
+ ))}
1167
+ </div>
1168
+ )}
1169
+
859
1170
  {loading ? (
860
1171
  <div className="loading-container">
861
1172
  <div className="spinner"></div>
@@ -863,50 +1174,87 @@
863
1174
  ) : entries.length === 0 ? (
864
1175
  <div className="card empty-state">
865
1176
  <div className="empty-icon">📭</div>
866
- <h3>No Entries Found</h3>
867
- <p style={{ marginTop: '8px' }}>
1177
+ <h3 style={{ fontWeight: '600', color: '#fff' }}>No Entries Found</h3>
1178
+ <p style={{ marginTop: '8px', fontSize: '0.9rem' }}>
868
1179
  Trigger some actions in your application to see activity recorded here.
869
1180
  </p>
870
1181
  </div>
871
1182
  ) : (
872
- <div className="card table-container">
873
- <table>
874
- <thead>
875
- <tr>
876
- <th style={{ width: '150px' }}>Type</th>
877
- <th>Payload Summary</th>
878
- <th style={{ width: '100px' }}>Status</th>
879
- <th style={{ width: '180px', textAlign: 'right' }}>Happened</th>
880
- </tr>
881
- </thead>
882
- <tbody>
883
- {entries.map((entry) => {
884
- const summary = getEntrySummary(entry);
885
- return (
886
- <tr key={entry.uuid} onClick={() => {
887
- setSelectedEntry(entry);
888
- setModalTab('details');
889
- }}>
890
- <td>
891
- <span className={`badge badge-method badge-${entry.type}`}>
892
- {entry.type}
893
- </span>
894
- </td>
895
- <td>
896
- <span style={{ fontWeight: '500' }}>{summary.title}</span>
897
- <span style={{ color: 'var(--text-muted)', marginLeft: '8px', fontSize: '0.8rem' }}>
898
- {summary.subtitle}
899
- </span>
900
- </td>
901
- <td>{summary.status}</td>
902
- <td style={{ textAlign: 'right', color: 'var(--text-muted)' }}>
903
- {getRelativeTime(entry.recordedAt)}
904
- </td>
905
- </tr>
906
- );
907
- })}
908
- </tbody>
909
- </table>
1183
+ <div className="card" style={{ display: 'flex', flexDirection: 'column' }}>
1184
+ <div className="table-container">
1185
+ <table>
1186
+ <thead>
1187
+ <tr>
1188
+ <th style={{ width: '130px' }}>Type</th>
1189
+ <th>Payload Summary</th>
1190
+ <th style={{ width: '110px' }}>Status</th>
1191
+ <th style={{ width: '180px', textAlign: 'right' }}>Happened</th>
1192
+ </tr>
1193
+ </thead>
1194
+ <tbody>
1195
+ {entries.map((entry) => {
1196
+ const summary = getEntrySummary(entry);
1197
+ const isSelected = selectedEntry && selectedEntry.uuid === entry.uuid;
1198
+ return (
1199
+ <tr
1200
+ key={entry.uuid}
1201
+ className={isSelected ? 'selected' : ''}
1202
+ onClick={() => {
1203
+ setSelectedEntry(entry);
1204
+ setModalTab('details');
1205
+ }}
1206
+ >
1207
+ <td>
1208
+ <span className={`badge badge-${entry.type}`}>
1209
+ {entry.type.replace('_', ' ')}
1210
+ </span>
1211
+ </td>
1212
+ <td>
1213
+ <span style={{ fontWeight: '600', color: '#fff' }}>{summary.title}</span>
1214
+ <span style={{ color: 'var(--text-muted)', marginLeft: '10px', fontSize: '0.825rem', fontFamily: varMono(entry.type) }}>
1215
+ {summary.subtitle}
1216
+ </span>
1217
+ </td>
1218
+ <td>{summary.status}</td>
1219
+ <td style={{ textAlign: 'right', color: 'var(--text-muted)', fontSize: '0.85rem' }}>
1220
+ {getRelativeTime(entry.recordedAt)}
1221
+ </td>
1222
+ </tr>
1223
+ );
1224
+ })}
1225
+ </tbody>
1226
+ </table>
1227
+ </div>
1228
+
1229
+ {/* Pagination Footer */}
1230
+ <div className="pagination-bar">
1231
+ <div className="shortcuts-help">
1232
+ <span>Shortcut:</span>
1233
+ <span><span className="key-cap">▲</span> <span className="key-cap">▼</span> Navigate</span>
1234
+ <span><span className="key-cap">Esc</span> Close</span>
1235
+ </div>
1236
+
1237
+ <div className="pagination-info">
1238
+ Showing page {currentPage} of {totalPages} ({totalEntries} total entries)
1239
+ </div>
1240
+
1241
+ <div className="pagination-buttons">
1242
+ <button
1243
+ className="pagination-btn"
1244
+ disabled={currentPage <= 1}
1245
+ onClick={() => fetchEntries(currentPage - 1)}
1246
+ >
1247
+ Previous
1248
+ </button>
1249
+ <button
1250
+ className="pagination-btn"
1251
+ disabled={currentPage >= totalPages}
1252
+ onClick={() => fetchEntries(currentPage + 1)}
1253
+ >
1254
+ Next
1255
+ </button>
1256
+ </div>
1257
+ </div>
910
1258
  </div>
911
1259
  )}
912
1260
  </div>
@@ -918,11 +1266,11 @@
918
1266
  <div className="modal-card" onClick={(e) => e.stopPropagation()}>
919
1267
  <div className="modal-header">
920
1268
  <div className="modal-title-group">
921
- <span className={`badge badge-method badge-${selectedEntry.type}`}>
922
- {selectedEntry.type}
1269
+ <span className={`badge badge-${selectedEntry.type}`}>
1270
+ {selectedEntry.type.replace('_', ' ')}
923
1271
  </span>
924
- <h3 style={{ fontSize: '1.1rem' }}>Entry Details</h3>
925
- <span style={{ color: 'var(--text-muted)', fontSize: '0.8rem', fontFamily: 'var(--font-mono)' }}>
1272
+ <h3 style={{ fontSize: '1.25rem', fontWeight: '700', color: '#fff' }}>Entry Details</h3>
1273
+ <span style={{ color: 'var(--text-muted)', fontSize: '0.8rem', fontFamily: 'var(--font-mono)', letterSpacing: '0.05em' }}>
926
1274
  {selectedEntry.uuid}
927
1275
  </span>
928
1276
  </div>
@@ -940,7 +1288,7 @@
940
1288
  className={`modal-tab ${modalTab === 'content' ? 'active' : ''}`}
941
1289
  onClick={() => setModalTab('content')}
942
1290
  >
943
- Raw Content
1291
+ Raw JSON
944
1292
  </div>
945
1293
  </div>
946
1294
 
@@ -949,12 +1297,14 @@
949
1297
  <div className="key-val-list">
950
1298
  <div className="key-val-row">
951
1299
  <div className="key-val-label">Recorded At</div>
952
- <div className="key-val-value">{formatTime(selectedEntry.recordedAt)}</div>
1300
+ <div className="key-val-value" style={{ color: '#fff', fontWeight: '500' }}>
1301
+ {formatTime(selectedEntry.recordedAt)}
1302
+ </div>
953
1303
  </div>
954
1304
  <div className="key-val-row">
955
1305
  <div className="key-val-label">Entry Type</div>
956
- <div className="key-val-value" style={{ textTransform: 'capitalize' }}>
957
- {selectedEntry.type}
1306
+ <div className="key-val-value" style={{ textTransform: 'capitalize', fontWeight: '500' }}>
1307
+ {selectedEntry.type.replace('_', ' ')}
958
1308
  </div>
959
1309
  </div>
960
1310
  {renderStructuredDetails(selectedEntry)}
@@ -972,6 +1322,11 @@
972
1322
  );
973
1323
  }
974
1324
 
1325
+ // Determine font family for subtitle
1326
+ function varMono(type) {
1327
+ return (type === 'query' || type === 'redis' || type === 'request') ? 'var(--font-mono)' : 'var(--font-sans)';
1328
+ }
1329
+
975
1330
  // Helper functions for displaying table items
976
1331
  function getEntrySummary(entry) {
977
1332
  const { type, content } = entry;
@@ -986,19 +1341,19 @@
986
1341
  <span className="badge badge-status-ok">{status}</span>
987
1342
  );
988
1343
  return {
989
- title: `${req.method || 'GET'} ${req.url || '/'}`,
990
- subtitle: duration,
1344
+ title: `${req.method || 'GET'}`,
1345
+ subtitle: `${req.url || '/'}`,
991
1346
  status: statusBadge
992
1347
  };
993
1348
  case 'query':
994
1349
  return {
995
1350
  title: content.query || 'DB Query',
996
- subtitle: `${content.operation || 'QUERY'} on ${content.entity || 'db'}`,
1351
+ subtitle: `${content.operation || 'QUERY'} on ${content.entity || 'db'} (${content.duration || 0}ms)`,
997
1352
  status: <span className="badge badge-status-ok">success</span>
998
1353
  };
999
1354
  case 'cache':
1000
1355
  return {
1001
- title: `${content.action} cache key`,
1356
+ title: `${content.action}`,
1002
1357
  subtitle: content.key || '',
1003
1358
  status: <span className="badge badge-status-ok">{content.action}</span>
1004
1359
  };
@@ -1082,11 +1437,11 @@
1082
1437
  <React.Fragment>
1083
1438
  <div className="key-val-row">
1084
1439
  <div className="key-val-label">HTTP Method</div>
1085
- <div className="key-val-value">{content.request?.method}</div>
1440
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.request?.method}</div>
1086
1441
  </div>
1087
1442
  <div className="key-val-row">
1088
1443
  <div className="key-val-label">URL</div>
1089
- <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.request?.url}</div>
1444
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)', color: '#818cf8' }}>{content.request?.url}</div>
1090
1445
  </div>
1091
1446
  <div className="key-val-row">
1092
1447
  <div className="key-val-label">IP Address</div>
@@ -1096,18 +1451,24 @@
1096
1451
  <div className="key-val-label">Duration</div>
1097
1452
  <div className="key-val-value">{content.duration} ms</div>
1098
1453
  </div>
1099
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1100
- <div className="key-val-label">Request Headers</div>
1101
- <pre className="json-code">{JSON.stringify(content.request?.headers, null, 2)}</pre>
1102
- </div>
1103
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1104
- <div className="key-val-label">Request Body</div>
1105
- <pre className="json-code">{JSON.stringify(content.request?.body, null, 2)}</pre>
1106
- </div>
1107
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1108
- <div className="key-val-label">Response Payload</div>
1109
- <pre className="json-code">{JSON.stringify(content.response, null, 2)}</pre>
1110
- </div>
1454
+ {content.request?.headers && (
1455
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1456
+ <div className="key-val-label">Request Headers</div>
1457
+ <pre className="json-code">{JSON.stringify(content.request?.headers, null, 2)}</pre>
1458
+ </div>
1459
+ )}
1460
+ {content.request?.body && (
1461
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1462
+ <div className="key-val-label">Request Body</div>
1463
+ <pre className="json-code">{JSON.stringify(content.request?.body, null, 2)}</pre>
1464
+ </div>
1465
+ )}
1466
+ {content.response && (
1467
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1468
+ <div className="key-val-label">Response Payload</div>
1469
+ <pre className="json-code">{JSON.stringify(content.response, null, 2)}</pre>
1470
+ </div>
1471
+ )}
1111
1472
  </React.Fragment>
1112
1473
  );
1113
1474
  case 'query':
@@ -1115,7 +1476,7 @@
1115
1476
  <React.Fragment>
1116
1477
  <div className="key-val-row">
1117
1478
  <div className="key-val-label">Operation</div>
1118
- <div className="key-val-value" style={{ fontWeight: '600' }}>{content.operation}</div>
1479
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.operation}</div>
1119
1480
  </div>
1120
1481
  <div className="key-val-row">
1121
1482
  <div className="key-val-label">Entity/Table</div>
@@ -1125,14 +1486,18 @@
1125
1486
  <div className="key-val-label">SQL Query</div>
1126
1487
  <pre className="json-code" style={{ color: '#818cf8', fontWeight: '500' }}>{content.query}</pre>
1127
1488
  </div>
1128
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1129
- <div className="key-val-label">Parameters</div>
1130
- <pre className="json-code">{JSON.stringify(content.parameters, null, 2)}</pre>
1131
- </div>
1132
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1133
- <div className="key-val-label">Entity Data</div>
1134
- <pre className="json-code">{JSON.stringify(content.data, null, 2)}</pre>
1135
- </div>
1489
+ {content.parameters && content.parameters.length > 0 && (
1490
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1491
+ <div className="key-val-label">Parameters</div>
1492
+ <pre className="json-code">{JSON.stringify(content.parameters, null, 2)}</pre>
1493
+ </div>
1494
+ )}
1495
+ {content.data && (
1496
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1497
+ <div className="key-val-label">Entity Data</div>
1498
+ <pre className="json-code">{JSON.stringify(content.data, null, 2)}</pre>
1499
+ </div>
1500
+ )}
1136
1501
  </React.Fragment>
1137
1502
  );
1138
1503
  case 'exception':
@@ -1144,7 +1509,7 @@
1144
1509
  </div>
1145
1510
  <div className="key-val-row">
1146
1511
  <div className="key-val-label">Message</div>
1147
- <div className="key-val-value" style={{ fontWeight: '500' }}>{content.message}</div>
1512
+ <div className="key-val-value" style={{ fontWeight: '500', color: '#fff' }}>{content.message}</div>
1148
1513
  </div>
1149
1514
  {content.status && (
1150
1515
  <div className="key-val-row">
@@ -1155,7 +1520,7 @@
1155
1520
  {content.stack && (
1156
1521
  <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1157
1522
  <div className="key-val-label">Stack Trace</div>
1158
- <pre className="json-code" style={{ color: '#ef4444', overflowX: 'auto' }}>{content.stack}</pre>
1523
+ <pre className="json-code" style={{ color: '#f43f5e', overflowX: 'auto' }}>{content.stack}</pre>
1159
1524
  </div>
1160
1525
  )}
1161
1526
  </React.Fragment>
@@ -1165,7 +1530,7 @@
1165
1530
  <React.Fragment>
1166
1531
  <div className="key-val-row">
1167
1532
  <div className="key-val-label">Level</div>
1168
- <div className="key-val-value" style={{ textTransform: 'uppercase' }}>{content.level}</div>
1533
+ <div className="key-val-value" style={{ textTransform: 'uppercase', fontWeight: '600' }}>{content.level}</div>
1169
1534
  </div>
1170
1535
  <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1171
1536
  <div className="key-val-label">Log Message</div>
@@ -1178,7 +1543,7 @@
1178
1543
  <React.Fragment>
1179
1544
  <div className="key-val-row">
1180
1545
  <div className="key-val-label">Action</div>
1181
- <div className="key-val-value">{content.action}</div>
1546
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.action}</div>
1182
1547
  </div>
1183
1548
  <div className="key-val-row">
1184
1549
  <div className="key-val-label">Key</div>
@@ -1203,7 +1568,7 @@
1203
1568
  <React.Fragment>
1204
1569
  <div className="key-val-row">
1205
1570
  <div className="key-val-label">Job ID</div>
1206
- <div className="key-val-value">{content.jobId}</div>
1571
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.jobId}</div>
1207
1572
  </div>
1208
1573
  <div className="key-val-row">
1209
1574
  <div className="key-val-label">Status</div>
@@ -1236,16 +1601,18 @@
1236
1601
  </div>
1237
1602
  <div className="key-val-row">
1238
1603
  <div className="key-val-label">Subject</div>
1239
- <div className="key-val-value" style={{ fontWeight: '600' }}>{content.subject}</div>
1240
- </div>
1241
- <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1242
- <div className="key-val-label">Plain Text Content</div>
1243
- <pre className="json-code" style={{ whiteSpace: 'pre-wrap' }}>{content.text}</pre>
1604
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.subject}</div>
1244
1605
  </div>
1606
+ {content.text && (
1607
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1608
+ <div className="key-val-label">Plain Text Content</div>
1609
+ <pre className="json-code" style={{ whiteSpace: 'pre-wrap' }}>{content.text}</pre>
1610
+ </div>
1611
+ )}
1245
1612
  {content.html && (
1246
1613
  <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1247
1614
  <div className="key-val-label">HTML Content</div>
1248
- <iframe srcDoc={content.html} style={{ border: '1px solid var(--border-color)', borderRadius: '8px', background: '#fff', width: '100%', height: '300px' }} />
1615
+ <iframe srcDoc={content.html} style={{ border: '1px solid var(--border-color)', borderRadius: '8px', background: '#fff', width: '100%', height: '350px' }} />
1249
1616
  </div>
1250
1617
  )}
1251
1618
  </React.Fragment>