@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.
- package/README.md +42 -34
- package/dist/constants.d.ts +12 -3
- package/dist/constants.js +14 -5
- package/dist/controllers/telescope.controller.d.ts +5 -2
- package/dist/controllers/telescope.controller.js +41 -13
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/interfaces/telescope-options.interface.d.ts +9 -3
- package/dist/storage/entities/telescope-entry.entity.js +34 -7
- package/dist/storage/telescope-repository.service.d.ts +17 -7
- package/dist/storage/telescope-repository.service.js +42 -17
- package/dist/telescope.module.d.ts +6 -2
- package/dist/telescope.module.js +148 -44
- package/dist/telescope.service.d.ts +21 -1
- package/dist/telescope.service.js +69 -23
- package/dist/ui/index.html +1635 -0
- package/dist/watchers/cache.watcher.d.ts +5 -4
- package/dist/watchers/cache.watcher.js +50 -34
- package/dist/watchers/event.watcher.d.ts +7 -3
- package/dist/watchers/event.watcher.js +22 -6
- package/dist/watchers/exception.watcher.js +7 -1
- package/dist/watchers/http-request.watcher.d.ts +3 -1
- package/dist/watchers/http-request.watcher.js +25 -8
- package/dist/watchers/log.watcher.d.ts +1 -0
- package/dist/watchers/log.watcher.js +22 -17
- package/dist/watchers/mail.watcher.d.ts +1 -1
- package/dist/watchers/mail.watcher.js +28 -56
- package/dist/watchers/query.watcher.d.ts +6 -3
- package/dist/watchers/query.watcher.js +36 -8
- package/dist/watchers/queue.watcher.d.ts +2 -4
- package/dist/watchers/queue.watcher.js +38 -32
- package/dist/watchers/redis.watcher.d.ts +3 -1
- package/dist/watchers/redis.watcher.js +16 -4
- package/dist/watchers/schedule.watcher.d.ts +8 -3
- package/dist/watchers/schedule.watcher.js +25 -11
- package/package.json +51 -19
- 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>
|