i0110 commited on
Commit
978858c
·
verified ·
1 Parent(s): 93a80e2

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +730 -1099
public/index.html CHANGED
@@ -4,269 +4,193 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>HF Space Manager</title>
 
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
8
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M13 21h-2v-11h2v11zm4 0h-2v-7h2v7zm4 0h-2v-4h2v4zm-16 0h-2v-14h2v14zm4 0h-2v-18h2v18z'/%3E%3C/svg%3E" type="image/svg+xml">
9
  <style>
10
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
  * {
12
  margin: 0;
13
  padding: 0;
14
  box-sizing: border-box;
15
  }
16
  body {
17
- font-family: 'Inter', "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
18
- background: var(--background-color);
19
- color: var(--text-color);
20
- padding: 25px 20px;
21
  min-height: 100vh;
22
  transition: background 0.3s ease, color 0.3s ease;
23
- line-height: 1.6;
24
  }
25
  :root {
26
- --background-color: #0a0a0a;
27
- --text-color: #e0e0e0;
28
- --card-background: #181818;
29
- --card-border: rgba(255, 255, 255, 0.08);
30
- --metric-background: #282828;
31
- --metric-border: rgba(255, 255, 255, 0.05);
32
- --metric-hover: #383838;
33
- --secondary-text: #b0b0b0;
34
- --label-color: #808080;
35
- --network-background: rgba(255, 255, 255, 0.05);
36
- --action-button-bg: #3a3a3a;
37
- --action-button-hover: #4a4a4a;
38
- --action-button-text: #e0e0e0;
39
- --accent-color: #4CAF50;
40
- --error-color: #f44336;
41
- --warning-color: #ffa500;
42
- --success-color: #4CAF50;
43
- --overlay-background: rgba(0, 0, 0, 0.8);
44
- }
45
- [data-theme="light"] {
46
- --background-color: #f0f2f5;
47
  --text-color: #333;
48
- --card-background: #ffffff;
49
- --card-border: rgba(0, 0, 0, 0.1);
50
- --metric-background: #e8ebef;
51
- --metric-border: rgba(0, 0, 0, 0.05);
52
- --metric-hover: #dadec3;
53
- --secondary-text: #555;
54
- --label-color: #888;
55
  --network-background: rgba(0, 0, 0, 0.05);
56
- --action-button-bg: #ddd;
57
- --action-button-hover: #ccc;
58
- --action-button-text: #333;
59
- --accent-color: #4CAF50;
60
- --error-color: #f44336;
61
- --warning-color: #ffa500;
62
- --success-color: #4CAF50;
63
- --overlay-background: rgba(0, 0, 0, 0.5);
64
  }
65
  .container {
66
- max-width: 1200px;
67
  margin: 0 auto;
68
- animation: fadeIn 0.6s ease;
69
  padding: 0 15px;
70
  }
71
  .overview {
72
  background: var(--card-background);
73
- border-radius: 12px;
74
- padding: 25px;
75
- margin-bottom: 30px;
76
  border: 1px solid var(--card-border);
77
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
78
- transition: background 0.3s ease, border 0.3s ease, box-shadow 0.3s ease;
79
  }
80
  .overview-title {
81
- font-size: 22px;
82
- font-weight: 600;
83
  display: flex;
84
  align-items: center;
85
  gap: 10px;
86
  margin-bottom: 20px;
87
  color: var(--text-color);
 
88
  }
89
- .header-container {
90
- display: flex;
91
- justify-content: space-between;
92
- align-items: center;
93
- margin-bottom: 20px;
94
- flex-wrap: wrap;
95
- gap: 15px;
96
- }
97
- .auth-buttons {
98
- display: flex;
99
- gap: 10px;
100
  }
101
  .theme-toggle {
102
  display: flex;
103
  align-items: center;
104
- gap: 8px;
105
- margin-bottom: 20px;
106
  font-size: 14px;
107
  color: var(--secondary-text);
108
  }
109
- .theme-toggle span {
110
- font-weight: 500;
111
- margin-right: 4px;
112
- }
113
  .theme-toggle button {
114
  background: var(--metric-background);
115
  border: 1px solid var(--metric-border);
116
  color: var(--text-color);
117
- padding: 8px;
118
- border-radius: 8px;
119
  cursor: pointer;
120
  display: flex;
121
  align-items: center;
122
  justify-content: center;
123
- width: 36px;
124
- height: 36px;
125
- transition: background 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
126
- outline: none;
127
  }
128
  .theme-toggle button:hover {
129
  background: var(--metric-hover);
130
- border-color: var(--metric-border);
131
- transform: translateY(-1px);
132
- }
133
- .theme-toggle button:active {
134
- transform: translateY(0);
135
- }
136
- .theme-toggle button:focus-visible {
137
- outline: 2px solid var(--accent-color);
138
- outline-offset: 2px;
139
  }
140
  .theme-toggle svg {
141
  width: 18px;
142
  height: 18px;
143
- fill: var(--secondary-text);
144
- transition: fill 0.3s ease;
145
- }
146
- .theme-toggle button[data-active="true"] {
147
- background: var(--accent-color);
148
- border-color: var(--accent-color);
149
- }
150
- .theme-toggle button[data-active="true"] svg {
151
- fill: #fff;
152
  }
153
  #summary {
154
  display: grid;
155
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
156
- gap: 15px;
157
  }
158
  #summary div {
159
  background: var(--metric-background);
160
- padding: 15px;
161
- border-radius: 8px;
162
  border: 1px solid var(--metric-border);
 
163
  transition: background 0.3s ease, border 0.3s ease;
164
- display: flex;
165
- flex-direction: column;
166
- align-items: flex-start;
167
  }
168
  #summary div {
169
- font-size: 13px;
170
  color: var(--secondary-text);
171
  }
172
  #summary span {
173
  display: block;
174
- font-size: 24px;
175
- font-weight: 700;
176
- margin-top: 6px;
177
  color: var(--text-color);
178
  overflow: hidden;
179
  text-overflow: ellipsis;
180
  white-space: nowrap;
181
- width: 100%;
 
 
 
 
182
  }
183
  .stats-container {
184
- display: flex;
185
- flex-direction: column;
186
- gap: 25px;
187
- margin-top: 25px;
188
  }
189
  .user-group {
190
  background: var(--card-background);
191
- border-radius: 12px;
192
  border: 1px solid var(--card-border);
193
  overflow: hidden;
194
  transition: background 0.3s ease, border 0.3s ease;
195
  }
196
  .user-group summary {
197
- padding: 18px 25px;
198
- font-weight: 600;
199
  cursor: pointer;
200
  color: var(--text-color);
201
  background: var(--metric-background);
202
  transition: background 0.2s ease;
203
- display: flex;
204
- align-items: center;
205
- justify-content: space-between;
206
- width: 100%;
207
  }
208
  .user-group summary:hover {
209
  background: var(--metric-hover);
210
  }
211
- .user-group summary::marker,
212
  .user-group summary::-webkit-details-marker {
213
- display: none;
214
- }
215
- .user-group {
216
- position: relative;
217
- }
218
- .user-group summary::after {
219
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="%23b0b0b0"><path d="M18.59 10.59L12 17.17 5.41 10.59 4 12l8 8 8-8z"/></svg>');
220
- display: inline-block;
221
- margin-left: 10px;
222
- transform: rotate(0deg);
223
- transition: transform 0.2s ease;
224
- fill: var(--secondary-text);
225
- }
226
- .user-group[open] summary::after {
227
- transform: rotate(180deg);
228
- }
229
- [data-theme="light"] .user-group summary::after {
230
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="%23555"><path d="M18.59 10.59L12 17.17 5.41 10.59 4 12l8 8 8-8z"/></svg>');
231
  }
232
  .user-servers {
233
  display: grid;
234
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
235
  gap: 15px;
236
- padding: 20px 25px 25px;
237
  }
238
  .server-card {
239
  background: var(--metric-background);
240
- border-radius: 10px;
241
- padding: 18px;
242
  border: 1px solid var(--metric-border);
243
  transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.3s ease, border 0.3s ease;
244
- min-height: 160px;
245
  display: flex;
246
  flex-direction: column;
247
- justify-content: space-between;
 
 
248
  }
249
  .server-card:hover {
250
- transform: translateY(-3px);
251
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
252
  }
253
  .server-header {
254
  display: flex;
255
  justify-content: space-between;
256
  align-items: center;
257
- margin-bottom: 15px;
258
- font-size: 15px;
259
- flex-wrap: wrap;
260
- gap: 10px;
261
  }
262
  .server-name {
263
  display: flex;
264
  align-items: center;
265
- gap: 10px;
266
  flex: 1;
267
  min-width: 0;
268
- font-weight: 600;
269
- color: var(--text-color);
270
  }
271
  .server-name div {
272
  overflow: hidden;
@@ -278,27 +202,20 @@
278
  width: 20px;
279
  height: 20px;
280
  border-radius: 4px;
281
- border: 1px solid rgba(0, 0, 0, 0.1);
282
  flex-shrink: 0;
283
- fill: var(--label-color);
284
  }
285
  .metric-grid {
286
  display: grid;
287
- grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
288
  gap: 10px;
289
- margin-top: 15px;
290
- }
291
- @media (max-width: 400px) {
292
- .metric-grid {
293
- grid-template-columns: repeat(2, 1fr);
294
- }
295
  }
296
  .metric-item {
297
  background: var(--card-background);
298
- padding: 10px;
299
  border-radius: 6px;
300
  border: 1px solid var(--metric-border);
301
- transition: background 0.3s ease, border 0.3s ease;
302
  overflow: hidden;
303
  }
304
  .metric-item:hover {
@@ -307,126 +224,87 @@
307
  .metric-label {
308
  color: var(--label-color);
309
  font-size: 12px;
310
- margin-bottom: 4px;
311
  white-space: nowrap;
312
- font-weight: 500;
313
  }
314
  .metric-value {
315
- font-size: 15px;
316
- font-weight: 600;
317
- color: var(--text-color);
318
  overflow: hidden;
319
  text-overflow: ellipsis;
320
  white-space: nowrap;
321
  max-width: 100%;
322
  }
323
- .metric-value.status-running { color: var(--success-color); }
324
- .metric-value.status-sleeping { color: var(--warning-color); }
325
- .metric-value.status-stopped { color: var(--error-color); }
326
  .status-dot {
327
  display: inline-block;
328
  border-radius: 50%;
 
329
  width: 10px;
330
  height: 10px;
331
  flex-shrink: 0;
332
- margin-right: 5px;
333
  }
334
- .status-dot.status-online {
335
- background-color: var(--success-color);
336
- animation: pulse 2s infinite;
337
- box-shadow: 0 0 0 rgba(76, 175, 80, 0.4);
338
  }
339
- .status-dot.status-offline {
340
- background-color: var(--error-color);
 
341
  }
342
- .status-dot.status-sleep {
343
- background-color: var(--warning-color);
 
344
  animation: none;
345
  }
346
- @keyframes pulse {
347
- 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
348
- 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
349
- 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
350
- }
351
  .action-buttons {
352
  display: flex;
353
- gap: 8px;
354
- margin-top: 15px;
355
- padding-top: 15px;
356
- border-top: 1px solid var(--metric-border);
357
  }
358
  .action-button {
359
  background: var(--action-button-bg);
360
- color: var(--action-button-text);
361
  border: none;
362
- padding: 8px 14px;
363
- border-radius: 6px;
364
  cursor: pointer;
365
  font-size: 13px;
366
- font-weight: 500;
367
- transition: background 0.2s ease, transform 0.1s ease;
368
- outline: none;
369
- flex-shrink: 0;
370
  }
371
  .action-button:hover {
372
  background: var(--action-button-hover);
373
- transform: translateY(-1px);
374
- }
375
- .action-button:active {
376
- transform: translateY(0);
377
- }
378
- .action-button:focus-visible {
379
- outline: 2px solid var(--accent-color);
380
- outline-offset: 2px;
381
  }
382
  .network-stats {
383
  background: var(--network-background);
384
  border: 1px solid var(--metric-border);
385
- margin-top: 20px;
386
  padding: 15px;
387
  border-radius: 8px;
 
 
388
  transition: background 0.3s ease, border 0.3s ease;
 
 
389
  font-size: 14px;
390
  color: var(--secondary-text);
391
- display: flex;
392
- gap: 20px;
393
- flex-wrap: wrap;
394
  }
395
- .network-stats strong {
396
  color: var(--text-color);
397
- font-weight: 600;
398
- margin-right: 5px;
399
  }
400
  @keyframes fadeIn {
401
  from { opacity: 0; transform: translateY(20px); }
402
  to { opacity: 1; transform: translateY(0); }
403
  }
404
- @media (max-width: 768px) {
405
- body {
406
- padding: 20px 15px;
407
- }
408
- .overview {
409
- padding: 20px;
410
- margin-bottom: 20px;
411
- }
412
- .overview-title {
413
- font-size: 20px;
414
- margin-bottom: 15px;
415
- }
416
- .header-container {
417
- flex-direction: column;
418
- align-items: flex-start;
419
- }
420
- .auth-buttons {
421
- width: 100%;
422
- justify-content: flex-end;
423
- }
424
- .theme-toggle {
425
- margin-bottom: 15px;
426
- }
427
  #summary {
428
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
429
- gap: 12px;
430
  }
431
  #summary div {
432
  padding: 12px;
@@ -434,54 +312,132 @@
434
  #summary span {
435
  font-size: 20px;
436
  }
437
- .user-group summary {
438
- padding: 15px 20px;
439
- }
440
  .user-servers {
441
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
442
- gap: 12px;
443
- padding: 15px 20px 20px;
444
- }
445
- .server-card {
446
- padding: 15px;
447
- }
448
- .server-header {
449
- font-size: 14px;
450
- margin-bottom: 10px;
451
  }
452
  .metric-grid {
 
453
  gap: 8px;
454
  }
455
  .metric-item {
456
- padding: 8px;
457
- }
458
- .metric-label {
459
- font-size: 11px;
460
  }
461
  .metric-value {
462
- font-size: 14px;
463
  }
464
- .action-button {
465
- padding: 7px 12px;
466
- font-size: 12px;
 
467
  }
468
- .network-stats {
469
- padding: 12px;
470
- gap: 15px;
 
 
 
471
  }
472
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  .filter-sort-panel {
474
  background: var(--card-background);
475
  border: 1px solid var(--card-border);
476
- border-radius: 12px;
477
- padding: 20px;
478
- margin-bottom: 25px;
479
  display: flex;
480
  flex-wrap: wrap;
481
  gap: 15px;
482
  align-items: center;
 
483
  transition: background 0.3s ease, border 0.3s ease;
484
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
485
  }
486
  .filter-sort-group {
487
  display: flex;
@@ -490,260 +446,108 @@
490
  font-size: 14px;
491
  color: var(--text-color);
492
  min-width: 200px;
493
- flex-basis: calc(50% - 10px);
494
- flex-grow: 1;
495
- max-width: 300px;
496
- }
497
- @media (max-width: 768px) {
498
- .filter-sort-group {
499
- flex-basis: 100%;
500
- max-width: 100%;
501
- }
502
- .refresh-button {
503
- width: 100%;
504
- justify-content: center;
505
- }
506
  }
507
  .filter-sort-group label {
508
  white-space: nowrap;
509
  color: var(--secondary-text);
510
  font-weight: 500;
511
- min-width: 60px;
512
  }
513
  .filter-sort-group select {
514
  flex: 1;
515
  background: var(--metric-background);
516
  border: 1px solid var(--metric-border);
517
  color: var(--text-color);
518
- padding: 10px 12px;
519
  border-radius: 6px;
520
  cursor: pointer;
521
  font-size: 14px;
522
- font-weight: 500;
523
- transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
524
  outline: none;
525
  appearance: none;
526
- background-image: url("data:image/svg+xml;utf8,<svg fill='%23b0b0b0' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");
527
  background-repeat: no-repeat;
528
  background-position: right 10px center;
529
  padding-right: 36px;
530
  }
531
- [data-theme="light"] .filter-sort-group select {
532
- background-image: url("data:image/svg+xml;utf8,<svg fill='%23555' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");
533
- }
534
  .filter-sort-group select:hover {
535
  background-color: var(--metric-hover);
536
- border-color: rgba(255, 255, 255, 0.15);
537
  }
538
  .filter-sort-group select:focus {
539
- border-color: var(--accent-color);
540
- box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
541
  }
542
  .refresh-button {
543
- background: var(--accent-color);
544
  border: none;
545
  color: #fff;
546
- padding: 10px 18px;
547
  border-radius: 6px;
548
  cursor: pointer;
549
  font-size: 14px;
550
- font-weight: 500;
551
- transition: background 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
552
  display: flex;
553
  align-items: center;
554
  gap: 8px;
555
  height: 40px;
556
- outline: none;
557
- }
558
- .refresh-button:disabled {
559
- opacity: 0.6;
560
- cursor: not-allowed;
561
  }
562
- .refresh-button:hover:not(:disabled) {
563
- background: #3a8d40;
564
  transform: translateY(-1px);
565
  }
566
- .refresh-button:active:not(:disabled) {
567
- transform: translateY(0);
568
- }
569
- .refresh-button:focus-visible:not(:disabled) {
570
- outline: 2px solid #fff;
571
- outline-offset: 2px;
572
  }
573
  .refresh-icon {
574
  width: 16px;
575
  height: 16px;
576
  fill: currentColor;
577
  }
578
- .refresh-button.loading .refresh-icon {
579
- animation: spin 1s linear infinite;
580
- }
581
- @keyframes spin {
582
- 0% { transform: rotate(0deg); }
583
- 100% { transform: rotate(360deg); }
 
 
584
  }
585
  .chart-container {
586
  display: none;
587
  margin-top: 15px;
588
- background: var(--metric-background);
589
- border: 1px solid var(--metric-border);
590
  border-radius: 8px;
591
- padding: 8px;
592
- height: 280px;
593
  transition: background 0.3s ease, border 0.3s ease;
594
  }
595
- .server-card.expanded .chart-container {
596
- display: block;
597
- }
598
- canvas {
599
- width: 100% !important;
600
- height: 100% !important;
601
- }
602
- @media (max-width: 600px) {
603
- .chart-container {
604
- height: 220px;
605
- }
606
- }
607
  .chart-toggle-button {
608
- background: var(--action-button-bg);
609
- color: var(--action-button-text);
610
- border: none;
611
  padding: 6px 12px;
612
  border-radius: 4px;
613
  cursor: pointer;
614
- font-size: 12px;
615
- transition: background 0.2s ease, transform 0.1s ease;
616
  margin-left: auto;
617
  white-space: nowrap;
618
- outline: none;
619
  }
620
  .chart-toggle-button:hover {
621
- background: var(--action-button-hover);
622
- transform: translateY(-1px);
623
- }
624
- .chart-toggle-button:active {
625
- transform: translateY(0);
626
- }
627
- .chart-toggle-button:focus-visible {
628
- outline: 2px solid var(--accent-color);
629
- outline-offset: 2px;
630
- }
631
- .login-overlay, .confirm-overlay, .loading-overlay {
632
- position: fixed;
633
- top: 0;
634
- left: 0;
635
- width: 100%;
636
- height: 100%;
637
- background: var(--overlay-background);
638
- display: flex;
639
- align-items: center;
640
- justify-content: center;
641
- z-index: 1000;
642
- visibility: hidden;
643
- opacity: 0;
644
- transition: opacity 0.3s ease, visibility 0.3s ease;
645
- }
646
- .login-overlay.visible, .confirm-overlay.visible, .loading-overlay.visible {
647
- visibility: visible;
648
- opacity: 1;
649
- }
650
- .login-box, .confirm-box {
651
- background: var(--card-background);
652
- padding: 30px;
653
- border-radius: 10px;
654
- border: 1px solid var(--card-border);
655
- width: 90%;
656
- max-width: 380px;
657
- text-align: center;
658
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
659
- transform: scale(0.95);
660
- transition: transform 0.3s ease;
661
- }
662
- .login-overlay.visible .login-box,
663
- .confirm-overlay.visible .confirm-box {
664
- transform: scale(1);
665
- }
666
- .login-box h2, .confirm-box h2 {
667
- margin-bottom: 25px;
668
- color: var(--text-color);
669
- font-size: 20px;
670
- font-weight: 600;
671
- }
672
- .login-box input {
673
- width: 100%;
674
- padding: 12px;
675
- margin: 8px 0;
676
- border: 1px solid var(--metric-border);
677
- border-radius: 6px;
678
- background: var(--metric-background);
679
- color: var(--text-color);
680
- font-size: 15px;
681
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
682
- outline: none;
683
- }
684
- .login-box input::placeholder {
685
- color: var(--secondary-text);
686
- opacity: 0.7;
687
- }
688
- .login-box input:focus {
689
- border-color: var(--accent-color);
690
- box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
691
- }
692
- .login-box .button-group, .confirm-box .button-group {
693
- display: flex;
694
- justify-content: space-between;
695
- gap: 10px;
696
- margin-top: 20px;
697
- }
698
- .login-box button, .confirm-box button {
699
- flex: 1;
700
- padding: 12px;
701
- background: var(--action-button-bg);
702
- border: none;
703
- border-radius: 6px;
704
- color: var(--action-button-text);
705
- cursor: pointer;
706
- font-size: 15px;
707
- font-weight: 500;
708
- transition: background 0.2s ease, transform 0.1s ease;
709
- outline: none;
710
- }
711
- .confirm-box button:first-child {
712
- background: var(--accent-color);
713
- color: #fff;
714
- }
715
- .confirm-box button:first-child:hover {
716
- background: #3a8d40;
717
- }
718
- .login-box button:hover, .confirm-box button:hover {
719
- background: var(--action-button-hover);
720
- transform: translateY(-1px);
721
- }
722
- .login-box button:active, .confirm-box button:active {
723
- transform: translateY(0);
724
- }
725
- .login-box button:focus-visible, .confirm-box button:focus-visible {
726
- outline: 2px solid var(--accent-color);
727
- outline-offset: 2px;
728
  }
729
- .login-error {
730
- color: var(--error-color);
731
- margin-top: 15px;
732
- font-size: 14px;
733
- min-height: 20px;
734
  }
735
- .confirm-box p {
736
- line-height: 1.5;
737
- font-size: 15px;
738
- color: var(--secondary-text);
739
  }
740
- .loader {
741
- border: 5px solid rgba(255, 255, 255, 0.3);
742
- border-top: 5px solid #fff;
743
- border-radius: 50%;
744
- width: 50px;
745
- height: 50px;
746
- animation: spin 0.8s linear infinite;
747
  }
748
  </style>
749
  </head>
@@ -751,14 +555,20 @@
751
  <div class="container">
752
  <div class="overview">
753
  <div class="header-container">
754
- <div class="overview-title">📊 系统概览</div>
 
 
 
 
 
 
755
  <div class="auth-buttons">
756
  <button class="login-button" id="loginButton" onclick="showLoginForm()">登录</button>
757
  <button class="logout-button" id="logoutButton" style="display: none;" onclick="logout()">登出</button>
758
  </div>
759
  </div>
760
  <div class="theme-toggle">
761
- <span>主题:</span>
762
  <button onclick="toggleTheme('system')" title="跟随系统">
763
  <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
764
  <path d="M3 5h18c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V7c0-1.1.9-2 2-2zm0 12h18V7H3v10zm2-8h2v2H5V9zm0 4h2v2H5v-2zm4-4h10v2H9V9zm0 4h10v2H9v-2z"/>
@@ -776,16 +586,17 @@
776
  </button>
777
  </div>
778
  <div id="summary">
779
- <div>总用户数 <span id="totalUsers">0</span></div>
780
- <div>总实例数 <span id="totalServers">0</span></div>
781
- <div>在线实例 <span id="onlineServers">0</span></div>
782
- <div>离线实例 <span id="offlineServers">0</span></div>
783
- <div>总上传 <span id="totalUpload">0 B/s</span></div>
784
- <div>总下载 <span id="totalDownload">0 B/s</span></div>
785
  </div>
786
  <div class="network-stats">
787
- 目前总上传速度: <strong id="totalUpload2">0 B/s</strong>
788
- 目前总下载速度: <strong id="totalDownload2">0 B/s</strong>
 
789
  </div>
790
  </div>
791
  <div class="filter-sort-panel">
@@ -820,14 +631,15 @@
820
  刷新数据
821
  </button>
822
  </div>
823
- <div id="servers" class="stats-container"></div>
 
824
  </div>
825
  <div id="loginOverlay" class="login-overlay">
826
  <div class="login-box">
827
  <h2>登录</h2>
828
- <input type="text" id="username" placeholder="用户名" autocomplete="username">
829
- <input type="password" id="password" placeholder="密码" autocomplete="current-password">
830
- <div class="button-group">
831
  <button onclick="login()">登录</button>
832
  <button onclick="hideLoginForm()">取消</button>
833
  </div>
@@ -837,54 +649,36 @@
837
  <div id="confirmOverlay" class="confirm-overlay">
838
  <div class="confirm-box">
839
  <h2 id="confirmTitle">确认操作</h2>
840
- <p id="confirmMessage" style="margin-bottom: 20px; color: var(--secondary-text);"></p>
841
- <div class="button-group">
842
- <button onclick="confirmAction()">确认</button>
843
- <button onclick="cancelAction()">取消</button>
844
- </div>
845
  </div>
846
  </div>
847
  <div id="loadingOverlay" class="loading-overlay">
848
  <div class="loader"></div>
849
  </div>
850
  <script>
851
- // 日志函数,便于调试
852
- function logDebug(message) {
853
- console.log(`[DEBUG] ${new Date().toISOString()}: ${message}`);
854
- }
855
- function logError(message) {
856
- console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
857
- }
858
-
859
- // 主题切换
860
  function setTheme(theme) {
861
- logDebug(`设置主题: ${theme}`);
862
- const htmlElement = document.documentElement;
863
- const themeToggleButtons = document.querySelectorAll('.theme-toggle button');
864
- themeToggleButtons.forEach(button => {
865
- button.removeAttribute('data-active');
866
- });
867
  if (theme === 'system') {
868
  localStorage.removeItem('theme');
869
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
870
- htmlElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
871
- document.querySelector('.theme-toggle button[title="跟随系统"]').setAttribute('data-active', 'true');
872
  } else {
873
  localStorage.setItem('theme', theme);
874
- htmlElement.setAttribute('data-theme', theme);
875
- document.querySelector(`.theme-toggle button[title="${theme === 'light' ? '浅色模式' : '深色模式'}"]`).setAttribute('data-active', 'true');
876
  }
877
  }
878
  function toggleTheme(theme) {
879
  setTheme(theme);
880
  }
881
  function initTheme() {
882
- logDebug('初始化主题...');
883
  const savedTheme = localStorage.getItem('theme');
884
  if (savedTheme) {
885
  setTheme(savedTheme);
886
  } else {
887
- setTheme('system');
 
888
  }
889
  }
890
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
@@ -894,114 +688,98 @@
894
  });
895
  initTheme();
896
 
897
- // 全局状态
898
  let isLoggedIn = false;
899
- let allInstances = [];
900
- const serverStatus = new Map();
901
- const instanceMap = new Map();
902
- const chartInstances = new Map();
903
 
904
- // 加载状态控制
905
  function showLoading() {
906
- logDebug('显示加载中...');
907
- document.getElementById('loadingOverlay').classList.add('visible');
908
- document.getElementById('refreshButton').classList.add('loading');
909
- document.getElementById('refreshButton').disabled = true;
910
  }
911
  function hideLoading() {
912
- logDebug('隐藏加载中...');
913
- document.getElementById('loadingOverlay').classList.remove('visible');
914
- document.getElementById('refreshButton').classList.remove('loading');
915
- document.getElementById('refreshButton').disabled = false;
916
  }
917
 
918
- // 登录状态管理
919
- async function checkLoginStatus() {
920
- logDebug('检查登录状态...');
921
  const token = localStorage.getItem('authToken');
922
  const loginButton = document.getElementById('loginButton');
923
  const logoutButton = document.getElementById('logoutButton');
924
  if (token) {
925
- logDebug('找到本地 token,尝试验证...');
926
  showLoading();
927
- try {
928
- const response = await fetch('/api/verify-token', {
929
- method: 'POST',
930
- headers: { 'Content-Type': 'application/json' },
931
- body: JSON.stringify({ token })
932
- });
933
- const data = await response.json();
 
 
934
  hideLoading();
935
  if (data.success) {
936
- logDebug('Token 验证成功,用户已登录');
937
  isLoggedIn = true;
938
  loginButton.style.display = 'none';
939
  logoutButton.style.display = 'block';
940
  updateActionButtons(true);
941
- return true;
942
  } else {
943
- logDebug('Token 验证失败或过期,清除本地存储: ' + data.message);
944
  localStorage.removeItem('authToken');
945
  isLoggedIn = false;
946
  loginButton.style.display = 'block';
947
  logoutButton.style.display = 'none';
948
  updateActionButtons(false);
949
- return false;
950
  }
951
- } catch (error) {
 
 
952
  hideLoading();
953
- logError('验证 token 请求失败,清除本地存储: ' + error.message);
954
  localStorage.removeItem('authToken');
955
  isLoggedIn = false;
956
  loginButton.style.display = 'block';
957
  logoutButton.style.display = 'none';
958
  updateActionButtons(false);
959
  return false;
960
- }
961
  } else {
962
- logDebug('本地存储中无 token,设置为未登录状态');
963
  isLoggedIn = false;
964
  loginButton.style.display = 'block';
965
  logoutButton.style.display = 'none';
966
  updateActionButtons(false);
967
- return false;
968
  }
969
  }
970
 
971
  function showLoginForm() {
972
- logDebug('显示登录表单');
973
- document.getElementById('loginOverlay').classList.add('visible');
974
  document.getElementById('username').value = '';
975
  document.getElementById('password').value = '';
976
  document.getElementById('loginError').style.display = 'none';
977
  }
978
  function hideLoginForm() {
979
- logDebug('隐藏登录表单');
980
- document.getElementById('loginOverlay').classList.remove('visible');
981
  }
982
- async function login() {
983
- logDebug('尝试登录...');
984
  const username = document.getElementById('username').value;
985
  const password = document.getElementById('password').value;
986
  const loginError = document.getElementById('loginError');
987
- if (!username || !password) {
988
- loginError.textContent = '请输入用户名和密码';
989
- loginError.style.display = 'block';
990
- return;
991
- }
992
  showLoading();
993
- try {
994
- logDebug('发送登录请求...');
995
- const response = await fetch('/api/login', {
996
- method: 'POST',
997
- headers: { 'Content-Type': 'application/json' },
998
- body: JSON.stringify({ username, password })
999
- });
1000
- const data = await response.json();
1001
- logDebug('收到登录响应: ' + JSON.stringify(data));
1002
  hideLoading();
1003
- if (data && data.success) {
1004
- logDebug('登录成功,保存 token');
1005
  localStorage.setItem('authToken', data.token);
1006
  isLoggedIn = true;
1007
  hideLoginForm();
@@ -1010,31 +788,29 @@
1010
  updateActionButtons(true);
1011
  refreshData();
1012
  } else {
1013
- logDebug('登录失败: ' + (data.message || '未知错误'));
1014
- loginError.textContent = data.message || '登录失败,请检查用户名和密码';
1015
  loginError.style.display = 'block';
1016
  }
1017
- } catch (error) {
 
1018
  hideLoading();
1019
- logError('登录请求失败: ' + error.message);
1020
- loginError.textContent = '登录请求失败,请检查网络';
1021
  loginError.style.display = 'block';
1022
- }
1023
  }
1024
- async function logout() {
1025
- logDebug('尝试登出...');
1026
  const token = localStorage.getItem('authToken');
1027
  if (token) {
1028
  showLoading();
1029
- try {
1030
- await fetch('/api/logout', {
1031
- method: 'POST',
1032
- headers: { 'Content-Type': 'application/json' },
1033
- body: JSON.stringify({ token })
1034
- });
1035
- } catch (error) {
1036
- logError('登出请求失败,但仍清除 token: ' + error.message);
1037
- } finally {
1038
  hideLoading();
1039
  localStorage.removeItem('authToken');
1040
  isLoggedIn = false;
@@ -1042,9 +818,17 @@
1042
  document.getElementById('logoutButton').style.display = 'none';
1043
  updateActionButtons(false);
1044
  refreshData();
1045
- }
 
 
 
 
 
 
 
 
 
1046
  } else {
1047
- logDebug('本地无 token,直接设置为未登录状态');
1048
  isLoggedIn = false;
1049
  document.getElementById('loginButton').style.display = 'block';
1050
  document.getElementById('logoutButton').style.display = 'none';
@@ -1053,7 +837,6 @@
1053
  }
1054
  }
1055
  function updateActionButtons(loggedIn) {
1056
- logDebug('更新操作按钮状态,是否已登录: ' + loggedIn);
1057
  isLoggedIn = loggedIn;
1058
  const cards = document.querySelectorAll('.server-card');
1059
  cards.forEach(card => {
@@ -1061,47 +844,33 @@
1061
  if (buttons) {
1062
  buttons.style.display = loggedIn ? 'flex' : 'none';
1063
  }
1064
- });
1065
- const chartToggleButtons = document.querySelectorAll('.chart-toggle-button');
1066
- chartToggleButtons.forEach(button => {
1067
- button.style.display = 'inline-block';
 
 
 
 
 
1068
  });
1069
  }
1070
 
1071
- // 初始化逻辑
1072
- window.onload = function() {
1073
- logDebug('页面加载完成,开始初始化...');
1074
- showLoading();
1075
- checkLoginStatus().then(() => {
1076
- logDebug('登录状态检查完成,开始初始化数据...');
1077
- initialize().catch(error => {
1078
- logError('初始化数据失败: ' + error.message);
1079
- hideLoading();
1080
- });
1081
- }).catch(error => {
1082
- logError('检查登录状态失败: ' + error.message);
1083
- hideLoading();
1084
- initialize().catch(err => {
1085
- logError('即使登录检查失败,初始化数据仍然失败: ' + err.message);
1086
- hideLoading();
1087
- });
1088
- });
1089
  };
1090
 
1091
- // 二次确认弹窗逻辑
1092
  let pendingAction = null;
1093
  let pendingRepoId = null;
1094
  function showConfirmDialog(action, repoId, title, message) {
1095
- if (!isLoggedIn) {
1096
- alert('请先登录以执行此操作');
1097
- showLoginForm();
1098
- return;
1099
- }
1100
  pendingAction = action;
1101
  pendingRepoId = repoId;
1102
  document.getElementById('confirmTitle').textContent = title;
1103
  document.getElementById('confirmMessage').textContent = message;
1104
- document.getElementById('confirmOverlay').classList.add('visible');
1105
  }
1106
  function confirmAction() {
1107
  if (pendingAction === 'restart') {
@@ -1114,29 +883,25 @@
1114
  function cancelAction() {
1115
  pendingAction = null;
1116
  pendingRepoId = null;
1117
- document.getElementById('confirmOverlay').classList.remove('visible');
1118
  }
1119
 
 
1120
  async function getUsernames() {
1121
- logDebug('尝试获取用户名列表...');
1122
  try {
 
1123
  const token = localStorage.getItem('authToken');
1124
  const headers = {};
1125
  if (token) {
1126
  headers['Authorization'] = `Bearer ${token}`;
1127
- logDebug('getUsernames 请求中附加 Token');
1128
  }
1129
  const response = await fetch('/api/config', { headers });
1130
- if (!response.ok) {
1131
- logError(`获取配置失败: ${response.status} ${response.statusText}`);
1132
- return [];
1133
- }
1134
  const config = await response.json();
1135
- logDebug('成功获取用户名列表: ' + JSON.stringify(config));
1136
  const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
1137
  document.getElementById('totalUsers').textContent = usernamesList.length;
 
1138
  const userFilter = document.getElementById('userFilter');
1139
- const currentUserFilterValue = userFilter.value;
1140
  userFilter.innerHTML = '<option value="all">全部用户</option>';
1141
  usernamesList.forEach(username => {
1142
  const option = document.createElement('option');
@@ -1144,170 +909,263 @@
1144
  option.textContent = username;
1145
  userFilter.appendChild(option);
1146
  });
1147
- if (usernamesList.includes(currentUserFilterValue)) {
1148
- userFilter.value = currentUserFilterValue;
1149
- } else {
1150
- userFilter.value = 'all';
1151
- }
1152
  return usernamesList;
1153
  } catch (error) {
1154
- logError('获取用户名失败: ' + error.message);
1155
  document.getElementById('totalUsers').textContent = 0;
1156
- const userFilter = document.getElementById('userFilter');
1157
- userFilter.innerHTML = '<option value="all">全部用户</option>';
1158
  return [];
1159
  }
1160
  }
1161
 
 
1162
  async function fetchInstances() {
1163
- logDebug('尝试获取实例列表...');
1164
  try {
 
1165
  const token = localStorage.getItem('authToken');
1166
  const headers = {};
1167
  if (token) {
1168
  headers['Authorization'] = `Bearer ${token}`;
1169
- logDebug('fetchInstances 请求中附加 Token');
1170
- } else {
1171
- logDebug('无可用 Token,未附加 Authorization 头');
1172
  }
1173
  const response = await fetch('/api/proxy/spaces', { headers });
1174
- if (!response.ok) {
1175
- const errorBody = await response.json().catch(() => ({}));
1176
- if (response.status === 401) {
1177
- logDebug('获取实例列表失败: 未授权 (可能是 token 过期)');
1178
- localStorage.removeItem('authToken');
1179
- isLoggedIn = false;
1180
- updateActionButtons(false);
1181
- document.getElementById('loginButton').style.display = 'block';
1182
- document.getElementById('logoutButton').style.display = 'none';
1183
- alert('登录已过期,请重新登录');
1184
- showLoginForm();
1185
- } else {
1186
- alert(`获取实例列表失败: ${response.status} ${response.statusText} - ${errorBody.error || '未知服务器错误'}`);
1187
- logError(`获取实例列表失败: ${response.status} ${response.statusText}` + JSON.stringify(errorBody));
1188
- }
1189
- return [];
1190
- }
1191
  const instances = await response.json();
1192
- logDebug('从后端获取的实例列表: ' + instances.length + ' 个实例');
1193
  if (instances.length === 0) {
1194
- logDebug('警告: 未获取到任何实例数据。');
1195
  }
1196
  return instances;
1197
  } catch (error) {
1198
- logError("获取实例列表失败:" + error.message);
1199
- alert('获取实例列表网络请求失败,请检查后端服务。');
1200
  return [];
1201
  }
1202
  }
1203
 
 
1204
  class MetricsStreamManager {
1205
  constructor() {
1206
  this.eventSource = null;
1207
- this.subscribedInstances = [];
1208
  }
1209
  connect(subscribedInstances = []) {
1210
- logDebug('尝试连接 SSE,订阅实例: ' + subscribedInstances.length);
1211
- if (this.eventSource && this.subscribedInstances.length === subscribedInstances.length && this.subscribedInstances.every((value, index) => value === subscribedInstances[index])) {
1212
- logDebug('SSE 连接已存在且订阅列表未改变,无需重新连接');
1213
- return;
1214
- }
1215
- this.disconnect();
1216
- this.subscribedInstances = subscribedInstances;
1217
- if (subscribedInstances.length === 0) {
1218
- logDebug('没有需要监控的实例,不建立 SSE 连接');
1219
- return;
1220
  }
1221
  const instancesParam = subscribedInstances.join(',');
1222
- const token = localStorage.getItem('authToken') || '';
1223
- const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token)}`;
1224
- logDebug('建立 SSE 连接 URL: ' + url.split('&token=')[0] + (token ? '&token=...(隐藏)' : '&token=空'));
1225
  this.eventSource = new EventSource(url);
1226
- this.eventSource.onopen = () => {
1227
- logDebug('SSE 连接已成功建立');
1228
- };
1229
  this.eventSource.addEventListener("metric", (event) => {
1230
  try {
1231
  const data = JSON.parse(event.data);
1232
  const { repoId, metrics } = data;
1233
- if (document.getElementById(`instance-${repoId}`)) {
1234
- logDebug(`收到 ${repoId} 的监控数据`);
1235
- updateServerCard(metrics, repoId);
1236
- }
1237
  } catch (error) {
1238
- logError(`解析监控数据失败: ` + event.data + ' ERROR: ' + error.message);
1239
  }
1240
  });
1241
  this.eventSource.onerror = (error) => {
1242
- logError(`SSE 连接错误: ` + JSON.stringify(error));
 
 
1243
  };
1244
- logDebug(`SSE 连接已建立,订阅实例: ${instancesParam}`);
1245
  }
1246
  disconnect() {
1247
  if (this.eventSource) {
1248
  this.eventSource.close();
1249
  this.eventSource = null;
1250
- logDebug(`SSE 连接已断开`);
1251
  }
1252
- this.subscribedInstances = [];
1253
  }
1254
  }
1255
-
1256
  const metricsStreamManager = new MetricsStreamManager();
 
 
 
 
1257
 
 
1258
  async function initialize() {
1259
- logDebug('开始初始化数据...');
1260
- showLoading();
1261
- try {
1262
- logDebug('获取用户名列表...');
1263
- await getUsernames();
1264
- logDebug('获取实例列表...');
1265
- allInstances = await fetchInstances();
1266
- logDebug('实例列表获取完成,数量: ' + allInstances.length);
1267
- chartInstances.forEach(chart => {
1268
- if (chart) {
1269
- chart.destroy();
1270
- }
1271
- });
1272
- chartInstances.clear();
1273
- serverStatus.clear();
1274
- instanceMap.clear();
1275
- logDebug('应用过滤和排序...');
1276
- applyFiltersAndSort();
1277
- logDebug('初始化完成');
1278
- } catch (error) {
1279
- logError('初始化失败: ' + error.message);
1280
- } finally {
1281
- hideLoading();
1282
- logDebug('初始化过程结束,隐藏加载状态');
1283
- }
1284
  }
1285
 
 
1286
  async function refreshData() {
1287
- logDebug('手动刷新数据...');
1288
  showLoading();
1289
  metricsStreamManager.disconnect();
1290
- await new Promise(resolve => setTimeout(resolve, 100));
 
 
 
 
 
1291
  await initialize();
 
 
 
1292
  }
1293
 
1294
- function renderInstanceCard(instance, container) {
1295
- logDebug(`渲染实例: ${instance.repo_id}`);
1296
- const instanceId = instance.repo_id;
1297
- if (instance.private && !isLoggedIn) {
1298
- const existingCard = document.getElementById(`instance-${instanceId}`);
1299
- if (existingCard) {
1300
- existingCard.remove();
1301
- instanceMap.delete(instanceId);
1302
- serverStatus.delete(instanceId);
1303
- if (chartInstances.has(instanceId)) {
1304
- chartInstances.get(instanceId).destroy();
1305
- chartInstances.delete(instanceId);
1306
- }
1307
  }
1308
- logDebug(`跳过私有实例渲染: ${instanceId} (未登录)`);
1309
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1310
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1311
  instanceMap.set(instanceId, instance);
1312
  const cardId = `instance-${instanceId}`;
1313
  let card = document.getElementById(cardId);
@@ -1315,15 +1173,22 @@
1315
  card = document.createElement('div');
1316
  card.id = cardId;
1317
  card.className = 'server-card';
 
 
 
1318
  const iconSvg = instance.private
1319
- ? `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18 8v-3c0-1.656-1.344-3-3-3h-6c-1.656 0-3 1.344-3 3v3h-3v14h18v-14h-3zm-10-1.5c0-.828.672-1.5 1.5-1.5h5c.828 0 1.5.672 1.5 1.5v2.5h-8v-2.5zm4 11.5c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/></svg>`
1320
- : `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3C1.9 3 1 3.9 1 5v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 5H4V6h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2z"/></svg>`;
 
 
 
 
1321
  card.innerHTML = `
1322
  <div class="server-header">
1323
  <div class="server-name">
1324
- <span class="status-dot"></span>
1325
  ${iconSvg}
1326
- <div>${instance.name} </div>
1327
  </div>
1328
  <div>
1329
  <button class="chart-toggle-button" onclick="toggleChart('${instanceId}')">查看图表</button>
@@ -1352,8 +1217,8 @@
1352
  </div>
1353
  </div>
1354
  <div class="action-buttons" style="display: ${isLoggedIn ? 'flex' : 'none'};">
1355
- <button class="action-button " onclick="viewInstance('${instance.url}')">查看空间</button>
1356
- <button class="action-button " onclick="manageInstance('${instance.repo_id}')">管理页面</button>
1357
  <button class="action-button" onclick="showConfirmDialog('restart', '${instance.repo_id}', '确认重启', '您确定要重启实例 ${instance.name} (${instance.repo_id}) 吗?')">重启</button>
1358
  <button class="action-button" onclick="showConfirmDialog('rebuild', '${instance.repo_id}', '确认重建', '您确定要重建实例 ${instance.name} (${instance.repo_id}) 吗?')">重建</button>
1359
  </div>
@@ -1362,144 +1227,85 @@
1362
  </div>
1363
  `;
1364
  container.appendChild(card);
1365
- logDebug(`新卡片已添加: ${instanceId}`);
1366
  }
1367
  const statusDot = card.querySelector('.status-dot');
1368
- const statusTextElement = card.querySelector('.metric-value.status');
1369
- const currentStatus = instance.status.toLowerCase();
1370
- statusDot.className = 'status-dot';
1371
- statusTextElement.className = 'metric-value status';
1372
- if (currentStatus === 'running') {
1373
- statusDot.classList.add('status-online');
1374
- statusTextElement.classList.add('status-running');
1375
- } else if (currentStatus === 'sleeping') {
1376
- statusDot.classList.add('status-sleep');
1377
- statusTextElement.classList.add('status-sleeping');
1378
  } else {
1379
- statusDot.classList.add('status-offline');
1380
- statusTextElement.classList.add('status-stopped');
1381
- }
1382
- serverStatus.set(instanceId, {
1383
- lastSeen: Date.now(),
1384
- isOnline: currentStatus === 'running',
1385
- isSleep: currentStatus === 'sleeping',
1386
- data: null,
1387
- status: instance.status
1388
- });
1389
- const buttons = card.querySelector('.action-buttons');
1390
- if (buttons) {
1391
- buttons.style.display = isLoggedIn ? 'flex' : 'none';
1392
- }
1393
- const chartToggleButton = card.querySelector('.chart-toggle-button');
1394
- if (chartToggleButton) {
1395
- chartToggleButton.style.display = 'inline-block';
1396
  }
 
1397
  }
1398
 
1399
- function renderInstances(instancesToRender) {
1400
- logDebug(`渲染实例列表,数量: ${instancesToRender.length}`);
1401
- hideLoading();
1402
- const serversContainer = document.getElementById('servers');
1403
- const currentInstanceIds = new Set(Array.from(serversContainer.querySelectorAll('.server-card')).map(card => card.id.replace('instance-', '')));
1404
- const userGroupsContainer = document.getElementById('servers');
1405
- userGroupsContainer.innerHTML = '';
1406
- const userGroups = {};
1407
- instancesToRender.forEach(instance => {
1408
- if (!userGroups[instance.owner]) {
1409
- userGroups[instance.owner] = [];
1410
- }
1411
- userGroups[instance.owner].push(instance);
1412
- });
1413
- Object.keys(userGroups).forEach(owner => {
1414
- let userGroup = document.createElement('details');
1415
- userGroup.className = 'user-group';
1416
- userGroup.id = `user-${owner}`;
1417
- userGroup.setAttribute('open', '');
1418
- const summary = document.createElement('summary');
1419
- summary.textContent = `用户: ${owner} (${userGroups[owner].length} 个实例)`;
1420
- userGroup.appendChild(summary);
1421
- const userServers = document.createElement('div');
1422
- userServers.className = 'user-servers';
1423
- userGroup.appendChild(userServers);
1424
- userGroupsContainer.appendChild(userGroup);
1425
- userGroups[owner].forEach(instance => {
1426
- renderInstanceCard(instance, userServers);
1427
- });
1428
- });
1429
- const newInstanceIdsToRender = new Set(instancesToRender.map(inst => inst.repo_id));
1430
- currentInstanceIds.forEach(instanceId => {
1431
- if (!newInstanceIdsToRender.has(instanceId)) {
1432
- if (chartInstances.has(instanceId)) {
1433
- logDebug(`清理不再显示的图表实例: ${instanceId}`);
1434
- chartInstances.get(instanceId).destroy();
1435
- chartInstances.delete(instanceId);
1436
- }
1437
  }
1438
- });
1439
- const runningInstancesInView = instancesToRender
1440
- .filter(instance => instance.status.toLowerCase() === 'running')
1441
- .map(instance => instance.repo_id);
1442
- logDebug(`连接 SSE,监控运行中实例: ${runningInstancesInView.length}`);
1443
- metricsStreamManager.connect(runningInstancesInView);
1444
- updateSummary();
1445
  }
1446
 
1447
- function updateServerCard(data, instanceId) {
1448
- logDebug(`更新实例卡片数据: ${instanceId}`);
1449
- const card = document.getElementById(`instance-${instanceId}`);
 
1450
  const instance = instanceMap.get(instanceId);
1451
- if (!card || !instance) {
1452
- logDebug(`卡片或实例不存在,跳过更新: ${instanceId}`);
1453
  return;
1454
  }
1455
- const statusDot = card.querySelector('.status-dot');
1456
- const statusTextElement = card.querySelector('.metric-value.status');
1457
- let isOnline = false;
1458
- let isSleep = false;
1459
- let currentStatus = instance.status.toLowerCase();
1460
- if (data && Object.keys(data).length > 0) {
1461
- currentStatus = 'running';
1462
- isOnline = true;
1463
- isSleep = false;
1464
- card.querySelector('.cpu-usage').textContent = `${data.cpu_usage_pct?.toFixed(2) || 'N/A'}%`;
1465
- const memoryPct = (data.memory_total_bytes > 0) ? ((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2) : 'N/A';
1466
- card.querySelector('.memory-usage').textContent = `${memoryPct}%`;
1467
- card.querySelector('.upload').textContent = `${formatBytes(data.tx_bps)}/s`;
1468
- card.querySelector('.download').textContent = `${formatBytes(data.rx_bps)}/s`;
1469
- const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1470
- if (chartContainer && chartContainer.parentElement.classList.contains('expanded')) {
1471
  updateChart(instanceId, data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1472
  }
1473
- } else {
1474
- card.querySelector('.cpu-usage').textContent = 'N/A';
1475
- card.querySelector('.memory-usage').textContent = 'N/A';
1476
- card.querySelector('.upload').textContent = 'N/A';
1477
- card.querySelector('.download').textContent = 'N/A';
1478
- }
1479
- statusDot.className = 'status-dot';
1480
- statusTextElement.className = 'metric-value status';
1481
- if (currentStatus === 'running') {
1482
- statusDot.classList.add('status-online');
1483
- statusTextElement.classList.add('status-running');
1484
- } else if (currentStatus === 'sleeping') {
1485
- statusDot.classList.add('status-sleep');
1486
- statusTextElement.classList.add('status-sleeping');
1487
- } else {
1488
- statusDot.classList.add('status-offline');
1489
- statusTextElement.classList.add('status-stopped');
1490
  }
1491
- serverStatus.set(instanceId, {
1492
- lastSeen: Date.now(),
1493
- isOnline: currentStatus === 'running',
1494
- isSleep: currentStatus === 'sleeping',
1495
- data: data || null,
1496
- status: instance.status
1497
- });
1498
- updateSummary();
1499
  }
1500
 
 
1501
  async function restartSpace(repoId) {
1502
- logDebug(`尝试重启实例: ${repoId}`);
1503
  try {
1504
  const token = localStorage.getItem('authToken');
1505
  if (!token || !isLoggedIn) {
@@ -1511,12 +1317,14 @@
1511
  const encodedRepoId = encodeURIComponent(repoId);
1512
  const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, {
1513
  method: 'POST',
1514
- headers: { 'Authorization': `Bearer ${token}` }
 
 
1515
  });
1516
  const result = await response.json();
1517
  hideLoading();
1518
  if (result.success) {
1519
- logDebug(`重启成功: ${repoId}`);
1520
  refreshData();
1521
  } else {
1522
  if (response.status === 401) {
@@ -1529,18 +1337,16 @@
1529
  showLoginForm();
1530
  } else {
1531
  alert(`重启失败: ${result.error || '未知错误'}`);
1532
- logError(`重启失败 (${repoId}): ${result.error || '未知错误'} ${JSON.stringify(result.details || {})}`);
1533
  }
1534
  }
1535
  } catch (error) {
1536
  hideLoading();
1537
- logError(`重启请求失败 (${repoId}): ${error.message}`);
1538
- alert(`重启请求失败: ${error.message || '检查网络或后端服务'}`);
1539
  }
1540
  }
1541
 
 
1542
  async function rebuildSpace(repoId) {
1543
- logDebug(`尝试重建实例: ${repoId}`);
1544
  try {
1545
  const token = localStorage.getItem('authToken');
1546
  if (!token || !isLoggedIn) {
@@ -1552,12 +1358,14 @@
1552
  const encodedRepoId = encodeURIComponent(repoId);
1553
  const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, {
1554
  method: 'POST',
1555
- headers: { 'Authorization': `Bearer ${token}` }
 
 
1556
  });
1557
  const result = await response.json();
1558
  hideLoading();
1559
  if (result.success) {
1560
- logDebug(`重建成功: ${repoId}`);
1561
  refreshData();
1562
  } else {
1563
  if (response.status === 401) {
@@ -1570,313 +1378,136 @@
1570
  showLoginForm();
1571
  } else {
1572
  alert(`重建失败: ${result.error || '未知错误'}`);
1573
- logError(`重建失败 (${repoId}): ${result.error || '未知错误'} ${JSON.stringify(result.details || {})}`);
1574
  }
1575
  }
1576
  } catch (error) {
1577
  hideLoading();
1578
- logError(`重建请求失败 (${repoId}): ${error.message}`);
1579
- alert(`重建请求失败: ${error.message || '检查网络或后端服务'}`);
1580
  }
1581
  }
1582
 
 
1583
  function updateSummary() {
1584
- logDebug('更新总览数据...');
1585
  let online = 0;
1586
  let offline = 0;
1587
- let totalRunningUpload = 0;
1588
- let totalRunningDownload = 0;
1589
  serverStatus.forEach((status, instanceId) => {
1590
- if (status.status?.toLowerCase() === 'running') {
 
1591
  online++;
1592
  if (status.data) {
1593
- totalRunningUpload += parseFloat(status.data.tx_bps) || 0;
1594
- totalRunningDownload += parseFloat(status.data.rx_bps) || 0;
1595
  }
1596
  } else {
1597
  offline++;
1598
  }
1599
  });
1600
- document.getElementById('totalServers').textContent = allInstances.length;
 
1601
  document.getElementById('onlineServers').textContent = online;
 
1602
  document.getElementById('offlineServers').textContent = offline;
1603
- document.getElementById('totalUpload').textContent = `${formatBytes(totalRunningUpload)}/s`;
1604
- document.getElementById('totalDownload').textContent = `${formatBytes(totalRunningDownload)}/s`;
1605
- const totalAllUpload = Array.from(serverStatus.values()).reduce((sum, status) => sum + (status.data?.tx_bps || 0), 0);
1606
- const totalAllDownload = Array.from(serverStatus.values()).reduce((sum, status) => sum + (status.data?.rx_bps || 0), 0);
1607
- document.getElementById('totalUpload2').textContent = `${formatBytes(totalAllUpload)}/s`;
1608
- document.getElementById('totalDownload2').textContent = `${formatBytes(totalAllDownload)}/s`;
 
 
 
1609
  }
1610
 
1611
- function formatBytes(bytes, dp = 2) {
1612
- if (bytes === null || bytes === undefined) return 'N/A';
1613
  if (bytes === 0) return '0 B';
1614
- const K = 1024;
1615
- const SIZES = ['B', 'KB', 'MB', 'GB', 'TB'];
1616
- const i = Math.floor(Math.log(bytes) / Math.log(K));
1617
- return parseFloat((bytes / Math.pow(K, i)).toFixed(dp)) + ' ' + SIZES[i];
1618
  }
1619
 
1620
- setInterval(async () => {
1621
- logDebug('定时刷新数据...');
1622
- if (!document.getElementById('loadingOverlay').classList.contains('visible')) {
1623
- await refreshData();
1624
- } else {
1625
- logDebug('正在手动刷新中,跳过定时刷新');
 
 
 
 
 
 
 
 
1626
  }
 
 
 
 
 
 
 
 
1627
  }, 300000);
1628
 
 
1629
  function applyFiltersAndSort() {
1630
- logDebug('应用过滤和排序...');
1631
  const statusFilter = document.getElementById('statusFilter').value;
1632
  const userFilter = document.getElementById('userFilter').value;
1633
  const sortBy = document.getElementById('sortBy').value;
1634
- let filteredInstances = allInstances.filter(instance => {
1635
- const statusMatch = statusFilter === 'all' || instance.status.toLowerCase() === statusFilter;
1636
- const userMatch = userFilter === 'all' || instance.owner === userFilter;
1637
- const privateMatch = !instance.private || isLoggedIn;
1638
- return statusMatch && userMatch && privateMatch;
1639
- });
 
1640
  filteredInstances.sort((a, b) => {
1641
  if (sortBy === 'name-asc') {
1642
  return a.name.localeCompare(b.name);
1643
  } else if (sortBy === 'name-desc') {
1644
  return b.name.localeCompare(a.name);
1645
  } else if (sortBy === 'status-asc') {
1646
- const statusOrder = { 'running': 0, 'sleeping': 1, 'stopped': 2, 'building': 3, 'build_error': 4, 'error': 5, 'unknown': 6 };
1647
- const statusA = statusOrder[a.status?.toLowerCase()] || 6;
1648
- const statusB = statusOrder[b.status?.toLowerCase()] || 6;
1649
- if (statusA !== statusB) {
1650
- return statusA - statusB;
1651
- }
1652
- return a.name.localeCompare(b.name);
1653
  } else if (sortBy === 'status-desc') {
1654
- const statusOrder = { 'running': 5, 'sleeping': 4, 'stopped': 3, 'building': 2, 'build_error': 1, 'error': 0, 'unknown': -1 };
1655
- const statusA = statusOrder[a.status?.toLowerCase()] || -1;
1656
- const statusB = statusOrder[b.status?.toLowerCase()] || -1;
1657
- if (statusA !== statusB) {
1658
- return statusB - statusA;
1659
- }
1660
- return b.name.localeCompare(a.name);
1661
  }
1662
  return 0;
1663
  });
1664
- logDebug(`过滤和排序后的实例数量: ${filteredInstances.length}`);
 
 
 
 
 
 
 
 
1665
  renderInstances(filteredInstances);
 
 
 
 
 
1666
  updateActionButtons(isLoggedIn);
1667
  }
1668
 
 
1669
  function viewInstance(url) {
1670
- logDebug(`尝试查看实例: ${url}`);
1671
- if (url && url !== 'N/A') {
1672
- window.open(url, '_blank');
1673
- } else {
1674
- alert('该实例没有可用的 public URL。');
1675
- }
1676
  }
 
 
1677
  function manageInstance(repoId) {
1678
- logDebug(`尝试管理实例: ${repoId}`);
1679
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;
1680
  window.open(manageUrl, '_blank');
1681
  }
1682
-
1683
- function createChart(instanceId) {
1684
- logDebug(`创建图表实例: ${instanceId}`);
1685
- const canvasId = `chart-${instanceId}`;
1686
- const canvas = document.getElementById(canvasId);
1687
- if (!canvas) {
1688
- logError(`Canvas element not found for instanceId: ${instanceId}`);
1689
- return null;
1690
- }
1691
- if (canvas.chart) {
1692
- logDebug(`Chart already exists for canvasId: ${canvasId}, destroying old one.`);
1693
- canvas.chart.destroy();
1694
- }
1695
- const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
1696
- const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)';
1697
- const textColor = isDarkMode ? '#e0e0e0' : '#333';
1698
- const ctx = canvas.getContext('2d');
1699
- const chart = new Chart(ctx, {
1700
- type: 'line',
1701
- data: {
1702
- labels: Array(30).fill(''),
1703
- datasets: [
1704
- {
1705
- label: 'CPU (%)',
1706
- data: [],
1707
- borderColor: '#4CAF50',
1708
- backgroundColor: 'rgba(76, 175, 80, 0.1)',
1709
- tension: 0.3,
1710
- fill: true,
1711
- pointRadius: 0,
1712
- hitRadius: 10,
1713
- pointHoverRadius: 5,
1714
- },
1715
- {
1716
- label: '内存 (%)',
1717
- data: [],
1718
- borderColor: '#2196F3',
1719
- backgroundColor: 'rgba(33, 150, 243, 0.1)',
1720
- tension: 0.3,
1721
- fill: true,
1722
- pointRadius: 0,
1723
- hitRadius: 10,
1724
- pointHoverRadius: 5,
1725
- },
1726
- {
1727
- label: '上传 (KB/s)',
1728
- data: [],
1729
- borderColor: '#FF9800',
1730
- backgroundColor: 'rgba(255, 152, 0, 0.1)',
1731
- tension: 0.3,
1732
- fill: true,
1733
- pointRadius: 0,
1734
- hitRadius: 10,
1735
- pointHoverRadius: 5,
1736
- },
1737
- {
1738
- label: '下载 (KB/s)',
1739
- data: [],
1740
- borderColor: '#9C27B0',
1741
- backgroundColor: 'rgba(156, 39, 176, 0.1)',
1742
- tension: 0.3,
1743
- fill: true,
1744
- pointRadius: 0,
1745
- hitRadius: 10,
1746
- pointHoverRadius: 5,
1747
- },
1748
- ]
1749
- },
1750
- options: {
1751
- responsive: true,
1752
- maintainAspectRatio: false,
1753
- animation: false,
1754
- plugins: {
1755
- legend: {
1756
- labels: {
1757
- color: textColor,
1758
- font: { size: 11 }
1759
- }
1760
- },
1761
- tooltip: {
1762
- mode: 'index',
1763
- intersect: false,
1764
- callbacks: {
1765
- label: function(context) {
1766
- let label = context.dataset.label || '';
1767
- if (label) {
1768
- label += ': ';
1769
- }
1770
- if (context.parsed.y !== null) {
1771
- label += context.parsed.y.toFixed(2);
1772
- if (label.includes('CPU (%)') || label.includes('内存 (%)')) {
1773
- label += '%';
1774
- }
1775
- }
1776
- return label;
1777
- }
1778
- },
1779
- titleFont: { size: 12 },
1780
- bodyFont: { size: 11 },
1781
- padding: 10,
1782
- displayColors: true,
1783
- boxPadding: 4,
1784
- }
1785
- },
1786
- scales: {
1787
- y: {
1788
- beginAtZero: true,
1789
- max: 100,
1790
- grid: { color: gridColor, drawBorder: false },
1791
- ticks: {
1792
- color: textColor,
1793
- font: { size: 10 },
1794
- callback: function(value) {
1795
- return value + '%';
1796
- }
1797
- },
1798
- title: {
1799
- display: true,
1800
- text: '使用率 (%) / 流量 (KB/s)',
1801
- color: textColor,
1802
- font: { size: 11, weight: 'bold' },
1803
- padding: { top: 0, bottom: 10 }
1804
- }
1805
- },
1806
- x: {
1807
- grid: { color: gridColor, drawBorder: false },
1808
- ticks: {
1809
- color: textColor,
1810
- font: { size: 10 },
1811
- maxRotation: 0,
1812
- minRotation: 0,
1813
- autoSkip: true,
1814
- autoSkipPadding: 15,
1815
- callback: function(value, index, values) {
1816
- return '';
1817
- }
1818
- }
1819
- }
1820
- },
1821
- hover: {
1822
- mode: 'nearest',
1823
- intersect: true
1824
- }
1825
- }
1826
- });
1827
- canvas.chart = chart;
1828
- chartInstances.set(instanceId, chart);
1829
- logDebug(`图表创建成功: ${instanceId}`);
1830
- return chart;
1831
- }
1832
-
1833
- function updateChart(instanceId, data) {
1834
- let chart = chartInstances.get(instanceId);
1835
- const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1836
- if (!chartContainer || !chartContainer.parentElement.classList.contains('expanded')) {
1837
- return;
1838
- }
1839
- if (!chart) {
1840
- chart = createChart(instanceId);
1841
- if (!chart) return;
1842
- logDebug(`Instance ${instanceId}: Chart created.`);
1843
- }
1844
- const cpuDataset = chart.data.datasets[0].data;
1845
- const memoryDataset = chart.data.datasets[1].data;
1846
- const uploadDataset = chart.data.datasets[2].data;
1847
- const downloadDataset = chart.data.datasets[3].data;
1848
- cpuDataset.push(data.cpu_usage_pct?.toFixed(2) || NaN);
1849
- const memoryPct = (data.memory_total_bytes > 0) ? ((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2) : NaN;
1850
- memoryDataset.push(memoryPct);
1851
- uploadDataset.push((data.tx_bps / 1024).toFixed(2));
1852
- downloadDataset.push((data.rx_bps / 1024).toFixed(2));
1853
- const maxDataPoints = 30;
1854
- if (cpuDataset.length > maxDataPoints) {
1855
- cpuDataset.shift();
1856
- memoryDataset.shift();
1857
- uploadDataset.shift();
1858
- downloadDataset.shift();
1859
- }
1860
- chart.update('quiet');
1861
- }
1862
-
1863
- function toggleChart(instanceId) {
1864
- logDebug(`切换图表显示: ${instanceId}`);
1865
- const card = document.getElementById(`instance-${instanceId}`);
1866
- const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1867
- const toggleButton = card.querySelector('.chart-toggle-button');
1868
- if (!card || !chartContainer) return;
1869
- if (card.classList.contains('expanded')) {
1870
- card.classList.remove('expanded');
1871
- toggleButton.textContent = '查看图表';
1872
- } else {
1873
- card.classList.add('expanded');
1874
- toggleButton.textContent = '收起图表';
1875
- if (!chartInstances.has(instanceId)) {
1876
- createChart(instanceId);
1877
- }
1878
- }
1879
- }
1880
  </script>
1881
  </body>
1882
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>HF Space Manager</title>
7
+ <!-- 引入 Chart.js CDN -->
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
 
9
  <style>
 
10
  * {
11
  margin: 0;
12
  padding: 0;
13
  box-sizing: border-box;
14
  }
15
  body {
16
+ font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
17
+ background: #f5f7fa; /* 参考截图,使用浅灰色背景 */
18
+ color: #333;
19
+ padding: 20px;
20
  min-height: 100vh;
21
  transition: background 0.3s ease, color 0.3s ease;
 
22
  }
23
  :root {
24
+ /* 浅色模式变量 */
25
+ --background-color: #f5f7fa;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  --text-color: #333;
27
+ --card-background: #fff;
28
+ --card-border: #e8ecef;
29
+ --metric-background: #f9f9fb;
30
+ --metric-border: #e8ecef;
31
+ --metric-hover: #f1f3f5;
32
+ --secondary-text: #666;
33
+ --label-color: #999;
34
  --network-background: rgba(0, 0, 0, 0.05);
35
+ --action-button-bg: linear-gradient(90deg, #34c759, #28a745); /* 绿色渐变按钮 */
36
+ --action-button-hover: linear-gradient(90deg, #2eb850, #23963d);
 
 
 
 
 
 
37
  }
38
  .container {
39
+ max-width: 1400px;
40
  margin: 0 auto;
41
+ animation: fadeIn 0.5s ease;
42
  padding: 0 15px;
43
  }
44
  .overview {
45
  background: var(--card-background);
46
+ border-radius: 15px;
47
+ padding: 20px;
48
+ margin-bottom: 25px;
49
  border: 1px solid var(--card-border);
50
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); /* 添加阴影 */
51
+ transition: background 0.3s ease, border 0.3s ease;
52
  }
53
  .overview-title {
54
+ font-size: 20px;
 
55
  display: flex;
56
  align-items: center;
57
  gap: 10px;
58
  margin-bottom: 20px;
59
  color: var(--text-color);
60
+ font-weight: 600;
61
  }
62
+ .overview-title svg {
63
+ width: 24px;
64
+ height: 24px;
65
+ fill: #34c759; /* 绿色图标 */
 
 
 
 
 
 
 
66
  }
67
  .theme-toggle {
68
  display: flex;
69
  align-items: center;
70
+ gap: 10px;
71
+ margin-bottom: 15px;
72
  font-size: 14px;
73
  color: var(--secondary-text);
74
  }
 
 
 
 
75
  .theme-toggle button {
76
  background: var(--metric-background);
77
  border: 1px solid var(--metric-border);
78
  color: var(--text-color);
79
+ padding: 6px;
80
+ border-radius: 6px;
81
  cursor: pointer;
82
  display: flex;
83
  align-items: center;
84
  justify-content: center;
85
+ width: 32px;
86
+ height: 32px;
87
+ transition: background 0.2s ease, transform 0.2s ease;
 
88
  }
89
  .theme-toggle button:hover {
90
  background: var(--metric-hover);
91
+ transform: scale(1.05);
 
 
 
 
 
 
 
 
92
  }
93
  .theme-toggle svg {
94
  width: 18px;
95
  height: 18px;
96
+ fill: var(--text-color);
 
 
 
 
 
 
 
 
97
  }
98
  #summary {
99
  display: grid;
100
+ grid-template-columns: repeat(6, 1fr);
101
+ gap: 24px; /* 增加卡片间距 */
102
  }
103
  #summary div {
104
  background: var(--metric-background);
105
+ padding: 16px;
106
+ border-radius: 12px;
107
  border: 1px solid var(--metric-border);
108
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03); /* 添加阴影 */
109
  transition: background 0.3s ease, border 0.3s ease;
110
+ text-align: center;
 
 
111
  }
112
  #summary div {
113
+ font-size: 14px;
114
  color: var(--secondary-text);
115
  }
116
  #summary span {
117
  display: block;
118
+ font-size: 24px; /* 增大数字字体 */
119
+ font-weight: bold;
120
+ margin-top: 8px;
121
  color: var(--text-color);
122
  overflow: hidden;
123
  text-overflow: ellipsis;
124
  white-space: nowrap;
125
+ }
126
+ #summary .no-data {
127
+ color: #999;
128
+ font-size: 16px;
129
+ margin-top: 8px;
130
  }
131
  .stats-container {
132
+ display: grid;
133
+ grid-template-columns: 1fr;
134
+ gap: 20px;
135
+ margin-top: 20px;
136
  }
137
  .user-group {
138
  background: var(--card-background);
139
+ border-radius: 10px;
140
  border: 1px solid var(--card-border);
141
  overflow: hidden;
142
  transition: background 0.3s ease, border 0.3s ease;
143
  }
144
  .user-group summary {
145
+ padding: 15px;
146
+ font-weight: bold;
147
  cursor: pointer;
148
  color: var(--text-color);
149
  background: var(--metric-background);
150
  transition: background 0.2s ease;
 
 
 
 
151
  }
152
  .user-group summary:hover {
153
  background: var(--metric-hover);
154
  }
 
155
  .user-group summary::-webkit-details-marker {
156
+ color: var(--text-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
  .user-servers {
159
  display: grid;
160
+ grid-template-columns: repeat(2, 1fr);
161
  gap: 15px;
162
+ padding: 15px;
163
  }
164
  .server-card {
165
  background: var(--metric-background);
166
+ border-radius: 8px;
167
+ padding: 15px;
168
  border: 1px solid var(--metric-border);
169
  transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.3s ease, border 0.3s ease;
170
+ min-height: 150px;
171
  display: flex;
172
  flex-direction: column;
173
+ }
174
+ .server-card.not-logged-in {
175
+ min-height: 120px;
176
  }
177
  .server-card:hover {
178
+ transform: translateY(-2px);
179
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
180
  }
181
  .server-header {
182
  display: flex;
183
  justify-content: space-between;
184
  align-items: center;
185
+ margin-bottom: 10px;
186
+ font-size: 14px;
 
 
187
  }
188
  .server-name {
189
  display: flex;
190
  align-items: center;
191
+ gap: 8px;
192
  flex: 1;
193
  min-width: 0;
 
 
194
  }
195
  .server-name div {
196
  overflow: hidden;
 
202
  width: 20px;
203
  height: 20px;
204
  border-radius: 4px;
 
205
  flex-shrink: 0;
 
206
  }
207
  .metric-grid {
208
  display: grid;
209
+ grid-template-columns: repeat(5, 1fr);
210
  gap: 10px;
211
+ margin-top: 10px;
 
 
 
 
 
212
  }
213
  .metric-item {
214
  background: var(--card-background);
215
+ padding: 8px;
216
  border-radius: 6px;
217
  border: 1px solid var(--metric-border);
218
+ transition: background 0.3s ease;
219
  overflow: hidden;
220
  }
221
  .metric-item:hover {
 
224
  .metric-label {
225
  color: var(--label-color);
226
  font-size: 12px;
227
+ margin-bottom: 3px;
228
  white-space: nowrap;
 
229
  }
230
  .metric-value {
231
+ font-size: 14px;
232
+ font-weight: 500;
 
233
  overflow: hidden;
234
  text-overflow: ellipsis;
235
  white-space: nowrap;
236
  max-width: 100%;
237
  }
 
 
 
238
  .status-dot {
239
  display: inline-block;
240
  border-radius: 50%;
241
+ animation: pulse 2s infinite;
242
  width: 10px;
243
  height: 10px;
244
  flex-shrink: 0;
 
245
  }
246
+ .status-online {
247
+ background-color: #34c759;
248
+ color: #34c759;
 
249
  }
250
+ .status-offline {
251
+ background-color: #f44336;
252
+ color: #f44336;
253
  }
254
+ .status-sleep {
255
+ background-color: #ffa500;
256
+ color: #ffa500;
257
  animation: none;
258
  }
 
 
 
 
 
259
  .action-buttons {
260
  display: flex;
261
+ gap: 10px;
262
+ margin-top: 10px;
 
 
263
  }
264
  .action-button {
265
  background: var(--action-button-bg);
266
+ color: #fff;
267
  border: none;
268
+ padding: 6px 12px;
269
+ border-radius: 4px;
270
  cursor: pointer;
271
  font-size: 13px;
272
+ transition: background 0.2s ease;
 
 
 
273
  }
274
  .action-button:hover {
275
  background: var(--action-button-hover);
 
 
 
 
 
 
 
 
276
  }
277
  .network-stats {
278
  background: var(--network-background);
279
  border: 1px solid var(--metric-border);
280
+ margin-top: 10px;
281
  padding: 15px;
282
  border-radius: 8px;
283
+ display: flex;
284
+ justify-content: space-between;
285
  transition: background 0.3s ease, border 0.3s ease;
286
+ }
287
+ .network-item {
288
  font-size: 14px;
289
  color: var(--secondary-text);
 
 
 
290
  }
291
+ .network-item span {
292
  color: var(--text-color);
293
+ font-weight: 500;
 
294
  }
295
  @keyframes fadeIn {
296
  from { opacity: 0; transform: translateY(20px); }
297
  to { opacity: 1; transform: translateY(0); }
298
  }
299
+ @keyframes pulse {
300
+ 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
301
+ 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
302
+ 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
303
+ }
304
+ @media (max-width: 600px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  #summary {
306
+ grid-template-columns: 1fr;
307
+ gap: 16px;
308
  }
309
  #summary div {
310
  padding: 12px;
 
312
  #summary span {
313
  font-size: 20px;
314
  }
 
 
 
315
  .user-servers {
316
+ grid-template-columns: 1fr !important;
 
 
 
 
 
 
 
 
 
317
  }
318
  .metric-grid {
319
+ grid-template-columns: repeat(2, 1fr);
320
  gap: 8px;
321
  }
322
  .metric-item {
323
+ padding: 6px;
 
 
 
324
  }
325
  .metric-value {
326
+ font-size: 13px;
327
  }
328
+ .server-header {
329
+ flex-direction: row;
330
+ flex-wrap: wrap;
331
+ gap: 8px;
332
  }
333
+ .container {
334
+ padding: 0 10px;
335
+ }
336
+ .overview {
337
+ padding: 15px;
338
+ margin-bottom: 20px;
339
  }
340
  }
341
+ .login-overlay, .confirm-overlay, .loading-overlay {
342
+ position: fixed;
343
+ top: 0;
344
+ left: 0;
345
+ width: 100%;
346
+ height: 100%;
347
+ background: rgba(0, 0, 0, 0.6);
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ z-index: 1000;
352
+ display: none;
353
+ }
354
+ .login-box, .confirm-box {
355
+ background: var(--card-background);
356
+ padding: 30px;
357
+ border-radius: 10px;
358
+ border: 1px solid var(--card-border);
359
+ width: 300px;
360
+ text-align: center;
361
+ }
362
+ .login-box h2, .confirm-box h2 {
363
+ margin-bottom: 20px;
364
+ color: var(--text-color);
365
+ }
366
+ .login-box input {
367
+ width: 100%;
368
+ padding: 10px;
369
+ margin: 10px 0;
370
+ border: 1px solid var(--metric-border);
371
+ border-radius: 5px;
372
+ background: var(--metric-background);
373
+ color: var(--text-color);
374
+ }
375
+ .login-box button, .confirm-box button {
376
+ width: 48%;
377
+ padding: 10px;
378
+ background: var(--action-button-bg);
379
+ border: none;
380
+ border-radius: 5px;
381
+ color: #fff;
382
+ cursor: pointer;
383
+ transition: background 0.2s ease;
384
+ margin: 5px 1%;
385
+ }
386
+ .login-box button:hover, .confirm-box button:hover {
387
+ background: var(--action-button-hover);
388
+ }
389
+ .login-error {
390
+ color: #f44336;
391
+ margin-top: 10px;
392
+ font-size: 14px;
393
+ }
394
+ .login-button, .logout-button {
395
+ background: var(--action-button-bg);
396
+ border: none;
397
+ color: #fff;
398
+ padding: 6px 12px;
399
+ border-radius: 4px;
400
+ cursor: pointer;
401
+ font-size: 13px;
402
+ transition: background 0.2s ease;
403
+ }
404
+ .login-button:hover, .logout-button:hover {
405
+ background: var(--action-button-hover);
406
+ }
407
+ .header-container {
408
+ display: flex;
409
+ justify-content: space-between;
410
+ align-items: center;
411
+ margin-bottom: 15px;
412
+ }
413
+ .auth-buttons {
414
+ display: flex;
415
+ gap: 10px;
416
+ }
417
+ .loader {
418
+ border: 5px solid var(--card-background);
419
+ border-top: 5px solid #34c759;
420
+ border-radius: 50%;
421
+ width: 50px;
422
+ height: 50px;
423
+ animation: spin 1s linear infinite;
424
+ }
425
+ @keyframes spin {
426
+ 0% { transform: rotate(0deg); }
427
+ 100% { transform: rotate(360deg); }
428
+ }
429
  .filter-sort-panel {
430
  background: var(--card-background);
431
  border: 1px solid var(--card-border);
432
+ border-radius: 10px;
433
+ padding: 15px;
434
+ margin-bottom: 20px;
435
  display: flex;
436
  flex-wrap: wrap;
437
  gap: 15px;
438
  align-items: center;
439
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
440
  transition: background 0.3s ease, border 0.3s ease;
 
441
  }
442
  .filter-sort-group {
443
  display: flex;
 
446
  font-size: 14px;
447
  color: var(--text-color);
448
  min-width: 200px;
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
  .filter-sort-group label {
451
  white-space: nowrap;
452
  color: var(--secondary-text);
453
  font-weight: 500;
 
454
  }
455
  .filter-sort-group select {
456
  flex: 1;
457
  background: var(--metric-background);
458
  border: 1px solid var(--metric-border);
459
  color: var(--text-color);
460
+ padding: 8px 12px;
461
  border-radius: 6px;
462
  cursor: pointer;
463
  font-size: 14px;
464
+ transition: background 0.2s ease, border 0.2s ease;
 
465
  outline: none;
466
  appearance: none;
467
+ background-image: url("data:image/svg+xml;utf8,<svg fill='%23333' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
468
  background-repeat: no-repeat;
469
  background-position: right 10px center;
470
  padding-right: 36px;
471
  }
 
 
 
472
  .filter-sort-group select:hover {
473
  background-color: var(--metric-hover);
474
+ border-color: #d1d5db;
475
  }
476
  .filter-sort-group select:focus {
477
+ border-color: #34c759;
478
+ box-shadow: 0 0 0 2px rgba(52, 199, 89, 0.3);
479
  }
480
  .refresh-button {
481
+ background: var(--action-button-bg);
482
  border: none;
483
  color: #fff;
484
+ padding: 8px 16px;
485
  border-radius: 6px;
486
  cursor: pointer;
487
  font-size: 14px;
488
+ transition: background 0.2s ease, transform 0.2s ease;
 
489
  display: flex;
490
  align-items: center;
491
  gap: 8px;
492
  height: 40px;
 
 
 
 
 
493
  }
494
+ .refresh-button:hover {
495
+ background: var(--action-button-hover);
496
  transform: translateY(-1px);
497
  }
498
+ .refresh-button.loading .refresh-icon {
499
+ animation: spin 1s linear infinite;
 
 
 
 
500
  }
501
  .refresh-icon {
502
  width: 16px;
503
  height: 16px;
504
  fill: currentColor;
505
  }
506
+ @media (max-width: 600px) {
507
+ .filter-sort-group {
508
+ min-width: 100%;
509
+ }
510
+ .filter-sort-panel {
511
+ gap: 12px;
512
+ padding: 12px;
513
+ }
514
  }
515
  .chart-container {
516
  display: none;
517
  margin-top: 15px;
518
+ background: var(--card-background);
519
+ border: 1px solid var(--card-border);
520
  border-radius: 8px;
521
+ padding: 10px;
522
+ height: 300px;
523
  transition: background 0.3s ease, border 0.3s ease;
524
  }
 
 
 
 
 
 
 
 
 
 
 
 
525
  .chart-toggle-button {
526
+ background: var(--metric-background);
527
+ color: var(--text-color);
528
+ border: 1px solid var(--metric-border);
529
  padding: 6px 12px;
530
  border-radius: 4px;
531
  cursor: pointer;
532
+ font-size: 13px;
533
+ transition: background 0.2s ease;
534
  margin-left: auto;
535
  white-space: nowrap;
 
536
  }
537
  .chart-toggle-button:hover {
538
+ background: var(--metric-hover);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  }
540
+ .expanded .chart-container {
541
+ display: block;
 
 
 
542
  }
543
+ canvas {
544
+ width: 100% !important;
545
+ height: auto !important;
 
546
  }
547
+ @media (max-width: 600px) {
548
+ .chart-container {
549
+ height: 250px;
550
+ }
 
 
 
551
  }
552
  </style>
553
  </head>
 
555
  <div class="container">
556
  <div class="overview">
557
  <div class="header-container">
558
+ <div class="overview-title">
559
+ <!-- 使用图表图标,与截图一致 -->
560
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
561
+ <path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h2v10H7V7zm4 4h2v6h-2v-6zm4-4h2v10h-2V7z"/>
562
+ </svg>
563
+ 系统概览
564
+ </div>
565
  <div class="auth-buttons">
566
  <button class="login-button" id="loginButton" onclick="showLoginForm()">登录</button>
567
  <button class="logout-button" id="logoutButton" style="display: none;" onclick="logout()">登出</button>
568
  </div>
569
  </div>
570
  <div class="theme-toggle">
571
+ 主题:
572
  <button onclick="toggleTheme('system')" title="跟随系统">
573
  <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
574
  <path d="M3 5h18c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H3c-1.1 0-2-.9-2-2V7c0-1.1.9-2 2-2zm0 12h18V7H3v10zm2-8h2v2H5V9zm0 4h2v2H5v-2zm4-4h10v2H9V9zm0 4h10v2H9v-2z"/>
 
586
  </button>
587
  </div>
588
  <div id="summary">
589
+ <div>总用户数: <span id="totalUsers">0</span><div class="no-data" id="totalUsersNoData" style="display: none;">暂无数据</div></div>
590
+ <div>总实例数: <span id="totalServers">0</span><div class="no-data" id="totalServersNoData" style="display: none;">暂无数据</div></div>
591
+ <div>在线实例: <span id="onlineServers">0</span><div class="no-data" id="onlineServersNoData" style="display: none;">暂无数据</div></div>
592
+ <div>离线实例: <span id="offlineServers">0</span><div class="no-data" id="offlineServersNoData" style="display: none;">暂无数据</div></div>
593
+ <div>总上传: <span id="totalUpload">0 B/s</span><div class="no-data" id="totalUploadNoData" style="display: none;">暂无数据</div></div>
594
+ <div>总下载: <span id="totalDownload">0 B/s</span><div class="no-data" id="totalDownloadNoData" style="display: none;">暂无数据</div></div>
595
  </div>
596
  <div class="network-stats">
597
+ <div class="network-item">当前在线上传速度: <span id="currentUploadSpeed">0 B/s</span></div>
598
+ <div class="network-item">当前在线下行速度: <span id="currentDownloadSpeed">0 B/s</span></div>
599
+ <div class="network-item">最后更新: <span id="lastUpdated">未知</span></div>
600
  </div>
601
  </div>
602
  <div class="filter-sort-panel">
 
631
  刷新数据
632
  </button>
633
  </div>
634
+ <div id="servers" class="stats-container">
635
+ </div>
636
  </div>
637
  <div id="loginOverlay" class="login-overlay">
638
  <div class="login-box">
639
  <h2>登录</h2>
640
+ <input type="text" id="username" placeholder="用户名">
641
+ <input type="password" id="password" placeholder="密码">
642
+ <div style="display: flex; justify-content: center; gap: 10px; margin-top: 20px;">
643
  <button onclick="login()">登录</button>
644
  <button onclick="hideLoginForm()">取消</button>
645
  </div>
 
649
  <div id="confirmOverlay" class="confirm-overlay">
650
  <div class="confirm-box">
651
  <h2 id="confirmTitle">确认操作</h2>
652
+ <p id="confirmMessage" style="margin-bottom: 20px; color: var(--text-color);"></p>
653
+ <button onclick="confirmAction()">确认</button>
654
+ <button onclick="cancelAction()">取消</button>
 
 
655
  </div>
656
  </div>
657
  <div id="loadingOverlay" class="loading-overlay">
658
  <div class="loader"></div>
659
  </div>
660
  <script>
661
+ // 主题切换功能(保持原有逻辑)
 
 
 
 
 
 
 
 
662
  function setTheme(theme) {
 
 
 
 
 
 
663
  if (theme === 'system') {
664
  localStorage.removeItem('theme');
665
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
666
+ document.documentElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
 
667
  } else {
668
  localStorage.setItem('theme', theme);
669
+ document.documentElement.setAttribute('data-theme', theme);
 
670
  }
671
  }
672
  function toggleTheme(theme) {
673
  setTheme(theme);
674
  }
675
  function initTheme() {
 
676
  const savedTheme = localStorage.getItem('theme');
677
  if (savedTheme) {
678
  setTheme(savedTheme);
679
  } else {
680
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
681
+ setTheme(systemPrefersDark ? 'dark' : 'light');
682
  }
683
  }
684
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
 
688
  });
689
  initTheme();
690
 
691
+ // 全局变量
692
  let isLoggedIn = false;
693
+ let lastUpdatedTime = null;
 
 
 
694
 
695
+ // 加载状态控制函数
696
  function showLoading() {
697
+ document.getElementById('loadingOverlay').style.display = 'flex';
698
+ const refreshButton = document.getElementById('refreshButton');
699
+ refreshButton.classList.add('loading');
700
+ refreshButton.disabled = true;
701
  }
702
  function hideLoading() {
703
+ document.getElementById('loadingOverlay').style.display = 'none';
704
+ const refreshButton = document.getElementById('refreshButton');
705
+ refreshButton.classList.remove('loading');
706
+ refreshButton.disabled = false;
707
  }
708
 
709
+ // 登录状态管理(保持原有逻辑)
710
+ function checkLoginStatus() {
 
711
  const token = localStorage.getItem('authToken');
712
  const loginButton = document.getElementById('loginButton');
713
  const logoutButton = document.getElementById('logoutButton');
714
  if (token) {
 
715
  showLoading();
716
+ return fetch('/api/verify-token', {
717
+ method: 'POST',
718
+ headers: {
719
+ 'Content-Type': 'application/json',
720
+ },
721
+ body: JSON.stringify({ token })
722
+ })
723
+ .then(response => response.json())
724
+ .then(data => {
725
  hideLoading();
726
  if (data.success) {
 
727
  isLoggedIn = true;
728
  loginButton.style.display = 'none';
729
  logoutButton.style.display = 'block';
730
  updateActionButtons(true);
 
731
  } else {
 
732
  localStorage.removeItem('authToken');
733
  isLoggedIn = false;
734
  loginButton.style.display = 'block';
735
  logoutButton.style.display = 'none';
736
  updateActionButtons(false);
 
737
  }
738
+ return data.success;
739
+ })
740
+ .catch(error => {
741
  hideLoading();
 
742
  localStorage.removeItem('authToken');
743
  isLoggedIn = false;
744
  loginButton.style.display = 'block';
745
  logoutButton.style.display = 'none';
746
  updateActionButtons(false);
747
  return false;
748
+ });
749
  } else {
 
750
  isLoggedIn = false;
751
  loginButton.style.display = 'block';
752
  logoutButton.style.display = 'none';
753
  updateActionButtons(false);
754
+ return Promise.resolve(false);
755
  }
756
  }
757
 
758
  function showLoginForm() {
759
+ document.getElementById('loginOverlay').style.display = 'flex';
 
760
  document.getElementById('username').value = '';
761
  document.getElementById('password').value = '';
762
  document.getElementById('loginError').style.display = 'none';
763
  }
764
  function hideLoginForm() {
765
+ document.getElementById('loginOverlay').style.display = 'none';
 
766
  }
767
+ function login() {
 
768
  const username = document.getElementById('username').value;
769
  const password = document.getElementById('password').value;
770
  const loginError = document.getElementById('loginError');
 
 
 
 
 
771
  showLoading();
772
+ fetch('/api/login', {
773
+ method: 'POST',
774
+ headers: {
775
+ 'Content-Type': 'application/json',
776
+ },
777
+ body: JSON.stringify({ username, password })
778
+ })
779
+ .then(response => response.json())
780
+ .then(data => {
781
  hideLoading();
782
+ if (data.success) {
 
783
  localStorage.setItem('authToken', data.token);
784
  isLoggedIn = true;
785
  hideLoginForm();
 
788
  updateActionButtons(true);
789
  refreshData();
790
  } else {
791
+ loginError.textContent = data.message || '登录失败';
 
792
  loginError.style.display = 'block';
793
  }
794
+ })
795
+ .catch(error => {
796
  hideLoading();
797
+ loginError.textContent = '登录请求失败,请稍后重试';
 
798
  loginError.style.display = 'block';
799
+ });
800
  }
801
+ function logout() {
 
802
  const token = localStorage.getItem('authToken');
803
  if (token) {
804
  showLoading();
805
+ fetch('/api/logout', {
806
+ method: 'POST',
807
+ headers: {
808
+ 'Content-Type': 'application/json',
809
+ },
810
+ body: JSON.stringify({ token })
811
+ })
812
+ .then(response => response.json())
813
+ .then(data => {
814
  hideLoading();
815
  localStorage.removeItem('authToken');
816
  isLoggedIn = false;
 
818
  document.getElementById('logoutButton').style.display = 'none';
819
  updateActionButtons(false);
820
  refreshData();
821
+ })
822
+ .catch(error => {
823
+ hideLoading();
824
+ localStorage.removeItem('authToken');
825
+ isLoggedIn = false;
826
+ document.getElementById('loginButton').style.display = 'block';
827
+ document.getElementById('logoutButton').style.display = 'none';
828
+ updateActionButtons(false);
829
+ refreshData();
830
+ });
831
  } else {
 
832
  isLoggedIn = false;
833
  document.getElementById('loginButton').style.display = 'block';
834
  document.getElementById('logoutButton').style.display = 'none';
 
837
  }
838
  }
839
  function updateActionButtons(loggedIn) {
 
840
  isLoggedIn = loggedIn;
841
  const cards = document.querySelectorAll('.server-card');
842
  cards.forEach(card => {
 
844
  if (buttons) {
845
  buttons.style.display = loggedIn ? 'flex' : 'none';
846
  }
847
+ if (loggedIn) {
848
+ card.classList.remove('not-logged-in');
849
+ } else {
850
+ card.classList.add('not-logged-in');
851
+ }
852
+ const chartToggleButton = card.querySelector('.chart-toggle-button');
853
+ if (chartToggleButton) {
854
+ chartToggleButton.style.display = 'inline-block';
855
+ }
856
  });
857
  }
858
 
859
+ // 页面加载时初始化
860
+ window.onload = async function() {
861
+ await checkLoginStatus();
862
+ await initialize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
  };
864
 
865
+ // 二次确认弹窗逻辑(保持原有逻辑)
866
  let pendingAction = null;
867
  let pendingRepoId = null;
868
  function showConfirmDialog(action, repoId, title, message) {
 
 
 
 
 
869
  pendingAction = action;
870
  pendingRepoId = repoId;
871
  document.getElementById('confirmTitle').textContent = title;
872
  document.getElementById('confirmMessage').textContent = message;
873
+ document.getElementById('confirmOverlay').style.display = 'flex';
874
  }
875
  function confirmAction() {
876
  if (pendingAction === 'restart') {
 
883
  function cancelAction() {
884
  pendingAction = null;
885
  pendingRepoId = null;
886
+ document.getElementById('confirmOverlay').style.display = 'none';
887
  }
888
 
889
+ // 获取用户名列表
890
  async function getUsernames() {
 
891
  try {
892
+ showLoading();
893
  const token = localStorage.getItem('authToken');
894
  const headers = {};
895
  if (token) {
896
  headers['Authorization'] = `Bearer ${token}`;
 
897
  }
898
  const response = await fetch('/api/config', { headers });
 
 
 
 
899
  const config = await response.json();
900
+ hideLoading();
901
  const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
902
  document.getElementById('totalUsers').textContent = usernamesList.length;
903
+ document.getElementById('totalUsersNoData').style.display = usernamesList.length === 0 ? 'block' : 'none';
904
  const userFilter = document.getElementById('userFilter');
 
905
  userFilter.innerHTML = '<option value="all">全部用户</option>';
906
  usernamesList.forEach(username => {
907
  const option = document.createElement('option');
 
909
  option.textContent = username;
910
  userFilter.appendChild(option);
911
  });
 
 
 
 
 
912
  return usernamesList;
913
  } catch (error) {
914
+ hideLoading();
915
  document.getElementById('totalUsers').textContent = 0;
916
+ document.getElementById('totalUsersNoData').style.display = 'block';
 
917
  return [];
918
  }
919
  }
920
 
921
+ // 获取实例列表
922
  async function fetchInstances() {
 
923
  try {
924
+ showLoading();
925
  const token = localStorage.getItem('authToken');
926
  const headers = {};
927
  if (token) {
928
  headers['Authorization'] = `Bearer ${token}`;
 
 
 
929
  }
930
  const response = await fetch('/api/proxy/spaces', { headers });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  const instances = await response.json();
932
+ hideLoading();
933
  if (instances.length === 0) {
934
+ alert('未获取到实例数据,可能是网络问题或数据暂不可用。');
935
  }
936
  return instances;
937
  } catch (error) {
938
+ hideLoading();
939
+ alert('获取实例列表失败,请稍后重试。');
940
  return [];
941
  }
942
  }
943
 
944
+ // 监控数据流管理
945
  class MetricsStreamManager {
946
  constructor() {
947
  this.eventSource = null;
 
948
  }
949
  connect(subscribedInstances = []) {
950
+ if (this.eventSource) {
951
+ this.eventSource.close();
 
 
 
 
 
 
 
 
952
  }
953
  const instancesParam = subscribedInstances.join(',');
954
+ const token = localStorage.getItem('authToken');
955
+ const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token || '')}`;
 
956
  this.eventSource = new EventSource(url);
 
 
 
957
  this.eventSource.addEventListener("metric", (event) => {
958
  try {
959
  const data = JSON.parse(event.data);
960
  const { repoId, metrics } = data;
961
+ updateServerCard(metrics, repoId);
 
 
 
962
  } catch (error) {
963
+ console.error(`解析监控数据失败:`, error);
964
  }
965
  });
966
  this.eventSource.onerror = (error) => {
967
+ this.eventSource.close();
968
+ this.eventSource = null;
969
+ setTimeout(() => this.connect(subscribedInstances), 5000);
970
  };
 
971
  }
972
  disconnect() {
973
  if (this.eventSource) {
974
  this.eventSource.close();
975
  this.eventSource = null;
 
976
  }
 
977
  }
978
  }
 
979
  const metricsStreamManager = new MetricsStreamManager();
980
+ const instanceMap = new Map();
981
+ const serverStatus = new Map();
982
+ let allInstances = [];
983
+ const chartInstances = new Map();
984
 
985
+ // 初始化页面数据
986
  async function initialize() {
987
+ await getUsernames();
988
+ const instances = await fetchInstances();
989
+ allInstances = instances;
990
+ renderInstances(allInstances);
991
+ const runningInstances = instances
992
+ .filter(instance => instance.status.toLowerCase() === 'running')
993
+ .map(instance => instance.repo_id);
994
+ metricsStreamManager.connect(runningInstances);
995
+ updateSummary();
996
+ updateActionButtons(isLoggedIn);
997
+ lastUpdatedTime = new Date();
998
+ updateLastUpdatedTime();
 
 
 
 
 
 
 
 
 
 
 
 
 
999
  }
1000
 
1001
+ // 手动刷新数据
1002
  async function refreshData() {
 
1003
  showLoading();
1004
  metricsStreamManager.disconnect();
1005
+ chartInstances.forEach(chart => {
1006
+ if (chart) {
1007
+ chart.destroy();
1008
+ }
1009
+ });
1010
+ chartInstances.clear();
1011
  await initialize();
1012
+ applyFiltersAndSort();
1013
+ lastUpdatedTime = new Date();
1014
+ updateLastUpdatedTime();
1015
  }
1016
 
1017
+ // 渲染实例卡片
1018
+ function renderInstances(instances) {
1019
+ const serversContainer = document.getElementById('servers');
1020
+ serversContainer.innerHTML = '';
1021
+ const userGroups = {};
1022
+ instances.forEach(instance => {
1023
+ if (!userGroups[instance.owner]) {
1024
+ userGroups[instance.owner] = [];
 
 
 
 
 
1025
  }
1026
+ userGroups[instance.owner].push(instance);
1027
+ });
1028
+ Object.keys(userGroups).forEach(owner => {
1029
+ let userGroup = document.createElement('details');
1030
+ userGroup.className = 'user-group';
1031
+ userGroup.id = `user-${owner}`;
1032
+ userGroup.setAttribute('open', '');
1033
+ const summary = document.createElement('summary');
1034
+ summary.textContent = `用户: ${owner}`;
1035
+ userGroup.appendChild(summary);
1036
+ const userServers = document.createElement('div');
1037
+ userServers.className = 'user-servers';
1038
+ userGroup.appendChild(userServers);
1039
+ serversContainer.appendChild(userGroup);
1040
+ userGroups[owner].forEach(instance => {
1041
+ renderInstanceCard(instance, userServers);
1042
+ });
1043
+ });
1044
+ }
1045
+
1046
+ // 创建图表
1047
+ function createChart(instanceId) {
1048
+ const canvasId = `chart-${instanceId}`;
1049
+ const canvas = document.getElementById(canvasId);
1050
+ if (!canvas) return null;
1051
+ const gridColor = 'rgba(0, 0, 0, 0.1)';
1052
+ const textColor = '#333';
1053
+ const ctx = canvas.getContext('2d');
1054
+ const chart = new Chart(ctx, {
1055
+ type: 'line',
1056
+ data: {
1057
+ labels: Array(30).fill(''),
1058
+ datasets: [
1059
+ {
1060
+ label: 'CPU 使用率 (%)',
1061
+ data: [],
1062
+ borderColor: '#34c759',
1063
+ backgroundColor: 'rgba(52, 199, 89, 0.2)',
1064
+ tension: 0.4,
1065
+ fill: true,
1066
+ },
1067
+ {
1068
+ label: '内存使用率 (%)',
1069
+ data: [],
1070
+ borderColor: '#2196F3',
1071
+ backgroundColor: 'rgba(33, 150, 243, 0.2)',
1072
+ tension: 0.4,
1073
+ fill: true,
1074
+ },
1075
+ {
1076
+ label: '上传速度 (KB/s)',
1077
+ data: [],
1078
+ borderColor: '#F44336',
1079
+ backgroundColor: 'rgba(244, 67, 54, 0.2)',
1080
+ tension: 0.4,
1081
+ fill: true,
1082
+ },
1083
+ {
1084
+ label: '下载速度 (KB/s)',
1085
+ data: [],
1086
+ borderColor: '#FF9800',
1087
+ backgroundColor: 'rgba(255, 152, 0, 0.2)',
1088
+ tension: 0.4,
1089
+ fill: true,
1090
+ },
1091
+ ]
1092
+ },
1093
+ options: {
1094
+ responsive: true,
1095
+ maintainAspectRatio: false,
1096
+ plugins: {
1097
+ legend: {
1098
+ labels: {
1099
+ color: textColor,
1100
+ font: { size: 12 }
1101
+ }
1102
+ },
1103
+ tooltip: {
1104
+ mode: 'index',
1105
+ intersect: false,
1106
+ }
1107
+ },
1108
+ scales: {
1109
+ y: {
1110
+ beginAtZero: true,
1111
+ grid: { color: gridColor },
1112
+ ticks: {
1113
+ color: textColor,
1114
+ font: { size: 11 }
1115
+ }
1116
+ },
1117
+ x: {
1118
+ grid: { color: gridColor },
1119
+ ticks: {
1120
+ color: textColor,
1121
+ font: { size: 11 },
1122
+ maxRotation: 0,
1123
+ minRotation: 0,
1124
+ autoSkip: true,
1125
+ autoSkipPadding: 10
1126
+ }
1127
+ }
1128
+ },
1129
+ elements: {
1130
+ point: {
1131
+ radius: 0,
1132
+ hitRadius: 5
1133
+ }
1134
+ },
1135
+ animation: false
1136
+ }
1137
+ });
1138
+ chartInstances.set(instanceId, chart);
1139
+ return chart;
1140
+ }
1141
+
1142
+ // 更新图表数据
1143
+ function updateChart(instanceId, data) {
1144
+ let chart = chartInstances.get(instanceId);
1145
+ if (!chart) {
1146
+ chart = createChart(instanceId);
1147
+ if (!chart) return;
1148
  }
1149
+ const cpuData = chart.data.datasets[0].data;
1150
+ const memoryData = chart.data.datasets[1].data;
1151
+ const uploadData = chart.data.datasets[2].data;
1152
+ const downloadData = chart.data.datasets[3].data;
1153
+ cpuData.push(data.cpu_usage_pct);
1154
+ memoryData.push(((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2));
1155
+ uploadData.push((data.tx_bps / 1024).toFixed(2));
1156
+ downloadData.push((data.rx_bps / 1024).toFixed(2));
1157
+ if (cpuData.length > 30) {
1158
+ cpuData.shift();
1159
+ memoryData.shift();
1160
+ uploadData.shift();
1161
+ downloadData.shift();
1162
+ }
1163
+ chart.update();
1164
+ }
1165
+
1166
+ // 渲染单个实例卡片
1167
+ function renderInstanceCard(instance, container) {
1168
+ const instanceId = instance.repo_id;
1169
  instanceMap.set(instanceId, instance);
1170
  const cardId = `instance-${instanceId}`;
1171
  let card = document.getElementById(cardId);
 
1173
  card = document.createElement('div');
1174
  card.id = cardId;
1175
  card.className = 'server-card';
1176
+ if (!isLoggedIn) {
1177
+ card.classList.add('not-logged-in');
1178
+ }
1179
  const iconSvg = instance.private
1180
+ ? `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
1181
+ <path d="M18 8v-3c0-1.656-1.344-3-3-3h-6c-1.656 0-3 1.344-3 3v3h-3v14h18v-14h-3zm-10-1.5c0-.828.672-1.5 1.5-1.5h5c.828 0 1.5.672 1.5 1.5v2.5h-8v-2.5zm4 11.5c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/>
1182
+ </svg>`
1183
+ : `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
1184
+ <path d="M21 3H3C1.9 3 1 3.9 1 5v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 5H4V6h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2zm1 4H3c-1.1 0-2 .9-2 2v3c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2v-3c0-1.1-.9-2-2-2zm-1 5H4v-2h16v2z"/>
1185
+ </svg>`;
1186
  card.innerHTML = `
1187
  <div class="server-header">
1188
  <div class="server-name">
1189
+ <div class="status-dot status-sleep"></div>
1190
  ${iconSvg}
1191
+ <div>${instance.name}</div>
1192
  </div>
1193
  <div>
1194
  <button class="chart-toggle-button" onclick="toggleChart('${instanceId}')">查看图表</button>
 
1217
  </div>
1218
  </div>
1219
  <div class="action-buttons" style="display: ${isLoggedIn ? 'flex' : 'none'};">
1220
+ <button class="action-button view-button" onclick="viewInstance('${instance.url}')">查看</button>
1221
+ <button class="action-button" onclick="manageInstance('${instance.repo_id}')">管理</button>
1222
  <button class="action-button" onclick="showConfirmDialog('restart', '${instance.repo_id}', '确认重启', '您确定要重启实例 ${instance.name} (${instance.repo_id}) 吗?')">重启</button>
1223
  <button class="action-button" onclick="showConfirmDialog('rebuild', '${instance.repo_id}', '确认重建', '您确定要重建实例 ${instance.name} (${instance.repo_id}) 吗?')">重建</button>
1224
  </div>
 
1227
  </div>
1228
  `;
1229
  container.appendChild(card);
 
1230
  }
1231
  const statusDot = card.querySelector('.status-dot');
1232
+ const initialStatus = instance.status.toLowerCase();
1233
+ if (initialStatus === 'running') {
1234
+ statusDot.className = 'status-dot status-online';
1235
+ } else if (initialStatus === 'sleeping') {
1236
+ statusDot.className = 'status-dot status-sleep';
 
 
 
 
 
1237
  } else {
1238
+ statusDot.className = 'status-dot status-offline';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
  }
1240
+ serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
1241
  }
1242
 
1243
+ // 切换图表显示/隐藏
1244
+ function toggleChart(instanceId) {
1245
+ const card = document.getElementById(`instance-${instanceId}`);
1246
+ const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1247
+ const toggleButton = card.querySelector('.chart-toggle-button');
1248
+ if (!card || !chartContainer) return;
1249
+ if (card.classList.contains('expanded')) {
1250
+ card.classList.remove('expanded');
1251
+ toggleButton.textContent = '查看图表';
1252
+ } else {
1253
+ card.classList.add('expanded');
1254
+ toggleButton.textContent = '收起图表';
1255
+ if (!chartInstances.has(instanceId)) {
1256
+ createChart(instanceId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  }
1258
+ }
 
 
 
 
 
 
1259
  }
1260
 
1261
+ // 更新实例卡片数据
1262
+ function updateServerCard(data, instanceId, isSleep = false) {
1263
+ const cardId = `instance-${instanceId}`;
1264
+ let card = document.getElementById(cardId);
1265
  const instance = instanceMap.get(instanceId);
1266
+ if (!card && instance) {
 
1267
  return;
1268
  }
1269
+ if (card) {
1270
+ const statusDot = card.querySelector('.status-dot');
1271
+ let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
1272
+ let isOnline = false;
1273
+ if (data) {
1274
+ cpuUsage = `${data.cpu_usage_pct}%`;
1275
+ memoryUsage = `${((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2)}%`;
1276
+ upload = `${formatBytes(data.tx_bps)}/s`;
1277
+ download = `${formatBytes(data.rx_bps)}/s`;
1278
+ statusDot.className = 'status-dot status-online';
1279
+ isOnline = true;
1280
+ isSleep = false;
 
 
 
 
1281
  updateChart(instanceId, data);
1282
+ } else {
1283
+ const currentStatus = instance?.status.toLowerCase() || 'unknown';
1284
+ if (currentStatus === 'running') {
1285
+ statusDot.className = 'status-dot status-online';
1286
+ isOnline = true;
1287
+ isSleep = false;
1288
+ } else if (currentStatus === 'sleeping') {
1289
+ statusDot.className = 'status-dot status-sleep';
1290
+ isOnline = false;
1291
+ isSleep = true;
1292
+ } else {
1293
+ statusDot.className = 'status-dot status-offline';
1294
+ isOnline = false;
1295
+ isSleep = false;
1296
+ }
1297
  }
1298
+ card.querySelector('.cpu-usage').textContent = cpuUsage;
1299
+ card.querySelector('.memory-usage').textContent = memoryUsage;
1300
+ card.querySelector('.upload').textContent = upload;
1301
+ card.querySelector('.download').textContent = download;
1302
+ serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
1303
+ updateSummary();
 
 
 
 
 
 
 
 
 
 
 
1304
  }
 
 
 
 
 
 
 
 
1305
  }
1306
 
1307
+ // 重启实例
1308
  async function restartSpace(repoId) {
 
1309
  try {
1310
  const token = localStorage.getItem('authToken');
1311
  if (!token || !isLoggedIn) {
 
1317
  const encodedRepoId = encodeURIComponent(repoId);
1318
  const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, {
1319
  method: 'POST',
1320
+ headers: {
1321
+ 'Authorization': `Bearer ${token}`
1322
+ }
1323
  });
1324
  const result = await response.json();
1325
  hideLoading();
1326
  if (result.success) {
1327
+ alert(`重启成功: ${repoId}`);
1328
  refreshData();
1329
  } else {
1330
  if (response.status === 401) {
 
1337
  showLoginForm();
1338
  } else {
1339
  alert(`重启失败: ${result.error || '未知错误'}`);
 
1340
  }
1341
  }
1342
  } catch (error) {
1343
  hideLoading();
1344
+ alert(`重启失败: ${error.message}`);
 
1345
  }
1346
  }
1347
 
1348
+ // 重建实例
1349
  async function rebuildSpace(repoId) {
 
1350
  try {
1351
  const token = localStorage.getItem('authToken');
1352
  if (!token || !isLoggedIn) {
 
1358
  const encodedRepoId = encodeURIComponent(repoId);
1359
  const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, {
1360
  method: 'POST',
1361
+ headers: {
1362
+ 'Authorization': `Bearer ${token}`
1363
+ }
1364
  });
1365
  const result = await response.json();
1366
  hideLoading();
1367
  if (result.success) {
1368
+ alert(`重建成功: ${repoId}`);
1369
  refreshData();
1370
  } else {
1371
  if (response.status === 401) {
 
1378
  showLoginForm();
1379
  } else {
1380
  alert(`重建失败: ${result.error || '未知错误'}`);
 
1381
  }
1382
  }
1383
  } catch (error) {
1384
  hideLoading();
1385
+ alert(`重建失败: ${error.message}`);
 
1386
  }
1387
  }
1388
 
1389
+ // 更新概览数据
1390
  function updateSummary() {
 
1391
  let online = 0;
1392
  let offline = 0;
1393
+ let totalUpload = 0;
1394
+ let totalDownload = 0;
1395
  serverStatus.forEach((status, instanceId) => {
1396
+ const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
1397
+ if (isRecentlyOnline) {
1398
  online++;
1399
  if (status.data) {
1400
+ totalUpload += parseFloat(status.data.tx_bps) || 0;
1401
+ totalDownload += parseFloat(status.data.rx_bps) || 0;
1402
  }
1403
  } else {
1404
  offline++;
1405
  }
1406
  });
1407
+ document.getElementById('totalServers').textContent = serverStatus.size;
1408
+ document.getElementById('totalServersNoData').style.display = serverStatus.size === 0 ? 'block' : 'none';
1409
  document.getElementById('onlineServers').textContent = online;
1410
+ document.getElementById('onlineServersNoData').style.display = online === 0 ? 'block' : 'none';
1411
  document.getElementById('offlineServers').textContent = offline;
1412
+ document.getElementById('offlineServersNoData').style.display = offline === 0 ? 'block' : 'none';
1413
+ const uploadText = `${formatBytes(totalUpload)}/s`;
1414
+ const downloadText = `${formatBytes(totalDownload)}/s`;
1415
+ document.getElementById('totalUpload').textContent = uploadText;
1416
+ document.getElementById('totalUploadNoData').style.display = totalUpload === 0 ? 'block' : 'none';
1417
+ document.getElementById('totalDownload').textContent = downloadText;
1418
+ document.getElementById('totalDownloadNoData').style.display = totalDownload === 0 ? 'block' : 'none';
1419
+ document.getElementById('currentUploadSpeed').textContent = uploadText;
1420
+ document.getElementById('currentDownloadSpeed').textContent = downloadText;
1421
  }
1422
 
1423
+ // 格式化字节单位
1424
+ function formatBytes(bytes) {
1425
  if (bytes === 0) return '0 B';
1426
+ const k = 1024;
1427
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
1428
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1429
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1430
  }
1431
 
1432
+ // 更新最后更新时间
1433
+ function updateLastUpdatedTime() {
1434
+ if (lastUpdatedTime) {
1435
+ const now = new Date();
1436
+ const diff = Math.floor((now - lastUpdatedTime) / 1000);
1437
+ let timeText = '';
1438
+ if (diff < 60) {
1439
+ timeText = `${diff}秒前`;
1440
+ } else if (diff < 3600) {
1441
+ timeText = `${Math.floor(diff / 60)}分钟前`;
1442
+ } else {
1443
+ timeText = lastUpdatedTime.toLocaleString('zh-CN');
1444
+ }
1445
+ document.getElementById('lastUpdated').textContent = timeText;
1446
  }
1447
+ }
1448
+
1449
+ // 定时更新
1450
+ setInterval(updateSummary, 5000);
1451
+ setInterval(updateLastUpdatedTime, 1000);
1452
+ setInterval(async () => {
1453
+ metricsStreamManager.disconnect();
1454
+ await initialize();
1455
  }, 300000);
1456
 
1457
+ // 应用过滤和排序
1458
  function applyFiltersAndSort() {
 
1459
  const statusFilter = document.getElementById('statusFilter').value;
1460
  const userFilter = document.getElementById('userFilter').value;
1461
  const sortBy = document.getElementById('sortBy').value;
1462
+ let filteredInstances = allInstances;
1463
+ if (statusFilter !== 'all') {
1464
+ filteredInstances = filteredInstances.filter(instance => instance.status.toLowerCase() === statusFilter);
1465
+ }
1466
+ if (userFilter !== 'all') {
1467
+ filteredInstances = filteredInstances.filter(instance => instance.owner === userFilter);
1468
+ }
1469
  filteredInstances.sort((a, b) => {
1470
  if (sortBy === 'name-asc') {
1471
  return a.name.localeCompare(b.name);
1472
  } else if (sortBy === 'name-desc') {
1473
  return b.name.localeCompare(a.name);
1474
  } else if (sortBy === 'status-asc') {
1475
+ const statusOrder = { 'running': 0, 'sleeping': 1, 'stopped': 2 };
1476
+ return statusOrder[a.status.toLowerCase()] - statusOrder[b.status.toLowerCase()];
 
 
 
 
 
1477
  } else if (sortBy === 'status-desc') {
1478
+ const statusOrder = { 'running': 2, 'sleeping': 1, 'stopped': 0 };
1479
+ return statusOrder[a.status.toLowerCase()] - statusOrder[b.status.toLowerCase()];
 
 
 
 
 
1480
  }
1481
  return 0;
1482
  });
1483
+ instanceMap.clear();
1484
+ serverStatus.clear();
1485
+ metricsStreamManager.disconnect();
1486
+ chartInstances.forEach(chart => {
1487
+ if (chart) {
1488
+ chart.destroy();
1489
+ }
1490
+ });
1491
+ chartInstances.clear();
1492
  renderInstances(filteredInstances);
1493
+ const runningInstances = filteredInstances
1494
+ .filter(instance => instance.status.toLowerCase() === 'running')
1495
+ .map(instance => instance.repo_id);
1496
+ metricsStreamManager.connect(runningInstances);
1497
+ updateSummary();
1498
  updateActionButtons(isLoggedIn);
1499
  }
1500
 
1501
+ // 查看实例
1502
  function viewInstance(url) {
1503
+ window.open(url, '_blank');
 
 
 
 
 
1504
  }
1505
+
1506
+ // 管理实例
1507
  function manageInstance(repoId) {
 
1508
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;
1509
  window.open(manageUrl, '_blank');
1510
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1511
  </script>
1512
  </body>
1513
  </html>