create-tinny-backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2012 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <title>Admin Dashboard | Server Status</title>
7
+ <link rel="shortcut icon" href="/imgs/logo.png" type="image/x-icon">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+
11
+ <style>
12
+ :root {
13
+ --bg: #f4f6fa;
14
+ --text: #1e293b;
15
+ --text-secondary: #475569;
16
+ --text-muted: #64748b;
17
+ --border: #e2e8f0;
18
+ --card-bg: #ffffff;
19
+ --card-border: #e9eef3;
20
+ --sidebar-bg: #f8fafc;
21
+ --input-focus: #2563eb;
22
+ --heading: #0f172a;
23
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
24
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
25
+ --shadow-lg: 0 12px 40px rgba(0,0,0,0.1);
26
+ --toggle-bg: #f1f5f9;
27
+ --badge-bg: #dbeafe;
28
+ --badge-text: #1e40af;
29
+ --error-bg: #fee2e2;
30
+ --error-text: #991b1b;
31
+ --success-bg: #dcfce7;
32
+ --success-text: #166534;
33
+ --terminal-bg: #0f172a;
34
+ --terminal-text: #e2e8f0;
35
+ --terminal-border: #1e293b;
36
+ --icon-bg: #f8fafc;
37
+ --code-color: #6d8ecc;
38
+ --path-color: #f56565;
39
+ --modal-overlay: rgba(0,0,0,0.5);
40
+ --input-bg: #f8fafc;
41
+ --input-border: #e2e8f0;
42
+ --btn-bg: #2563eb;
43
+ --btn-hover: #1d4ed8;
44
+ --btn-text: #ffffff;
45
+ --scrollbar-track: #e2e8f0;
46
+ --scrollbar-thumb: #94a3b8;
47
+ }
48
+
49
+ [data-theme="dark"] {
50
+ --bg: #1a1a1a;
51
+ --text: #d4d4d4;
52
+ --text-secondary: #a0a0a0;
53
+ --text-muted: #808080;
54
+ --border: #3a3a3a;
55
+ --card-bg: #2a2a2a;
56
+ --card-border: #3a3a3a;
57
+ --sidebar-bg: #222222;
58
+ --input-focus: #60a5fa;
59
+ --heading: #e0e0e0;
60
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
61
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
62
+ --shadow-lg: 0 12px 40px rgba(0,0,0,0.5);
63
+ --toggle-bg: #333333;
64
+ --badge-bg: #2a2a2a;
65
+ --badge-text: #a0a0a0;
66
+ --error-bg: #3a1a1a;
67
+ --error-text: #fc8181;
68
+ --success-bg: #1a3a2a;
69
+ --success-text: #48bb78;
70
+ --terminal-bg: #0a0a0a;
71
+ --terminal-text: #d4d4d4;
72
+ --terminal-border: #2a2a2a;
73
+ --icon-bg: #222222;
74
+ --code-color: #60a5fa;
75
+ --path-color: #fc8181;
76
+ --modal-overlay: rgba(0,0,0,0.7);
77
+ --input-bg: #1e1e1e;
78
+ --input-border: #3a3a3a;
79
+ --btn-bg: #3b82f6;
80
+ --btn-hover: #2563eb;
81
+ --btn-text: #ffffff;
82
+ --scrollbar-track: #2a2a2a;
83
+ --scrollbar-thumb: #4a4a4a;
84
+ }
85
+
86
+ * {
87
+ margin: 0;
88
+ padding: 0;
89
+ box-sizing: border-box;
90
+ }
91
+
92
+ body {
93
+ background: var(--bg);
94
+ font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
95
+ padding: 2rem;
96
+ min-height: 100vh;
97
+ display: flex;
98
+ justify-content: center;
99
+ align-items: flex-start;
100
+ color: var(--text);
101
+ transition: background 0.3s ease, color 0.3s ease;
102
+ }
103
+
104
+ .dashboard {
105
+ max-width: 1440px;
106
+ width: 100%;
107
+ margin: 0 auto;
108
+ display: grid;
109
+ grid-template-columns: 320px 1fr;
110
+ gap: 2rem;
111
+ }
112
+
113
+ /* ===== LEFT PANEL - SESSIONS ===== */
114
+ .sessions-panel {
115
+ background: var(--card-bg);
116
+ border: 1px solid var(--card-border);
117
+ border-radius: 1.3rem;
118
+ overflow: hidden;
119
+ box-shadow: var(--shadow-md);
120
+ height: fit-content;
121
+ position: sticky;
122
+ top: 2rem;
123
+ transition: background 0.3s ease, border-color 0.3s ease;
124
+ }
125
+
126
+ .sessions-header {
127
+ background: var(--sidebar-bg);
128
+ padding: 1rem 1.2rem;
129
+ border-bottom: 1px solid var(--border);
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ transition: background 0.3s ease, border-color 0.3s ease;
134
+ }
135
+
136
+ .sessions-header h2 {
137
+ font-size: 0.9rem;
138
+ font-weight: 600;
139
+ color: var(--heading);
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 0.6rem;
143
+ }
144
+
145
+ .sessions-header h2 i {
146
+ color: var(--input-focus);
147
+ }
148
+
149
+ .session-count {
150
+ background: var(--toggle-bg);
151
+ padding: 0.2rem 0.7rem;
152
+ border-radius: 2rem;
153
+ font-size: 0.7rem;
154
+ color: var(--text-muted);
155
+ border: 1px solid var(--border);
156
+ }
157
+
158
+ .sessions-list {
159
+ padding: 0.8rem;
160
+ max-height: 600px;
161
+ overflow-y: auto;
162
+ }
163
+
164
+ .sessions-list::-webkit-scrollbar {
165
+ width: 4px;
166
+ }
167
+
168
+ .sessions-list::-webkit-scrollbar-track {
169
+ background: var(--scrollbar-track);
170
+ }
171
+
172
+ .sessions-list::-webkit-scrollbar-thumb {
173
+ background: var(--scrollbar-thumb);
174
+ border-radius: 10px;
175
+ }
176
+
177
+ .session-item {
178
+ background: var(--icon-bg);
179
+ border: 1px solid var(--border);
180
+ border-radius: 0.8rem;
181
+ padding: 0.8rem 1rem;
182
+ margin-bottom: 0.6rem;
183
+ transition: all 0.2s ease;
184
+ position: relative;
185
+ }
186
+
187
+ .session-item:hover {
188
+ border-color: var(--input-focus);
189
+ background: var(--toggle-bg);
190
+ }
191
+
192
+ .session-item.current-session {
193
+ border-color: var(--input-focus);
194
+ background: var(--badge-bg);
195
+ box-shadow: 0 0 20px rgba(37, 99, 235, 0.1);
196
+ border-left: 4px solid var(--input-focus);
197
+ }
198
+
199
+ .session-item.current-session::after {
200
+ content: '● Current';
201
+ position: absolute;
202
+ right: 0.8rem;
203
+ top: 0.8rem;
204
+ color: var(--input-focus);
205
+ font-size: 0.6rem;
206
+ font-weight: 600;
207
+ background: var(--badge-bg);
208
+ padding: 0.15rem 0.5rem;
209
+ border-radius: 1rem;
210
+ border: 1px solid var(--input-focus);
211
+ }
212
+
213
+ .session-item-header {
214
+ display: flex;
215
+ justify-content: space-between;
216
+ align-items: center;
217
+ margin-bottom: 0.4rem;
218
+ padding-right: 70px;
219
+ }
220
+
221
+ .session-id-display {
222
+ font-family: 'Courier New', monospace;
223
+ font-size: 0.75rem;
224
+ color: var(--text);
225
+ font-weight: 500;
226
+ }
227
+
228
+ .session-status-badge {
229
+ font-size: 0.6rem;
230
+ padding: 0.15rem 0.6rem;
231
+ border-radius: 2rem;
232
+ font-weight: 500;
233
+ text-transform: uppercase;
234
+ }
235
+
236
+ .session-status-badge.active {
237
+ background: var(--success-bg);
238
+ color: var(--success-text);
239
+ }
240
+
241
+ .session-status-badge.inactive {
242
+ background: var(--error-bg);
243
+ color: var(--error-text);
244
+ }
245
+
246
+ .session-details {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 0.25rem;
250
+ font-size: 0.7rem;
251
+ color: var(--text-muted);
252
+ }
253
+
254
+ .session-details span {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 0.4rem;
258
+ }
259
+
260
+ .session-details i {
261
+ width: 14px;
262
+ font-size: 0.65rem;
263
+ color: var(--text-muted);
264
+ }
265
+
266
+ .session-logout-btn {
267
+ position: absolute;
268
+ right: 0.8rem;
269
+ bottom: 0.8rem;
270
+ background: var(--error-bg);
271
+ border: 1px solid var(--error-text);
272
+ color: var(--error-text);
273
+ padding: 0.2rem 0.6rem;
274
+ border-radius: 1rem;
275
+ font-size: 0.6rem;
276
+ cursor: pointer;
277
+ transition: all 0.2s ease;
278
+ font-family: 'Inter', sans-serif;
279
+ font-weight: 500;
280
+ display: none;
281
+ }
282
+
283
+ .session-item:hover .session-logout-btn {
284
+ display: block;
285
+ }
286
+
287
+ .session-logout-btn:hover:not(:disabled) {
288
+ background: var(--error-text);
289
+ color: white;
290
+ }
291
+
292
+ .session-logout-btn:disabled {
293
+ opacity: 0.5;
294
+ cursor: not-allowed;
295
+ }
296
+
297
+ .no-sessions {
298
+ text-align: center;
299
+ padding: 2rem 1rem;
300
+ color: var(--text-muted);
301
+ font-size: 0.85rem;
302
+ }
303
+
304
+ .no-sessions i {
305
+ font-size: 2rem;
306
+ color: var(--border);
307
+ display: block;
308
+ margin-bottom: 0.8rem;
309
+ }
310
+
311
+ /* ===== RIGHT PANEL ===== */
312
+ .main-content {
313
+ display: flex;
314
+ flex-direction: column;
315
+ gap: 2rem;
316
+ }
317
+
318
+ /* Header */
319
+ .header {
320
+ display: flex;
321
+ justify-content: space-between;
322
+ align-items: center;
323
+ flex-wrap: wrap;
324
+ gap: 1rem;
325
+ }
326
+
327
+ .title-section h1 {
328
+ font-weight: 700;
329
+ font-size: 1.8rem;
330
+ letter-spacing: -0.3px;
331
+ color: var(--heading);
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 0.6rem;
335
+ }
336
+
337
+ .title-section h1 i {
338
+ color: var(--input-focus);
339
+ font-size: 1.8rem;
340
+ }
341
+
342
+ .sub {
343
+ color: var(--text-muted);
344
+ font-size: 0.85rem;
345
+ margin-top: 0.3rem;
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.5rem;
349
+ }
350
+
351
+ .header-actions {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 0.8rem;
355
+ flex-wrap: wrap;
356
+ }
357
+
358
+ .theme-toggle,
359
+ .refresh-btn,
360
+ .logout-btn,
361
+ .settings-btn {
362
+ background: var(--toggle-bg);
363
+ border: 1px solid var(--border);
364
+ color: var(--text);
365
+ padding: 0.6rem 1.3rem;
366
+ border-radius: 2.5rem;
367
+ font-size: 0.85rem;
368
+ font-weight: 500;
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 0.5rem;
372
+ cursor: pointer;
373
+ transition: all 0.2s ease;
374
+ letter-spacing: 0.3px;
375
+ font-family: 'Inter', sans-serif;
376
+ }
377
+
378
+ .theme-toggle {
379
+ padding: 0.6rem;
380
+ width: 42px;
381
+ justify-content: center;
382
+ }
383
+
384
+ .theme-toggle:hover,
385
+ .refresh-btn:hover,
386
+ .settings-btn:hover {
387
+ background: var(--input-focus);
388
+ border-color: var(--input-focus);
389
+ color: white;
390
+ }
391
+
392
+ .logout-btn {
393
+ background: var(--error-bg);
394
+ border-color: var(--error-text);
395
+ color: var(--error-text);
396
+ }
397
+
398
+ .logout-btn:hover:not(:disabled) {
399
+ background: var(--error-text);
400
+ border-color: var(--error-text);
401
+ color: white;
402
+ }
403
+
404
+ .logout-btn:disabled {
405
+ opacity: 0.5;
406
+ cursor: not-allowed;
407
+ }
408
+
409
+ .refresh-btn i,
410
+ .logout-btn i,
411
+ .theme-toggle i,
412
+ .settings-btn i {
413
+ font-size: 0.9rem;
414
+ }
415
+
416
+ /* ===== STATS ROW ===== */
417
+ .stats-row {
418
+ display: flex;
419
+ flex-wrap: wrap;
420
+ gap: 1.8rem;
421
+ justify-content: flex-start;
422
+ align-items: flex-start;
423
+ }
424
+
425
+ .stat-circle-group {
426
+ display: flex;
427
+ flex-direction: column;
428
+ align-items: center;
429
+ gap: 0.6rem;
430
+ min-width: 100px;
431
+ }
432
+
433
+ .circle-wrapper {
434
+ position: relative;
435
+ width: 90px;
436
+ height: 90px;
437
+ }
438
+
439
+ .circle-svg {
440
+ transform: rotate(-90deg);
441
+ width: 90px;
442
+ height: 90px;
443
+ }
444
+
445
+ .circle-bg {
446
+ fill: none;
447
+ stroke: var(--border);
448
+ stroke-width: 6;
449
+ }
450
+
451
+ .circle-progress {
452
+ fill: none;
453
+ stroke: var(--input-focus);
454
+ stroke-width: 6;
455
+ stroke-linecap: round;
456
+ transition: stroke-dashoffset 0.6s ease;
457
+ }
458
+
459
+ .circle-inner {
460
+ position: absolute;
461
+ top: 50%;
462
+ left: 50%;
463
+ transform: translate(-50%, -50%);
464
+ text-align: center;
465
+ font-weight: 500;
466
+ color: var(--text);
467
+ }
468
+
469
+ .circle-value {
470
+ font-size: 1.25rem;
471
+ font-weight: 600;
472
+ line-height: 1.2;
473
+ }
474
+
475
+ .circle-unit {
476
+ font-size: 0.7rem;
477
+ color: var(--text-muted);
478
+ }
479
+
480
+ .stat-label {
481
+ font-size: 0.75rem;
482
+ text-transform: uppercase;
483
+ letter-spacing: 0.6px;
484
+ color: var(--text-muted);
485
+ font-weight: 500;
486
+ text-align: center;
487
+ }
488
+
489
+ .stat-detail {
490
+ font-size: 0.7rem;
491
+ color: var(--text-muted);
492
+ margin-top: -0.3rem;
493
+ }
494
+
495
+ /* Memory bar card */
496
+ .memory-bar-card {
497
+ background: var(--card-bg);
498
+ border: 1px solid var(--card-border);
499
+ border-radius: 1.2rem;
500
+ padding: 1.2rem 1.3rem;
501
+ display: flex;
502
+ flex-direction: column;
503
+ gap: 0.5rem;
504
+ min-width: 180px;
505
+ box-shadow: var(--shadow-sm);
506
+ }
507
+
508
+ .memory-header {
509
+ display: flex;
510
+ justify-content: space-between;
511
+ font-size: 0.8rem;
512
+ color: var(--text-secondary);
513
+ font-weight: 500;
514
+ }
515
+
516
+ .memory-bar-bg {
517
+ background: var(--border);
518
+ height: 8px;
519
+ border-radius: 20px;
520
+ overflow: hidden;
521
+ width: 100%;
522
+ }
523
+
524
+ .memory-bar-fill {
525
+ background: var(--input-focus);
526
+ height: 100%;
527
+ border-radius: 20px;
528
+ width: 0%;
529
+ transition: width 0.5s ease;
530
+ }
531
+
532
+ .memory-stats {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ font-size: 0.75rem;
536
+ color: var(--text-muted);
537
+ }
538
+
539
+ /* ===== ROUTES SECTION ===== */
540
+ .routes-section {
541
+ background: var(--card-bg);
542
+ border: 1px solid var(--card-border);
543
+ border-radius: 1.3rem;
544
+ overflow: hidden;
545
+ box-shadow: var(--shadow-sm);
546
+ transition: background 0.3s ease, border-color 0.3s ease;
547
+ }
548
+
549
+ .routes-header {
550
+ background: var(--sidebar-bg);
551
+ padding: 0.8rem 1.2rem;
552
+ border-bottom: 1px solid var(--border);
553
+ display: flex;
554
+ justify-content: space-between;
555
+ align-items: center;
556
+ transition: background 0.3s ease, border-color 0.3s ease;
557
+ }
558
+
559
+ .routes-header h3 {
560
+ font-size: 0.85rem;
561
+ font-weight: 600;
562
+ color: var(--heading);
563
+ display: flex;
564
+ align-items: center;
565
+ gap: 0.6rem;
566
+ }
567
+
568
+ .routes-header h3 i {
569
+ color: var(--input-focus);
570
+ }
571
+
572
+ .routes-count {
573
+ background: var(--toggle-bg);
574
+ padding: 0.15rem 0.6rem;
575
+ border-radius: 2rem;
576
+ font-size: 0.65rem;
577
+ color: var(--text-muted);
578
+ border: 1px solid var(--border);
579
+ }
580
+
581
+ .routes-grid {
582
+ padding: 0.8rem 1rem;
583
+ display: flex;
584
+ flex-wrap: wrap;
585
+ gap: 0.6rem;
586
+ }
587
+
588
+ .route-tag {
589
+ background: var(--icon-bg);
590
+ border: 1px solid var(--border);
591
+ border-radius: 2rem;
592
+ padding: 0.4rem 0.9rem;
593
+ font-size: 0.75rem;
594
+ font-family: 'Courier New', monospace;
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 0.5rem;
598
+ transition: all 0.2s ease;
599
+ }
600
+
601
+ .route-tag:hover {
602
+ border-color: var(--input-focus);
603
+ background: var(--toggle-bg);
604
+ transform: translateY(-1px);
605
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
606
+ }
607
+
608
+ .route-method {
609
+ font-weight: 700;
610
+ font-size: 0.65rem;
611
+ padding: 0.1rem 0.4rem;
612
+ border-radius: 0.3rem;
613
+ letter-spacing: 0.5px;
614
+ }
615
+
616
+ .route-method.get { color: #48bb78; background: rgba(72, 187, 120, 0.15); }
617
+ .route-method.post { color: #ecc94b; background: rgba(236, 201, 75, 0.15); }
618
+ .route-method.put { color: #4299e1; background: rgba(66, 153, 225, 0.15); }
619
+ .route-method.delete { color: #fc8181; background: rgba(252, 129, 129, 0.15); }
620
+ .route-method.patch { color: #9f7aea; background: rgba(159, 122, 234, 0.15); }
621
+
622
+ .route-path {
623
+ color: var(--text);
624
+ }
625
+
626
+ .route-path i {
627
+ color: var(--text-muted);
628
+ font-size: 0.6rem;
629
+ margin: 0 0.2rem;
630
+ }
631
+
632
+ .no-routes {
633
+ padding: 1rem;
634
+ color: var(--text-muted);
635
+ font-size: 0.8rem;
636
+ text-align: center;
637
+ width: 100%;
638
+ }
639
+
640
+ /* ===== INFO CARDS ===== */
641
+ .info-cards {
642
+ display: flex;
643
+ flex-wrap: wrap;
644
+ gap: 1rem;
645
+ }
646
+
647
+ .info-card {
648
+ background: var(--card-bg);
649
+ border: 1px solid var(--card-border);
650
+ border-radius: 1rem;
651
+ padding: 0.9rem 1.3rem;
652
+ display: flex;
653
+ align-items: center;
654
+ gap: 0.8rem;
655
+ box-shadow: var(--shadow-sm);
656
+ transition: all 0.2s ease;
657
+ cursor: default;
658
+ }
659
+
660
+ .info-card:hover {
661
+ border-color: var(--input-focus);
662
+ box-shadow: var(--shadow-md);
663
+ }
664
+
665
+ .info-card i {
666
+ color: var(--input-focus);
667
+ font-size: 1.2rem;
668
+ }
669
+
670
+ .info-text {
671
+ display: flex;
672
+ flex-direction: column;
673
+ }
674
+
675
+ .info-label {
676
+ font-size: 0.7rem;
677
+ text-transform: uppercase;
678
+ color: var(--text-muted);
679
+ }
680
+
681
+ .info-value {
682
+ font-weight: 600;
683
+ color: var(--text);
684
+ }
685
+
686
+ /* ===== TERMINAL ===== */
687
+ .terminal-panel {
688
+ background: var(--terminal-bg);
689
+ border: 1px solid var(--terminal-border);
690
+ border-radius: 1.3rem;
691
+ overflow: hidden;
692
+ box-shadow: var(--shadow-lg);
693
+ display: flex;
694
+ flex-direction: column;
695
+ height: 380px;
696
+ transition: background 0.3s ease, border-color 0.3s ease;
697
+ }
698
+
699
+ .terminal-header {
700
+ background: var(--sidebar-bg);
701
+ padding: 0.7rem 1.2rem;
702
+ display: flex;
703
+ align-items: center;
704
+ gap: 0.6rem;
705
+ border-bottom: 1px solid var(--border);
706
+ transition: background 0.3s ease, border-color 0.3s ease;
707
+ }
708
+
709
+ .terminal-dot {
710
+ width: 11px;
711
+ height: 11px;
712
+ border-radius: 50%;
713
+ display: inline-block;
714
+ }
715
+
716
+ .dot-red { background: #f56565; }
717
+ .dot-yellow { background: #ecc94b; }
718
+ .dot-green { background: #48bb78; }
719
+
720
+ .terminal-title {
721
+ margin-left: 0.8rem;
722
+ color: var(--text-muted);
723
+ font-size: 0.8rem;
724
+ font-weight: 500;
725
+ letter-spacing: 0.3px;
726
+ }
727
+
728
+ .terminal-body {
729
+ flex: 1;
730
+ padding: 0.8rem 1rem;
731
+ overflow-y: auto;
732
+ font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
733
+ font-size: 0.8rem;
734
+ color: var(--terminal-text);
735
+ background: var(--terminal-bg);
736
+ display: flex;
737
+ flex-direction: column;
738
+ gap: 0.25rem;
739
+ }
740
+
741
+ .terminal-body::-webkit-scrollbar {
742
+ width: 4px;
743
+ }
744
+
745
+ .terminal-body::-webkit-scrollbar-track {
746
+ background: var(--terminal-border);
747
+ }
748
+
749
+ .terminal-body::-webkit-scrollbar-thumb {
750
+ background: var(--text-muted);
751
+ border-radius: 10px;
752
+ }
753
+
754
+ .terminal-line {
755
+ display: flex;
756
+ gap: 0.6rem;
757
+ align-items: baseline;
758
+ border-bottom: 1px solid rgba(255,255,255,0.03);
759
+ padding: 0.2rem 0;
760
+ word-break: break-all;
761
+ }
762
+
763
+ .terminal-prompt {
764
+ color: var(--text-muted);
765
+ user-select: none;
766
+ }
767
+
768
+ .terminal-timestamp {
769
+ color: var(--text-muted);
770
+ margin-right: 0.5rem;
771
+ font-size: 0.7rem;
772
+ white-space: nowrap;
773
+ }
774
+
775
+ .terminal-message { color: var(--terminal-text); }
776
+ .terminal-error { color: #fc8181; }
777
+ .terminal-success { color: #48bb78; }
778
+ .terminal-warning { color: #ecc94b; }
779
+
780
+ .loading-text {
781
+ color: var(--text-muted);
782
+ font-style: italic;
783
+ }
784
+
785
+ /* ===== MODAL ===== */
786
+ .modal-overlay {
787
+ display: none;
788
+ position: fixed;
789
+ inset: 0;
790
+ background: var(--modal-overlay);
791
+ z-index: 999;
792
+ justify-content: center;
793
+ align-items: center;
794
+ animation: fadeIn 0.3s ease;
795
+ }
796
+
797
+ .modal-overlay.show {
798
+ display: flex;
799
+ }
800
+
801
+ .modal {
802
+ background: var(--card-bg);
803
+ border: 1px solid var(--card-border);
804
+ border-radius: 1.3rem;
805
+ padding: 2rem;
806
+ max-width: 480px;
807
+ width: 90%;
808
+ max-height: 90vh;
809
+ overflow-y: auto;
810
+ box-shadow: var(--shadow-lg);
811
+ animation: slideUp 0.3s ease;
812
+ }
813
+
814
+ @keyframes slideUp {
815
+ from {
816
+ opacity: 0;
817
+ transform: translateY(20px) scale(0.95);
818
+ }
819
+ to {
820
+ opacity: 1;
821
+ transform: translateY(0) scale(1);
822
+ }
823
+ }
824
+
825
+ .modal-header {
826
+ display: flex;
827
+ justify-content: space-between;
828
+ align-items: center;
829
+ margin-bottom: 1.5rem;
830
+ }
831
+
832
+ .modal-header h2 {
833
+ font-size: 1.3rem;
834
+ font-weight: 600;
835
+ color: var(--heading);
836
+ display: flex;
837
+ align-items: center;
838
+ gap: 0.6rem;
839
+ }
840
+
841
+ .modal-header h2 i {
842
+ color: var(--input-focus);
843
+ }
844
+
845
+ .modal-close {
846
+ background: var(--toggle-bg);
847
+ border: 1px solid var(--border);
848
+ color: var(--text);
849
+ width: 32px;
850
+ height: 32px;
851
+ border-radius: 0.6rem;
852
+ display: flex;
853
+ align-items: center;
854
+ justify-content: center;
855
+ cursor: pointer;
856
+ transition: all 0.2s ease;
857
+ font-size: 1rem;
858
+ }
859
+
860
+ .modal-close:hover {
861
+ background: var(--error-bg);
862
+ border-color: var(--error-text);
863
+ color: var(--error-text);
864
+ }
865
+
866
+ .modal .form-group {
867
+ margin-bottom: 1.2rem;
868
+ }
869
+
870
+ .modal .form-group label {
871
+ display: block;
872
+ margin-bottom: 0.5rem;
873
+ font-size: 0.85rem;
874
+ font-weight: 600;
875
+ color: var(--text-secondary);
876
+ letter-spacing: 0.3px;
877
+ }
878
+
879
+ .modal .form-group label i {
880
+ margin-right: 0.5rem;
881
+ color: var(--input-focus);
882
+ }
883
+
884
+ .modal .input-wrapper {
885
+ position: relative;
886
+ }
887
+
888
+ .modal .input-wrapper .input-icon {
889
+ position: absolute;
890
+ left: 1rem;
891
+ top: 50%;
892
+ transform: translateY(-50%);
893
+ color: var(--text-muted);
894
+ font-size: 0.9rem;
895
+ transition: color 0.3s ease;
896
+ pointer-events: none;
897
+ }
898
+
899
+ .modal .input-wrapper input {
900
+ width: 100%;
901
+ padding: 0.9rem 1rem 0.9rem 2.8rem;
902
+ background: var(--input-bg);
903
+ color: var(--text);
904
+ border: 1px solid var(--input-border);
905
+ border-radius: 0.8rem;
906
+ font-size: 0.95rem;
907
+ font-family: 'Inter', sans-serif;
908
+ transition: all 0.3s ease;
909
+ }
910
+
911
+ .modal .input-wrapper input::placeholder {
912
+ color: var(--text-muted);
913
+ }
914
+
915
+ .modal .input-wrapper input:focus {
916
+ outline: none;
917
+ border-color: var(--input-focus);
918
+ background: var(--card-bg);
919
+ box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1);
920
+ }
921
+
922
+ .modal .input-wrapper input:focus ~ .input-icon {
923
+ color: var(--input-focus);
924
+ }
925
+
926
+ .modal .input-wrapper input:disabled {
927
+ opacity: 0.5;
928
+ cursor: not-allowed;
929
+ }
930
+
931
+ .modal .btn {
932
+ width: 100%;
933
+ padding: 0.9rem;
934
+ border: none;
935
+ border-radius: 0.8rem;
936
+ font-size: 0.95rem;
937
+ font-weight: 600;
938
+ font-family: 'Inter', sans-serif;
939
+ cursor: pointer;
940
+ transition: all 0.3s ease;
941
+ display: flex;
942
+ align-items: center;
943
+ justify-content: center;
944
+ gap: 0.6rem;
945
+ }
946
+
947
+ .modal .btn-primary {
948
+ background: var(--btn-bg);
949
+ color: var(--btn-text);
950
+ }
951
+
952
+ .modal .btn-primary:hover:not(:disabled) {
953
+ background: var(--btn-hover);
954
+ transform: translateY(-2px);
955
+ box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3);
956
+ }
957
+
958
+ .modal .btn-primary:disabled {
959
+ opacity: 0.6;
960
+ cursor: not-allowed;
961
+ transform: none;
962
+ }
963
+
964
+ .modal .btn-secondary {
965
+ background: var(--toggle-bg);
966
+ color: var(--text);
967
+ border: 1px solid var(--border);
968
+ }
969
+
970
+ .modal .btn-secondary:hover {
971
+ background: var(--border);
972
+ transform: translateY(-2px);
973
+ }
974
+
975
+ .modal .message {
976
+ margin-top: 1rem;
977
+ padding: 0.8rem 1rem;
978
+ border-radius: 0.8rem;
979
+ text-align: center;
980
+ font-size: 0.85rem;
981
+ display: none;
982
+ }
983
+
984
+ .modal .message.show {
985
+ display: block;
986
+ }
987
+
988
+ .modal .message.success {
989
+ background: var(--success-bg);
990
+ color: var(--success-text);
991
+ border: 1px solid var(--success-text);
992
+ }
993
+
994
+ .modal .message.error {
995
+ background: var(--error-bg);
996
+ color: var(--error-text);
997
+ border: 1px solid var(--error-text);
998
+ }
999
+
1000
+ .modal .message.info {
1001
+ background: var(--badge-bg);
1002
+ color: var(--badge-text);
1003
+ border: 1px solid var(--badge-text);
1004
+ }
1005
+
1006
+ .modal .spinner {
1007
+ display: inline-block;
1008
+ width: 16px;
1009
+ height: 16px;
1010
+ border: 2px solid rgba(255, 255, 255, 0.3);
1011
+ border-top-color: #fff;
1012
+ border-radius: 50%;
1013
+ animation: spin 0.6s linear infinite;
1014
+ }
1015
+
1016
+ @keyframes spin {
1017
+ to { transform: rotate(360deg); }
1018
+ }
1019
+
1020
+ .modal-actions {
1021
+ display: flex;
1022
+ gap: 0.8rem;
1023
+ margin-top: 0.5rem;
1024
+ }
1025
+
1026
+ .modal-actions .btn {
1027
+ flex: 1;
1028
+ }
1029
+
1030
+ /* ===== NOTIFICATION ===== */
1031
+ .notification {
1032
+ position: fixed;
1033
+ bottom: 20px;
1034
+ right: 20px;
1035
+ background: var(--card-bg);
1036
+ border: 1px solid var(--input-focus);
1037
+ color: var(--text);
1038
+ padding: 1rem 1.5rem;
1039
+ border-radius: 0.8rem;
1040
+ font-size: 0.9rem;
1041
+ box-shadow: var(--shadow-lg);
1042
+ z-index: 9999;
1043
+ display: flex;
1044
+ align-items: center;
1045
+ gap: 0.8rem;
1046
+ animation: slideIn 0.3s ease;
1047
+ max-width: 400px;
1048
+ }
1049
+
1050
+ .notification.success { border-color: #48bb78; }
1051
+ .notification.error { border-color: #fc8181; }
1052
+ .notification.warning { border-color: #ecc94b; }
1053
+ .notification i { font-size: 1.2rem; }
1054
+ .notification.success i { color: #48bb78; }
1055
+ .notification.error i { color: #fc8181; }
1056
+ .notification.warning i { color: #ecc94b; }
1057
+
1058
+ @keyframes slideIn {
1059
+ from { transform: translateX(100%); opacity: 0; }
1060
+ to { transform: translateX(0%); opacity: 1; }
1061
+ }
1062
+
1063
+ @keyframes slideOut {
1064
+ from { transform: translateX(0%); opacity: 1; }
1065
+ to { transform: translateX(100%); opacity: 0; }
1066
+ }
1067
+
1068
+ @keyframes fadeIn {
1069
+ from { opacity: 0; }
1070
+ to { opacity: 1; }
1071
+ }
1072
+
1073
+ /* ===== RESPONSIVE ===== */
1074
+ @media (max-width: 1024px) {
1075
+ .dashboard {
1076
+ grid-template-columns: 1fr;
1077
+ }
1078
+
1079
+ .sessions-panel {
1080
+ position: static;
1081
+ order: -1;
1082
+ }
1083
+ }
1084
+
1085
+ @media (max-width: 700px) {
1086
+ body {
1087
+ padding: 1rem;
1088
+ }
1089
+ .stats-row {
1090
+ gap: 1rem;
1091
+ }
1092
+ .header-actions {
1093
+ width: 100%;
1094
+ justify-content: flex-start;
1095
+ }
1096
+ .routes-grid {
1097
+ gap: 0.4rem;
1098
+ }
1099
+ .route-tag {
1100
+ font-size: 0.7rem;
1101
+ padding: 0.3rem 0.7rem;
1102
+ }
1103
+ .title-section h1 {
1104
+ font-size: 1.5rem;
1105
+ }
1106
+ .modal {
1107
+ padding: 1.5rem;
1108
+ }
1109
+ .modal-actions {
1110
+ flex-direction: column;
1111
+ }
1112
+ .session-item-header {
1113
+ padding-right: 0;
1114
+ }
1115
+ .session-item.current-session::after {
1116
+ position: static;
1117
+ display: inline-block;
1118
+ margin-left: 0.5rem;
1119
+ font-size: 0.5rem;
1120
+ padding: 0.1rem 0.4rem;
1121
+ }
1122
+ .session-logout-btn {
1123
+ position: static;
1124
+ display: inline-block;
1125
+ margin-top: 0.3rem;
1126
+ }
1127
+ .session-item:hover .session-logout-btn {
1128
+ display: inline-block;
1129
+ }
1130
+ }
1131
+ </style>
1132
+ </head>
1133
+ <body>
1134
+
1135
+ <div class="dashboard">
1136
+ <!-- ===== LEFT PANEL: SESSIONS ===== -->
1137
+ <div class="sessions-panel">
1138
+ <div class="sessions-header">
1139
+ <h2><i class="fas fa-users"></i> Live Sessions</h2>
1140
+ <span class="session-count" id="session-count">0</span>
1141
+ </div>
1142
+ <div class="sessions-list" id="sessions-list">
1143
+ <div class="no-sessions">
1144
+ <i class="fas fa-user-slash"></i>
1145
+ <p>No active sessions</p>
1146
+ </div>
1147
+ </div>
1148
+ </div>
1149
+
1150
+ <!-- ===== RIGHT PANEL: MAIN CONTENT ===== -->
1151
+ <div class="main-content">
1152
+ <!-- Header -->
1153
+ <div class="header">
1154
+ <div class="title-section">
1155
+ <h1><i class="fas fa-cube"></i> System Overview</h1>
1156
+ <div class="sub">
1157
+ <i class="fas fa-sync-alt fa-spin" id="loading-indicator" style="display: none;"></i>
1158
+ <span id="last-updated">Awaiting data...</span>
1159
+ </div>
1160
+ </div>
1161
+ <div class="header-actions">
1162
+ <button class="theme-toggle" id="themeToggle" title="Toggle dark/light mode">
1163
+ <i class="fas fa-moon"></i>
1164
+ </button>
1165
+ <button class="settings-btn" id="settingsBtn" title="Settings">
1166
+ <i class="fas fa-cog"></i>
1167
+ </button>
1168
+ <button class="logout-btn" id="logout-btn">
1169
+ <i class="fas fa-sign-out-alt"></i> Logout
1170
+ </button>
1171
+ <button class="refresh-btn" id="refresh-btn">
1172
+ <i class="fas fa-arrows-rotate"></i> Refresh
1173
+ </button>
1174
+ <button class="refresh-btn" id="documentation-btn">
1175
+ <i class="fa-regular fa-newspaper"></i> Docs
1176
+ </button>
1177
+ </div>
1178
+ </div>
1179
+
1180
+ <!-- Stats -->
1181
+ <div class="stats-row" id="stats-row">
1182
+ <div class="loading-text">Loading metrics...</div>
1183
+ </div>
1184
+
1185
+ <!-- Info Cards -->
1186
+ <div class="info-cards" id="info-cards"></div>
1187
+
1188
+ <!-- Routes -->
1189
+ <div class="routes-section">
1190
+ <div class="routes-header">
1191
+ <h3><i class="fas fa-route"></i> Registered Routes</h3>
1192
+ <span class="routes-count" id="routes-count">0 endpoints</span>
1193
+ </div>
1194
+ <div class="routes-grid" id="routes-grid">
1195
+ <div class="no-routes">No routes registered</div>
1196
+ </div>
1197
+ </div>
1198
+
1199
+ <!-- Terminal -->
1200
+ <div class="terminal-panel">
1201
+ <div class="terminal-header">
1202
+ <span class="terminal-dot dot-red"></span>
1203
+ <span class="terminal-dot dot-yellow"></span>
1204
+ <span class="terminal-dot dot-green"></span>
1205
+ <span class="terminal-title">messages — logs (live every 2s)</span>
1206
+ <span style="margin-left: auto; font-size:0.7rem; color:var(--text-muted);" id="message-count">0</span>
1207
+ </div>
1208
+ <div class="terminal-body" id="terminal-body">
1209
+ <div class="terminal-line"><span class="terminal-prompt">$</span> waiting for messages...</div>
1210
+ </div>
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+
1215
+ <!-- ===== SETTINGS MODAL ===== -->
1216
+ <div class="modal-overlay" id="settingsModal">
1217
+ <div class="modal">
1218
+ <div class="modal-header">
1219
+ <h2><i class="fas fa-cog"></i> Settings</h2>
1220
+ <button class="modal-close" id="settingsModalClose">
1221
+ <i class="fas fa-times"></i>
1222
+ </button>
1223
+ </div>
1224
+
1225
+ <!-- Change Username -->
1226
+ <div style="margin-bottom: 1.5rem;">
1227
+ <h3 style="font-size: 0.9rem; color: var(--heading); margin-bottom: 0.8rem;">
1228
+ <i class="fas fa-user-edit" style="color: var(--input-focus);"></i> Change Username
1229
+ </h3>
1230
+ <form id="changeUsernameForm">
1231
+ <div class="form-group">
1232
+ <label for="newUsername">
1233
+ <i class="fas fa-user"></i> New Username
1234
+ </label>
1235
+ <div class="input-wrapper">
1236
+ <input
1237
+ type="text"
1238
+ id="newUsername"
1239
+ placeholder="Enter new username"
1240
+ required
1241
+ minlength="3"
1242
+ maxlength="20"
1243
+ >
1244
+ <i class="fas fa-user input-icon"></i>
1245
+ </div>
1246
+ </div>
1247
+ <button type="submit" class="btn btn-primary" id="changeUsernameBtn">
1248
+ <i class="fas fa-save"></i>
1249
+ <span>Update Username</span>
1250
+ </button>
1251
+ <div id="usernameMessage" class="message"></div>
1252
+ </form>
1253
+ </div>
1254
+
1255
+ <hr style="border-color: var(--border); margin: 1.5rem 0;">
1256
+
1257
+ <!-- Change Password -->
1258
+ <div>
1259
+ <h3 style="font-size: 0.9rem; color: var(--heading); margin-bottom: 0.8rem;">
1260
+ <i class="fas fa-key" style="color: var(--input-focus);"></i> Change Password
1261
+ </h3>
1262
+ <form id="changePasswordForm">
1263
+ <div class="form-group">
1264
+ <label for="currentPassword">
1265
+ <i class="fas fa-lock"></i> Current Password
1266
+ </label>
1267
+ <div class="input-wrapper">
1268
+ <input
1269
+ type="password"
1270
+ id="currentPassword"
1271
+ placeholder="Enter current password"
1272
+ required
1273
+ >
1274
+ <i class="fas fa-key input-icon"></i>
1275
+ </div>
1276
+ </div>
1277
+ <div class="form-group">
1278
+ <label for="newPassword">
1279
+ <i class="fas fa-lock"></i> New Password
1280
+ </label>
1281
+ <div class="input-wrapper">
1282
+ <input
1283
+ type="password"
1284
+ id="newPassword"
1285
+ placeholder="Enter new password (min 8 chars)"
1286
+ required
1287
+ minlength="8"
1288
+ >
1289
+ <i class="fas fa-key input-icon"></i>
1290
+ </div>
1291
+ </div>
1292
+ <div class="form-group">
1293
+ <label for="confirmPassword">
1294
+ <i class="fas fa-check-circle"></i> Confirm New Password
1295
+ </label>
1296
+ <div class="input-wrapper">
1297
+ <input
1298
+ type="password"
1299
+ id="confirmPassword"
1300
+ placeholder="Confirm new password"
1301
+ required
1302
+ >
1303
+ <i class="fas fa-check input-icon"></i>
1304
+ </div>
1305
+ </div>
1306
+ <button type="submit" class="btn btn-primary" id="changePasswordBtn">
1307
+ <i class="fas fa-save"></i>
1308
+ <span>Update Password</span>
1309
+ </button>
1310
+ <div id="passwordMessage" class="message"></div>
1311
+ </form>
1312
+ </div>
1313
+
1314
+ <div style="margin-top: 1.5rem; text-align: center; color: var(--text-muted); font-size: 0.8rem;">
1315
+ <i class="fas fa-shield-alt" style="color: var(--input-focus);"></i>
1316
+ Changes are applied immediately
1317
+ </div>
1318
+ </div>
1319
+ </div>
1320
+
1321
+ <script>
1322
+ (function() {
1323
+ // ===== THEME TOGGLE =====
1324
+ const html = document.documentElement;
1325
+ const themeToggle = document.getElementById('themeToggle');
1326
+ const savedTheme = localStorage.getItem('tinnybackend-theme');
1327
+
1328
+ if (savedTheme) {
1329
+ html.setAttribute('data-theme', savedTheme);
1330
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
1331
+ html.setAttribute('data-theme', 'dark');
1332
+ }
1333
+
1334
+ function updateThemeUI(theme) {
1335
+ const icon = themeToggle.querySelector('i');
1336
+ if (theme === 'dark') {
1337
+ icon.classList.remove('fa-moon');
1338
+ icon.classList.add('fa-sun');
1339
+ } else {
1340
+ icon.classList.remove('fa-sun');
1341
+ icon.classList.add('fa-moon');
1342
+ }
1343
+ }
1344
+
1345
+ updateThemeUI(html.getAttribute('data-theme'));
1346
+
1347
+ themeToggle.addEventListener('click', () => {
1348
+ const currentTheme = html.getAttribute('data-theme');
1349
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1350
+ html.setAttribute('data-theme', newTheme);
1351
+ localStorage.setItem('tinnybackend-theme', newTheme);
1352
+ updateThemeUI(newTheme);
1353
+ });
1354
+
1355
+ if (window.matchMedia) {
1356
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
1357
+ if (!localStorage.getItem('tinnybackend-theme')) {
1358
+ const newTheme = e.matches ? 'dark' : 'light';
1359
+ html.setAttribute('data-theme', newTheme);
1360
+ updateThemeUI(newTheme);
1361
+ }
1362
+ });
1363
+ }
1364
+
1365
+ // ===== DOM ELEMENTS =====
1366
+ const STATUS_URL = '/admin/status';
1367
+ const MESSAGES_URL = '/messages';
1368
+ const LOGOUT_URL = '/api/logout';
1369
+ const CHANGE_PASSWORD_URL = '/passwd/change/auth';
1370
+ const CHANGE_USERNAME_URL = '/api/user/change';
1371
+
1372
+ const statsRow = document.getElementById('stats-row');
1373
+ const infoCards = document.getElementById('info-cards');
1374
+ const terminalBody = document.getElementById('terminal-body');
1375
+ const messageCountSpan = document.getElementById('message-count');
1376
+ const lastUpdatedSpan = document.getElementById('last-updated');
1377
+ const loadingIndicator = document.getElementById('loading-indicator');
1378
+ const refreshBtn = document.getElementById('refresh-btn');
1379
+ const docsBtn = document.getElementById('documentation-btn');
1380
+ const logoutBtn = document.getElementById('logout-btn');
1381
+ const settingsBtn = document.getElementById('settingsBtn');
1382
+ const sessionsList = document.getElementById('sessions-list');
1383
+ const sessionCount = document.getElementById('session-count');
1384
+ const routesGrid = document.getElementById('routes-grid');
1385
+ const routesCount = document.getElementById('routes-count');
1386
+
1387
+ // Modal elements
1388
+ const settingsModal = document.getElementById('settingsModal');
1389
+ const settingsModalClose = document.getElementById('settingsModalClose');
1390
+ const changeUsernameForm = document.getElementById('changeUsernameForm');
1391
+ const changePasswordForm = document.getElementById('changePasswordForm');
1392
+ const newUsernameInput = document.getElementById('newUsername');
1393
+ const currentPasswordInput = document.getElementById('currentPassword');
1394
+ const newPasswordInput = document.getElementById('newPassword');
1395
+ const confirmPasswordInput = document.getElementById('confirmPassword');
1396
+ const changeUsernameBtn = document.getElementById('changeUsernameBtn');
1397
+ const changePasswordBtn = document.getElementById('changePasswordBtn');
1398
+ const usernameMessage = document.getElementById('usernameMessage');
1399
+ const passwordMessage = document.getElementById('passwordMessage');
1400
+
1401
+ let isRedirecting = false;
1402
+ let currentSessionId = null;
1403
+
1404
+ // ===== HELPER FUNCTIONS =====
1405
+ function parseGB(str) {
1406
+ if (!str) return 0;
1407
+ const num = parseFloat(str.replace(/[^0-9.]/g, ''));
1408
+ return isNaN(num) ? 0 : num;
1409
+ }
1410
+
1411
+ function computeCpuPercent(times) {
1412
+ if (!times) return 0;
1413
+ const total = (times.user||0)+(times.nice||0)+(times.sys||0)+(times.idle||0)+(times.irq||0);
1414
+ if (total === 0) return 0;
1415
+ return parseFloat((((total - times.idle) / total) * 100).toFixed(1));
1416
+ }
1417
+
1418
+ function formatTimestamp(timestamp) {
1419
+ if (!timestamp) return 'Unknown';
1420
+ const date = new Date(timestamp);
1421
+ return date.toLocaleString();
1422
+ }
1423
+
1424
+ function getMethodColor(method) {
1425
+ const colors = {
1426
+ 'GET': 'get',
1427
+ 'POST': 'post',
1428
+ 'PUT': 'put',
1429
+ 'DELETE': 'delete',
1430
+ 'PATCH': 'patch'
1431
+ };
1432
+ return colors[method.toUpperCase()] || '';
1433
+ }
1434
+
1435
+ function escapeHtml(text) {
1436
+ if (!text) return 'Unknown';
1437
+ return String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1438
+ }
1439
+
1440
+ // ===== SHOW NOTIFICATION =====
1441
+ function showNotification(message, type = 'info') {
1442
+ const notification = document.createElement('div');
1443
+ notification.className = `notification ${type}`;
1444
+
1445
+ let icon = 'fa-info-circle';
1446
+ if (type === 'success') icon = 'fa-check-circle';
1447
+ else if (type === 'error') icon = 'fa-exclamation-circle';
1448
+ else if (type === 'warning') icon = 'fa-exclamation-triangle';
1449
+
1450
+ notification.innerHTML = `
1451
+ <i class="fas ${icon}"></i>
1452
+ <span>${message}</span>
1453
+ `;
1454
+
1455
+ document.body.appendChild(notification);
1456
+
1457
+ setTimeout(() => {
1458
+ notification.style.animation = 'slideOut 0.3s ease';
1459
+ setTimeout(() => notification.remove(), 300);
1460
+ }, 3000);
1461
+ }
1462
+
1463
+ // ===== HANDLE REDIRECT ON ERROR =====
1464
+ function handleFetchError(error, context = '') {
1465
+ console.error(`Fetch error (${context}):`, error);
1466
+
1467
+ if (error.status === 401 || error.status === 403) {
1468
+ showNotification('Session expired. Redirecting to login...', 'warning');
1469
+ setTimeout(() => {
1470
+ if (!isRedirecting) {
1471
+ isRedirecting = true;
1472
+ window.location.href = '/';
1473
+ }
1474
+ }, 1500);
1475
+ return true;
1476
+ }
1477
+ return false;
1478
+ }
1479
+
1480
+ // ===== RENDER FUNCTIONS =====
1481
+ function renderRoutes(routes) {
1482
+ if (!routes || routes.length === 0) {
1483
+ routesGrid.innerHTML = `<div class="no-routes">No routes registered</div>`;
1484
+ routesCount.textContent = '0 endpoints';
1485
+ return;
1486
+ }
1487
+
1488
+ routesCount.textContent = `${routes.length} endpoints`;
1489
+
1490
+ let html = '';
1491
+ routes.forEach(route => {
1492
+ const method = route.method || 'GET';
1493
+ const path = route.path || '/';
1494
+ const methodClass = getMethodColor(method);
1495
+
1496
+ html += `
1497
+ <div class="route-tag">
1498
+ <span class="route-method ${methodClass}">${method.toUpperCase()}</span>
1499
+ <span class="route-path">
1500
+ <i class="fas fa-chevron-right"></i>
1501
+ ${escapeHtml(path)}
1502
+ </span>
1503
+ </div>
1504
+ `;
1505
+ });
1506
+
1507
+ routesGrid.innerHTML = html;
1508
+ }
1509
+
1510
+ function renderSessions(sessions, currentId) {
1511
+ if (!sessions || sessions.length === 0) {
1512
+ sessionsList.innerHTML = `
1513
+ <div class="no-sessions">
1514
+ <i class="fas fa-user-slash"></i>
1515
+ <p>No active sessions</p>
1516
+ </div>
1517
+ `;
1518
+ sessionCount.textContent = '0';
1519
+ return;
1520
+ }
1521
+
1522
+ sessionCount.textContent = sessions.length;
1523
+
1524
+ let html = '';
1525
+ sessions.forEach((session) => {
1526
+ const isCurrent = session.isValid && session.id === currentId;
1527
+ const shortId = session.id ? session.id.substring(0, 8) + '...' : 'Unknown';
1528
+ const userAgent = session.userAgent || 'Unknown';
1529
+ const ip = session.ip || 'Unknown';
1530
+ const creation = session.creation ? formatTimestamp(session.creation) : 'Unknown';
1531
+ const requestCount = session.request || 0;
1532
+ const isValid = session.isValid !== undefined ? session.isValid : true;
1533
+
1534
+ html += `
1535
+ <div class="session-item ${isCurrent ? 'current-session' : ''}">
1536
+ <div class="session-item-header">
1537
+ <span class="session-id-display">
1538
+ <i class="fas fa-fingerprint"></i> ${shortId}
1539
+ </span>
1540
+ <span class="session-status-badge ${isValid ? 'active' : 'inactive'}">
1541
+ ${isValid ? 'Active' : 'Inactive'}
1542
+ </span>
1543
+ </div>
1544
+ <div class="session-details">
1545
+ <span><i class="fas fa-desktop"></i> ${escapeHtml(userAgent)}</span>
1546
+ <span><i class="fas fa-network-wired"></i> ${escapeHtml(ip)}</span>
1547
+ <span><i class="fas fa-clock"></i> Started: ${creation}</span>
1548
+ <span><i class="fas fa-tasks"></i> Requests: ${requestCount}</span>
1549
+ </div>
1550
+ ${!isCurrent ? `
1551
+ <button class="session-logout-btn" data-session-id="${session.id}">
1552
+ <i class="fas fa-sign-out-alt"></i> Logout
1553
+ </button>
1554
+ ` : ''}
1555
+ </div>
1556
+ `;
1557
+ });
1558
+
1559
+ sessionsList.innerHTML = html;
1560
+
1561
+ // Add event listeners for session logout buttons (only for non-current sessions)
1562
+ document.querySelectorAll('.session-logout-btn').forEach(btn => {
1563
+ btn.addEventListener('click', async (e) => {
1564
+ const sessionId = btn.dataset.sessionId;
1565
+ await logoutSession(sessionId, btn);
1566
+ });
1567
+ });
1568
+ }
1569
+
1570
+ function renderStatus(data) {
1571
+ const cpuModel = data.Cpu?.model || 'Unknown';
1572
+ const cpuSpeed = data.Cpu?.speed ? `${data.Cpu.speed} MHz` : '';
1573
+ const cpuPercent = computeCpuPercent(data.Cpu?.times);
1574
+ const memUsed = data['Used Memory'] || '0 GB';
1575
+ const memTotal = data['Memory'] || '8 GB';
1576
+ const memFree = data['Free Memory'] || '0 GB';
1577
+ const usedGb = parseGB(memUsed);
1578
+ const totalGb = parseGB(memTotal);
1579
+ const memPercent = totalGb > 0 ? (usedGb / totalGb) * 100 : 0;
1580
+ const machineUptime = data['Machin uptime'] || '—';
1581
+ const serverUptime = data['Server uptime'] || '—';
1582
+ const arch = data['Arch'] || '—';
1583
+ const platform = data['Platform'] || '—';
1584
+ const clients = data['Connected clients'] ?? 0;
1585
+ const requests = data['Total requests'] ?? 0;
1586
+
1587
+ // Get current session ID from the data
1588
+ currentSessionId = data['Currnet-session'] || null;
1589
+
1590
+ // Render sessions
1591
+ if (data.sessions) {
1592
+ renderSessions(data.sessions, currentSessionId);
1593
+ }
1594
+
1595
+ // Render routes
1596
+ if (data.routes) {
1597
+ renderRoutes(data.routes);
1598
+ }
1599
+
1600
+ // Build circular stats
1601
+ const circumference = 2 * Math.PI * 38;
1602
+ const cpuOffset = circumference - (cpuPercent / 100) * circumference;
1603
+ const memCircleOffset = circumference - (memPercent / 100) * circumference;
1604
+
1605
+ statsRow.innerHTML = `
1606
+ <div class="stat-circle-group">
1607
+ <div class="circle-wrapper">
1608
+ <svg class="circle-svg" viewBox="0 0 90 90">
1609
+ <circle class="circle-bg" cx="45" cy="45" r="38"/>
1610
+ <circle class="circle-progress" cx="45" cy="45" r="38" stroke-dasharray="${circumference}" stroke-dashoffset="${cpuOffset}"/>
1611
+ </svg>
1612
+ <div class="circle-inner">
1613
+ <span class="circle-value">${cpuPercent}%</span>
1614
+ <span class="circle-unit">CPU</span>
1615
+ </div>
1616
+ </div>
1617
+ <span class="stat-label">${cpuModel}</span>
1618
+ <span class="stat-detail">${cpuSpeed}</span>
1619
+ </div>
1620
+
1621
+ <div class="stat-circle-group">
1622
+ <div class="circle-wrapper">
1623
+ <svg class="circle-svg" viewBox="0 0 90 90">
1624
+ <circle class="circle-bg" cx="45" cy="45" r="38"/>
1625
+ <circle class="circle-progress" cx="45" cy="45" r="38" stroke-dasharray="${circumference}" stroke-dashoffset="${memCircleOffset}"/>
1626
+ </svg>
1627
+ <div class="circle-inner">
1628
+ <span class="circle-value">${usedGb.toFixed(1)}</span>
1629
+ <span class="circle-unit">/ ${totalGb.toFixed(0)} GB</span>
1630
+ </div>
1631
+ </div>
1632
+ <span class="stat-label">Memory</span>
1633
+ </div>
1634
+
1635
+ <div class="memory-bar-card">
1636
+ <div class="memory-header">
1637
+ <span><i class="fas fa-memory"></i> RAM</span>
1638
+ <span>${memUsed} / ${memTotal}</span>
1639
+ </div>
1640
+ <div class="memory-bar-bg">
1641
+ <div class="memory-bar-fill" style="width:${memPercent}%;"></div>
1642
+ </div>
1643
+ <div class="memory-stats">
1644
+ <span>Used: ${memUsed}</span>
1645
+ <span>Free: ${memFree}</span>
1646
+ </div>
1647
+ </div>
1648
+
1649
+ <div class="stat-circle-group">
1650
+ <div class="circle-wrapper" style="width:70px; height:70px;">
1651
+ <div class="circle-inner" style="width:100%;">
1652
+ <span class="circle-value" style="font-size:1rem;">${machineUptime}</span>
1653
+ </div>
1654
+ </div>
1655
+ <span class="stat-label">Machine uptime</span>
1656
+ </div>
1657
+
1658
+ <div class="stat-circle-group">
1659
+ <div class="circle-wrapper" style="width:70px; height:70px;">
1660
+ <div class="circle-inner" style="width:100%;">
1661
+ <span class="circle-value" style="font-size:1rem;">${serverUptime}</span>
1662
+ </div>
1663
+ </div>
1664
+ <span class="stat-label">Server uptime</span>
1665
+ </div>
1666
+ `;
1667
+
1668
+ infoCards.innerHTML = `
1669
+ <div class="info-card"><i class="fas fa-microchip"></i><div class="info-text"><span class="info-label">Architecture</span><span class="info-value">${arch} / ${platform}</span></div></div>
1670
+ <div class="info-card"><i class="fas fa-plug"></i><div class="info-text"><span class="info-label">Clients</span><span class="info-value">${clients}</span></div></div>
1671
+ <div class="info-card"><i class="fas fa-globe"></i><div class="info-text"><span class="info-label">Requests</span><span class="info-value">${requests}</span></div></div>
1672
+ <div class="info-card"><i class="fas fa-users"></i><div class="info-text"><span class="info-label">Sessions</span><span class="info-value">${data.sessions ? data.sessions.length : 0}</span></div></div>
1673
+ <div class="info-card"><i class="fas fa-route"></i><div class="info-text"><span class="info-label">Routes</span><span class="info-value">${data.routes ? data.routes.length : 0}</span></div></div>
1674
+ ${currentSessionId ? `<div class="info-card"><i class="fas fa-fingerprint"></i><div class="info-text"><span class="info-label">Current Session</span><span class="info-value" style="font-size:0.7rem;font-family:monospace;">${currentSessionId.substring(0, 12)}...</span></div></div>` : ''}
1675
+ `;
1676
+
1677
+ lastUpdatedSpan.textContent = `Updated ${new Date().toLocaleTimeString()}`;
1678
+ }
1679
+
1680
+ // ===== TERMINAL =====
1681
+ function appendTerminalMessage(msg) {
1682
+ if (!msg) return;
1683
+ const messageText = msg.message || '';
1684
+ const type = msg.type || 'info';
1685
+ const date = msg.date || new Date().toLocaleString();
1686
+ let lineClass = 'terminal-message';
1687
+ if (type === 'error') lineClass = 'terminal-error';
1688
+ else if (type === 'success') lineClass = 'terminal-success';
1689
+ else if (type === 'warning') lineClass = 'terminal-warning';
1690
+
1691
+ const line = document.createElement('div');
1692
+ line.className = 'terminal-line';
1693
+ line.innerHTML = `<span class="terminal-prompt">❯</span><span class="terminal-timestamp">[${escapeHtml(date)}]</span><span class="${lineClass}">${escapeHtml(messageText)}</span>`;
1694
+ terminalBody.appendChild(line);
1695
+ terminalBody.scrollTop = terminalBody.scrollHeight;
1696
+ const currentCount = terminalBody.querySelectorAll('.terminal-line').length;
1697
+ messageCountSpan.textContent = `${currentCount} lines`;
1698
+ }
1699
+
1700
+ // ===== FETCH FUNCTIONS =====
1701
+ async function fetchStatus() {
1702
+ loadingIndicator.style.display = 'inline-block';
1703
+ try {
1704
+ const res = await fetch(STATUS_URL);
1705
+ if (!res.ok) {
1706
+ if (res.status === 401 || res.status === 403) {
1707
+ handleFetchError({ status: res.status }, 'status');
1708
+ return;
1709
+ }
1710
+ throw new Error(`Status ${res.status}`);
1711
+ }
1712
+ const data = await res.json();
1713
+ renderStatus(data);
1714
+ } catch (err) {
1715
+ console.error('Status fetch error', err);
1716
+ statsRow.innerHTML = `<div style="color:var(--error-text);">Failed to load status.</div>`;
1717
+ } finally {
1718
+ loadingIndicator.style.display = 'none';
1719
+ }
1720
+ }
1721
+
1722
+ async function fetchMessages() {
1723
+ try {
1724
+ const res = await fetch(MESSAGES_URL);
1725
+ if (!res.ok) {
1726
+ if (res.status === 401 || res.status === 403) {
1727
+ handleFetchError({ status: res.status }, 'messages');
1728
+ return;
1729
+ }
1730
+ return;
1731
+ }
1732
+ const data = await res.json();
1733
+ const messages = Array.isArray(data) ? data : (data.logs || data.messages || []);
1734
+ if (messages.length === 0) return;
1735
+ const currentLines = terminalBody.querySelectorAll('.terminal-line').length;
1736
+ if (currentLines === 0 || messages.length > currentLines) {
1737
+ const newMessages = messages.slice(currentLines);
1738
+ newMessages.forEach(m => appendTerminalMessage(m));
1739
+ } else if (messages.length < currentLines) {
1740
+ terminalBody.innerHTML = '';
1741
+ messages.forEach(m => appendTerminalMessage(m));
1742
+ }
1743
+ } catch (e) {
1744
+ console.warn('Messages fetch error', e);
1745
+ }
1746
+ }
1747
+
1748
+ // ===== SESSION LOGOUT =====
1749
+ async function logoutSession(sessionId, button) {
1750
+ if (!sessionId) return;
1751
+
1752
+ button.disabled = true;
1753
+ button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
1754
+
1755
+ try {
1756
+ const response = await fetch(`/api/logout/${sessionId}`, {
1757
+ method: 'POST',
1758
+ credentials: 'same-origin'
1759
+ });
1760
+
1761
+ if (response.ok) {
1762
+ showNotification(`Session ${sessionId.substring(0, 8)}... logged out`, 'success');
1763
+ await fetchStatus(); // Refresh sessions
1764
+ } else if (response.status === 401 || response.status === 403) {
1765
+ handleFetchError({ status: response.status }, 'session logout');
1766
+ } else {
1767
+ const data = await response.json();
1768
+ showNotification(data.message || 'Failed to logout session', 'error');
1769
+ }
1770
+ } catch (error) {
1771
+ console.error('Session logout error:', error);
1772
+ showNotification('Error logging out session', 'error');
1773
+ } finally {
1774
+ button.disabled = false;
1775
+ button.innerHTML = '<i class="fas fa-sign-out-alt"></i> Logout';
1776
+ }
1777
+ }
1778
+
1779
+ // ===== LOGOUT =====
1780
+ async function performLogout() {
1781
+ try {
1782
+ logoutBtn.disabled = true;
1783
+ logoutBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Logging out...';
1784
+
1785
+ const response = await fetch(LOGOUT_URL, {
1786
+ method: 'GET',
1787
+ credentials: 'same-origin'
1788
+ });
1789
+
1790
+ const contentType = response.headers.get('content-type');
1791
+
1792
+ if (contentType && contentType.includes('text/html')) {
1793
+ showNotification('Logged out successfully', 'success');
1794
+ window.location.href = '/';
1795
+ return;
1796
+ }
1797
+
1798
+ try {
1799
+ const data = await response.json();
1800
+ if (response.ok && data.success) {
1801
+ showNotification('Logged out successfully', 'success');
1802
+ setTimeout(() => {
1803
+ window.location.href = '/';
1804
+ }, 1500);
1805
+ } else {
1806
+ showNotification(data.message || 'Logout failed', 'error');
1807
+ }
1808
+ } catch (jsonError) {
1809
+ if (response.redirected) {
1810
+ showNotification('Logged out successfully', 'success');
1811
+ window.location.href = '/';
1812
+ } else {
1813
+ showNotification('Logged out successfully', 'success');
1814
+ window.location.href = '/';
1815
+ }
1816
+ }
1817
+ } catch (error) {
1818
+ console.error('Logout error:', error);
1819
+ showNotification('Error during logout', 'error');
1820
+ window.location.href = '/';
1821
+ } finally {
1822
+ logoutBtn.disabled = false;
1823
+ logoutBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i> Logout';
1824
+ }
1825
+ }
1826
+
1827
+ // ===== MODAL FUNCTIONS =====
1828
+ function openModal() {
1829
+ settingsModal.classList.add('show');
1830
+ document.body.style.overflow = 'hidden';
1831
+ }
1832
+
1833
+ function closeModal() {
1834
+ settingsModal.classList.remove('show');
1835
+ document.body.style.overflow = '';
1836
+ usernameMessage.className = 'message';
1837
+ usernameMessage.textContent = '';
1838
+ passwordMessage.className = 'message';
1839
+ passwordMessage.textContent = '';
1840
+ }
1841
+
1842
+ settingsBtn.addEventListener('click', openModal);
1843
+ settingsModalClose.addEventListener('click', closeModal);
1844
+ settingsModal.addEventListener('click', (e) => {
1845
+ if (e.target === settingsModal) closeModal();
1846
+ });
1847
+
1848
+ // ===== CHANGE USERNAME =====
1849
+ changeUsernameForm.addEventListener('submit', async (e) => {
1850
+ e.preventDefault();
1851
+ usernameMessage.className = 'message';
1852
+ usernameMessage.textContent = '';
1853
+
1854
+ const newUsername = newUsernameInput.value.trim();
1855
+
1856
+ if (newUsername.length < 3) {
1857
+ usernameMessage.className = 'message show error';
1858
+ usernameMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Username must be at least 3 characters';
1859
+ return;
1860
+ }
1861
+
1862
+ if (newUsername.length > 20) {
1863
+ usernameMessage.className = 'message show error';
1864
+ usernameMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Username must be less than 20 characters';
1865
+ return;
1866
+ }
1867
+
1868
+ changeUsernameBtn.disabled = true;
1869
+ changeUsernameBtn.innerHTML = '<span class="spinner"></span> Updating...';
1870
+
1871
+ try {
1872
+ const response = await fetch(CHANGE_USERNAME_URL, {
1873
+ method: 'POST',
1874
+ headers: { 'Content-Type': 'application/json' },
1875
+ body: JSON.stringify({ userName: newUsername }),
1876
+ credentials: 'same-origin'
1877
+ });
1878
+
1879
+ const data = await response.json();
1880
+
1881
+ if (response.ok) {
1882
+ usernameMessage.className = 'message show success';
1883
+ usernameMessage.innerHTML = '<i class="fas fa-check-circle"></i> Username updated successfully!';
1884
+ showNotification('Username updated successfully!', 'success');
1885
+ newUsernameInput.value = '';
1886
+ setTimeout(() => {
1887
+ closeModal();
1888
+ window.location.reload();
1889
+ }, 1500);
1890
+ } else if (response.status === 401 || response.status === 403) {
1891
+ handleFetchError({ status: response.status }, 'username change');
1892
+ } else {
1893
+ usernameMessage.className = 'message show error';
1894
+ usernameMessage.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${data.message || data.status || 'Failed to update username'}`;
1895
+ }
1896
+ } catch (error) {
1897
+ console.error('Username change error:', error);
1898
+ usernameMessage.className = 'message show error';
1899
+ usernameMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Network error. Please try again.';
1900
+ } finally {
1901
+ changeUsernameBtn.disabled = false;
1902
+ changeUsernameBtn.innerHTML = '<i class="fas fa-save"></i> <span>Update Username</span>';
1903
+ }
1904
+ });
1905
+
1906
+ // ===== CHANGE PASSWORD =====
1907
+ changePasswordForm.addEventListener('submit', async (e) => {
1908
+ e.preventDefault();
1909
+ passwordMessage.className = 'message';
1910
+ passwordMessage.textContent = '';
1911
+
1912
+ const currentPassword = currentPasswordInput.value;
1913
+ const newPassword = newPasswordInput.value;
1914
+ const confirmPassword = confirmPasswordInput.value;
1915
+
1916
+ if (!currentPassword || !newPassword || !confirmPassword) {
1917
+ passwordMessage.className = 'message show error';
1918
+ passwordMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Please fill in all fields';
1919
+ return;
1920
+ }
1921
+
1922
+ if (newPassword.length < 8) {
1923
+ passwordMessage.className = 'message show error';
1924
+ passwordMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Password must be at least 8 characters';
1925
+ return;
1926
+ }
1927
+
1928
+ if (newPassword !== confirmPassword) {
1929
+ passwordMessage.className = 'message show error';
1930
+ passwordMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Passwords do not match';
1931
+ return;
1932
+ }
1933
+
1934
+ changePasswordBtn.disabled = true;
1935
+ changePasswordBtn.innerHTML = '<span class="spinner"></span> Updating...';
1936
+
1937
+ try {
1938
+ const response = await fetch(CHANGE_PASSWORD_URL, {
1939
+ method: 'POST',
1940
+ headers: { 'Content-Type': 'application/json' },
1941
+ body: JSON.stringify({
1942
+ oldpwd: currentPassword,
1943
+ newpwd: newPassword
1944
+ }),
1945
+ credentials: 'same-origin'
1946
+ });
1947
+
1948
+ const data = await response.json();
1949
+
1950
+ if (response.ok) {
1951
+ passwordMessage.className = 'message show success';
1952
+ passwordMessage.innerHTML = '<i class="fas fa-check-circle"></i> Password updated successfully!';
1953
+ showNotification('Password updated successfully!', 'success');
1954
+ currentPasswordInput.value = '';
1955
+ newPasswordInput.value = '';
1956
+ confirmPasswordInput.value = '';
1957
+ setTimeout(() => {
1958
+ closeModal();
1959
+ window.location.reload();
1960
+ }, 1500);
1961
+ } else if (response.status === 401 || response.status === 403) {
1962
+ handleFetchError({ status: response.status }, 'password change');
1963
+ } else {
1964
+ passwordMessage.className = 'message show error';
1965
+ passwordMessage.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${data.message || data.status || 'Failed to update password'}`;
1966
+ }
1967
+ } catch (error) {
1968
+ console.error('Password change error:', error);
1969
+ passwordMessage.className = 'message show error';
1970
+ passwordMessage.innerHTML = '<i class="fas fa-exclamation-circle"></i> Network error. Please try again.';
1971
+ } finally {
1972
+ changePasswordBtn.disabled = false;
1973
+ changePasswordBtn.innerHTML = '<i class="fas fa-save"></i> <span>Update Password</span>';
1974
+ }
1975
+ });
1976
+
1977
+ // ===== INITIALIZATION =====
1978
+ fetchStatus();
1979
+ fetchMessages();
1980
+
1981
+ // Poll messages every 2 seconds
1982
+ setInterval(fetchMessages, 2000);
1983
+
1984
+ // Poll status every 5 seconds
1985
+ setInterval(fetchStatus, 5000);
1986
+
1987
+ // Event listeners
1988
+ refreshBtn.addEventListener('click', (e) => {
1989
+ e.preventDefault();
1990
+ fetchStatus();
1991
+ fetchMessages();
1992
+ showNotification('Status refreshed', 'info');
1993
+ });
1994
+
1995
+ logoutBtn.addEventListener('click', performLogout);
1996
+
1997
+ docsBtn.addEventListener('click', () => {
1998
+ window.open("/docs", "_blank");
1999
+ });
2000
+
2001
+ // Keyboard shortcut for modal (Escape key)
2002
+ document.addEventListener('keydown', (e) => {
2003
+ if (e.key === 'Escape' && settingsModal.classList.contains('show')) {
2004
+ closeModal();
2005
+ }
2006
+ });
2007
+
2008
+ })();
2009
+ </script>
2010
+
2011
+ </body>
2012
+ </html>