nbugs commited on
Commit
96b1c0e
·
verified ·
1 Parent(s): bf2e0ed

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +68 -0
public/index.html CHANGED
@@ -809,18 +809,22 @@
809
  <script>
810
  // 全局变量,表示当前是否已登录
811
  let isLoggedIn = false;
 
812
  // 加载状态控制函数
813
  function showLoading() {
814
  document.getElementById('loadingOverlay').style.display = 'flex';
815
  }
 
816
  function hideLoading() {
817
  document.getElementById('loadingOverlay').style.display = 'none';
818
  }
 
819
  // 登录状态管理
820
  function checkLoginStatus() {
821
  const token = localStorage.getItem('authToken');
822
  const loginButton = document.getElementById('loginButton');
823
  const logoutButton = document.getElementById('logoutButton');
 
824
  if (token) {
825
  console.log('本地存储中找到 token,尝试验证:', token.slice(0, 8) + '...');
826
  showLoading();
@@ -869,6 +873,7 @@
869
  return Promise.resolve(false);
870
  }
871
  }
 
872
  function showLoginForm() {
873
  document.getElementById('loginOverlay').style.display = 'flex';
874
  document.getElementById('username').value = '';
@@ -876,9 +881,11 @@
876
  document.getElementById('loginError').style.display = 'none';
877
  document.getElementById('username').focus();
878
  }
 
879
  function hideLoginForm() {
880
  document.getElementById('loginOverlay').style.display = 'none';
881
  }
 
882
  function login() {
883
  const username = document.getElementById('username').value;
884
  const password = document.getElementById('password').value;
@@ -916,6 +923,7 @@
916
  loginError.style.display = 'block';
917
  });
918
  }
 
919
  function logout() {
920
  const token = localStorage.getItem('authToken');
921
  if (token) {
@@ -957,6 +965,7 @@
957
  refreshData(); // 登出后刷新数据以隐藏 private 实例
958
  }
959
  }
 
960
  function updateActionButtons(loggedIn) {
961
  console.log('更新操作按钮状态,是否已登录:', loggedIn);
962
  isLoggedIn = loggedIn;
@@ -979,6 +988,7 @@
979
  }
980
  });
981
  }
 
982
  // 使用 window.onload 确保页面完全加载后检查登录状态
983
  window.onload = async function() {
984
  console.log('页面加载完成,开始检查登录状态');
@@ -986,9 +996,11 @@
986
  console.log('登录状态检查完成,初始化数据');
987
  await initialize();
988
  };
 
989
  // 二次确认弹窗逻辑
990
  let pendingAction = null;
991
  let pendingRepoId = null;
 
992
  function showConfirmDialog(action, repoId, title, message) {
993
  pendingAction = action;
994
  pendingRepoId = repoId;
@@ -996,6 +1008,7 @@
996
  document.getElementById('confirmMessage').textContent = message;
997
  document.getElementById('confirmOverlay').style.display = 'flex';
998
  }
 
999
  function confirmAction() {
1000
  if (pendingAction === 'restart') {
1001
  restartSpace(pendingRepoId);
@@ -1004,11 +1017,13 @@
1004
  }
1005
  cancelAction();
1006
  }
 
1007
  function cancelAction() {
1008
  pendingAction = null;
1009
  pendingRepoId = null;
1010
  document.getElementById('confirmOverlay').style.display = 'none';
1011
  }
 
1012
  async function getUsernames() {
1013
  try {
1014
  showLoading();
@@ -1040,6 +1055,7 @@
1040
  return [];
1041
  }
1042
  }
 
1043
  async function fetchInstances() {
1044
  try {
1045
  showLoading();
@@ -1066,20 +1082,24 @@
1066
  return [];
1067
  }
1068
  }
 
1069
  class MetricsStreamManager {
1070
  constructor() {
1071
  this.eventSource = null;
1072
  }
 
1073
  connect(subscribedInstances = []) {
1074
  if (this.eventSource) {
1075
  this.eventSource.close();
1076
  }
 
1077
  const instancesParam = subscribedInstances.join(',');
1078
  const token = localStorage.getItem('authToken');
1079
  // 由于 EventSource 不支持直接设置 Authorization 头,这里通过查询参数传递 token
1080
  const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token || '')}`;
1081
  console.log('SSE 连接 URL:', url.split('&token=')[0] + (token ? '&token=... (隐藏)' : '&token=空'));
1082
  this.eventSource = new EventSource(url);
 
1083
  this.eventSource.addEventListener("metric", (event) => {
1084
  try {
1085
  const data = JSON.parse(event.data);
@@ -1089,6 +1109,7 @@
1089
  console.error(`解析监控数据失败:`, error);
1090
  }
1091
  });
 
1092
  this.eventSource.onerror = (error) => {
1093
  console.error(`SSE 连接错误:`, error);
1094
  this.eventSource.close();
@@ -1096,8 +1117,10 @@
1096
  // 可选:尝试重新连接
1097
  setTimeout(() => this.connect(subscribedInstances), 5000);
1098
  };
 
1099
  console.log(`SSE 连接已建立,订阅实例: ${instancesParam || '无'}`);
1100
  }
 
1101
  disconnect() {
1102
  if (this.eventSource) {
1103
  this.eventSource.close();
@@ -1106,12 +1129,14 @@
1106
  }
1107
  }
1108
  }
 
1109
  const metricsStreamManager = new MetricsStreamManager();
1110
  const instanceMap = new Map();
1111
  const serverStatus = new Map();
1112
  let allInstances = []; // 存储所有实例数据,用于过滤和排序
1113
  const chartInstances = new Map(); // 存储每个实例的图表实例
1114
  const chartDataBuffer = new Map(); // 存储图表数据缓冲以减少更新频率
 
1115
  async function initialize() {
1116
  await getUsernames();
1117
  const instances = await fetchInstances();
@@ -1128,6 +1153,7 @@
1128
  updateSummary();
1129
  updateActionButtons(isLoggedIn);
1130
  }
 
1131
  // 手动刷新数据函数
1132
  async function refreshData() {
1133
  metricsStreamManager.disconnect();
@@ -1142,10 +1168,12 @@
1142
  await initialize();
1143
  applyFiltersAndSort(); // 确保刷新后重新应用过滤和排序
1144
  }
 
1145
  function renderInstances(instances) {
1146
  const serversContainer = document.getElementById('servers');
1147
  serversContainer.innerHTML = ''; // 清空现有内容
1148
  const userGroups = {};
 
1149
  // 按用户分组
1150
  instances.forEach(instance => {
1151
  if (!userGroups[instance.owner]) {
@@ -1153,6 +1181,7 @@
1153
  }
1154
  userGroups[instance.owner].push(instance);
1155
  });
 
1156
  // 渲染每个用户组
1157
  Object.keys(userGroups).forEach(owner => {
1158
  let userGroup = document.createElement('details');
@@ -1169,23 +1198,27 @@
1169
  userGroup.appendChild(userServers);
1170
 
1171
  serversContainer.appendChild(userGroup);
 
1172
  // 渲染该用户下的实例
1173
  userGroups[owner].forEach(instance => {
1174
  renderInstanceCard(instance, userServers);
1175
  });
1176
  });
1177
  }
 
1178
  // 图表配置函数
1179
  function createChart(instanceId) {
1180
  const canvasId = `chart-${instanceId}`;
1181
  const canvas = document.getElementById(canvasId);
1182
  if (!canvas) return null;
 
1183
  const gridColor = 'rgba(0, 212, 255, 0.1)';
1184
  const textColor = '#E0E0FF';
1185
  const cpuColor = '#00FFAA';
1186
  const memoryColor = '#00D4FF';
1187
  const uploadColor = '#FF9500';
1188
  const downloadColor = '#FF00FF';
 
1189
  const ctx = canvas.getContext('2d');
1190
  const chart = new Chart(ctx, {
1191
  type: 'line',
@@ -1285,31 +1318,37 @@
1285
  chartInstances.set(instanceId, chart);
1286
  return chart;
1287
  }
 
1288
  // 更新图表数据,带有缓冲机制减少更新频率
1289
  function updateChart(instanceId, data) {
1290
  let buffer = chartDataBuffer.get(instanceId) || { data: [], count: 0 };
1291
  buffer.data.push(data);
1292
  buffer.count++;
 
1293
  if (buffer.count < 2) { // 每 2 次数据更新才渲染一次
1294
  chartDataBuffer.set(instanceId, buffer);
1295
  return;
1296
  }
 
1297
  let chart = chartInstances.get(instanceId);
1298
  if (!chart) {
1299
  chart = createChart(instanceId);
1300
  if (!chart) return;
1301
  }
 
1302
  // 获取当前数据集
1303
  const cpuData = chart.data.datasets[0].data;
1304
  const memoryData = chart.data.datasets[1].data;
1305
  const uploadData = chart.data.datasets[2].data;
1306
  const downloadData = chart.data.datasets[3].data;
 
1307
  // 追加最新数据
1308
  const latestData = buffer.data[buffer.data.length - 1];
1309
  cpuData.push(latestData.cpu_usage_pct);
1310
  memoryData.push(((latestData.memory_used_bytes / latestData.memory_total_bytes) * 100).toFixed(2));
1311
  uploadData.push((latestData.tx_bps / 1024).toFixed(2)); // 转换为 KB/s
1312
  downloadData.push((latestData.rx_bps / 1024).toFixed(2)); // 转换为 KB/s
 
1313
  // 限制数据点数量为 30 个
1314
  if (cpuData.length > 30) {
1315
  cpuData.shift();
@@ -1317,12 +1356,14 @@
1317
  uploadData.shift();
1318
  downloadData.shift();
1319
  }
 
1320
  // 更新图表
1321
  chart.update();
1322
 
1323
  // 清空缓冲
1324
  chartDataBuffer.set(instanceId, { data: [], count: 0 });
1325
  }
 
1326
  // 计算相对时间的辅助函数
1327
  function formatRelativeTime(dateStr) {
1328
  if (!dateStr) return '未知时间';
@@ -1334,6 +1375,7 @@
1334
  const diffMins = Math.floor(diffSecs / 60);
1335
  const diffHrs = Math.floor(diffMins / 60);
1336
  const diffDays = Math.floor(diffHrs / 24);
 
1337
  if (diffDays > 7) {
1338
  return date.toLocaleDateString(); // 超过7天显示日期
1339
  } else if (diffDays > 0) {
@@ -1350,15 +1392,19 @@
1350
  return '未知时间';
1351
  }
1352
  }
 
1353
  // 切换信息块显示/隐藏的函数
1354
  function toggleInfoBlock(instanceId) {
1355
  const card = document.getElementById(`instance-${instanceId}`);
1356
  if (!card) return;
 
1357
  card.classList.toggle('info-expanded');
1358
  }
 
1359
  function renderInstanceCard(instance, container) {
1360
  const instanceId = instance.repo_id;
1361
  instanceMap.set(instanceId, instance);
 
1362
  const cardId = `instance-${instanceId}`;
1363
  let card = document.getElementById(cardId);
1364
  if (!card) {
@@ -1381,6 +1427,7 @@
1381
  const description = instance.short_description && instance.short_description.trim() ? instance.short_description : 'N/A';
1382
  // 处理 last_modified,格式化为相对时间
1383
  const lastModified = formatRelativeTime(instance.last_modified);
 
1384
  card.innerHTML = `
1385
  <div class="server-header">
1386
  <div class="server-name" onclick="toggleInfoBlock('${instanceId}')">
@@ -1447,12 +1494,14 @@
1447
  }
1448
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
1449
  }
 
1450
  // 切换图表显示/隐藏
1451
  function toggleChart(instanceId) {
1452
  const card = document.getElementById(`instance-${instanceId}`);
1453
  const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1454
  const toggleButton = card.querySelector('.chart-toggle-button');
1455
  if (!card || !chartContainer) return;
 
1456
  if (card.classList.contains('expanded')) {
1457
  card.classList.remove('expanded');
1458
  toggleButton.textContent = '查看图表';
@@ -1465,6 +1514,7 @@
1465
  }
1466
  }
1467
  }
 
1468
  function updateServerCard(data, instanceId, isSleep = false) {
1469
  const cardId = `instance-${instanceId}`;
1470
  let card = document.getElementById(cardId);
@@ -1474,10 +1524,12 @@
1474
  // 如果卡片不存在,但实例存在,说明可能被过滤掉了,不渲染
1475
  return;
1476
  }
 
1477
  if (card) {
1478
  const statusDot = card.querySelector('.status-dot');
1479
  let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
1480
  let isOnline = false;
 
1481
  if (data) {
1482
  cpuUsage = `${data.cpu_usage_pct}%`;
1483
  memoryUsage = `${((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2)}%`;
@@ -1504,14 +1556,17 @@
1504
  isSleep = false;
1505
  }
1506
  }
 
1507
  card.querySelector('.cpu-usage').textContent = cpuUsage;
1508
  card.querySelector('.memory-usage').textContent = memoryUsage;
1509
  card.querySelector('.upload').textContent = upload;
1510
  card.querySelector('.download').textContent = download;
 
1511
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
1512
  updateSummary();
1513
  }
1514
  }
 
1515
  async function restartSpace(repoId) {
1516
  try {
1517
  const token = localStorage.getItem('authToken');
@@ -1554,6 +1609,7 @@
1554
  alert(`重启失败: ${error.message}`);
1555
  }
1556
  }
 
1557
  async function rebuildSpace(repoId) {
1558
  try {
1559
  const token = localStorage.getItem('authToken');
@@ -1596,11 +1652,13 @@
1596
  alert(`重建失败: ${error.message}`);
1597
  }
1598
  }
 
1599
  function updateSummary() {
1600
  let online = 0;
1601
  let offline = 0;
1602
  let totalUpload = 0;
1603
  let totalDownload = 0;
 
1604
  serverStatus.forEach((status, instanceId) => {
1605
  const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
1606
  if (isRecentlyOnline) {
@@ -1613,12 +1671,14 @@
1613
  offline++;
1614
  }
1615
  });
 
1616
  document.getElementById('totalServers').textContent = serverStatus.size;
1617
  document.getElementById('onlineServers').textContent = online;
1618
  document.getElementById('offlineServers').textContent = offline;
1619
  document.getElementById('totalUpload').textContent = `${formatBytes(totalUpload)}/s`;
1620
  document.getElementById('totalDownload').textContent = `${formatBytes(totalDownload)}/s`;
1621
  }
 
1622
  function formatBytes(bytes) {
1623
  if (bytes === 0) return '0 B';
1624
  const k = 1024;
@@ -1626,16 +1686,20 @@
1626
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1627
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1628
  }
 
1629
  setInterval(updateSummary, 5000);
 
1630
  setInterval(async () => {
1631
  metricsStreamManager.disconnect();
1632
  await initialize();
1633
  }, 300000);
 
1634
  // 应用过滤和排序
1635
  function applyFiltersAndSort() {
1636
  const statusFilter = document.getElementById('statusFilter').value;
1637
  const userFilter = document.getElementById('userFilter').value;
1638
  const sortBy = document.getElementById('sortBy').value;
 
1639
  // 过滤实例
1640
  let filteredInstances = allInstances;
1641
  if (statusFilter !== 'all') {
@@ -1644,6 +1708,7 @@
1644
  if (userFilter !== 'all') {
1645
  filteredInstances = filteredInstances.filter(instance => instance.owner === userFilter);
1646
  }
 
1647
  // 排序实例
1648
  filteredInstances.sort((a, b) => {
1649
  if (sortBy === 'name-asc') {
@@ -1659,6 +1724,7 @@
1659
  }
1660
  return 0;
1661
  });
 
1662
  // 重新渲染过滤和排序后的实例
1663
  instanceMap.clear();
1664
  serverStatus.clear();
@@ -1682,10 +1748,12 @@
1682
  updateSummary();
1683
  updateActionButtons(isLoggedIn);
1684
  }
 
1685
  // 新增函数:在新标签页中打开实例的URL
1686
  function viewInstance(url) {
1687
  window.open(url, '_blank');
1688
  }
 
1689
  // 新增函数:在新标签页中打开实例的管理页面
1690
  function manageInstance(repoId) {
1691
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;
 
809
  <script>
810
  // 全局变量,表示当前是否已登录
811
  let isLoggedIn = false;
812
+
813
  // 加载状态控制函数
814
  function showLoading() {
815
  document.getElementById('loadingOverlay').style.display = 'flex';
816
  }
817
+
818
  function hideLoading() {
819
  document.getElementById('loadingOverlay').style.display = 'none';
820
  }
821
+
822
  // 登录状态管理
823
  function checkLoginStatus() {
824
  const token = localStorage.getItem('authToken');
825
  const loginButton = document.getElementById('loginButton');
826
  const logoutButton = document.getElementById('logoutButton');
827
+
828
  if (token) {
829
  console.log('本地存储中找到 token,尝试验证:', token.slice(0, 8) + '...');
830
  showLoading();
 
873
  return Promise.resolve(false);
874
  }
875
  }
876
+
877
  function showLoginForm() {
878
  document.getElementById('loginOverlay').style.display = 'flex';
879
  document.getElementById('username').value = '';
 
881
  document.getElementById('loginError').style.display = 'none';
882
  document.getElementById('username').focus();
883
  }
884
+
885
  function hideLoginForm() {
886
  document.getElementById('loginOverlay').style.display = 'none';
887
  }
888
+
889
  function login() {
890
  const username = document.getElementById('username').value;
891
  const password = document.getElementById('password').value;
 
923
  loginError.style.display = 'block';
924
  });
925
  }
926
+
927
  function logout() {
928
  const token = localStorage.getItem('authToken');
929
  if (token) {
 
965
  refreshData(); // 登出后刷新数据以隐藏 private 实例
966
  }
967
  }
968
+
969
  function updateActionButtons(loggedIn) {
970
  console.log('更新操作按钮状态,是否已登录:', loggedIn);
971
  isLoggedIn = loggedIn;
 
988
  }
989
  });
990
  }
991
+
992
  // 使用 window.onload 确保页面完全加载后检查登录状态
993
  window.onload = async function() {
994
  console.log('页面加载完成,开始检查登录状态');
 
996
  console.log('登录状态检查完成,初始化数据');
997
  await initialize();
998
  };
999
+
1000
  // 二次确认弹窗逻辑
1001
  let pendingAction = null;
1002
  let pendingRepoId = null;
1003
+
1004
  function showConfirmDialog(action, repoId, title, message) {
1005
  pendingAction = action;
1006
  pendingRepoId = repoId;
 
1008
  document.getElementById('confirmMessage').textContent = message;
1009
  document.getElementById('confirmOverlay').style.display = 'flex';
1010
  }
1011
+
1012
  function confirmAction() {
1013
  if (pendingAction === 'restart') {
1014
  restartSpace(pendingRepoId);
 
1017
  }
1018
  cancelAction();
1019
  }
1020
+
1021
  function cancelAction() {
1022
  pendingAction = null;
1023
  pendingRepoId = null;
1024
  document.getElementById('confirmOverlay').style.display = 'none';
1025
  }
1026
+
1027
  async function getUsernames() {
1028
  try {
1029
  showLoading();
 
1055
  return [];
1056
  }
1057
  }
1058
+
1059
  async function fetchInstances() {
1060
  try {
1061
  showLoading();
 
1082
  return [];
1083
  }
1084
  }
1085
+
1086
  class MetricsStreamManager {
1087
  constructor() {
1088
  this.eventSource = null;
1089
  }
1090
+
1091
  connect(subscribedInstances = []) {
1092
  if (this.eventSource) {
1093
  this.eventSource.close();
1094
  }
1095
+
1096
  const instancesParam = subscribedInstances.join(',');
1097
  const token = localStorage.getItem('authToken');
1098
  // 由于 EventSource 不支持直接设置 Authorization 头,这里通过查询参数传递 token
1099
  const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}&token=${encodeURIComponent(token || '')}`;
1100
  console.log('SSE 连接 URL:', url.split('&token=')[0] + (token ? '&token=... (隐藏)' : '&token=空'));
1101
  this.eventSource = new EventSource(url);
1102
+
1103
  this.eventSource.addEventListener("metric", (event) => {
1104
  try {
1105
  const data = JSON.parse(event.data);
 
1109
  console.error(`解析监控数据失败:`, error);
1110
  }
1111
  });
1112
+
1113
  this.eventSource.onerror = (error) => {
1114
  console.error(`SSE 连接错误:`, error);
1115
  this.eventSource.close();
 
1117
  // 可选:尝试重新连接
1118
  setTimeout(() => this.connect(subscribedInstances), 5000);
1119
  };
1120
+
1121
  console.log(`SSE 连接已建立,订阅实例: ${instancesParam || '无'}`);
1122
  }
1123
+
1124
  disconnect() {
1125
  if (this.eventSource) {
1126
  this.eventSource.close();
 
1129
  }
1130
  }
1131
  }
1132
+
1133
  const metricsStreamManager = new MetricsStreamManager();
1134
  const instanceMap = new Map();
1135
  const serverStatus = new Map();
1136
  let allInstances = []; // 存储所有实例数据,用于过滤和排序
1137
  const chartInstances = new Map(); // 存储每个实例的图表实例
1138
  const chartDataBuffer = new Map(); // 存储图表数据缓冲以减少更新频率
1139
+
1140
  async function initialize() {
1141
  await getUsernames();
1142
  const instances = await fetchInstances();
 
1153
  updateSummary();
1154
  updateActionButtons(isLoggedIn);
1155
  }
1156
+
1157
  // 手动刷新数据函数
1158
  async function refreshData() {
1159
  metricsStreamManager.disconnect();
 
1168
  await initialize();
1169
  applyFiltersAndSort(); // 确保刷新后重新应用过滤和排序
1170
  }
1171
+
1172
  function renderInstances(instances) {
1173
  const serversContainer = document.getElementById('servers');
1174
  serversContainer.innerHTML = ''; // 清空现有内容
1175
  const userGroups = {};
1176
+
1177
  // 按用户分组
1178
  instances.forEach(instance => {
1179
  if (!userGroups[instance.owner]) {
 
1181
  }
1182
  userGroups[instance.owner].push(instance);
1183
  });
1184
+
1185
  // 渲染每个用户组
1186
  Object.keys(userGroups).forEach(owner => {
1187
  let userGroup = document.createElement('details');
 
1198
  userGroup.appendChild(userServers);
1199
 
1200
  serversContainer.appendChild(userGroup);
1201
+
1202
  // 渲染该用户下的实例
1203
  userGroups[owner].forEach(instance => {
1204
  renderInstanceCard(instance, userServers);
1205
  });
1206
  });
1207
  }
1208
+
1209
  // 图表配置函数
1210
  function createChart(instanceId) {
1211
  const canvasId = `chart-${instanceId}`;
1212
  const canvas = document.getElementById(canvasId);
1213
  if (!canvas) return null;
1214
+
1215
  const gridColor = 'rgba(0, 212, 255, 0.1)';
1216
  const textColor = '#E0E0FF';
1217
  const cpuColor = '#00FFAA';
1218
  const memoryColor = '#00D4FF';
1219
  const uploadColor = '#FF9500';
1220
  const downloadColor = '#FF00FF';
1221
+
1222
  const ctx = canvas.getContext('2d');
1223
  const chart = new Chart(ctx, {
1224
  type: 'line',
 
1318
  chartInstances.set(instanceId, chart);
1319
  return chart;
1320
  }
1321
+
1322
  // 更新图表数据,带有缓冲机制减少更新频率
1323
  function updateChart(instanceId, data) {
1324
  let buffer = chartDataBuffer.get(instanceId) || { data: [], count: 0 };
1325
  buffer.data.push(data);
1326
  buffer.count++;
1327
+
1328
  if (buffer.count < 2) { // 每 2 次数据更新才渲染一次
1329
  chartDataBuffer.set(instanceId, buffer);
1330
  return;
1331
  }
1332
+
1333
  let chart = chartInstances.get(instanceId);
1334
  if (!chart) {
1335
  chart = createChart(instanceId);
1336
  if (!chart) return;
1337
  }
1338
+
1339
  // 获取当前数据集
1340
  const cpuData = chart.data.datasets[0].data;
1341
  const memoryData = chart.data.datasets[1].data;
1342
  const uploadData = chart.data.datasets[2].data;
1343
  const downloadData = chart.data.datasets[3].data;
1344
+
1345
  // 追加最新数据
1346
  const latestData = buffer.data[buffer.data.length - 1];
1347
  cpuData.push(latestData.cpu_usage_pct);
1348
  memoryData.push(((latestData.memory_used_bytes / latestData.memory_total_bytes) * 100).toFixed(2));
1349
  uploadData.push((latestData.tx_bps / 1024).toFixed(2)); // 转换为 KB/s
1350
  downloadData.push((latestData.rx_bps / 1024).toFixed(2)); // 转换为 KB/s
1351
+
1352
  // 限制数据点数量为 30 个
1353
  if (cpuData.length > 30) {
1354
  cpuData.shift();
 
1356
  uploadData.shift();
1357
  downloadData.shift();
1358
  }
1359
+
1360
  // 更新图表
1361
  chart.update();
1362
 
1363
  // 清空缓冲
1364
  chartDataBuffer.set(instanceId, { data: [], count: 0 });
1365
  }
1366
+
1367
  // 计算相对时间的辅助函数
1368
  function formatRelativeTime(dateStr) {
1369
  if (!dateStr) return '未知时间';
 
1375
  const diffMins = Math.floor(diffSecs / 60);
1376
  const diffHrs = Math.floor(diffMins / 60);
1377
  const diffDays = Math.floor(diffHrs / 24);
1378
+
1379
  if (diffDays > 7) {
1380
  return date.toLocaleDateString(); // 超过7天显示日期
1381
  } else if (diffDays > 0) {
 
1392
  return '未知时间';
1393
  }
1394
  }
1395
+
1396
  // 切换信息块显示/隐藏的函数
1397
  function toggleInfoBlock(instanceId) {
1398
  const card = document.getElementById(`instance-${instanceId}`);
1399
  if (!card) return;
1400
+
1401
  card.classList.toggle('info-expanded');
1402
  }
1403
+
1404
  function renderInstanceCard(instance, container) {
1405
  const instanceId = instance.repo_id;
1406
  instanceMap.set(instanceId, instance);
1407
+
1408
  const cardId = `instance-${instanceId}`;
1409
  let card = document.getElementById(cardId);
1410
  if (!card) {
 
1427
  const description = instance.short_description && instance.short_description.trim() ? instance.short_description : 'N/A';
1428
  // 处理 last_modified,格式化为相对时间
1429
  const lastModified = formatRelativeTime(instance.last_modified);
1430
+
1431
  card.innerHTML = `
1432
  <div class="server-header">
1433
  <div class="server-name" onclick="toggleInfoBlock('${instanceId}')">
 
1494
  }
1495
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline: initialStatus === 'running', isSleep: initialStatus === 'sleeping', data: null, status: instance.status });
1496
  }
1497
+
1498
  // 切换图表显示/隐藏
1499
  function toggleChart(instanceId) {
1500
  const card = document.getElementById(`instance-${instanceId}`);
1501
  const chartContainer = document.getElementById(`chart-container-${instanceId}`);
1502
  const toggleButton = card.querySelector('.chart-toggle-button');
1503
  if (!card || !chartContainer) return;
1504
+
1505
  if (card.classList.contains('expanded')) {
1506
  card.classList.remove('expanded');
1507
  toggleButton.textContent = '查看图表';
 
1514
  }
1515
  }
1516
  }
1517
+
1518
  function updateServerCard(data, instanceId, isSleep = false) {
1519
  const cardId = `instance-${instanceId}`;
1520
  let card = document.getElementById(cardId);
 
1524
  // 如果卡片不存在,但实例存在,说明可能被过滤掉了,不渲染
1525
  return;
1526
  }
1527
+
1528
  if (card) {
1529
  const statusDot = card.querySelector('.status-dot');
1530
  let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
1531
  let isOnline = false;
1532
+
1533
  if (data) {
1534
  cpuUsage = `${data.cpu_usage_pct}%`;
1535
  memoryUsage = `${((data.memory_used_bytes / data.memory_total_bytes) * 100).toFixed(2)}%`;
 
1556
  isSleep = false;
1557
  }
1558
  }
1559
+
1560
  card.querySelector('.cpu-usage').textContent = cpuUsage;
1561
  card.querySelector('.memory-usage').textContent = memoryUsage;
1562
  card.querySelector('.upload').textContent = upload;
1563
  card.querySelector('.download').textContent = download;
1564
+
1565
  serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
1566
  updateSummary();
1567
  }
1568
  }
1569
+
1570
  async function restartSpace(repoId) {
1571
  try {
1572
  const token = localStorage.getItem('authToken');
 
1609
  alert(`重启失败: ${error.message}`);
1610
  }
1611
  }
1612
+
1613
  async function rebuildSpace(repoId) {
1614
  try {
1615
  const token = localStorage.getItem('authToken');
 
1652
  alert(`重建失败: ${error.message}`);
1653
  }
1654
  }
1655
+
1656
  function updateSummary() {
1657
  let online = 0;
1658
  let offline = 0;
1659
  let totalUpload = 0;
1660
  let totalDownload = 0;
1661
+
1662
  serverStatus.forEach((status, instanceId) => {
1663
  const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
1664
  if (isRecentlyOnline) {
 
1671
  offline++;
1672
  }
1673
  });
1674
+
1675
  document.getElementById('totalServers').textContent = serverStatus.size;
1676
  document.getElementById('onlineServers').textContent = online;
1677
  document.getElementById('offlineServers').textContent = offline;
1678
  document.getElementById('totalUpload').textContent = `${formatBytes(totalUpload)}/s`;
1679
  document.getElementById('totalDownload').textContent = `${formatBytes(totalDownload)}/s`;
1680
  }
1681
+
1682
  function formatBytes(bytes) {
1683
  if (bytes === 0) return '0 B';
1684
  const k = 1024;
 
1686
  const i = Math.floor(Math.log(bytes) / Math.log(k));
1687
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1688
  }
1689
+
1690
  setInterval(updateSummary, 5000);
1691
+
1692
  setInterval(async () => {
1693
  metricsStreamManager.disconnect();
1694
  await initialize();
1695
  }, 300000);
1696
+
1697
  // 应用过滤和排序
1698
  function applyFiltersAndSort() {
1699
  const statusFilter = document.getElementById('statusFilter').value;
1700
  const userFilter = document.getElementById('userFilter').value;
1701
  const sortBy = document.getElementById('sortBy').value;
1702
+
1703
  // 过滤实例
1704
  let filteredInstances = allInstances;
1705
  if (statusFilter !== 'all') {
 
1708
  if (userFilter !== 'all') {
1709
  filteredInstances = filteredInstances.filter(instance => instance.owner === userFilter);
1710
  }
1711
+
1712
  // 排序实例
1713
  filteredInstances.sort((a, b) => {
1714
  if (sortBy === 'name-asc') {
 
1724
  }
1725
  return 0;
1726
  });
1727
+
1728
  // 重新渲染过滤和排序后的实例
1729
  instanceMap.clear();
1730
  serverStatus.clear();
 
1748
  updateSummary();
1749
  updateActionButtons(isLoggedIn);
1750
  }
1751
+
1752
  // 新增函数:在新标签页中打开实例的URL
1753
  function viewInstance(url) {
1754
  window.open(url, '_blank');
1755
  }
1756
+
1757
  // 新增函数:在新标签页中打开实例的管理页面
1758
  function manageInstance(repoId) {
1759
  const manageUrl = `https://huggingface.co/spaces/${repoId}`;