yangtb24 commited on
Commit
8a62759
·
verified ·
1 Parent(s): b5ebe66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -135
app.py CHANGED
@@ -106,6 +106,8 @@ body {
106
  width: 20px;
107
  height: 20px;
108
  border-radius: 4px;
 
 
109
  }
110
  .metric-grid {
111
  display: grid;
@@ -142,12 +144,12 @@ body {
142
  }
143
  .status-online {
144
  background-color: #2ecc71;
145
- color: #2ecc71;
146
  box-shadow: 0 0 5px rgba(46, 204, 113, 0.4);
147
  }
148
  .status-offline {
149
  background-color: #e74c3c;
150
- color: #e74c3c;
151
  box-shadow: 0 0 5px rgba(231, 76, 60, 0.4);
152
  }
153
  @keyframes fadeIn {
@@ -196,18 +198,15 @@ 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
- <!-- Use a CDN for Font Awesome (more reliable) -->
207
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
208
- <style>
209
- {lightModeStyle} /* Include styles directly in a <style> tag */
210
- </style>
211
  </head>
212
  <body>
213
  <div class="container">
@@ -224,9 +223,8 @@ htmlTemplate = f"""
224
  <div id="servers" class="stats-container">
225
  </div>
226
  </div>
227
-
228
  <script>
229
- const username = '{{username}}';
230
 
231
  async function fetchInstances() {{
232
  try {{
@@ -239,58 +237,49 @@ htmlTemplate = f"""
239
  }}
240
  }}
241
 
242
- class MetricsManager {{
243
  constructor() {{
244
  this.eventSources = new Map();
245
- this.servers = new Map();
246
  this.instanceOwners = new Map();
247
  this.spaceIds = new Map();
248
  }}
249
 
250
- connect(instanceId, username) {
251
  if (this.eventSources.has(instanceId)) return;
252
 
253
- try {
254
- const eventSource = new EventSource(`/metrics/${username}/${instanceId}`);
255
-
256
- this.spaceIds.set(instanceId, instanceId);
257
- this.instanceOwners.set(instanceId, username);
258
- this.servers.set(instanceId, { lastSeen: 0, uploadBps: 0, downloadBps: 0 });
259
-
260
- eventSource.onopen = () => {
261
- console.log(`EventSource connected: ${username}/${instanceId}`);
262
- };
263
-
264
- eventSource.addEventListener("metric", (event) => {
265
- try {
266
- const data = JSON.parse(event.data);
267
- this.servers.set(instanceId, {
268
- lastSeen: Date.now(),
269
- uploadBps: data.tx_bps,
270
- downloadBps: data.rx_bps,
271
- });
272
- updateServerCard(data, instanceId);
273
-
274
- } catch (error) {
275
- console.error(`解析数据失败 (${instanceId}):`, error);
276
- }
277
- });
278
-
279
- eventSource.onerror = (error) => {
280
- console.error(`EventSource error (${instanceId}):`, error);
281
- eventSource.close();
282
- this.eventSources.delete(instanceId);
283
- };
284
-
285
- this.eventSources.set(instanceId, eventSource);
286
- } catch (error) {
287
- console.error(`连接失败 (${username}/${instanceId}):`, error);
288
- }
289
- }
290
 
291
  disconnectAll() {{
292
  this.eventSources.forEach(es => es.close());
293
  this.eventSources.clear();
 
294
  }}
295
  }}
296
 
@@ -303,38 +292,39 @@ htmlTemplate = f"""
303
  }});
304
  }}
305
 
 
 
 
 
306
 
307
- function updateServerCard(data, spaceId) {
308
- const serverId = data.replica;
309
- const serverElement = document.getElementById(`server-${serverId}`);
310
- const owner = metricsManager.instanceOwners.get(spaceId);
311
-
312
- if (!serverElement) {
313
- const card = document.createElement('div');
314
- card.id = `server-${serverId}`;
315
- card.className = 'server-card';
316
- card.innerHTML = `
317
  <div class="server-header">
318
  <div class="server-name">
319
  <div class="status-dot status-online"></div>
320
- <!-- Removed the placeholder SVG, Font Awesome will handle it -->
321
- <div>${serverId} (${owner}/${spaceId})</div>
 
 
322
  </div>
323
  </div>
324
  <div class="metric-grid">
325
  <div class="metric-item">
326
- <div class="metric-label">CPU</div>
327
- <div class="progress-bar-container">
328
- <div class="cpu-progress-bar"></div>
329
- </div>
330
- <div class="metric-value cpu-usage">0%</div>
331
  </div>
332
  <div class="metric-item">
333
- <div class="metric-label">内存</div>
334
- <div class="progress-bar-container">
335
- <div class="memory-progress-bar"></div>
336
- </div>
337
- <div class="metric-value memory-usage">0%</div>
338
  </div>
339
  <div class="metric-item">
340
  <div class="metric-label">上传</div>
@@ -346,55 +336,55 @@ htmlTemplate = f"""
346
  </div>
347
  </div>
348
  `;
349
- document.getElementById('servers').appendChild(card);
350
- }
351
 
352
- const card = document.getElementById(`server-${serverId}`);
353
  const cpuUsage = data.cpu_usage_pct;
354
  const memoryUsage = (data.memory_used_bytes / data.memory_total_bytes) * 100;
355
  const uploadBps = data.tx_bps;
356
  const downloadBps = data.rx_bps;
357
 
358
- card.querySelector('.cpu-usage').textContent = `${cpuUsage.toFixed(2)}%`;
359
- card.querySelector('.cpu-progress-bar').style.width = `${cpuUsage}%`;
360
 
361
- card.querySelector('.memory-usage').textContent = `${memoryUsage.toFixed(2)}%`;
362
- card.querySelector('.memory-progress-bar').style.width = `${memoryUsage}%`;
363
 
364
- card.querySelector('.upload').textContent = `${formatBytes(uploadBps)}/s`;
365
- card.querySelector('.download').textContent = `${formatBytes(downloadBps)}/s`;
366
  updateSummary();
367
- }
368
 
369
  function updateSummary() {{
370
- const now = Date.now();
371
- let online = 0;
372
- let offline = 0;
373
- let totalUpload = 0;
374
- let totalDownload = 0;
375
-
376
- for (const [serverId, serverData] of metricsManager.servers) {{
377
- const isOnline = (now - serverData.lastSeen) < 10000;
378
- const serverCard = document.getElementById(`server-${{serverId}}`);
379
-
380
- if (serverCard) {{
381
- const statusDot = serverCard.querySelector('.status-dot');
382
- statusDot.className = `status-dot status-${{isOnline ? 'online' : 'offline'}}`;
383
- }}
384
-
385
- if (isOnline) {{
386
- online++;
387
- totalUpload += serverData.uploadBps || 0;
388
- totalDownload += serverData.downloadBps || 0;
389
- }} else {{
390
- offline++;
391
- }}
392
  }}
393
- document.getElementById('totalServers').textContent = metricsManager.servers.size;
 
 
 
 
 
 
 
 
 
 
394
  document.getElementById('onlineServers').textContent = online;
395
  document.getElementById('offlineServers').textContent = offline;
396
- document.getElementById('totalUpload').textContent = `${formatBytes(totalUpload)}/s`;
397
- document.getElementById('totalDownload').textContent = `${formatBytes(totalDownload)}/s`;
398
  }}
399
 
400
  function formatBytes(bytes) {{
@@ -408,22 +398,23 @@ htmlTemplate = f"""
408
  initialize();
409
  setInterval(updateSummary, 2000);
410
  setInterval(async () => {{
411
- metricsManager.disconnectAll();
412
- await initialize();
413
  }}, 300000);
414
-
415
  </script>
416
  </body>
417
  </html>
418
- """
 
 
419
 
420
  @app.route('/')
421
  def index():
422
- return render_template_string(htmlTemplate, username=os.environ.get("USERNAME", "yangtb24"))
423
 
424
  @app.route('/instances')
425
  def get_instances():
426
- instances = fetch_instances(os.environ.get("USERNAME", "yangtb24"))
427
  return jsonify(instances)
428
 
429
  @app.route('/metrics/<username>/<instance_id>')
@@ -433,39 +424,43 @@ def stream_metrics(username, instance_id):
433
  def generate():
434
  try:
435
  response = requests.get(url, stream=True, headers={"Accept": "text/event-stream"}, timeout=15)
436
- response.raise_for_status()
437
 
 
438
  for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
439
  if chunk:
440
- # Simplfied event parsing, as we now *know* we're getting valid JSON
441
- if chunk.startswith("event: metric"):
442
- try:
443
- data_str = chunk.split("data:", 1)[1].strip()
444
- data_json = json.loads(data_str)
445
- yield f"event: metric\ndata: {json.dumps(data_json)}\n\n"
446
- except (IndexError, json.JSONDecodeError) as e:
447
- print(f"Error parsing SSE chunk: {e}")
448
- continue # Skip to the next chunk on error
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  except requests.exceptions.RequestException as e:
450
  print(f"Request Exception: {e}")
451
- yield f"event: error\ndata: Connection error: {e}\n\n"
452
  except Exception as e:
453
  print(f"An error occurred: {e}")
454
- yield f"event: error\ndata: An error occurred: {e}\n\n"
455
-
456
- return Response(generate(), mimetype='text/event-stream')
457
 
458
 
 
459
 
460
- def fetch_instances(username):
461
- try:
462
- response = requests.get(f"https://huggingface.co/api/spaces?author={username}")
463
- response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
464
- user_instances = response.json()
465
- return [{"id": instance["id"].split('/')[1], "owner": username} for instance in user_instances]
466
- except requests.exceptions.RequestException as e:
467
- print(f"Error fetching instances: {e}")
468
- return []
469
 
470
  if __name__ == '__main__':
471
  app.run(debug=True, host='0.0.0.0', port=7860)
 
106
  width: 20px;
107
  height: 20px;
108
  border-radius: 4px;
109
+ /* You might want to add: */
110
+ /* fill: currentColor; */
111
  }
112
  .metric-grid {
113
  display: grid;
 
144
  }
145
  .status-online {
146
  background-color: #2ecc71;
147
+ /* color: #2ecc71; <-- This is redundant, as background-color sets the color */
148
  box-shadow: 0 0 5px rgba(46, 204, 113, 0.4);
149
  }
150
  .status-offline {
151
  background-color: #e74c3c;
152
+ /* color: #e74c3c; <-- Redundant */
153
  box-shadow: 0 0 5px rgba(231, 76, 60, 0.4);
154
  }
155
  @keyframes fadeIn {
 
198
  }
199
  """
200
 
201
+ htmlTemplate = """
202
  <!DOCTYPE html>
203
  <html lang="zh">
204
  <head>
205
  <meta charset="UTF-8">
206
  <title>HF Space Monitor</title>
207
  <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>">
208
+ <style>{}</style>
209
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
 
 
 
210
  </head>
211
  <body>
212
  <div class="container">
 
223
  <div id="servers" class="stats-container">
224
  </div>
225
  </div>
 
226
  <script>
227
+ const username = '{}';
228
 
229
  async function fetchInstances() {{
230
  try {{
 
237
  }}
238
  }}
239
 
240
+ class MetricsManager {{
241
  constructor() {{
242
  this.eventSources = new Map();
243
+ this.serversData = new Map(); // Use a consistent name
244
  this.instanceOwners = new Map();
245
  this.spaceIds = new Map();
246
  }}
247
 
248
+ connect(instanceId, username) {{
249
  if (this.eventSources.has(instanceId)) return;
250
 
251
+ const eventSource = new EventSource(`/metrics/${{username}}/${{instanceId}}`);
252
+ this.eventSources.set(instanceId, eventSource);
253
+ this.spaceIds.set(instanceId, instanceId);
254
+ this.instanceOwners.set(instanceId, username);
255
+ this.serversData.set(instanceId, {{ lastSeen: 0, uploadBps: 0, downloadBps: 0 }});
256
+
257
+ eventSource.addEventListener("metric", (event) => {{
258
+ try {{
259
+ const data = JSON.parse(event.data);
260
+ this.serversData.set(instanceId, {{
261
+ lastSeen: Date.now(),
262
+ uploadBps: data.tx_bps,
263
+ downloadBps: data.rx_bps,
264
+ }});
265
+ updateServerCard(data, instanceId);
266
+ }} catch (error) {{
267
+ console.error(`解析数据失败 (${{instanceId}}):`, error);
268
+ }}
269
+ }});
270
+
271
+ eventSource.onerror = (error) => {{
272
+ console.error(`EventSource 错误 (${{instanceId}}):`, error);
273
+ eventSource.close();
274
+ this.eventSources.delete(instanceId); // Remove on error
275
+ }};
276
+
277
+ }}
 
 
 
 
 
 
 
 
 
 
278
 
279
  disconnectAll() {{
280
  this.eventSources.forEach(es => es.close());
281
  this.eventSources.clear();
282
+ this.serversData.clear(); // Clear server data on disconnect
283
  }}
284
  }}
285
 
 
292
  }});
293
  }}
294
 
295
+ function updateServerCard(data, spaceId) {{
296
+ const serverId = data.replica;
297
+ let serverElement = document.getElementById(`server-${{serverId}}`);
298
+ const owner = metricsManager.instanceOwners.get(spaceId);
299
 
300
+ if (!serverElement) {{
301
+ serverElement = document.createElement('div');
302
+ serverElement.id = `server-${{serverId}}`;
303
+ serverElement.className = 'server-card';
304
+ serverElement.innerHTML = `
 
 
 
 
 
305
  <div class="server-header">
306
  <div class="server-name">
307
  <div class="status-dot status-online"></div>
308
+ <svg class="server-flag" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
309
+ <path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 16H4V5h16v14z"/>
310
+ </svg>
311
+ <div>${{serverId}} (${{owner}}/${{spaceId}})</div>
312
  </div>
313
  </div>
314
  <div class="metric-grid">
315
  <div class="metric-item">
316
+ <div class="metric-label">CPU</div>
317
+ <div class="progress-bar-container">
318
+ <div class="cpu-progress-bar"></div>
319
+ </div>
320
+ <div class="metric-value cpu-usage">0%</div>
321
  </div>
322
  <div class="metric-item">
323
+ <div class="metric-label">内存</div>
324
+ <div class="progress-bar-container">
325
+ <div class="memory-progress-bar"></div>
326
+ </div>
327
+ <div class="metric-value memory-usage">0%</div>
328
  </div>
329
  <div class="metric-item">
330
  <div class="metric-label">上传</div>
 
336
  </div>
337
  </div>
338
  `;
339
+ document.getElementById('servers').appendChild(serverElement);
340
+ }}
341
 
 
342
  const cpuUsage = data.cpu_usage_pct;
343
  const memoryUsage = (data.memory_used_bytes / data.memory_total_bytes) * 100;
344
  const uploadBps = data.tx_bps;
345
  const downloadBps = data.rx_bps;
346
 
347
+ serverElement.querySelector('.cpu-usage').textContent = `${{cpuUsage.toFixed(2)}}%`;
348
+ serverElement.querySelector('.cpu-progress-bar').style.width = `${{cpuUsage}}%`;
349
 
350
+ serverElement.querySelector('.memory-usage').textContent = `${{memoryUsage.toFixed(2)}}%`;
351
+ serverElement.querySelector('.memory-progress-bar').style.width = `${{memoryUsage}}%`;
352
 
353
+ serverElement.querySelector('.upload').textContent = `${{formatBytes(uploadBps)}}/s`;
354
+ serverElement.querySelector('.download').textContent = `${{formatBytes(downloadBps)}}/s`;
355
  updateSummary();
356
+ }}
357
 
358
  function updateSummary() {{
359
+ const now = Date.now();
360
+ let online = 0;
361
+ let offline = 0;
362
+ let totalUpload = 0;
363
+ let totalDownload = 0;
364
+
365
+ for (const [serverId, serverData] of metricsManager.serversData) {{
366
+ const isOnline = (now - serverData.lastSeen) < 10000;
367
+ const serverCard = document.getElementById(`server-${{serverId}}`);
368
+
369
+ if (serverCard) {{
370
+ const statusDot = serverCard.querySelector('.status-dot');
371
+ statusDot.className = `status-dot status-${{isOnline ? 'online' : 'offline'}}`;
 
 
 
 
 
 
 
 
 
372
  }}
373
+
374
+ if (isOnline) {{
375
+ online++;
376
+ totalUpload += serverData.uploadBps || 0;
377
+ totalDownload += serverData.downloadBps || 0;
378
+ }} else {{
379
+ offline++;
380
+ }}
381
+ }}
382
+
383
+ document.getElementById('totalServers').textContent = metricsManager.serversData.size;
384
  document.getElementById('onlineServers').textContent = online;
385
  document.getElementById('offlineServers').textContent = offline;
386
+ document.getElementById('totalUpload').textContent = `${{formatBytes(totalUpload)}}/s`;
387
+ document.getElementById('totalDownload').textContent = `${{formatBytes(totalDownload)}}/s`;
388
  }}
389
 
390
  function formatBytes(bytes) {{
 
398
  initialize();
399
  setInterval(updateSummary, 2000);
400
  setInterval(async () => {{
401
+ metricsManager.disconnectAll();
402
+ await initialize();
403
  }}, 300000);
 
404
  </script>
405
  </body>
406
  </html>
407
+ """.format(lightModeStyle, USERNAME)
408
+
409
+
410
 
411
  @app.route('/')
412
  def index():
413
+ return render_template_string(htmlTemplate)
414
 
415
  @app.route('/instances')
416
  def get_instances():
417
+ instances = fetch_instances(USERNAME)
418
  return jsonify(instances)
419
 
420
  @app.route('/metrics/<username>/<instance_id>')
 
424
  def generate():
425
  try:
426
  response = requests.get(url, stream=True, headers={"Accept": "text/event-stream"}, timeout=15)
427
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
428
 
429
+ buffer = ""
430
  for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
431
  if chunk:
432
+ buffer += chunk
433
+ while "\n\n" in buffer:
434
+ event_data, buffer = buffer.split("\n\n", 1)
435
+ lines = event_data.split("\n")
436
+ event_type = "message" # Default event type
437
+ data_lines = []
438
+
439
+ for line in lines:
440
+ if line.startswith("event:"):
441
+ event_type = line.split(":", 1)[1].strip()
442
+ elif line.startswith("data:"):
443
+ data_lines.append(line.split(":", 1)[1].strip())
444
+
445
+ if event_type == "metric":
446
+ try:
447
+ # Parse and re-serialize to ensure valid JSON
448
+ json_data = json.loads("".join(data_lines))
449
+ yield f"event: {event_type}\ndata: {json.dumps(json_data)}\n\n"
450
+ except json.JSONDecodeError as e:
451
+ print(f"JSONDecodeError: {e}")
452
+ yield f"event: error\ndata: Invalid JSON received\n\n"
453
+
454
  except requests.exceptions.RequestException as e:
455
  print(f"Request Exception: {e}")
456
+ yield f"event: error\ndata: Connection error\n\n" # Simplified error message
457
  except Exception as e:
458
  print(f"An error occurred: {e}")
459
+ yield f"event: error\ndata: An unexpected error occurred\n\n"
 
 
460
 
461
 
462
+ return Response(generate(), mimetype='text/event-stream')
463
 
 
 
 
 
 
 
 
 
 
464
 
465
  if __name__ == '__main__':
466
  app.run(debug=True, host='0.0.0.0', port=7860)