Update public/index.html
Browse files- public/index.html +115 -48
public/index.html
CHANGED
|
@@ -546,6 +546,17 @@
|
|
| 546 |
height: 250px; /* 小屏幕图表高度略减 */
|
| 547 |
}
|
| 548 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
</style>
|
| 550 |
</head>
|
| 551 |
<body>
|
|
@@ -934,53 +945,84 @@
|
|
| 934 |
}
|
| 935 |
}
|
| 936 |
|
| 937 |
-
class
|
| 938 |
constructor() {
|
| 939 |
-
this.
|
| 940 |
-
this.
|
|
|
|
| 941 |
}
|
| 942 |
|
| 943 |
-
|
| 944 |
-
if (this.
|
| 945 |
-
|
| 946 |
}
|
| 947 |
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
});
|
| 963 |
-
|
| 964 |
-
eventSource.onerror = (error) => {
|
| 965 |
-
eventSource.close();
|
| 966 |
-
this.eventSources.delete(instanceId);
|
| 967 |
-
updateServerCard(null, instanceId, false);
|
| 968 |
-
};
|
| 969 |
-
|
| 970 |
-
this.eventSources.set(instanceId, eventSource);
|
| 971 |
-
} catch (error) {
|
| 972 |
-
console.error(`连接失败 (${username}/${instanceId}):`, error);
|
| 973 |
-
updateServerCard(null, instanceId, false);
|
| 974 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
}
|
| 976 |
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
|
|
|
|
|
|
|
|
|
| 980 |
}
|
| 981 |
}
|
| 982 |
|
| 983 |
-
const
|
| 984 |
const instanceMap = new Map();
|
| 985 |
const serverStatus = new Map();
|
| 986 |
let allInstances = []; // 存储所有实例数据,用于过滤和排序
|
|
@@ -991,18 +1033,21 @@
|
|
| 991 |
const instances = await fetchInstances();
|
| 992 |
allInstances = instances;
|
| 993 |
renderInstances(allInstances);
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
|
|
|
|
|
|
|
|
|
| 999 |
updateSummary();
|
| 1000 |
updateActionButtons(isLoggedIn);
|
| 1001 |
}
|
| 1002 |
|
| 1003 |
// 手动刷新数据函数
|
| 1004 |
async function refreshData() {
|
| 1005 |
-
|
| 1006 |
// 销毁所有图表实例
|
| 1007 |
chartInstances.forEach(chart => {
|
| 1008 |
if (chart) {
|
|
@@ -1205,7 +1250,13 @@
|
|
| 1205 |
</svg>
|
| 1206 |
<div>${instance.name} (${instance.repo_id})</div>
|
| 1207 |
</div>
|
| 1208 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
</div>
|
| 1210 |
<div class="metric-grid">
|
| 1211 |
<div class="metric-item">
|
|
@@ -1446,7 +1497,7 @@
|
|
| 1446 |
setInterval(updateSummary, 5000);
|
| 1447 |
|
| 1448 |
setInterval(async () => {
|
| 1449 |
-
|
| 1450 |
await initialize();
|
| 1451 |
}, 300000);
|
| 1452 |
|
|
@@ -1484,7 +1535,7 @@
|
|
| 1484 |
// 重新渲染过滤和排序后的实例
|
| 1485 |
instanceMap.clear();
|
| 1486 |
serverStatus.clear();
|
| 1487 |
-
|
| 1488 |
// 销毁现有图表实例
|
| 1489 |
chartInstances.forEach(chart => {
|
| 1490 |
if (chart) {
|
|
@@ -1493,14 +1544,30 @@
|
|
| 1493 |
});
|
| 1494 |
chartInstances.clear();
|
| 1495 |
renderInstances(filteredInstances);
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
|
|
|
|
|
|
|
|
|
| 1501 |
updateSummary();
|
| 1502 |
updateActionButtons(isLoggedIn);
|
| 1503 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1504 |
</script>
|
| 1505 |
</body>
|
| 1506 |
</html>
|
|
|
|
| 546 |
height: 250px; /* 小屏幕图表高度略减 */
|
| 547 |
}
|
| 548 |
}
|
| 549 |
+
/* 监控复选框样式 */
|
| 550 |
+
input[type="checkbox"] {
|
| 551 |
+
accent-color: #4CAF50; /* 选中时颜色 */
|
| 552 |
+
width: 16px;
|
| 553 |
+
height: 16px;
|
| 554 |
+
cursor: pointer;
|
| 555 |
+
}
|
| 556 |
+
label {
|
| 557 |
+
color: var(--text-color);
|
| 558 |
+
user-select: none;
|
| 559 |
+
}
|
| 560 |
</style>
|
| 561 |
</head>
|
| 562 |
<body>
|
|
|
|
| 945 |
}
|
| 946 |
}
|
| 947 |
|
| 948 |
+
class MetricsStreamManager {
|
| 949 |
constructor() {
|
| 950 |
+
this.eventSource = null;
|
| 951 |
+
this.clientId = null;
|
| 952 |
+
this.subscribedInstances = new Set();
|
| 953 |
}
|
| 954 |
|
| 955 |
+
connect(subscribedInstances = []) {
|
| 956 |
+
if (this.eventSource) {
|
| 957 |
+
this.eventSource.close();
|
| 958 |
}
|
| 959 |
|
| 960 |
+
this.subscribedInstances = new Set(subscribedInstances);
|
| 961 |
+
const instancesParam = Array.from(this.subscribedInstances).join(',');
|
| 962 |
+
const url = `/api/proxy/live-metrics-stream?instances=${encodeURIComponent(instancesParam)}`;
|
| 963 |
+
this.eventSource = new EventSource(url);
|
| 964 |
+
|
| 965 |
+
this.eventSource.addEventListener("metric", (event) => {
|
| 966 |
+
try {
|
| 967 |
+
const data = JSON.parse(event.data);
|
| 968 |
+
const { repoId, metrics } = data;
|
| 969 |
+
updateServerCard(metrics, repoId);
|
| 970 |
+
} catch (error) {
|
| 971 |
+
console.error(`解析监控数据失败:`, error);
|
| 972 |
+
}
|
| 973 |
+
});
|
| 974 |
|
| 975 |
+
this.eventSource.onerror = (error) => {
|
| 976 |
+
console.error(`SSE 连接错误:`, error);
|
| 977 |
+
this.eventSource.close();
|
| 978 |
+
this.eventSource = null;
|
| 979 |
+
// 可选:尝试重新连接
|
| 980 |
+
setTimeout(() => this.connect(Array.from(this.subscribedInstances)), 5000);
|
| 981 |
+
};
|
| 982 |
|
| 983 |
+
console.log(`SSE 连接已建立,订阅实例: ${instancesParam || '无'}`);
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
updateSubscriptions(newInstances) {
|
| 987 |
+
if (!this.clientId) {
|
| 988 |
+
this.clientId = localStorage.getItem('sseClientId') || crypto.randomUUID();
|
| 989 |
+
localStorage.setItem('sseClientId', this.clientId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
}
|
| 991 |
+
|
| 992 |
+
this.subscribedInstances = new Set(newInstances);
|
| 993 |
+
fetch('/api/proxy/update-subscriptions', {
|
| 994 |
+
method: 'POST',
|
| 995 |
+
headers: {
|
| 996 |
+
'Content-Type': 'application/json',
|
| 997 |
+
},
|
| 998 |
+
body: JSON.stringify({
|
| 999 |
+
clientId: this.clientId,
|
| 1000 |
+
instances: Array.from(this.subscribedInstances)
|
| 1001 |
+
})
|
| 1002 |
+
})
|
| 1003 |
+
.then(response => response.json())
|
| 1004 |
+
.then(data => {
|
| 1005 |
+
if (data.success) {
|
| 1006 |
+
console.log(`订阅更新成功: ${Array.from(this.subscribedInstances).join(', ') || '无'}`);
|
| 1007 |
+
} else {
|
| 1008 |
+
console.error(`订阅更新失败:`, data.error);
|
| 1009 |
+
}
|
| 1010 |
+
})
|
| 1011 |
+
.catch(error => {
|
| 1012 |
+
console.error(`更新订阅请求失败:`, error);
|
| 1013 |
+
});
|
| 1014 |
}
|
| 1015 |
|
| 1016 |
+
disconnect() {
|
| 1017 |
+
if (this.eventSource) {
|
| 1018 |
+
this.eventSource.close();
|
| 1019 |
+
this.eventSource = null;
|
| 1020 |
+
console.log(`SSE 连接已断开`);
|
| 1021 |
+
}
|
| 1022 |
}
|
| 1023 |
}
|
| 1024 |
|
| 1025 |
+
const metricsStreamManager = new MetricsStreamManager();
|
| 1026 |
const instanceMap = new Map();
|
| 1027 |
const serverStatus = new Map();
|
| 1028 |
let allInstances = []; // 存储所有实例数据,用于过滤和排序
|
|
|
|
| 1033 |
const instances = await fetchInstances();
|
| 1034 |
allInstances = instances;
|
| 1035 |
renderInstances(allInstances);
|
| 1036 |
+
|
| 1037 |
+
// 默认只监控状态为 running 的实例
|
| 1038 |
+
const runningInstances = instances
|
| 1039 |
+
.filter(instance => instance.status.toLowerCase() === 'running')
|
| 1040 |
+
.map(instance => instance.repo_id);
|
| 1041 |
+
|
| 1042 |
+
metricsStreamManager.connect(runningInstances);
|
| 1043 |
+
|
| 1044 |
updateSummary();
|
| 1045 |
updateActionButtons(isLoggedIn);
|
| 1046 |
}
|
| 1047 |
|
| 1048 |
// 手动刷新数据函数
|
| 1049 |
async function refreshData() {
|
| 1050 |
+
metricsStreamManager.disconnect();
|
| 1051 |
// 销毁所有图表实例
|
| 1052 |
chartInstances.forEach(chart => {
|
| 1053 |
if (chart) {
|
|
|
|
| 1250 |
</svg>
|
| 1251 |
<div>${instance.name} (${instance.repo_id})</div>
|
| 1252 |
</div>
|
| 1253 |
+
<div>
|
| 1254 |
+
<label style="display: inline-flex; align-items: center; gap: 5px; font-size: 13px; cursor: pointer;">
|
| 1255 |
+
监控
|
| 1256 |
+
<input type="checkbox" id="monitor-${instanceId}" onchange="toggleMonitorInstance('${instanceId}', this.checked)" ${instance.status.toLowerCase() === 'running' ? 'checked' : ''}>
|
| 1257 |
+
</label>
|
| 1258 |
+
<button class="chart-toggle-button" onclick="toggleChart('${instanceId}')">查看图表</button>
|
| 1259 |
+
</div>
|
| 1260 |
</div>
|
| 1261 |
<div class="metric-grid">
|
| 1262 |
<div class="metric-item">
|
|
|
|
| 1497 |
setInterval(updateSummary, 5000);
|
| 1498 |
|
| 1499 |
setInterval(async () => {
|
| 1500 |
+
metricsStreamManager.disconnect();
|
| 1501 |
await initialize();
|
| 1502 |
}, 300000);
|
| 1503 |
|
|
|
|
| 1535 |
// 重新渲染过滤和排序后的实例
|
| 1536 |
instanceMap.clear();
|
| 1537 |
serverStatus.clear();
|
| 1538 |
+
metricsStreamManager.disconnect();
|
| 1539 |
// 销毁现有图表实例
|
| 1540 |
chartInstances.forEach(chart => {
|
| 1541 |
if (chart) {
|
|
|
|
| 1544 |
});
|
| 1545 |
chartInstances.clear();
|
| 1546 |
renderInstances(filteredInstances);
|
| 1547 |
+
|
| 1548 |
+
// 重新计算当前订阅的实例,只保留仍可见的实例
|
| 1549 |
+
const visibleInstanceIds = new Set(filteredInstances.map(instance => instance.repo_id));
|
| 1550 |
+
const updatedSubscriptions = Array.from(metricsStreamManager.subscribedInstances)
|
| 1551 |
+
.filter(instanceId => visibleInstanceIds.has(instanceId));
|
| 1552 |
+
metricsStreamManager.subscribedInstances = new Set(updatedSubscriptions);
|
| 1553 |
+
metricsStreamManager.connect(updatedSubscriptions); // 重新连接
|
| 1554 |
+
|
| 1555 |
updateSummary();
|
| 1556 |
updateActionButtons(isLoggedIn);
|
| 1557 |
}
|
| 1558 |
+
|
| 1559 |
+
// 新增函数:切换实例监控状态
|
| 1560 |
+
function toggleMonitorInstance(instanceId, checked) {
|
| 1561 |
+
const checkbox = document.getElementById(`monitor-${instanceId}`);
|
| 1562 |
+
if (!checkbox) return;
|
| 1563 |
+
|
| 1564 |
+
if (checked) {
|
| 1565 |
+
metricsStreamManager.subscribedInstances.add(instanceId);
|
| 1566 |
+
} else {
|
| 1567 |
+
metricsStreamManager.subscribedInstances.delete(instanceId);
|
| 1568 |
+
}
|
| 1569 |
+
metricsStreamManager.updateSubscriptions(Array.from(metricsStreamManager.subscribedInstances));
|
| 1570 |
+
}
|
| 1571 |
</script>
|
| 1572 |
</body>
|
| 1573 |
</html>
|