Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df319b9c4a | |||
| ecbba836d9 | |||
| bb19141edc | |||
| c960048042 | |||
| 51f7ee9ad0 | |||
| ea904fb2b2 | |||
| 21240dc108 | |||
| 1d7f4c2bed | |||
| 88c11277cf | |||
| e5ae822ed2 | |||
| 50341749b6 | |||
| 7904117b1d |
10
README.md
10
README.md
@@ -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 分离
|
||||||
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
- 🔧 **易于维护**: 每个模块职责明确,便于扩展和维护
|
||||||
|
|||||||
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/database.cpython-312.pyc
Normal file
BIN
__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_config.cpython-312.pyc
Normal file
BIN
__pycache__/routes_config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_cron.cpython-312.pyc
Normal file
BIN
__pycache__/routes_cron.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_email.cpython-312.pyc
Normal file
BIN
__pycache__/routes_email.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_project.cpython-312.pyc
Normal file
BIN
__pycache__/routes_project.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_system.cpython-312.pyc
Normal file
BIN
__pycache__/routes_system.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/templates.cpython-312.pyc
Normal file
BIN
__pycache__/templates.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/utils.cpython-312.pyc
Normal file
BIN
__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
@@ -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
|
||||||
|
|||||||
17
logs/app.log
17
logs/app.log
@@ -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
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
598
templates.py
598
templates.py
@@ -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
2554
templates_full.txt
Normal file
File diff suppressed because it is too large
Load Diff
29
utils.py
29
utils.py
@@ -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):
|
||||||
"""从命令推断任务名称"""
|
"""从命令推断任务名称"""
|
||||||
|
|||||||
Reference in New Issue
Block a user