yangtb24 commited on
Commit
f4e96a0
·
verified ·
1 Parent(s): 25a13d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +240 -346
app.py CHANGED
@@ -3,216 +3,10 @@ import requests
3
  import json
4
  from threading import Lock
5
  import time
6
- from datetime import datetime, timedelta
7
 
8
  app = Flask(__name__)
9
 
10
- # Global variables to store metrics data, updated by background threads
11
- metrics_data = {}
12
- server_status = {} # {server_id: last_seen_timestamp}
13
- summary_data = {
14
- "totalServers": 0,
15
- "onlineServers": 0,
16
- "offlineServers": 0,
17
- "totalUpload": 0,
18
- "totalDownload": 0,
19
- }
20
- data_lock = Lock() # Lock for thread-safe access to metrics_data and server_status
21
- last_fetch_time = datetime.min # Initialize to a time in the distant past
22
-
23
- USERNAME = 'yangtb24' # Replace with your Hugging Face username
24
- FETCH_INTERVAL = 300 # Fetch instances every 5 minutes (seconds)
25
- SSE_UPDATE_INTERVAL = 2 # Update SSE connections and metrics every 2 seconds (for display)
26
- SSE_TIMEOUT = 10 # Consider a server offline after this many seconds without updates
27
-
28
-
29
- def fetch_instances():
30
- global last_fetch_time
31
- if datetime.now() - last_fetch_time < timedelta(seconds=FETCH_INTERVAL):
32
- return # Don't fetch if within the interval
33
-
34
- try:
35
- response = requests.get(f"https://huggingface.co/api/spaces?author={USERNAME}")
36
- response.raise_for_status() # Raise an exception for bad status codes
37
- user_instances = response.json()
38
- instance_ids = [instance["id"].split('/')[1] for instance in user_instances]
39
-
40
- with data_lock:
41
- # Remove old servers
42
- for server_id in list(server_status.keys()):
43
- if not any(server_id in instance_id for instance_id in instance_ids):
44
- del server_status[server_id]
45
- if server_id in metrics_data: # Check if server_id exists before deleting
46
- del metrics_data[server_id]
47
-
48
-
49
- # Add new connections (if any). Connections are made in the get_metrics_stream() function.
50
- for instance_id in instance_ids:
51
- if not any(instance_id in server_id for server_id in server_status):
52
- server_status[instance_id] = 0 # Initialize with 0 (will be updated by SSE)
53
-
54
- last_fetch_time = datetime.now()
55
-
56
- except requests.exceptions.RequestException as e:
57
- print(f"Error fetching instances: {e}")
58
-
59
-
60
- def format_bytes(bytes_value):
61
- if bytes_value == 0:
62
- return '0 B'
63
- k = 1024
64
- sizes = ['B', 'KB', 'MB', 'GB', 'TB']
65
- i = 0
66
- while bytes_value >= k and i < len(sizes) - 1:
67
- bytes_value /= k
68
- i += 1
69
- return f"{bytes_value:.2f} {sizes[i]}"
70
-
71
-
72
- def update_summary():
73
- global summary_data
74
- with data_lock:
75
- now = time.time()
76
- online_count = 0
77
- offline_count = 0
78
- total_upload = 0
79
- total_download = 0
80
-
81
- for server_id, last_seen in server_status.items():
82
- is_online = (now - last_seen) < SSE_TIMEOUT
83
-
84
- if is_online:
85
- online_count += 1
86
- if server_id in metrics_data: # Make sure server_id exists
87
- total_upload += metrics_data[server_id].get('tx_bps', 0)
88
- total_download += metrics_data[server_id].get('rx_bps', 0)
89
- else:
90
- offline_count += 1
91
-
92
-
93
- summary_data = {
94
- "totalServers": len(server_status),
95
- "onlineServers": online_count,
96
- "offlineServers": offline_count,
97
- "totalUpload": total_upload,
98
- "totalDownload": total_download,
99
- }
100
-
101
-
102
- def get_metrics_stream():
103
- """
104
- Generator function for the Server-Sent Events (SSE) stream.
105
- """
106
-
107
- while True:
108
- fetch_instances() # fetch instances periodically
109
-
110
- with data_lock:
111
- # Prepare data for all servers
112
- all_servers_data = []
113
- for server_id in server_status:
114
- instance_id = server_id #instance and server are the same here.
115
- owner = USERNAME
116
-
117
- if server_id not in metrics_data:
118
- # Attempt to establish initial connection (important for new instances)
119
- try:
120
- response = requests.get(f"https://api.hf.space/v1/{owner}/{instance_id}/live-metrics/sse", stream=True, timeout=5) #timeout added
121
- response.raise_for_status()
122
- if response.status_code == 200:
123
- # Read the first event to initialize
124
- first_event = next(response.iter_lines()).decode('utf-8')
125
- if first_event.startswith("event: metric"):
126
- data = json.loads(first_event.split("data: ", 1)[1])
127
- metrics_data[server_id] = data
128
- server_status[server_id] = time.time()
129
-
130
- except (requests.exceptions.RequestException, StopIteration, json.JSONDecodeError) as e:
131
- print(f"Initial connection failed for {server_id}: {e}")
132
- server_status[server_id] = 0 # Mark as potentially offline
133
- continue # Skip to the next server
134
-
135
- # Process existing connections and updates
136
- else:
137
- try:
138
- response = requests.get(f"https://api.hf.space/v1/{owner}/{instance_id}/live-metrics/sse", stream=True, timeout=5)
139
- response.raise_for_status()
140
-
141
- if response.status_code == 200:
142
- # Read the next event
143
- try:
144
- event = next(response.iter_lines()).decode('utf-8')
145
- if event.startswith("event: metric"):
146
- data = json.loads(event.split("data: ", 1)[1])
147
- metrics_data[server_id] = data
148
- server_status[server_id] = time.time()
149
-
150
- except StopIteration:
151
- # print(f"No data received for {server_id}, connection likely closed.")
152
- server_status[server_id] = 0 # Mark as potentially offline
153
- except (json.JSONDecodeError, requests.exceptions.RequestException) as e:
154
- print(f"Error processing SSE for {server_id}: {e}")
155
- server_status[server_id] = 0
156
- else:
157
- print(f"Failed to connect for {server_id}, status code: {response.status_code}")
158
- server_status[server_id] = 0
159
-
160
- except requests.exceptions.RequestException as e:
161
- print(f"Connection error for {server_id}: {e}")
162
- server_status[server_id] = 0 # Mark as potentially offline
163
-
164
-
165
-
166
- # Prepare data for this server (even if offline)
167
- if server_id in metrics_data:
168
- data = metrics_data[server_id]
169
- cpu_usage = data.get('cpu_usage_pct', 0)
170
- memory_used = data.get('memory_used_bytes', 0)
171
- memory_total = data.get('memory_total_bytes', 1) # Avoid division by zero
172
- memory_usage = (memory_used / memory_total) * 100 if memory_total > 0 else 0
173
- upload_bps = data.get('tx_bps', 0)
174
- download_bps = data.get('rx_bps', 0)
175
- else:
176
- # Default values if no data is available
177
- cpu_usage = 0
178
- memory_usage = 0
179
- upload_bps = 0
180
- download_bps = 0
181
-
182
- server_data = {
183
- "serverId": server_id,
184
- "owner": owner,
185
- "spaceId": instance_id,
186
- "isOnline": (time.time() - server_status.get(server_id, 0)) < SSE_TIMEOUT,
187
- "cpuUsage": f"{cpu_usage:.2f}",
188
- "memoryUsage": f"{memory_usage:.2f}",
189
- "upload": format_bytes(upload_bps),
190
- "download": format_bytes(download_bps),
191
- }
192
- all_servers_data.append(server_data)
193
-
194
-
195
- update_summary() # update summary after processing each server.
196
- # Include summary data in the SSE payload
197
- payload = {
198
- "servers": all_servers_data,
199
- "summary": summary_data,
200
- }
201
-
202
- yield f"data: {json.dumps(payload)}\n\n"
203
- time.sleep(SSE_UPDATE_INTERVAL)
204
-
205
-
206
- @app.route('/')
207
- def index():
208
- return render_template('index.html')
209
-
210
- @app.route('/metrics')
211
- def metrics():
212
- return Response(get_metrics_stream(), mimetype='text/event-stream')
213
-
214
-
215
- light_mode_style = """
216
  * {
217
  margin: 0;
218
  padding: 0;
@@ -403,147 +197,247 @@ body {
403
  }
404
  """
405
 
406
- @app.route('/test')
407
- def test():
408
- return "Test route is working!"
409
-
410
- # Integrated HTML template
411
- html_template = f"""
412
- <!DOCTYPE html>
413
- <html lang="zh">
414
- <head>
415
- <meta charset="UTF-8">
416
- <title>HF Space Monitor</title>
417
- <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
418
- <style>{light_mode_style}</style>
419
- </head>
420
- <body>
421
- <div class="container">
422
- <div class="overview">
423
- <div class="overview-title"><i class="fas fa-chart-line"></i>系统概览</div>
424
- <div id="summary">
425
- <div>总实例数: <span id="totalServers">0</span></div>
426
- <div>在线实例: <span id="onlineServers">0</span></div>
427
- <div>离线实例: <span id="offlineServers">0</span></div>
428
- <div>总上传: <span id="totalUpload">0 B/s</span></div>
429
- <div>总下载: <span id="totalDownload">0 B/s</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  </div>
431
- </div>
432
- <div id="servers" class="stats-container">
433
- <!-- 服务器卡片将在这里动态生成 -->
434
- </div>
435
- </div>
436
- <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js" integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3uWCGാരി7eIq7jWqVl3J8jL/kof/tfu9Xqzh/y/VM5sJd/tq5iEew==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
437
-
438
- <script>
439
- const username = 'yangtb24';
440
-
441
- function updateServerCard(serverData) {{
442
- const serverId = serverData.serverId;
443
- let serverElement = document.getElementById(`server-${{serverId}}`); // Corrected line
444
-
445
- if (!serverElement) {{
446
- const card = document.createElement('div');
447
- card.id = `server-${{serverId}}`; // Corrected line
448
- card.className = 'server-card';
449
- card.innerHTML = `
450
- <div class="server-header">
451
- <div class="server-name">
452
- <div class="status-dot status-offline"></div>
453
- <svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
454
- <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"/>
455
- </svg>
456
- <div>${{serverId}} (${{serverData.owner}}/${{serverData.spaceId}})</div>
457
- </div>
458
- </div>
459
- <div class="metric-grid">
460
- <div class="metric-item">
461
- <div class="metric-label">CPU</div>
462
- <div class="progress-bar-container">
463
- <div class="cpu-progress-bar"></div>
464
- </div>
465
- <div class="metric-value cpu-usage">0%</div>
466
- </div>
467
- <div class="metric-item">
468
- <div class="metric-label">内存</div>
469
- <div class="progress-bar-container">
470
- <div class="memory-progress-bar"></div>
471
- </div>
472
- <div class="metric-value memory-usage">0%</div>
473
- </div>
474
- <div class="metric-item">
475
- <div class="metric-label">上传</div>
476
- <div class="metric-value upload">0 KB/s</div>
477
- </div>
478
- <div class="metric-item">
479
- <div class="metric-label">下载</div>
480
- <div class="metric-value download">0 KB/s</div>
481
- </div>
482
- </div>
483
- `;
484
- document.getElementById('servers').appendChild(card);
485
- serverElement = card; // Update serverElement to the newly created card
486
- }}
487
-
488
- const statusDot = serverElement.querySelector('.status-dot');
489
- statusDot.className = `status-dot status-${{serverData.isOnline ? 'online' : 'offline'}}`;
490
-
491
- serverElement.querySelector('.cpu-usage').textContent = `${{serverData.cpuUsage}}%`;
492
- serverElement.querySelector('.cpu-progress-bar').style.width = `${{serverData.cpuUsage}}%`;
493
-
494
- serverElement.querySelector('.memory-usage').textContent = `${{serverData.memoryUsage}}%`;
495
- serverElement.querySelector('.memory-progress-bar').style.width = `${{serverData.memoryUsage}}%`;
496
-
497
- serverElement.querySelector('.upload').textContent = `${{serverData.upload}}/s`;
498
- serverElement.querySelector('.download').textContent = `${{serverData.download}}/s`;
499
- }}
500
-
501
-
502
- function updateSummary(summaryData) {{
503
- document.getElementById('totalServers').textContent = summaryData.totalServers;
504
- document.getElementById('onlineServers').textContent = summaryData.onlineServers;
505
- document.getElementById('offlineServers').textContent = summaryData.offlineServers;
506
- document.getElementById('totalUpload').textContent = `${{formatBytes(summaryData.totalUpload)}}/s`;
507
- document.getElementById('totalDownload').textContent = `${{formatBytes(summaryData.totalDownload)}}/s`;
508
- }}
509
-
510
- function formatBytes(bytes) {{
511
- if (bytes === 0) return '0 B';
512
- const k = 1024;
513
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
514
- const i = Math.floor(Math.log(bytes) / Math.log(k));
515
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
516
- }}
517
-
518
- function connectSSE() {{
519
- const eventSource = new EventSource('/metrics');
520
-
521
- eventSource.onmessage = function(event) {{
522
- const data = JSON.parse(event.data);
523
- data.servers.forEach(serverData => {{
524
- updateServerCard(serverData);
525
- }});
526
- updateSummary(data.summary);
527
- }};
528
-
529
- eventSource.onerror = function(err) {{
530
- console.error("EventSource failed:", err);
531
- eventSource.close();
532
- setTimeout(connectSSE, 5000); // Attempt to reconnect after 5 seconds
533
- }};
534
- }}
535
- connectSSE();
536
-
537
- </script>
538
- </body>
539
- </html>
540
- """
541
 
542
- @app.route('/index.html') # Explicitly serve index.html (for clarity)
543
- def index_html():
544
- return render_template('index.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
 
 
 
 
546
 
547
- if __name__ == '__main__':
548
- app.run(debug=True, host='0.0.0.0', port=7860)
549
 
 
3
  import json
4
  from threading import Lock
5
  import time
 
6
 
7
  app = Flask(__name__)
8
 
9
+ lightModeStyle = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  * {
11
  margin: 0;
12
  padding: 0;
 
197
  }
198
  """
199
 
200
+ # 全局变量,用于存储服务器数据
201
+ servers_data = {}
202
+ servers_data_lock = Lock() # 数据锁
203
+ username = 'yangtb24' # 替换为你的 Hugging Face 用户名
204
+
205
+ def fetch_instances(username):
206
+ """获取 Hugging Face Space 实例列表"""
207
+ try:
208
+ response = requests.get(f"https://huggingface.co/api/spaces?author={username}")
209
+ response.raise_for_status() # 抛出异常,如果请求失败
210
+ user_instances = response.json()
211
+ return [
212
+ {"id": instance["id"].split("/")[1], "owner": username}
213
+ for instance in user_instances
214
+ ]
215
+ except requests.RequestException as e:
216
+ print(f"获取实例列表失败:{e}")
217
+ return []
218
+
219
+
220
+ def format_bytes(bytes_num):
221
+ """格式化字节数"""
222
+ if bytes_num == 0:
223
+ return '0 B'
224
+ k = 1024
225
+ sizes = ['B', 'KB', 'MB', 'GB', 'TB']
226
+ i = 0
227
+ while bytes_num >= k:
228
+ bytes_num /= k
229
+ i += 1
230
+ return f"{bytes_num:.2f} {sizes[i]}"
231
+
232
+
233
+ def update_server_data(data, space_id, owner):
234
+ """更新服务器数据"""
235
+ global servers_data
236
+
237
+ server_id = data["replica"]
238
+ with servers_data_lock:
239
+ if server_id not in servers_data:
240
+ servers_data[server_id] = {
241
+ "space_id": space_id,
242
+ "owner": owner,
243
+ "last_seen": time.time(),
244
+ "cpu_usage": 0,
245
+ "memory_usage": 0,
246
+ "upload": 0,
247
+ "download": 0,
248
+ }
249
+
250
+ servers_data[server_id]["last_seen"] = time.time()
251
+ servers_data[server_id]["cpu_usage"] = data["cpu_usage_pct"]
252
+ servers_data[server_id]["memory_usage"] = (
253
+ (data["memory_used_bytes"] / data["memory_total_bytes"]) * 100
254
+ if data["memory_total_bytes"] > 0 else 0
255
+ )
256
+ servers_data[server_id]["upload"] = data["tx_bps"]
257
+ servers_data[server_id]["download"] = data["rx_bps"]
258
+
259
+
260
+ def get_server_cards_html():
261
+ """生成服务器卡片的 HTML"""
262
+ global servers_data
263
+
264
+ server_cards_html = ""
265
+ with servers_data_lock:
266
+ for server_id, data in servers_data.items():
267
+ is_online = (time.time() - data["last_seen"]) < 10
268
+ status_class = "status-online" if is_online else "status-offline"
269
+
270
+ server_cards_html += f"""
271
+ <div class="server-card" id="server-{server_id}">
272
+ <div class="server-header">
273
+ <div class="server-name">
274
+ <div class="status-dot {status_class}"></div>
275
+ <svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
276
+ <path d="M21 3H3C1.9 3 1 3.9 1 5v3c0 1.1.9 2 2 10h18c1.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"/>
277
+ </svg>
278
+ <div>{server_id} ({data['owner']}/{data['space_id']})</div>
279
+ </div>
280
+ </div>
281
+ <div class="metric-grid">
282
+ <div class="metric-item">
283
+ <div class="metric-label">CPU</div>
284
+ <div class="progress-bar-container">
285
+ <div class="cpu-progress-bar" style="width: {data['cpu_usage']:.2f}%"></div>
286
+ </div>
287
+ <div class="metric-value cpu-usage">{data['cpu_usage']:.2f}%</div>
288
+ </div>
289
+ <div class="metric-item">
290
+ <div class="metric-label">内存</div>
291
+ <div class="progress-bar-container">
292
+ <div class="memory-progress-bar" style="width: {data['memory_usage']:.2f}%"></div>
293
+ </div>
294
+ <div class="metric-value memory-usage">{data['memory_usage']:.2f}%</div>
295
+ </div>
296
+ <div class="metric-item">
297
+ <div class="metric-label">上传</div>
298
+ <div class="metric-value upload">{format_bytes(data['upload'])}/s</div>
299
+ </div>
300
+ <div class="metric-item">
301
+ <div class="metric-label">下载</div>
302
+ <div class="metric-value download">{format_bytes(data['download'])}/s</div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ """
307
+ return server_cards_html
308
+
309
+
310
+ def get_summary_html():
311
+ """生成概览的 HTML"""
312
+ global servers_data
313
+
314
+ total_servers = 0
315
+ online_servers = 0
316
+ offline_servers = 0
317
+ total_upload = 0
318
+ total_download = 0
319
+
320
+ with servers_data_lock:
321
+ total_servers = len(servers_data)
322
+ for server_id, data in servers_data.items():
323
+ if (time.time() - data["last_seen"]) < 10:
324
+ online_servers += 1
325
+ total_upload += data["upload"]
326
+ total_download += data["download"]
327
+ else:
328
+ offline_servers += 1
329
+
330
+ return f"""
331
+ <div>总实例数: <span id="totalServers">{total_servers}</span></div>
332
+ <div>在线实例: <span id="onlineServers">{online_servers}</span></div>
333
+ <div>离线实例: <span id="offlineServers">{offline_servers}</span></div>
334
+ <div>总上传: <span id="totalUpload">{format_bytes(total_upload)}/s</span></div>
335
+ <div>总下载: <span id="totalDownload">{format_bytes(total_download)}/s</span></div>
336
+ """
337
+
338
+ @app.route("/")
339
+ def home():
340
+ """主页"""
341
+ global servers_data, username
342
+ # 获取实例
343
+ instances = fetch_instances(username)
344
+
345
+ # 构建 HTML 页面
346
+ html = f"""
347
+ <!DOCTYPE html>
348
+ <html lang="zh">
349
+ <head>
350
+ <meta charset="UTF-8">
351
+ <title>HF Space Monitor</title>
352
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
353
+ <style>{lightModeStyle}</style>
354
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js" integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3uWCGാരി7eIq7jWqVl3J8jL/kof/tfu9Xqzh/y/VM5sJd/tq5iEew==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
355
+
356
+ </head>
357
+ <body>
358
+ <div class="container">
359
+ <div class="overview">
360
+ <div class="overview-title"><i class="fas fa-chart-line"></i>系统概览</div>
361
+ <div id="summary">
362
+ {get_summary_html()}
363
+ </div>
364
+ </div>
365
+ <div id="servers" class="stats-container">
366
+ {get_server_cards_html()}
367
+ </div>
368
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ </body>
371
+ </html>
372
+ """
373
+ return render_template_string(html)
374
+
375
+ @app.route("/metrics")
376
+ def metrics():
377
+ """SSE 端点"""
378
+ global username
379
+
380
+ def generate():
381
+ instances = fetch_instances(username)
382
+ # 使用字典推导式来创建连接
383
+ event_sources = {
384
+ instance["id"]: requests.get(
385
+ f"https://api.hf.space/v1/{username}/{instance['id']}/live-metrics/sse",
386
+ stream=True
387
+ )
388
+ for instance in instances
389
+ }
390
+ try:
391
+ while True: # 外层循环,处理重连
392
+ for instance in instances:
393
+ space_id = instance["id"]
394
+
395
+ try:
396
+ # 检查连接是否还活跃, 如果不活跃, 重新建立连接
397
+ if event_sources[space_id].raw.closed:
398
+ print(f"Reconnecting to {space_id}...")
399
+ event_sources[space_id] = requests.get(
400
+ f"https://api.hf.space/v1/{username}/{space_id}/live-metrics/sse",
401
+ stream=True
402
+ )
403
+
404
+ # 读取一行数据
405
+ line = event_sources[space_id].raw.readline()
406
+
407
+ if not line: # 如果读取到空行,跳过
408
+ continue
409
+
410
+ line = line.decode("utf-8").strip()
411
+
412
+ if line.startswith("event: metric"):
413
+ data_line = event_sources[space_id].raw.readline().decode("utf-8").strip()
414
+ if data_line.startswith("data: "):
415
+ try:
416
+ data = json.loads(data_line[6:])
417
+ update_server_data(data, space_id, username)
418
+ # print(f"Received data for {space_id}: {data}") # 调试输出
419
+ yield f"data: {json.dumps({'status': 'updated'})}\\n\\n" # 发送确认消息
420
+ except json.JSONDecodeError:
421
+ print(f"Error decoding JSON for {space_id}: {data_line}")
422
+ except requests.exceptions.RequestException as e:
423
+ print(f"Connection error for {space_id}: {e}")
424
+ # 在这里可以添加重试逻辑, 例如等待一段时间后重新连接
425
+
426
+ time.sleep(2) # 短暂休眠,避免过于频繁的轮询
427
+
428
+ finally: # 确保关闭所有连接
429
+ for es in event_sources.values():
430
+ es.close()
431
+ print("All EventSources closed.")
432
+
433
+
434
+ return Response(generate(), mimetype="text/event-stream")
435
+
436
 
437
+ def render_template_string(html_string):
438
+ """渲染 HTML 字符串"""
439
+ return Response(html_string, mimetype="text/html")
440
 
441
+ if __name__ == "__main__":
442
+ app.run(debug=True, host="0.0.0.0", port=7860) # 部署到hf,port用7860
443