yangtb24 commited on
Commit
3be2cf1
·
verified ·
1 Parent(s): b80478e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +294 -288
app.py CHANGED
@@ -1,11 +1,218 @@
1
- from flask import Flask, render_template_string
2
  import requests
3
  import json
4
  from threading import Lock
 
 
5
 
6
  app = Flask(__name__)
7
 
8
- lightModeStyle = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  * {
10
  margin: 0;
11
  padding: 0;
@@ -196,14 +403,19 @@ body {
196
  }
197
  """
198
 
199
- htmlTemplate = f"""
 
 
 
 
 
200
  <!DOCTYPE html>
201
  <html lang="zh">
202
  <head>
203
  <meta charset="UTF-8">
204
  <title>HF Space Monitor</title>
205
  <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>">
206
- <style>{lightModeStyle}</style>
207
  </head>
208
  <body>
209
  <div class="container">
@@ -226,318 +438,112 @@ htmlTemplate = f"""
226
  <script>
227
  const username = 'yangtb24';
228
 
229
- async function fetchInstances() {{
230
- try {{
231
- const response = await fetch(`/instances`);
232
- const data = await response.json();
233
- return data.instances;
234
- }} catch (error) {{
235
- console.error("获取实例列表失败:", error);
236
- return [];
237
- }}
238
- }}
239
-
240
-
241
- class MetricsManager {{
242
- constructor() {{
243
- this.eventSources = new Map();
244
- this.servers = new Map();
245
- this.instanceOwners = new Map();
246
- this.spaceIds = new Map();
247
- this.lock = false;
248
- }}
249
-
250
-
251
- async connect(instanceId, username) {{
252
- if (this.eventSources.has(instanceId)) return;
253
-
254
- // Fetch initial metrics
255
- try {{
256
- const initialResponse = await fetch(`/metrics?instanceId=${instanceId}&username=${username}`);
257
- const initialData = await initialResponse.json();
258
-
259
- if(initialData && initialData.metrics) {{
260
- this.updateServerCard(initialData.metrics, instanceId, username);
261
- }}
262
- }}
263
- catch (error) {{
264
- console.error(`Initial metrics fetch failed (${instanceId}):`, error);
265
- }}
266
-
267
- try {{
268
- const eventSource = new EventSource(
269
- `/stream?instanceId=${instanceId}&username=${username}`
270
- );
271
-
272
- this.spaceIds.set(instanceId, instanceId);
273
- this.instanceOwners.set(instanceId, username);
274
-
275
- eventSource.addEventListener("metric", (event) => {{
276
- try {{
277
- const data = JSON.parse(event.data);
278
- this.updateServerCard(data, instanceId, username);
279
- }} catch (error) {{
280
- console.error(`解析数据失败 (${instanceId}):`, error);
281
- }}
282
- }});
283
-
284
- eventSource.onerror = (error) => {{
285
- console.error(`EventSource 错误 (${instanceId}):`, error);
286
- eventSource.close();
287
- this.eventSources.delete(instanceId);
288
- }};
289
-
290
- this.eventSources.set(instanceId, eventSource);
291
- }} catch (error) {{
292
- console.error(`连接失败 (${username}/${instanceId}):`, error);
293
- }}
294
- }}
295
-
296
-
297
- disconnectAll() {{
298
- this.eventSources.forEach(es => es.close());
299
- this.eventSources.clear();
300
- }}
301
-
302
- updateServerCard(data, spaceId, owner) {{
303
- if (!data || !data.replica) return;
304
-
305
- const serverId = data.replica;
306
- const serverElement = document.getElementById(`server-${serverId}`);
307
-
308
 
309
- if (!serverElement) {{
310
  const card = document.createElement('div');
311
  card.id = `server-${serverId}`;
312
  card.className = 'server-card';
313
  card.innerHTML = `
314
- <div class="server-header">
315
- <div class="server-name">
316
- <div class="status-dot status-online"></div>
317
- <svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
318
- <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"/>
319
- </svg>
320
- <div>${serverId} (${owner}/${spaceId})</div>
321
- </div>
322
- </div>
323
- <div class="metric-grid">
324
- <div class="metric-item">
325
- <div class="metric-label">CPU</div>
326
- <div class="progress-bar-container">
327
- <div class="cpu-progress-bar"></div>
328
  </div>
329
- <div class="metric-value cpu-usage">0%</div>
330
  </div>
331
- <div class="metric-item">
332
- <div class="metric-label">内存</div>
333
- <div class="progress-bar-container">
334
- <div class="memory-progress-bar"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  </div>
336
- <div class="metric-value memory-usage">0%</div>
337
- </div>
338
- <div class="metric-item">
339
- <div class="metric-label">上传</div>
340
- <div class="metric-value upload">0 KB/s</div>
341
- </div>
342
- <div class="metric-item">
343
- <div class="metric-label">下载</div>
344
- <div class="metric-value download">0 KB/s</div>
345
  </div>
346
- </div>
347
  `;
348
  document.getElementById('servers').appendChild(card);
349
- }}
350
-
351
- const card = document.getElementById(`server-${serverId}`);
352
- const cpuUsage = data.cpu_usage_pct;
353
- const memoryUsage = (data.memory_used_bytes / data.memory_total_bytes) * 100;
354
- const uploadBps = data.tx_bps;
355
- const downloadBps = data.rx_bps;
356
-
357
- card.querySelector('.cpu-usage').textContent = `${cpuUsage.toFixed(2)}%`;
358
- card.querySelector('.cpu-progress-bar').style.width = `${cpuUsage}%`;
359
 
360
- card.querySelector('.memory-usage').textContent = `${memoryUsage.toFixed(2)}%`;
361
- card.querySelector('.memory-progress-bar').style.width = `${memoryUsage}%`;
362
 
363
- card.querySelector('.upload').textContent = `${this.formatBytes(uploadBps)}/s`;
364
- card.querySelector('.download').textContent = `${this.formatBytes(downloadBps)}/s`;
365
 
366
- this.servers.set(serverId, Date.now());
367
- this.updateSummary();
368
- }}
369
 
370
- updateSummary() {{
371
- const now = Date.now();
372
- let online = 0;
373
- let offline = 0;
374
- let totalUpload = 0;
375
- let totalDownload = 0;
376
-
377
- this.servers.forEach((lastSeen, serverId) => {{
378
- const isOnline = (now - lastSeen) < 10000;
379
- const serverCard = document.getElementById(`server-${serverId}`);
380
- if (serverCard) {{
381
- const statusDot = serverCard.querySelector('.status-dot');
382
- statusDot.className = `status-dot status-${{isOnline ? 'online' : 'offline'}}`;
383
-
384
- if (isOnline) {{
385
- const uploadText = serverCard.querySelector('.upload').textContent;
386
- const downloadText = serverCard.querySelector('.download').textContent;
387
- totalUpload += parseFloat(uploadText) || 0;
388
- totalDownload += parseFloat(downloadText) || 0;
389
- }}
390
- }}
391
- isOnline ? online++ : offline++;
392
- }});
393
-
394
- document.getElementById('totalServers').textContent = this.servers.size;
395
- document.getElementById('onlineServers').textContent = online;
396
- document.getElementById('offlineServers').textContent = offline;
397
- document.getElementById('totalUpload').textContent = `${this.formatBytes(totalUpload)}/s`;
398
- document.getElementById('totalDownload').textContent = `${this.formatBytes(totalDownload)}/s`;
399
- }}
400
-
401
- formatBytes(bytes) {{
402
- if (bytes === 0) return '0 B';
403
- const k = 1024;
404
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
405
- const i = Math.floor(Math.log(bytes) / Math.log(k));
406
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
407
- }}
408
  }}
409
 
410
- const metricsManager = new MetricsManager();
411
 
412
- async function initialize() {{
413
- const instances = await fetchInstances();
414
- if (instances) {{
415
- instances.forEach(instance => {{
416
- metricsManager.connect(instance.id, instance.owner);
417
- }});
418
- }}
419
  }}
420
 
421
- initialize();
422
- setInterval(() => metricsManager.updateSummary(), 2000);
 
 
 
 
 
423
 
424
- // Re-initialize every 5 minutes
425
- setInterval(async () => {{
426
- metricsManager.disconnectAll();
427
- await initialize();
428
- }}, 300000);
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
  </script>
431
  </body>
432
  </html>
433
  """
434
 
435
- # Global variable to store instances and metrics. Using a dictionary is better
436
- # than separate Maps, as it allows atomic updates with a lock.
437
- data = {
438
- "instances": [],
439
- "metrics": {} # Key: instanceId, Value: {metrics: {}, last_updated: timestamp}
440
- }
441
- data_lock = Lock() # Use a lock for thread safety
442
-
443
- USERNAME = 'yangtb24'
444
-
445
- def fetch_instances(username):
446
- try:
447
- response = requests.get(f"https://huggingface.co/api/spaces?author={username}")
448
- response.raise_for_status() # Raise an exception for bad status codes
449
- user_instances = response.json()
450
- return [{"id": instance["id"].split('/')[1], "owner": username} for instance in user_instances]
451
- except requests.exceptions.RequestException as e:
452
- print(f"Error fetching instances: {e}")
453
- return []
454
-
455
- def fetch_initial_metrics(username, instance_id):
456
- """Fetches initial metrics using a regular GET request."""
457
- try:
458
- url = f"https://api.hf.space/v1/{username}/{instance_id}/live-metrics"
459
- response = requests.get(url)
460
- response.raise_for_status()
461
- return response.json()
462
- except requests.exceptions.RequestException as e:
463
- print(f"Error fetching initial metrics for {username}/{instance_id}: {e}")
464
- return None
465
 
466
 
467
- @app.route('/')
468
- def home():
469
- return render_template_string(htmlTemplate)
470
-
471
-
472
- @app.route('/instances')
473
- def get_instances():
474
- global data
475
- with data_lock:
476
- # Refresh instances every time /instances is called, but only if
477
- # they haven't been updated in the last 5 minutes.
478
- if not data["instances"] or (time.time() - data.get("instances_last_updated", 0) > 300):
479
- data["instances"] = fetch_instances(USERNAME)
480
- data["instances_last_updated"] = time.time() # Add timestamp
481
- return jsonify({"instances": data["instances"]})
482
-
483
- @app.route('/metrics')
484
- def get_metrics():
485
- """Route to get initial metrics for an instance."""
486
- instance_id = request.args.get('instanceId')
487
- username = request.args.get('username')
488
- if not instance_id or not username:
489
- return jsonify({"error": "instanceId and username are required"}), 400
490
-
491
- metrics = fetch_initial_metrics(username, instance_id)
492
- if metrics:
493
- return jsonify({"metrics": metrics})
494
- else:
495
- return jsonify({"error": "Failed to fetch initial metrics"}), 500
496
-
497
-
498
- @app.route('/stream')
499
- def stream():
500
- instance_id = request.args.get('instanceId')
501
- username = request.args.get('username')
502
- if not instance_id or not username:
503
- return "instanceId and username are required", 400
504
-
505
- def generate():
506
- global data
507
- try:
508
- url = f"https://api.hf.space/v1/{username}/{instance_id}/live-metrics/sse"
509
- with requests.get(url, stream=True) as r:
510
- r.raise_for_status()
511
- for line in r.iter_lines():
512
- if line:
513
- decoded_line = line.decode('utf-8')
514
- # Check if the line starts with "data:"
515
- if decoded_line.startswith("data:"):
516
- try:
517
- # Extract the JSON part of the message
518
- json_str = decoded_line.split("data:", 1)[1].strip()
519
- metric_data = json.loads(json_str)
520
-
521
- with data_lock:
522
- # Update the metrics and last_updated timestamp
523
- data["metrics"][instance_id] = {
524
- "metrics": metric_data,
525
- "last_updated": time.time()
526
- }
527
- yield f"data: {json.dumps(metric_data)}\n\n"
528
-
529
- except json.JSONDecodeError as e:
530
- print(f"JSONDecodeError: {e}, Line: {decoded_line}")
531
- # Optionally: yield an error message to the client
532
- # yield "data: {{'error': 'Invalid JSON'}}\n\n"
533
- except requests.exceptions.RequestException as e:
534
- print(f"RequestException during streaming: {e}")
535
- # yield "data: {{'error': 'Connection lost'}}\n\n" # Notify client
536
-
537
- return app.response_class(generate(), mimetype='text/event-stream')
538
-
539
- import time
540
-
541
  if __name__ == '__main__':
542
- app.run(debug=True, port=7860, host="0.0.0.0") # Hugging Face uses port 7860
543
 
 
1
+ from flask import Flask, render_template, Response
2
  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
  }
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">
 
438
  <script>
439
  const username = 'yangtb24';
440
 
441
+ function updateServerCard(serverData) {{
442
+ const serverId = serverData.serverId;
443
+ let serverElement = document.getElementById(`server-${serverId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
+ if (!serverElement) {{
446
  const card = document.createElement('div');
447
  card.id = `server-${serverId}`;
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