@ngn-net/nestjs-telescope 0.1.1

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 (45) hide show
  1. package/README.md +110 -0
  2. package/dist/constants.d.ts +3 -0
  3. package/dist/constants.js +7 -0
  4. package/dist/controllers/telescope.controller.d.ts +17 -0
  5. package/dist/controllers/telescope.controller.js +126 -0
  6. package/dist/enums/entry-type.enum.d.ts +14 -0
  7. package/dist/enums/entry-type.enum.js +19 -0
  8. package/dist/guards/telescope-jwt.guard.d.ts +7 -0
  9. package/dist/guards/telescope-jwt.guard.js +44 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.js +25 -0
  12. package/dist/interfaces/entry.interface.d.ts +10 -0
  13. package/dist/interfaces/entry.interface.js +2 -0
  14. package/dist/interfaces/telescope-options.interface.d.ts +13 -0
  15. package/dist/interfaces/telescope-options.interface.js +2 -0
  16. package/dist/storage/entities/telescope-entry.entity.d.ts +10 -0
  17. package/dist/storage/entities/telescope-entry.entity.js +56 -0
  18. package/dist/storage/telescope-repository.service.d.ts +18 -0
  19. package/dist/storage/telescope-repository.service.js +77 -0
  20. package/dist/telescope.module.d.ts +4 -0
  21. package/dist/telescope.module.js +84 -0
  22. package/dist/telescope.service.d.ts +13 -0
  23. package/dist/telescope.service.js +62 -0
  24. package/dist/watchers/cache.watcher.d.ts +7 -0
  25. package/dist/watchers/cache.watcher.js +62 -0
  26. package/dist/watchers/event.watcher.d.ts +13 -0
  27. package/dist/watchers/event.watcher.js +51 -0
  28. package/dist/watchers/exception.watcher.d.ts +8 -0
  29. package/dist/watchers/exception.watcher.js +44 -0
  30. package/dist/watchers/http-request.watcher.d.ts +8 -0
  31. package/dist/watchers/http-request.watcher.js +52 -0
  32. package/dist/watchers/log.watcher.d.ts +7 -0
  33. package/dist/watchers/log.watcher.js +63 -0
  34. package/dist/watchers/mail.watcher.d.ts +12 -0
  35. package/dist/watchers/mail.watcher.js +90 -0
  36. package/dist/watchers/query.watcher.d.ts +10 -0
  37. package/dist/watchers/query.watcher.js +52 -0
  38. package/dist/watchers/queue.watcher.d.ts +10 -0
  39. package/dist/watchers/queue.watcher.js +66 -0
  40. package/dist/watchers/redis.watcher.d.ts +7 -0
  41. package/dist/watchers/redis.watcher.js +74 -0
  42. package/dist/watchers/schedule.watcher.d.ts +9 -0
  43. package/dist/watchers/schedule.watcher.js +92 -0
  44. package/package.json +49 -0
  45. package/ui/index.html +1268 -0
package/ui/index.html ADDED
@@ -0,0 +1,1268 @@
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: Inter & 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=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
+
12
+ <style>
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;
21
+ --primary: #6366f1;
22
+ --primary-hover: #4f46e5;
23
+ --accent-green: #10b981;
24
+ --accent-red: #ef4444;
25
+ --accent-blue: #3b82f6;
26
+ --accent-purple: #8b5cf6;
27
+ --accent-amber: #f59e0b;
28
+ --font-sans: 'Inter', sans-serif;
29
+ --font-mono: 'JetBrains Mono', monospace;
30
+ }
31
+
32
+ * {
33
+ box-sizing: border-box;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
37
+
38
+ body {
39
+ font-family: var(--font-sans);
40
+ background-color: var(--bg-main);
41
+ color: var(--text-main);
42
+ overflow: hidden;
43
+ height: 100vh;
44
+ display: flex;
45
+ }
46
+
47
+ /* Scrollbars */
48
+ ::-webkit-scrollbar {
49
+ width: 6px;
50
+ height: 6px;
51
+ }
52
+ ::-webkit-scrollbar-track {
53
+ background: rgba(0, 0, 0, 0.1);
54
+ }
55
+ ::-webkit-scrollbar-thumb {
56
+ background: rgba(255, 255, 255, 0.1);
57
+ border-radius: 4px;
58
+ }
59
+ ::-webkit-scrollbar-thumb:hover {
60
+ background: rgba(255, 255, 255, 0.2);
61
+ }
62
+
63
+ #app {
64
+ width: 100%;
65
+ height: 100%;
66
+ display: flex;
67
+ }
68
+
69
+ /* Sidebar */
70
+ .sidebar {
71
+ width: 260px;
72
+ background-color: var(--bg-sidebar);
73
+ border-right: 1px solid var(--border-color);
74
+ display: flex;
75
+ flex-direction: column;
76
+ flex-shrink: 0;
77
+ }
78
+
79
+ .sidebar-header {
80
+ padding: 24px;
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 12px;
84
+ border-bottom: 1px solid var(--border-color);
85
+ }
86
+
87
+ .logo {
88
+ background: linear-gradient(135deg, var(--primary), #a78bfa);
89
+ width: 36px;
90
+ height: 36px;
91
+ border-radius: 8px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ font-weight: 700;
96
+ color: #fff;
97
+ font-size: 1.2rem;
98
+ box-shadow: 0 0 15px rgba(99, 102, 241, 0.4);
99
+ }
100
+
101
+ .sidebar-title {
102
+ font-weight: 600;
103
+ font-size: 1.1rem;
104
+ letter-spacing: -0.025em;
105
+ background: linear-gradient(to right, #fff, #9ca3af);
106
+ -webkit-background-clip: text;
107
+ -webkit-text-fill-color: transparent;
108
+ }
109
+
110
+ .nav-list {
111
+ list-style: none;
112
+ padding: 16px 12px;
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 4px;
116
+ overflow-y: auto;
117
+ flex-grow: 1;
118
+ }
119
+
120
+ .nav-item {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 12px;
124
+ padding: 10px 16px;
125
+ border-radius: 8px;
126
+ cursor: pointer;
127
+ color: var(--text-muted);
128
+ font-weight: 500;
129
+ font-size: 0.9rem;
130
+ transition: all 0.2s ease;
131
+ text-decoration: none;
132
+ }
133
+
134
+ .nav-item:hover {
135
+ color: var(--text-main);
136
+ background-color: rgba(255, 255, 255, 0.03);
137
+ }
138
+
139
+ .nav-item.active {
140
+ color: #fff;
141
+ background-color: rgba(99, 102, 241, 0.15);
142
+ border-left: 3px solid var(--primary);
143
+ }
144
+
145
+ .nav-icon {
146
+ width: 18px;
147
+ height: 18px;
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ }
152
+
153
+ .sidebar-footer {
154
+ padding: 16px;
155
+ border-top: 1px solid var(--border-color);
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ font-size: 0.8rem;
160
+ color: var(--text-muted);
161
+ }
162
+
163
+ .logout-btn {
164
+ background: none;
165
+ border: none;
166
+ color: var(--accent-red);
167
+ cursor: pointer;
168
+ font-weight: 500;
169
+ }
170
+
171
+ .logout-btn:hover {
172
+ text-decoration: underline;
173
+ }
174
+
175
+ /* Main Content */
176
+ .main-container {
177
+ flex-grow: 1;
178
+ display: flex;
179
+ flex-direction: column;
180
+ overflow: hidden;
181
+ background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.05), transparent 600px);
182
+ }
183
+
184
+ /* Header */
185
+ .header {
186
+ height: 70px;
187
+ border-bottom: 1px solid var(--border-color);
188
+ padding: 0 24px;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: space-between;
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ .header-left {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 16px;
199
+ }
200
+
201
+ .view-title {
202
+ font-size: 1.25rem;
203
+ font-weight: 600;
204
+ }
205
+
206
+ .header-right {
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 16px;
210
+ }
211
+
212
+ .search-input {
213
+ background-color: var(--bg-input);
214
+ border: 1px solid var(--border-color);
215
+ border-radius: 8px;
216
+ color: var(--text-main);
217
+ padding: 8px 16px;
218
+ font-size: 0.875rem;
219
+ outline: none;
220
+ width: 240px;
221
+ transition: all 0.2s ease;
222
+ }
223
+
224
+ .search-input:focus {
225
+ border-color: var(--primary);
226
+ box-shadow: 0 0 8px rgba(99, 102, 241, 0.2);
227
+ }
228
+
229
+ .btn {
230
+ background-color: var(--primary);
231
+ color: #fff;
232
+ border: none;
233
+ border-radius: 8px;
234
+ padding: 8px 16px;
235
+ font-size: 0.875rem;
236
+ font-weight: 500;
237
+ cursor: pointer;
238
+ transition: all 0.2s ease;
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 8px;
242
+ }
243
+
244
+ .btn:hover {
245
+ background-color: var(--primary-hover);
246
+ }
247
+
248
+ .btn-danger {
249
+ background-color: rgba(239, 68, 68, 0.1);
250
+ color: var(--accent-red);
251
+ border: 1px solid rgba(239, 68, 68, 0.2);
252
+ }
253
+
254
+ .btn-danger:hover {
255
+ background-color: var(--accent-red);
256
+ color: #fff;
257
+ }
258
+
259
+ .toggle-container {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 8px;
263
+ font-size: 0.85rem;
264
+ color: var(--text-muted);
265
+ cursor: pointer;
266
+ }
267
+
268
+ .switch {
269
+ position: relative;
270
+ display: inline-block;
271
+ width: 36px;
272
+ height: 20px;
273
+ }
274
+
275
+ .switch input {
276
+ opacity: 0;
277
+ width: 0;
278
+ height: 0;
279
+ }
280
+
281
+ .slider {
282
+ position: absolute;
283
+ cursor: pointer;
284
+ top: 0;
285
+ left: 0;
286
+ right: 0;
287
+ bottom: 0;
288
+ background-color: var(--bg-input);
289
+ transition: .3s;
290
+ border-radius: 34px;
291
+ border: 1px solid var(--border-color);
292
+ }
293
+
294
+ .slider:before {
295
+ position: absolute;
296
+ content: "";
297
+ height: 12px;
298
+ width: 12px;
299
+ left: 3px;
300
+ bottom: 3px;
301
+ background-color: var(--text-muted);
302
+ transition: .3s;
303
+ border-radius: 50%;
304
+ }
305
+
306
+ input:checked + .slider {
307
+ background-color: var(--primary);
308
+ }
309
+
310
+ input:checked + .slider:before {
311
+ transform: translateX(16px);
312
+ background-color: #fff;
313
+ }
314
+
315
+ /* Content Area */
316
+ .content-area {
317
+ flex-grow: 1;
318
+ overflow-y: auto;
319
+ padding: 24px;
320
+ }
321
+
322
+ /* Cards & Glassmorphism */
323
+ .card {
324
+ background-color: var(--bg-card);
325
+ backdrop-filter: blur(10px);
326
+ border: 1px solid var(--border-color);
327
+ border-radius: 12px;
328
+ overflow: hidden;
329
+ }
330
+
331
+ /* List Table */
332
+ .table-container {
333
+ overflow-x: auto;
334
+ }
335
+
336
+ table {
337
+ width: 100%;
338
+ border-collapse: collapse;
339
+ text-align: left;
340
+ }
341
+
342
+ th {
343
+ padding: 14px 20px;
344
+ font-size: 0.75rem;
345
+ text-transform: uppercase;
346
+ letter-spacing: 0.05em;
347
+ color: var(--text-muted);
348
+ border-bottom: 1px solid var(--border-color);
349
+ font-weight: 600;
350
+ }
351
+
352
+ td {
353
+ padding: 14px 20px;
354
+ font-size: 0.875rem;
355
+ border-bottom: 1px solid var(--border-color);
356
+ color: var(--text-main);
357
+ max-width: 400px;
358
+ white-space: nowrap;
359
+ overflow: hidden;
360
+ text-overflow: ellipsis;
361
+ }
362
+
363
+ tr:last-child td {
364
+ border-bottom: none;
365
+ }
366
+
367
+ tr {
368
+ cursor: pointer;
369
+ transition: background-color 0.2s ease;
370
+ }
371
+
372
+ tr:hover {
373
+ background-color: rgba(255, 255, 255, 0.02);
374
+ }
375
+
376
+ /* Tags */
377
+ .badge {
378
+ display: inline-flex;
379
+ align-items: center;
380
+ padding: 4px 8px;
381
+ border-radius: 6px;
382
+ font-size: 0.75rem;
383
+ font-weight: 600;
384
+ letter-spacing: 0.025em;
385
+ }
386
+
387
+ .badge-method {
388
+ text-transform: uppercase;
389
+ }
390
+ .badge-get { background-color: rgba(16, 185, 129, 0.15); color: var(--accent-green); }
391
+ .badge-post { background-color: rgba(59, 130, 246, 0.15); color: var(--accent-blue); }
392
+ .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); }
394
+ .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); }
396
+
397
+ /* Login Modal */
398
+ .login-overlay {
399
+ position: fixed;
400
+ top: 0;
401
+ left: 0;
402
+ right: 0;
403
+ bottom: 0;
404
+ background-color: rgba(11, 15, 25, 0.95);
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
408
+ z-index: 1000;
409
+ }
410
+
411
+ .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);
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 24px;
421
+ text-align: center;
422
+ }
423
+
424
+ .login-logo-wrapper {
425
+ display: flex;
426
+ justify-content: center;
427
+ }
428
+
429
+ .login-input {
430
+ background-color: var(--bg-main);
431
+ border: 1px solid var(--border-color);
432
+ border-radius: 8px;
433
+ color: var(--text-main);
434
+ padding: 12px 16px;
435
+ font-size: 1rem;
436
+ outline: none;
437
+ width: 100%;
438
+ text-align: center;
439
+ transition: all 0.2s ease;
440
+ }
441
+
442
+ .login-input:focus {
443
+ border-color: var(--primary);
444
+ }
445
+
446
+ .error-msg {
447
+ color: var(--accent-red);
448
+ font-size: 0.85rem;
449
+ }
450
+
451
+ /* Detail Modal */
452
+ .modal-overlay {
453
+ position: fixed;
454
+ top: 0;
455
+ left: 0;
456
+ right: 0;
457
+ bottom: 0;
458
+ background-color: rgba(0, 0, 0, 0.7);
459
+ backdrop-filter: blur(4px);
460
+ display: flex;
461
+ align-items: center;
462
+ justify-content: center;
463
+ z-index: 999;
464
+ padding: 40px;
465
+ }
466
+
467
+ .modal-card {
468
+ width: 100%;
469
+ max-width: 900px;
470
+ height: 90%;
471
+ background-color: var(--bg-sidebar);
472
+ border: 1px solid var(--border-color);
473
+ border-radius: 16px;
474
+ display: flex;
475
+ flex-direction: column;
476
+ overflow: hidden;
477
+ animation: modalFadeIn 0.2s ease-out;
478
+ }
479
+
480
+ @keyframes modalFadeIn {
481
+ from { transform: scale(0.95); opacity: 0; }
482
+ to { transform: scale(1); opacity: 1; }
483
+ }
484
+
485
+ .modal-header {
486
+ padding: 20px 24px;
487
+ border-bottom: 1px solid var(--border-color);
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: space-between;
491
+ }
492
+
493
+ .modal-title-group {
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 12px;
497
+ }
498
+
499
+ .modal-close {
500
+ background: none;
501
+ border: none;
502
+ color: var(--text-muted);
503
+ cursor: pointer;
504
+ font-size: 1.5rem;
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: center;
508
+ width: 32px;
509
+ height: 32px;
510
+ border-radius: 50%;
511
+ transition: all 0.2s ease;
512
+ }
513
+
514
+ .modal-close:hover {
515
+ background-color: rgba(255, 255, 255, 0.05);
516
+ color: #fff;
517
+ }
518
+
519
+ .modal-tabs {
520
+ display: flex;
521
+ background-color: rgba(0, 0, 0, 0.2);
522
+ border-bottom: 1px solid var(--border-color);
523
+ padding: 0 24px;
524
+ }
525
+
526
+ .modal-tab {
527
+ padding: 14px 20px;
528
+ cursor: pointer;
529
+ color: var(--text-muted);
530
+ font-weight: 500;
531
+ font-size: 0.9rem;
532
+ border-bottom: 2px solid transparent;
533
+ transition: all 0.2s ease;
534
+ }
535
+
536
+ .modal-tab:hover {
537
+ color: var(--text-main);
538
+ }
539
+
540
+ .modal-tab.active {
541
+ color: var(--primary);
542
+ border-bottom-color: var(--primary);
543
+ }
544
+
545
+ .modal-body {
546
+ flex-grow: 1;
547
+ overflow-y: auto;
548
+ padding: 24px;
549
+ }
550
+
551
+ /* JSON display */
552
+ .json-code {
553
+ 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;
558
+ border: 1px solid var(--border-color);
559
+ white-space: pre-wrap;
560
+ word-break: break-all;
561
+ color: #cbd5e1;
562
+ overflow-x: auto;
563
+ }
564
+
565
+ .key-val-list {
566
+ display: flex;
567
+ flex-direction: column;
568
+ gap: 12px;
569
+ }
570
+
571
+ .key-val-row {
572
+ display: grid;
573
+ grid-template-columns: 200px 1fr;
574
+ gap: 16px;
575
+ border-bottom: 1px solid var(--border-color);
576
+ padding-bottom: 12px;
577
+ }
578
+
579
+ .key-val-row:last-child {
580
+ border-bottom: none;
581
+ }
582
+
583
+ .key-val-label {
584
+ font-weight: 600;
585
+ color: var(--text-muted);
586
+ font-size: 0.875rem;
587
+ }
588
+
589
+ .key-val-value {
590
+ font-size: 0.875rem;
591
+ word-break: break-all;
592
+ }
593
+
594
+ /* Loading Spinner */
595
+ .spinner {
596
+ border: 3px solid rgba(255, 255, 255, 0.1);
597
+ border-top: 3px solid var(--primary);
598
+ border-radius: 50%;
599
+ width: 24px;
600
+ height: 24px;
601
+ animation: spin 1s linear infinite;
602
+ }
603
+
604
+ @keyframes spin {
605
+ 0% { transform: rotate(0deg); }
606
+ 100% { transform: rotate(360deg); }
607
+ }
608
+
609
+ .loading-container {
610
+ display: flex;
611
+ align-items: center;
612
+ justify-content: center;
613
+ height: 200px;
614
+ }
615
+
616
+ .empty-state {
617
+ text-align: center;
618
+ padding: 48px;
619
+ color: var(--text-muted);
620
+ }
621
+
622
+ .empty-icon {
623
+ font-size: 2.5rem;
624
+ margin-bottom: 12px;
625
+ opacity: 0.5;
626
+ }
627
+ </style>
628
+ </head>
629
+ <body>
630
+ <div id="app"></div>
631
+
632
+ <!-- UMD React dependencies loaded securely -->
633
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
634
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
635
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
636
+
637
+ <script type="text/babel">
638
+ const { useState, useEffect, useRef } = React;
639
+
640
+ const NAVIGATION_TABS = [
641
+ { id: 'all', label: 'All Entries', icon: '🔍' },
642
+ { id: 'request', label: 'HTTP Requests', icon: '🌐' },
643
+ { id: 'query', label: 'Database Queries', icon: '💾' },
644
+ { id: 'cache', label: 'Cache Ops', icon: '⚡' },
645
+ { id: 'job', label: 'Queue Jobs', icon: '⚙️' },
646
+ { id: 'event', label: 'Events', icon: '📢' },
647
+ { id: 'mail', label: 'Mails', icon: '✉️' },
648
+ { id: 'log', label: 'Logs', icon: '📋' },
649
+ { id: 'exception', label: 'Exceptions', icon: '❌' },
650
+ { id: 'scheduled_task', label: 'Scheduled Tasks', icon: '⏱️' },
651
+ { id: 'redis', label: 'Redis Commands', icon: '🔑' },
652
+ ];
653
+
654
+ function App() {
655
+ const [token, setToken] = useState(localStorage.getItem('telescope_token') || '');
656
+ const [password, setPassword] = useState('');
657
+ const [loginError, setLoginError] = useState('');
658
+ const [activeTab, setActiveTab] = useState('all');
659
+ const [entries, setEntries] = useState([]);
660
+ const [searchTerm, setSearchTerm] = useState('');
661
+ const [loading, setLoading] = useState(false);
662
+ const [polling, setPolling] = useState(true);
663
+ const [selectedEntry, setSelectedEntry] = useState(null);
664
+ const [modalTab, setModalTab] = useState('details');
665
+
666
+ const prefixPath = window.location.pathname.split('/api')[0].split('/')[1] || 'telescope';
667
+
668
+ useEffect(() => {
669
+ if (token) {
670
+ fetchEntries();
671
+ }
672
+ }, [token, activeTab, searchTerm]);
673
+
674
+ useEffect(() => {
675
+ let interval;
676
+ if (polling && token) {
677
+ interval = setInterval(() => {
678
+ fetchEntries(true);
679
+ }, 3000);
680
+ }
681
+ return () => clearInterval(interval);
682
+ }, [polling, token, activeTab, searchTerm]);
683
+
684
+ const fetchEntries = async (silent = false) => {
685
+ if (!silent) setLoading(true);
686
+ try {
687
+ const typeQuery = activeTab !== 'all' ? `type=${activeTab}` : '';
688
+ const searchQuery = searchTerm ? `search=${encodeURIComponent(searchTerm)}` : '';
689
+ const query = [typeQuery, searchQuery].filter(Boolean).join('&');
690
+
691
+ const res = await fetch(`/${prefixPath}/api/entries?${query}`, {
692
+ headers: {
693
+ 'Authorization': `Bearer ${token}`
694
+ }
695
+ });
696
+ if (res.status === 401) {
697
+ handleLogout();
698
+ return;
699
+ }
700
+ const data = await res.json();
701
+ setEntries(data);
702
+ } catch (e) {
703
+ console.error(e);
704
+ } finally {
705
+ setLoading(false);
706
+ }
707
+ };
708
+
709
+ const handleLogin = async (e) => {
710
+ e.preventDefault();
711
+ setLoginError('');
712
+ try {
713
+ const res = await fetch(`/${prefixPath}/api/login`, {
714
+ method: 'POST',
715
+ headers: { 'Content-Type': 'application/json' },
716
+ body: JSON.stringify({ password })
717
+ });
718
+ if (!res.ok) {
719
+ throw new Error('Invalid password');
720
+ }
721
+ const data = await res.json();
722
+ localStorage.setItem('telescope_token', data.token);
723
+ setToken(data.token);
724
+ } catch (err) {
725
+ setLoginError('Authentication failed. Check password.');
726
+ }
727
+ };
728
+
729
+ const handleLogout = () => {
730
+ localStorage.removeItem('telescope_token');
731
+ setToken('');
732
+ };
733
+
734
+ const handleClearAll = async () => {
735
+ if (!confirm('Are you sure you want to clear all recorded entries?')) return;
736
+ try {
737
+ await fetch(`/${prefixPath}/api/entries`, {
738
+ method: 'DELETE',
739
+ headers: {
740
+ 'Authorization': `Bearer ${token}`
741
+ }
742
+ });
743
+ setEntries([]);
744
+ setSelectedEntry(null);
745
+ } catch (e) {
746
+ console.error(e);
747
+ }
748
+ };
749
+
750
+ const formatTime = (dateStr) => {
751
+ const date = new Date(dateStr);
752
+ return date.toLocaleTimeString() + ' ' + date.toLocaleDateString();
753
+ };
754
+
755
+ const getRelativeTime = (dateStr) => {
756
+ const date = new Date(dateStr);
757
+ const seconds = Math.floor((new Date() - date) / 1000);
758
+ if (seconds < 5) return 'just now';
759
+ if (seconds < 60) return `${seconds}s ago`;
760
+ const minutes = Math.floor(seconds / 60);
761
+ if (minutes < 60) return `${minutes}m ago`;
762
+ const hours = Math.floor(minutes / 60);
763
+ if (hours < 24) return `${hours}h ago`;
764
+ return date.toLocaleDateString();
765
+ };
766
+
767
+ if (!token) {
768
+ return (
769
+ <div className="login-overlay">
770
+ <form className="login-card" onSubmit={handleLogin}>
771
+ <div className="login-logo-wrapper">
772
+ <div className="logo">T</div>
773
+ </div>
774
+ <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.
778
+ </p>
779
+ </div>
780
+ <input
781
+ type="password"
782
+ className="login-input"
783
+ placeholder="Enter password"
784
+ value={password}
785
+ onChange={(e) => setPassword(e.target.value)}
786
+ required
787
+ autoFocus
788
+ />
789
+ {loginError && <div className="error-msg">{loginError}</div>}
790
+ <button type="submit" className="btn" style={{ justifyContent: 'center' }}>
791
+ Access Dashboard
792
+ </button>
793
+ </form>
794
+ </div>
795
+ );
796
+ }
797
+
798
+ return (
799
+ <React.Fragment>
800
+ {/* Sidebar */}
801
+ <div className="sidebar">
802
+ <div className="sidebar-header">
803
+ <div className="logo">T</div>
804
+ <div className="sidebar-title">Telescope</div>
805
+ </div>
806
+ <ul className="nav-list">
807
+ {NAVIGATION_TABS.map((tab) => (
808
+ <li
809
+ key={tab.id}
810
+ className={`nav-item ${activeTab === tab.id ? 'active' : ''}`}
811
+ onClick={() => {
812
+ setActiveTab(tab.id);
813
+ setSelectedEntry(null);
814
+ }}
815
+ >
816
+ <span className="nav-icon">{tab.icon}</span>
817
+ {tab.label}
818
+ </li>
819
+ ))}
820
+ </ul>
821
+ <div className="sidebar-footer">
822
+ <span>NestJS Telescope</span>
823
+ <button className="logout-btn" onClick={handleLogout}>Log Out</button>
824
+ </div>
825
+ </div>
826
+
827
+ {/* Main Panel */}
828
+ <div className="main-container">
829
+ {/* Header */}
830
+ <div className="header">
831
+ <div className="header-left">
832
+ <div className="view-title">
833
+ {NAVIGATION_TABS.find((t) => t.id === activeTab)?.label}
834
+ </div>
835
+ <div className="toggle-container" onClick={() => setPolling(!polling)}>
836
+ <label className="switch">
837
+ <input type="checkbox" checked={polling} onChange={() => {}} />
838
+ <span className="slider"></span>
839
+ </label>
840
+ <span>Auto-Refresh</span>
841
+ </div>
842
+ </div>
843
+ <div className="header-right">
844
+ <input
845
+ type="text"
846
+ placeholder="Search entries..."
847
+ className="search-input"
848
+ value={searchTerm}
849
+ onChange={(e) => setSearchTerm(e.target.value)}
850
+ />
851
+ <button className="btn btn-danger" onClick={handleClearAll}>
852
+ Clear Entries
853
+ </button>
854
+ </div>
855
+ </div>
856
+
857
+ {/* Content Area */}
858
+ <div className="content-area">
859
+ {loading ? (
860
+ <div className="loading-container">
861
+ <div className="spinner"></div>
862
+ </div>
863
+ ) : entries.length === 0 ? (
864
+ <div className="card empty-state">
865
+ <div className="empty-icon">📭</div>
866
+ <h3>No Entries Found</h3>
867
+ <p style={{ marginTop: '8px' }}>
868
+ Trigger some actions in your application to see activity recorded here.
869
+ </p>
870
+ </div>
871
+ ) : (
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>
910
+ </div>
911
+ )}
912
+ </div>
913
+ </div>
914
+
915
+ {/* Details Modal */}
916
+ {selectedEntry && (
917
+ <div className="modal-overlay" onClick={() => setSelectedEntry(null)}>
918
+ <div className="modal-card" onClick={(e) => e.stopPropagation()}>
919
+ <div className="modal-header">
920
+ <div className="modal-title-group">
921
+ <span className={`badge badge-method badge-${selectedEntry.type}`}>
922
+ {selectedEntry.type}
923
+ </span>
924
+ <h3 style={{ fontSize: '1.1rem' }}>Entry Details</h3>
925
+ <span style={{ color: 'var(--text-muted)', fontSize: '0.8rem', fontFamily: 'var(--font-mono)' }}>
926
+ {selectedEntry.uuid}
927
+ </span>
928
+ </div>
929
+ <button className="modal-close" onClick={() => setSelectedEntry(null)}>×</button>
930
+ </div>
931
+
932
+ <div className="modal-tabs">
933
+ <div
934
+ className={`modal-tab ${modalTab === 'details' ? 'active' : ''}`}
935
+ onClick={() => setModalTab('details')}
936
+ >
937
+ Summary
938
+ </div>
939
+ <div
940
+ className={`modal-tab ${modalTab === 'content' ? 'active' : ''}`}
941
+ onClick={() => setModalTab('content')}
942
+ >
943
+ Raw Content
944
+ </div>
945
+ </div>
946
+
947
+ <div className="modal-body">
948
+ {modalTab === 'details' ? (
949
+ <div className="key-val-list">
950
+ <div className="key-val-row">
951
+ <div className="key-val-label">Recorded At</div>
952
+ <div className="key-val-value">{formatTime(selectedEntry.recordedAt)}</div>
953
+ </div>
954
+ <div className="key-val-row">
955
+ <div className="key-val-label">Entry Type</div>
956
+ <div className="key-val-value" style={{ textTransform: 'capitalize' }}>
957
+ {selectedEntry.type}
958
+ </div>
959
+ </div>
960
+ {renderStructuredDetails(selectedEntry)}
961
+ </div>
962
+ ) : (
963
+ <pre className="json-code">
964
+ {JSON.stringify(selectedEntry.content, null, 2)}
965
+ </pre>
966
+ )}
967
+ </div>
968
+ </div>
969
+ </div>
970
+ )}
971
+ </React.Fragment>
972
+ );
973
+ }
974
+
975
+ // Helper functions for displaying table items
976
+ function getEntrySummary(entry) {
977
+ const { type, content } = entry;
978
+ switch (type) {
979
+ case 'request':
980
+ const req = content.request || {};
981
+ const duration = content.duration ? `${content.duration}ms` : '';
982
+ const status = content.response?.statusCode || content.response?.status || 200;
983
+ const statusBadge = status >= 400 ? (
984
+ <span className="badge badge-status-err">{status}</span>
985
+ ) : (
986
+ <span className="badge badge-status-ok">{status}</span>
987
+ );
988
+ return {
989
+ title: `${req.method || 'GET'} ${req.url || '/'}`,
990
+ subtitle: duration,
991
+ status: statusBadge
992
+ };
993
+ case 'query':
994
+ return {
995
+ title: content.query || 'DB Query',
996
+ subtitle: `${content.operation || 'QUERY'} on ${content.entity || 'db'}`,
997
+ status: <span className="badge badge-status-ok">success</span>
998
+ };
999
+ case 'cache':
1000
+ return {
1001
+ title: `${content.action} cache key`,
1002
+ subtitle: content.key || '',
1003
+ status: <span className="badge badge-status-ok">{content.action}</span>
1004
+ };
1005
+ case 'job':
1006
+ return {
1007
+ title: `Job ${content.jobId || 'Unknown'}`,
1008
+ subtitle: content.status || '',
1009
+ status: content.status === 'failed' ? (
1010
+ <span className="badge badge-status-err">failed</span>
1011
+ ) : (
1012
+ <span className="badge badge-status-ok">{content.status}</span>
1013
+ )
1014
+ };
1015
+ case 'event':
1016
+ return {
1017
+ title: content.event || 'Application Event',
1018
+ subtitle: `Listeners: ${content.listeners ?? 0}`,
1019
+ status: <span className="badge badge-status-ok">fired</span>
1020
+ };
1021
+ case 'mail':
1022
+ return {
1023
+ title: content.subject || 'No Subject',
1024
+ subtitle: `to: ${content.to || 'unknown'}`,
1025
+ status: <span className="badge badge-status-ok">sent</span>
1026
+ };
1027
+ case 'log':
1028
+ const lvl = content.level || 'log';
1029
+ const lvlBadge = lvl === 'error' ? (
1030
+ <span className="badge badge-status-err">error</span>
1031
+ ) : lvl === 'warn' ? (
1032
+ <span className="badge badge-status-err" style={{ backgroundColor: 'rgba(245, 158, 11, 0.15)', color: 'var(--accent-amber)' }}>warn</span>
1033
+ ) : (
1034
+ <span className="badge badge-status-ok" style={{ backgroundColor: 'rgba(59, 130, 246, 0.15)', color: 'var(--accent-blue)' }}>{lvl}</span>
1035
+ );
1036
+ return {
1037
+ title: content.message || '',
1038
+ subtitle: '',
1039
+ status: lvlBadge
1040
+ };
1041
+ case 'exception':
1042
+ return {
1043
+ title: content.name || 'Exception',
1044
+ subtitle: content.message || '',
1045
+ status: <span className="badge badge-status-err">failed</span>
1046
+ };
1047
+ case 'scheduled_task':
1048
+ return {
1049
+ title: content.name || 'Cron Job',
1050
+ subtitle: `Cron: ${content.cronTime || ''}`,
1051
+ status: content.status === 'failed' ? (
1052
+ <span className="badge badge-status-err">failed</span>
1053
+ ) : (
1054
+ <span className="badge badge-status-ok">{content.status}</span>
1055
+ )
1056
+ };
1057
+ case 'redis':
1058
+ return {
1059
+ title: `${content.command || 'COMMAND'}`,
1060
+ subtitle: (content.arguments || []).join(' '),
1061
+ status: content.status === 'failed' ? (
1062
+ <span className="badge badge-status-err">failed</span>
1063
+ ) : (
1064
+ <span className="badge badge-status-ok">success</span>
1065
+ )
1066
+ };
1067
+ default:
1068
+ return {
1069
+ title: 'Telescope Entry',
1070
+ subtitle: '',
1071
+ status: null
1072
+ };
1073
+ }
1074
+ }
1075
+
1076
+ // Helper functions for displaying details inside modal
1077
+ function renderStructuredDetails(entry) {
1078
+ const { type, content } = entry;
1079
+ switch (type) {
1080
+ case 'request':
1081
+ return (
1082
+ <React.Fragment>
1083
+ <div className="key-val-row">
1084
+ <div className="key-val-label">HTTP Method</div>
1085
+ <div className="key-val-value">{content.request?.method}</div>
1086
+ </div>
1087
+ <div className="key-val-row">
1088
+ <div className="key-val-label">URL</div>
1089
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.request?.url}</div>
1090
+ </div>
1091
+ <div className="key-val-row">
1092
+ <div className="key-val-label">IP Address</div>
1093
+ <div className="key-val-value">{content.request?.ip}</div>
1094
+ </div>
1095
+ <div className="key-val-row">
1096
+ <div className="key-val-label">Duration</div>
1097
+ <div className="key-val-value">{content.duration} ms</div>
1098
+ </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>
1111
+ </React.Fragment>
1112
+ );
1113
+ case 'query':
1114
+ return (
1115
+ <React.Fragment>
1116
+ <div className="key-val-row">
1117
+ <div className="key-val-label">Operation</div>
1118
+ <div className="key-val-value" style={{ fontWeight: '600' }}>{content.operation}</div>
1119
+ </div>
1120
+ <div className="key-val-row">
1121
+ <div className="key-val-label">Entity/Table</div>
1122
+ <div className="key-val-value">{content.entity}</div>
1123
+ </div>
1124
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1125
+ <div className="key-val-label">SQL Query</div>
1126
+ <pre className="json-code" style={{ color: '#818cf8', fontWeight: '500' }}>{content.query}</pre>
1127
+ </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>
1136
+ </React.Fragment>
1137
+ );
1138
+ case 'exception':
1139
+ return (
1140
+ <React.Fragment>
1141
+ <div className="key-val-row">
1142
+ <div className="key-val-label">Name</div>
1143
+ <div className="key-val-value" style={{ color: 'var(--accent-red)', fontWeight: '600' }}>{content.name}</div>
1144
+ </div>
1145
+ <div className="key-val-row">
1146
+ <div className="key-val-label">Message</div>
1147
+ <div className="key-val-value" style={{ fontWeight: '500' }}>{content.message}</div>
1148
+ </div>
1149
+ {content.status && (
1150
+ <div className="key-val-row">
1151
+ <div className="key-val-label">HTTP Status</div>
1152
+ <div className="key-val-value">{content.status}</div>
1153
+ </div>
1154
+ )}
1155
+ {content.stack && (
1156
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1157
+ <div className="key-val-label">Stack Trace</div>
1158
+ <pre className="json-code" style={{ color: '#ef4444', overflowX: 'auto' }}>{content.stack}</pre>
1159
+ </div>
1160
+ )}
1161
+ </React.Fragment>
1162
+ );
1163
+ case 'log':
1164
+ return (
1165
+ <React.Fragment>
1166
+ <div className="key-val-row">
1167
+ <div className="key-val-label">Level</div>
1168
+ <div className="key-val-value" style={{ textTransform: 'uppercase' }}>{content.level}</div>
1169
+ </div>
1170
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1171
+ <div className="key-val-label">Log Message</div>
1172
+ <pre className="json-code">{content.message}</pre>
1173
+ </div>
1174
+ </React.Fragment>
1175
+ );
1176
+ case 'cache':
1177
+ return (
1178
+ <React.Fragment>
1179
+ <div className="key-val-row">
1180
+ <div className="key-val-label">Action</div>
1181
+ <div className="key-val-value">{content.action}</div>
1182
+ </div>
1183
+ <div className="key-val-row">
1184
+ <div className="key-val-label">Key</div>
1185
+ <div className="key-val-value" style={{ fontFamily: 'var(--font-mono)' }}>{content.key}</div>
1186
+ </div>
1187
+ {content.value !== undefined && (
1188
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1189
+ <div className="key-val-label">Stored Value</div>
1190
+ <pre className="json-code">{JSON.stringify(content.value, null, 2)}</pre>
1191
+ </div>
1192
+ )}
1193
+ {content.ttl !== undefined && (
1194
+ <div className="key-val-row">
1195
+ <div className="key-val-label">TTL</div>
1196
+ <div className="key-val-value">{content.ttl} seconds</div>
1197
+ </div>
1198
+ )}
1199
+ </React.Fragment>
1200
+ );
1201
+ case 'job':
1202
+ return (
1203
+ <React.Fragment>
1204
+ <div className="key-val-row">
1205
+ <div className="key-val-label">Job ID</div>
1206
+ <div className="key-val-value">{content.jobId}</div>
1207
+ </div>
1208
+ <div className="key-val-row">
1209
+ <div className="key-val-label">Status</div>
1210
+ <div className="key-val-value">{content.status}</div>
1211
+ </div>
1212
+ {content.returnvalue !== undefined && (
1213
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1214
+ <div className="key-val-label">Returned Value</div>
1215
+ <pre className="json-code">{JSON.stringify(content.returnvalue, null, 2)}</pre>
1216
+ </div>
1217
+ )}
1218
+ {content.failedReason && (
1219
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1220
+ <div className="key-val-label">Failure Reason</div>
1221
+ <pre className="json-code" style={{ color: 'var(--accent-red)' }}>{content.failedReason}</pre>
1222
+ </div>
1223
+ )}
1224
+ </React.Fragment>
1225
+ );
1226
+ case 'mail':
1227
+ return (
1228
+ <React.Fragment>
1229
+ <div className="key-val-row">
1230
+ <div className="key-val-label">From</div>
1231
+ <div className="key-val-value">{content.from}</div>
1232
+ </div>
1233
+ <div className="key-val-row">
1234
+ <div className="key-val-label">To</div>
1235
+ <div className="key-val-value">{content.to}</div>
1236
+ </div>
1237
+ <div className="key-val-row">
1238
+ <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>
1244
+ </div>
1245
+ {content.html && (
1246
+ <div className="key-val-row" style={{ flexDirection: 'column', display: 'flex', gap: '8px' }}>
1247
+ <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' }} />
1249
+ </div>
1250
+ )}
1251
+ </React.Fragment>
1252
+ );
1253
+ default:
1254
+ return (
1255
+ <div className="key-val-row">
1256
+ <div className="key-val-label">Payload</div>
1257
+ <pre className="json-code">{JSON.stringify(content, null, 2)}</pre>
1258
+ </div>
1259
+ );
1260
+ }
1261
+ }
1262
+
1263
+ const container = document.getElementById('app');
1264
+ const root = ReactDOM.createRoot(container);
1265
+ root.render(<App />);
1266
+ </script>
1267
+ </body>
1268
+ </html>