Rename to hkt.sh
This commit is contained in:
61
projects/status-panel/public/agent.sh
Normal file
61
projects/status-panel/public/agent.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Status Panel Agent
|
||||
|
||||
SECRET="$1"
|
||||
SERVER="$2"
|
||||
|
||||
if [ -z "$SECRET" ] || [ -z "$SERVER" ]; then
|
||||
echo "Usage: $0 <secret> <server_url>"
|
||||
echo "Example: $0 abc123 http://1.2.3.4:3800"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
# CPU
|
||||
CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 2>/dev/null || echo 0)
|
||||
[ -z "$CPU" ] && CPU=$(top -bn1 | awk '/%Cpu/ {print $2}' | head -1)
|
||||
|
||||
# Memory
|
||||
MEM_INFO=$(free -b | awk '/Mem:/ {print $3,$2}')
|
||||
MEM_USED=$(echo $MEM_INFO | awk '{print $1}')
|
||||
MEM_TOTAL=$(echo $MEM_INFO | awk '{print $2}')
|
||||
|
||||
# Disk
|
||||
DISK_INFO=$(df -B1 / | awk 'NR==2 {print $3,$2}')
|
||||
DISK_USED=$(echo $DISK_INFO | awk '{print $1}')
|
||||
DISK_TOTAL=$(echo $DISK_INFO | awk '{print $2}')
|
||||
|
||||
# Network (需要计算差值)
|
||||
NET_INFO=$(cat /proc/net/dev | grep -E 'eth0|ens|enp' | awk '{print $2,$10}')
|
||||
NET_IN=$(echo $NET_INFO | awk '{print $1}')
|
||||
NET_OUT=$(echo $NET_INFO | awk '{print $2}')
|
||||
|
||||
# Uptime
|
||||
UPTIME=$(cat /proc/uptime | awk '{print int($1)}')
|
||||
|
||||
# Load
|
||||
LOAD=$(cat /proc/loadavg | awk '{print $1,$2,$3}')
|
||||
LOAD1=$(echo $LOAD | awk '{print $1}')
|
||||
LOAD5=$(echo $LOAD | awk '{print $2}')
|
||||
LOAD15=$(echo $LOAD | awk '{print $3}')
|
||||
|
||||
# Send
|
||||
curl -s -X POST "$SERVER/report" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"secret\": \"$SECRET\",
|
||||
\"cpu\": $CPU,
|
||||
\"mem_used\": $MEM_USED,
|
||||
\"mem_total\": $MEM_TOTAL,
|
||||
\"disk_used\": $DISK_USED,
|
||||
\"disk_total\": $DISK_TOTAL,
|
||||
\"net_in\": $NET_IN,
|
||||
\"net_out\": $NET_OUT,
|
||||
\"uptime\": $UPTIME,
|
||||
\"load1\": $LOAD1,
|
||||
\"load5\": $LOAD5,
|
||||
\"load15\": $LOAD15
|
||||
}" > /dev/null 2>&1
|
||||
|
||||
sleep 5
|
||||
done
|
||||
178
projects/status-panel/public/index.html
Normal file
178
projects/status-panel/public/index.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>服务器探针</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); min-height: 100vh; color: #fff; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||
h1 { text-align: center; padding: 30px 0; font-weight: 300; }
|
||||
.nodes { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
|
||||
.node { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 20px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); }
|
||||
.node.offline { opacity: 0.5; border-color: #ff4757; }
|
||||
.node-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.node-name { font-size: 18px; font-weight: 500; }
|
||||
.status { width: 10px; height: 10px; border-radius: 50%; background: #2ed573; box-shadow: 0 0 10px #2ed573; }
|
||||
.status.offline { background: #ff4757; box-shadow: 0 0 10px #ff4757; }
|
||||
.row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.row:last-child { border: none; }
|
||||
.label { color: rgba(255,255,255,0.6); }
|
||||
.value { font-family: 'SF Mono', Monaco, monospace; }
|
||||
.progress { height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; margin-top: 5px; overflow: hidden; }
|
||||
.progress-bar { height: 100%; border-radius: 3px; transition: width 0.5s; }
|
||||
.progress-bar.cpu { background: linear-gradient(90deg, #2ed573, #1e90ff); }
|
||||
.progress-bar.mem { background: linear-gradient(90deg, #ffa502, #ff6348); }
|
||||
.progress-bar.disk { background: linear-gradient(90deg, #a55eea, #5352ed); }
|
||||
.admin { position: fixed; top: 20px; right: 20px; }
|
||||
.btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 10px 20px; border-radius: 8px; cursor: pointer; }
|
||||
.btn:hover { background: rgba(255,255,255,0.2); }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); justify-content: center; align-items: center; }
|
||||
.modal.show { display: flex; }
|
||||
.modal-content { background: #1a1a2e; padding: 30px; border-radius: 12px; max-width: 500px; width: 90%; }
|
||||
.modal-content h2 { margin-bottom: 20px; }
|
||||
.modal-content input { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #fff; border-radius: 8px; }
|
||||
.code-block { background: #000; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px; overflow-x: auto; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🖥️ 服务器探针</h1>
|
||||
<div class="nodes" id="nodes"></div>
|
||||
</div>
|
||||
<div class="admin">
|
||||
<button class="btn" onclick="showAddModal()">+ 添加节点</button>
|
||||
</div>
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-content">
|
||||
<h2 id="modalTitle">添加节点</h2>
|
||||
<input type="text" id="nodeName" placeholder="节点名称">
|
||||
<div id="addForm"><button class="btn" onclick="addNode()">添加</button></div>
|
||||
<div id="result" style="display:none">
|
||||
<p>Agent 命令(在目标服务器执行):</p>
|
||||
<div class="code-block" id="agentCmd"></div>
|
||||
</div>
|
||||
<button class="btn" style="margin-top:15px" onclick="closeModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const API = '/api/nodes';
|
||||
let ws;
|
||||
|
||||
// 加载节点
|
||||
async function loadNodes() {
|
||||
const res = await fetch(API);
|
||||
const nodes = await res.json();
|
||||
render(nodes);
|
||||
}
|
||||
|
||||
// 渲染
|
||||
function render(nodes) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
document.getElementById('nodes').innerHTML = nodes.map(n => {
|
||||
const online = n.last_seen && (now - n.last_seen) < 60;
|
||||
const cpu = n.cpu || 0;
|
||||
const mem = n.mem_total ? (n.mem_used / n.mem_total * 100) : 0;
|
||||
const disk = n.disk_total ? (n.disk_used / n.disk_total * 100) : 0;
|
||||
return `
|
||||
<div class="node ${online ? '' : 'offline'}">
|
||||
<div class="node-header">
|
||||
<span class="node-name">${n.name}</span>
|
||||
<span class="status ${online ? '' : 'offline'}"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">CPU</span>
|
||||
<span class="value">${cpu.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="progress"><div class="progress-bar cpu" style="width:${cpu}%"></div></div>
|
||||
<div class="row">
|
||||
<span class="label">内存</span>
|
||||
<span class="value">${formatSize(n.mem_used)} / ${formatSize(n.mem_total)}</span>
|
||||
</div>
|
||||
<div class="progress"><div class="progress-bar mem" style="width:${mem}%"></div></div>
|
||||
<div class="row">
|
||||
<span class="label">磁盘</span>
|
||||
<span class="value">${formatSize(n.disk_used)} / ${formatSize(n.disk_total)}</span>
|
||||
</div>
|
||||
<div class="progress"><div class="progress-bar disk" style="width:${disk}%"></div></div>
|
||||
<div class="row">
|
||||
<span class="label">网络 ↑↓</span>
|
||||
<span class="value">${formatSize(n.net_out)}/s | ${formatSize(n.net_in)}/s</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">负载</span>
|
||||
<span class="value">${(n.load1||0).toFixed(2)} ${(n.load5||0).toFixed(2)} ${(n.load15||0).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">在线</span>
|
||||
<span class="value">${formatUptime(n.uptime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formatSize(b) {
|
||||
if (!b) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
while (b >= 1024 && i < 4) { b /= 1024; i++; }
|
||||
return b.toFixed(1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function formatUptime(s) {
|
||||
if (!s) return '-';
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
return d + '天' + h + '小时';
|
||||
}
|
||||
|
||||
// WebSocket
|
||||
function connectWS() {
|
||||
ws = new WebSocket(`ws://${location.host}`);
|
||||
ws.onmessage = e => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'update') loadNodes();
|
||||
};
|
||||
ws.onclose = () => setTimeout(connectWS, 3000);
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
async function addNode() {
|
||||
const name = document.getElementById('nodeName').value;
|
||||
if (!name) return alert('请输入名称');
|
||||
const res = await fetch(API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) return alert(data.error);
|
||||
|
||||
const server = location.host.split(':')[0];
|
||||
const port = location.port || (location.protocol === 'https:' ? 443 : 80);
|
||||
const cmd = `curl -sSL http://${server}:${port}/agent.sh | bash -s -- ${data.secret} http://${server}:${port}`;
|
||||
document.getElementById('agentCmd').textContent = cmd;
|
||||
document.getElementById('addForm').style.display = 'none';
|
||||
document.getElementById('result').style.display = 'block';
|
||||
loadNodes();
|
||||
}
|
||||
|
||||
function showAddModal() {
|
||||
document.getElementById('nodeName').value = '';
|
||||
document.getElementById('addForm').style.display = 'block';
|
||||
document.getElementById('result').style.display = 'none';
|
||||
document.getElementById('modal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('show');
|
||||
}
|
||||
|
||||
loadNodes();
|
||||
connectWS();
|
||||
setInterval(loadNodes, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user