mistpe commited on
Commit
ae7166a
·
verified ·
1 Parent(s): c63db81

Update templates/dashboard.html

Browse files
Files changed (1) hide show
  1. templates/dashboard.html +390 -194
templates/dashboard.html CHANGED
@@ -9,20 +9,15 @@
9
  :root {
10
  --primary-color: #00ff9d;
11
  --primary-dark: #00cc7d;
12
- --secondary-color: #ff00ff;
13
  --background-color: #0a0b0f;
14
  --card-background: #12141c;
15
  --text-primary: #ffffff;
16
  --text-secondary: #7f8ea3;
 
17
  --success-color: #00ff9d;
18
  --warning-color: #ff9d00;
19
  --danger-color: #ff2d55;
20
  --sleeping-color: #00ffff;
21
- --border-color: #1e2029;
22
- --card-border: 1px solid #2a2d3a;
23
- --card-glow: 0 0 20px rgba(0, 255, 157, 0.1);
24
- --border-radius: 12px;
25
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
26
  }
27
 
28
  * {
@@ -38,150 +33,170 @@
38
  radial-gradient(circle at 10% 20%, rgba(0, 255, 157, 0.03) 0%, transparent 20%),
39
  radial-gradient(circle at 90% 80%, rgba(255, 0, 255, 0.03) 0%, transparent 20%);
40
  min-height: 100vh;
41
- display: flex;
42
- flex-direction: column;
43
  color: var(--text-primary);
44
  }
45
 
46
  .header {
47
- background: rgba(18, 20, 28, 0.8);
48
  backdrop-filter: blur(10px);
49
  position: fixed;
50
  top: 0;
51
  left: 0;
52
  right: 0;
53
  z-index: 1000;
 
 
 
 
 
 
54
  padding: 1rem 2rem;
55
  display: flex;
56
  justify-content: space-between;
57
  align-items: center;
58
- border-bottom: 1px solid var(--border-color);
59
  }
60
 
61
- .header h1 {
 
 
 
 
 
 
62
  font-size: 1.5rem;
63
- color: var(--text-primary);
64
  font-weight: 600;
 
65
  display: flex;
66
  align-items: center;
67
  gap: 0.5rem;
68
  }
69
 
70
- .header h1 i {
71
  color: var(--primary-color);
72
  text-shadow: 0 0 10px var(--primary-color);
73
  }
74
 
75
- .container {
76
- max-width: 1400px;
77
- margin: 90px auto 2rem;
78
- padding: 0 2rem;
79
  }
80
 
81
- .owner-section {
82
- background: var(--card-background);
83
- border-radius: var(--border-radius);
84
- padding: 2rem;
85
- margin-bottom: 2rem;
86
- border: var(--card-border);
87
- box-shadow: var(--card-glow);
88
- position: relative;
89
- overflow: hidden;
90
  }
91
 
92
- .owner-section::before {
93
- content: '';
94
  position: absolute;
95
- top: 0;
96
- left: 0;
97
- right: 0;
98
- height: 1px;
99
- background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
100
- opacity: 0.5;
101
  }
102
 
103
- .owner-header {
104
  display: flex;
105
- justify-content: space-between;
106
  align-items: center;
107
- margin-bottom: 2rem;
108
- padding-bottom: 1rem;
109
- border-bottom: 1px solid var(--border-color);
110
  }
111
 
112
- .owner-name {
113
- font-size: 1.5rem;
114
- font-weight: 600;
115
- color: var(--text-primary);
116
- display: flex;
117
- align-items: center;
118
- gap: 0.5rem;
 
119
  }
120
 
121
- .status-stats {
122
- display: flex;
123
- gap: 1rem;
124
- flex-wrap: wrap;
125
  }
126
 
127
- .stat-item {
128
- padding: 0.5rem 1rem;
129
- border-radius: 8px;
130
- background: rgba(255, 255, 255, 0.05);
131
- display: flex;
132
- align-items: center;
133
- gap: 0.5rem;
 
 
 
 
134
  border: 1px solid var(--border-color);
135
  }
136
 
137
- .space-grid {
138
  display: grid;
139
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
140
- gap: 1.5rem;
 
141
  }
142
 
143
- .space-card {
144
- background: var(--card-background);
145
- border-radius: var(--border-radius);
146
  padding: 1.5rem;
147
- border: var(--card-border);
148
- transition: var(--transition);
149
- position: relative;
150
- overflow: hidden;
151
  }
152
 
153
- .space-card:hover {
154
  transform: translateY(-2px);
155
- box-shadow: var(--card-glow);
156
  }
157
 
158
- .space-card::after {
159
- content: '';
160
- position: absolute;
161
- top: 0;
162
- right: 0;
163
- width: 6px;
164
- height: 100%;
165
- background: var(--primary-color);
166
- opacity: 0.5;
167
- transition: var(--transition);
 
 
168
  }
169
 
170
- .space-card:hover::after {
171
- opacity: 1;
 
 
 
 
172
  }
173
 
174
- .space-header {
 
 
175
  display: flex;
176
  justify-content: space-between;
177
- align-items: flex-start;
178
- margin-bottom: 1rem;
179
  }
180
 
181
- .space-name {
182
  font-size: 1.25rem;
183
  font-weight: 600;
184
- color: var(--text-primary);
 
 
 
 
 
 
 
 
185
  }
186
 
187
  .status-badge {
@@ -189,46 +204,50 @@
189
  border-radius: 6px;
190
  font-size: 0.875rem;
191
  font-weight: 500;
192
- text-transform: uppercase;
193
- letter-spacing: 0.5px;
194
- border: 1px solid transparent;
195
  }
196
 
197
- .status-RUNNING {
198
- background-color: rgba(0, 255, 157, 0.1);
199
- border-color: var(--success-color);
200
- color: var(--success-color);
 
201
  }
202
 
203
- .status-BUILDING {
204
- background-color: rgba(255, 157, 0, 0.1);
205
- border-color: var(--warning-color);
206
- color: var(--warning-color);
 
 
207
  }
208
 
209
- .status-SLEEPING {
210
- background-color: rgba(0, 255, 255, 0.1);
211
- border-color: var(--sleeping-color);
212
- color: var(--sleeping-color);
213
  }
214
 
215
- .status-STOPPED {
216
- background-color: rgba(127, 142, 163, 0.1);
217
- border-color: var(--text-secondary);
218
- color: var(--text-secondary);
 
 
 
219
  }
220
 
221
- .status-FAILED, .status-BUILD_ERROR {
222
- background-color: rgba(255, 45, 85, 0.1);
223
- border-color: var(--danger-color);
224
- color: var(--danger-color);
225
  }
226
 
227
  .space-info {
228
  display: grid;
229
  grid-template-columns: repeat(2, 1fr);
230
  gap: 1rem;
231
- margin-bottom: 1.5rem;
232
  }
233
 
234
  .info-item {
@@ -238,58 +257,111 @@
238
  }
239
 
240
  .info-label {
241
- font-size: 0.875rem;
242
  color: var(--text-secondary);
 
243
  text-transform: uppercase;
244
  letter-spacing: 0.5px;
245
  }
246
 
247
  .info-value {
248
- font-size: 0.9375rem;
249
  color: var(--text-primary);
250
- font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  }
252
 
253
  .action-buttons {
254
  display: grid;
255
  grid-template-columns: repeat(3, 1fr);
256
  gap: 0.75rem;
 
 
257
  }
258
 
259
  .action-button {
260
  padding: 0.75rem;
261
  border-radius: 6px;
262
- font-size: 0.9375rem;
263
  font-weight: 500;
264
- text-align: center;
265
- cursor: pointer;
266
- transition: var(--transition);
267
  display: flex;
268
  align-items: center;
269
  justify-content: center;
270
  gap: 0.5rem;
271
- text-decoration: none;
 
272
  border: 1px solid var(--border-color);
273
  background: transparent;
274
  color: var(--text-primary);
 
275
  }
276
 
277
  .action-button:hover {
278
  border-color: var(--primary-color);
279
- box-shadow: 0 0 10px rgba(0, 255, 157, 0.2);
280
  }
281
 
282
  .action-button.restart {
283
- background-color: rgba(0, 255, 157, 0.1);
284
  border-color: var(--primary-color);
285
  color: var(--primary-color);
286
  }
287
 
288
  .action-button.restart:hover {
289
- background-color: var(--primary-color);
290
  color: var(--background-color);
291
  }
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  .loading-overlay {
294
  position: fixed;
295
  top: 0;
@@ -305,32 +377,51 @@
305
  }
306
 
307
  .loading-spinner {
308
- width: 40px;
309
- height: 40px;
310
- border: 3px solid var(--card-background);
311
- border-top: 3px solid var(--primary-color);
 
 
 
 
 
 
 
 
312
  border-radius: 50%;
313
- animation: spin 1s linear infinite;
314
- box-shadow: 0 0 20px rgba(0, 255, 157, 0.2);
 
315
  }
316
 
317
  @keyframes spin {
318
- 0% { transform: rotate(0deg); }
319
- 100% { transform: rotate(360deg); }
320
  }
321
 
322
  @media (max-width: 768px) {
323
- .container {
324
- padding: 0 1rem;
 
 
325
  }
326
 
327
- .owner-header {
 
328
  flex-direction: column;
329
  gap: 1rem;
330
  }
331
 
332
- .status-stats {
333
- justify-content: flex-start;
 
 
 
 
 
 
 
 
334
  }
335
 
336
  .space-grid {
@@ -340,6 +431,11 @@
340
  .space-info {
341
  grid-template-columns: 1fr;
342
  }
 
 
 
 
 
343
  }
344
  </style>
345
  </head>
@@ -349,10 +445,27 @@
349
  </div>
350
 
351
  <header class="header">
352
- <h1><i class="fas fa-server"></i> HF Space Manager</h1>
353
- <a href="/logout" class="action-button">
354
- <i class="fas fa-sign-out-alt"></i> 退出
355
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  </header>
357
 
358
  <div class="container">
@@ -364,84 +477,129 @@
364
  {% endif %}
365
  {% set _ = grouped_spaces[space.owner].append(space) %}
366
  {% endfor %}
367
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  {% for owner, owner_spaces in grouped_spaces.items() %}
369
- {% set running_count = owner_spaces | selectattr('status','equalto','RUNNING') | list | length %}
370
- {% set building_count = owner_spaces | selectattr('status','equalto','BUILDING') | list | length %}
371
- {% set sleeping_count = owner_spaces | selectattr('status','equalto','SLEEPING') | list | length %}
372
- {% set stopped_count = owner_spaces | selectattr('status','equalto','STOPPED') | list | length %}
373
- {% set failed_count = owner_spaces | selectattr('status','equalto','FAILED') | list | length %}
 
374
  <div class="owner-section">
375
  <div class="owner-header">
376
  <div class="owner-name">
377
- <i class="fas fa-user"></i>
378
  {{ owner }}
379
  </div>
380
  <div class="status-stats">
381
- <div class="stat-item">
382
- <i class="fas fa-cube"></i>
383
- 总数: {{ owner_spaces|length }}
384
- </div>
385
- <div class="stat-item">
386
  <i class="fas fa-play-circle"></i>
387
  运行中: {{ running_count }}
388
- </div>
389
- <div class="stat-item">
390
  <i class="fas fa-moon"></i>
391
  休眠: {{ sleeping_count }}
392
- </div>
393
- <div class="stat-item">
394
  <i class="fas fa-stop-circle"></i>
395
  停止: {{ stopped_count }}
396
- </div>
397
- <div class="stat-item">
398
  <i class="fas fa-exclamation-circle"></i>
399
  失败: {{ failed_count }}
400
- </div>
401
  </div>
402
  </div>
 
403
  <div class="space-grid">
404
  {% for space in owner_spaces %}
405
  <div class="space-card" data-space-id="{{ space.repo_id }}">
406
  <div class="space-header">
407
- <div class="space-name">{{ space.name }}</div>
408
- <span class="status-badge status-{{ space.status }}">{{ space.status }}</span>
409
- </div>
410
- <div class="space-info">
411
- <div class="info-item">
412
- <span class="info-label">Space ID</span>
413
- <span class="info-value">{{ space.repo_id }}</span>
414
- </div>
415
- <div class="info-item">
416
- <span class="info-label">创建时间</span>
417
- <span class="info-value">{{ space.created_at }}</span>
418
- </div>
419
- <div class="info-item">
420
- <span class="info-label">最后修改</span>
421
- <span class="info-value">{{ space.last_modified }}</span>
422
  </div>
423
- <div class="info-item">
424
- <span class="info-label">SDK 版本</span>
425
- <span class="info-value">{{ space.sdk }}</span>
426
- </div>
427
- <div class="info-item">
428
- <span class="info-label">应用端口</span>
429
- <span class="info-value">{{ space.app_port }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  </div>
431
- <div class="info-item">
432
- <span class="info-label">访问权限</span>
433
- <span class="info-value">{{ '私有' if space.private else '公开' }}</span>
 
 
 
 
 
 
 
434
  </div>
435
  </div>
 
436
  <div class="action-buttons">
437
- <a href="{{ space.url }}" target="_blank" class="action-button view">
438
- <i class="fas fa-external-link-alt"></i> 查看
 
439
  </a>
440
  <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">
441
- <i class="fas fa-sync-alt"></i> 重启
 
442
  </button>
443
- <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button rebuild">
444
- <i class="fas fa-tools"></i> 重建
 
445
  </button>
446
  </div>
447
  </div>
@@ -451,8 +609,9 @@
451
  {% endfor %}
452
  {% else %}
453
  <div class="owner-section">
454
- <p style="text-align: center; color: var(--text-secondary);">
455
- <i class="fas fa-info-circle"></i> 没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。
 
456
  </p>
457
  </div>
458
  {% endif %}
@@ -462,6 +621,35 @@
462
  <script>
463
  const socket = io();
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  socket.on('connect', () => {
466
  console.log('Connected to server');
467
  });
@@ -481,10 +669,6 @@
481
  socket.connect();
482
  }
483
  });
484
-
485
- window.addEventListener('load', function() {
486
- document.getElementById('loading').style.display = 'none';
487
- });
488
 
489
  function updateSpaceStatuses() {
490
  document.querySelectorAll('.space-card').forEach(card => {
@@ -495,7 +679,7 @@
495
  const statusElement = card.querySelector('.status-badge');
496
  if (statusElement && data.status) {
497
  statusElement.className = `status-badge status-${data.status}`;
498
- statusElement.textContent = data.status;
499
  }
500
  })
501
  .catch(error => console.error('Error updating status:', error));
@@ -510,7 +694,19 @@
510
  }
511
  }
512
 
 
513
  setInterval(updateSpaceStatuses, 30000);
 
 
 
 
 
 
 
 
 
 
 
514
  </script>
515
  </body>
516
  </html>
 
9
  :root {
10
  --primary-color: #00ff9d;
11
  --primary-dark: #00cc7d;
 
12
  --background-color: #0a0b0f;
13
  --card-background: #12141c;
14
  --text-primary: #ffffff;
15
  --text-secondary: #7f8ea3;
16
+ --border-color: #1e2029;
17
  --success-color: #00ff9d;
18
  --warning-color: #ff9d00;
19
  --danger-color: #ff2d55;
20
  --sleeping-color: #00ffff;
 
 
 
 
 
21
  }
22
 
23
  * {
 
33
  radial-gradient(circle at 10% 20%, rgba(0, 255, 157, 0.03) 0%, transparent 20%),
34
  radial-gradient(circle at 90% 80%, rgba(255, 0, 255, 0.03) 0%, transparent 20%);
35
  min-height: 100vh;
 
 
36
  color: var(--text-primary);
37
  }
38
 
39
  .header {
40
+ background: rgba(18, 20, 28, 0.95);
41
  backdrop-filter: blur(10px);
42
  position: fixed;
43
  top: 0;
44
  left: 0;
45
  right: 0;
46
  z-index: 1000;
47
+ border-bottom: 1px solid var(--border-color);
48
+ }
49
+
50
+ .header-content {
51
+ max-width: 1400px;
52
+ margin: 0 auto;
53
  padding: 1rem 2rem;
54
  display: flex;
55
  justify-content: space-between;
56
  align-items: center;
 
57
  }
58
 
59
+ .nav-section {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 2rem;
63
+ }
64
+
65
+ .logo {
66
  font-size: 1.5rem;
 
67
  font-weight: 600;
68
+ color: var(--text-primary);
69
  display: flex;
70
  align-items: center;
71
  gap: 0.5rem;
72
  }
73
 
74
+ .logo i {
75
  color: var(--primary-color);
76
  text-shadow: 0 0 10px var(--primary-color);
77
  }
78
 
79
+ .search-bar {
80
+ position: relative;
81
+ width: 300px;
 
82
  }
83
 
84
+ .search-bar input {
85
+ width: 100%;
86
+ padding: 0.5rem 1rem 0.5rem 2.5rem;
87
+ background: rgba(255, 255, 255, 0.05);
88
+ border: 1px solid var(--border-color);
89
+ border-radius: 6px;
90
+ color: var(--text-primary);
91
+ font-size: 0.9rem;
 
92
  }
93
 
94
+ .search-bar i {
 
95
  position: absolute;
96
+ left: 0.8rem;
97
+ top: 50%;
98
+ transform: translateY(-50%);
99
+ color: var(--text-secondary);
 
 
100
  }
101
 
102
+ .user-section {
103
  display: flex;
 
104
  align-items: center;
105
+ gap: 1rem;
 
 
106
  }
107
 
108
+ .theme-toggle {
109
+ background: none;
110
+ border: none;
111
+ color: var(--text-secondary);
112
+ cursor: pointer;
113
+ padding: 0.5rem;
114
+ border-radius: 4px;
115
+ transition: all 0.3s ease;
116
  }
117
 
118
+ .theme-toggle:hover {
119
+ color: var(--primary-color);
120
+ background: rgba(0, 255, 157, 0.1);
 
121
  }
122
 
123
+ .container {
124
+ max-width: 1400px;
125
+ margin: 80px auto 0;
126
+ padding: 2rem;
127
+ }
128
+
129
+ .dashboard-header {
130
+ margin-bottom: 2rem;
131
+ padding: 1.5rem;
132
+ background: var(--card-background);
133
+ border-radius: 12px;
134
  border: 1px solid var(--border-color);
135
  }
136
 
137
+ .stats-grid {
138
  display: grid;
139
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
140
+ gap: 1rem;
141
+ margin-top: 1rem;
142
  }
143
 
144
+ .stat-card {
145
+ background: rgba(255, 255, 255, 0.03);
 
146
  padding: 1.5rem;
147
+ border-radius: 8px;
148
+ border: 1px solid var(--border-color);
149
+ transition: all 0.3s ease;
 
150
  }
151
 
152
+ .stat-card:hover {
153
  transform: translateY(-2px);
154
+ border-color: var(--primary-color);
155
  }
156
 
157
+ .stat-value {
158
+ font-size: 2rem;
159
+ font-weight: 600;
160
+ margin-bottom: 0.5rem;
161
+ color: var(--primary-color);
162
+ }
163
+
164
+ .stat-label {
165
+ color: var(--text-secondary);
166
+ font-size: 0.9rem;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.5px;
169
  }
170
 
171
+ .owner-section {
172
+ background: var(--card-background);
173
+ border-radius: 12px;
174
+ margin-bottom: 2rem;
175
+ border: 1px solid var(--border-color);
176
+ overflow: hidden;
177
  }
178
 
179
+ .owner-header {
180
+ padding: 1.5rem;
181
+ border-bottom: 1px solid var(--border-color);
182
  display: flex;
183
  justify-content: space-between;
184
+ align-items: center;
185
+ background: rgba(255, 255, 255, 0.02);
186
  }
187
 
188
+ .owner-name {
189
  font-size: 1.25rem;
190
  font-weight: 600;
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 0.5rem;
194
+ }
195
+
196
+ .status-stats {
197
+ display: flex;
198
+ gap: 1rem;
199
+ flex-wrap: wrap;
200
  }
201
 
202
  .status-badge {
 
204
  border-radius: 6px;
205
  font-size: 0.875rem;
206
  font-weight: 500;
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
  }
211
 
212
+ .space-grid {
213
+ display: grid;
214
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
215
+ gap: 1.5rem;
216
+ padding: 1.5rem;
217
  }
218
 
219
+ .space-card {
220
+ background: rgba(255, 255, 255, 0.02);
221
+ border-radius: 10px;
222
+ border: 1px solid var(--border-color);
223
+ overflow: hidden;
224
+ transition: all 0.3s ease;
225
  }
226
 
227
+ .space-card:hover {
228
+ transform: translateY(-2px);
229
+ border-color: var(--primary-color);
230
+ box-shadow: 0 0 20px rgba(0, 255, 157, 0.1);
231
  }
232
 
233
+ .space-header {
234
+ padding: 1rem;
235
+ background: rgba(255, 255, 255, 0.02);
236
+ border-bottom: 1px solid var(--border-color);
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
  }
241
 
242
+ .space-content {
243
+ padding: 1rem;
 
 
244
  }
245
 
246
  .space-info {
247
  display: grid;
248
  grid-template-columns: repeat(2, 1fr);
249
  gap: 1rem;
250
+ margin-bottom: 1rem;
251
  }
252
 
253
  .info-item {
 
257
  }
258
 
259
  .info-label {
 
260
  color: var(--text-secondary);
261
+ font-size: 0.8rem;
262
  text-transform: uppercase;
263
  letter-spacing: 0.5px;
264
  }
265
 
266
  .info-value {
 
267
  color: var(--text-primary);
268
+ font-size: 0.9rem;
269
+ }
270
+
271
+ .space-metrics {
272
+ padding: 1rem;
273
+ background: rgba(255, 255, 255, 0.02);
274
+ border-radius: 8px;
275
+ margin-bottom: 1rem;
276
+ display: flex;
277
+ justify-content: space-around;
278
+ }
279
+
280
+ .metric-item {
281
+ text-align: center;
282
+ }
283
+
284
+ .metric-value {
285
+ font-size: 1.25rem;
286
+ font-weight: 600;
287
+ color: var(--primary-color);
288
+ }
289
+
290
+ .metric-label {
291
+ font-size: 0.8rem;
292
+ color: var(--text-secondary);
293
  }
294
 
295
  .action-buttons {
296
  display: grid;
297
  grid-template-columns: repeat(3, 1fr);
298
  gap: 0.75rem;
299
+ padding: 1rem;
300
+ background: rgba(0, 0, 0, 0.2);
301
  }
302
 
303
  .action-button {
304
  padding: 0.75rem;
305
  border-radius: 6px;
306
+ font-size: 0.9rem;
307
  font-weight: 500;
 
 
 
308
  display: flex;
309
  align-items: center;
310
  justify-content: center;
311
  gap: 0.5rem;
312
+ cursor: pointer;
313
+ transition: all 0.3s ease;
314
  border: 1px solid var(--border-color);
315
  background: transparent;
316
  color: var(--text-primary);
317
+ text-decoration: none;
318
  }
319
 
320
  .action-button:hover {
321
  border-color: var(--primary-color);
322
+ background: rgba(0, 255, 157, 0.1);
323
  }
324
 
325
  .action-button.restart {
 
326
  border-color: var(--primary-color);
327
  color: var(--primary-color);
328
  }
329
 
330
  .action-button.restart:hover {
331
+ background: var(--primary-color);
332
  color: var(--background-color);
333
  }
334
 
335
+ .status-RUNNING {
336
+ background: rgba(0, 255, 157, 0.1);
337
+ border: 1px solid var(--success-color);
338
+ color: var(--success-color);
339
+ }
340
+
341
+ .status-BUILDING {
342
+ background: rgba(255, 157, 0, 0.1);
343
+ border: 1px solid var(--warning-color);
344
+ color: var(--warning-color);
345
+ }
346
+
347
+ .status-SLEEPING {
348
+ background: rgba(0, 255, 255, 0.1);
349
+ border: 1px solid var(--sleeping-color);
350
+ color: var(--sleeping-color);
351
+ }
352
+
353
+ .status-STOPPED {
354
+ background: rgba(127, 142, 163, 0.1);
355
+ border: 1px solid var(--text-secondary);
356
+ color: var(--text-secondary);
357
+ }
358
+
359
+ .status-FAILED {
360
+ background: rgba(255, 45, 85, 0.1);
361
+ border: 1px solid var(--danger-color);
362
+ color: var(--danger-color);
363
+ }
364
+
365
  .loading-overlay {
366
  position: fixed;
367
  top: 0;
 
377
  }
378
 
379
  .loading-spinner {
380
+ position: relative;
381
+ width: 60px;
382
+ height: 60px;
383
+ }
384
+
385
+ .loading-spinner::after {
386
+ content: '';
387
+ position: absolute;
388
+ top: 0;
389
+ left: 0;
390
+ width: 100%;
391
+ height: 100%;
392
  border-radius: 50%;
393
+ border: 3px solid var(--border-color);
394
+ border-top-color: var(--primary-color);
395
+ animation: spin 1s infinite linear;
396
  }
397
 
398
  @keyframes spin {
399
+ to { transform: rotate(360deg); }
 
400
  }
401
 
402
  @media (max-width: 768px) {
403
+ .header-content {
404
+ flex-direction: column;
405
+ gap: 1rem;
406
+ padding: 1rem;
407
  }
408
 
409
+ .nav-section {
410
+ width: 100%;
411
  flex-direction: column;
412
  gap: 1rem;
413
  }
414
 
415
+ .search-bar {
416
+ width: 100%;
417
+ }
418
+
419
+ .container {
420
+ padding: 1rem;
421
+ }
422
+
423
+ .stats-grid {
424
+ grid-template-columns: 1fr;
425
  }
426
 
427
  .space-grid {
 
431
  .space-info {
432
  grid-template-columns: 1fr;
433
  }
434
+
435
+ .status-stats {
436
+ flex-direction: column;
437
+ align-items: flex-start;
438
+ }
439
  }
440
  </style>
441
  </head>
 
445
  </div>
446
 
447
  <header class="header">
448
+ <div class="header-content">
449
+ <div class="nav-section">
450
+ <div class="logo">
451
+ <i class="fas fa-server"></i>
452
+ HF Space Manager
453
+ </div>
454
+ <div class="search-bar">
455
+ <i class="fas fa-search"></i>
456
+ <input type="text" placeholder="搜索 Spaces..." id="spaceSearch">
457
+ </div>
458
+ </div>
459
+ <div class="user-section">
460
+ <button class="theme-toggle" title="切换主题">
461
+ <i class="fas fa-moon"></i>
462
+ </button>
463
+ <a href="/logout" class="action-button">
464
+ <i class="fas fa-sign-out-alt"></i>
465
+ 退出
466
+ </a>
467
+ </div>
468
+ </div>
469
  </header>
470
 
471
  <div class="container">
 
477
  {% endif %}
478
  {% set _ = grouped_spaces[space.owner].append(space) %}
479
  {% endfor %}
480
+
481
+ <div class="dashboard-header">
482
+ <div class="stats-grid">
483
+ {% set total_spaces = spaces|length %}
484
+ {% set running_spaces = spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
485
+ {% set sleeping_spaces = spaces|selectattr('status', 'equalto', 'SLEEPING')|list|length %}
486
+ {% set stopped_spaces = spaces|selectattr('status', 'equalto', 'STOPPED')|list|length %}
487
+ {% set failed_spaces = spaces|selectattr('status', 'equalto', 'FAILED')|list|length %}
488
+
489
+ <div class="stat-card">
490
+ <div class="stat-value">{{ total_spaces }}</div>
491
+ <div class="stat-label">总空间数</div>
492
+ </div>
493
+ <div class="stat-card">
494
+ <div class="stat-value">{{ running_spaces }}</div>
495
+ <div class="stat-label">运行中</div>
496
+ </div>
497
+ <div class="stat-card">
498
+ <div class="stat-value">{{ sleeping_spaces }}</div>
499
+ <div class="stat-label">休眠中</div>
500
+ </div>
501
+ <div class="stat-card">
502
+ <div class="stat-value">{{ stopped_spaces }}</div>
503
+ <div class="stat-label">已停止</div>
504
+ </div>
505
+ <div class="stat-card">
506
+ <div class="stat-value">{{ failed_spaces }}</div>
507
+ <div class="stat-label">运行失败</div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
  {% for owner, owner_spaces in grouped_spaces.items() %}
513
+ {% set running_count = owner_spaces|selectattr('status', 'equalto', 'RUNNING')|list|length %}
514
+ {% set building_count = owner_spaces|selectattr('status', 'equalto', 'BUILDING')|list|length %}
515
+ {% set sleeping_count = owner_spaces|selectattr('status', 'equalto', 'SLEEPING')|list|length %}
516
+ {% set stopped_count = owner_spaces|selectattr('status', 'equalto', 'STOPPED')|list|length %}
517
+ {% set failed_count = owner_spaces|selectattr('status', 'equalto', 'FAILED')|list|length %}
518
+
519
  <div class="owner-section">
520
  <div class="owner-header">
521
  <div class="owner-name">
522
+ <i class="fas fa-user-circle"></i>
523
  {{ owner }}
524
  </div>
525
  <div class="status-stats">
526
+ <span class="status-badge status-RUNNING">
 
 
 
 
527
  <i class="fas fa-play-circle"></i>
528
  运行中: {{ running_count }}
529
+ </span>
530
+ <span class="status-badge status-SLEEPING">
531
  <i class="fas fa-moon"></i>
532
  休眠: {{ sleeping_count }}
533
+ </span>
534
+ <span class="status-badge status-STOPPED">
535
  <i class="fas fa-stop-circle"></i>
536
  停止: {{ stopped_count }}
537
+ </span>
538
+ <span class="status-badge status-FAILED">
539
  <i class="fas fa-exclamation-circle"></i>
540
  失败: {{ failed_count }}
541
+ </span>
542
  </div>
543
  </div>
544
+
545
  <div class="space-grid">
546
  {% for space in owner_spaces %}
547
  <div class="space-card" data-space-id="{{ space.repo_id }}">
548
  <div class="space-header">
549
+ <div class="space-name">
550
+ <i class="fas fa-cube"></i>
551
+ {{ space.name }}
 
 
 
 
 
 
 
 
 
 
 
 
552
  </div>
553
+ <span class="status-badge status-{{ space.status }}">
554
+ <i class="fas fa-circle"></i>
555
+ {{ space.status }}
556
+ </span>
557
+ </div>
558
+
559
+ <div class="space-content">
560
+ <div class="space-info">
561
+ <div class="info-item">
562
+ <span class="info-label">Space ID</span>
563
+ <span class="info-value">{{ space.repo_id }}</span>
564
+ </div>
565
+ <div class="info-item">
566
+ <span class="info-label">创建时间</span>
567
+ <span class="info-value">{{ space.created_at }}</span>
568
+ </div>
569
+ <div class="info-item">
570
+ <span class="info-label">最后修改</span>
571
+ <span class="info-value">{{ space.last_modified }}</span>
572
+ </div>
573
+ <div class="info-item">
574
+ <span class="info-label">应用端口</span>
575
+ <span class="info-value">{{ space.app_port }}</span>
576
+ </div>
577
  </div>
578
+
579
+ <div class="space-metrics">
580
+ <div class="metric-item">
581
+ <div class="metric-value">{{ space.sdk }}</div>
582
+ <div class="metric-label">SDK 版本</div>
583
+ </div>
584
+ <div class="metric-item">
585
+ <div class="metric-value">{{ '私有' if space.private else '公开' }}</div>
586
+ <div class="metric-label">访问权限</div>
587
+ </div>
588
  </div>
589
  </div>
590
+
591
  <div class="action-buttons">
592
+ <a href="{{ space.url }}" target="_blank" class="action-button">
593
+ <i class="fas fa-external-link-alt"></i>
594
+ 查看
595
  </a>
596
  <button onclick="confirmAction('restart', '{{ space.repo_id }}')" class="action-button restart">
597
+ <i class="fas fa-sync-alt"></i>
598
+ 重启
599
  </button>
600
+ <button onclick="confirmAction('rebuild', '{{ space.repo_id }}')" class="action-button">
601
+ <i class="fas fa-tools"></i>
602
+ 重建
603
  </button>
604
  </div>
605
  </div>
 
609
  {% endfor %}
610
  {% else %}
611
  <div class="owner-section">
612
+ <p style="text-align: center; padding: 2rem; color: var(--text-secondary);">
613
+ <i class="fas fa-info-circle"></i>
614
+ 没有找到任何 Spaces。请确保你的账户中有创建的 Spaces,并且提供的 token 有正确的权限。
615
  </p>
616
  </div>
617
  {% endif %}
 
621
  <script>
622
  const socket = io();
623
 
624
+ // 页面加载完成后隐藏加载动画
625
+ window.addEventListener('load', function() {
626
+ document.getElementById('loading').style.display = 'none';
627
+ });
628
+
629
+ // 搜索功能
630
+ document.getElementById('spaceSearch').addEventListener('input', function(e) {
631
+ const searchTerm = e.target.value.toLowerCase();
632
+ document.querySelectorAll('.space-card').forEach(card => {
633
+ const spaceName = card.querySelector('.space-name').textContent.toLowerCase();
634
+ const spaceId = card.dataset.spaceId.toLowerCase();
635
+ if (spaceName.includes(searchTerm) || spaceId.includes(searchTerm)) {
636
+ card.style.display = '';
637
+ } else {
638
+ card.style.display = 'none';
639
+ }
640
+ });
641
+ });
642
+
643
+ // 主题切换
644
+ const themeToggle = document.querySelector('.theme-toggle');
645
+ themeToggle.addEventListener('click', function() {
646
+ document.body.classList.toggle('light-theme');
647
+ const icon = this.querySelector('i');
648
+ icon.classList.toggle('fa-sun');
649
+ icon.classList.toggle('fa-moon');
650
+ });
651
+
652
+ // WebSocket 连接
653
  socket.on('connect', () => {
654
  console.log('Connected to server');
655
  });
 
669
  socket.connect();
670
  }
671
  });
 
 
 
 
672
 
673
  function updateSpaceStatuses() {
674
  document.querySelectorAll('.space-card').forEach(card => {
 
679
  const statusElement = card.querySelector('.status-badge');
680
  if (statusElement && data.status) {
681
  statusElement.className = `status-badge status-${data.status}`;
682
+ statusElement.innerHTML = `<i class="fas fa-circle"></i> ${data.status}`;
683
  }
684
  })
685
  .catch(error => console.error('Error updating status:', error));
 
694
  }
695
  }
696
 
697
+ // 每30秒更新状态
698
  setInterval(updateSpaceStatuses, 30000);
699
+
700
+ // 添加卡片动画
701
+ document.querySelectorAll('.space-card').forEach(card => {
702
+ card.addEventListener('mouseenter', function() {
703
+ this.style.transform = 'translateY(-5px)';
704
+ });
705
+
706
+ card.addEventListener('mouseleave', function() {
707
+ this.style.transform = 'translateY(0)';
708
+ });
709
+ });
710
  </script>
711
  </body>
712
  </html>