@ngn-net/nestjs-telescope 0.1.6 → 0.1.7

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 +69 -23
  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
@@ -0,0 +1,1635 @@
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>NestJS Telescope Dashboard</title>
7
+ <!-- Google Fonts: Outfit (Premium Sans) & JetBrains Mono -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
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
+
12
+ <style>
13
+ :root {
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;
23
+ --primary: #6366f1;
24
+ --primary-hover: #4f46e5;
25
+ --primary-glow: rgba(99, 102, 241, 0.3);
26
+ --accent-green: #10b981;
27
+ --accent-red: #f43f5e;
28
+ --accent-blue: #3b82f6;
29
+ --accent-purple: #8b5cf6;
30
+ --accent-amber: #f59e0b;
31
+ --font-sans: 'Outfit', sans-serif;
32
+ --font-mono: 'JetBrains Mono', monospace;
33
+ }
34
+
35
+ * {
36
+ box-sizing: border-box;
37
+ margin: 0;
38
+ padding: 0;
39
+ }
40
+
41
+ body {
42
+ font-family: var(--font-sans);
43
+ background-color: var(--bg-main);
44
+ color: var(--text-main);
45
+ overflow: hidden;
46
+ height: 100vh;
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%);
51
+ }
52
+
53
+ /* Scrollbars */
54
+ ::-webkit-scrollbar {
55
+ width: 6px;
56
+ height: 6px;
57
+ }
58
+ ::-webkit-scrollbar-track {
59
+ background: rgba(0, 0, 0, 0.2);
60
+ }
61
+ ::-webkit-scrollbar-thumb {
62
+ background: rgba(255, 255, 255, 0.1);
63
+ border-radius: 4px;
64
+ }
65
+ ::-webkit-scrollbar-thumb:hover {
66
+ background: rgba(255, 255, 255, 0.2);
67
+ }
68
+
69
+ #app {
70
+ width: 100%;
71
+ height: 100%;
72
+ display: flex;
73
+ }
74
+
75
+ /* Sidebar with Glassmorphism */
76
+ .sidebar {
77
+ width: 260px;
78
+ background: var(--bg-sidebar);
79
+ backdrop-filter: blur(12px);
80
+ -webkit-backdrop-filter: blur(12px);
81
+ border-right: 1px solid var(--border-color);
82
+ display: flex;
83
+ flex-direction: column;
84
+ flex-shrink: 0;
85
+ z-index: 10;
86
+ }
87
+
88
+ .sidebar-header {
89
+ padding: 24px;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 12px;
93
+ border-bottom: 1px solid var(--border-color);
94
+ }
95
+
96
+ .logo {
97
+ background: linear-gradient(135deg, var(--primary), #8b5cf6);
98
+ width: 38px;
99
+ height: 38px;
100
+ border-radius: 10px;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ font-weight: 700;
105
+ color: #fff;
106
+ font-size: 1.3rem;
107
+ box-shadow: 0 0 20px var(--primary-glow);
108
+ }
109
+
110
+ .sidebar-title {
111
+ font-weight: 700;
112
+ font-size: 1.2rem;
113
+ letter-spacing: -0.02em;
114
+ background: linear-gradient(to right, #ffffff, #cbd5e1);
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ }
118
+
119
+ .nav-list {
120
+ list-style: none;
121
+ padding: 16px 12px;
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 6px;
125
+ overflow-y: auto;
126
+ flex-grow: 1;
127
+ }
128
+
129
+ .nav-item {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ padding: 10px 16px;
134
+ border-radius: 8px;
135
+ cursor: pointer;
136
+ color: var(--text-muted);
137
+ font-weight: 500;
138
+ font-size: 0.92rem;
139
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
140
+ text-decoration: none;
141
+ }
142
+
143
+ .nav-item:hover {
144
+ color: var(--text-main);
145
+ background-color: rgba(255, 255, 255, 0.04);
146
+ transform: translateX(4px);
147
+ }
148
+
149
+ .nav-item.active {
150
+ color: #fff;
151
+ background: linear-gradient(90deg, rgba(99, 102, 241, 0.15) 0%, rgba(99, 102, 241, 0.02) 100%);
152
+ border-left: 3px solid var(--primary);
153
+ box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.05);
154
+ }
155
+
156
+ .nav-item-left {
157
+ display: flex;
158
+ align-items: 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;
180
+ }
181
+
182
+ .sidebar-footer {
183
+ padding: 16px 20px;
184
+ border-top: 1px solid var(--border-color);
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ font-size: 0.8rem;
189
+ color: var(--text-muted);
190
+ }
191
+
192
+ .logout-btn {
193
+ background: none;
194
+ border: none;
195
+ color: var(--accent-red);
196
+ cursor: pointer;
197
+ font-weight: 600;
198
+ font-family: var(--font-sans);
199
+ transition: opacity 0.2s;
200
+ }
201
+
202
+ .logout-btn:hover {
203
+ opacity: 0.8;
204
+ text-decoration: underline;
205
+ }
206
+
207
+ /* Main Container */
208
+ .main-container {
209
+ flex-grow: 1;
210
+ display: flex;
211
+ flex-direction: column;
212
+ overflow: hidden;
213
+ background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.03), transparent 700px);
214
+ }
215
+
216
+ /* Header */
217
+ .header {
218
+ height: 72px;
219
+ border-bottom: 1px solid var(--border-color);
220
+ padding: 0 28px;
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: space-between;
224
+ flex-shrink: 0;
225
+ }
226
+
227
+ .header-left {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 20px;
231
+ }
232
+
233
+ .view-title {
234
+ font-size: 1.35rem;
235
+ font-weight: 700;
236
+ letter-spacing: -0.01em;
237
+ }
238
+
239
+ .header-right {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 16px;
243
+ }
244
+
245
+ .search-input {
246
+ background-color: var(--bg-input);
247
+ border: 1px solid var(--border-color);
248
+ border-radius: 8px;
249
+ color: var(--text-main);
250
+ padding: 9px 16px;
251
+ font-size: 0.875rem;
252
+ font-family: var(--font-sans);
253
+ outline: none;
254
+ width: 260px;
255
+ transition: all 0.2s ease;
256
+ }
257
+
258
+ .search-input:focus {
259
+ border-color: var(--border-focus);
260
+ box-shadow: 0 0 12px var(--primary-glow);
261
+ }
262
+
263
+ .btn {
264
+ background-color: var(--primary);
265
+ color: #fff;
266
+ border: none;
267
+ border-radius: 8px;
268
+ padding: 9px 18px;
269
+ font-size: 0.875rem;
270
+ font-weight: 600;
271
+ font-family: var(--font-sans);
272
+ cursor: pointer;
273
+ transition: all 0.2s ease;
274
+ display: flex;
275
+ align-items: center;
276
+ gap: 8px;
277
+ }
278
+
279
+ .btn:hover {
280
+ background-color: var(--primary-hover);
281
+ transform: translateY(-1px);
282
+ }
283
+
284
+ .btn:active {
285
+ transform: translateY(0);
286
+ }
287
+
288
+ .btn-danger {
289
+ background-color: rgba(244, 63, 94, 0.1);
290
+ color: var(--accent-red);
291
+ border: 1px solid rgba(244, 63, 94, 0.2);
292
+ }
293
+
294
+ .btn-danger:hover {
295
+ background-color: var(--accent-red);
296
+ color: #fff;
297
+ }
298
+
299
+ .toggle-container {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 8px;
303
+ font-size: 0.85rem;
304
+ color: var(--text-muted);
305
+ cursor: pointer;
306
+ user-select: none;
307
+ }
308
+
309
+ .switch {
310
+ position: relative;
311
+ display: inline-block;
312
+ width: 38px;
313
+ height: 22px;
314
+ }
315
+
316
+ .switch input {
317
+ opacity: 0;
318
+ width: 0;
319
+ height: 0;
320
+ }
321
+
322
+ .slider {
323
+ position: absolute;
324
+ cursor: pointer;
325
+ top: 0;
326
+ left: 0;
327
+ right: 0;
328
+ bottom: 0;
329
+ background-color: var(--bg-input);
330
+ transition: .2s;
331
+ border-radius: 34px;
332
+ border: 1px solid var(--border-color);
333
+ }
334
+
335
+ .slider:before {
336
+ position: absolute;
337
+ content: "";
338
+ height: 14px;
339
+ width: 14px;
340
+ left: 3px;
341
+ bottom: 3px;
342
+ background-color: var(--text-muted);
343
+ transition: .2s;
344
+ border-radius: 50%;
345
+ }
346
+
347
+ input:checked + .slider {
348
+ background-color: var(--primary);
349
+ border-color: transparent;
350
+ }
351
+
352
+ input:checked + .slider:before {
353
+ transform: translateX(16px);
354
+ background-color: #fff;
355
+ }
356
+
357
+ /* Content Area */
358
+ .content-area {
359
+ flex-grow: 1;
360
+ overflow-y: auto;
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;
412
+ }
413
+
414
+ /* Card & Table View */
415
+ .card {
416
+ background: var(--bg-card);
417
+ backdrop-filter: blur(10px);
418
+ -webkit-backdrop-filter: blur(10px);
419
+ border: 1px solid var(--border-color);
420
+ border-radius: 14px;
421
+ overflow: hidden;
422
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
423
+ }
424
+
425
+ .table-container {
426
+ overflow-x: auto;
427
+ }
428
+
429
+ table {
430
+ width: 100%;
431
+ border-collapse: collapse;
432
+ text-align: left;
433
+ }
434
+
435
+ th {
436
+ padding: 16px 24px;
437
+ font-size: 0.78rem;
438
+ text-transform: uppercase;
439
+ letter-spacing: 0.06em;
440
+ color: var(--text-muted);
441
+ border-bottom: 1px solid var(--border-color);
442
+ font-weight: 700;
443
+ background-color: rgba(10, 15, 30, 0.3);
444
+ }
445
+
446
+ td {
447
+ padding: 16px 24px;
448
+ font-size: 0.9rem;
449
+ border-bottom: 1px solid var(--border-color);
450
+ color: var(--text-main);
451
+ max-width: 450px;
452
+ white-space: nowrap;
453
+ overflow: hidden;
454
+ text-overflow: ellipsis;
455
+ }
456
+
457
+ tr:last-child td {
458
+ border-bottom: none;
459
+ }
460
+
461
+ tr {
462
+ cursor: pointer;
463
+ transition: background-color 0.15s ease;
464
+ }
465
+
466
+ tr:hover {
467
+ background-color: rgba(255, 255, 255, 0.015);
468
+ }
469
+
470
+ tr.selected {
471
+ background-color: rgba(99, 102, 241, 0.07);
472
+ }
473
+
474
+ /* Badges */
475
+ .badge {
476
+ display: inline-flex;
477
+ align-items: center;
478
+ padding: 4px 8px;
479
+ border-radius: 6px;
480
+ font-size: 0.72rem;
481
+ font-weight: 700;
482
+ letter-spacing: 0.03em;
483
+ }
484
+
485
+ .badge-method {
486
+ text-transform: uppercase;
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
+
500
+ .badge-get { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
501
+ .badge-post { background-color: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
502
+ .badge-put { background-color: rgba(245, 158, 11, 0.15); color: var(--accent-amber); }
503
+ .badge-delete { background-color: rgba(244, 63, 94, 0.15); color: var(--accent-red); }
504
+
505
+ .badge-status-ok { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
506
+ .badge-status-err { background-color: rgba(244, 63, 94, 0.15); color: var(--accent-red); }
507
+
508
+ /* Animated Login Screen */
509
+ .login-overlay {
510
+ position: fixed;
511
+ top: 0;
512
+ left: 0;
513
+ right: 0;
514
+ bottom: 0;
515
+ background: radial-gradient(circle at center, #0c122c 0%, #050714 100%);
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ z-index: 1000;
520
+ }
521
+
522
+ .login-card {
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);
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 28px;
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); }
541
+ }
542
+
543
+ .login-logo-wrapper {
544
+ display: flex;
545
+ justify-content: center;
546
+ }
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
+
562
+ .login-input {
563
+ background-color: var(--bg-input);
564
+ border: 1px solid var(--border-color);
565
+ border-radius: 8px;
566
+ color: var(--text-main);
567
+ padding: 14px;
568
+ font-size: 1rem;
569
+ font-family: var(--font-sans);
570
+ outline: none;
571
+ width: 100%;
572
+ text-align: center;
573
+ transition: all 0.2s ease;
574
+ }
575
+
576
+ .login-input:focus {
577
+ border-color: var(--border-focus);
578
+ box-shadow: 0 0 12px var(--primary-glow);
579
+ }
580
+
581
+ .error-msg {
582
+ color: var(--accent-red);
583
+ font-size: 0.85rem;
584
+ font-weight: 500;
585
+ }
586
+
587
+ /* Modal Styles with Blur & Entrance animation */
588
+ .modal-overlay {
589
+ position: fixed;
590
+ top: 0;
591
+ left: 0;
592
+ right: 0;
593
+ bottom: 0;
594
+ background-color: rgba(2, 4, 12, 0.75);
595
+ backdrop-filter: blur(8px);
596
+ -webkit-backdrop-filter: blur(8px);
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: center;
600
+ z-index: 999;
601
+ padding: 30px;
602
+ }
603
+
604
+ .modal-card {
605
+ width: 100%;
606
+ max-width: 950px;
607
+ height: 85%;
608
+ background-color: #0b1226;
609
+ border: 1px solid var(--border-color);
610
+ border-radius: 18px;
611
+ display: flex;
612
+ flex-direction: column;
613
+ overflow: hidden;
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);
616
+ }
617
+
618
+ @keyframes modalEntrance {
619
+ from { transform: scale(0.96) translateY(10px); opacity: 0; }
620
+ to { transform: scale(1) translateY(0); opacity: 1; }
621
+ }
622
+
623
+ .modal-header {
624
+ padding: 22px 28px;
625
+ border-bottom: 1px solid var(--border-color);
626
+ display: flex;
627
+ align-items: center;
628
+ justify-content: space-between;
629
+ background-color: rgba(0, 0, 0, 0.15);
630
+ }
631
+
632
+ .modal-title-group {
633
+ display: flex;
634
+ align-items: center;
635
+ gap: 16px;
636
+ }
637
+
638
+ .modal-close {
639
+ background: none;
640
+ border: none;
641
+ color: var(--text-muted);
642
+ cursor: pointer;
643
+ font-size: 1.8rem;
644
+ display: flex;
645
+ align-items: center;
646
+ justify-content: center;
647
+ width: 36px;
648
+ height: 36px;
649
+ border-radius: 50%;
650
+ transition: all 0.2s ease;
651
+ }
652
+
653
+ .modal-close:hover {
654
+ background-color: rgba(255, 255, 255, 0.05);
655
+ color: #fff;
656
+ transform: rotate(90deg);
657
+ }
658
+
659
+ .modal-tabs {
660
+ display: flex;
661
+ background-color: rgba(0, 0, 0, 0.25);
662
+ border-bottom: 1px solid var(--border-color);
663
+ padding: 0 28px;
664
+ }
665
+
666
+ .modal-tab {
667
+ padding: 14px 22px;
668
+ cursor: pointer;
669
+ color: var(--text-muted);
670
+ font-weight: 600;
671
+ font-size: 0.9rem;
672
+ border-bottom: 2px solid transparent;
673
+ transition: all 0.2s;
674
+ }
675
+
676
+ .modal-tab:hover {
677
+ color: var(--text-main);
678
+ }
679
+
680
+ .modal-tab.active {
681
+ color: var(--primary);
682
+ border-bottom-color: var(--primary);
683
+ }
684
+
685
+ .modal-body {
686
+ flex-grow: 1;
687
+ overflow-y: auto;
688
+ padding: 28px;
689
+ background-color: rgba(10, 15, 30, 0.2);
690
+ }
691
+
692
+ /* JSON Display */
693
+ .json-code {
694
+ font-family: var(--font-mono);
695
+ font-size: 0.825rem;
696
+ background-color: rgba(2, 4, 10, 0.6);
697
+ padding: 20px;
698
+ border-radius: 10px;
699
+ border: 1px solid var(--border-color);
700
+ white-space: pre-wrap;
701
+ word-break: break-all;
702
+ color: #e2e8f0;
703
+ overflow-x: auto;
704
+ }
705
+
706
+ .key-val-list {
707
+ display: flex;
708
+ flex-direction: column;
709
+ gap: 16px;
710
+ }
711
+
712
+ .key-val-row {
713
+ display: grid;
714
+ grid-template-columns: 220px 1fr;
715
+ gap: 20px;
716
+ border-bottom: 1px solid var(--border-color);
717
+ padding-bottom: 14px;
718
+ }
719
+
720
+ .key-val-row:last-child {
721
+ border-bottom: none;
722
+ }
723
+
724
+ .key-val-label {
725
+ font-weight: 600;
726
+ color: var(--text-muted);
727
+ font-size: 0.9rem;
728
+ }
729
+
730
+ .key-val-value {
731
+ font-size: 0.9rem;
732
+ word-break: break-all;
733
+ }
734
+
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 */
800
+ .spinner {
801
+ border: 3px solid rgba(255, 255, 255, 0.05);
802
+ border-top: 3px solid var(--primary);
803
+ border-radius: 50%;
804
+ width: 28px;
805
+ height: 28px;
806
+ animation: spin 0.8s linear infinite;
807
+ }
808
+
809
+ @keyframes spin {
810
+ 0% { transform: rotate(0deg); }
811
+ 100% { transform: rotate(360deg); }
812
+ }
813
+
814
+ .loading-container {
815
+ display: flex;
816
+ align-items: center;
817
+ justify-content: center;
818
+ height: 250px;
819
+ }
820
+
821
+ .empty-state {
822
+ text-align: center;
823
+ padding: 64px 32px;
824
+ color: var(--text-muted);
825
+ }
826
+
827
+ .empty-icon {
828
+ font-size: 3rem;
829
+ margin-bottom: 16px;
830
+ opacity: 0.6;
831
+ }
832
+ </style>
833
+ </head>
834
+ <body>
835
+ <div id="app"></div>
836
+
837
+ <!-- UMD React dependencies loaded securely -->
838
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
839
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
840
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
841
+
842
+ <script type="text/babel">
843
+ const { useState, useEffect, useRef } = React;
844
+
845
+ const NAVIGATION_TABS = [
846
+ { id: 'all', label: 'All Entries', icon: '🔍' },
847
+ { id: 'request', label: 'HTTP Requests', icon: '🌐' },
848
+ { id: 'query', label: 'Database Queries', icon: '💾' },
849
+ { id: 'cache', label: 'Cache Ops', icon: '⚡' },
850
+ { id: 'job', label: 'Queue Jobs', icon: '⚙️' },
851
+ { id: 'event', label: 'Events', icon: '📢' },
852
+ { id: 'mail', label: 'Mails', icon: '✉️' },
853
+ { id: 'log', label: 'Logs', icon: '📋' },
854
+ { id: 'exception', label: 'Exceptions', icon: '❌' },
855
+ { id: 'scheduled_task', label: 'Scheduled Tasks', icon: '⏱️' },
856
+ { id: 'redis', label: 'Redis Commands', icon: '🔑' },
857
+ ];
858
+
859
+ function App() {
860
+ const [token, setToken] = useState(localStorage.getItem('telescope_token') || '');
861
+ const [password, setPassword] = useState('');
862
+ const [loginError, setLoginError] = useState('');
863
+ const [activeTab, setActiveTab] = useState('all');
864
+ const [entries, setEntries] = useState([]);
865
+ const [stats, setStats] = useState({});
866
+ const [searchTerm, setSearchTerm] = useState('');
867
+ const [loading, setLoading] = useState(false);
868
+ const [polling, setPolling] = useState(true);
869
+ const [selectedEntry, setSelectedEntry] = useState(null);
870
+ const [modalTab, setModalTab] = useState('details');
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
+
878
+ const prefixPath = window.location.pathname.split('/api')[0].split('/')[1] || 'telescope';
879
+
880
+ // Load entries when dependencies change
881
+ useEffect(() => {
882
+ if (token) {
883
+ fetchEntries(1, false); // Reset page to 1 on tab or search change
884
+ }
885
+ }, [token, activeTab, searchTerm]);
886
+
887
+ // Polling effect
888
+ useEffect(() => {
889
+ let interval;
890
+ if (polling && token) {
891
+ interval = setInterval(() => {
892
+ fetchEntries(currentPage, true);
893
+ fetchStats();
894
+ }, 3000);
895
+ }
896
+ return () => clearInterval(interval);
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
+ };
919
+
920
+ const fetchEntries = async (page = 1, silent = false) => {
921
+ if (!silent) setLoading(true);
922
+ try {
923
+ const typeQuery = activeTab !== 'all' ? `type=${activeTab}` : '';
924
+ const searchQuery = searchTerm ? `search=${encodeURIComponent(searchTerm)}` : '';
925
+ const paginationQuery = `page=${page}&perPage=${perPage}`;
926
+ const query = [typeQuery, searchQuery, paginationQuery].filter(Boolean).join('&');
927
+
928
+ const res = await fetch(`/${prefixPath}/api/entries?${query}`, {
929
+ headers: {
930
+ 'Authorization': `Bearer ${token}`
931
+ }
932
+ });
933
+ if (res.status === 401) {
934
+ handleLogout();
935
+ return;
936
+ }
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
+ }
949
+ } catch (e) {
950
+ console.error(e);
951
+ } finally {
952
+ setLoading(false);
953
+ }
954
+ };
955
+
956
+ const handleLogin = async (e) => {
957
+ e.preventDefault();
958
+ setLoginError('');
959
+ try {
960
+ const res = await fetch(`/${prefixPath}/api/login`, {
961
+ method: 'POST',
962
+ headers: { 'Content-Type': 'application/json' },
963
+ body: JSON.stringify({ password })
964
+ });
965
+ if (!res.ok) {
966
+ throw new Error('Invalid password');
967
+ }
968
+ const data = await res.json();
969
+ localStorage.setItem('telescope_token', data.token);
970
+ setToken(data.token);
971
+ } catch (err) {
972
+ setLoginError('Authentication failed. Check password.');
973
+ }
974
+ };
975
+
976
+ const handleLogout = () => {
977
+ localStorage.removeItem('telescope_token');
978
+ setToken('');
979
+ };
980
+
981
+ const handleClearAll = async () => {
982
+ if (!confirm('Are you sure you want to clear all recorded entries?')) return;
983
+ try {
984
+ await fetch(`/${prefixPath}/api/entries`, {
985
+ method: 'DELETE',
986
+ headers: {
987
+ 'Authorization': `Bearer ${token}`
988
+ }
989
+ });
990
+ setEntries([]);
991
+ setStats({});
992
+ setSelectedEntry(null);
993
+ } catch (e) {
994
+ console.error(e);
995
+ }
996
+ };
997
+
998
+ const formatTime = (dateStr) => {
999
+ const date = new Date(dateStr);
1000
+ return date.toLocaleTimeString() + ' ' + date.toLocaleDateString();
1001
+ };
1002
+
1003
+ const getRelativeTime = (dateStr) => {
1004
+ const date = new Date(dateStr);
1005
+ const seconds = Math.floor((new Date() - date) / 1000);
1006
+ if (seconds < 5) return 'just now';
1007
+ if (seconds < 60) return `${seconds}s ago`;
1008
+ const minutes = Math.floor(seconds / 60);
1009
+ if (minutes < 60) return `${minutes}m ago`;
1010
+ const hours = Math.floor(minutes / 60);
1011
+ if (hours < 24) return `${hours}h ago`;
1012
+ return date.toLocaleDateString();
1013
+ };
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
1047
+ if (!token) {
1048
+ return (
1049
+ <div className="login-overlay">
1050
+ <form className="login-card" onSubmit={handleLogin}>
1051
+ <div className="login-logo-wrapper">
1052
+ <div className="login-logo">T</div>
1053
+ </div>
1054
+ <div>
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
1058
+ </p>
1059
+ </div>
1060
+ <input
1061
+ type="password"
1062
+ className="login-input"
1063
+ placeholder="Enter password"
1064
+ value={password}
1065
+ onChange={(e) => setPassword(e.target.value)}
1066
+ required
1067
+ autoFocus
1068
+ />
1069
+ {loginError && <div className="error-msg">{loginError}</div>}
1070
+ <button type="submit" className="btn" style={{ justifyContent: 'center', padding: '12px' }}>
1071
+ Access Monitor
1072
+ </button>
1073
+ </form>
1074
+ </div>
1075
+ );
1076
+ }
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
+
1086
+ return (
1087
+ <React.Fragment>
1088
+ {/* Sidebar */}
1089
+ <div className="sidebar">
1090
+ <div className="sidebar-header">
1091
+ <div className="logo">T</div>
1092
+ <div className="sidebar-title">Telescope</div>
1093
+ </div>
1094
+ <ul className="nav-list">
1095
+ {NAVIGATION_TABS.map((tab) => (
1096
+ <li
1097
+ key={tab.id}
1098
+ className={`nav-item ${activeTab === tab.id ? 'active' : ''}`}
1099
+ onClick={() => {
1100
+ setActiveTab(tab.id);
1101
+ setSelectedEntry(null);
1102
+ setCurrentPage(1);
1103
+ }}
1104
+ >
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>
1110
+ </li>
1111
+ ))}
1112
+ </ul>
1113
+ <div className="sidebar-footer">
1114
+ <span>NestJS Telescope</span>
1115
+ <button className="logout-btn" onClick={handleLogout}>Log Out</button>
1116
+ </div>
1117
+ </div>
1118
+
1119
+ {/* Main Panel */}
1120
+ <div className="main-container">
1121
+ {/* Header */}
1122
+ <div className="header">
1123
+ <div className="header-left">
1124
+ <div className="view-title">
1125
+ {NAVIGATION_TABS.find((t) => t.id === activeTab)?.label}
1126
+ </div>
1127
+ <div className="toggle-container" onClick={() => setPolling(!polling)}>
1128
+ <label className="switch">
1129
+ <input type="checkbox" checked={polling} onChange={() => {}} />
1130
+ <span className="slider"></span>
1131
+ </label>
1132
+ <span>Auto-Refresh</span>
1133
+ </div>
1134
+ </div>
1135
+ <div className="header-right">
1136
+ <input
1137
+ type="text"
1138
+ placeholder="Search entries..."
1139
+ className="search-input"
1140
+ value={searchTerm}
1141
+ onChange={(e) => setSearchTerm(e.target.value)}
1142
+ />
1143
+ <button className="btn btn-danger" onClick={handleClearAll}>
1144
+ Clear Entries
1145
+ </button>
1146
+ </div>
1147
+ </div>
1148
+
1149
+ {/* Content Area */}
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
+
1170
+ {loading ? (
1171
+ <div className="loading-container">
1172
+ <div className="spinner"></div>
1173
+ </div>
1174
+ ) : entries.length === 0 ? (
1175
+ <div className="card empty-state">
1176
+ <div className="empty-icon">📭</div>
1177
+ <h3 style={{ fontWeight: '600', color: '#fff' }}>No Entries Found</h3>
1178
+ <p style={{ marginTop: '8px', fontSize: '0.9rem' }}>
1179
+ Trigger some actions in your application to see activity recorded here.
1180
+ </p>
1181
+ </div>
1182
+ ) : (
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>
1258
+ </div>
1259
+ )}
1260
+ </div>
1261
+ </div>
1262
+
1263
+ {/* Details Modal */}
1264
+ {selectedEntry && (
1265
+ <div className="modal-overlay" onClick={() => setSelectedEntry(null)}>
1266
+ <div className="modal-card" onClick={(e) => e.stopPropagation()}>
1267
+ <div className="modal-header">
1268
+ <div className="modal-title-group">
1269
+ <span className={`badge badge-${selectedEntry.type}`}>
1270
+ {selectedEntry.type.replace('_', ' ')}
1271
+ </span>
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' }}>
1274
+ {selectedEntry.uuid}
1275
+ </span>
1276
+ </div>
1277
+ <button className="modal-close" onClick={() => setSelectedEntry(null)}>×</button>
1278
+ </div>
1279
+
1280
+ <div className="modal-tabs">
1281
+ <div
1282
+ className={`modal-tab ${modalTab === 'details' ? 'active' : ''}`}
1283
+ onClick={() => setModalTab('details')}
1284
+ >
1285
+ Summary
1286
+ </div>
1287
+ <div
1288
+ className={`modal-tab ${modalTab === 'content' ? 'active' : ''}`}
1289
+ onClick={() => setModalTab('content')}
1290
+ >
1291
+ Raw JSON
1292
+ </div>
1293
+ </div>
1294
+
1295
+ <div className="modal-body">
1296
+ {modalTab === 'details' ? (
1297
+ <div className="key-val-list">
1298
+ <div className="key-val-row">
1299
+ <div className="key-val-label">Recorded At</div>
1300
+ <div className="key-val-value" style={{ color: '#fff', fontWeight: '500' }}>
1301
+ {formatTime(selectedEntry.recordedAt)}
1302
+ </div>
1303
+ </div>
1304
+ <div className="key-val-row">
1305
+ <div className="key-val-label">Entry Type</div>
1306
+ <div className="key-val-value" style={{ textTransform: 'capitalize', fontWeight: '500' }}>
1307
+ {selectedEntry.type.replace('_', ' ')}
1308
+ </div>
1309
+ </div>
1310
+ {renderStructuredDetails(selectedEntry)}
1311
+ </div>
1312
+ ) : (
1313
+ <pre className="json-code">
1314
+ {JSON.stringify(selectedEntry.content, null, 2)}
1315
+ </pre>
1316
+ )}
1317
+ </div>
1318
+ </div>
1319
+ </div>
1320
+ )}
1321
+ </React.Fragment>
1322
+ );
1323
+ }
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
+
1330
+ // Helper functions for displaying table items
1331
+ function getEntrySummary(entry) {
1332
+ const { type, content } = entry;
1333
+ switch (type) {
1334
+ case 'request':
1335
+ const req = content.request || {};
1336
+ const duration = content.duration ? `${content.duration}ms` : '';
1337
+ const status = content.response?.statusCode || content.response?.status || 200;
1338
+ const statusBadge = status >= 400 ? (
1339
+ <span className="badge badge-status-err">{status}</span>
1340
+ ) : (
1341
+ <span className="badge badge-status-ok">{status}</span>
1342
+ );
1343
+ return {
1344
+ title: `${req.method || 'GET'}`,
1345
+ subtitle: `${req.url || '/'}`,
1346
+ status: statusBadge
1347
+ };
1348
+ case 'query':
1349
+ return {
1350
+ title: content.query || 'DB Query',
1351
+ subtitle: `${content.operation || 'QUERY'} on ${content.entity || 'db'} (${content.duration || 0}ms)`,
1352
+ status: <span className="badge badge-status-ok">success</span>
1353
+ };
1354
+ case 'cache':
1355
+ return {
1356
+ title: `${content.action}`,
1357
+ subtitle: content.key || '',
1358
+ status: <span className="badge badge-status-ok">{content.action}</span>
1359
+ };
1360
+ case 'job':
1361
+ return {
1362
+ title: `Job ${content.jobId || 'Unknown'}`,
1363
+ subtitle: content.status || '',
1364
+ status: content.status === 'failed' ? (
1365
+ <span className="badge badge-status-err">failed</span>
1366
+ ) : (
1367
+ <span className="badge badge-status-ok">{content.status}</span>
1368
+ )
1369
+ };
1370
+ case 'event':
1371
+ return {
1372
+ title: content.event || 'Application Event',
1373
+ subtitle: `Listeners: ${content.listeners ?? 0}`,
1374
+ status: <span className="badge badge-status-ok">fired</span>
1375
+ };
1376
+ case 'mail':
1377
+ return {
1378
+ title: content.subject || 'No Subject',
1379
+ subtitle: `to: ${content.to || 'unknown'}`,
1380
+ status: <span className="badge badge-status-ok">sent</span>
1381
+ };
1382
+ case 'log':
1383
+ const lvl = content.level || 'log';
1384
+ const lvlBadge = lvl === 'error' ? (
1385
+ <span className="badge badge-status-err">error</span>
1386
+ ) : lvl === 'warn' ? (
1387
+ <span className="badge badge-status-err" style={{ backgroundColor: 'rgba(245, 158, 11, 0.15)', color: 'var(--accent-amber)' }}>warn</span>
1388
+ ) : (
1389
+ <span className="badge badge-status-ok" style={{ backgroundColor: 'rgba(59, 130, 246, 0.15)', color: 'var(--accent-blue)' }}>{lvl}</span>
1390
+ );
1391
+ return {
1392
+ title: content.message || '',
1393
+ subtitle: '',
1394
+ status: lvlBadge
1395
+ };
1396
+ case 'exception':
1397
+ return {
1398
+ title: content.name || 'Exception',
1399
+ subtitle: content.message || '',
1400
+ status: <span className="badge badge-status-err">failed</span>
1401
+ };
1402
+ case 'scheduled_task':
1403
+ return {
1404
+ title: content.name || 'Cron Job',
1405
+ subtitle: `Cron: ${content.cronTime || ''}`,
1406
+ status: content.status === 'failed' ? (
1407
+ <span className="badge badge-status-err">failed</span>
1408
+ ) : (
1409
+ <span className="badge badge-status-ok">{content.status}</span>
1410
+ )
1411
+ };
1412
+ case 'redis':
1413
+ return {
1414
+ title: `${content.command || 'COMMAND'}`,
1415
+ subtitle: (content.arguments || []).join(' '),
1416
+ status: content.status === 'failed' ? (
1417
+ <span className="badge badge-status-err">failed</span>
1418
+ ) : (
1419
+ <span className="badge badge-status-ok">success</span>
1420
+ )
1421
+ };
1422
+ default:
1423
+ return {
1424
+ title: 'Telescope Entry',
1425
+ subtitle: '',
1426
+ status: null
1427
+ };
1428
+ }
1429
+ }
1430
+
1431
+ // Helper functions for displaying details inside modal
1432
+ function renderStructuredDetails(entry) {
1433
+ const { type, content } = entry;
1434
+ switch (type) {
1435
+ case 'request':
1436
+ return (
1437
+ <React.Fragment>
1438
+ <div className="key-val-row">
1439
+ <div className="key-val-label">HTTP Method</div>
1440
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.request?.method}</div>
1441
+ </div>
1442
+ <div className="key-val-row">
1443
+ <div className="key-val-label">URL</div>
1444
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)', color: '#818cf8' }}>{content.request?.url}</div>
1445
+ </div>
1446
+ <div className="key-val-row">
1447
+ <div className="key-val-label">IP Address</div>
1448
+ <div className="key-val-value">{content.request?.ip}</div>
1449
+ </div>
1450
+ <div className="key-val-row">
1451
+ <div className="key-val-label">Duration</div>
1452
+ <div className="key-val-value">{content.duration} ms</div>
1453
+ </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
+ )}
1472
+ </React.Fragment>
1473
+ );
1474
+ case 'query':
1475
+ return (
1476
+ <React.Fragment>
1477
+ <div className="key-val-row">
1478
+ <div className="key-val-label">Operation</div>
1479
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.operation}</div>
1480
+ </div>
1481
+ <div className="key-val-row">
1482
+ <div className="key-val-label">Entity/Table</div>
1483
+ <div className="key-val-value">{content.entity}</div>
1484
+ </div>
1485
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1486
+ <div className="key-val-label">SQL Query</div>
1487
+ <pre className="json-code" style={{ color: '#818cf8', fontWeight: '500' }}>{content.query}</pre>
1488
+ </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
+ )}
1501
+ </React.Fragment>
1502
+ );
1503
+ case 'exception':
1504
+ return (
1505
+ <React.Fragment>
1506
+ <div className="key-val-row">
1507
+ <div className="key-val-label">Name</div>
1508
+ <div className="key-val-value" style={{ color: 'var(--accent-red)', fontWeight: '600' }}>{content.name}</div>
1509
+ </div>
1510
+ <div className="key-val-row">
1511
+ <div className="key-val-label">Message</div>
1512
+ <div className="key-val-value" style={{ fontWeight: '500', color: '#fff' }}>{content.message}</div>
1513
+ </div>
1514
+ {content.status && (
1515
+ <div className="key-val-row">
1516
+ <div className="key-val-label">HTTP Status</div>
1517
+ <div className="key-val-value">{content.status}</div>
1518
+ </div>
1519
+ )}
1520
+ {content.stack && (
1521
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1522
+ <div className="key-val-label">Stack Trace</div>
1523
+ <pre className="json-code" style={{ color: '#f43f5e', overflowX: 'auto' }}>{content.stack}</pre>
1524
+ </div>
1525
+ )}
1526
+ </React.Fragment>
1527
+ );
1528
+ case 'log':
1529
+ return (
1530
+ <React.Fragment>
1531
+ <div className="key-val-row">
1532
+ <div className="key-val-label">Level</div>
1533
+ <div className="key-val-value" style={{ textTransform: 'uppercase', fontWeight: '600' }}>{content.level}</div>
1534
+ </div>
1535
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1536
+ <div className="key-val-label">Log Message</div>
1537
+ <pre className="json-code">{content.message}</pre>
1538
+ </div>
1539
+ </React.Fragment>
1540
+ );
1541
+ case 'cache':
1542
+ return (
1543
+ <React.Fragment>
1544
+ <div className="key-val-row">
1545
+ <div className="key-val-label">Action</div>
1546
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.action}</div>
1547
+ </div>
1548
+ <div className="key-val-row">
1549
+ <div className="key-val-label">Key</div>
1550
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.key}</div>
1551
+ </div>
1552
+ {content.value !== undefined && (
1553
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1554
+ <div className="key-val-label">Stored Value</div>
1555
+ <pre className="json-code">{JSON.stringify(content.value, null, 2)}</pre>
1556
+ </div>
1557
+ )}
1558
+ {content.ttl !== undefined && (
1559
+ <div className="key-val-row">
1560
+ <div className="key-val-label">TTL</div>
1561
+ <div className="key-val-value">{content.ttl} seconds</div>
1562
+ </div>
1563
+ )}
1564
+ </React.Fragment>
1565
+ );
1566
+ case 'job':
1567
+ return (
1568
+ <React.Fragment>
1569
+ <div className="key-val-row">
1570
+ <div className="key-val-label">Job ID</div>
1571
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.jobId}</div>
1572
+ </div>
1573
+ <div className="key-val-row">
1574
+ <div className="key-val-label">Status</div>
1575
+ <div className="key-val-value">{content.status}</div>
1576
+ </div>
1577
+ {content.returnvalue !== undefined && (
1578
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1579
+ <div className="key-val-label">Returned Value</div>
1580
+ <pre className="json-code">{JSON.stringify(content.returnvalue, null, 2)}</pre>
1581
+ </div>
1582
+ )}
1583
+ {content.failedReason && (
1584
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1585
+ <div className="key-val-label">Failure Reason</div>
1586
+ <pre className="json-code" style={{ color: 'var(--accent-red)' }}>{content.failedReason}</pre>
1587
+ </div>
1588
+ )}
1589
+ </React.Fragment>
1590
+ );
1591
+ case 'mail':
1592
+ return (
1593
+ <React.Fragment>
1594
+ <div className="key-val-row">
1595
+ <div className="key-val-label">From</div>
1596
+ <div className="key-val-value">{content.from}</div>
1597
+ </div>
1598
+ <div className="key-val-row">
1599
+ <div className="key-val-label">To</div>
1600
+ <div className="key-val-value">{content.to}</div>
1601
+ </div>
1602
+ <div className="key-val-row">
1603
+ <div className="key-val-label">Subject</div>
1604
+ <div className="key-val-value" style={{ fontWeight: '600', color: '#fff' }}>{content.subject}</div>
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
+ )}
1612
+ {content.html && (
1613
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1614
+ <div className="key-val-label">HTML Content</div>
1615
+ <iframe srcDoc={content.html} style={{ border: '1px solid var(--border-color)', borderRadius: '8px', background: '#fff', width: '100%', height: '350px' }} />
1616
+ </div>
1617
+ )}
1618
+ </React.Fragment>
1619
+ );
1620
+ default:
1621
+ return (
1622
+ <div className="key-val-row">
1623
+ <div className="key-val-label">Payload</div>
1624
+ <pre className="json-code">{JSON.stringify(content, null, 2)}</pre>
1625
+ </div>
1626
+ );
1627
+ }
1628
+ }
1629
+
1630
+ const container = document.getElementById('app');
1631
+ const root = ReactDOM.createRoot(container);
1632
+ root.render(<App />);
1633
+ </script>
1634
+ </body>
1635
+ </html>