Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b5a69325 | |||
| 23810cf180 | |||
| 16400b0359 | |||
| df319b9c4a | |||
| ecbba836d9 | |||
| bb19141edc | |||
| c960048042 | |||
| 51f7ee9ad0 | |||
| ea904fb2b2 | |||
| 21240dc108 | |||
| 1d7f4c2bed | |||
| 88c11277cf | |||
| e5ae822ed2 |
10
README.md
10
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
## 版本信息
|
||||
|
||||
- **当前版本**: v3.4.0
|
||||
- **当前版本**: v3.4.1
|
||||
- **端口**: 16022
|
||||
- **更新日期**: 2026-06-01
|
||||
|
||||
@@ -158,7 +158,13 @@ python3 app_main.py
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v3.4.0 (2026-06-01)
|
||||
### v3.4.1 (2026-06-01)
|
||||
- 🎯 **模块化重构**: 将 3700+ 行单文件拆分为多个模块
|
||||
- 📁 **清晰结构**: config、database、utils、routes、templates 分离
|
||||
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
||||
- ✅ **功能完整**: 保留所有原有网页功能
|
||||
|
||||
### v3.4.0 (2026-06-01) [已废弃]
|
||||
- 🎯 **模块化重构**: 将 3700+ 行单文件拆分为多个模块
|
||||
- 📁 **清晰结构**: config、database、utils、routes、templates 分离
|
||||
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"thresholds": {
|
||||
"cpu": 75,
|
||||
"memory": 80,
|
||||
"disk": 85,
|
||||
"interval": 60
|
||||
"interval": 60,
|
||||
"memory": 80,
|
||||
"cpu_temp": 70
|
||||
},
|
||||
"web_settings": {
|
||||
"external_ip": "121.40.164.32",
|
||||
|
||||
@@ -20,12 +20,13 @@ ALERT_CONFIG_FILE = 'alert_config.json'
|
||||
|
||||
# 应用配置
|
||||
APP_NAME = "项目服务管理面板"
|
||||
APP_VERSION = "v3.4.0"
|
||||
APP_VERSION = "v3.4.1"
|
||||
APP_PORT = 16022
|
||||
|
||||
# 监控配置
|
||||
DEFAULT_THRESHOLDS = {
|
||||
'cpu': 80,
|
||||
'cpu_temp': 80,
|
||||
'memory': 85,
|
||||
'disk': 90,
|
||||
'interval': 60
|
||||
|
||||
30374
logs/app.log
30374
logs/app.log
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,9 @@ def register_email_routes(app):
|
||||
def api_email_send():
|
||||
"""发送邮件通知"""
|
||||
data = request.get_json()
|
||||
log_message(f"📨 收到邮件请求: {data}")
|
||||
if not data:
|
||||
log_message(f"❌ 无效数据: content-type={request.content_type}")
|
||||
return jsonify({'error': '无效数据'}), 400
|
||||
|
||||
to_email = data.get('to')
|
||||
@@ -26,33 +28,40 @@ def register_email_routes(app):
|
||||
body = data.get('body')
|
||||
|
||||
if not to_email or not subject or not body:
|
||||
log_message(f"❌ 缺少参数: to={to_email}, subject={subject}, body_len={len(body) if body else 0}")
|
||||
return jsonify({'error': '缺少必要参数'}), 400
|
||||
|
||||
try:
|
||||
# SMTP配置 (从环境变量或默认值)
|
||||
smtp_host = os.environ.get('SMTP_HOST', 'mail.tphai.com')
|
||||
smtp_port = int(os.environ.get('SMTP_PORT', 587))
|
||||
smtp_user = os.environ.get('SMTP_USER', 'favor@tphai.com')
|
||||
smtp_pass = os.environ.get('SMTP_PASS', 'favor@!')
|
||||
smtp_user = os.environ.get('SMTP_USER', 'hz4th_coder@tphai.com')
|
||||
smtp_pass = os.environ.get('SMTP_PASS', 'hz4th_coder@!')
|
||||
|
||||
# 创建邮件
|
||||
log_message(f"📧 准备发送邮件: to={to_email}, subject={subject}, user={smtp_user}")
|
||||
|
||||
# 创建邮件(完全对齐 send_email.py 的方式)
|
||||
from email.utils import formataddr, formatdate
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = smtp_user
|
||||
msg['From'] = formataddr(('黄庄4号程序员', smtp_user))
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
|
||||
# 添加正文
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件
|
||||
# 发送邮件(无SSL模式,完全对齐 send_email.py)
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.ehlo()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(msg)
|
||||
# 使用 sendmail 而非 send_message,与已验证成功的脚本一致
|
||||
server.sendmail(smtp_user, [to_email], msg.as_string())
|
||||
server.quit()
|
||||
|
||||
log_message(f"邮件发送成功: {subject} -> {to_email}")
|
||||
log_message(f"✅ 邮件发送成功: {subject} -> {to_email}")
|
||||
|
||||
return jsonify({'message': '邮件发送成功'})
|
||||
except Exception as e:
|
||||
log_message(f"邮件发送失败: {e}")
|
||||
log_message(f"❌ 邮件发送失败: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -20,10 +20,25 @@ def register_project_routes(app):
|
||||
"""获取项目列表"""
|
||||
projects = load_projects()
|
||||
|
||||
# 添加状态信息
|
||||
# 添加状态信息(结构化对象,前端期望 status.status 和 status.ports)
|
||||
for project in projects:
|
||||
ports = project.get('ports', [])
|
||||
project['status'] = 'running' if all(check_port(p) for p in ports) else 'stopped'
|
||||
port_statuses = {}
|
||||
for p in ports:
|
||||
port_statuses[str(p)] = {'running': check_port(p)}
|
||||
|
||||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||||
if running_count == len(ports) and len(ports) > 0:
|
||||
overall = 'running'
|
||||
elif running_count > 0:
|
||||
overall = 'partial'
|
||||
else:
|
||||
overall = 'stopped'
|
||||
|
||||
project['status'] = {
|
||||
'status': overall,
|
||||
'ports': port_statuses
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'projects': projects,
|
||||
@@ -169,10 +184,25 @@ def register_project_routes(app):
|
||||
data = load_projects_full()
|
||||
services = data.get('external_services', [])
|
||||
|
||||
# 添加状态
|
||||
# 添加状态(结构化对象,前端期望 status.status 和 status.ports)
|
||||
for service in services:
|
||||
ports = service.get('ports', [])
|
||||
service['status'] = 'running' if all(check_port(p) for p in ports) else 'stopped'
|
||||
port_statuses = {}
|
||||
for p in ports:
|
||||
port_statuses[str(p)] = {'running': check_port(p)}
|
||||
|
||||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||||
if running_count == len(ports) and len(ports) > 0:
|
||||
overall = 'running'
|
||||
elif running_count > 0:
|
||||
overall = 'partial'
|
||||
else:
|
||||
overall = 'stopped'
|
||||
|
||||
service['status'] = {
|
||||
'status': overall,
|
||||
'ports': port_statuses
|
||||
}
|
||||
|
||||
return jsonify({'services': services})
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from flask import jsonify
|
||||
from datetime import datetime
|
||||
from flask import jsonify, request
|
||||
from utils import log_message, get_cpu_temperatures, load_alert_config
|
||||
|
||||
try:
|
||||
@@ -46,6 +47,7 @@ def register_system_routes(app):
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_total_gb = round(disk.total / (1024**3), 2)
|
||||
disk_used_gb = round(disk.used / (1024**3), 2)
|
||||
disk_free_gb = round(disk.free / (1024**3), 2)
|
||||
disk_percent = disk.percent
|
||||
|
||||
# 网速(需要计算差值)
|
||||
@@ -82,10 +84,15 @@ def register_system_routes(app):
|
||||
|
||||
return jsonify({
|
||||
'available': True,
|
||||
'system': {
|
||||
'uptime': uptime_str,
|
||||
'boot_time': datetime.fromtimestamp(boot_time).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'process_count': process_count
|
||||
},
|
||||
'cpu': {
|
||||
'percent': cpu_percent,
|
||||
'count_physical': cpu_count,
|
||||
'count_logical': cpu_count_logical,
|
||||
'count': cpu_count,
|
||||
'logical': cpu_count_logical,
|
||||
'temp_package_0': cpu_temps.get('package_0'),
|
||||
'temp_package_1': cpu_temps.get('package_1')
|
||||
},
|
||||
@@ -98,17 +105,19 @@ def register_system_routes(app):
|
||||
'disk': {
|
||||
'total_gb': disk_total_gb,
|
||||
'used_gb': disk_used_gb,
|
||||
'free_gb': disk_free_gb,
|
||||
'percent': disk_percent
|
||||
},
|
||||
'network': {
|
||||
'upload_speed_kb': round(upload_speed, 2),
|
||||
'download_speed_kb': round(download_speed, 2)
|
||||
},
|
||||
'process_count': process_count,
|
||||
'uptime': uptime_str
|
||||
'sent_mb': round(net.bytes_sent / (1024**2), 2),
|
||||
'recv_mb': round(net.bytes_recv / (1024**2), 2),
|
||||
'sent_speed': round(upload_speed, 2),
|
||||
'recv_speed': round(download_speed, 2)
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
log_message(f"系统监控API错误: {e}")
|
||||
return jsonify({'error': str(e), 'available': False}), 500
|
||||
|
||||
@app.route('/api/system/processes')
|
||||
def api_system_processes():
|
||||
@@ -117,6 +126,9 @@ def register_system_routes(app):
|
||||
return jsonify({'error': 'psutil未安装'}), 500
|
||||
|
||||
try:
|
||||
limit = int(request.args.get('limit', 10))
|
||||
limit = max(1, min(50, limit))
|
||||
|
||||
processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
|
||||
try:
|
||||
@@ -132,7 +144,7 @@ def register_system_routes(app):
|
||||
|
||||
# 按CPU排序
|
||||
processes.sort(key=lambda x: x['cpu'], reverse=True)
|
||||
processes = processes[:50] # 只返回前50个
|
||||
processes = processes[:limit]
|
||||
|
||||
return jsonify({'processes': processes})
|
||||
except Exception as e:
|
||||
@@ -167,7 +179,6 @@ def register_system_routes(app):
|
||||
@app.route('/api/alerts', methods=['POST'])
|
||||
def api_save_alerts():
|
||||
"""保存预警配置"""
|
||||
from flask import request
|
||||
import json
|
||||
from config import ALERT_CONFIG_FILE
|
||||
|
||||
|
||||
@@ -16,9 +16,16 @@ def get_html_template():
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
template_content = f.read()
|
||||
|
||||
# 替换版本号
|
||||
# 替换版本号(支持多种格式)
|
||||
template_content = template_content.replace('v2.1', APP_VERSION)
|
||||
template_content = template_content.replace('v3.3.1', APP_VERSION)
|
||||
template_content = template_content.replace('v3.3.5', APP_VERSION)
|
||||
template_content = template_content.replace('v3.4.0', APP_VERSION)
|
||||
template_content = template_content.replace('v3.4.1', APP_VERSION) # 预留未来版本
|
||||
|
||||
# 替换默认IP地址
|
||||
template_content = template_content.replace("'192.168.2.17'", "'121.40.164.32'")
|
||||
template_content = template_content.replace('"192.168.2.17"', '"121.40.164.32"')
|
||||
|
||||
return template_content
|
||||
except:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# ==================== HTML 模板 ====================
|
||||
|
||||
HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -307,6 +305,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<span class="text-gray-500 text-xs">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-temp-hot-line threshold-icon text-red-400"></i>
|
||||
<span class="text-gray-300 text-sm">CPU 温度</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="thresholdCpuTemp" class="threshold-input" min="30" max="120" value="80" onchange="saveThresholds()">
|
||||
<span class="text-gray-500 text-xs">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-database-2-line threshold-icon text-green-400"></i>
|
||||
@@ -352,9 +360,14 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<h4 class="text-gray-200 text-sm font-medium flex items-center gap-2">
|
||||
<i class="ri-mail-line text-green-400"></i> 邮件通知规则
|
||||
</h4>
|
||||
<button onclick="showAddEmailRuleModal()" class="btn bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs flex items-center gap-1">
|
||||
<i class="ri-add-line"></i> 添加规则
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="showTestEmailModal()" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-xs flex items-center gap-1">
|
||||
<i class="ri-send-plane-line"></i> 测试发送
|
||||
</button>
|
||||
<button onclick="showAddEmailRuleModal()" class="btn bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-xs flex items-center gap-1">
|
||||
<i class="ri-add-line"></i> 添加规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailRulesList" class="space-y-2 text-sm">
|
||||
<div class="text-gray-500 text-center py-4">暂无邮件通知规则</div>
|
||||
@@ -663,20 +676,39 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">监控资源 *</label>
|
||||
<select id="emailRuleResource" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
<option value="cpu">CPU 使用率</option>
|
||||
<option value="memory">内存使用率</option>
|
||||
<option value="disk">磁盘使用率</option>
|
||||
</select>
|
||||
<label class="block text-gray-400 text-sm mb-1">监控资源 *(可多选组合)</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
|
||||
<input type="checkbox" value="cpu" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
|
||||
<i class="ri-cpu-line text-blue-400"></i>
|
||||
<span class="text-gray-200 text-sm">CPU 使用率</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
|
||||
<input type="checkbox" value="cpu_temp" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
|
||||
<i class="ri-temp-hot-line text-red-400"></i>
|
||||
<span class="text-gray-200 text-sm">CPU 温度</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
|
||||
<input type="checkbox" value="memory" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
|
||||
<i class="ri-database-2-line text-green-400"></i>
|
||||
<span class="text-gray-200 text-sm">内存使用率</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 bg-gray-700/50 px-3 py-2 rounded-lg cursor-pointer hover:bg-gray-700">
|
||||
<input type="checkbox" value="disk" class="email-rule-resource w-4 h-4" onchange="onResourceChange()">
|
||||
<i class="ri-hard-drive-2-line text-orange-400"></i>
|
||||
<span class="text-gray-200 text-sm">磁盘使用率</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-1">勾选多项可随机组合监控,任一资源超标即触发通知</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">触发阈值 *</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="emailRuleThreshold" required min="1" max="100" value="80" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
<span class="text-gray-400">%</span>
|
||||
<input type="number" id="emailRuleThreshold" required min="1" class="w-20 bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="80">
|
||||
<span class="text-gray-400" id="emailRuleThresholdUnit">% / °C</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-1">对不同资源类型使用统一阈值(如CPU用%则填百分比,温度用°C则填温度值)</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -736,6 +768,45 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试邮件发送模态框 -->
|
||||
<div id="testEmailModal" class="modal-overlay hidden">
|
||||
<div class="card rounded-xl modal-content p-6" style="max-width: 420px;">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg"><i class="ri-send-plane-line text-blue-400"></i> 测试邮件发送</h3>
|
||||
<button onclick="closeTestEmailModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
|
||||
<form id="testEmailForm" onsubmit="sendTestEmail(event)">
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">收件邮箱 *</label>
|
||||
<input type="email" id="testEmailAddress" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="your@email.com">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">邮件主题</label>
|
||||
<input type="text" id="testEmailSubject" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" value="[测试邮件] 项目服务管理面板">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-400 text-sm mb-1">邮件内容</label>
|
||||
<textarea id="testEmailBody" rows="3" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">这是一封测试邮件,用于验证邮件通知功能是否正常工作。
|
||||
|
||||
发送自: 项目服务管理面板
|
||||
时间: ${new Date().toLocaleString()}</textarea>
|
||||
</div>
|
||||
|
||||
<div id="testEmailResult" class="mb-3 hidden"></div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeTestEmailModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-1">
|
||||
<i class="ri-send-plane-line"></i> 发送测试
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div class="nav-buttons">
|
||||
<button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
|
||||
@@ -758,7 +829,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
let realtimeTimer = null;
|
||||
let realtimeInterval = 2; // 秒
|
||||
let lastAlertTime = 0; // 上次警告时间
|
||||
let thresholds = { cpu: 80, memory: 85, disk: 90, interval: 60 };
|
||||
let thresholds = { cpu: 80, cpu_temp: 80, memory: 85, disk: 90, interval: 60 };
|
||||
let desktopNotifyEnabled = false;
|
||||
|
||||
function switchTab(tab) {
|
||||
@@ -801,6 +872,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
if (config.thresholds) {
|
||||
thresholds = config.thresholds;
|
||||
document.getElementById('thresholdCpu').value = thresholds.cpu || 80;
|
||||
document.getElementById('thresholdCpuTemp').value = thresholds.cpu_temp || 80;
|
||||
document.getElementById('thresholdMemory').value = thresholds.memory || 85;
|
||||
document.getElementById('thresholdDisk').value = thresholds.disk || 90;
|
||||
document.getElementById('thresholdInterval').value = thresholds.interval || 60;
|
||||
@@ -813,6 +885,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
// 保存阈值设置
|
||||
async function saveThresholds() {
|
||||
thresholds.cpu = parseInt(document.getElementById('thresholdCpu').value) || 80;
|
||||
thresholds.cpu_temp = parseInt(document.getElementById('thresholdCpuTemp').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;
|
||||
@@ -937,6 +1010,12 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
if (data.disk.percent >= thresholds.disk) {
|
||||
warnings.push(`磁盘使用率 ${data.disk.percent.toFixed(1)}% 超过阈值 ${thresholds.disk}%`);
|
||||
}
|
||||
if (data.cpu.temp_package_0 !== null && data.cpu.temp_package_0 >= thresholds.cpu_temp) {
|
||||
warnings.push(`CPU0 温度 ${data.cpu.temp_package_0.toFixed(1)}°C 超过阈值 ${thresholds.cpu_temp}°C`);
|
||||
}
|
||||
if (data.cpu.temp_package_1 !== null && data.cpu.temp_package_1 >= thresholds.cpu_temp) {
|
||||
warnings.push(`CPU1 温度 ${data.cpu.temp_package_1.toFixed(1)}°C 超过阈值 ${thresholds.cpu_temp}°C`);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
lastAlertTime = now;
|
||||
@@ -1482,7 +1561,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceNames = { 'cpu': 'CPU', 'memory': '内存', 'disk': '磁盘' };
|
||||
const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘' };
|
||||
const resourceUnits = { 'cpu': '%', 'cpu_temp': '°C', 'memory': '%', 'disk': '%' };
|
||||
|
||||
list.innerHTML = rules.map(rule => {
|
||||
const silentInfo = rule.silentEnabled ? `静默:${rule.silentStart}-${rule.silentEnd}` : '';
|
||||
@@ -1493,12 +1573,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
else if (triggerType === 'duration') triggerDisplay = `持续${triggerParam}秒`;
|
||||
else if (triggerType === 'average') triggerDisplay = `平均${triggerParam}次`;
|
||||
|
||||
// 显示资源列表(支持多资源和旧版单资源兼容)
|
||||
const resources = rule.resources || (rule.resource ? [rule.resource] : []);
|
||||
const resourceDisplay = resources.map(r => resourceNames[r] || r).join('+');
|
||||
|
||||
return `
|
||||
<div class="flex items-center justify-between bg-gray-700/50 px-3 py-2 rounded-lg">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class="px-2 py-0.5 rounded text-xs ${rule.enabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}">${rule.enabled ? '启用' : '禁用'}</span>
|
||||
<span class="text-gray-200">${rule.name}</span>
|
||||
<span class="text-gray-400 text-xs">${resourceNames[rule.resource]} ≥ ${rule.threshold}%</span>
|
||||
<span class="text-gray-400 text-xs">${resourceDisplay} ≥ ${rule.threshold}</span>
|
||||
<span class="text-blue-400/70 text-xs"><i class="ri-flashlight-line"></i> ${triggerDisplay}</span>
|
||||
<span class="text-gray-400 text-xs">→ ${rule.email}</span>
|
||||
${silentInfo ? `<span class="text-yellow-400/70 text-xs"><i class="ri-moon-line"></i> ${silentInfo}</span>` : ''}
|
||||
@@ -1516,8 +1600,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
document.getElementById('emailRuleModalTitle').textContent = '添加邮件通知规则';
|
||||
document.getElementById('emailRuleId').value = '';
|
||||
document.getElementById('emailRuleName').value = '';
|
||||
document.getElementById('emailRuleResource').value = 'cpu';
|
||||
// 重置资源复选框
|
||||
document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
|
||||
document.getElementById('emailRuleThreshold').value = 80;
|
||||
document.getElementById('emailRuleThresholdUnit').textContent = '% / °C';
|
||||
document.getElementById('emailRuleTriggerType').value = 'single';
|
||||
document.getElementById('emailRuleTriggerParam').value = 3;
|
||||
document.getElementById('triggerParamsDiv').style.display = 'none';
|
||||
@@ -1534,6 +1620,33 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
document.getElementById('emailRuleModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function getSelectedResources() {
|
||||
const checkboxes = document.querySelectorAll('.email-rule-resource:checked');
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function onResourceChange() {
|
||||
const selected = getSelectedResources();
|
||||
const unitEl = document.getElementById('emailRuleThresholdUnit');
|
||||
const thresholdEl = document.getElementById('emailRuleThreshold');
|
||||
|
||||
if (selected.length === 0) {
|
||||
unitEl.textContent = '% / °C';
|
||||
} else if (selected.length === 1 && selected[0] === 'cpu_temp') {
|
||||
unitEl.textContent = '°C';
|
||||
if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80;
|
||||
thresholdEl.max = 120;
|
||||
thresholdEl.min = 30;
|
||||
} else {
|
||||
unitEl.textContent = '%';
|
||||
thresholdEl.max = 100;
|
||||
thresholdEl.min = 1;
|
||||
if (selected.includes('cpu_temp')) {
|
||||
unitEl.textContent = '% / °C';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTriggerParams() {
|
||||
const triggerType = document.getElementById('emailRuleTriggerType').value;
|
||||
const paramsDiv = document.getElementById('triggerParamsDiv');
|
||||
@@ -1582,7 +1695,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
document.getElementById('emailRuleModalTitle').textContent = '编辑邮件通知规则';
|
||||
document.getElementById('emailRuleId').value = rule.id;
|
||||
document.getElementById('emailRuleName').value = rule.name;
|
||||
document.getElementById('emailRuleResource').value = rule.resource;
|
||||
// 重置所有复选框
|
||||
document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
|
||||
// 勾选已保存的资源(支持新版resources数组和旧版resource字符串兼容)
|
||||
const resources = rule.resources || (rule.resource ? [rule.resource] : []);
|
||||
resources.forEach(r => {
|
||||
const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
onResourceChange();
|
||||
document.getElementById('emailRuleThreshold').value = rule.threshold;
|
||||
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
|
||||
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
|
||||
@@ -1601,7 +1722,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
|
||||
const ruleId = document.getElementById('emailRuleId').value;
|
||||
const name = document.getElementById('emailRuleName').value.trim();
|
||||
const resource = document.getElementById('emailRuleResource').value;
|
||||
const resources = getSelectedResources();
|
||||
const threshold = parseInt(document.getElementById('emailRuleThreshold').value);
|
||||
const triggerType = document.getElementById('emailRuleTriggerType').value;
|
||||
const triggerParam = parseInt(document.getElementById('emailRuleTriggerParam').value) || 3;
|
||||
@@ -1617,19 +1738,24 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
return;
|
||||
}
|
||||
|
||||
if (resources.length === 0) {
|
||||
alert('请至少选择一个监控资源');
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = getEmailRules();
|
||||
|
||||
if (ruleId) {
|
||||
// 编辑
|
||||
const idx = rules.findIndex(r => r.id === ruleId);
|
||||
if (idx >= 0) {
|
||||
rules[idx] = { ...rules[idx], name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
|
||||
rules[idx] = { ...rules[idx], name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
|
||||
}
|
||||
} else {
|
||||
// 新增
|
||||
rules.push({
|
||||
id: 'rule_' + Date.now(),
|
||||
name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
|
||||
name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
|
||||
lastSent: 0,
|
||||
exceedCount: 0, // 连续超过次数
|
||||
exceedDuration: 0, // 累计超过时间
|
||||
@@ -1683,77 +1809,90 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
}
|
||||
}
|
||||
|
||||
// 检查阈值和触发机制
|
||||
const value = data[rule.resource]?.percent;
|
||||
if (value === undefined) continue;
|
||||
// 检查阈值和触发机制(支持多资源)
|
||||
const resources = rule.resources || (rule.resource ? [rule.resource] : []);
|
||||
const resourceNames = { 'cpu': 'CPU使用率', 'cpu_temp': 'CPU温度', 'memory': '内存使用率', 'disk': '磁盘使用率' };
|
||||
|
||||
// 初始化触发相关数据
|
||||
if (!rule.exceedCount) rule.exceedCount = 0;
|
||||
if (!rule.exceedDuration) rule.exceedDuration = 0;
|
||||
if (!rule.recentValues) rule.recentValues = [];
|
||||
if (!rule.triggerType) rule.triggerType = 'single';
|
||||
if (!rule.triggerParam) rule.triggerParam = 3;
|
||||
// 收集所有超标的资源
|
||||
const exceededResources = [];
|
||||
|
||||
const triggerType = rule.triggerType;
|
||||
const triggerParam = rule.triggerParam;
|
||||
const isExceedThreshold = value >= rule.threshold;
|
||||
|
||||
let shouldTrigger = false;
|
||||
|
||||
// 根据触发类型判断
|
||||
switch (triggerType) {
|
||||
case 'single':
|
||||
// 一次性触发:超过阈值立即触发
|
||||
if (isExceedThreshold) shouldTrigger = true;
|
||||
break;
|
||||
for (const resource of resources) {
|
||||
let value;
|
||||
if (resource === 'cpu_temp') {
|
||||
// CPU温度取两个Package中较高的那个
|
||||
const t0 = data.cpu?.temp_package_0;
|
||||
const t1 = data.cpu?.temp_package_1;
|
||||
if (t0 === null && t1 === null) continue;
|
||||
if (t0 === null) value = t1;
|
||||
else if (t1 === null) value = t0;
|
||||
else value = Math.max(t0, t1);
|
||||
} else {
|
||||
value = data[resource]?.percent;
|
||||
}
|
||||
|
||||
case 'continuous':
|
||||
// 连续触发:连续超过阈值N次
|
||||
if (isExceedThreshold) {
|
||||
rule.exceedCount++;
|
||||
if (rule.exceedCount >= triggerParam) {
|
||||
shouldTrigger = true;
|
||||
rule.exceedCount = 0; // 触发后重置计数
|
||||
}
|
||||
} else {
|
||||
rule.exceedCount = 0; // 未超过阈值时重置计数
|
||||
}
|
||||
break;
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
case 'duration':
|
||||
// 持续时间触发:累计超过阈值N秒
|
||||
if (isExceedThreshold) {
|
||||
// 增加累计时间(假设实时监控间隔为2秒)
|
||||
const intervalSeconds = realtimeInterval || 2;
|
||||
rule.exceedDuration += intervalSeconds;
|
||||
if (rule.exceedDuration >= triggerParam) {
|
||||
shouldTrigger = true;
|
||||
rule.exceedDuration = 0; // 触发后重置
|
||||
}
|
||||
} else {
|
||||
rule.exceedDuration = 0; // 未超过阈值时重置累计时间
|
||||
}
|
||||
break;
|
||||
const isExceedThreshold = value >= rule.threshold;
|
||||
|
||||
case 'average':
|
||||
// 平均值触发:最近N次平均值超过阈值
|
||||
rule.recentValues.push(value);
|
||||
if (rule.recentValues.length > triggerParam) {
|
||||
rule.recentValues.shift(); // 保持最近N个值
|
||||
}
|
||||
if (rule.recentValues.length >= triggerParam) {
|
||||
const avg = rule.recentValues.reduce((a, b) => a + b, 0) / rule.recentValues.length;
|
||||
if (avg >= rule.threshold) {
|
||||
shouldTrigger = true;
|
||||
// 初始化触发相关数据(key后缀避免不同资源冲突)
|
||||
const ck = '_' + resource;
|
||||
if (!rule['exceedCount' + ck]) rule['exceedCount' + ck] = 0;
|
||||
if (!rule['exceedDuration' + ck]) rule['exceedDuration' + ck] = 0;
|
||||
if (!rule['recentValues' + ck]) rule['recentValues' + ck] = [];
|
||||
|
||||
const triggerType = rule.triggerType || 'single';
|
||||
const triggerParam = rule.triggerParam || 3;
|
||||
let resourceTriggered = false;
|
||||
|
||||
switch (triggerType) {
|
||||
case 'single':
|
||||
if (isExceedThreshold) resourceTriggered = true;
|
||||
break;
|
||||
case 'continuous':
|
||||
if (isExceedThreshold) {
|
||||
rule['exceedCount' + ck]++;
|
||||
if (rule['exceedCount' + ck] >= triggerParam) {
|
||||
resourceTriggered = true;
|
||||
rule['exceedCount' + ck] = 0;
|
||||
}
|
||||
} else {
|
||||
rule['exceedCount' + ck] = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'duration':
|
||||
if (isExceedThreshold) {
|
||||
const intervalSec = realtimeInterval || 2;
|
||||
rule['exceedDuration' + ck] += intervalSec;
|
||||
if (rule['exceedDuration' + ck] >= triggerParam) {
|
||||
resourceTriggered = true;
|
||||
rule['exceedDuration' + ck] = 0;
|
||||
}
|
||||
} else {
|
||||
rule['exceedDuration' + ck] = 0;
|
||||
}
|
||||
break;
|
||||
case 'average':
|
||||
rule['recentValues' + ck].push(value);
|
||||
if (rule['recentValues' + ck].length > triggerParam) {
|
||||
rule['recentValues' + ck].shift();
|
||||
}
|
||||
if (rule['recentValues' + ck].length >= triggerParam) {
|
||||
const avg = rule['recentValues' + ck].reduce((a, b) => a + b, 0) / rule['recentValues' + ck].length;
|
||||
if (avg >= rule.threshold) resourceTriggered = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (resourceTriggered) {
|
||||
const unit = resource === 'cpu_temp' ? '°C' : '%';
|
||||
exceededResources.push(`${resourceNames[resource]}: ${value.toFixed(1)}${unit} (阈值: ${rule.threshold}${unit})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存更新的规则数据
|
||||
saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r));
|
||||
|
||||
if (!shouldTrigger) continue;
|
||||
if (exceededResources.length === 0) continue;
|
||||
|
||||
// 发送邮件
|
||||
try {
|
||||
@@ -1763,7 +1902,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
body: JSON.stringify({
|
||||
to: rule.email,
|
||||
subject: `[系统告警] ${rule.name}`,
|
||||
body: `${rule.name} 触发告警\n\n${rule.resource.toUpperCase()} 使用率: ${value.toFixed(1)}%\n阈值: ${rule.threshold}%\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
|
||||
body: `${rule.name} 触发告警\n\n${exceededResources.join('\n')}\n\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1778,6 +1917,65 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 测试邮件发送 ====================
|
||||
|
||||
function showTestEmailModal() {
|
||||
document.getElementById('testEmailAddress').value = '';
|
||||
document.getElementById('testEmailSubject').value = '[测试邮件] 项目服务管理面板';
|
||||
document.getElementById('testEmailBody').value = '这是一封测试邮件,用于验证邮件通知功能是否正常工作。\n\n发送自: 项目服务管理面板\n时间: ' + new Date().toLocaleString();
|
||||
document.getElementById('testEmailResult').classList.add('hidden');
|
||||
document.getElementById('testEmailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTestEmailModal() {
|
||||
document.getElementById('testEmailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function sendTestEmail(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById('testEmailAddress').value.trim();
|
||||
const subject = document.getElementById('testEmailSubject').value.trim();
|
||||
const body = document.getElementById('testEmailBody').value.trim();
|
||||
const resultEl = document.getElementById('testEmailResult');
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
|
||||
if (!email) {
|
||||
alert('请输入收件邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> 发送中...';
|
||||
resultEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/email/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ to: email, subject, body })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
resultEl.className = 'mb-3 p-3 rounded-lg bg-green-500/20 text-green-400 text-sm';
|
||||
resultEl.innerHTML = `<i class="ri-checkbox-circle-line"></i> 测试邮件发送成功!<br>收件人: ${email}`;
|
||||
} else {
|
||||
resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
|
||||
resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 发送失败: ${data.error || '未知错误'}`;
|
||||
}
|
||||
resultEl.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
|
||||
resultEl.innerHTML = `<i class="ri-error-warning-line"></i> 网络错误: ${e.message}`;
|
||||
resultEl.classList.remove('hidden');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="ri-send-plane-line"></i> 发送测试';
|
||||
}
|
||||
}
|
||||
|
||||
function showAddServiceModal() {
|
||||
document.getElementById('serviceName').value = '';
|
||||
document.getElementById('servicePort').value = '';
|
||||
@@ -2353,14 +2551,4 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
}, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.makedirs(os.path.join(BASE_DIR, 'logs'), exist_ok=True)
|
||||
os.makedirs(CRON_BACKUP_DIR, exist_ok=True)
|
||||
init_db()
|
||||
|
||||
log_message("=" * 50)
|
||||
log_message("项目服务管理面板 v2.0.0 启动")
|
||||
</html>
|
||||
Reference in New Issue
Block a user