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

Create public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +938 -0
public/index.html ADDED
@@ -0,0 +1,938 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
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;
424
+ document.documentElement.setAttribute('data-theme', systemPrefersDark ? 'dark' : 'light');
425
+ } else {
426
+ localStorage.setItem('theme', theme);
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');
592
+ if (buttons) {
593
+ buttons.style.display = loggedIn ? 'flex' : 'none';
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');
630
+ const config = await response.json();
631
+ const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
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');
643
+ const instances = await response.json();
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);
667
+ updateServerCard(data, instanceId);
668
+ } catch (error) {
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',
838
+ headers: {
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',
875
+ headers: {
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) {
909
+ online++;
910
+ if (status.data) {
911
+ totalUpload += parseFloat(status.data.tx_bps) || 0;
912
+ totalDownload += parseFloat(status.data.rx_bps) || 0;
913
+ }
914
+ } else {
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;
927
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
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>