nbugs commited on
Commit
636fbda
·
verified ·
1 Parent(s): 7f1eae8

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +1345 -474
public/index.html CHANGED
@@ -2,422 +2,897 @@
2
  <html lang="zh">
3
  <head>
4
  <meta charset="UTF-8">
 
5
  <title>HuggingFace 实例监控面板</title>
 
6
  <style>
 
7
  * {
8
  margin: 0;
9
  padding: 0;
10
  box-sizing: border-box;
11
  }
12
- body {
13
- font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
14
- background: var(--background-color);
15
- color: var(--text-color);
16
- padding: 20px;
17
- min-height: 100vh;
18
- transition: background 0.3s ease, color 0.3s ease;
19
- }
20
  :root {
21
  /* 深色模式变量(默认) */
22
- --background-color: #0a0a0a;
23
- --text-color: #fff;
24
- --card-background: #1a1a1a;
25
- --card-border: rgba(255, 255, 255, 0.1);
26
- --metric-background: #141414;
27
  --metric-border: rgba(255, 255, 255, 0.05);
28
- --metric-hover: #202020;
29
- --secondary-text: #888;
30
- --label-color: #666;
31
- --network-background: rgba(255, 255, 255, 0.07);
32
- --action-button-bg: #3a3a3a;
33
- --action-button-hover: #4a4a4a;
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  [data-theme="light"] {
36
  /* 浅色模式变量 */
37
- --background-color: #f5f5f5;
38
- --text-color: #333;
39
- --card-background: #fff;
40
- --card-border: rgba(0, 0, 0, 0.1);
41
- --metric-background: #f9f9f9;
42
  --metric-border: rgba(0, 0, 0, 0.05);
43
- --metric-hover: #eaeaea;
44
- --secondary-text: #666;
45
- --label-color: #999;
46
- --network-background: rgba(0, 0, 0, 0.07);
47
- --action-button-bg: #e0e0e0;
48
- --action-button-hover: #d0d0d0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
 
 
50
  .container {
51
  max-width: 1400px;
52
  margin: 0 auto;
53
  animation: fadeIn 0.5s ease;
54
- padding: 0 20px;
55
  }
56
- .overview {
 
 
 
 
 
 
 
57
  background: var(--card-background);
58
- border-radius: 15px;
59
- padding: 25px;
60
- margin-bottom: 30px;
61
- border: 1px solid var(--card-border);
62
- transition: background 0.3s ease, border 0.3s ease;
63
- }
64
- .overview-title {
65
- font-size: 18px;
 
 
 
 
 
66
  display: flex;
67
  align-items: center;
68
  gap: 10px;
69
- margin-bottom: 20px;
70
- color: var(--text-color);
71
  }
72
- .theme-toggle {
 
 
 
 
 
 
73
  display: flex;
74
  align-items: center;
75
- gap: 10px;
76
- margin-bottom: 20px;
77
- font-size: 14px;
78
- color: var(--secondary-text);
79
  }
80
- .theme-toggle button {
 
 
 
 
 
81
  background: var(--metric-background);
 
 
82
  border: 1px solid var(--metric-border);
83
- color: var(--text-color);
 
 
 
 
 
84
  padding: 6px;
85
- border-radius: 6px;
86
  cursor: pointer;
87
  display: flex;
88
  align-items: center;
89
  justify-content: center;
90
  width: 32px;
91
  height: 32px;
92
- transition: background 0.2s ease, transform 0.2s ease;
93
  }
 
94
  .theme-toggle button:hover {
95
- background: var(--metric-hover);
96
- transform: scale(1.05);
 
 
 
 
 
97
  }
98
- .theme-toggle svg {
99
- width: 18px;
100
- height: 18px;
101
- fill: var(--text-color);
102
  }
103
- #summary {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  display: grid;
105
- grid-template-columns: repeat(6, 1fr);
106
  gap: 15px;
107
  }
108
- #summary div {
 
109
  background: var(--metric-background);
110
- padding: 15px;
111
- border-radius: 8px;
112
  border: 1px solid var(--metric-border);
113
- transition: background 0.3s ease, border 0.3s ease;
 
 
 
114
  }
115
- #summary div {
116
- font-size: 14px;
 
 
 
 
 
 
 
117
  color: var(--secondary-text);
 
 
 
118
  }
119
- #summary span {
120
- display: block;
121
- font-size: 24px;
122
- font-weight: bold;
123
- margin-top: 5px;
124
- color: var(--text-color);
125
  }
126
- .stats-container {
127
- display: grid;
128
- grid-template-columns: 1fr;
129
- gap: 20px;
130
- margin-top: 20px;
131
  }
 
 
132
  .user-group {
133
  background: var(--card-background);
134
- border-radius: 10px;
135
  border: 1px solid var(--card-border);
136
  overflow: hidden;
137
- transition: background 0.3s ease, border 0.3s ease;
 
 
138
  }
139
- .user-group summary {
140
- padding: 15px;
141
- font-weight: bold;
 
 
 
 
 
142
  cursor: pointer;
143
  color: var(--text-color);
144
  background: var(--metric-background);
145
- transition: background 0.2s ease;
 
 
 
 
146
  }
147
- .user-group summary:hover {
 
148
  background: var(--metric-hover);
149
  }
150
- .user-group summary::-webkit-details-marker {
151
- color: var(--text-color);
 
152
  }
153
- .user-servers {
 
 
 
 
 
 
 
 
 
154
  display: grid;
155
- grid-template-columns: repeat(2, 1fr);
156
- gap: 15px;
157
- padding: 15px;
158
  }
 
159
  .server-card {
160
  background: var(--metric-background);
161
- border-radius: 8px;
162
- padding: 15px;
163
  border: 1px solid var(--metric-border);
164
- transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.3s ease, border 0.3s ease;
165
- height: auto;
 
 
166
  }
 
167
  .server-card:hover {
168
- transform: translateY(-2px);
169
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
 
170
  }
 
171
  .server-header {
172
  display: flex;
173
  justify-content: space-between;
174
  align-items: center;
175
- margin-bottom: 10px;
176
- font-size: 14px;
177
  }
 
178
  .server-name {
179
  display: flex;
180
  align-items: center;
181
  gap: 10px;
 
 
182
  }
183
- .server-flag {
184
- width: 20px;
185
- height: 20px;
186
- border-radius: 4px;
 
187
  }
 
 
188
  .metric-grid {
189
  display: grid;
190
- grid-template-columns: repeat(5, 1fr);
191
- gap: 10px;
192
- margin-top: 10px;
193
  }
 
194
  .metric-item {
195
  background: var(--card-background);
196
- padding: 8px;
197
- border-radius: 6px;
198
  border: 1px solid var(--metric-border);
199
- transition: background 0.3s ease;
200
  }
 
201
  .metric-item:hover {
202
  background: var(--metric-hover);
 
203
  }
 
204
  .metric-label {
205
  color: var(--label-color);
206
- font-size: 12px;
207
- margin-bottom: 3px;
 
 
 
 
 
 
 
208
  }
 
209
  .metric-value {
210
- font-size: 14px;
 
 
 
 
 
 
 
 
 
 
211
  font-weight: 500;
212
  }
 
213
  .status-dot {
214
  display: inline-block;
215
- border-radius: 50%;
216
- animation: pulse 2s infinite;
217
  width: 10px;
218
  height: 10px;
 
 
219
  }
 
220
  .status-online {
221
- background-color: #4CAF50;
222
- color: #4CAF50;
 
223
  }
 
224
  .status-offline {
225
- background-color: #f44336;
226
- color: #f44336;
227
  }
 
228
  .status-sleep {
229
- background-color: #ffa500;
230
- color: #ffa500;
231
- animation: none;
232
  }
 
 
233
  .action-buttons {
234
  display: flex;
235
  gap: 10px;
236
- margin-top: 10px;
 
 
237
  }
238
- .action-button {
 
 
 
 
 
239
  background: var(--action-button-bg);
240
  color: var(--text-color);
241
- border: none;
242
- padding: 6px 12px;
243
- border-radius: 4px;
244
  cursor: pointer;
245
- font-size: 13px;
246
- transition: background 0.2s ease;
 
 
247
  }
248
- .action-button:hover {
 
249
  background: var(--action-button-hover);
250
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  .network-stats {
252
  background: var(--network-background);
253
  border: 1px solid var(--metric-border);
254
- margin-top: 20px;
255
- padding: 15px;
256
- border-radius: 8px;
257
- transition: background 0.3s ease, border 0.3s ease;
258
- }
259
- .network-item {
260
- font-size: 14px;
261
  color: var(--secondary-text);
262
  }
263
- @keyframes fadeIn {
264
- from { opacity: 0; transform: translateY(20px); }
265
- to { opacity: 1; transform: translateY(0); }
266
  }
267
- @keyframes pulse {
268
- 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
269
- 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
270
- 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
 
 
271
  }
272
- @media (max-width: 768px) {
273
- #summary {
274
- grid-template-columns: 1fr;
275
- }
276
- .user-servers {
277
- grid-template-columns: 1fr;
278
- }
279
- .metric-grid {
280
- grid-template-columns: 1fr 1fr;
281
- }
282
  }
283
- .login-overlay, .confirm-overlay {
 
 
284
  position: fixed;
285
  top: 0;
286
  left: 0;
287
  width: 100%;
288
  height: 100%;
289
  background: rgba(0, 0, 0, 0.7);
 
290
  display: flex;
291
  align-items: center;
292
  justify-content: center;
293
  z-index: 1000;
 
 
 
294
  }
295
- .login-box, .confirm-box {
 
 
 
 
 
 
296
  background: var(--card-background);
297
  padding: 30px;
298
- border-radius: 10px;
299
  border: 1px solid var(--card-border);
300
- width: 300px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  text-align: center;
302
  }
303
- .login-box h2, .confirm-box h2 {
304
- margin-bottom: 20px;
 
 
305
  color: var(--text-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
- .login-box input {
 
 
 
 
 
 
 
 
308
  width: 100%;
309
- padding: 10px;
310
- margin: 10px 0;
311
  border: 1px solid var(--metric-border);
312
- border-radius: 5px;
313
  background: var(--metric-background);
314
  color: var(--text-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  }
316
- .login-box button, .confirm-box button {
317
- width: 48%;
318
- padding: 10px;
319
- background: var(--action-button-bg);
320
- border: none;
321
- border-radius: 5px;
322
- color: var(--text-color);
323
- cursor: pointer;
324
- transition: background 0.2s ease;
325
- margin: 5px 1%;
326
  }
327
- .login-box button:hover, .confirm-box button:hover {
328
- background: var(--action-button-hover);
 
 
 
 
 
 
 
329
  }
330
- .login-error {
331
- color: #f44336;
332
- margin-top: 10px;
333
- font-size: 14px;
334
  }
335
- .login-button, .logout-button {
336
- background: var(--action-button-bg);
337
- border: none;
338
- color: var(--text-color);
339
- padding: 6px 12px;
340
- border-radius: 4px;
341
- cursor: pointer;
342
- font-size: 13px;
343
- transition: background 0.2s ease;
344
  }
345
- .login-button:hover, .logout-button:hover {
346
- background: var(--action-button-hover);
 
 
 
347
  }
348
- .header-container {
349
- display: flex;
350
- justify-content: space-between;
351
- align-items: center;
352
- margin-bottom: 20px;
353
  }
354
- .auth-buttons {
355
- display: flex;
356
- gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
358
  </style>
359
  </head>
360
  <body>
361
- <div class="container">
362
- <div class="overview">
363
- <div class="header-container">
364
- <div class="overview-title">📊 系统概览</div>
365
- <div class="auth-buttons">
366
- <button class="login-button" id="loginButton" onclick="showLoginForm()">登录</button>
367
- <button class="logout-button" id="logoutButton" style="display: none;" onclick="logout()">登出</button>
368
- </div>
369
- </div>
370
  <div class="theme-toggle">
371
- 主题:
372
- <button onclick="toggleTheme('system')" title="跟随系统">
373
- <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
374
- <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"/>
375
- </svg>
376
  </button>
377
- <button onclick="toggleTheme('light')" title="浅色模式">
378
- <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
379
- <path d="M12 15.5a3.5 3.5 0 100-7 3.5 3.5 0 000 7zM12 2a.5.5 0 01.5.5v2a.5.5 0 01-1 0v-2A.5.5 0 0112 2zm0 17a.5.5 0 01.5.5v2a.5.5 0 01-1 0v-2a.5.5 0 01.5-.5zM2 12h2.5a.5.5 0 010 1H2a.5.5 0 010-1zm17.5 0h2a.5.5 0 010 1h-2a.5.5 0 010-1zM4.2 5.8l1.4-1.4a.5.5 0 01.7 0l1.4 1.4a.5.5 0 010 .7l-1.4 1.4a.5.5 0 01-.7 0L4.2 6.5a.5.5 0 010-.7zm13.2 0l1.4 1.4a.5.5 0 010 .7l1.4 1.4a.5.5 0 010 .7l-1.4 1.4a.5.5 0 01-.7 0l-1.4-1.4a.5.5 0 010-.7l-1.4-1.4a.5.5 0 010-.7l1.4-1.4a.5.5 0 01.7 0zM6.5 17.8l-1.4 1.4a.5.5 0 01-.7 0l-1.4-1.4a.5.5 0 010-.7l1.4-1.4a.5.5 0 01.7 0l1.4 1.4a.5.5 0 010 .7zm11 0l1.4 1.4a.5.5 0 010 .7l1.4 1.4a.5.5 0 010 .7l-1.4 1.4a.5.5 0 01-.7 0l-1.4-1.4a.5.5 0 010-.7l-1.4-1.4a.5.5 0 010-.7l1.4-1.4a.5.5 0 01.7 0z"/>
380
- </svg>
381
  </button>
382
- <button onclick="toggleTheme('dark')" title="深色模式">
383
- <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
384
- <path d="M21.4 13.7C20.6 13.9 19.8 14 19 14c-5 0-9-4-9-9 0-.2 0-.4.1-.6C5.6 5.6 2.5 9.5 2.2 14c-.1.9.1 1.8.5 2.6.7 1.5 2.1 2.5 3.8 2.8 1.9.2 3.8.3 5.6.3 2.6 0 5.1-.6 7.4-1.7 2-.9 3.7-2.4 4.9-4.2.3-.5.5-1.1.6-1.7 0 .6-.2 1.2-.6 1.6zM12.1 18.5c-1.7 0-3.5-.1-5.2-.4-1.5-.2-2.8-1.2-3.4-2.6-.3-.7-.5-1.6-.4-2.4.3-4.1 3-7.5 6.9-8.5-.1.4-.2.8-.2 1.2 0 4.5 3.3 8.2 7.5 8.9-1.9.9-3.9 1.4-6 1.4-.4 0-.8 0-1.2.4z"/>
385
- </svg>
386
  </button>
387
  </div>
388
- <div id="summary">
389
- <div>总用户数: <span id="totalUsers">0</span></div>
390
- <div>总实例数: <span id="totalServers">0</span></div>
391
- <div>在线实例: <span id="onlineServers">0</span></div>
392
- <div>离线实例: <span id="offlineServers">0</span></div>
393
- <div>总上传: <span id="totalUpload">0 B/s</span></div>
394
- <div>总下载: <span id="totalDownload">0 B/s</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  </div>
396
  </div>
397
- <div id="servers" class="stats-container">
 
 
 
 
 
 
 
398
  </div>
399
  </div>
400
- <div id="loginOverlay" class="login-overlay" style="display: none;">
401
- <div class="login-box">
402
- <h2>登录</h2>
403
- <input type="text" id="username" placeholder="用户名">
404
- <input type="password" id="password" placeholder="密码">
405
- <button onclick="login()">登录</button>
406
- <div id="loginError" class="login-error" style="display: none;"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  </div>
408
  </div>
409
- <div id="confirmOverlay" class="confirm-overlay" style="display: none;">
410
- <div class="confirm-box">
411
- <h2 id="confirmTitle">确认操作</h2>
412
- <p id="confirmMessage" style="margin-bottom: 20px; color: var(--text-color);"></p>
413
- <button onclick="confirmAction()">确认</button>
414
- <button onclick="cancelAction()">取消</button>
 
 
 
 
 
 
 
 
415
  </div>
416
  </div>
417
 
418
  <script>
419
  // 主题切换功能
420
  function setTheme(theme) {
 
 
 
 
 
 
 
 
421
  if (theme === 'system') {
422
  localStorage.removeItem('theme');
423
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -427,165 +902,213 @@
427
  document.documentElement.setAttribute('data-theme', theme);
428
  }
429
  }
430
- function toggleTheme(theme) {
431
- setTheme(theme);
432
- }
433
  // 初始化主题
434
  function initTheme() {
435
- const savedTheme = localStorage.getItem('theme');
436
- if (savedTheme) {
437
- setTheme(savedTheme);
438
- } else {
439
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
440
- setTheme(systemPrefersDark ? 'dark' : 'light');
 
 
441
  }
 
 
 
442
  }
 
443
  // 监听系统主题变化
444
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
445
- if (!localStorage.getItem('theme')) {
446
- setTheme('system');
 
447
  }
448
  });
 
 
 
 
 
 
 
449
  initTheme();
 
450
  // 全局变量,表示当前是否已登录
451
  let isLoggedIn = false;
 
452
  // 登录状态管理
453
- function checkLoginStatus() {
454
  const token = localStorage.getItem('authToken');
455
  const loginButton = document.getElementById('loginButton');
456
  const logoutButton = document.getElementById('logoutButton');
 
457
  // 先检查本地是否有 token,决定初始 UI 状态
458
  if (token) {
459
- console.log('本地存储中找到 token,尝试验证:', token.slice(0, 8) + '...');
460
- // 先假设未登录,直到验证成功
461
- loginButton.style.display = 'block';
462
- logoutButton.style.display = 'none';
463
- updateActionButtons(false);
464
- // 发送请求验证 token
465
- return fetch('/api/verify-token', {
466
- method: 'POST',
467
- headers: {
468
- 'Content-Type': 'application/json',
469
- },
470
- body: JSON.stringify({ token })
471
- })
472
- .then(response => response.json())
473
- .then(data => {
474
  if (data.success) {
475
  console.log('Token 验证成功,用户已登录');
476
  isLoggedIn = true;
477
  loginButton.style.display = 'none';
478
- logoutButton.style.display = 'block';
479
  updateActionButtons(true);
 
480
  } else {
481
- console.log('Token 验证失败,清除本地存储:', data.message);
482
  localStorage.removeItem('authToken');
483
- isLoggedIn = false;
484
- loginButton.style.display = 'block';
485
- logoutButton.style.display = 'none';
486
- updateActionButtons(false);
487
  }
488
- return data.success;
489
- })
490
- .catch(error => {
491
  console.error('验证 token 失败,清除本地存储:', error);
492
  localStorage.removeItem('authToken');
493
- isLoggedIn = false;
494
- loginButton.style.display = 'block';
495
- logoutButton.style.display = 'none';
496
- updateActionButtons(false);
497
- return false;
498
- });
499
- } else {
500
- console.log('本地存储中无 token,显示未登录状态');
501
- isLoggedIn = false;
502
- loginButton.style.display = 'block';
503
- logoutButton.style.display = 'none';
504
- updateActionButtons(false);
505
- return Promise.resolve(false);
506
  }
507
- }
 
 
 
 
 
 
 
 
 
508
  function showLoginForm() {
509
- document.getElementById('loginOverlay').style.display = 'flex';
510
  document.getElementById('username').value = '';
511
  document.getElementById('password').value = '';
512
  document.getElementById('loginError').style.display = 'none';
 
 
 
 
 
513
  }
 
 
514
  function hideLoginForm() {
515
- document.getElementById('loginOverlay').style.display = 'none';
516
  }
517
- function login() {
518
- const username = document.getElementById('username').value;
 
 
519
  const password = document.getElementById('password').value;
520
  const loginError = document.getElementById('loginError');
521
- fetch('/api/login', {
522
- method: 'POST',
523
- headers: {
524
- 'Content-Type': 'application/json',
525
- },
526
- body: JSON.stringify({ username, password })
527
- })
528
- .then(response => response.json())
529
- .then(data => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  if (data.success) {
531
  console.log('登录成功,保存 token');
532
  localStorage.setItem('authToken', data.token);
533
  isLoggedIn = true;
534
  hideLoginForm();
535
- // 手动更新 UI 状态
 
536
  document.getElementById('loginButton').style.display = 'none';
537
- document.getElementById('logoutButton').style.display = 'block';
538
  updateActionButtons(true);
 
 
 
539
  } else {
540
  console.log('登录失败:', data.message);
541
- loginError.textContent = data.message || '登录失败';
542
  loginError.style.display = 'block';
543
  }
544
- })
545
- .catch(error => {
546
  console.error('登录请求失败:', error);
547
- loginError.textContent = '登录请求失败,请稍后重试';
548
  loginError.style.display = 'block';
549
- });
550
  }
551
- function logout() {
 
 
552
  const token = localStorage.getItem('authToken');
 
553
  if (token) {
554
- fetch('/api/logout', {
555
- method: 'POST',
556
- headers: {
557
- 'Content-Type': 'application/json',
558
- },
559
- body: JSON.stringify({ token })
560
- })
561
- .then(response => response.json())
562
- .then(data => {
563
- console.log('登出成功,清除 token');
 
 
 
 
 
 
564
  localStorage.removeItem('authToken');
565
  isLoggedIn = false;
566
- document.getElementById('loginButton').style.display = 'block';
 
 
567
  document.getElementById('logoutButton').style.display = 'none';
568
  updateActionButtons(false);
569
- })
570
- .catch(error => {
571
- console.error('登出失败,但仍清除 token:', error);
 
 
 
 
572
  localStorage.removeItem('authToken');
573
  isLoggedIn = false;
574
- document.getElementById('loginButton').style.display = 'block';
575
  document.getElementById('logoutButton').style.display = 'none';
576
  updateActionButtons(false);
577
- });
578
- } else {
579
- console.log('本地无 token,直接设置为未登录');
580
- isLoggedIn = false;
581
- document.getElementById('loginButton').style.display = 'block';
582
- document.getElementById('logoutButton').style.display = 'none';
583
- updateActionButtons(false);
584
  }
585
  }
 
 
586
  function updateActionButtons(loggedIn) {
587
  console.log('更新操作按钮状态,是否已登录:', loggedIn);
588
  isLoggedIn = loggedIn;
 
589
  const cards = document.querySelectorAll('.server-card');
590
  cards.forEach(card => {
591
  const buttons = card.querySelector('.action-buttons');
@@ -594,36 +1117,92 @@
594
  }
595
  });
596
  }
597
- // 使用 window.onload 确保页面完全加载后检查登录状态
598
- window.onload = async function() {
599
- console.log('页面加载完成,开始检查登录状态');
600
- await checkLoginStatus();
601
- console.log('登录状态检查完成,初始化数据');
602
- initialize();
603
- };
604
  // 二次确认弹窗逻辑
605
  let pendingAction = null;
606
  let pendingRepoId = null;
 
607
  function showConfirmDialog(action, repoId, title, message) {
608
  pendingAction = action;
609
  pendingRepoId = repoId;
 
610
  document.getElementById('confirmTitle').textContent = title;
611
  document.getElementById('confirmMessage').textContent = message;
612
- document.getElementById('confirmOverlay').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
613
  }
614
- function confirmAction() {
 
 
 
 
615
  if (pendingAction === 'restart') {
616
- restartSpace(pendingRepoId);
617
  } else if (pendingAction === 'rebuild') {
618
- rebuildSpace(pendingRepoId);
619
  }
620
- cancelAction();
 
 
621
  }
 
622
  function cancelAction() {
623
  pendingAction = null;
624
  pendingRepoId = null;
625
- document.getElementById('confirmOverlay').style.display = 'none';
626
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  async function getUsernames() {
628
  try {
629
  const response = await fetch('/api/config');
@@ -632,11 +1211,13 @@
632
  document.getElementById('totalUsers').textContent = usernamesList.length;
633
  return usernamesList;
634
  } catch (error) {
635
- console.error('Failed to fetch usernames:', error);
636
  document.getElementById('totalUsers').textContent = 0;
637
  return [];
638
  }
639
  }
 
 
640
  async function fetchInstances() {
641
  try {
642
  const response = await fetch('/api/proxy/spaces');
@@ -644,23 +1225,30 @@
644
  return instances;
645
  } catch (error) {
646
  console.error("获取实例列表失败:", error);
 
647
  return [];
648
  }
649
  }
 
 
650
  class MetricsManager {
651
  constructor() {
652
  this.eventSources = new Map();
653
  this.instanceOwners = new Map();
654
  }
 
655
  async connect(instanceId, username) {
656
  if (this.eventSources.has(instanceId)) {
657
  return;
658
  }
 
659
  try {
660
  const eventSource = new EventSource(
661
  `/api/proxy/live-metrics/${username}/${instanceId.split('/')[1]}`
662
  );
 
663
  this.instanceOwners.set(instanceId, username);
 
664
  eventSource.addEventListener("metric", (event) => {
665
  try {
666
  const data = JSON.parse(event.data);
@@ -669,169 +1257,299 @@
669
  console.error(`解析数据失败 (${instanceId}):`, error);
670
  }
671
  });
 
672
  eventSource.onerror = (error) => {
 
673
  eventSource.close();
674
  this.eventSources.delete(instanceId);
675
  updateServerCard(null, instanceId, false);
676
  };
 
677
  this.eventSources.set(instanceId, eventSource);
678
  } catch (error) {
679
  console.error(`连接失败 (${username}/${instanceId}):`, error);
680
  updateServerCard(null, instanceId, false);
681
  }
682
  }
 
683
  disconnectAll() {
684
  this.eventSources.forEach(es => es.close());
685
  this.eventSources.clear();
686
  }
687
  }
 
688
  const metricsManager = new MetricsManager();
689
  const instanceMap = new Map();
690
  const serverStatus = new Map();
 
 
691
  async function initialize() {
692
- await getUsernames();
693
- const instances = await fetchInstances();
694
- instances.forEach(instance => {
695
- renderInstanceCard(instance);
696
- if (instance.status.toLowerCase() === 'running') {
697
- metricsManager.connect(instance.repo_id, instance.owner);
698
- }
699
- });
700
- updateSummary();
701
- // 初始化完成后再次确保按钮状态正确
702
- updateActionButtons(isLoggedIn);
703
- }
704
- function renderInstanceCard(instance) {
705
- const instanceId = instance.repo_id;
706
- instanceMap.set(instanceId, instance);
707
- let userGroup = document.getElementById(`user-${instance.owner}`);
708
- if (!userGroup) {
709
- userGroup = document.createElement('details');
710
- userGroup.className = 'user-group';
711
- userGroup.id = `user-${instance.owner}`;
712
- userGroup.setAttribute('open', '');
713
 
714
- const summary = document.createElement('summary');
715
- summary.textContent = `用户: ${instance.owner}`;
716
- userGroup.appendChild(summary);
717
 
718
- const userServers = document.createElement('div');
719
- userServers.className = 'user-servers';
720
- userGroup.appendChild(userServers);
 
 
 
 
 
721
 
722
- document.getElementById('servers').appendChild(userGroup);
723
- }
724
-
725
- const userServers = userGroup.querySelector('.user-servers');
726
- const cardId = `instance-${instanceId}`;
727
- let card = document.getElementById(cardId);
728
- if (!card) {
729
- card = document.createElement('div');
730
- card.id = cardId;
731
- card.className = 'server-card';
732
- card.innerHTML = `
733
- <div class="server-header">
734
- <div class="server-name">
735
- <div class="status-dot status-sleep"></div>
736
- <svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
737
- <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"/>
738
- </svg>
739
- <div>${instance.name} (${instance.repo_id})</div>
 
 
740
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  </div>
742
- <div class="metric-grid">
743
- <div class="metric-item">
744
- <div class="metric-label">状态</div>
745
- <div class="metric-value status">${instance.status}</div>
746
- </div>
747
- <div class="metric-item">
748
- <div class="metric-label">CPU</div>
749
- <div class="metric-value cpu-usage">N/A</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  </div>
751
- <div class="metric-item">
752
- <div class="metric-label">内存</div>
753
- <div class="metric-value memory-usage">N/A</div>
 
 
 
 
 
 
 
 
754
  </div>
755
- <div class="metric-item">
756
- <div class="metric-label">上传</div>
757
- <div class="metric-value upload">N/A</div>
 
 
758
  </div>
759
- <div class="metric-item">
760
- <div class="metric-label">下载</div>
761
- <div class="metric-value download">N/A</div>
 
 
762
  </div>
 
763
  </div>
764
- <div class="action-buttons" style="display: ${isLoggedIn ? 'flex' : 'none'};">
765
- <button class="action-button" onclick="showConfirmDialog('restart', '${instance.repo_id}', '确认重启', '您确定要重启实例 ${instance.name} (${instance.repo_id}) 吗?')">重启</button>
766
- <button class="action-button" onclick="showConfirmDialog('rebuild', '${instance.repo_id}', '确认重建', '您确定要重建实例 ${instance.name} (${instance.repo_id}) 吗?')">重建</button>
 
 
767
  </div>
768
- `;
769
- userServers.appendChild(card);
770
- }
771
- const statusDot = card.querySelector('.status-dot');
772
- const initialStatus = instance.status.toLowerCase();
773
- if (initialStatus === 'running') {
774
- statusDot.className = 'status-dot status-online';
775
- } else if (initialStatus === 'sleeping') {
776
- statusDot.className = 'status-dot status-sleep';
777
- } else {
778
- statusDot.className = 'status-dot status-offline';
779
- }
780
- serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
 
 
 
 
 
 
 
 
 
781
  }
 
 
782
  function updateServerCard(data, instanceId, isSleep = false) {
783
- const cardId = `instance-${instanceId}`;
784
- let card = document.getElementById(cardId);
785
  const instance = instanceMap.get(instanceId);
786
 
787
- if (!card && instance) {
788
- renderInstanceCard(instance);
789
- card = document.getElementById(cardId);
790
- }
791
- if (card) {
792
- const statusDot = card.querySelector('.status-dot');
793
- let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
794
- let isOnline = false;
795
- if (data) {
796
- cpuUsage = `${data.cpu_usage_pct}%`;
797
- memoryUsage = `${((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2)}%`;
798
- upload = `${formatBytes(data.tx_bps)}/s`;
799
- download = `${formatBytes(data.rx_bps)}/s`;
 
 
 
 
 
 
 
 
 
 
 
800
  statusDot.className = 'status-dot status-online';
 
801
  isOnline = true;
802
  isSleep = false;
 
 
 
 
 
803
  } else {
804
- const currentStatus = instance?.status.toLowerCase() || 'unknown';
805
- if (currentStatus === 'running') {
806
- statusDot.className = 'status-dot status-online';
807
- isOnline = true;
808
- isSleep = false;
809
- } else if (currentStatus === 'sleeping') {
810
- statusDot.className = 'status-dot status-sleep';
811
- isOnline = false;
812
- isSleep = true;
813
- } else {
814
- statusDot.className = 'status-dot status-offline';
815
- isOnline = false;
816
- isSleep = false;
817
- }
818
  }
819
- card.querySelector('.cpu-usage').textContent = cpuUsage;
820
- card.querySelector('.memory-usage').textContent = memoryUsage;
821
- card.querySelector('.upload').textContent = upload;
822
- card.querySelector('.download').textContent = download;
823
- serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
824
- updateSummary();
825
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  }
 
 
827
  async function restartSpace(repoId) {
828
  try {
829
  const token = localStorage.getItem('authToken');
830
  if (!token || !isLoggedIn) {
831
- alert('请先登录以执行此操作');
832
  showLoginForm();
833
  return;
834
  }
 
 
 
 
835
  const encodedRepoId = encodeURIComponent(repoId);
836
  const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, {
837
  method: 'POST',
@@ -839,36 +1557,56 @@
839
  'Authorization': `Bearer ${token}`
840
  }
841
  });
 
842
  const result = await response.json();
 
843
  if (result.success) {
844
- alert(`重启成功: ${repoId}`);
 
 
 
 
 
 
 
 
 
 
 
 
845
  } else {
846
  if (response.status === 401) {
847
- alert('登录已过期,请重新登录');
848
  localStorage.removeItem('authToken');
849
  isLoggedIn = false;
850
- document.getElementById('loginButton').style.display = 'block';
851
  document.getElementById('logoutButton').style.display = 'none';
852
  updateActionButtons(false);
853
  showLoginForm();
854
  } else {
855
- alert(`重启失败: ${result.error || '未知错误'}`);
856
  console.error(`重启失败 (${repoId}):`, result.error, result.details);
857
  }
858
  }
859
  } catch (error) {
860
  console.error(`重启失败 (${repoId}):`, error);
861
- alert(`重启失败: ${error.message}`);
862
  }
863
  }
 
 
864
  async function rebuildSpace(repoId) {
865
  try {
866
  const token = localStorage.getItem('authToken');
867
  if (!token || !isLoggedIn) {
868
- alert('请先登录以执行此操作');
869
  showLoginForm();
870
  return;
871
  }
 
 
 
 
872
  const encodedRepoId = encodeURIComponent(repoId);
873
  const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, {
874
  method: 'POST',
@@ -876,33 +1614,50 @@
876
  'Authorization': `Bearer ${token}`
877
  }
878
  });
 
879
  const result = await response.json();
 
880
  if (result.success) {
881
- alert(`重建成功: ${repoId}`);
 
 
 
 
 
 
 
 
 
 
 
 
882
  } else {
883
  if (response.status === 401) {
884
- alert('登录已过期,请重新登录');
885
  localStorage.removeItem('authToken');
886
  isLoggedIn = false;
887
- document.getElementById('loginButton').style.display = 'block';
888
  document.getElementById('logoutButton').style.display = 'none';
889
  updateActionButtons(false);
890
  showLoginForm();
891
  } else {
892
- alert(`重建失败: ${result.error || '未知错误'}`);
893
  console.error(`重建失败 (${repoId}):`, result.error, result.details);
894
  }
895
  }
896
  } catch (error) {
897
  console.error(`重建失败 (${repoId}):`, error);
898
- alert(`重建失败: ${error.message}`);
899
  }
900
  }
 
 
901
  function updateSummary() {
902
  let online = 0;
903
  let offline = 0;
904
  let totalUpload = 0;
905
  let totalDownload = 0;
 
906
  serverStatus.forEach((status, instanceId) => {
907
  const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
908
  if (isRecentlyOnline) {
@@ -915,12 +1670,15 @@
915
  offline++;
916
  }
917
  });
 
918
  document.getElementById('totalServers').textContent = serverStatus.size;
919
  document.getElementById('onlineServers').textContent = online;
920
  document.getElementById('offlineServers').textContent = offline;
921
  document.getElementById('totalUpload').textContent = `${formatBytes(totalUpload)}/s`;
922
  document.getElementById('totalDownload').textContent = `${formatBytes(totalDownload)}/s`;
923
  }
 
 
924
  function formatBytes(bytes) {
925
  if (bytes === 0) return '0 B';
926
  const k = 1024;
@@ -928,11 +1686,124 @@
928
  const i = Math.floor(Math.log(bytes) / Math.log(k));
929
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
930
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  setInterval(updateSummary, 5000);
 
 
932
  setInterval(async () => {
 
933
  metricsManager.disconnectAll();
934
  await initialize();
935
- }, 300000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  </script>
937
  </body>
938
  </html>
 
2
  <html lang="zh">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>HuggingFace 实例监控面板</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/fonts/remixicon.css">
8
  <style>
9
+ /* 基础样式重置与变量定义 */
10
  * {
11
  margin: 0;
12
  padding: 0;
13
  box-sizing: border-box;
14
  }
15
+
 
 
 
 
 
 
 
16
  :root {
17
  /* 深色模式变量(默认) */
18
+ --background-color: #0f1116;
19
+ --card-background: #1a1d25;
20
+ --card-border: rgba(255, 255, 255, 0.08);
21
+ --card-hover-border: rgba(255, 255, 255, 0.15);
22
+ --metric-background: #252836;
23
  --metric-border: rgba(255, 255, 255, 0.05);
24
+ --metric-hover: #2d3142;
25
+ --text-color: #f0f2f5;
26
+ --secondary-text: #a0a8b8;
27
+ --label-color: #7a8194;
28
+ --network-background: rgba(255, 255, 255, 0.05);
29
+ --action-button-bg: #2e3446;
30
+ --action-button-hover: #3a4156;
31
+ --action-button-active: #4a5166;
32
+ --primary-color: #5c6cff;
33
+ --primary-hover: #4a5aee;
34
+ --primary-active: #3a4add;
35
+ --danger-color: #ff5c5c;
36
+ --danger-hover: #ee4a4a;
37
+ --danger-active: #dd3a3a;
38
+ --success-color: #4caf50;
39
+ --warning-color: #ffa500;
40
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
41
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
42
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.4);
43
+ --border-radius-sm: 6px;
44
+ --border-radius-md: 10px;
45
+ --border-radius-lg: 15px;
46
+ --transition-fast: 0.15s ease;
47
+ --transition-normal: 0.25s ease;
48
+ --transition-slow: 0.35s ease;
49
+ --font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
50
+ --header-height: 70px;
51
+ }
52
+
53
  [data-theme="light"] {
54
  /* 浅色模式变量 */
55
+ --background-color: #f5f7fa;
56
+ --card-background: #ffffff;
57
+ --card-border: rgba(0, 0, 0, 0.08);
58
+ --card-hover-border: rgba(0, 0, 0, 0.15);
59
+ --metric-background: #f0f2f5;
60
  --metric-border: rgba(0, 0, 0, 0.05);
61
+ --metric-hover: #e6e8ec;
62
+ --text-color: #1a1d25;
63
+ --secondary-text: #5a6474;
64
+ --label-color: #7a8194;
65
+ --network-background: rgba(0, 0, 0, 0.03);
66
+ --action-button-bg: #e6e8ec;
67
+ --action-button-hover: #d6d8dc;
68
+ --action-button-active: #c6c8cc;
69
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.05);
70
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
71
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
72
+ }
73
+
74
+ /* 基础样式 */
75
+ body {
76
+ font-family: var(--font-family);
77
+ background: var(--background-color);
78
+ color: var(--text-color);
79
+ min-height: 100vh;
80
+ transition: background var(--transition-normal), color var(--transition-normal);
81
+ line-height: 1.5;
82
+ padding-top: var(--header-height);
83
  }
84
+
85
+ /* 布局容器 */
86
  .container {
87
  max-width: 1400px;
88
  margin: 0 auto;
89
  animation: fadeIn 0.5s ease;
90
+ padding: 20px;
91
  }
92
+
93
+ /* 顶部导航栏 */
94
+ .header {
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ height: var(--header-height);
100
  background: var(--card-background);
101
+ border-bottom: 1px solid var(--card-border);
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ padding: 0 20px;
106
+ z-index: 100;
107
+ transition: background var(--transition-normal), border var(--transition-normal);
108
+ box-shadow: var(--shadow-sm);
109
+ }
110
+
111
+ .header-title {
112
+ font-size: 1.25rem;
113
+ font-weight: 600;
114
  display: flex;
115
  align-items: center;
116
  gap: 10px;
 
 
117
  }
118
+
119
+ .header-title i {
120
+ font-size: 1.5rem;
121
+ color: var(--primary-color);
122
+ }
123
+
124
+ .header-actions {
125
  display: flex;
126
  align-items: center;
127
+ gap: 15px;
 
 
 
128
  }
129
+
130
+ /* 主题切换 */
131
+ .theme-toggle {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 8px;
135
  background: var(--metric-background);
136
+ border-radius: var(--border-radius-sm);
137
+ padding: 4px;
138
  border: 1px solid var(--metric-border);
139
+ }
140
+
141
+ .theme-toggle button {
142
+ background: transparent;
143
+ border: none;
144
+ color: var(--secondary-text);
145
  padding: 6px;
146
+ border-radius: 4px;
147
  cursor: pointer;
148
  display: flex;
149
  align-items: center;
150
  justify-content: center;
151
  width: 32px;
152
  height: 32px;
153
+ transition: background var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
154
  }
155
+
156
  .theme-toggle button:hover {
157
+ color: var(--text-color);
158
+ background: rgba(128, 128, 128, 0.1);
159
+ }
160
+
161
+ .theme-toggle button.active {
162
+ background: var(--primary-color);
163
+ color: white;
164
  }
165
+
166
+ .theme-toggle button:active {
167
+ transform: scale(0.95);
 
168
  }
169
+
170
+ .theme-toggle i {
171
+ font-size: 1.25rem;
172
+ }
173
+
174
+ /* 卡片样式 */
175
+ .card {
176
+ background: var(--card-background);
177
+ border-radius: var(--border-radius-md);
178
+ padding: 25px;
179
+ margin-bottom: 25px;
180
+ border: 1px solid var(--card-border);
181
+ transition: background var(--transition-normal), border var(--transition-normal), box-shadow var(--transition-normal);
182
+ box-shadow: var(--shadow-sm);
183
+ }
184
+
185
+ .card:hover {
186
+ border-color: var(--card-hover-border);
187
+ box-shadow: var(--shadow-md);
188
+ }
189
+
190
+ .card-title {
191
+ font-size: 1.125rem;
192
+ font-weight: 600;
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 10px;
196
+ margin-bottom: 20px;
197
+ color: var(--text-color);
198
+ }
199
+
200
+ /* 概览统计 */
201
+ .stats-grid {
202
  display: grid;
203
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
204
  gap: 15px;
205
  }
206
+
207
+ .stat-item {
208
  background: var(--metric-background);
209
+ padding: 20px;
210
+ border-radius: var(--border-radius-sm);
211
  border: 1px solid var(--metric-border);
212
+ transition: all var(--transition-normal);
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 8px;
216
  }
217
+
218
+ .stat-item:hover {
219
+ background: var(--metric-hover);
220
+ transform: translateY(-2px);
221
+ box-shadow: var(--shadow-sm);
222
+ }
223
+
224
+ .stat-label {
225
+ font-size: 0.875rem;
226
  color: var(--secondary-text);
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 6px;
230
  }
231
+
232
+ .stat-label i {
233
+ font-size: 1rem;
234
+ opacity: 0.8;
 
 
235
  }
236
+
237
+ .stat-value {
238
+ font-size: 1.5rem;
239
+ font-weight: 600;
240
+ color: var(--text-color);
241
  }
242
+
243
+ /* 用户组和服务器卡片 */
244
  .user-group {
245
  background: var(--card-background);
246
+ border-radius: var(--border-radius-md);
247
  border: 1px solid var(--card-border);
248
  overflow: hidden;
249
+ transition: all var(--transition-normal);
250
+ margin-bottom: 20px;
251
+ box-shadow: var(--shadow-sm);
252
  }
253
+
254
+ .user-group:hover {
255
+ box-shadow: var(--shadow-md);
256
+ }
257
+
258
+ .user-group-header {
259
+ padding: 15px 20px;
260
+ font-weight: 600;
261
  cursor: pointer;
262
  color: var(--text-color);
263
  background: var(--metric-background);
264
+ transition: background var(--transition-fast);
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: space-between;
268
+ user-select: none;
269
  }
270
+
271
+ .user-group-header:hover {
272
  background: var(--metric-hover);
273
  }
274
+
275
+ .user-group-header .toggle-icon {
276
+ transition: transform var(--transition-normal);
277
  }
278
+
279
+ .user-group[open] .user-group-header .toggle-icon {
280
+ transform: rotate(180deg);
281
+ }
282
+
283
+ .user-group-content {
284
+ padding: 20px;
285
+ }
286
+
287
+ .server-grid {
288
  display: grid;
289
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
290
+ gap: 20px;
 
291
  }
292
+
293
  .server-card {
294
  background: var(--metric-background);
295
+ border-radius: var(--border-radius-sm);
296
+ padding: 20px;
297
  border: 1px solid var(--metric-border);
298
+ transition: all var(--transition-normal);
299
+ height: 100%;
300
+ display: flex;
301
+ flex-direction: column;
302
  }
303
+
304
  .server-card:hover {
305
+ transform: translateY(-3px);
306
+ box-shadow: var(--shadow-md);
307
+ border-color: var(--card-hover-border);
308
  }
309
+
310
  .server-header {
311
  display: flex;
312
  justify-content: space-between;
313
  align-items: center;
314
+ margin-bottom: 15px;
315
+ font-size: 0.875rem;
316
  }
317
+
318
  .server-name {
319
  display: flex;
320
  align-items: center;
321
  gap: 10px;
322
+ font-weight: 500;
323
+ color: var(--text-color);
324
  }
325
+
326
+ .server-id {
327
+ font-size: 0.75rem;
328
+ color: var(--secondary-text);
329
+ margin-top: 2px;
330
  }
331
+
332
+ /* 指标网格 */
333
  .metric-grid {
334
  display: grid;
335
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
336
+ gap: 12px;
337
+ margin-top: 15px;
338
  }
339
+
340
  .metric-item {
341
  background: var(--card-background);
342
+ padding: 12px;
343
+ border-radius: var(--border-radius-sm);
344
  border: 1px solid var(--metric-border);
345
+ transition: all var(--transition-normal);
346
  }
347
+
348
  .metric-item:hover {
349
  background: var(--metric-hover);
350
+ transform: translateY(-2px);
351
  }
352
+
353
  .metric-label {
354
  color: var(--label-color);
355
+ font-size: 0.75rem;
356
+ margin-bottom: 5px;
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 4px;
360
+ }
361
+
362
+ .metric-label i {
363
+ font-size: 0.875rem;
364
  }
365
+
366
  .metric-value {
367
+ font-size: 0.9375rem;
368
+ font-weight: 500;
369
+ color: var(--text-color);
370
+ }
371
+
372
+ /* 状态指示器 */
373
+ .status-indicator {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 6px;
377
+ font-size: 0.8125rem;
378
  font-weight: 500;
379
  }
380
+
381
  .status-dot {
382
  display: inline-block;
 
 
383
  width: 10px;
384
  height: 10px;
385
+ border-radius: 50%;
386
+ flex-shrink: 0;
387
  }
388
+
389
  .status-online {
390
+ background-color: var(--success-color);
391
+ box-shadow: 0 0 0 rgba(76, 175, 80, 0.4);
392
+ animation: pulse 2s infinite;
393
  }
394
+
395
  .status-offline {
396
+ background-color: var(--danger-color);
 
397
  }
398
+
399
  .status-sleep {
400
+ background-color: var(--warning-color);
 
 
401
  }
402
+
403
+ /* 操作按钮 */
404
  .action-buttons {
405
  display: flex;
406
  gap: 10px;
407
+ margin-top: 20px;
408
+ margin-top: auto;
409
+ padding-top: 15px;
410
  }
411
+
412
+ .btn {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ gap: 6px;
417
  background: var(--action-button-bg);
418
  color: var(--text-color);
419
+ border: 1px solid var(--metric-border);
420
+ padding: 8px 16px;
421
+ border-radius: var(--border-radius-sm);
422
  cursor: pointer;
423
+ font-size: 0.875rem;
424
+ font-weight: 500;
425
+ transition: all var(--transition-fast);
426
+ white-space: nowrap;
427
  }
428
+
429
+ .btn:hover {
430
  background: var(--action-button-hover);
431
+ transform: translateY(-1px);
432
+ }
433
+
434
+ .btn:active {
435
+ background: var(--action-button-active);
436
+ transform: translateY(0);
437
+ }
438
+
439
+ .btn i {
440
+ font-size: 1rem;
441
+ }
442
+
443
+ .btn-primary {
444
+ background: var(--primary-color);
445
+ color: white;
446
+ border-color: var(--primary-color);
447
+ }
448
+
449
+ .btn-primary:hover {
450
+ background: var(--primary-hover);
451
+ border-color: var(--primary-hover);
452
+ }
453
+
454
+ .btn-primary:active {
455
+ background: var(--primary-active);
456
+ border-color: var(--primary-active);
457
+ }
458
+
459
+ .btn-danger {
460
+ background: var(--danger-color);
461
+ color: white;
462
+ border-color: var(--danger-color);
463
+ }
464
+
465
+ .btn-danger:hover {
466
+ background: var(--danger-hover);
467
+ border-color: var(--danger-hover);
468
+ }
469
+
470
+ .btn-danger:active {
471
+ background: var(--danger-active);
472
+ border-color: var(--danger-active);
473
+ }
474
+
475
+ /* 网络统计 */
476
  .network-stats {
477
  background: var(--network-background);
478
  border: 1px solid var(--metric-border);
479
+ margin-top: 15px;
480
+ padding: 12px;
481
+ border-radius: var(--border-radius-sm);
482
+ transition: all var(--transition-normal);
483
+ font-size: 0.8125rem;
 
 
484
  color: var(--secondary-text);
485
  }
486
+
487
+ .network-stats:hover {
488
+ border-color: var(--card-hover-border);
489
  }
490
+
491
+ .network-item {
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 6px;
495
+ margin-bottom: 4px;
496
  }
497
+
498
+ .network-item:last-child {
499
+ margin-bottom: 0;
500
+ }
501
+
502
+ .network-item i {
503
+ font-size: 0.9375rem;
504
+ opacity: 0.8;
 
 
505
  }
506
+
507
+ /* 登录和确认对话框 */
508
+ .overlay {
509
  position: fixed;
510
  top: 0;
511
  left: 0;
512
  width: 100%;
513
  height: 100%;
514
  background: rgba(0, 0, 0, 0.7);
515
+ backdrop-filter: blur(5px);
516
  display: flex;
517
  align-items: center;
518
  justify-content: center;
519
  z-index: 1000;
520
+ opacity: 0;
521
+ visibility: hidden;
522
+ transition: opacity var(--transition-normal), visibility var(--transition-normal);
523
  }
524
+
525
+ .overlay.active {
526
+ opacity: 1;
527
+ visibility: visible;
528
+ }
529
+
530
+ .dialog {
531
  background: var(--card-background);
532
  padding: 30px;
533
+ border-radius: var(--border-radius-md);
534
  border: 1px solid var(--card-border);
535
+ width: 350px;
536
+ max-width: 90%;
537
+ box-shadow: var(--shadow-lg);
538
+ transform: translateY(20px);
539
+ opacity: 0;
540
+ transition: transform var(--transition-normal), opacity var(--transition-normal);
541
+ }
542
+
543
+ .overlay.active .dialog {
544
+ transform: translateY(0);
545
+ opacity: 1;
546
+ }
547
+
548
+ .dialog-header {
549
+ margin-bottom: 20px;
550
  text-align: center;
551
  }
552
+
553
+ .dialog-title {
554
+ font-size: 1.25rem;
555
+ font-weight: 600;
556
  color: var(--text-color);
557
+ margin-bottom: 5px;
558
+ }
559
+
560
+ .dialog-subtitle {
561
+ font-size: 0.875rem;
562
+ color: var(--secondary-text);
563
+ }
564
+
565
+ .dialog-body {
566
+ margin-bottom: 25px;
567
+ }
568
+
569
+ .form-group {
570
+ margin-bottom: 15px;
571
  }
572
+
573
+ .form-group label {
574
+ display: block;
575
+ margin-bottom: 6px;
576
+ font-size: 0.875rem;
577
+ color: var(--secondary-text);
578
+ }
579
+
580
+ .form-control {
581
  width: 100%;
582
+ padding: 10px 12px;
 
583
  border: 1px solid var(--metric-border);
584
+ border-radius: var(--border-radius-sm);
585
  background: var(--metric-background);
586
  color: var(--text-color);
587
+ font-size: 0.9375rem;
588
+ transition: all var(--transition-fast);
589
+ }
590
+
591
+ .form-control:focus {
592
+ outline: none;
593
+ border-color: var(--primary-color);
594
+ box-shadow: 0 0 0 3px rgba(92, 108, 255, 0.2);
595
+ }
596
+
597
+ .form-error {
598
+ color: var(--danger-color);
599
+ font-size: 0.8125rem;
600
+ margin-top: 6px;
601
+ display: none;
602
+ }
603
+
604
+ .dialog-footer {
605
+ display: flex;
606
+ gap: 10px;
607
+ justify-content: flex-end;
608
  }
609
+
610
+ /* 加载状态 */
611
+ .loading-container {
612
+ display: flex;
613
+ flex-direction: column;
614
+ align-items: center;
615
+ justify-content: center;
616
+ padding: 50px 0;
617
+ color: var(--secondary-text);
 
618
  }
619
+
620
+ .loading-spinner {
621
+ width: 40px;
622
+ height: 40px;
623
+ border: 3px solid rgba(92, 108, 255, 0.2);
624
+ border-radius: 50%;
625
+ border-top-color: var(--primary-color);
626
+ animation: spin 1s linear infinite;
627
+ margin-bottom: 15px;
628
  }
629
+
630
+ .loading-text {
631
+ font-size: 0.9375rem;
 
632
  }
633
+
634
+ /* 动画 */
635
+ @keyframes fadeIn {
636
+ from { opacity: 0; transform: translateY(20px); }
637
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
638
  }
639
+
640
+ @keyframes pulse {
641
+ 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
642
+ 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
643
+ 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
644
  }
645
+
646
+ @keyframes spin {
647
+ 0% { transform: rotate(0deg); }
648
+ 100% { transform: rotate(360deg); }
 
649
  }
650
+
651
+ /* 响应式设计 */
652
+ @media (max-width: 1200px) {
653
+ .stats-grid {
654
+ grid-template-columns: repeat(3, 1fr);
655
+ }
656
+
657
+ .server-grid {
658
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
659
+ }
660
+ }
661
+
662
+ @media (max-width: 992px) {
663
+ .stats-grid {
664
+ grid-template-columns: repeat(2, 1fr);
665
+ }
666
+
667
+ .server-grid {
668
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
669
+ }
670
+
671
+ .card {
672
+ padding: 20px;
673
+ }
674
+ }
675
+
676
+ @media (max-width: 768px) {
677
+ .header {
678
+ padding: 0 15px;
679
+ }
680
+
681
+ .header-title span {
682
+ display: none;
683
+ }
684
+
685
+ .container {
686
+ padding: 15px;
687
+ }
688
+
689
+ .stats-grid {
690
+ grid-template-columns: 1fr 1fr;
691
+ gap: 12px;
692
+ }
693
+
694
+ .server-grid {
695
+ grid-template-columns: 1fr;
696
+ }
697
+
698
+ .metric-grid {
699
+ grid-template-columns: repeat(2, 1fr);
700
+ }
701
+
702
+ .card {
703
+ padding: 15px;
704
+ margin-bottom: 15px;
705
+ }
706
+
707
+ .card-title {
708
+ font-size: 1rem;
709
+ margin-bottom: 15px;
710
+ }
711
+
712
+ .stat-value {
713
+ font-size: 1.25rem;
714
+ }
715
+ }
716
+
717
+ @media (max-width: 576px) {
718
+ .header-actions {
719
+ gap: 8px;
720
+ }
721
+
722
+ .theme-toggle {
723
+ display: none;
724
+ }
725
+
726
+ .stats-grid {
727
+ grid-template-columns: 1fr;
728
+ }
729
+
730
+ .stat-item {
731
+ padding: 15px;
732
+ }
733
+
734
+ .user-group-header {
735
+ padding: 12px 15px;
736
+ }
737
+
738
+ .user-group-content {
739
+ padding: 15px;
740
+ }
741
+
742
+ .server-card {
743
+ padding: 15px;
744
+ }
745
+
746
+ .action-buttons {
747
+ flex-wrap: wrap;
748
+ }
749
+
750
+ .btn {
751
+ flex: 1;
752
+ min-width: 0;
753
+ padding: 8px 10px;
754
+ }
755
  }
756
  </style>
757
  </head>
758
  <body>
759
+ <!-- 顶部导航栏 -->
760
+ <header class="header">
761
+ <div class="header-title">
762
+ <i class="ri-dashboard-3-line"></i>
763
+ <span>HuggingFace 实例监控面板</span>
764
+ </div>
765
+ <div class="header-actions">
 
 
766
  <div class="theme-toggle">
767
+ <button id="theme-system" title="跟随系统">
768
+ <i class="ri-computer-line"></i>
 
 
 
769
  </button>
770
+ <button id="theme-light" title="浅色模式">
771
+ <i class="ri-sun-line"></i>
 
 
772
  </button>
773
+ <button id="theme-dark" title="深色模式">
774
+ <i class="ri-moon-line"></i>
 
 
775
  </button>
776
  </div>
777
+ <button id="loginButton" class="btn btn-primary" onclick="showLoginForm()">
778
+ <i class="ri-login-circle-line"></i> 登录
779
+ </button>
780
+ <button id="logoutButton" class="btn" onclick="logout()" style="display: none;">
781
+ <i class="ri-logout-circle-line"></i> 登出
782
+ </button>
783
+ </div>
784
+ </header>
785
+
786
+ <div class="container">
787
+ <!-- 系统概览卡片 -->
788
+ <div class="card">
789
+ <h2 class="card-title">
790
+ <i class="ri-bar-chart-box-line"></i> 系统概览
791
+ </h2>
792
+ <div id="summary" class="stats-grid">
793
+ <div class="stat-item">
794
+ <div class="stat-label">
795
+ <i class="ri-user-line"></i> 总用户数
796
+ </div>
797
+ <div id="totalUsers" class="stat-value">0</div>
798
+ </div>
799
+ <div class="stat-item">
800
+ <div class="stat-label">
801
+ <i class="ri-server-line"></i> 总实例数
802
+ </div>
803
+ <div id="totalServers" class="stat-value">0</div>
804
+ </div>
805
+ <div class="stat-item">
806
+ <div class="stat-label">
807
+ <i class="ri-checkbox-circle-line"></i> 在线实例
808
+ </div>
809
+ <div id="onlineServers" class="stat-value">0</div>
810
+ </div>
811
+ <div class="stat-item">
812
+ <div class="stat-label">
813
+ <i class="ri-close-circle-line"></i> 离线实例
814
+ </div>
815
+ <div id="offlineServers" class="stat-value">0</div>
816
+ </div>
817
+ <div class="stat-item">
818
+ <div class="stat-label">
819
+ <i class="ri-upload-2-line"></i> 总上传
820
+ </div>
821
+ <div id="totalUpload" class="stat-value">0 B/s</div>
822
+ </div>
823
+ <div class="stat-item">
824
+ <div class="stat-label">
825
+ <i class="ri-download-2-line"></i> 总下载
826
+ </div>
827
+ <div id="totalDownload" class="stat-value">0 B/s</div>
828
+ </div>
829
  </div>
830
  </div>
831
+
832
+ <!-- 服务器列表区域 -->
833
+ <div id="serversContainer">
834
+ <div class="loading-container" id="loadingIndicator">
835
+ <div class="loading-spinner"></div>
836
+ <div class="loading-text">正在加载实例数据...</div>
837
+ </div>
838
+ <div id="servers" class="stats-container" style="display: none;"></div>
839
  </div>
840
  </div>
841
+
842
+ <!-- 登录对话框 -->
843
+ <div id="loginOverlay" class="overlay">
844
+ <div class="dialog">
845
+ <div class="dialog-header">
846
+ <h2 class="dialog-title">管理员登录</h2>
847
+ <p class="dialog-subtitle">请输入您的凭据以继续</p>
848
+ </div>
849
+ <div class="dialog-body">
850
+ <div class="form-group">
851
+ <label for="username">用户名</label>
852
+ <input type="text" id="username" class="form-control" placeholder="请输入用户名">
853
+ </div>
854
+ <div class="form-group">
855
+ <label for="password">密码</label>
856
+ <input type="password" id="password" class="form-control" placeholder="请输入密码">
857
+ <div id="loginError" class="form-error"></div>
858
+ </div>
859
+ </div>
860
+ <div class="dialog-footer">
861
+ <button class="btn" onclick="hideLoginForm()">取消</button>
862
+ <button class="btn btn-primary" onclick="login()">
863
+ <i class="ri-login-circle-line"></i> 登录
864
+ </button>
865
+ </div>
866
  </div>
867
  </div>
868
+
869
+ <!-- 确认操作对话框 -->
870
+ <div id="confirmOverlay" class="overlay">
871
+ <div class="dialog">
872
+ <div class="dialog-header">
873
+ <h2 id="confirmTitle" class="dialog-title">确认操作</h2>
874
+ </div>
875
+ <div class="dialog-body">
876
+ <p id="confirmMessage"></p>
877
+ </div>
878
+ <div class="dialog-footer">
879
+ <button class="btn" onclick="cancelAction()">取消</button>
880
+ <button id="confirmButton" class="btn btn-primary" onclick="confirmAction()">确认</button>
881
+ </div>
882
  </div>
883
  </div>
884
 
885
  <script>
886
  // 主题切换功能
887
  function setTheme(theme) {
888
+ // 移除所有主题按钮的active类
889
+ document.querySelectorAll('.theme-toggle button').forEach(btn => {
890
+ btn.classList.remove('active');
891
+ });
892
+
893
+ // 设置当前主题按钮为active
894
+ document.getElementById(`theme-${theme}`).classList.add('active');
895
+
896
  if (theme === 'system') {
897
  localStorage.removeItem('theme');
898
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
 
902
  document.documentElement.setAttribute('data-theme', theme);
903
  }
904
  }
905
+
 
 
906
  // 初始化主题
907
  function initTheme() {
908
+ const savedTheme = localStorage.getItem('theme') || 'system';
909
+
910
+ if (savedTheme === 'system') {
 
911
  const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
912
+ document.documentElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
913
+ } else {
914
+ document.documentElement.setAttribute('data-theme', savedTheme);
915
  }
916
+
917
+ // 设置对应按钮为active
918
+ document.getElementById(`theme-${savedTheme}`).classList.add('active');
919
  }
920
+
921
  // 监听系统主题变化
922
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
923
+ if (!localStorage.getItem('theme') || localStorage.getItem('theme') === 'system') {
924
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
925
+ document.documentElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
926
  }
927
  });
928
+
929
+ // 添加主题切换按钮事件监听
930
+ document.getElementById('theme-system').addEventListener('click', () => setTheme('system'));
931
+ document.getElementById('theme-light').addEventListener('click', () => setTheme('light'));
932
+ document.getElementById('theme-dark').addEventListener('click', () => setTheme('dark'));
933
+
934
+ // 初始化主题
935
  initTheme();
936
+
937
  // 全局变量,表示当前是否已登录
938
  let isLoggedIn = false;
939
+
940
  // 登录状态管理
941
+ async function checkLoginStatus() {
942
  const token = localStorage.getItem('authToken');
943
  const loginButton = document.getElementById('loginButton');
944
  const logoutButton = document.getElementById('logoutButton');
945
+
946
  // 先检查本地是否有 token,决定初始 UI 状态
947
  if (token) {
948
+ console.log('本地存储中找到 token,尝试验证');
949
+
950
+ try {
951
+ const response = await fetch('/api/verify-token', {
952
+ method: 'POST',
953
+ headers: {
954
+ 'Content-Type': 'application/json',
955
+ },
956
+ body: JSON.stringify({ token })
957
+ });
958
+
959
+ const data = await response.json();
960
+
 
 
961
  if (data.success) {
962
  console.log('Token 验证成功,用户已登录');
963
  isLoggedIn = true;
964
  loginButton.style.display = 'none';
965
+ logoutButton.style.display = 'inline-flex';
966
  updateActionButtons(true);
967
+ return true;
968
  } else {
969
+ console.log('Token 验证失败,清除本地存储');
970
  localStorage.removeItem('authToken');
 
 
 
 
971
  }
972
+ } catch (error) {
 
 
973
  console.error('验证 token 失败,清除本地存储:', error);
974
  localStorage.removeItem('authToken');
975
+ }
 
 
 
 
 
 
 
 
 
 
 
 
976
  }
977
+
978
+ // 如果没有token或验证失败,显示未登录状态
979
+ isLoggedIn = false;
980
+ loginButton.style.display = 'inline-flex';
981
+ logoutButton.style.display = 'none';
982
+ updateActionButtons(false);
983
+ return false;
984
+ }
985
+
986
+ // 显示登录表单
987
  function showLoginForm() {
988
+ document.getElementById('loginOverlay').classList.add('active');
989
  document.getElementById('username').value = '';
990
  document.getElementById('password').value = '';
991
  document.getElementById('loginError').style.display = 'none';
992
+
993
+ // 聚焦用户名输入框
994
+ setTimeout(() => {
995
+ document.getElementById('username').focus();
996
+ }, 300);
997
  }
998
+
999
+ // 隐藏登录表单
1000
  function hideLoginForm() {
1001
+ document.getElementById('loginOverlay').classList.remove('active');
1002
  }
1003
+
1004
+ // 登录处理
1005
+ async function login() {
1006
+ const username = document.getElementById('username').value.trim();
1007
  const password = document.getElementById('password').value;
1008
  const loginError = document.getElementById('loginError');
1009
+
1010
+ // 简单验证
1011
+ if (!username || !password) {
1012
+ loginError.textContent = '请输入用户名和密码';
1013
+ loginError.style.display = 'block';
1014
+ return;
1015
+ }
1016
+
1017
+ try {
1018
+ // 显示加载状态
1019
+ const loginButton = document.querySelector('#loginOverlay .btn-primary');
1020
+ const originalText = loginButton.innerHTML;
1021
+ loginButton.innerHTML = '<i class="ri-loader-2-line"></i> 登录中...';
1022
+ loginButton.disabled = true;
1023
+
1024
+ const response = await fetch('/api/login', {
1025
+ method: 'POST',
1026
+ headers: {
1027
+ 'Content-Type': 'application/json',
1028
+ },
1029
+ body: JSON.stringify({ username, password })
1030
+ });
1031
+
1032
+ const data = await response.json();
1033
+
1034
+ // 恢复按钮状态
1035
+ loginButton.innerHTML = originalText;
1036
+ loginButton.disabled = false;
1037
+
1038
  if (data.success) {
1039
  console.log('登录成功,保存 token');
1040
  localStorage.setItem('authToken', data.token);
1041
  isLoggedIn = true;
1042
  hideLoginForm();
1043
+
1044
+ // 更新 UI 状态
1045
  document.getElementById('loginButton').style.display = 'none';
1046
+ document.getElementById('logoutButton').style.display = 'inline-flex';
1047
  updateActionButtons(true);
1048
+
1049
+ // 显示成功提示
1050
+ showToast('登录成功', 'success');
1051
  } else {
1052
  console.log('登录失败:', data.message);
1053
+ loginError.textContent = data.message || '用户名或密码错误';
1054
  loginError.style.display = 'block';
1055
  }
1056
+ } catch (error) {
 
1057
  console.error('登录请求失败:', error);
1058
+ loginError.textContent = '网络错误,请稍后重试';
1059
  loginError.style.display = 'block';
1060
+ }
1061
  }
1062
+
1063
+ // 登出处理
1064
+ async function logout() {
1065
  const token = localStorage.getItem('authToken');
1066
+
1067
  if (token) {
1068
+ try {
1069
+ // 显示加载状态
1070
+ const logoutButton = document.getElementById('logoutButton');
1071
+ const originalText = logoutButton.innerHTML;
1072
+ logoutButton.innerHTML = '<i class="ri-loader-2-line"></i> 登出中...';
1073
+ logoutButton.disabled = true;
1074
+
1075
+ await fetch('/api/logout', {
1076
+ method: 'POST',
1077
+ headers: {
1078
+ 'Content-Type': 'application/json',
1079
+ },
1080
+ body: JSON.stringify({ token })
1081
+ });
1082
+
1083
+ // 无论成功与否都清除token
1084
  localStorage.removeItem('authToken');
1085
  isLoggedIn = false;
1086
+
1087
+ // 更新UI状态
1088
+ document.getElementById('loginButton').style.display = 'inline-flex';
1089
  document.getElementById('logoutButton').style.display = 'none';
1090
  updateActionButtons(false);
1091
+
1092
+ // 显示成功提示
1093
+ showToast('已成功登出', 'success');
1094
+ } catch (error) {
1095
+ console.error('登出请求失败:', error);
1096
+
1097
+ // 即使请求失败也清除token
1098
  localStorage.removeItem('authToken');
1099
  isLoggedIn = false;
1100
+ document.getElementById('loginButton').style.display = 'inline-flex';
1101
  document.getElementById('logoutButton').style.display = 'none';
1102
  updateActionButtons(false);
1103
+ }
 
 
 
 
 
 
1104
  }
1105
  }
1106
+
1107
+ // 更新操作按钮状态
1108
  function updateActionButtons(loggedIn) {
1109
  console.log('更新操作按钮状态,是否已登录:', loggedIn);
1110
  isLoggedIn = loggedIn;
1111
+
1112
  const cards = document.querySelectorAll('.server-card');
1113
  cards.forEach(card => {
1114
  const buttons = card.querySelector('.action-buttons');
 
1117
  }
1118
  });
1119
  }
1120
+
 
 
 
 
 
 
1121
  // 二次确认弹窗逻辑
1122
  let pendingAction = null;
1123
  let pendingRepoId = null;
1124
+
1125
  function showConfirmDialog(action, repoId, title, message) {
1126
  pendingAction = action;
1127
  pendingRepoId = repoId;
1128
+
1129
  document.getElementById('confirmTitle').textContent = title;
1130
  document.getElementById('confirmMessage').textContent = message;
1131
+
1132
+ // 设置确认按钮样式
1133
+ const confirmButton = document.getElementById('confirmButton');
1134
+ if (action === 'restart') {
1135
+ confirmButton.className = 'btn btn-primary';
1136
+ confirmButton.innerHTML = '<i class="ri-restart-line"></i> 确认重启';
1137
+ } else if (action === 'rebuild') {
1138
+ confirmButton.className = 'btn btn-danger';
1139
+ confirmButton.innerHTML = '<i class="ri-building-line"></i> 确认重建';
1140
+ }
1141
+
1142
+ document.getElementById('confirmOverlay').classList.add('active');
1143
  }
1144
+
1145
+ async function confirmAction() {
1146
+ // 隐藏对话框
1147
+ document.getElementById('confirmOverlay').classList.remove('active');
1148
+
1149
  if (pendingAction === 'restart') {
1150
+ await restartSpace(pendingRepoId);
1151
  } else if (pendingAction === 'rebuild') {
1152
+ await rebuildSpace(pendingRepoId);
1153
  }
1154
+
1155
+ pendingAction = null;
1156
+ pendingRepoId = null;
1157
  }
1158
+
1159
  function cancelAction() {
1160
  pendingAction = null;
1161
  pendingRepoId = null;
1162
+ document.getElementById('confirmOverlay').classList.remove('active');
1163
+ }
1164
+
1165
+ // 创建Toast提示
1166
+ function showToast(message, type = 'info') {
1167
+ // 如果已有toast,先移除
1168
+ const existingToast = document.querySelector('.toast');
1169
+ if (existingToast) {
1170
+ existingToast.remove();
1171
+ }
1172
+
1173
+ // 创建新toast
1174
+ const toast = document.createElement('div');
1175
+ toast.className = `toast toast-${type}`;
1176
+
1177
+ // 设置图标
1178
+ let icon = 'information-line';
1179
+ if (type === 'success') icon = 'check-line';
1180
+ if (type === 'error') icon = 'error-warning-line';
1181
+ if (type === 'warning') icon = 'alert-line';
1182
+
1183
+ toast.innerHTML = `
1184
+ <i class="ri-${icon}"></i>
1185
+ <span>${message}</span>
1186
+ `;
1187
+
1188
+ // 添加到body
1189
+ document.body.appendChild(toast);
1190
+
1191
+ // 显示动画
1192
+ setTimeout(() => {
1193
+ toast.classList.add('show');
1194
+ }, 10);
1195
+
1196
+ // 自动关闭
1197
+ setTimeout(() => {
1198
+ toast.classList.remove('show');
1199
+ setTimeout(() => {
1200
+ toast.remove();
1201
+ }, 300);
1202
+ }, 3000);
1203
+ }
1204
+
1205
+ // 获取用户列表
1206
  async function getUsernames() {
1207
  try {
1208
  const response = await fetch('/api/config');
 
1211
  document.getElementById('totalUsers').textContent = usernamesList.length;
1212
  return usernamesList;
1213
  } catch (error) {
1214
+ console.error('获取用户列表失败:', error);
1215
  document.getElementById('totalUsers').textContent = 0;
1216
  return [];
1217
  }
1218
  }
1219
+
1220
+ // 获取实例列表
1221
  async function fetchInstances() {
1222
  try {
1223
  const response = await fetch('/api/proxy/spaces');
 
1225
  return instances;
1226
  } catch (error) {
1227
  console.error("获取实例列表失败:", error);
1228
+ showToast('获取实例列表失败,请刷新页面重试', 'error');
1229
  return [];
1230
  }
1231
  }
1232
+
1233
+ // 指标管理器
1234
  class MetricsManager {
1235
  constructor() {
1236
  this.eventSources = new Map();
1237
  this.instanceOwners = new Map();
1238
  }
1239
+
1240
  async connect(instanceId, username) {
1241
  if (this.eventSources.has(instanceId)) {
1242
  return;
1243
  }
1244
+
1245
  try {
1246
  const eventSource = new EventSource(
1247
  `/api/proxy/live-metrics/${username}/${instanceId.split('/')[1]}`
1248
  );
1249
+
1250
  this.instanceOwners.set(instanceId, username);
1251
+
1252
  eventSource.addEventListener("metric", (event) => {
1253
  try {
1254
  const data = JSON.parse(event.data);
 
1257
  console.error(`解析数据失败 (${instanceId}):`, error);
1258
  }
1259
  });
1260
+
1261
  eventSource.onerror = (error) => {
1262
+ console.warn(`指标连接错误 (${instanceId}):`, error);
1263
  eventSource.close();
1264
  this.eventSources.delete(instanceId);
1265
  updateServerCard(null, instanceId, false);
1266
  };
1267
+
1268
  this.eventSources.set(instanceId, eventSource);
1269
  } catch (error) {
1270
  console.error(`连接失败 (${username}/${instanceId}):`, error);
1271
  updateServerCard(null, instanceId, false);
1272
  }
1273
  }
1274
+
1275
  disconnectAll() {
1276
  this.eventSources.forEach(es => es.close());
1277
  this.eventSources.clear();
1278
  }
1279
  }
1280
+
1281
  const metricsManager = new MetricsManager();
1282
  const instanceMap = new Map();
1283
  const serverStatus = new Map();
1284
+
1285
+ // 初始化应用
1286
  async function initialize() {
1287
+ // 显示加载指示器
1288
+ document.getElementById('loadingIndicator').style.display = 'flex';
1289
+ document.getElementById('servers').style.display = 'none';
1290
+
1291
+ try {
1292
+ // 获取用户列表
1293
+ await getUsernames();
1294
+
1295
+ // 获取实例列表
1296
+ const instances = await fetchInstances();
 
 
 
 
 
 
 
 
 
 
 
1297
 
1298
+ // 隐藏加载指示器,显示服务器列表
1299
+ document.getElementById('loadingIndicator').style.display = 'none';
1300
+ document.getElementById('servers').style.display = 'block';
1301
 
1302
+ // 按用户分组实例
1303
+ const instancesByUser = {};
1304
+ instances.forEach(instance => {
1305
+ if (!instancesByUser[instance.owner]) {
1306
+ instancesByUser[instance.owner] = [];
1307
+ }
1308
+ instancesByUser[instance.owner].push(instance);
1309
+ });
1310
 
1311
+ // 清空现有服务器列表
1312
+ document.getElementById('servers').innerHTML = '';
1313
+
1314
+ // 渲染用户组和实例卡片
1315
+ Object.keys(instancesByUser).sort().forEach(username => {
1316
+ const userInstances = instancesByUser[username];
1317
+
1318
+ // 创建用户组
1319
+ const userGroup = document.createElement('details');
1320
+ userGroup.className = 'user-group';
1321
+ userGroup.setAttribute('open', '');
1322
+
1323
+ // 创建用户组标题
1324
+ const userHeader = document.createElement('summary');
1325
+ userHeader.className = 'user-group-header';
1326
+ userHeader.innerHTML = `
1327
+ <div>
1328
+ <i class="ri-user-3-line"></i>
1329
+ 用户: <strong>${username}</strong>
1330
+ <span class="server-count">(${userInstances.length}个实例)</span>
1331
  </div>
1332
+ <i class="ri-arrow-down-s-line toggle-icon"></i>
1333
+ `;
1334
+ userGroup.appendChild(userHeader);
1335
+
1336
+ // 创建用户组内容容器
1337
+ const userContent = document.createElement('div');
1338
+ userContent.className = 'user-group-content';
1339
+
1340
+ // 创建服务器网格
1341
+ const serverGrid = document.createElement('div');
1342
+ serverGrid.className = 'server-grid';
1343
+
1344
+ // 添加每个实例卡片
1345
+ userInstances.forEach(instance => {
1346
+ const card = createServerCard(instance);
1347
+ serverGrid.appendChild(card);
1348
+
1349
+ // 如果实例正在运行,连接指标
1350
+ if (instance.status.toLowerCase() === 'running') {
1351
+ metricsManager.connect(instance.repo_id, instance.owner);
1352
+ }
1353
+ });
1354
+
1355
+ userContent.appendChild(serverGrid);
1356
+ userGroup.appendChild(userContent);
1357
+ document.getElementById('servers').appendChild(userGroup);
1358
+ });
1359
+
1360
+ // 更新概览统计
1361
+ updateSummary();
1362
+
1363
+ // 确保按钮状态正确
1364
+ updateActionButtons(isLoggedIn);
1365
+ } catch (error) {
1366
+ console.error('初始化失败:', error);
1367
+ document.getElementById('loadingIndicator').style.display = 'none';
1368
+
1369
+ // 显示错误信息
1370
+ const errorContainer = document.createElement('div');
1371
+ errorContainer.className = 'card';
1372
+ errorContainer.innerHTML = `
1373
+ <div style="text-align: center; padding: 30px;">
1374
+ <i class="ri-error-warning-line" style="font-size: 3rem; color: var(--danger-color);"></i>
1375
+ <h3 style="margin: 15px 0;">加载失败</h3>
1376
+ <p style="margin-bottom: 20px; color: var(--secondary-text);">无法加载实例数据,请检查网络连接后重试。</p>
1377
+ <button class="btn btn-primary" onclick="initialize()">
1378
+ <i class="ri-refresh-line"></i> 重新加载
1379
+ </button>
1380
  </div>
1381
+ `;
1382
+ document.getElementById('servers').innerHTML = '';
1383
+ document.getElementById('servers').appendChild(errorContainer);
1384
+ document.getElementById('servers').style.display = 'block';
1385
+ }
1386
+ }
1387
+
1388
+ // 创建服务器卡片
1389
+ function createServerCard(instance) {
1390
+ const instanceId = instance.repo_id;
1391
+ instanceMap.set(instanceId, instance);
1392
+
1393
+ // 创建卡片元素
1394
+ const card = document.createElement('div');
1395
+ card.id = `instance-${instanceId}`;
1396
+ card.className = 'server-card';
1397
+
1398
+ // 设置状态样式
1399
+ let statusClass = 'status-offline';
1400
+ let statusText = '离线';
1401
+
1402
+ if (instance.status.toLowerCase() === 'running') {
1403
+ statusClass = 'status-online';
1404
+ statusText = '在线';
1405
+ } else if (instance.status.toLowerCase() === 'sleeping') {
1406
+ statusClass = 'status-sleep';
1407
+ statusText = '休眠';
1408
+ }
1409
+
1410
+ // 设置卡片内容
1411
+ card.innerHTML = `
1412
+ <div class="server-header">
1413
+ <div class="server-name">
1414
+ <i class="ri-server-line"></i>
1415
+ <div>
1416
+ ${instance.name}
1417
+ <div class="server-id">${instance.repo_id}</div>
1418
  </div>
1419
+ </div>
1420
+ <div class="status-indicator">
1421
+ <span class="status-dot ${statusClass}"></span>
1422
+ <span>${statusText}</span>
1423
+ </div>
1424
+ </div>
1425
+
1426
+ <div class="metric-grid">
1427
+ <div class="metric-item">
1428
+ <div class="metric-label">
1429
+ <i class="ri-cpu-line"></i> CPU
1430
  </div>
1431
+ <div class="metric-value cpu-usage">N/A</div>
1432
+ </div>
1433
+ <div class="metric-item">
1434
+ <div class="metric-label">
1435
+ <i class="ri-hard-drive-2-line"></i> 内存
1436
  </div>
1437
+ <div class="metric-value memory-usage">N/A</div>
1438
+ </div>
1439
+ <div class="metric-item">
1440
+ <div class="metric-label">
1441
+ <i class="ri-upload-2-line"></i> 上传
1442
  </div>
1443
+ <div class="metric-value upload">N/A</div>
1444
  </div>
1445
+ <div class="metric-item">
1446
+ <div class="metric-label">
1447
+ <i class="ri-download-2-line"></i> 下载
1448
+ </div>
1449
+ <div class="metric-value download">N/A</div>
1450
  </div>
1451
+ </div>
1452
+
1453
+ <div class="action-buttons" style="display: ${isLoggedIn ? 'flex' : 'none'};">
1454
+ <button class="btn" onclick="showConfirmDialog('restart', '${instance.repo_id}', '确认重启', '您确定要重启实例 ${instance.name} (${instance.repo_id}) 吗?')">
1455
+ <i class="ri-restart-line"></i> 重启
1456
+ </button>
1457
+ <button class="btn" onclick="showConfirmDialog('rebuild', '${instance.repo_id}', '确认重建', '您确定要重建实例 ${instance.name} (${instance.repo_id}) 吗?这将重新构建整个实例。')">
1458
+ <i class="ri-building-line"></i> 重建
1459
+ </button>
1460
+ </div>
1461
+ `;
1462
+
1463
+ // 记录服务器状态
1464
+ serverStatus.set(instanceId, {
1465
+ lastSeen: Date.now(),
1466
+ isOnline: instance.status.toLowerCase() === 'running',
1467
+ isSleep: instance.status.toLowerCase() === 'sleeping',
1468
+ data: null,
1469
+ status: instance.status
1470
+ });
1471
+
1472
+ return card;
1473
  }
1474
+
1475
+ // 更新服务器卡片
1476
  function updateServerCard(data, instanceId, isSleep = false) {
1477
+ const card = document.getElementById(`instance-${instanceId}`);
 
1478
  const instance = instanceMap.get(instanceId);
1479
 
1480
+ if (!card || !instance) return;
1481
+
1482
+ const statusDot = card.querySelector('.status-dot');
1483
+ const statusText = card.querySelector('.status-indicator span:last-child');
1484
+
1485
+ let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
1486
+ let isOnline = false;
1487
+
1488
+ if (data) {
1489
+ // 有数据,实例在线
1490
+ cpuUsage = `${data.cpu_usage_pct}%`;
1491
+ memoryUsage = `${((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2)}%`;
1492
+ upload = `${formatBytes(data.tx_bps)}/s`;
1493
+ download = `${formatBytes(data.rx_bps)}/s`;
1494
+
1495
+ statusDot.className = 'status-dot status-online';
1496
+ statusText.textContent = '在线';
1497
+ isOnline = true;
1498
+ isSleep = false;
1499
+ } else {
1500
+ // 无数据,根据实例状态设置
1501
+ const currentStatus = instance?.status.toLowerCase() || 'unknown';
1502
+
1503
+ if (currentStatus === 'running') {
1504
  statusDot.className = 'status-dot status-online';
1505
+ statusText.textContent = '在线';
1506
  isOnline = true;
1507
  isSleep = false;
1508
+ } else if (currentStatus === 'sleeping') {
1509
+ statusDot.className = 'status-dot status-sleep';
1510
+ statusText.textContent = '休眠';
1511
+ isOnline = false;
1512
+ isSleep = true;
1513
  } else {
1514
+ statusDot.className = 'status-dot status-offline';
1515
+ statusText.textContent = '离线';
1516
+ isOnline = false;
1517
+ isSleep = false;
 
 
 
 
 
 
 
 
 
 
1518
  }
 
 
 
 
 
 
1519
  }
1520
+
1521
+ // 更新指标显示
1522
+ card.querySelector('.cpu-usage').textContent = cpuUsage;
1523
+ card.querySelector('.memory-usage').textContent = memoryUsage;
1524
+ card.querySelector('.upload').textContent = upload;
1525
+ card.querySelector('.download').textContent = download;
1526
+
1527
+ // 更新状态记录
1528
+ serverStatus.set(instanceId, {
1529
+ lastSeen: Date.now(),
1530
+ isOnline,
1531
+ isSleep,
1532
+ data: data || null,
1533
+ status: instance?.status || 'unknown'
1534
+ });
1535
+
1536
+ // 更新概览统计
1537
+ updateSummary();
1538
  }
1539
+
1540
+ // 重启实例
1541
  async function restartSpace(repoId) {
1542
  try {
1543
  const token = localStorage.getItem('authToken');
1544
  if (!token || !isLoggedIn) {
1545
+ showToast('请先登录以执行此操作', 'warning');
1546
  showLoginForm();
1547
  return;
1548
  }
1549
+
1550
+ // 显示加载提示
1551
+ showToast(`正在重启实例: ${repoId}`, 'info');
1552
+
1553
  const encodedRepoId = encodeURIComponent(repoId);
1554
  const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, {
1555
  method: 'POST',
 
1557
  'Authorization': `Bearer ${token}`
1558
  }
1559
  });
1560
+
1561
  const result = await response.json();
1562
+
1563
  if (result.success) {
1564
+ showToast(`实例重启成功: ${repoId}`, 'success');
1565
+
1566
+ // 更新实例状态
1567
+ const instance = instanceMap.get(repoId);
1568
+ if (instance) {
1569
+ instance.status = 'RUNNING';
1570
+ updateServerCard(null, repoId);
1571
+
1572
+ // 重新连接指标
1573
+ setTimeout(() => {
1574
+ metricsManager.connect(repoId, instance.owner);
1575
+ }, 5000);
1576
+ }
1577
  } else {
1578
  if (response.status === 401) {
1579
+ showToast('登录已过期,请重新登录', 'error');
1580
  localStorage.removeItem('authToken');
1581
  isLoggedIn = false;
1582
+ document.getElementById('loginButton').style.display = 'inline-flex';
1583
  document.getElementById('logoutButton').style.display = 'none';
1584
  updateActionButtons(false);
1585
  showLoginForm();
1586
  } else {
1587
+ showToast(`重启失败: ${result.error || '未知错误'}`, 'error');
1588
  console.error(`重启失败 (${repoId}):`, result.error, result.details);
1589
  }
1590
  }
1591
  } catch (error) {
1592
  console.error(`重启失败 (${repoId}):`, error);
1593
+ showToast(`重启失败: ${error.message}`, 'error');
1594
  }
1595
  }
1596
+
1597
+ // 重建实例
1598
  async function rebuildSpace(repoId) {
1599
  try {
1600
  const token = localStorage.getItem('authToken');
1601
  if (!token || !isLoggedIn) {
1602
+ showToast('请先登录以执行此操作', 'warning');
1603
  showLoginForm();
1604
  return;
1605
  }
1606
+
1607
+ // 显示加载提示
1608
+ showToast(`正在重建实例: ${repoId}`, 'info');
1609
+
1610
  const encodedRepoId = encodeURIComponent(repoId);
1611
  const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, {
1612
  method: 'POST',
 
1614
  'Authorization': `Bearer ${token}`
1615
  }
1616
  });
1617
+
1618
  const result = await response.json();
1619
+
1620
  if (result.success) {
1621
+ showToast(`实例重建成功: ${repoId}`, 'success');
1622
+
1623
+ // 更新实例状态
1624
+ const instance = instanceMap.get(repoId);
1625
+ if (instance) {
1626
+ instance.status = 'BUILDING';
1627
+ updateServerCard(null, repoId);
1628
+
1629
+ // 一段时间后刷新页面以获取最新状态
1630
+ setTimeout(() => {
1631
+ initialize();
1632
+ }, 10000);
1633
+ }
1634
  } else {
1635
  if (response.status === 401) {
1636
+ showToast('登录已过期,请重新登录', 'error');
1637
  localStorage.removeItem('authToken');
1638
  isLoggedIn = false;
1639
+ document.getElementById('loginButton').style.display = 'inline-flex';
1640
  document.getElementById('logoutButton').style.display = 'none';
1641
  updateActionButtons(false);
1642
  showLoginForm();
1643
  } else {
1644
+ showToast(`重建失败: ${result.error || '未知错误'}`, 'error');
1645
  console.error(`重建失败 (${repoId}):`, result.error, result.details);
1646
  }
1647
  }
1648
  } catch (error) {
1649
  console.error(`重建失败 (${repoId}):`, error);
1650
+ showToast(`重建失败: ${error.message}`, 'error');
1651
  }
1652
  }
1653
+
1654
+ // 更新概览统计
1655
  function updateSummary() {
1656
  let online = 0;
1657
  let offline = 0;
1658
  let totalUpload = 0;
1659
  let totalDownload = 0;
1660
+
1661
  serverStatus.forEach((status, instanceId) => {
1662
  const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
1663
  if (isRecentlyOnline) {
 
1670
  offline++;
1671
  }
1672
  });
1673
+
1674
  document.getElementById('totalServers').textContent = serverStatus.size;
1675
  document.getElementById('onlineServers').textContent = online;
1676
  document.getElementById('offlineServers').textContent = offline;
1677
  document.getElementById('totalUpload').textContent = `${formatBytes(totalUpload)}/s`;
1678
  document.getElementById('totalDownload').textContent = `${formatBytes(totalDownload)}/s`;
1679
  }
1680
+
1681
+ // 格式化字节数
1682
  function formatBytes(bytes) {
1683
  if (bytes === 0) return '0 B';
1684
  const k = 1024;
 
1686
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1687
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1688
  }
1689
+
1690
+ // 添加Toast样式
1691
+ const style = document.createElement('style');
1692
+ style.textContent = `
1693
+ .toast {
1694
+ position: fixed;
1695
+ bottom: 20px;
1696
+ right: 20px;
1697
+ background: var(--card-background);
1698
+ color: var(--text-color);
1699
+ padding: 12px 20px;
1700
+ border-radius: var(--border-radius-sm);
1701
+ box-shadow: var(--shadow-md);
1702
+ display: flex;
1703
+ align-items: center;
1704
+ gap: 10px;
1705
+ z-index: 1100;
1706
+ transform: translateY(100px);
1707
+ opacity: 0;
1708
+ transition: transform 0.3s ease, opacity 0.3s ease;
1709
+ max-width: 350px;
1710
+ border-left: 4px solid var(--primary-color);
1711
+ }
1712
+
1713
+ .toast.show {
1714
+ transform: translateY(0);
1715
+ opacity: 1;
1716
+ }
1717
+
1718
+ .toast i {
1719
+ font-size: 1.25rem;
1720
+ }
1721
+
1722
+ .toast-success {
1723
+ border-left-color: var(--success-color);
1724
+ }
1725
+
1726
+ .toast-success i {
1727
+ color: var(--success-color);
1728
+ }
1729
+
1730
+ .toast-error {
1731
+ border-left-color: var(--danger-color);
1732
+ }
1733
+
1734
+ .toast-error i {
1735
+ color: var(--danger-color);
1736
+ }
1737
+
1738
+ .toast-warning {
1739
+ border-left-color: var(--warning-color);
1740
+ }
1741
+
1742
+ .toast-warning i {
1743
+ color: var(--warning-color);
1744
+ }
1745
+
1746
+ @media (max-width: 576px) {
1747
+ .toast {
1748
+ left: 20px;
1749
+ right: 20px;
1750
+ max-width: calc(100% - 40px);
1751
+ }
1752
+ }
1753
+ `;
1754
+ document.head.appendChild(style);
1755
+
1756
+ // 添加键盘事件监听
1757
+ document.addEventListener('keydown', function(e) {
1758
+ // ESC键关闭对话框
1759
+ if (e.key === 'Escape') {
1760
+ if (document.getElementById('loginOverlay').classList.contains('active')) {
1761
+ hideLoginForm();
1762
+ }
1763
+ if (document.getElementById('confirmOverlay').classList.contains('active')) {
1764
+ cancelAction();
1765
+ }
1766
+ }
1767
+
1768
+ // 在登录表单中按Enter键提交
1769
+ if (e.key === 'Enter' && document.getElementById('loginOverlay').classList.contains('active')) {
1770
+ if (document.activeElement === document.getElementById('username') ||
1771
+ document.activeElement === document.getElementById('password')) {
1772
+ login();
1773
+ }
1774
+ }
1775
+ });
1776
+
1777
+ // 定期更新概览统计
1778
  setInterval(updateSummary, 5000);
1779
+
1780
+ // 定期刷新所有数据
1781
  setInterval(async () => {
1782
+ console.log('执行定期刷新...');
1783
  metricsManager.disconnectAll();
1784
  await initialize();
1785
+ }, 300000); // 每5分钟刷新一次
1786
+
1787
+ // 页面加载完成后初始化
1788
+ window.onload = async function() {
1789
+ console.log('页面加载完成,开始检查登录状态');
1790
+ await checkLoginStatus();
1791
+ console.log('登录状态检查完成,初始化数据');
1792
+ initialize();
1793
+
1794
+ // 添加登录表单的回车键提交
1795
+ document.getElementById('username').addEventListener('keyup', function(e) {
1796
+ if (e.key === 'Enter') {
1797
+ document.getElementById('password').focus();
1798
+ }
1799
+ });
1800
+
1801
+ document.getElementById('password').addEventListener('keyup', function(e) {
1802
+ if (e.key === 'Enter') {
1803
+ login();
1804
+ }
1805
+ });
1806
+ };
1807
  </script>
1808
  </body>
1809
  </html>