i0110 commited on
Commit
9a8b0d7
·
verified ·
1 Parent(s): f2128fb

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +558 -26
public/index.html CHANGED
@@ -2,11 +2,11 @@
2
  <html lang="zh">
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>HF Space Manager</title>
 
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
8
  <style>
9
- /* 样式部分保持不变,与原代码一致 */
10
  * {
11
  margin: 0;
12
  padding: 0;
@@ -50,7 +50,502 @@
50
  --action-button-bg: #e0e0e0;
51
  --action-button-hover: #d0d0d0;
52
  }
53
- /* 其他样式保持不变,略过... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  </style>
55
  </head>
56
  <body>
@@ -90,6 +585,7 @@
90
  <div>总下载: <span id="totalDownload">0 B/s</span></div>
91
  </div>
92
  </div>
 
93
  <div class="filter-sort-panel">
94
  <div class="filter-sort-group">
95
  <label for="statusFilter">过滤状态:</label>
@@ -290,7 +786,7 @@
290
  document.getElementById('loginButton').style.display = 'none';
291
  document.getElementById('logoutButton').style.display = 'block';
292
  updateActionButtons(true);
293
- refreshData();
294
  } else {
295
  console.log('登录失败:', data.message);
296
  loginError.textContent = data.message || '登录失败';
@@ -325,7 +821,7 @@
325
  document.getElementById('loginButton').style.display = 'block';
326
  document.getElementById('logoutButton').style.display = 'none';
327
  updateActionButtons(false);
328
- refreshData();
329
  })
330
  .catch(error => {
331
  hideLoading();
@@ -335,7 +831,7 @@
335
  document.getElementById('loginButton').style.display = 'block';
336
  document.getElementById('logoutButton').style.display = 'none';
337
  updateActionButtons(false);
338
- refreshData();
339
  });
340
  } else {
341
  console.log('本地无 token,直接设置为未登录');
@@ -343,7 +839,7 @@
343
  document.getElementById('loginButton').style.display = 'block';
344
  document.getElementById('logoutButton').style.display = 'none';
345
  updateActionButtons(false);
346
- refreshData();
347
  }
348
  }
349
 
@@ -356,11 +852,13 @@
356
  if (buttons) {
357
  buttons.style.display = loggedIn ? 'flex' : 'none';
358
  }
 
359
  if (loggedIn) {
360
  card.classList.remove('not-logged-in');
361
  } else {
362
  card.classList.add('not-logged-in');
363
  }
 
364
  const chartToggleButton = card.querySelector('.chart-toggle-button');
365
  if (chartToggleButton) {
366
  chartToggleButton.style.display = 'inline-block';
@@ -368,6 +866,7 @@
368
  });
369
  }
370
 
 
371
  window.onload = async function() {
372
  console.log('页面加载完成,开始检查登录状态');
373
  await checkLoginStatus();
@@ -375,6 +874,7 @@
375
  await initialize();
376
  };
377
 
 
378
  let pendingAction = null;
379
  let pendingRepoId = null;
380
 
@@ -415,6 +915,7 @@
415
  hideLoading();
416
  const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
417
  document.getElementById('totalUsers').textContent = usernamesList.length;
 
418
  const userFilter = document.getElementById('userFilter');
419
  userFilter.innerHTML = '<option value="all">全部用户</option>';
420
  usernamesList.forEach(username => {
@@ -471,6 +972,7 @@
471
 
472
  const instancesParam = subscribedInstances.join(',');
473
  const token = localStorage.getItem('authToken');
 
474
  const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token || '')}`;
475
  console.log('SSE 连接 URL:', url.split('&token=')[0] + (token ? '&token=... (隐藏)' : '&token=空'));
476
  this.eventSource = new EventSource(url);
@@ -489,6 +991,7 @@
489
  console.error(`SSE 连接错误:`, error);
490
  this.eventSource.close();
491
  this.eventSource = null;
 
492
  setTimeout(() => this.connect(subscribedInstances), 5000);
493
  };
494
 
@@ -507,8 +1010,8 @@
507
  const metricsStreamManager = new MetricsStreamManager();
508
  const instanceMap = new Map();
509
  const serverStatus = new Map();
510
- let allInstances = [];
511
- const chartInstances = new Map();
512
 
513
  async function initialize() {
514
  await getUsernames();
@@ -516,17 +1019,21 @@
516
  allInstances = instances;
517
  renderInstances(allInstances);
518
 
 
519
  const runningInstances = instances
520
  .filter(instance => instance.status.toLowerCase() === 'running')
521
  .map(instance => instance.repo_id);
522
 
523
  metricsStreamManager.connect(runningInstances);
 
524
  updateSummary();
525
  updateActionButtons(isLoggedIn);
526
  }
527
 
 
528
  async function refreshData() {
529
  metricsStreamManager.disconnect();
 
530
  chartInstances.forEach(chart => {
531
  if (chart) {
532
  chart.destroy();
@@ -534,14 +1041,15 @@
534
  });
535
  chartInstances.clear();
536
  await initialize();
537
- applyFiltersAndSort();
538
  }
539
 
540
  function renderInstances(instances) {
541
  const serversContainer = document.getElementById('servers');
542
- serversContainer.innerHTML = '';
543
  const userGroups = {};
544
 
 
545
  instances.forEach(instance => {
546
  if (!userGroups[instance.owner]) {
547
  userGroups[instance.owner] = [];
@@ -549,6 +1057,7 @@
549
  userGroups[instance.owner].push(instance);
550
  });
551
 
 
552
  Object.keys(userGroups).forEach(owner => {
553
  let userGroup = document.createElement('details');
554
  userGroup.className = 'user-group';
@@ -565,17 +1074,20 @@
565
 
566
  serversContainer.appendChild(userGroup);
567
 
 
568
  userGroups[owner].forEach(instance => {
569
  renderInstanceCard(instance, userServers);
570
  });
571
  });
572
  }
573
 
 
574
  function createChart(instanceId) {
575
  const canvasId = `chart-${instanceId}`;
576
  const canvas = document.getElementById(canvasId);
577
  if (!canvas) return null;
578
 
 
579
  const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
580
  const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
581
  const textColor = isDarkMode ? '#fff' : '#333';
@@ -584,12 +1096,12 @@
584
  const chart = new Chart(ctx, {
585
  type: 'line',
586
  data: {
587
- labels: Array(30).fill(''),
588
  datasets: [
589
  {
590
  label: 'CPU 使用率 (%)',
591
  data: [],
592
- borderColor: '#4CAF50',
593
  backgroundColor: 'rgba(76, 175, 80, 0.2)',
594
  tension: 0.4,
595
  fill: true,
@@ -597,7 +1109,7 @@
597
  {
598
  label: '内存使用率 (%)',
599
  data: [],
600
- borderColor: '#2196F3',
601
  backgroundColor: 'rgba(33, 150, 243, 0.2)',
602
  tension: 0.4,
603
  fill: true,
@@ -605,7 +1117,7 @@
605
  {
606
  label: '上传速度 (KB/s)',
607
  data: [],
608
- borderColor: '#F44336',
609
  backgroundColor: 'rgba(244, 67, 54, 0.2)',
610
  tension: 0.4,
611
  fill: true,
@@ -613,7 +1125,7 @@
613
  {
614
  label: '下载速度 (KB/s)',
615
  data: [],
616
- borderColor: '#FF9800',
617
  backgroundColor: 'rgba(255, 152, 0, 0.2)',
618
  tension: 0.4,
619
  fill: true,
@@ -658,34 +1170,38 @@
658
  },
659
  elements: {
660
  point: {
661
- radius: 0,
662
- hitRadius: 5
663
  }
664
  },
665
- animation: false
666
  }
667
  });
668
  chartInstances.set(instanceId, chart);
669
  return chart;
670
  }
671
 
 
672
  function updateChart(instanceId, data) {
673
  let chart = chartInstances.get(instanceId);
674
  if (!chart) {
675
  chart = createChart(instanceId);
676
- if (!chart) return;
677
  }
678
 
 
679
  const cpuData = chart.data.datasets[0].data;
680
  const memoryData = chart.data.datasets[1].data;
681
  const uploadData = chart.data.datasets[2].data;
682
  const downloadData = chart.data.datasets[3].data;
683
 
 
684
  cpuData.push(data.cpu_usage_pct);
685
  memoryData.push(((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2));
686
- uploadData.push((data.tx_bps / 1024).toFixed(2));
687
- downloadData.push((data.rx_bps / 1024).toFixed(2));
688
 
 
689
  if (cpuData.length > 30) {
690
  cpuData.shift();
691
  memoryData.shift();
@@ -693,6 +1209,7 @@
693
  downloadData.shift();
694
  }
695
 
 
696
  chart.update();
697
  }
698
 
@@ -707,22 +1224,23 @@
707
  card.id = cardId;
708
  card.className = 'server-card';
709
  if (!isLoggedIn) {
710
- card.classList.add('not-logged-in');
711
  }
 
712
  const iconSvg = instance.private
713
  ? `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
714
  <path d="M18 8v-3c0-1.656-1.344-3-3-3h-6c-1.656 0-3 1.344-3 3v3h-3v14h18v-14h-3zm-10-1.5c0-.828.672-1.5 1.5-1.5h5c.828 0 1.5.672 1.5 1.5v2.5h-8v-2.5zm4 11.5c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/>
715
- </svg>`
716
  : `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
717
  <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"/>
718
- </svg>`;
719
 
720
  card.innerHTML = `
721
  <div class="server-header">
722
  <div class="server-name">
723
  <div class="status-dot status-sleep"></div>
724
  ${iconSvg}
725
- <div>${instance.name}</div>
726
  </div>
727
  <div>
728
  <button class="chart-toggle-button" onclick="toggleChart('${instanceId}')">查看图表</button>
@@ -774,6 +1292,7 @@
774
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
775
  }
776
 
 
777
  function toggleChart(instanceId) {
778
  const card = document.getElementById(`instance-${instanceId}`);
779
  const chartContainer = document.getElementById(`chart-container-${instanceId}`);
@@ -786,6 +1305,7 @@
786
  } else {
787
  card.classList.add('expanded');
788
  toggleButton.textContent = '收起图表';
 
789
  if (!chartInstances.has(instanceId)) {
790
  createChart(instanceId);
791
  }
@@ -798,6 +1318,7 @@
798
  const instance = instanceMap.get(instanceId);
799
 
800
  if (!card && instance) {
 
801
  return;
802
  }
803
 
@@ -814,6 +1335,7 @@
814
  statusDot.className = 'status-dot status-online';
815
  isOnline = true;
816
  isSleep = false;
 
817
  updateChart(instanceId, data);
818
  } else {
819
  const currentStatus = instance?.status.toLowerCase() || 'unknown';
@@ -862,6 +1384,7 @@
862
  hideLoading();
863
  if (result.success) {
864
  alert(`重启成功: ${repoId}`);
 
865
  refreshData();
866
  } else {
867
  if (response.status === 401) {
@@ -904,6 +1427,7 @@
904
  hideLoading();
905
  if (result.success) {
906
  alert(`重建成功: ${repoId}`);
 
907
  refreshData();
908
  } else {
909
  if (response.status === 401) {
@@ -967,11 +1491,13 @@
967
  await initialize();
968
  }, 300000);
969
 
 
970
  function applyFiltersAndSort() {
971
  const statusFilter = document.getElementById('statusFilter').value;
972
  const userFilter = document.getElementById('userFilter').value;
973
  const sortBy = document.getElementById('sortBy').value;
974
 
 
975
  let filteredInstances = allInstances;
976
  if (statusFilter !== 'all') {
977
  filteredInstances = filteredInstances.filter(instance => instance.status.toLowerCase() === statusFilter);
@@ -980,6 +1506,7 @@
980
  filteredInstances = filteredInstances.filter(instance => instance.owner === userFilter);
981
  }
982
 
 
983
  filteredInstances.sort((a, b) => {
984
  if (sortBy === 'name-asc') {
985
  return a.name.localeCompare(b.name);
@@ -995,9 +1522,11 @@
995
  return 0;
996
  });
997
 
 
998
  instanceMap.clear();
999
  serverStatus.clear();
1000
  metricsStreamManager.disconnect();
 
1001
  chartInstances.forEach(chart => {
1002
  if (chart) {
1003
  chart.destroy();
@@ -1006,6 +1535,7 @@
1006
  chartInstances.clear();
1007
  renderInstances(filteredInstances);
1008
 
 
1009
  const runningInstances = filteredInstances
1010
  .filter(instance => instance.status.toLowerCase() === 'running')
1011
  .map(instance => instance.repo_id);
@@ -1015,10 +1545,12 @@
1015
  updateActionButtons(isLoggedIn);
1016
  }
1017
 
 
1018
  function viewInstance(url) {
1019
  window.open(url, '_blank');
1020
  }
1021
 
 
1022
  function manageInstance(repoId) {
1023
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;
1024
  window.open(manageUrl, '_blank');
 
2
  <html lang="zh">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <!-- 确保 viewport 设置正确,防止缩放问题 -->
6
  <title>HF Space Manager</title>
7
+ <!-- 引入 Chart.js CDN -->
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
9
  <style>
 
10
  * {
11
  margin: 0;
12
  padding: 0;
 
50
  --action-button-bg: #e0e0e0;
51
  --action-button-hover: #d0d0d0;
52
  }
53
+ .container {
54
+ max-width: 1400px;
55
+ margin: 0 auto;
56
+ animation: fadeIn 0.5s ease;
57
+ padding: 0 15px;
58
+ }
59
+ .overview {
60
+ background: var(--card-background);
61
+ border-radius: 15px;
62
+ padding: 20px;
63
+ margin-bottom: 25px;
64
+ border: 1px solid var(--card-border);
65
+ transition: background 0.3s ease, border 0.3s ease;
66
+ }
67
+ .overview-title {
68
+ font-size: 18px;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 10px;
72
+ margin-bottom: 15px;
73
+ color: var(--text-color);
74
+ }
75
+ .theme-toggle {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ margin-bottom: 15px;
80
+ font-size: 14px;
81
+ color: var(--secondary-text);
82
+ }
83
+ .theme-toggle button {
84
+ background: var(--metric-background);
85
+ border: 1px solid var(--metric-border);
86
+ color: var(--text-color);
87
+ padding: 6px;
88
+ border-radius: 6px;
89
+ cursor: pointer;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ width: 32px;
94
+ height: 32px;
95
+ transition: background 0.2s ease, transform 0.2s ease;
96
+ }
97
+ .theme-toggle button:hover {
98
+ background: var(--metric-hover);
99
+ transform: scale(1.05);
100
+ }
101
+ .theme-toggle svg {
102
+ width: 18px;
103
+ height: 18px;
104
+ fill: var(--text-color);
105
+ }
106
+ #summary {
107
+ display: grid;
108
+ grid-template-columns: repeat(6, 1fr);
109
+ gap: 12px;
110
+ }
111
+ #summary div {
112
+ background: var(--metric-background);
113
+ padding: 12px;
114
+ border-radius: 8px;
115
+ border: 1px solid var(--metric-border);
116
+ transition: background 0.3s ease, border 0.3s ease;
117
+ }
118
+ #summary div {
119
+ font-size: 13px;
120
+ color: var(--secondary-text);
121
+ }
122
+ #summary span {
123
+ display: block;
124
+ font-size: 22px;
125
+ font-weight: bold;
126
+ margin-top: 5px;
127
+ color: var(--text-color);
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ white-space: nowrap;
131
+ }
132
+ .stats-container {
133
+ display: grid;
134
+ grid-template-columns: 1fr;
135
+ gap: 20px;
136
+ margin-top: 20px;
137
+ }
138
+ .user-group {
139
+ background: var(--card-background);
140
+ border-radius: 10px;
141
+ border: 1px solid var(--card-border);
142
+ overflow: hidden;
143
+ transition: background 0.3s ease, border 0.3s ease;
144
+ }
145
+ .user-group summary {
146
+ padding: 15px;
147
+ font-weight: bold;
148
+ cursor: pointer;
149
+ color: var(--text-color);
150
+ background: var(--metric-background);
151
+ transition: background 0.2s ease;
152
+ }
153
+ .user-group summary:hover {
154
+ background: var(--metric-hover);
155
+ }
156
+ .user-group summary::-webkit-details-marker {
157
+ color: var(--text-color);
158
+ }
159
+ .user-servers {
160
+ display: grid;
161
+ grid-template-columns: repeat(2, 1fr);
162
+ gap: 15px;
163
+ padding: 15px;
164
+ }
165
+ .server-card {
166
+ background: var(--metric-background);
167
+ border-radius: 8px;
168
+ padding: 15px;
169
+ border: 1px solid var(--metric-border);
170
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.3s ease, border 0.3s ease;
171
+ min-height: 150px; /* 登录状态下的最小高度,适配有操作按钮的情况 */
172
+ display: flex;
173
+ flex-direction: column;
174
+ }
175
+ .server-card.not-logged-in {
176
+ min-height: 120px; /* 未登录状态下的最小高度,减少空白 */
177
+ }
178
+ .server-card:hover {
179
+ transform: translateY(-2px);
180
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
181
+ }
182
+ .server-header {
183
+ display: flex;
184
+ justify-content: space-between;
185
+ align-items: center;
186
+ margin-bottom: 10px;
187
+ font-size: 14px;
188
+ }
189
+ .server-name {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ flex: 1;
194
+ min-width: 0; /* 防止内容溢出 */
195
+ }
196
+ .server-name div {
197
+ overflow: hidden;
198
+ text-overflow: ellipsis;
199
+ white-space: nowrap;
200
+ max-width: 100%; /* 限制名称的最大宽度 */
201
+ }
202
+ .server-flag {
203
+ width: 20px;
204
+ height: 20px;
205
+ border-radius: 4px;
206
+ flex-shrink: 0;
207
+ }
208
+ .metric-grid {
209
+ display: grid;
210
+ grid-template-columns: repeat(5, 1fr);
211
+ gap: 10px;
212
+ margin-top: 10px;
213
+ }
214
+ .metric-item {
215
+ background: var(--card-background);
216
+ padding: 8px;
217
+ border-radius: 6px;
218
+ border: 1px solid var(--metric-border);
219
+ transition: background 0.3s ease;
220
+ overflow: hidden; /* 防止内容溢出 */
221
+ }
222
+ .metric-item:hover {
223
+ background: var(--metric-hover);
224
+ }
225
+ .metric-label {
226
+ color: var(--label-color);
227
+ font-size: 12px;
228
+ margin-bottom: 3px;
229
+ white-space: nowrap;
230
+ }
231
+ .metric-value {
232
+ font-size: 14px;
233
+ font-weight: 500;
234
+ overflow: hidden;
235
+ text-overflow: ellipsis;
236
+ white-space: nowrap;
237
+ max-width: 100%; /* 限制指标值的宽度 */
238
+ }
239
+ .status-dot {
240
+ display: inline-block;
241
+ border-radius: 50%;
242
+ animation: pulse 2s infinite;
243
+ width: 10px;
244
+ height: 10px;
245
+ flex-shrink: 0;
246
+ }
247
+ .status-online {
248
+ background-color: #4CAF50;
249
+ color: #4CAF50;
250
+ }
251
+ .status-offline {
252
+ background-color: #f44336;
253
+ color: #f44336;
254
+ }
255
+ .status-sleep {
256
+ background-color: #ffa500;
257
+ color: #ffa500;
258
+ animation: none;
259
+ }
260
+ .action-buttons {
261
+ display: flex;
262
+ gap: 10px;
263
+ margin-top: 10px;
264
+ }
265
+ .action-button {
266
+ background: var(--action-button-bg);
267
+ color: var(--text-color);
268
+ border: none;
269
+ padding: 6px 12px;
270
+ border-radius: 4px;
271
+ cursor: pointer;
272
+ font-size: 13px;
273
+ transition: background 0.2s ease;
274
+ }
275
+ .action-button:hover {
276
+ background: var(--action-button-hover);
277
+ }
278
+ .network-stats {
279
+ background: var(--network-background);
280
+ border: 1px solid var(--metric-border);
281
+ margin-top: 20px;
282
+ padding: 15px;
283
+ border-radius: 8px;
284
+ transition: background 0.3s ease, border 0.3s ease;
285
+ }
286
+ .network-item {
287
+ font-size: 14px;
288
+ color: var(--secondary-text);
289
+ }
290
+ @keyframes fadeIn {
291
+ from { opacity: 0; transform: translateY(20px); }
292
+ to { opacity: 1; transform: translateY(0); }
293
+ }
294
+ @keyframes pulse {
295
+ 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
296
+ 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
297
+ 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
298
+ }
299
+ @media (max-width: 600px) { /* 降低阈值,确保覆盖更多小屏幕设备如 iPhone */
300
+ #summary {
301
+ grid-template-columns: 1fr; /* 小屏幕单列显示 */
302
+ gap: 10px;
303
+ }
304
+ #summary div {
305
+ padding: 10px;
306
+ }
307
+ #summary span {
308
+ font-size: 20px; /* 小屏幕字体略小 */
309
+ }
310
+ .user-servers {
311
+ grid-template-columns: 1fr !important; /* 小屏幕强制单列显示实例卡片,使用 !important 提高优先级 */
312
+ }
313
+ .metric-grid {
314
+ grid-template-columns: repeat(2, 1fr); /* 小屏幕指标网格改为2列 */
315
+ gap: 8px;
316
+ }
317
+ .metric-item {
318
+ padding: 6px;
319
+ }
320
+ .metric-value {
321
+ font-size: 13px; /* 小屏幕字体略小 */
322
+ }
323
+ .server-header {
324
+ flex-direction: row;
325
+ flex-wrap: wrap;
326
+ gap: 8px; /* 小屏幕避免标题和按钮过于紧密 */
327
+ }
328
+ .container {
329
+ padding: 0 10px; /* 小屏幕减少整体边距 */
330
+ }
331
+ .overview {
332
+ padding: 15px;
333
+ margin-bottom: 20px;
334
+ }
335
+ }
336
+ .login-overlay, .confirm-overlay, .loading-overlay {
337
+ position: fixed;
338
+ top: 0;
339
+ left: 0;
340
+ width: 100%;
341
+ height: 100%;
342
+ background: rgba(0, 0, 0, 0.6);
343
+ display: flex;
344
+ align-items: center;
345
+ justify-content: center;
346
+ z-index: 1000;
347
+ display: none;
348
+ }
349
+ .login-box, .confirm-box {
350
+ background: var(--card-background);
351
+ padding: 30px;
352
+ border-radius: 10px;
353
+ border: 1px solid var(--card-border);
354
+ width: 300px;
355
+ text-align: center;
356
+ }
357
+ .login-box h2, .confirm-box h2 {
358
+ margin-bottom: 20px;
359
+ color: var(--text-color);
360
+ }
361
+ .login-box input {
362
+ width: 100%;
363
+ padding: 10px;
364
+ margin: 10px 0;
365
+ border: 1px solid var(--metric-border);
366
+ border-radius: 5px;
367
+ background: var(--metric-background);
368
+ color: var(--text-color);
369
+ }
370
+ .login-box button, .confirm-box button {
371
+ width: 48%;
372
+ padding: 10px;
373
+ background: var(--action-button-bg);
374
+ border: none;
375
+ border-radius: 5px;
376
+ color: var(--text-color);
377
+ cursor: pointer;
378
+ transition: background 0.2s ease;
379
+ margin: 5px 1%;
380
+ }
381
+ .login-box button:hover, .confirm-box button:hover {
382
+ background: var(--action-button-hover);
383
+ }
384
+ .login-error {
385
+ color: #f44336;
386
+ margin-top: 10px;
387
+ font-size: 14px;
388
+ }
389
+ .login-button, .logout-button {
390
+ background: var(--action-button-bg);
391
+ border: none;
392
+ color: var(--text-color);
393
+ padding: 6px 12px;
394
+ border-radius: 4px;
395
+ cursor: pointer;
396
+ font-size: 13px;
397
+ transition: background 0.2s ease;
398
+ }
399
+ .login-button:hover, .logout-button:hover {
400
+ background: var(--action-button-hover);
401
+ }
402
+ .header-container {
403
+ display: flex;
404
+ justify-content: space-between;
405
+ align-items: center;
406
+ margin-bottom: 15px;
407
+ }
408
+ .auth-buttons {
409
+ display: flex;
410
+ gap: 10px;
411
+ }
412
+ /* 加载状态指示器样式 */
413
+ .loader {
414
+ border: 5px solid var(--card-background);
415
+ border-top: 5px solid #4CAF50;
416
+ border-radius: 50%;
417
+ width: 50px;
418
+ height: 50px;
419
+ animation: spin 1s linear infinite;
420
+ }
421
+ @keyframes spin {
422
+ 0% { transform: rotate(0deg); }
423
+ 100% { transform: rotate(360deg); }
424
+ }
425
+ /* 优化后的过滤与排序控件样式 */
426
+ .filter-sort-panel {
427
+ background: var(--card-background);
428
+ border: 1px solid var(--card-border);
429
+ border-radius: 10px;
430
+ padding: 15px;
431
+ margin-bottom: 20px;
432
+ display: flex;
433
+ flex-wrap: wrap;
434
+ gap: 15px;
435
+ align-items: center;
436
+ transition: background 0.3s ease, border 0.3s ease;
437
+ }
438
+ .filter-sort-group {
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 10px;
442
+ font-size: 14px;
443
+ color: var(--text-color);
444
+ min-width: 200px; /* 确保控件有足够的宽度 */
445
+ }
446
+ .filter-sort-group label {
447
+ white-space: nowrap;
448
+ color: var(--secondary-text);
449
+ font-weight: 500;
450
+ }
451
+ .filter-sort-group select {
452
+ flex: 1;
453
+ background: var(--metric-background);
454
+ border: 1px solid var(--metric-border);
455
+ color: var(--text-color);
456
+ padding: 8px 12px;
457
+ border-radius: 6px;
458
+ cursor: pointer;
459
+ font-size: 14px;
460
+ transition: background 0.2s ease, border 0.2s ease;
461
+ outline: none;
462
+ appearance: none; /* 移除默认的下拉箭头 */
463
+ background-image: url("data:image/svg+xml;utf8,<svg fill='%23fff' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
464
+ background-repeat: no-repeat;
465
+ background-position: right 10px center;
466
+ padding-right: 36px; /* 为箭头留出空间 */
467
+ }
468
+ [data-theme="light"] .filter-sort-group select {
469
+ background-image: url("data:image/svg+xml;utf8,<svg fill='%23333' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
470
+ }
471
+ .filter-sort-group select:hover {
472
+ background-color: var(--metric-hover);
473
+ border-color: rgba(255, 255, 255, 0.15); /* 微调 hover 时的边框 */
474
+ }
475
+ .filter-sort-group select:focus {
476
+ border-color: #4CAF50;
477
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
478
+ }
479
+ .refresh-button {
480
+ background: var(--action-button-bg);
481
+ border: none;
482
+ color: var(--text-color);
483
+ padding: 8px 16px;
484
+ border-radius: 6px;
485
+ cursor: pointer;
486
+ font-size: 14px;
487
+ transition: background 0.2s ease, transform 0.2s ease;
488
+ display: flex;
489
+ align-items: center;
490
+ gap: 8px;
491
+ height: 40px;
492
+ }
493
+ .refresh-button:hover {
494
+ background: var(--action-button-hover);
495
+ transform: translateY(-1px);
496
+ }
497
+ .refresh-icon {
498
+ width: 16px;
499
+ height: 16px;
500
+ fill: currentColor;
501
+ }
502
+ @media (max-width: 600px) {
503
+ .filter-sort-group {
504
+ min-width: 100%; /* 小屏时控件占满一行 */
505
+ }
506
+ .filter-sort-panel {
507
+ gap: 12px; /* 小屏时减少间距 */
508
+ padding: 12px;
509
+ }
510
+ }
511
+ /* 图表相关样式 */
512
+ .chart-container {
513
+ display: none; /* 默认隐藏 */
514
+ margin-top: 15px;
515
+ background: var(--card-background);
516
+ border: 1px solid var(--card-border);
517
+ border-radius: 8px;
518
+ padding: 10px;
519
+ height: 300px;
520
+ transition: background 0.3s ease, border 0.3s ease;
521
+ }
522
+ .chart-toggle-button {
523
+ background: var(--action-button-bg);
524
+ color: var(--text-color);
525
+ border: none;
526
+ padding: 6px 12px;
527
+ border-radius: 4px;
528
+ cursor: pointer;
529
+ font-size: 13px;
530
+ transition: background 0.2s ease;
531
+ margin-left: auto;
532
+ white-space: nowrap;
533
+ }
534
+ .chart-toggle-button:hover {
535
+ background: var(--action-button-hover);
536
+ }
537
+ .expanded .chart-container {
538
+ display: block;
539
+ }
540
+ canvas {
541
+ width: 100% !important;
542
+ height: auto !important;
543
+ }
544
+ @media (max-width: 600px) {
545
+ .chart-container {
546
+ height: 250px; /* 小屏幕图表高度略减 */
547
+ }
548
+ }
549
  </style>
550
  </head>
551
  <body>
 
585
  <div>总下载: <span id="totalDownload">0 B/s</span></div>
586
  </div>
587
  </div>
588
+ <!-- 优化后的过滤与排序面板 -->
589
  <div class="filter-sort-panel">
590
  <div class="filter-sort-group">
591
  <label for="statusFilter">过滤状态:</label>
 
786
  document.getElementById('loginButton').style.display = 'none';
787
  document.getElementById('logoutButton').style.display = 'block';
788
  updateActionButtons(true);
789
+ refreshData(); // 登录成功后刷新数据以显示所有实例包括 private
790
  } else {
791
  console.log('登录失败:', data.message);
792
  loginError.textContent = data.message || '登录失败';
 
821
  document.getElementById('loginButton').style.display = 'block';
822
  document.getElementById('logoutButton').style.display = 'none';
823
  updateActionButtons(false);
824
+ refreshData(); // 登出后刷新数据以隐藏 private 实例
825
  })
826
  .catch(error => {
827
  hideLoading();
 
831
  document.getElementById('loginButton').style.display = 'block';
832
  document.getElementById('logoutButton').style.display = 'none';
833
  updateActionButtons(false);
834
+ refreshData(); // 登出后刷新数据以隐藏 private 实例
835
  });
836
  } else {
837
  console.log('本地无 token,直接设置为未登录');
 
839
  document.getElementById('loginButton').style.display = 'block';
840
  document.getElementById('logoutButton').style.display = 'none';
841
  updateActionButtons(false);
842
+ refreshData(); // 登出后刷新数据以隐藏 private 实例
843
  }
844
  }
845
 
 
852
  if (buttons) {
853
  buttons.style.display = loggedIn ? 'flex' : 'none';
854
  }
855
+ // 动态添加或去除 not-logged-in 类以调整卡片高度
856
  if (loggedIn) {
857
  card.classList.remove('not-logged-in');
858
  } else {
859
  card.classList.add('not-logged-in');
860
  }
861
+ // 确保图表切换按钮总是可见,不管是否登录
862
  const chartToggleButton = card.querySelector('.chart-toggle-button');
863
  if (chartToggleButton) {
864
  chartToggleButton.style.display = 'inline-block';
 
866
  });
867
  }
868
 
869
+ // 使用 window.onload 确保页面完全加载后检查登录状态
870
  window.onload = async function() {
871
  console.log('页面加载完成,开始检查登录状态');
872
  await checkLoginStatus();
 
874
  await initialize();
875
  };
876
 
877
+ // 二次确认弹窗逻辑
878
  let pendingAction = null;
879
  let pendingRepoId = null;
880
 
 
915
  hideLoading();
916
  const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
917
  document.getElementById('totalUsers').textContent = usernamesList.length;
918
+ // 更新用户过滤下拉菜单
919
  const userFilter = document.getElementById('userFilter');
920
  userFilter.innerHTML = '<option value="all">全部用户</option>';
921
  usernamesList.forEach(username => {
 
972
 
973
  const instancesParam = subscribedInstances.join(',');
974
  const token = localStorage.getItem('authToken');
975
+ // 由于 EventSource 不支持直接设置 Authorization 头,这里通过查询参数传递 token
976
  const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token || '')}`;
977
  console.log('SSE 连接 URL:', url.split('&token=')[0] + (token ? '&token=... (隐藏)' : '&token=空'));
978
  this.eventSource = new EventSource(url);
 
991
  console.error(`SSE 连接错误:`, error);
992
  this.eventSource.close();
993
  this.eventSource = null;
994
+ // 可选:尝试重新连接
995
  setTimeout(() => this.connect(subscribedInstances), 5000);
996
  };
997
 
 
1010
  const metricsStreamManager = new MetricsStreamManager();
1011
  const instanceMap = new Map();
1012
  const serverStatus = new Map();
1013
+ let allInstances = []; // 存储所有实例数据,用于过滤和排序
1014
+ const chartInstances = new Map(); // 存储每个实例的图表实例
1015
 
1016
  async function initialize() {
1017
  await getUsernames();
 
1019
  allInstances = instances;
1020
  renderInstances(allInstances);
1021
 
1022
+ // 默认只监控状态为 running 的实例
1023
  const runningInstances = instances
1024
  .filter(instance => instance.status.toLowerCase() === 'running')
1025
  .map(instance => instance.repo_id);
1026
 
1027
  metricsStreamManager.connect(runningInstances);
1028
+
1029
  updateSummary();
1030
  updateActionButtons(isLoggedIn);
1031
  }
1032
 
1033
+ // 手动刷新数据函数
1034
  async function refreshData() {
1035
  metricsStreamManager.disconnect();
1036
+ // 销毁所有图表实例
1037
  chartInstances.forEach(chart => {
1038
  if (chart) {
1039
  chart.destroy();
 
1041
  });
1042
  chartInstances.clear();
1043
  await initialize();
1044
+ applyFiltersAndSort(); // 确保刷新后��新应用过滤和排序
1045
  }
1046
 
1047
  function renderInstances(instances) {
1048
  const serversContainer = document.getElementById('servers');
1049
+ serversContainer.innerHTML = ''; // 清空现有内容
1050
  const userGroups = {};
1051
 
1052
+ // 按用户分组
1053
  instances.forEach(instance => {
1054
  if (!userGroups[instance.owner]) {
1055
  userGroups[instance.owner] = [];
 
1057
  userGroups[instance.owner].push(instance);
1058
  });
1059
 
1060
+ // 渲染每个用户组
1061
  Object.keys(userGroups).forEach(owner => {
1062
  let userGroup = document.createElement('details');
1063
  userGroup.className = 'user-group';
 
1074
 
1075
  serversContainer.appendChild(userGroup);
1076
 
1077
+ // 渲染该用户下的实例
1078
  userGroups[owner].forEach(instance => {
1079
  renderInstanceCard(instance, userServers);
1080
  });
1081
  });
1082
  }
1083
 
1084
+ // 图表配置函数
1085
  function createChart(instanceId) {
1086
  const canvasId = `chart-${instanceId}`;
1087
  const canvas = document.getElementById(canvasId);
1088
  if (!canvas) return null;
1089
 
1090
+ // 根据当前主题设置图表样式
1091
  const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
1092
  const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
1093
  const textColor = isDarkMode ? '#fff' : '#333';
 
1096
  const chart = new Chart(ctx, {
1097
  type: 'line',
1098
  data: {
1099
+ labels: Array(30).fill(''), // 初始为空标签,最多30个数据点
1100
  datasets: [
1101
  {
1102
  label: 'CPU 使用率 (%)',
1103
  data: [],
1104
+ borderColor: '#4CAF50', // 绿色
1105
  backgroundColor: 'rgba(76, 175, 80, 0.2)',
1106
  tension: 0.4,
1107
  fill: true,
 
1109
  {
1110
  label: '内存使用率 (%)',
1111
  data: [],
1112
+ borderColor: '#2196F3', // 蓝色
1113
  backgroundColor: 'rgba(33, 150, 243, 0.2)',
1114
  tension: 0.4,
1115
  fill: true,
 
1117
  {
1118
  label: '上传速度 (KB/s)',
1119
  data: [],
1120
+ borderColor: '#F44336', // 红色
1121
  backgroundColor: 'rgba(244, 67, 54, 0.2)',
1122
  tension: 0.4,
1123
  fill: true,
 
1125
  {
1126
  label: '下载速度 (KB/s)',
1127
  data: [],
1128
+ borderColor: '#FF9800', // 橙色
1129
  backgroundColor: 'rgba(255, 152, 0, 0.2)',
1130
  tension: 0.4,
1131
  fill: true,
 
1170
  },
1171
  elements: {
1172
  point: {
1173
+ radius: 0, // 隐藏数据点
1174
+ hitRadius: 5 // 确保悬停仍有效
1175
  }
1176
  },
1177
+ animation: false // 关闭动画,提升性能
1178
  }
1179
  });
1180
  chartInstances.set(instanceId, chart);
1181
  return chart;
1182
  }
1183
 
1184
+ // 更新图表数据
1185
  function updateChart(instanceId, data) {
1186
  let chart = chartInstances.get(instanceId);
1187
  if (!chart) {
1188
  chart = createChart(instanceId);
1189
+ if (!chart) return; // 如果图表未创建成功,直接返回
1190
  }
1191
 
1192
+ // 获取当前数据集
1193
  const cpuData = chart.data.datasets[0].data;
1194
  const memoryData = chart.data.datasets[1].data;
1195
  const uploadData = chart.data.datasets[2].data;
1196
  const downloadData = chart.data.datasets[3].data;
1197
 
1198
+ // 追加新数据
1199
  cpuData.push(data.cpu_usage_pct);
1200
  memoryData.push(((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2));
1201
+ uploadData.push((data.tx_bps / 1024).toFixed(2)); // 转换为 KB/s
1202
+ downloadData.push((data.rx_bps / 1024).toFixed(2)); // 转换为 KB/s
1203
 
1204
+ // 限制数据点数量为 30 个
1205
  if (cpuData.length > 30) {
1206
  cpuData.shift();
1207
  memoryData.shift();
 
1209
  downloadData.shift();
1210
  }
1211
 
1212
+ // 更新图表
1213
  chart.update();
1214
  }
1215
 
 
1224
  card.id = cardId;
1225
  card.className = 'server-card';
1226
  if (!isLoggedIn) {
1227
+ card.classList.add('not-logged-in'); // 未登录时添加类以调整高度
1228
  }
1229
+ // 根据 instance.private 属性选择不同的图标
1230
  const iconSvg = instance.private
1231
  ? `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
1232
  <path d="M18 8v-3c0-1.656-1.344-3-3-3h-6c-1.656 0-3 1.344-3 3v3h-3v14h18v-14h-3zm-10-1.5c0-.828.672-1.5 1.5-1.5h5c.828 0 1.5.672 1.5 1.5v2.5h-8v-2.5zm4 11.5c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/>
1233
+ </svg>` // 锁图标,表示 private 实例
1234
  : `<svg class="server-flag" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
1235
  <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"/>
1236
+ </svg>`; // 服务器图标,表示非 private 实例
1237
 
1238
  card.innerHTML = `
1239
  <div class="server-header">
1240
  <div class="server-name">
1241
  <div class="status-dot status-sleep"></div>
1242
  ${iconSvg}
1243
+ <div>${instance.name} </div>
1244
  </div>
1245
  <div>
1246
  <button class="chart-toggle-button" onclick="toggleChart('${instanceId}')">查看图表</button>
 
1292
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
1293
  }
1294
 
1295
+ // 切换图表显示/隐藏
1296
  function toggleChart(instanceId) {
1297
  const card = document.getElementById(`instance-${instanceId}`);
1298
  const chartContainer = document.getElementById(`chart-container-${instanceId}`);
 
1305
  } else {
1306
  card.classList.add('expanded');
1307
  toggleButton.textContent = '收起图表';
1308
+ // 如果图表未初始化,则创建
1309
  if (!chartInstances.has(instanceId)) {
1310
  createChart(instanceId);
1311
  }
 
1318
  const instance = instanceMap.get(instanceId);
1319
 
1320
  if (!card && instance) {
1321
+ // 如果卡片不存在,但实例存在,说明可能被过滤掉了,不渲染
1322
  return;
1323
  }
1324
 
 
1335
  statusDot.className = 'status-dot status-online';
1336
  isOnline = true;
1337
  isSleep = false;
1338
+ // 更新图表数据
1339
  updateChart(instanceId, data);
1340
  } else {
1341
  const currentStatus = instance?.status.toLowerCase() || 'unknown';
 
1384
  hideLoading();
1385
  if (result.success) {
1386
  alert(`重启成功: ${repoId}`);
1387
+ // 操作成功后立即刷新数据
1388
  refreshData();
1389
  } else {
1390
  if (response.status === 401) {
 
1427
  hideLoading();
1428
  if (result.success) {
1429
  alert(`重建成功: ${repoId}`);
1430
+ // 操作成功后立即刷新数据
1431
  refreshData();
1432
  } else {
1433
  if (response.status === 401) {
 
1491
  await initialize();
1492
  }, 300000);
1493
 
1494
+ // 应用过滤和排序
1495
  function applyFiltersAndSort() {
1496
  const statusFilter = document.getElementById('statusFilter').value;
1497
  const userFilter = document.getElementById('userFilter').value;
1498
  const sortBy = document.getElementById('sortBy').value;
1499
 
1500
+ // 过滤实例
1501
  let filteredInstances = allInstances;
1502
  if (statusFilter !== 'all') {
1503
  filteredInstances = filteredInstances.filter(instance => instance.status.toLowerCase() === statusFilter);
 
1506
  filteredInstances = filteredInstances.filter(instance => instance.owner === userFilter);
1507
  }
1508
 
1509
+ // 排序实例
1510
  filteredInstances.sort((a, b) => {
1511
  if (sortBy === 'name-asc') {
1512
  return a.name.localeCompare(b.name);
 
1522
  return 0;
1523
  });
1524
 
1525
+ // 重新渲染过滤和排序后的实例
1526
  instanceMap.clear();
1527
  serverStatus.clear();
1528
  metricsStreamManager.disconnect();
1529
+ // 销毁现有图表实例
1530
  chartInstances.forEach(chart => {
1531
  if (chart) {
1532
  chart.destroy();
 
1535
  chartInstances.clear();
1536
  renderInstances(filteredInstances);
1537
 
1538
+ // 默认监控状态为 running 的实例
1539
  const runningInstances = filteredInstances
1540
  .filter(instance => instance.status.toLowerCase() === 'running')
1541
  .map(instance => instance.repo_id);
 
1545
  updateActionButtons(isLoggedIn);
1546
  }
1547
 
1548
+ // 新增函数:在新标签页中打开实例的URL
1549
  function viewInstance(url) {
1550
  window.open(url, '_blank');
1551
  }
1552
 
1553
+ // 新增函数:在新标签页中打开实例的管理页面
1554
  function manageInstance(repoId) {
1555
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;
1556
  window.open(manageUrl, '_blank');