Update public/index.html
Browse files- 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}`;
|