feat: 系统资源阈值监控和弹窗警告
This commit is contained in:
149
app.py
149
app.py
@@ -1040,7 +1040,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>项目服务管理面板 v2.7</title>
|
<title>项目服务管理面板 v2.8</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||||
@@ -1075,6 +1075,21 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
.speed-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; background: #334155; font-size: 13px; }
|
.speed-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; background: #334155; font-size: 13px; }
|
||||||
.speed-badge.upload { color: #f97316; }
|
.speed-badge.upload { color: #f97316; }
|
||||||
.speed-badge.download { color: #3b82f6; }
|
.speed-badge.download { color: #3b82f6; }
|
||||||
|
.threshold-section { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
||||||
|
.threshold-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #334155; }
|
||||||
|
.threshold-item:last-child { border-bottom: none; }
|
||||||
|
.threshold-label { flex: 1; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.threshold-input { width: 60px; background: #334155; border: 1px solid #475569; border-radius: 6px; padding: 4px 8px; color: #f1f5f9; text-align: center; }
|
||||||
|
.threshold-input:focus { outline: none; border-color: #3b82f6; }
|
||||||
|
.threshold-icon { font-size: 18px; }
|
||||||
|
.threshold-warning { color: #f97316; }
|
||||||
|
.alert-popup { position: fixed; top: 20px; right: 20px; z-index: 1000; background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); border-radius: 12px; padding: 16px 20px; color: white; box-shadow: 0 10px 40px rgba(0,0,0,0.3); animation: slideIn 0.3s ease-out; max-width: 320px; }
|
||||||
|
.alert-popup .alert-title { font-weight: bold; font-size: 16px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.alert-popup .alert-content { font-size: 14px; }
|
||||||
|
.alert-popup .alert-close { position: absolute; top: 8px; right: 12px; cursor: pointer; opacity: 0.7; }
|
||||||
|
.alert-popup .alert-close:hover { opacity: 1; }
|
||||||
|
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||||
|
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
|
||||||
.tab-btn { border-bottom: 2px solid transparent; }
|
.tab-btn { border-bottom: 2px solid transparent; }
|
||||||
.tab-btn.active { border-bottom-color: #3b82f6; }
|
.tab-btn.active { border-bottom-color: #3b82f6; }
|
||||||
.cron-card { transition: all 0.2s; }
|
.cron-card { transition: all 0.2s; }
|
||||||
@@ -1294,6 +1309,56 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 阈值监控设置 -->
|
||||||
|
<div class="threshold-section">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-bold text-gray-200 flex items-center gap-2">
|
||||||
|
<i class="ri-alarm-warning-line text-orange-400"></i> 监控阈值设置
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs text-gray-500">超过阈值将弹窗警告</span>
|
||||||
|
</div>
|
||||||
|
<div class="threshold-item">
|
||||||
|
<div class="threshold-label">
|
||||||
|
<i class="ri-cpu-line threshold-icon text-blue-400"></i>
|
||||||
|
<span class="text-gray-300 text-sm">CPU 使用率</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="thresholdCpu" class="threshold-input" min="0" max="100" value="80" onchange="saveThresholds()">
|
||||||
|
<span class="text-gray-500 text-xs">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="threshold-item">
|
||||||
|
<div class="threshold-label">
|
||||||
|
<i class="ri-memory-card-line threshold-icon text-green-400"></i>
|
||||||
|
<span class="text-gray-300 text-sm">内存使用率</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="thresholdMemory" class="threshold-input" min="0" max="100" value="85" onchange="saveThresholds()">
|
||||||
|
<span class="text-gray-500 text-xs">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="threshold-item">
|
||||||
|
<div class="threshold-label">
|
||||||
|
<i class="ri-hard-drive-2-line threshold-icon text-orange-400"></i>
|
||||||
|
<span class="text-gray-300 text-sm">磁盘使用率</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="thresholdDisk" class="threshold-input" min="0" max="100" value="90" onchange="saveThresholds()">
|
||||||
|
<span class="text-gray-500 text-xs">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="threshold-item">
|
||||||
|
<div class="threshold-label">
|
||||||
|
<i class="ri-timer-line threshold-icon text-purple-400"></i>
|
||||||
|
<span class="text-gray-300 text-sm">警告间隔</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" id="thresholdInterval" class="threshold-input" min="10" max="300" value="60" onchange="saveThresholds()">
|
||||||
|
<span class="text-gray-500 text-xs">秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 控制按钮 -->
|
<!-- 控制按钮 -->
|
||||||
<div class="flex gap-3 mb-4 items-center">
|
<div class="flex gap-3 mb-4 items-center">
|
||||||
<button onclick="loadSystemStats()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
<button onclick="loadSystemStats()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
@@ -1592,6 +1657,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
loadCronTasks();
|
loadCronTasks();
|
||||||
}
|
}
|
||||||
if (tab === 'system') {
|
if (tab === 'system') {
|
||||||
|
loadThresholds();
|
||||||
loadSystemStats();
|
loadSystemStats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1599,6 +1665,77 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
// 系统资源加载
|
// 系统资源加载
|
||||||
let realtimeTimer = null;
|
let realtimeTimer = null;
|
||||||
let realtimeInterval = 2; // 秒
|
let realtimeInterval = 2; // 秒
|
||||||
|
let lastAlertTime = 0; // 上次警告时间
|
||||||
|
let thresholds = { cpu: 80, memory: 85, disk: 90, interval: 60 };
|
||||||
|
|
||||||
|
// 加载阈值设置
|
||||||
|
function loadThresholds() {
|
||||||
|
const saved = localStorage.getItem('systemThresholds');
|
||||||
|
if (saved) {
|
||||||
|
thresholds = JSON.parse(saved);
|
||||||
|
document.getElementById('thresholdCpu').value = thresholds.cpu || 80;
|
||||||
|
document.getElementById('thresholdMemory').value = thresholds.memory || 85;
|
||||||
|
document.getElementById('thresholdDisk').value = thresholds.disk || 90;
|
||||||
|
document.getElementById('thresholdInterval').value = thresholds.interval || 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存阈值设置
|
||||||
|
function saveThresholds() {
|
||||||
|
thresholds.cpu = parseInt(document.getElementById('thresholdCpu').value) || 80;
|
||||||
|
thresholds.memory = parseInt(document.getElementById('thresholdMemory').value) || 85;
|
||||||
|
thresholds.disk = parseInt(document.getElementById('thresholdDisk').value) || 90;
|
||||||
|
thresholds.interval = parseInt(document.getElementById('thresholdInterval').value) || 60;
|
||||||
|
localStorage.setItem('systemThresholds', JSON.stringify(thresholds));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查阈值并弹窗警告
|
||||||
|
function checkThresholds(data) {
|
||||||
|
const now = Date.now();
|
||||||
|
const intervalMs = thresholds.interval * 1000;
|
||||||
|
|
||||||
|
// 检查是否超过间隔时间
|
||||||
|
if (now - lastAlertTime < intervalMs) return;
|
||||||
|
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
if (data.cpu.percent >= thresholds.cpu) {
|
||||||
|
warnings.push(`CPU使用率 ${data.cpu.percent.toFixed(1)}% 超过阈值 ${thresholds.cpu}%`);
|
||||||
|
}
|
||||||
|
if (data.memory.percent >= thresholds.memory) {
|
||||||
|
warnings.push(`内存使用率 ${data.memory.percent.toFixed(1)}% 超过阈值 ${thresholds.memory}%`);
|
||||||
|
}
|
||||||
|
if (data.disk.percent >= thresholds.disk) {
|
||||||
|
warnings.push(`磁盘使用率 ${data.disk.percent.toFixed(1)}% 超过阈值 ${thresholds.disk}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
lastAlertTime = now;
|
||||||
|
showAlertPopup(warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示警告弹窗
|
||||||
|
function showAlertPopup(warnings) {
|
||||||
|
// 移除已有弹窗
|
||||||
|
const existing = document.querySelector('.alert-popup');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const popup = document.createElement('div');
|
||||||
|
popup.className = 'alert-popup';
|
||||||
|
popup.innerHTML = `
|
||||||
|
<span class="alert-close" onclick="this.parentElement.remove()"><i class="ri-close-line"></i></span>
|
||||||
|
<div class="alert-title"><i class="ri-alarm-warning-line"></i> 资源告警</div>
|
||||||
|
<div class="alert-content">${warnings.map(w => `<p>⚠️ ${w}</p>`).join('')}</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
|
// 自动关闭(10秒后)
|
||||||
|
setTimeout(() => {
|
||||||
|
popup.style.animation = 'slideOut 0.3s ease-out forwards';
|
||||||
|
setTimeout(() => popup.remove(), 300);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
function updateRealtimeInterval() {
|
function updateRealtimeInterval() {
|
||||||
realtimeInterval = parseInt(document.getElementById('realtimeIntervalSelect').value) || 2;
|
realtimeInterval = parseInt(document.getElementById('realtimeIntervalSelect').value) || 2;
|
||||||
@@ -1653,6 +1790,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
document.getElementById('netSentSpeed').textContent = data.network.sent_speed || '-';
|
document.getElementById('netSentSpeed').textContent = data.network.sent_speed || '-';
|
||||||
document.getElementById('netRecvSpeed').textContent = data.network.recv_speed || '-';
|
document.getElementById('netRecvSpeed').textContent = data.network.recv_speed || '-';
|
||||||
|
|
||||||
|
// 检查阈值
|
||||||
|
checkThresholds(data);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('系统资源加载失败:', e);
|
console.error('系统资源加载失败:', e);
|
||||||
}
|
}
|
||||||
@@ -1697,6 +1837,13 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
renderProjects();
|
renderProjects();
|
||||||
updateStats();
|
updateStats();
|
||||||
updateConnectionStatus(true);
|
updateConnectionStatus(true);
|
||||||
|
|
||||||
|
// 同时检查系统资源阈值(主页也有效)
|
||||||
|
loadThresholds();
|
||||||
|
fetch('/api/system/stats').then(r => r.json()).then(sysData => {
|
||||||
|
if (sysData.available) checkThresholds(sysData);
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载失败:', e);
|
console.error('加载失败:', e);
|
||||||
updateConnectionStatus(false);
|
updateConnectionStatus(false);
|
||||||
|
|||||||
36
logs/app.log
36
logs/app.log
@@ -1,8 +1,8 @@
|
|||||||
[2026-04-23 17:35:41] ==================================================
|
[2026-04-23 17:47:44] ==================================================
|
||||||
[2026-04-23 17:35:41] 项目服务管理面板 v2.0.0 启动
|
[2026-04-23 17:47:44] 项目服务管理面板 v2.0.0 启动
|
||||||
[2026-04-23 17:35:41] 访问地址: http://localhost:19013
|
[2026-04-23 17:47:44] 访问地址: http://localhost:19013
|
||||||
[2026-04-23 17:35:41] 进程PID: 1133475
|
[2026-04-23 17:47:44] 进程PID: 1138715
|
||||||
[2026-04-23 17:35:41] ==================================================
|
[2026-04-23 17:47:44] ==================================================
|
||||||
* Serving Flask app 'app'
|
* Serving Flask app 'app'
|
||||||
* Debug mode: off
|
* Debug mode: off
|
||||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||||
@@ -10,8 +10,24 @@ WARNING: This is a development server. Do not use it in a production deployment.
|
|||||||
* Running on http://127.0.0.1:19013
|
* Running on http://127.0.0.1:19013
|
||||||
* Running on http://192.168.2.17:19013
|
* Running on http://192.168.2.17:19013
|
||||||
Press CTRL+C to quit
|
Press CTRL+C to quit
|
||||||
127.0.0.1 - - [23/Apr/2026 17:35:43] "GET / HTTP/1.1" 200 -
|
127.0.0.1 - - [23/Apr/2026 17:47:47] "GET / HTTP/1.1" 200 -
|
||||||
127.0.0.1 - - [23/Apr/2026 17:35:44] "GET / HTTP/1.1" 200 -
|
192.168.2.14 - - [23/Apr/2026 17:47:48] "GET /api/projects HTTP/1.1" 200 -
|
||||||
192.168.2.8 - - [23/Apr/2026 17:35:45] "GET /api/projects HTTP/1.1" 200 -
|
127.0.0.1 - - [23/Apr/2026 17:47:49] "GET / HTTP/1.1" 200 -
|
||||||
127.0.0.1 - - [23/Apr/2026 17:35:53] "GET / HTTP/1.1" 200 -
|
127.0.0.1 - - [23/Apr/2026 17:47:55] "GET / HTTP/1.1" 200 -
|
||||||
192.168.2.8 - - [23/Apr/2026 17:35:55] "GET /api/projects HTTP/1.1" 200 -
|
192.168.2.8 - - [23/Apr/2026 17:47:56] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:47:56] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.14 - - [23/Apr/2026 17:47:58] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:05] "GET / HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:06] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.8 - - [23/Apr/2026 17:48:06] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
192.168.2.14 - - [23/Apr/2026 17:48:08] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:13] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.8 - - [23/Apr/2026 17:48:14] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:15] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.8 - - [23/Apr/2026 17:48:16] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:17] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.14 - - [23/Apr/2026 17:48:18] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:25] "GET / HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [23/Apr/2026 17:48:26] "GET / HTTP/1.1" 200 -
|
||||||
|
192.168.2.8 - - [23/Apr/2026 17:48:27] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
192.168.2.14 - - [23/Apr/2026 17:48:28] "GET /api/projects HTTP/1.1" 200 -
|
||||||
|
|||||||
Reference in New Issue
Block a user