Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
158 lines (152 loc) · 6.58 KB
/
Copy pathmain.js
File metadata and controls
158 lines (152 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
const TIMES_LIMIT = 48;
const nameMap = {
"picoclaw": "🦞龙虾",
"cloudreve": "🗄️网盘",
"frpc": "🌐内网穿透",
"adguardhome": "🛜家庭dns",
"home": "😎主页",
"postgresql": "🛢️pg数据库",
"nginx": "🧭反向代理",
"dhcpd": "🔗家庭dhcp服务器"
};
export default {
async fetch(request, env) {
return handleRequest(request, env);
}
};
function getStatus(cpu, mem, bad) {
return cpu > 60 || mem > 70 || bad >= 3 ? '#ff4466' : cpu > 30 || mem > 50 || bad > 0 ? '#ffaa44' : '#36d399';
}
function parseStatus(s) {
if (!s) return { text: '未知', color: '#ff4466' };
if (s.startsWith('Exited (0)')) {
return { text: '正常停止', color: '#ffaa44' };
}
if (s.startsWith('Exited (')) {
const code = s.match(/\((\d+)\)/)?.[1] || '?';
return { text: `异常退出 (${code})`, color: '#ff4466' };
}
if (s.startsWith('Up')) {
const t = s.replace(/^Up /, '').replace(/About an hour/, '约1小时').replace(/ hours?/, '小时').replace(/ minutes?/, '分钟');
return { text: `运行中 ${t}`, color: '#36d399' };
}
if (s.includes('Restarting')) return { text: '重启中', color: '#ffaa44' };
if (s.includes('Created')) return { text: '已创建', color: '#ffaa44' };
if (s.includes('Paused')) return { text: '已暂停', color: '#ffaa44' };
if (s.includes('Dead')) return { text: '已失效', color: '#ff4466' };
return { text: s, color: '#ff4466' };
}
async function handleRequest(request, env) {
const url = new URL(request.url);
if (url.pathname === "/update" && request.method === "POST") {
const apiKey = request.headers.get("x-api-key");
if (apiKey !== env.API_KEY) return new Response(JSON.stringify({ error: 1 }), { status: 401 });
try {
const data = await request.json();
const key = 'item_' + Date.now();
await env.KV.put(key, JSON.stringify(data));
let index = await env.KV.get('historys', { type: 'json' }) || [];
if (index.length >= TIMES_LIMIT) {
const delKey = index.pop();
await env.KV.delete(delKey);
}
index.unshift(key);
await env.KV.put('historys', JSON.stringify(index));
return new Response(JSON.stringify({ ok: 1 }));
} catch (e) {
return new Response(JSON.stringify({ error: 1 }), { status: 400 });
}
}
const index = await env.KV.get('historys', { type: 'json' }) || [];
const history = [];
for (const key of index) {
const item = await env.KV.get(key, { type: 'json' });
if (item) history.push(item);
}
return new Response(render(history, nameMap), { headers: { 'Content-Type': 'text/html;charset=utf-8' } });
}
function render(history, nameMap) {
const now = Math.floor(Date.now() / 1000);
let dots = [];
let faultCards = 0;
if (history.length > 0) {
const lastTime = history[0].timestamp;
const diff = now - lastTime;
const realDots = history.map(h => {
const cpu = parseFloat(h.cpu_usage) || 0;
const mem = parseFloat(h.mem_usage) || 0;
const bad = h.containers ? h.containers.filter(c => !c.status.startsWith('Up')).length : 0;
return getStatus(cpu, mem, bad);
});
realDots.reverse();
const backFill = diff >= 3600 ? Math.min(Math.floor(diff / 3600), TIMES_LIMIT - realDots.length) : 0;
const frontFill = TIMES_LIMIT - realDots.length - backFill;
dots = [
...Array(frontFill).fill('#888888'),
...realDots,
...Array(backFill).fill('#888888')
];
faultCards = backFill;
} else {
dots = Array(TIMES_LIMIT).fill('#888888');
faultCards = TIMES_LIMIT;
}
const faultHtml = Array(faultCards).fill(`
<div class="card" style="border-left:4px solid #888888">
<div class="header">
<div>服务器故障</div>
</div>
</div>`).join('');
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>监控面板</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:20px;background:#121218;color:#e0e0e0;font-family:Arial}
.container{max-width:800px;margin:0 auto}
.bar{display:flex;gap:8px;margin-bottom:20px}
.dot{flex:1;height:24px;border-radius:6px}
.card{background:#1a1a24;border-radius:12px;margin-bottom:12px;overflow:hidden}
.body{padding:0 16px;max-height:0;overflow:hidden;transition:all .3s}
.open{padding:10px 16px 20px;max-height:600px}
.header{padding:14px 16px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}
.item{margin:8px 0}
</style>
</head>
<body>
<div class="container">
<h2>${TIMES_LIMIT}小时状态</h2>
<div class="bar">${dots.map(c => `<div class="dot" style="background:${c}"></div>`).join('')}</div>
${history.map((h, i) => {
const cpu = parseFloat(h.cpu_usage) || 0
const mem = parseFloat(h.mem_usage) || 0
const bad = h.containers ? h.containers.filter(c => !c.status.startsWith('Up')).length : 0
const time = new Date(h.timestamp * 1000).toLocaleString()
const color = getStatus(cpu, mem, bad)
const open = i === 0 ? ' open' : ''
return `
<div class="card" style="border-left:4px solid ${color}">
<div class="header" onclick="this.nextElementSibling.classList.toggle('open')">
<div>${time}</div>
</div>
<div class="body${open}">
<div class="item">🧠CPU: ${cpu.toFixed(0)}%</div>
<div class="item">🧮内存: ${mem.toFixed(0)}%</div>
<div class="item">⚠️异常容器: ${bad}</div>
<br>
${h.containers ? h.containers.map(c => {
const st = parseStatus(c.status)
const alias = nameMap[c.name] || c.name
return `<div class="item">${alias} <span style='color:${st.color}'>${st.text}</span> 重启:${c.restarts}</div>`
}).join('') : '无数据'}
</div>
</div>`
}).join('')}
${faultHtml}
</div>
</body>
</html>`;
}