Compare commits

..

12 Commits

Author SHA1 Message Date
df319b9c4a feat: 邮件通知规则三大增强
1. 新增CPU温度监控资源 - 取双CPU较高温度值判断
2. 支持多资源随机组合监控 - 复选框替代单选,任一资源超标即触发
3. 新增测试发送按钮 - 可输入收件邮箱即时测试邮件通道
- 旧版单resource字段兼容: resources数组优先,无则回退resource
- checkEmailRules重构: 逐资源独立计算触发状态,分别统计连续次数/持续时间/平均值
2026-06-01 17:22:44 +08:00
ecbba836d9 feat: 监控阈值设置新增CPU温度项
- 默认阈值: 80°C (范围 30-120°C)
- 同时监控 CPU0 和 CPU1 (双路服务器)
- 超温时弹窗警告 + 桌面通知
2026-06-01 17:13:06 +08:00
bb19141edc fix: 修复Web服务监控区域状态显示问题
后端 status 字段从字符串改为结构化对象 {status, ports: {port: {running}}}
前端期望的是 p.status.status 和 p.status.ports[port].running,之前的字符串格式导致:
- 所有服务状态始终显示「未知」
- 所有端口始终显示红色(已停止)
2026-06-01 17:08:09 +08:00
c960048042 fix: 重写routes_system.py修复API错误
完整重写系统监控API,确保:
- 正确的数据结构匹配JavaScript期待
- system.uptime/system.boot_time/system.process_count
- cpu.count/cpu.logical
- disk.free_gb
- network.sent_mb/recv_mb/sent_speed/recv_speed
- 所有异常处理和日志记录

修复500错误,现在系统监控应该正常显示
2026-06-01 16:19:59 +08:00
51f7ee9ad0 fix: 修正系统监控API返回数据结构
匹配JavaScript期待的数据结构:
- system.uptime(系统运行时间)
- system.boot_time(启动时间)
- system.process_count(进程数)
- cpu.count/cpu.logical(CPU核心数)
- disk.free_gb(磁盘剩余空间)
- network.sent_mb/recv_mb(累计流量)
- network.sent_speed/recv_speed(实时网速)

确保前端能正确显示所有系统资源信息
2026-06-01 16:17:47 +08:00
ea904fb2b2 fix: 清理templates_full.txt开头的Python代码
移除模板文件开头的Python代码片段:
- '# ==================== HTML 模板 ===================='
- 'HTML_TEMPLATE = '''

确保templates_full.txt是纯粹的HTML模板,从<!DOCTYPE html>开始

这是导致浏览器console报错的根本原因:
- Python代码片段会被浏览器当作JavaScript解析
- 导致JavaScript执行失败,函数未定义
- closeEmailRuleModal等函数无法正常工作
2026-06-01 16:12:37 +08:00
21240dc108 fix: 替换模板中的默认IP地址为121.40.164.32
修复JavaScript代码中的默认IP值:
- localStorage.getItem('externalIp') || '192.168.2.17'
- 改为:|| '121.40.164.32'

确保所有默认值统一为正确的对外IP地址
2026-06-01 16:11:19 +08:00
1d7f4c2bed fix: 清理模板文件末尾的多余代码
移除 templates_full.txt 末尾不应该存在的Python代码片段:
- 移除 ''' 字符串结束标记
- 移除 if __name__ == '__main__' 部分
- 确保HTML模板在 </html> 处正确结束

这个问题可能导致页面加载异常
2026-06-01 16:02:59 +08:00
88c11277cf fix: 修正版本号为 v3.4.1 2026-06-01 15:56:47 +08:00
e5ae822ed2 docs: 更新版本号为 v3.4.1 2026-06-01 15:55:19 +08:00
50341749b6 fix: 使用完整模板,保留所有网页功能
 包含所有原有功能:
- 阈值设置(CPU/内存/磁盘/间隔)
- 邮件规则管理
- 自定义链接添加
- 进程列表显示
- Docker容器监控
- Cron任务管理
- 系统资源实时监控
- 配置导入导出

📝 templates_full.txt: 从v3.3.5提取的完整HTML模板(2366行)
📝 templates.py: 动态加载完整模板并替换版本号
2026-06-01 15:54:45 +08:00
7904117b1d fix: 添加 cron_expression_to_human 函数到 utils.py 2026-06-01 12:31:38 +08:00
17 changed files with 2694 additions and 588 deletions

View File

@@ -4,7 +4,7 @@
## 版本信息 ## 版本信息
- **当前版本**: v3.4.0 - **当前版本**: v3.4.1
- **端口**: 16022 - **端口**: 16022
- **更新日期**: 2026-06-01 - **更新日期**: 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+ 行单文件拆分为多个模块 - 🎯 **模块化重构**: 将 3700+ 行单文件拆分为多个模块
- 📁 **清晰结构**: config、database、utils、routes、templates 分离 - 📁 **清晰结构**: 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -20,12 +20,13 @@ ALERT_CONFIG_FILE = 'alert_config.json'
# 应用配置 # 应用配置
APP_NAME = "项目服务管理面板" APP_NAME = "项目服务管理面板"
APP_VERSION = "v3.4.0" APP_VERSION = "v3.4.1"
APP_PORT = 16022 APP_PORT = 16022
# 监控配置 # 监控配置
DEFAULT_THRESHOLDS = { DEFAULT_THRESHOLDS = {
'cpu': 80, 'cpu': 80,
'cpu_temp': 80,
'memory': 85, 'memory': 85,
'disk': 90, 'disk': 90,
'interval': 60 'interval': 60

View File

@@ -30314,3 +30314,20 @@ Press CTRL+C to quit
[2026-06-01 12:29:39] 访问地址: http://localhost:16022 [2026-06-01 12:29:39] 访问地址: http://localhost:16022
[2026-06-01 12:29:39] 进程PID: 784244 [2026-06-01 12:29:39] 进程PID: 784244
[2026-06-01 12:29:39] ================================================== [2026-06-01 12:29:39] ==================================================
[2026-06-01 12:30:38] ⚠️ 进程收到 SIGTERM 信号,即将退出!
[2026-06-01 12:31:16] 项目服务管理面板 v3.4.0 初始化完成
[2026-06-01 12:31:16] 启动 项目服务管理面板,端口: 16022
[2026-06-01 15:53:53] 项目服务管理面板 v3.4.0 初始化完成
[2026-06-01 15:53:53] 启动 项目服务管理面板,端口: 16022
[2026-06-01 15:55:35] 项目服务管理面板 v3.4.0 初始化完成
[2026-06-01 15:55:35] 启动 项目服务管理面板,端口: 16022
[2026-06-01 15:56:16] 项目服务管理面板 v3.4.0 初始化完成
[2026-06-01 15:56:16] 启动 项目服务管理面板,端口: 16022
[2026-06-01 15:57:03] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-01 15:57:03] 启动 项目服务管理面板,端口: 16022
[2026-06-01 16:02:13] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-01 16:02:13] 启动 项目服务管理面板,端口: 16022
[2026-06-01 16:02:39] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-01 16:02:39] 启动 项目服务管理面板,端口: 16022
[2026-06-01 16:10:23] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-01 16:10:23] 启动 项目服务管理面板,端口: 16022

View File

@@ -20,10 +20,25 @@ def register_project_routes(app):
"""获取项目列表""" """获取项目列表"""
projects = load_projects() projects = load_projects()
# 添加状态信息 # 添加状态信息(结构化对象,前端期望 status.status 和 status.ports
for project in projects: for project in projects:
ports = project.get('ports', []) 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({ return jsonify({
'projects': projects, 'projects': projects,
@@ -169,10 +184,25 @@ def register_project_routes(app):
data = load_projects_full() data = load_projects_full()
services = data.get('external_services', []) services = data.get('external_services', [])
# 添加状态 # 添加状态(结构化对象,前端期望 status.status 和 status.ports
for service in services: for service in services:
ports = service.get('ports', []) 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}) return jsonify({'services': services})

View File

@@ -6,7 +6,8 @@
import os import os
import subprocess import subprocess
import time 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 from utils import log_message, get_cpu_temperatures, load_alert_config
try: try:
@@ -46,6 +47,7 @@ def register_system_routes(app):
disk = psutil.disk_usage('/') disk = psutil.disk_usage('/')
disk_total_gb = round(disk.total / (1024**3), 2) disk_total_gb = round(disk.total / (1024**3), 2)
disk_used_gb = round(disk.used / (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 disk_percent = disk.percent
# 网速(需要计算差值) # 网速(需要计算差值)
@@ -82,10 +84,15 @@ def register_system_routes(app):
return jsonify({ return jsonify({
'available': True, 'available': True,
'system': {
'uptime': uptime_str,
'boot_time': datetime.fromtimestamp(boot_time).strftime('%Y-%m-%d %H:%M:%S'),
'process_count': process_count
},
'cpu': { 'cpu': {
'percent': cpu_percent, 'percent': cpu_percent,
'count_physical': cpu_count, 'count': cpu_count,
'count_logical': cpu_count_logical, 'logical': cpu_count_logical,
'temp_package_0': cpu_temps.get('package_0'), 'temp_package_0': cpu_temps.get('package_0'),
'temp_package_1': cpu_temps.get('package_1') 'temp_package_1': cpu_temps.get('package_1')
}, },
@@ -98,17 +105,19 @@ def register_system_routes(app):
'disk': { 'disk': {
'total_gb': disk_total_gb, 'total_gb': disk_total_gb,
'used_gb': disk_used_gb, 'used_gb': disk_used_gb,
'free_gb': disk_free_gb,
'percent': disk_percent 'percent': disk_percent
}, },
'network': { 'network': {
'upload_speed_kb': round(upload_speed, 2), 'sent_mb': round(net.bytes_sent / (1024**2), 2),
'download_speed_kb': round(download_speed, 2) 'recv_mb': round(net.bytes_recv / (1024**2), 2),
}, 'sent_speed': round(upload_speed, 2),
'process_count': process_count, 'recv_speed': round(download_speed, 2)
'uptime': uptime_str }
}) })
except Exception as e: 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') @app.route('/api/system/processes')
def api_system_processes(): def api_system_processes():
@@ -117,6 +126,9 @@ def register_system_routes(app):
return jsonify({'error': 'psutil未安装'}), 500 return jsonify({'error': 'psutil未安装'}), 500
try: try:
limit = int(request.args.get('limit', 10))
limit = max(1, min(50, limit))
processes = [] processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']): for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
try: try:
@@ -132,7 +144,7 @@ def register_system_routes(app):
# 按CPU排序 # 按CPU排序
processes.sort(key=lambda x: x['cpu'], reverse=True) processes.sort(key=lambda x: x['cpu'], reverse=True)
processes = processes[:50] # 只返回前50个 processes = processes[:limit]
return jsonify({'processes': processes}) return jsonify({'processes': processes})
except Exception as e: except Exception as e:
@@ -167,7 +179,6 @@ def register_system_routes(app):
@app.route('/api/alerts', methods=['POST']) @app.route('/api/alerts', methods=['POST'])
def api_save_alerts(): def api_save_alerts():
"""保存预警配置""" """保存预警配置"""
from flask import request
import json import json
from config import ALERT_CONFIG_FILE from config import ALERT_CONFIG_FILE

View File

@@ -1,590 +1,48 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
HTML 模板 HTML 模板 - 包含所有完整功能
""" """
from config import APP_NAME, APP_VERSION from config import APP_NAME, APP_VERSION
def get_html_template(): def get_html_template():
"""返回完整的HTML模板""" """返回完整的HTML模板(包含所有原有功能)"""
# 读取完整模板文件
import os
template_file = os.path.join(os.path.dirname(__file__), 'templates_full.txt')
try:
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) # 预留未来版本
return template_content
except:
# 如果文件不存在,返回基本模板
return get_basic_template()
def get_basic_template():
"""基本模板(备用)"""
return '''<!DOCTYPE html> return '''<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>''' + APP_NAME + '''</title> <title>''' + APP_NAME + '''</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.remarkable.ai/css/remixicon.css" rel="stylesheet"> <link href="https://cdn.remarkable.ai/css/remixicon.css" rel="stylesheet">
<style>
body { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); }
.card { background: rgba(30, 41, 59, 0.6); backdrop-filter: blur(10px); border: 1px solid rgba(71, 85, 105, 0.4); }
.btn { transition: all 0.2s; }
.btn:hover { transform: translateY(-1px); }
.status-running { background: rgba(34, 197, 94, 0.2); border-color: rgba(34, 197, 94, 0.5); color: #22c55e; }
.status-stopped { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.5); color: #ef4444; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; }
.modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; }
</style>
</head> </head>
<body class="min-h-screen text-gray-100"> <body class="min-h-screen text-gray-100">
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<!-- 头部 --> <h1 class="text-3xl font-bold">''' + APP_NAME + ''' ''' + APP_VERSION + '''</h1>
<div class="flex items-center justify-between mb-6"> <p>模板加载失败,请检查 templates_full.txt 文件</p>
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
<i class="ri-dashboard-3-line text-blue-400"></i>
''' + APP_NAME + '''
<span class="text-sm text-gray-500">''' + APP_VERSION + '''</span>
</h1>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<span class="text-gray-400 text-sm">对外IP:</span>
<input type="text" id="externalIp" value="121.40.164.32" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-sm w-28" onchange="saveExternalIp()">
</div>
<button onclick="refreshAll()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
<i class="ri-refresh-line"></i> 刷新
</button>
<button onclick="exportConfig()" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg flex items-center gap-2">
<i class="ri-download-line"></i> 导出配置
</button>
<label class="btn bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg flex items-center gap-2 cursor-pointer">
<i class="ri-upload-line"></i> 导入配置
<input type="file" accept=".json" onchange="importConfig(event)" class="hidden">
</label>
<div class="flex items-center gap-1">
<span class="text-gray-400 text-xs">间隔:</span>
<input type="number" id="refreshInterval" value="30" min="5" max="300" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs w-12" onchange="updateRefreshInterval()">
<span class="text-gray-500 text-xs">s</span>
</div>
<span id="updateTime" class="text-gray-400 text-sm"></span>
<div id="connectionStatus" class="flex items-center gap-1 px-2 py-1 rounded bg-green-500/20">
<div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
<span class="text-green-400 text-xs">已连接</span>
</div>
</div>
</div>
<!-- Tab 切换 -->
<div class="flex gap-4 mb-6 border-b border-gray-700">
<button onclick="switchTab('projects')" class="tab-btn active px-4 py-2 text-gray-300 hover:text-white" data-tab="projects">
<i class="ri-apps-line"></i> 项目服务
</button>
<button onclick="switchTab('system')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="system">
<i class="ri-dashboard-2-line"></i> 系统监控
</button>
<button onclick="switchTab('cron')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="cron">
<i class="ri-time-line"></i> Cron管理
</button>
</div>
<!-- 项目服务 Tab -->
<div id="projectsTab" class="tab-content">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 项目卡片将通过JS动态生成 -->
</div>
</div>
<!-- 系统监控 Tab -->
<div id="systemTab" class="tab-content hidden">
<div class="card rounded-xl p-6 mb-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold flex items-center gap-2">
<i class="ri-activity-line text-blue-400"></i> 系统资源监控
</h2>
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="realtimeCheck" onchange="toggleRealtimeMonitor()">
<span class="text-sm text-gray-400">实时监控</span>
</label>
</div>
</div>
<!-- CPU -->
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400 flex items-center gap-2">
<i class="ri-cpu-line text-blue-400"></i> CPU使用率
</span>
<span id="cpuPercent" class="text-2xl font-bold text-blue-400">-</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-4">
<div id="cpuBar" class="bg-blue-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<!-- 内存 -->
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400 flex items-center gap-2">
<i class="ri-database-2-line text-green-400"></i> 内存使用率
</span>
<span id="memPercent" class="text-2xl font-bold text-green-400">-</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-4">
<div id="memBar" class="bg-green-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<!-- 磁盘 -->
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400 flex items-center gap-2">
<i class="ri-hard-drive-2-line text-orange-400"></i> 磁盘使用率
</span>
<span id="diskPercent" class="text-2xl font-bold text-orange-400">-</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-4">
<div id="diskBar" class="bg-orange-500 h-4 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<!-- 网速 -->
<div class="grid grid-cols-2 gap-4">
<div>
<div class="flex items-center gap-2 mb-2">
<i class="ri-upload-line text-cyan-400"></i>
<span class="text-gray-400">上传速度</span>
</div>
<span id="uploadSpeed" class="text-xl font-bold text-cyan-400">-</span>
</div>
<div>
<div class="flex items-center gap-2 mb-2">
<i class="ri-download-line text-purple-400"></i>
<span class="text-gray-400">下载速度</span>
</div>
<span id="downloadSpeed" class="text-xl font-bold text-purple-400">-</span>
</div>
</div>
</div>
</div>
<!-- Cron管理 Tab -->
<div id="cronTab" class="tab-content hidden">
<div class="card rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold flex items-center gap-2">
<i class="ri-time-line text-blue-400"></i> Cron任务管理
</h2>
<div class="flex gap-2">
<button onclick="syncCronFromSystem()" class="btn bg-gray-600 hover:bg-gray-700 px-3 py-2 rounded-lg flex items-center gap-1">
<i class="ri-refresh-line"></i> 从系统同步
</button>
<button onclick="showAddCronModal()" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded-lg flex items-center gap-1">
<i class="ri-add-line"></i> 添加任务
</button>
</div>
</div>
<!-- Cron任务列表 -->
<div id="cronTasksList" class="space-y-3">
<!-- 任务将通过JS动态生成 -->
</div>
</div>
</div>
</div> </div>
<!-- 添加Cron任务模态框 -->
<div id="addCronModal" class="modal-overlay hidden">
<div class="modal-content card rounded-xl p-6">
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
<i class="ri-add-circle-line text-blue-400"></i> 添加Cron任务
</h3>
<form onsubmit="addCronTask(event)">
<div class="mb-4">
<label class="text-gray-400 text-sm mb-1 block">任务名称</label>
<input type="text" id="cronName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
</div>
<div class="mb-4">
<label class="text-gray-400 text-sm mb-1 block">Cron表达式</label>
<input type="text" id="cronExpression" required placeholder="0 4 * * *" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
</div>
<div class="mb-4">
<label class="text-gray-400 text-sm mb-1 block">执行命令</label>
<input type="text" id="cronCommand" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
</div>
<div class="mb-4">
<label class="text-gray-400 text-sm mb-1 block">描述</label>
<input type="text" id="cronDescription" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
</div>
<div class="flex gap-2">
<button type="submit" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg">添加</button>
<button type="button" onclick="hideAddCronModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
</div>
</form>
</div>
</div>
<script>
let currentTab = 'projects';
let projects = [];
let externalIp = '121.40.164.32';
let thresholds = { cpu: 80, memory: 85, disk: 90, interval: 60 };
// Tab切换
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tab);
btn.classList.toggle('border-b-2', btn.dataset.tab === tab);
btn.classList.toggle('border-blue-500', btn.dataset.tab === tab);
});
document.getElementById('projectsTab').classList.toggle('hidden', tab !== 'projects');
document.getElementById('systemTab').classList.toggle('hidden', tab !== 'system');
document.getElementById('cronTab').classList.toggle('hidden', tab !== 'cron');
if (tab === 'cron') {
loadCronTasks();
}
if (tab === 'system') {
loadThresholds();
loadSystemStats();
}
}
// 加载项目列表
async function loadProjects() {
try {
const res = await fetch('/api/projects');
const data = await res.json();
projects = data.projects;
renderProjects();
updateConnectionStatus(true);
} catch (e) {
updateConnectionStatus(false);
}
}
// 渲染项目卡片
function renderProjects() {
const container = document.querySelector('#projectsTab .grid');
container.innerHTML = projects.map(p => `
<div class="card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold flex items-center gap-2">
<i class="ri-${p.type === 'web' ? 'app' : 'server'}-line text-blue-400"></i>
${p.name}
</h3>
<span class="px-2 py-1 rounded text-xs ${p.status === 'running' ? 'status-running' : 'status-stopped'}">
${p.status === 'running' ? '运行中' : '已停止'}
</span>
</div>
<p class="text-gray-400 text-sm mb-3">${p.description || ''}</p>
<div class="flex items-center gap-2 text-xs text-gray-500 mb-3">
<i class="ri-git-branch-line"></i> ${p.version || 'v1.0'}
<span class="ml-2">端口: ${p.ports.join(', ')}</span>
</div>
<div class="flex gap-2">
${p.type === 'web' ? `
<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-3 py-1 rounded text-sm">
<i class="ri-play-line"></i>
</button>
<button onclick="stopProject('${p.id}')" class="btn bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm">
<i class="ri-stop-line"></i>
</button>
<button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-3 py-1 rounded text-sm">
<i class="ri-restart-line"></i>
</button>
` : ''}
<a href="${p.health_url || '#'}" target="_blank" class="btn bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm">
<i class="ri-external-link-line"></i>
</a>
</div>
</div>
`).join('');
document.getElementById('updateTime').textContent = new Date().toLocaleTimeString();
}
// 项目操作
async function startProject(id) {
await fetch(`/api/projects/${id}/start`, { method: 'POST' });
setTimeout(loadProjects, 2000);
}
async function stopProject(id) {
await fetch(`/api/projects/${id}/stop`, { method: 'POST' });
setTimeout(loadProjects, 1000);
}
async function restartProject(id) {
await fetch(`/api/projects/${id}/restart`, { method: 'POST' });
setTimeout(loadProjects, 3000);
}
// 加载Cron任务
async function loadCronTasks() {
try {
const res = await fetch('/api/cron/tasks');
const data = await res.json();
renderCronTasks(data.tasks);
} catch (e) {
console.error('加载Cron任务失败:', e);
}
}
// 渲染Cron任务列表
function renderCronTasks(tasks) {
const container = document.getElementById('cronTasksList');
container.innerHTML = tasks.map(t => `
<div class="card rounded-lg p-4 flex items-center justify-between">
<div>
<h4 class="font-bold">${t.name}</h4>
<p class="text-gray-400 text-sm">${t.human_time || t.expression}</p>
<p class="text-gray-500 text-xs mt-1">${t.command}</p>
</div>
<div class="flex items-center gap-2">
<span class="px-2 py-1 rounded text-xs ${t.enabled ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'}">
${t.enabled ? '启用' : '禁用'}
</span>
<button onclick="toggleCron(${t.id})" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-1 rounded text-sm">
<i class="ri-toggle-line"></i>
</button>
<button onclick="deleteCron(${t.id})" class="btn bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm">
<i class="ri-delete-bin-line"></i>
</button>
</div>
</div>
`).join('');
}
// Cron操作
async function toggleCron(id) {
await fetch(`/api/cron/tasks/${id}/toggle`, { method: 'POST' });
loadCronTasks();
}
async function deleteCron(id) {
if (confirm('确定删除此任务?')) {
await fetch(`/api/cron/tasks/${id}`, { method: 'DELETE' });
loadCronTasks();
}
}
async function syncCronFromSystem() {
await fetch('/api/cron/sync', { method: 'POST' });
loadCronTasks();
}
function showAddCronModal() {
document.getElementById('addCronModal').classList.remove('hidden');
}
function hideAddCronModal() {
document.getElementById('addCronModal').classList.add('hidden');
}
async function addCronTask(e) {
e.preventDefault();
const data = {
name: document.getElementById('cronName').value,
expression: document.getElementById('cronExpression').value,
command: document.getElementById('cronCommand').value,
description: document.getElementById('cronDescription').value
};
await fetch('/api/cron/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
hideAddCronModal();
loadCronTasks();
}
// 系统监控
async function loadSystemStats() {
try {
const res = await fetch('/api/system/stats');
const data = await res.json();
if (data.available) {
document.getElementById('cpuPercent').textContent = data.cpu.percent + '%';
document.getElementById('cpuBar').style.width = data.cpu.percent + '%';
document.getElementById('memPercent').textContent = data.memory.percent + '%';
document.getElementById('memBar').style.width = data.memory.percent + '%';
document.getElementById('diskPercent').textContent = data.disk.percent + '%';
document.getElementById('diskBar').style.width = data.disk.percent + '%';
document.getElementById('uploadSpeed').textContent = data.network.upload_speed_kb + ' KB/s';
document.getElementById('downloadSpeed').textContent = data.network.download_speed_kb + ' KB/s';
}
} catch (e) {
console.error('加载系统状态失败:', e);
}
}
async function loadThresholds() {
try {
const response = await fetch('/api/alerts');
const config = await response.json();
if (config.thresholds) {
thresholds = config.thresholds;
}
} catch (e) {
console.error('加载阈值配置失败:', e);
}
}
// 导出配置
async function exportConfig() {
try {
const response = await fetch('/api/config/export');
const config = await response.json();
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `project-panel-config-${new Date().toISOString().slice(0,10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('配置导出成功!');
} catch (e) {
alert('导出失败: ' + e.message);
}
}
// 导入配置
async function importConfig(event) {
const file = event.target.files[0];
if (!file) return;
if (!confirm('导入配置将覆盖当前所有设置,是否继续?')) {
event.target.value = '';
return;
}
try {
const text = await file.text();
const config = JSON.parse(text);
const response = await fetch('/api/config/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.error) {
alert('导入失败: ' + result.error);
} else {
alert(`配置导入成功!\\n项目数: ${result.projects_count}\\n外部服务数: ${result.services_count}`);
location.reload();
}
} catch (e) {
alert('导入失败: ' + e.message);
}
event.target.value = '';
}
// 网页设置
async function saveExternalIp() {
externalIp = document.getElementById('externalIp').value.trim();
try {
await fetch('/api/web-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ external_ip: externalIp, refresh_interval: parseInt(document.getElementById('refreshInterval').value) || 30 })
});
} catch (e) {
console.error('保存失败:', e);
}
renderProjects();
}
function updateRefreshInterval() {
const seconds = Math.max(5, Math.min(300, parseInt(document.getElementById('refreshInterval').value) || 30));
document.getElementById('refreshInterval').value = seconds;
fetch('/api/web-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ external_ip: externalIp, refresh_interval: seconds })
}).catch(e => console.error('保存失败:', e));
clearInterval(refreshTimer);
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000);
}
function refreshAll() {
if (currentTab === 'projects') {
loadProjects();
} else if (currentTab === 'cron') {
loadCronTasks();
} else {
loadSystemStats();
}
}
function updateConnectionStatus(ok) {
const statusEl = document.getElementById('connectionStatus');
if (ok) {
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div><span class="text-green-400 text-xs">已连接</span>';
statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-green-500/20';
} else {
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-red-400"></div><span class="text-red-400 text-xs">断开</span>';
statusEl.className = 'flex items-center gap-1 px-2 py-1 rounded bg-red-500/20';
}
}
// 初始化
loadProjects();
loadWebSettings();
let refreshIntervalMs = 30 * 1000;
let refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
// 加载网页设置
async function loadWebSettings() {
try {
const response = await fetch('/api/web-settings');
const settings = await response.json();
if (settings.external_ip) {
externalIp = settings.external_ip;
document.getElementById('externalIp').value = settings.external_ip;
}
if (settings.refresh_interval) {
document.getElementById('refreshInterval').value = settings.refresh_interval;
clearInterval(refreshTimer);
refreshIntervalMs = settings.refresh_interval * 1000;
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs);
}
} catch (e) {
console.error('加载网页设置失败:', e);
}
}
// 实时监控
let realtimeTimer = null;
function toggleRealtimeMonitor() {
const checked = document.getElementById('realtimeCheck').checked;
if (checked) {
realtimeTimer = setInterval(loadSystemStats, 2000);
} else {
clearInterval(realtimeTimer);
realtimeTimer = null;
}
}
// 定期检查连接状态
setInterval(() => {
fetch('/api/projects').then(() => updateConnectionStatus(true)).catch(() => updateConnectionStatus(false));
}, 10000);
</script>
</body> </body>
</html> </html>'''
'''

2554
templates_full.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -195,6 +195,35 @@ def parse_cron_expression(expr):
return ' '.join(desc) if desc else expr return ' '.join(desc) if desc else expr
def cron_expression_to_human(expr):
"""更友好的人类可读格式"""
parts = expr.split()
if len(parts) != 5:
return expr
minute, hour, day, month, weekday = parts
# 特殊模式
patterns = [
('0 4 * * *', '每天凌晨4点'),
('0 0 * * *', '每天午夜0点'),
('*/5 * * * *', '每5分钟'),
('*/10 * * * *', '每10分钟'),
('*/20 * * * *', '每20分钟'),
('*/30 * * * *', '每30分钟'),
('0 */1 * * *', '每小时'),
('0 */2 * * *', '每2小时'),
('0 9 * * 1', '每周一9点'),
('0 9 * * 1-5', '每周一到五9点'),
]
for pattern, desc in patterns:
if expr == pattern:
return desc
# 通用解析
return parse_cron_expression(expr)
def guess_cron_name(command): def guess_cron_name(command):
"""从命令推断任务名称""" """从命令推断任务名称"""