Update public/index.html
Browse files- public/index.html +142 -36
public/index.html
CHANGED
@@ -280,12 +280,86 @@
|
|
280 |
grid-template-columns: 1fr 1fr;
|
281 |
}
|
282 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
</style>
|
284 |
</head>
|
285 |
<body>
|
286 |
<div class="container">
|
287 |
<div class="overview">
|
288 |
-
<div class="
|
|
|
|
|
|
|
289 |
<div class="theme-toggle">
|
290 |
主题:
|
291 |
<button onclick="toggleTheme('system')" title="跟随系统">
|
@@ -316,6 +390,15 @@
|
|
316 |
<div id="servers" class="stats-container">
|
317 |
</div>
|
318 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
319 |
|
320 |
<script>
|
321 |
// 主题切换功能
|
@@ -354,11 +437,64 @@
|
|
354 |
|
355 |
initTheme();
|
356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
357 |
async function getUsernames() {
|
358 |
try {
|
359 |
const response = await fetch('/api/config');
|
360 |
const config = await response.json();
|
361 |
-
console.log('Usernames from API:', config.usernames);
|
362 |
const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
|
363 |
document.getElementById('totalUsers').textContent = usernamesList.length;
|
364 |
return usernamesList;
|
@@ -373,7 +509,6 @@
|
|
373 |
try {
|
374 |
const response = await fetch('/api/proxy/spaces');
|
375 |
const instances = await response.json();
|
376 |
-
console.log(`Found ${instances.length} spaces`);
|
377 |
return instances;
|
378 |
} catch (error) {
|
379 |
console.error("获取实例列表失败:", error);
|
@@ -389,12 +524,10 @@
|
|
389 |
|
390 |
async connect(instanceId, username) {
|
391 |
if (this.eventSources.has(instanceId)) {
|
392 |
-
console.log(`已经连接到实例 ${instanceId} 的监控数据,跳过重复连接`);
|
393 |
return;
|
394 |
}
|
395 |
|
396 |
try {
|
397 |
-
console.log(`尝试连接监控数据: /api/proxy/live-metrics/${username}/${instanceId.split('/')[1]}`);
|
398 |
const eventSource = new EventSource(
|
399 |
`/api/proxy/live-metrics/${username}/${instanceId.split('/')[1]}`
|
400 |
);
|
@@ -404,35 +537,26 @@
|
|
404 |
eventSource.addEventListener("metric", (event) => {
|
405 |
try {
|
406 |
const data = JSON.parse(event.data);
|
407 |
-
console.log(`收到监控数据 (${instanceId}):`, data);
|
408 |
updateServerCard(data, instanceId);
|
409 |
} catch (error) {
|
410 |
-
console.error(`解析数据失败 (${instanceId}):`, error
|
411 |
}
|
412 |
});
|
413 |
|
414 |
eventSource.onerror = (error) => {
|
415 |
-
console.error(`EventSource 错误 (${instanceId}):`, error);
|
416 |
eventSource.close();
|
417 |
this.eventSources.delete(instanceId);
|
418 |
-
// 不设置为 sleep,通过 status 判断状态
|
419 |
updateServerCard(null, instanceId, false);
|
420 |
};
|
421 |
|
422 |
-
eventSource.onopen = () => {
|
423 |
-
console.log(`成功打开 EventSource 连接 (${instanceId})`);
|
424 |
-
};
|
425 |
-
|
426 |
this.eventSources.set(instanceId, eventSource);
|
427 |
-
console.log(`已创建 EventSource 连接 (${instanceId}),当前总连接数: ${this.eventSources.size}`);
|
428 |
} catch (error) {
|
429 |
console.error(`连接失败 (${username}/${instanceId}):`, error);
|
430 |
-
updateServerCard(null, instanceId, false);
|
431 |
}
|
432 |
}
|
433 |
|
434 |
disconnectAll() {
|
435 |
-
console.log(`断开所有监控数据连接 (${this.eventSources.size} 个)`);
|
436 |
this.eventSources.forEach(es => es.close());
|
437 |
this.eventSources.clear();
|
438 |
}
|
@@ -443,16 +567,12 @@
|
|
443 |
const serverStatus = new Map();
|
444 |
|
445 |
async function initialize() {
|
446 |
-
await getUsernames();
|
447 |
const instances = await fetchInstances();
|
448 |
instances.forEach(instance => {
|
449 |
renderInstanceCard(instance);
|
450 |
-
// 只连接状态为 running 的实例
|
451 |
if (instance.status.toLowerCase() === 'running') {
|
452 |
-
console.log(`实例 ${instance.repo_id} 状态为 running,尝试连接监控数据`);
|
453 |
metricsManager.connect(instance.repo_id, instance.owner);
|
454 |
-
} else {
|
455 |
-
console.log(`实例 ${instance.repo_id} 状态为 ${instance.status},不连接监控数据`);
|
456 |
}
|
457 |
});
|
458 |
updateSummary();
|
@@ -483,11 +603,9 @@
|
|
483 |
}
|
484 |
|
485 |
const userServers = userGroup.querySelector('.user-servers');
|
486 |
-
// 使用完整 repo_id 作为卡片 ID
|
487 |
const cardId = `instance-${instanceId}`;
|
488 |
let card = document.getElementById(cardId);
|
489 |
if (!card) {
|
490 |
-
console.log(`渲染卡片,instanceId: ${instanceId}, cardId: ${cardId}`);
|
491 |
card = document.createElement('div');
|
492 |
card.id = cardId;
|
493 |
card.className = 'server-card';
|
@@ -523,14 +641,13 @@
|
|
523 |
<div class="metric-value download">N/A</div>
|
524 |
</div>
|
525 |
</div>
|
526 |
-
<div class="action-buttons">
|
527 |
<button class="action-button" onclick="restartSpace('${instance.repo_id}')">重启</button>
|
528 |
<button class="action-button" onclick="rebuildSpace('${instance.repo_id}')">重建</button>
|
529 |
</div>
|
530 |
`;
|
531 |
userServers.appendChild(card);
|
532 |
}
|
533 |
-
// 根据状态设置初始显示
|
534 |
const statusDot = card.querySelector('.status-dot');
|
535 |
const initialStatus = instance.status.toLowerCase();
|
536 |
if (initialStatus === 'running') {
|
@@ -544,19 +661,16 @@
|
|
544 |
}
|
545 |
|
546 |
function updateServerCard(data, instanceId, isSleep = false) {
|
547 |
-
// 卡片 ID 使用完整 repo_id
|
548 |
const cardId = `instance-${instanceId}`;
|
549 |
let card = document.getElementById(cardId);
|
550 |
const instance = instanceMap.get(instanceId);
|
551 |
|
552 |
if (!card && instance) {
|
553 |
-
console.log(`卡片 ${cardId} 不存在,重新渲染`);
|
554 |
renderInstanceCard(instance);
|
555 |
card = document.getElementById(cardId);
|
556 |
}
|
557 |
|
558 |
if (card) {
|
559 |
-
console.log(`更新卡片显示,instanceId: ${instanceId}, cardId: ${cardId}`);
|
560 |
const statusDot = card.querySelector('.status-dot');
|
561 |
let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
|
562 |
let isOnline = false;
|
@@ -569,9 +683,7 @@
|
|
569 |
statusDot.className = 'status-dot status-online';
|
570 |
isOnline = true;
|
571 |
isSleep = false;
|
572 |
-
console.log(`更新监控数据 (${instanceId}): CPU=${cpuUsage}, 内存=${memoryUsage}, 上传=${upload}, 下载=${download}`);
|
573 |
} else {
|
574 |
-
// 如果没有监控数据,根据实例状态设置图标
|
575 |
const currentStatus = instance?.status.toLowerCase() || 'unknown';
|
576 |
if (currentStatus === 'running') {
|
577 |
statusDot.className = 'status-dot status-online';
|
@@ -586,7 +698,6 @@
|
|
586 |
isOnline = false;
|
587 |
isSleep = false;
|
588 |
}
|
589 |
-
console.log(`无监控数据,基于状态更新 (${instanceId}): 状态=${currentStatus}`);
|
590 |
}
|
591 |
|
592 |
card.querySelector('.cpu-usage').textContent = cpuUsage;
|
@@ -596,14 +707,11 @@
|
|
596 |
|
597 |
serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
|
598 |
updateSummary();
|
599 |
-
} else {
|
600 |
-
console.error(`卡片 ${cardId} 未找到,无法更新显示`);
|
601 |
}
|
602 |
}
|
603 |
|
604 |
async function restartSpace(repoId) {
|
605 |
try {
|
606 |
-
console.log(`尝试重启 Space: ${repoId}`);
|
607 |
const encodedRepoId = encodeURIComponent(repoId);
|
608 |
const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, { method: 'POST' });
|
609 |
const result = await response.json();
|
@@ -621,7 +729,6 @@
|
|
621 |
|
622 |
async function rebuildSpace(repoId) {
|
623 |
try {
|
624 |
-
console.log(`尝试重建 Space: ${repoId}`);
|
625 |
const encodedRepoId = encodeURIComponent(repoId);
|
626 |
const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, { method: 'POST' });
|
627 |
const result = await response.json();
|
@@ -644,7 +751,6 @@
|
|
644 |
let totalDownload = 0;
|
645 |
|
646 |
serverStatus.forEach((status, instanceId) => {
|
647 |
-
const now = Date.now();
|
648 |
const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
|
649 |
if (isRecentlyOnline) {
|
650 |
online++;
|
|
|
280 |
grid-template-columns: 1fr 1fr;
|
281 |
}
|
282 |
}
|
283 |
+
.login-overlay {
|
284 |
+
position: fixed;
|
285 |
+
top: 0;
|
286 |
+
left: 0;
|
287 |
+
width: 100%;
|
288 |
+
height: 100%;
|
289 |
+
background: rgba(0, 0, 0, 0.7);
|
290 |
+
display: flex;
|
291 |
+
align-items: center;
|
292 |
+
justify-content: center;
|
293 |
+
z-index: 1000;
|
294 |
+
}
|
295 |
+
.login-box {
|
296 |
+
background: var(--card-background);
|
297 |
+
padding: 30px;
|
298 |
+
border-radius: 10px;
|
299 |
+
border: 1px solid var(--card-border);
|
300 |
+
width: 300px;
|
301 |
+
text-align: center;
|
302 |
+
}
|
303 |
+
.login-box h2 {
|
304 |
+
margin-bottom: 20px;
|
305 |
+
color: var(--text-color);
|
306 |
+
}
|
307 |
+
.login-box input {
|
308 |
+
width: 100%;
|
309 |
+
padding: 10px;
|
310 |
+
margin: 10px 0;
|
311 |
+
border: 1px solid var(--metric-border);
|
312 |
+
border-radius: 5px;
|
313 |
+
background: var(--metric-background);
|
314 |
+
color: var(--text-color);
|
315 |
+
}
|
316 |
+
.login-box button {
|
317 |
+
width: 100%;
|
318 |
+
padding: 10px;
|
319 |
+
background: var(--action-button-bg);
|
320 |
+
border: none;
|
321 |
+
border-radius: 5px;
|
322 |
+
color: var(--text-color);
|
323 |
+
cursor: pointer;
|
324 |
+
transition: background 0.2s ease;
|
325 |
+
}
|
326 |
+
.login-box button:hover {
|
327 |
+
background: var(--action-button-hover);
|
328 |
+
}
|
329 |
+
.login-error {
|
330 |
+
color: #f44336;
|
331 |
+
margin-top: 10px;
|
332 |
+
font-size: 14px;
|
333 |
+
}
|
334 |
+
.logout-button {
|
335 |
+
background: var(--action-button-bg);
|
336 |
+
border: none;
|
337 |
+
color: var(--text-color);
|
338 |
+
padding: 6px 12px;
|
339 |
+
border-radius: 4px;
|
340 |
+
cursor: pointer;
|
341 |
+
font-size: 13px;
|
342 |
+
transition: background 0.2s ease;
|
343 |
+
margin-left: auto;
|
344 |
+
}
|
345 |
+
.logout-button:hover {
|
346 |
+
background: var(--action-button-hover);
|
347 |
+
}
|
348 |
+
.header-container {
|
349 |
+
display: flex;
|
350 |
+
justify-content: space-between;
|
351 |
+
align-items: center;
|
352 |
+
margin-bottom: 20px;
|
353 |
+
}
|
354 |
</style>
|
355 |
</head>
|
356 |
<body>
|
357 |
<div class="container">
|
358 |
<div class="overview">
|
359 |
+
<div class="header-container">
|
360 |
+
<div class="overview-title">📊 系统概览</div>
|
361 |
+
<button class="logout-button" id="logoutButton" style="display: none;" onclick="logout()">登出</button>
|
362 |
+
</div>
|
363 |
<div class="theme-toggle">
|
364 |
主题:
|
365 |
<button onclick="toggleTheme('system')" title="跟随系统">
|
|
|
390 |
<div id="servers" class="stats-container">
|
391 |
</div>
|
392 |
</div>
|
393 |
+
<div id="loginOverlay" class="login-overlay" style="display: none;">
|
394 |
+
<div class="login-box">
|
395 |
+
<h2>登录</h2>
|
396 |
+
<input type="text" id="username" placeholder="用户名">
|
397 |
+
<input type="password" id="password" placeholder="密码">
|
398 |
+
<button onclick="login()">登录</button>
|
399 |
+
<div id="loginError" class="login-error" style="display: none;"></div>
|
400 |
+
</div>
|
401 |
+
</div>
|
402 |
|
403 |
<script>
|
404 |
// 主题切换功能
|
|
|
437 |
|
438 |
initTheme();
|
439 |
|
440 |
+
// 登录状态管理
|
441 |
+
function checkLoginStatus() {
|
442 |
+
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
443 |
+
document.getElementById('loginOverlay').style.display = isLoggedIn ? 'none' : 'flex';
|
444 |
+
document.getElementById('logoutButton').style.display = isLoggedIn ? 'block' : 'none';
|
445 |
+
if (isLoggedIn) {
|
446 |
+
updateActionButtons(true);
|
447 |
+
} else {
|
448 |
+
updateActionButtons(false);
|
449 |
+
}
|
450 |
+
}
|
451 |
+
|
452 |
+
function login() {
|
453 |
+
const username = document.getElementById('username').value;
|
454 |
+
const password = document.getElementById('password').value;
|
455 |
+
const loginError = document.getElementById('loginError');
|
456 |
+
|
457 |
+
// 模拟后端验证 (前端硬编码验证,实际环境中应通过接口验证)
|
458 |
+
fetch('/api/config').then(response => response.json()).then(config => {
|
459 |
+
// 这里仅做演示,实际环境中不应将用户凭据硬编码在前端
|
460 |
+
const adminUsername = 'admin'; // 默认值,实际应从环境变量获取,但这里无法直接获取后端环境变量
|
461 |
+
const adminPassword = 'password'; // 默认值,实际应从环境变量获取
|
462 |
+
if (username === adminUsername && password === adminPassword) {
|
463 |
+
localStorage.setItem('isLoggedIn', 'true');
|
464 |
+
loginError.style.display = 'none';
|
465 |
+
checkLoginStatus();
|
466 |
+
} else {
|
467 |
+
loginError.textContent = '用户名或密码错误';
|
468 |
+
loginError.style.display = 'block';
|
469 |
+
}
|
470 |
+
}).catch(err => {
|
471 |
+
loginError.textContent = '登录系统错误,请稍后重试';
|
472 |
+
loginError.style.display = 'block';
|
473 |
+
console.error('获取配置失败,无法验证登录:', err);
|
474 |
+
});
|
475 |
+
}
|
476 |
+
|
477 |
+
function logout() {
|
478 |
+
localStorage.removeItem('isLoggedIn');
|
479 |
+
checkLoginStatus();
|
480 |
+
}
|
481 |
+
|
482 |
+
function updateActionButtons(isLoggedIn) {
|
483 |
+
const cards = document.querySelectorAll('.server-card');
|
484 |
+
cards.forEach(card => {
|
485 |
+
const buttons = card.querySelector('.action-buttons');
|
486 |
+
if (buttons) {
|
487 |
+
buttons.style.display = isLoggedIn ? 'flex' : 'none';
|
488 |
+
}
|
489 |
+
});
|
490 |
+
}
|
491 |
+
|
492 |
+
window.onload = checkLoginStatus;
|
493 |
+
|
494 |
async function getUsernames() {
|
495 |
try {
|
496 |
const response = await fetch('/api/config');
|
497 |
const config = await response.json();
|
|
|
498 |
const usernamesList = config.usernames ? config.usernames.split(',').map(name => name.trim()).filter(name => name) : [];
|
499 |
document.getElementById('totalUsers').textContent = usernamesList.length;
|
500 |
return usernamesList;
|
|
|
509 |
try {
|
510 |
const response = await fetch('/api/proxy/spaces');
|
511 |
const instances = await response.json();
|
|
|
512 |
return instances;
|
513 |
} catch (error) {
|
514 |
console.error("获取实例列表失败:", error);
|
|
|
524 |
|
525 |
async connect(instanceId, username) {
|
526 |
if (this.eventSources.has(instanceId)) {
|
|
|
527 |
return;
|
528 |
}
|
529 |
|
530 |
try {
|
|
|
531 |
const eventSource = new EventSource(
|
532 |
`/api/proxy/live-metrics/${username}/${instanceId.split('/')[1]}`
|
533 |
);
|
|
|
537 |
eventSource.addEventListener("metric", (event) => {
|
538 |
try {
|
539 |
const data = JSON.parse(event.data);
|
|
|
540 |
updateServerCard(data, instanceId);
|
541 |
} catch (error) {
|
542 |
+
console.error(`解析数据失败 (${instanceId}):`, error);
|
543 |
}
|
544 |
});
|
545 |
|
546 |
eventSource.onerror = (error) => {
|
|
|
547 |
eventSource.close();
|
548 |
this.eventSources.delete(instanceId);
|
|
|
549 |
updateServerCard(null, instanceId, false);
|
550 |
};
|
551 |
|
|
|
|
|
|
|
|
|
552 |
this.eventSources.set(instanceId, eventSource);
|
|
|
553 |
} catch (error) {
|
554 |
console.error(`连接失败 (${username}/${instanceId}):`, error);
|
555 |
+
updateServerCard(null, instanceId, false);
|
556 |
}
|
557 |
}
|
558 |
|
559 |
disconnectAll() {
|
|
|
560 |
this.eventSources.forEach(es => es.close());
|
561 |
this.eventSources.clear();
|
562 |
}
|
|
|
567 |
const serverStatus = new Map();
|
568 |
|
569 |
async function initialize() {
|
570 |
+
await getUsernames();
|
571 |
const instances = await fetchInstances();
|
572 |
instances.forEach(instance => {
|
573 |
renderInstanceCard(instance);
|
|
|
574 |
if (instance.status.toLowerCase() === 'running') {
|
|
|
575 |
metricsManager.connect(instance.repo_id, instance.owner);
|
|
|
|
|
576 |
}
|
577 |
});
|
578 |
updateSummary();
|
|
|
603 |
}
|
604 |
|
605 |
const userServers = userGroup.querySelector('.user-servers');
|
|
|
606 |
const cardId = `instance-${instanceId}`;
|
607 |
let card = document.getElementById(cardId);
|
608 |
if (!card) {
|
|
|
609 |
card = document.createElement('div');
|
610 |
card.id = cardId;
|
611 |
card.className = 'server-card';
|
|
|
641 |
<div class="metric-value download">N/A</div>
|
642 |
</div>
|
643 |
</div>
|
644 |
+
<div class="action-buttons" style="display: none;">
|
645 |
<button class="action-button" onclick="restartSpace('${instance.repo_id}')">重启</button>
|
646 |
<button class="action-button" onclick="rebuildSpace('${instance.repo_id}')">重建</button>
|
647 |
</div>
|
648 |
`;
|
649 |
userServers.appendChild(card);
|
650 |
}
|
|
|
651 |
const statusDot = card.querySelector('.status-dot');
|
652 |
const initialStatus = instance.status.toLowerCase();
|
653 |
if (initialStatus === 'running') {
|
|
|
661 |
}
|
662 |
|
663 |
function updateServerCard(data, instanceId, isSleep = false) {
|
|
|
664 |
const cardId = `instance-${instanceId}`;
|
665 |
let card = document.getElementById(cardId);
|
666 |
const instance = instanceMap.get(instanceId);
|
667 |
|
668 |
if (!card && instance) {
|
|
|
669 |
renderInstanceCard(instance);
|
670 |
card = document.getElementById(cardId);
|
671 |
}
|
672 |
|
673 |
if (card) {
|
|
|
674 |
const statusDot = card.querySelector('.status-dot');
|
675 |
let upload = 'N/A', download = 'N/A', cpuUsage = 'N/A', memoryUsage = 'N/A';
|
676 |
let isOnline = false;
|
|
|
683 |
statusDot.className = 'status-dot status-online';
|
684 |
isOnline = true;
|
685 |
isSleep = false;
|
|
|
686 |
} else {
|
|
|
687 |
const currentStatus = instance?.status.toLowerCase() || 'unknown';
|
688 |
if (currentStatus === 'running') {
|
689 |
statusDot.className = 'status-dot status-online';
|
|
|
698 |
isOnline = false;
|
699 |
isSleep = false;
|
700 |
}
|
|
|
701 |
}
|
702 |
|
703 |
card.querySelector('.cpu-usage').textContent = cpuUsage;
|
|
|
707 |
|
708 |
serverStatus.set(instanceId, { lastSeen: Date.now(), isOnline, isSleep, data: data || null, status: instance?.status || 'unknown' });
|
709 |
updateSummary();
|
|
|
|
|
710 |
}
|
711 |
}
|
712 |
|
713 |
async function restartSpace(repoId) {
|
714 |
try {
|
|
|
715 |
const encodedRepoId = encodeURIComponent(repoId);
|
716 |
const response = await fetch(`/api/proxy/restart/${encodedRepoId}`, { method: 'POST' });
|
717 |
const result = await response.json();
|
|
|
729 |
|
730 |
async function rebuildSpace(repoId) {
|
731 |
try {
|
|
|
732 |
const encodedRepoId = encodeURIComponent(repoId);
|
733 |
const response = await fetch(`/api/proxy/rebuild/${encodedRepoId}`, { method: 'POST' });
|
734 |
const result = await response.json();
|
|
|
751 |
let totalDownload = 0;
|
752 |
|
753 |
serverStatus.forEach((status, instanceId) => {
|
|
|
754 |
const isRecentlyOnline = status.isOnline || status.status.toLowerCase() === 'running';
|
755 |
if (isRecentlyOnline) {
|
756 |
online++;
|