Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a155e8fcd9 |
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-310.pyc
Normal file
BIN
__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_discovery.cpython-312.pyc
Normal file
BIN
__pycache__/routes_discovery.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/routes_docker.cpython-312.pyc
Normal file
BIN
__pycache__/routes_docker.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/templates.cpython-310.pyc
Normal file
BIN
__pycache__/templates.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
446278
logs/app.log
446278
logs/app.log
File diff suppressed because it is too large
Load Diff
1
logs/watchdog.log
Normal file
1
logs/watchdog.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[2026-06-01 20:29:22] 守护进程启动,监控端口 16022
|
||||||
@@ -10,29 +10,35 @@
|
|||||||
"id": "pdf-translate-web",
|
"id": "pdf-translate-web",
|
||||||
"name": "PDF翻译助手",
|
"name": "PDF翻译助手",
|
||||||
"type": "web",
|
"type": "web",
|
||||||
"ports": [16021],
|
"ports": [
|
||||||
"directory": "works/pdf-translate-web",
|
16021
|
||||||
|
],
|
||||||
|
"directory": "pdf-translate-web",
|
||||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||||
"stop_cmd": "pkill -f 'python.*pdf-translate-web.*app.py' || true",
|
"stop_cmd": "pkill -f 'python.*pdf-translate-web.*app.py' || true",
|
||||||
"health_url": "http://localhost:16021/",
|
"health_url": "http://localhost:16021/",
|
||||||
"description": "英文PDF翻译中文网站,支持用户系统、会员体系",
|
"description": "英文PDF翻译中文网站,支持用户系统、会员体系",
|
||||||
"admin_url": "http://localhost:16021/admin",
|
"admin_url": "http://localhost:16021/admin",
|
||||||
"git_repo": "http://121.40.164.32:12007/coder/pdf-translate-web",
|
"git_repo": "http://121.40.164.32:12007/coder/pdf-translate-web",
|
||||||
"version": "v2.0.0"
|
"version": "v2.0.0",
|
||||||
|
"log_file": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "project-panel",
|
"id": "project-panel",
|
||||||
"name": "项目服务管理面板",
|
"name": "项目服务管理面板",
|
||||||
"type": "web",
|
"type": "web",
|
||||||
"ports": [16022],
|
"ports": [
|
||||||
"directory": "works/project-panel",
|
16022
|
||||||
|
],
|
||||||
|
"directory": "project-panel",
|
||||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||||
"stop_cmd": "pkill -f 'python.*project-panel.*app.py' || true",
|
"stop_cmd": "pkill -f python.*project-panel.*app.py || true",
|
||||||
"health_url": "http://localhost:16022/",
|
"health_url": "http://localhost:16022/",
|
||||||
"description": "统一管理所有 Web 服务项目,支持 Docker 监控、系统资源监控、Cron 管理",
|
"description": "统一管理所有 Web 服务项目,支持 Docker 监控、系统资源监控、Cron 管理",
|
||||||
"admin_url": "http://localhost:16022",
|
"admin_url": "http://localhost:16022",
|
||||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel",
|
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel",
|
||||||
"version": "v3.0.0"
|
"version": "v3.0.0",
|
||||||
|
"log_file": "logs/app.log"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"external_services": [
|
"external_services": [
|
||||||
@@ -40,7 +46,9 @@
|
|||||||
"id": "meilisearch",
|
"id": "meilisearch",
|
||||||
"name": "Meilisearch 搜索引擎",
|
"name": "Meilisearch 搜索引擎",
|
||||||
"type": "external",
|
"type": "external",
|
||||||
"ports": [16001],
|
"ports": [
|
||||||
|
16001
|
||||||
|
],
|
||||||
"health_url": "http://localhost:16001/health",
|
"health_url": "http://localhost:16001/health",
|
||||||
"description": "高性能搜索引擎,只读监控"
|
"description": "高性能搜索引擎,只读监控"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ def register_project_routes(app):
|
|||||||
|
|
||||||
if project.get('start_cmds'):
|
if project.get('start_cmds'):
|
||||||
for name, info in project['start_cmds'].items():
|
for name, info in project['start_cmds'].items():
|
||||||
|
log_message(f"[API-START] 项目={project_id} 命令={info['cmd']} 目录={cwd}")
|
||||||
run_command(info['cmd'], cwd, project_id, f'start-{name}')
|
run_command(info['cmd'], cwd, project_id, f'start-{name}')
|
||||||
else:
|
else:
|
||||||
cmd = project.get('start_cmd', 'python3 app.py')
|
cmd = project.get('start_cmd', 'python3 app.py')
|
||||||
|
log_message(f"[API-START] 项目={project_id} 命令={cmd} 目录={cwd}")
|
||||||
run_command(cmd, cwd, project_id, 'start')
|
run_command(cmd, cwd, project_id, 'start')
|
||||||
|
|
||||||
return jsonify({'message': '服务启动中...'})
|
return jsonify({'message': '服务启动中...'})
|
||||||
@@ -128,7 +130,9 @@ def register_project_routes(app):
|
|||||||
'directory': data.get('directory', ''),
|
'directory': data.get('directory', ''),
|
||||||
'start_cmd': data.get('start_cmd', ''),
|
'start_cmd': data.get('start_cmd', ''),
|
||||||
'stop_cmd': data.get('stop_cmd', ''),
|
'stop_cmd': data.get('stop_cmd', ''),
|
||||||
|
'log_file': data.get('log_file', ''),
|
||||||
'health_url': data.get('health_url', ''),
|
'health_url': data.get('health_url', ''),
|
||||||
|
'admin_url': data.get('admin_url', ''),
|
||||||
'description': data.get('description', ''),
|
'description': data.get('description', ''),
|
||||||
'git_repo': data.get('git_repo', ''),
|
'git_repo': data.get('git_repo', ''),
|
||||||
'version': data.get('version', 'v1.0.0')
|
'version': data.get('version', 'v1.0.0')
|
||||||
@@ -152,7 +156,7 @@ def register_project_routes(app):
|
|||||||
return jsonify({'error': '项目不存在'}), 404
|
return jsonify({'error': '项目不存在'}), 404
|
||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
for key in ['name', 'ports', 'directory', 'start_cmd', 'stop_cmd', 'health_url', 'description', 'git_repo', 'version']:
|
for key in ['name', 'ports', 'directory', 'log_file', 'start_cmd', 'stop_cmd', 'health_url', 'admin_url', 'description', 'git_repo', 'version']:
|
||||||
if key in data:
|
if key in data:
|
||||||
project[key] = data[key]
|
project[key] = data[key]
|
||||||
|
|
||||||
@@ -162,6 +166,55 @@ def register_project_routes(app):
|
|||||||
|
|
||||||
return jsonify({'message': '项目更新成功'})
|
return jsonify({'message': '项目更新成功'})
|
||||||
|
|
||||||
|
@app.route('/api/projects/<project_id>/log')
|
||||||
|
def api_project_log(project_id):
|
||||||
|
"""获取项目日志"""
|
||||||
|
projects = load_projects()
|
||||||
|
project = next((p for p in projects if p['id'] == project_id), None)
|
||||||
|
if not project:
|
||||||
|
return jsonify({'error': '项目不存在'}), 404
|
||||||
|
|
||||||
|
lines = request.args.get('lines', 200, type=int)
|
||||||
|
lines = max(1, min(lines, 5000)) # 限制 1-5000 行
|
||||||
|
|
||||||
|
log_file = project.get('log_file', '')
|
||||||
|
if not log_file:
|
||||||
|
# 默认:项目目录下的 logs/app.log
|
||||||
|
cwd = project.get('directory', '')
|
||||||
|
if cwd:
|
||||||
|
from config import WORKSPACE_DIR
|
||||||
|
import os
|
||||||
|
abs_dir = cwd if cwd.startswith('/') else os.path.join(WORKSPACE_DIR, cwd)
|
||||||
|
log_file = os.path.join(abs_dir, 'logs', 'app.log')
|
||||||
|
|
||||||
|
# 支持相对路径和绝对路径
|
||||||
|
import os
|
||||||
|
if not log_file.startswith('/'):
|
||||||
|
cwd = project.get('directory', '')
|
||||||
|
if cwd:
|
||||||
|
from config import WORKSPACE_DIR
|
||||||
|
abs_dir = cwd if cwd.startswith('/') else os.path.join(WORKSPACE_DIR, cwd)
|
||||||
|
log_file = os.path.join(abs_dir, log_file)
|
||||||
|
|
||||||
|
if not os.path.exists(log_file):
|
||||||
|
return jsonify({'log': f'日志文件不存在: {log_file}'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取最后 N 行(高效方式)
|
||||||
|
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
all_lines = f.readlines()
|
||||||
|
tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||||
|
content = ''.join(tail_lines)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'log': content if content else '(日志为空)',
|
||||||
|
'file': log_file,
|
||||||
|
'total_lines': len(all_lines),
|
||||||
|
'shown_lines': len(tail_lines)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': f'读取日志失败: {str(e)}'}), 500
|
||||||
|
|
||||||
@app.route('/api/projects/<project_id>', methods=['DELETE'])
|
@app.route('/api/projects/<project_id>', methods=['DELETE'])
|
||||||
def api_delete_project(project_id):
|
def api_delete_project(project_id):
|
||||||
"""删除项目"""
|
"""删除项目"""
|
||||||
|
|||||||
@@ -701,15 +701,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增自定义服务模态框 -->
|
<!-- 新增/编辑自定义服务模态框 -->
|
||||||
<div id="addServiceModal" class="modal-overlay hidden">
|
<div id="addServiceModal" class="modal-overlay hidden">
|
||||||
<div class="card rounded-xl modal-content p-6">
|
<div class="card rounded-xl modal-content p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="font-bold text-lg">新增自定义服务</h3>
|
<h3 id="addServiceModalTitle" class="font-bold text-lg">新增自定义服务</h3>
|
||||||
<button onclick="closeAddServiceModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
<button onclick="closeAddServiceModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="addServiceForm" onsubmit="saveCustomService(event)">
|
<form id="addServiceForm" onsubmit="saveCustomService(event)">
|
||||||
|
<input type="hidden" id="editServiceId">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block text-gray-400 text-sm mb-1">服务名称 *</label>
|
<label class="block text-gray-400 text-sm mb-1">服务名称 *</label>
|
||||||
<input type="text" id="serviceName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:外部API、监控面板">
|
<input type="text" id="serviceName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:外部API、监控面板">
|
||||||
@@ -912,6 +913,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑项目配置模态框 -->
|
||||||
|
<div id="editProjectModal" class="modal-overlay hidden">
|
||||||
|
<div class="card rounded-xl modal-content p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 id="editProjectModalTitle" class="font-bold text-lg">编辑项目配置</h3>
|
||||||
|
<button onclick="closeEditProjectModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="editProjectForm" onsubmit="saveProjectEdit(event)">
|
||||||
|
<input type="hidden" id="editProjectId">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">项目名称</label>
|
||||||
|
<input type="text" id="editProjectName" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">项目目录 <span class="text-xs text-gray-500">(相对或绝对路径)</span></label>
|
||||||
|
<input type="text" id="editProjectDir" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="相对:works/my-project 或 绝对:/home/user/project">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">端口(逗号分隔)</label>
|
||||||
|
<input type="text" id="editProjectPorts" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:16022,16023">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">健康检查URL</label>
|
||||||
|
<input type="text" id="editProjectHealthUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://localhost:16022/">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">后台URL</label>
|
||||||
|
<input type="text" id="editProjectAdminUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://localhost:16022/admin">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">Git 仓库地址</label>
|
||||||
|
<input type="text" id="editProjectGitRepo" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://.../project.git">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">版本</label>
|
||||||
|
<input type="text" id="editProjectVersion" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="v1.0.0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">📁 日志文件路径 <span class="text-xs text-gray-500">(相对或绝对路径)</span></label>
|
||||||
|
<input type="text" id="editProjectLogFile" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" placeholder="相对:logs/app.log 或绝对路径。留空则默认 项目目录/logs/app.log">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">服务日志文件的位置,点击「日志」按钮时读取此文件</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">📋 启动命令</label>
|
||||||
|
<textarea id="editProjectStartCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" rows="3" placeholder="例如:nohup python3 app.py > logs/app.log 2>&1 & disown"></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">服务启动时执行的命令</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">🛑 停止命令</label>
|
||||||
|
<textarea id="editProjectStopCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" rows="2" placeholder="例如:pkill -f 'python.*project.*app.py' || true"></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">服务停止时执行的命令</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-400 text-sm mb-1">描述</label>
|
||||||
|
<input type="text" id="editProjectDesc" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" onclick="closeEditProjectModal()" 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">💾 保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 导航按钮 -->
|
<!-- 导航按钮 -->
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
|
<button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
|
||||||
@@ -1404,6 +1482,16 @@
|
|||||||
const res = await fetch('/api/projects');
|
const res = await fetch('/api/projects');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
projects = data.projects;
|
projects = data.projects;
|
||||||
|
|
||||||
|
// 加载外部服务列表
|
||||||
|
try {
|
||||||
|
const extRes = await fetch('/api/external-services');
|
||||||
|
const extData = await extRes.json();
|
||||||
|
window._externalServices = extData.services || [];
|
||||||
|
} catch (e) {
|
||||||
|
window._externalServices = [];
|
||||||
|
}
|
||||||
|
|
||||||
renderProjects();
|
renderProjects();
|
||||||
updateStats();
|
updateStats();
|
||||||
updateConnectionStatus(true);
|
updateConnectionStatus(true);
|
||||||
@@ -1544,7 +1632,8 @@
|
|||||||
<div class="flex items-center gap-1 text-xs flex-wrap">
|
<div class="flex items-center gap-1 text-xs flex-wrap">
|
||||||
<a href="${p.url}" target="_blank" class="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30">访问</a>
|
<a href="${p.url}" target="_blank" class="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30">访问</a>
|
||||||
${p.admin_url ? `<a href="${p.admin_url}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
|
${p.admin_url ? `<a href="${p.admin_url}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
|
||||||
<button onclick="deleteCustomService('${p.id}')" class="text-red-400 hover:text-red-300 ml-2" title="删除服务"><i class="ri-delete-bin-line"></i></button>
|
<button onclick="editCustomService('${p.id}')" class="text-blue-400 hover:text-blue-300 ml-1" title="编辑服务"><i class="ri-edit-line"></i></button>
|
||||||
|
<button onclick="deleteCustomService('${p.id}')" class="text-red-400 hover:text-red-300 ml-1" title="删除服务"><i class="ri-delete-bin-line"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1590,6 +1679,7 @@
|
|||||||
<button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-2 py-0.5 rounded text-xs">重启</button>
|
<button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-2 py-0.5 rounded text-xs">重启</button>
|
||||||
` : `<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-2 py-0.5 rounded text-xs">启动</button>`}
|
` : `<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-2 py-0.5 rounded text-xs">启动</button>`}
|
||||||
<button onclick="viewLog('${p.id}')" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-0.5 rounded text-xs">日志</button>
|
<button onclick="viewLog('${p.id}')" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-0.5 rounded text-xs">日志</button>
|
||||||
|
<button onclick="editProject('${p.id}')" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded text-xs">编辑</button>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="toggleServiceActive('${p.id}')" class="text-xs ${isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-400 hover:text-gray-300'}" title="${isActive ? '点击归档' : '点击激活'}">
|
<button onclick="toggleServiceActive('${p.id}')" class="text-xs ${isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-400 hover:text-gray-300'}" title="${isActive ? '点击归档' : '点击激活'}">
|
||||||
<i class="ri-${isActive ? 'checkbox-circle' : 'archive'}-line"></i>
|
<i class="ri-${isActive ? 'checkbox-circle' : 'archive'}-line"></i>
|
||||||
@@ -1819,7 +1909,7 @@
|
|||||||
return Array.from(checkboxes).map(cb => cb.value);
|
return Array.from(checkboxes).map(cb => cb.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResourceChange() {
|
function onResourceChange(preselectedServiceId) {
|
||||||
const selected = getSelectedResources();
|
const selected = getSelectedResources();
|
||||||
const unitEl = document.getElementById('emailRuleThresholdUnit');
|
const unitEl = document.getElementById('emailRuleThresholdUnit');
|
||||||
const thresholdEl = document.getElementById('emailRuleThreshold');
|
const thresholdEl = document.getElementById('emailRuleThreshold');
|
||||||
@@ -1833,7 +1923,7 @@
|
|||||||
// 显示/隐藏服务选择器
|
// 显示/隐藏服务选择器
|
||||||
if (hasServiceDown) {
|
if (hasServiceDown) {
|
||||||
serviceSelector.style.display = 'block';
|
serviceSelector.style.display = 'block';
|
||||||
populateServiceSelector();
|
populateServiceSelector(preselectedServiceId);
|
||||||
// 服务下线使用分钟作为阈值单位
|
// 服务下线使用分钟作为阈值单位
|
||||||
unitEl.textContent = '分钟';
|
unitEl.textContent = '分钟';
|
||||||
thresholdEl.value = 5;
|
thresholdEl.value = 5;
|
||||||
@@ -1872,17 +1962,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateServiceSelector() {
|
function populateServiceSelector(preselectedId) {
|
||||||
const sel = document.getElementById('emailRuleServiceId');
|
const sel = document.getElementById('emailRuleServiceId');
|
||||||
const currentVal = sel.value;
|
const currentVal = preselectedId || sel.value;
|
||||||
// 收集所有web类型服务
|
|
||||||
const webServices = projects.filter(p => p.type === 'web');
|
// 收集所有可监控的服务
|
||||||
let options = '<option value="">-- 请选择要监控的服务 --</option>';
|
const allServices = [];
|
||||||
webServices.forEach(p => {
|
|
||||||
const portsStr = p.ports ? p.ports.join(',') : '';
|
// 1. Web 类型项目
|
||||||
const selected = p.id === currentVal ? ' selected' : '';
|
const webProjects = projects.filter(p => p.type === 'web');
|
||||||
options += `<option value="${p.id}"${selected}>${p.name} (端口: ${portsStr})</option>`;
|
webProjects.forEach(p => {
|
||||||
|
allServices.push({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
ports: p.ports || [],
|
||||||
|
type: 'web',
|
||||||
|
description: p.description || ''
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 自定义服务(localStorage)
|
||||||
|
const customServices = getCustomServices();
|
||||||
|
customServices.forEach(s => {
|
||||||
|
const port = s.port ? [parseInt(s.port)] : [];
|
||||||
|
allServices.push({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
ports: port,
|
||||||
|
type: 'custom',
|
||||||
|
description: s.description || '自定义外部服务'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 外部服务(从 projects.json 中的 external_services)
|
||||||
|
const externalServices = window._externalServices || [];
|
||||||
|
externalServices.forEach(s => {
|
||||||
|
allServices.push({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
ports: s.ports || [],
|
||||||
|
type: 'external',
|
||||||
|
description: s.description || ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeLabels = { web: 'Web服务', custom: '自定义', external: '外部' };
|
||||||
|
let options = '<option value="">-- 请选择要监控的服务 --</option>';
|
||||||
|
allServices.forEach(s => {
|
||||||
|
const portsStr = s.ports.length > 0 ? s.ports.join(',') : '无端口';
|
||||||
|
const label = s.type !== 'web' ? ` [${typeLabels[s.type]}]` : '';
|
||||||
|
const selected = s.id === currentVal ? ' selected' : '';
|
||||||
|
options += `<option value="${s.id}"${selected}>${s.name}${label} (端口: ${portsStr})</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allServices.length === 0) {
|
||||||
|
options += '<option value="" disabled>暂无可用服务</option>';
|
||||||
|
}
|
||||||
|
|
||||||
sel.innerHTML = options;
|
sel.innerHTML = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1946,11 +2082,8 @@
|
|||||||
const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
|
const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
|
||||||
if (cb) cb.checked = true;
|
if (cb) cb.checked = true;
|
||||||
});
|
});
|
||||||
onResourceChange();
|
onResourceChange(rule.serviceId);
|
||||||
// 服务下线相关
|
// 服务下线相关 - serviceId 已通过 onResourceChange 传入
|
||||||
if (rule.serviceId) {
|
|
||||||
document.getElementById('emailRuleServiceId').value = rule.serviceId;
|
|
||||||
}
|
|
||||||
document.getElementById('emailRuleThreshold').value = rule.threshold;
|
document.getElementById('emailRuleThreshold').value = rule.threshold;
|
||||||
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
|
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
|
||||||
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
|
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
|
||||||
@@ -2261,6 +2394,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showAddServiceModal() {
|
function showAddServiceModal() {
|
||||||
|
document.getElementById('addServiceModalTitle').textContent = '新增自定义服务';
|
||||||
|
document.getElementById('editServiceId').value = '';
|
||||||
document.getElementById('serviceName').value = '';
|
document.getElementById('serviceName').value = '';
|
||||||
document.getElementById('servicePort').value = '';
|
document.getElementById('servicePort').value = '';
|
||||||
document.getElementById('servicePath').value = '';
|
document.getElementById('servicePath').value = '';
|
||||||
@@ -2271,6 +2406,36 @@
|
|||||||
document.getElementById('addServiceModal').classList.remove('hidden');
|
document.getElementById('addServiceModal').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editCustomService(serviceId) {
|
||||||
|
const services = getCustomServices();
|
||||||
|
const svc = services.find(s => s.id === serviceId);
|
||||||
|
if (!svc) return;
|
||||||
|
|
||||||
|
document.getElementById('addServiceModalTitle').textContent = `编辑自定义服务: ${svc.name}`;
|
||||||
|
document.getElementById('editServiceId').value = svc.id;
|
||||||
|
document.getElementById('serviceName').value = svc.name || '';
|
||||||
|
document.getElementById('servicePort').value = svc.port || '';
|
||||||
|
|
||||||
|
// 尝试从 url 中解析 path
|
||||||
|
let path = '';
|
||||||
|
if (svc.url && svc.port) {
|
||||||
|
const prefix = `http://${externalIp}:${svc.port}`;
|
||||||
|
if (svc.url.startsWith(prefix)) {
|
||||||
|
path = svc.url.substring(prefix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('servicePath').value = path;
|
||||||
|
document.getElementById('serviceFullUrl').value = svc.url || '';
|
||||||
|
document.getElementById('serviceAdminUrl').value = svc.admin_url || '';
|
||||||
|
document.getElementById('serviceDesc').value = svc.description || '';
|
||||||
|
|
||||||
|
if (svc.port) {
|
||||||
|
updateServicePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('addServiceModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
function closeAddServiceModal() {
|
function closeAddServiceModal() {
|
||||||
document.getElementById('addServiceModal').classList.add('hidden');
|
document.getElementById('addServiceModal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -2290,6 +2455,7 @@
|
|||||||
function saveCustomService(event) {
|
function saveCustomService(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
const editId = document.getElementById('editServiceId').value;
|
||||||
const name = document.getElementById('serviceName').value.trim();
|
const name = document.getElementById('serviceName').value.trim();
|
||||||
const port = document.getElementById('servicePort').value.trim();
|
const port = document.getElementById('servicePort').value.trim();
|
||||||
const path = document.getElementById('servicePath').value.trim();
|
const path = document.getElementById('servicePath').value.trim();
|
||||||
@@ -2313,6 +2479,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const services = getCustomServices();
|
const services = getCustomServices();
|
||||||
|
|
||||||
|
if (editId) {
|
||||||
|
// 编辑模式:更新已有服务
|
||||||
|
const idx = services.findIndex(s => s.id === editId);
|
||||||
|
if (idx === -1) {
|
||||||
|
alert('服务不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
services[idx].name = name;
|
||||||
|
services[idx].port = port || '';
|
||||||
|
services[idx].url = mainUrl;
|
||||||
|
services[idx].admin_url = adminUrl || null;
|
||||||
|
services[idx].description = desc || '自定义外部服务';
|
||||||
|
} else {
|
||||||
|
// 新增模式
|
||||||
const newService = {
|
const newService = {
|
||||||
id: 'custom_' + Date.now(),
|
id: 'custom_' + Date.now(),
|
||||||
name: name,
|
name: name,
|
||||||
@@ -2324,8 +2505,9 @@
|
|||||||
isCustom: true
|
isCustom: true
|
||||||
};
|
};
|
||||||
services.push(newService);
|
services.push(newService);
|
||||||
saveCustomServices(services);
|
}
|
||||||
|
|
||||||
|
saveCustomServices(services);
|
||||||
closeAddServiceModal();
|
closeAddServiceModal();
|
||||||
loadProjects();
|
loadProjects();
|
||||||
}
|
}
|
||||||
@@ -2393,16 +2575,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function viewLog(id) {
|
async function viewLog(id, lines = 200) {
|
||||||
const project = projects.find(p => p.id === id);
|
const project = projects.find(p => p.id === id);
|
||||||
document.getElementById('logTitle').textContent = `${project?.name || id} - 日志`;
|
document.getElementById('logTitle').textContent = `${project?.name || id} - 日志 (最近${lines}行)`;
|
||||||
|
document.getElementById('logContent').textContent = '加载中...';
|
||||||
document.getElementById('logModal').classList.remove('hidden');
|
document.getElementById('logModal').classList.remove('hidden');
|
||||||
document.getElementById('logModal').classList.add('flex');
|
document.getElementById('logModal').classList.add('flex');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/projects/${id}/log`);
|
const res = await fetch(`/api/projects/${id}/log?lines=${lines}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
document.getElementById('logContent').textContent = data.log || '暂无日志';
|
let display = data.log || '暂无日志';
|
||||||
|
if (data.file) {
|
||||||
|
display = `📁 日志文件: ${data.file}\n📊 共 ${data.total_lines} 行,显示最近 ${data.shown_lines} 行\n${'─'.repeat(60)}\n\n${display}`;
|
||||||
|
}
|
||||||
|
document.getElementById('logContent').textContent = display;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('logContent').textContent = '获取日志失败: ' + e.message;
|
document.getElementById('logContent').textContent = '获取日志失败: ' + e.message;
|
||||||
}
|
}
|
||||||
@@ -2413,6 +2600,86 @@
|
|||||||
document.getElementById('logModal').classList.remove('flex');
|
document.getElementById('logModal').classList.remove('flex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 项目编辑 ====================
|
||||||
|
|
||||||
|
function editProject(id) {
|
||||||
|
const project = projects.find(p => p.id === id);
|
||||||
|
if (!project || project.type !== 'web') return;
|
||||||
|
|
||||||
|
document.getElementById('editProjectModalTitle').textContent = `${project.name} - 编辑配置`;
|
||||||
|
document.getElementById('editProjectId').value = project.id;
|
||||||
|
document.getElementById('editProjectName').value = project.name || '';
|
||||||
|
document.getElementById('editProjectDir').value = project.directory || '';
|
||||||
|
document.getElementById('editProjectPorts').value = (project.ports || []).join(',');
|
||||||
|
document.getElementById('editProjectHealthUrl').value = project.health_url || '';
|
||||||
|
document.getElementById('editProjectAdminUrl').value = project.admin_url || '';
|
||||||
|
document.getElementById('editProjectGitRepo').value = project.git_repo || '';
|
||||||
|
document.getElementById('editProjectVersion').value = project.version || '';
|
||||||
|
document.getElementById('editProjectLogFile').value = project.log_file || '';
|
||||||
|
document.getElementById('editProjectStartCmd').value = project.start_cmd || '';
|
||||||
|
document.getElementById('editProjectStopCmd').value = project.stop_cmd || '';
|
||||||
|
document.getElementById('editProjectDesc').value = project.description || '';
|
||||||
|
|
||||||
|
document.getElementById('editProjectModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditProjectModal() {
|
||||||
|
document.getElementById('editProjectModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProjectEdit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const id = document.getElementById('editProjectId').value;
|
||||||
|
const name = document.getElementById('editProjectName').value.trim();
|
||||||
|
const directory = document.getElementById('editProjectDir').value.trim();
|
||||||
|
const portsStr = document.getElementById('editProjectPorts').value.trim();
|
||||||
|
const healthUrl = document.getElementById('editProjectHealthUrl').value.trim();
|
||||||
|
const adminUrl = document.getElementById('editProjectAdminUrl').value.trim();
|
||||||
|
const gitRepo = document.getElementById('editProjectGitRepo').value.trim();
|
||||||
|
const version = document.getElementById('editProjectVersion').value.trim();
|
||||||
|
const logFile = document.getElementById('editProjectLogFile').value.trim();
|
||||||
|
const startCmd = document.getElementById('editProjectStartCmd').value.trim();
|
||||||
|
const stopCmd = document.getElementById('editProjectStopCmd').value.trim();
|
||||||
|
const description = document.getElementById('editProjectDesc').value.trim();
|
||||||
|
|
||||||
|
if (!name) { alert('请输入项目名称'); return; }
|
||||||
|
|
||||||
|
const ports = portsStr ? portsStr.split(',').map(p => parseInt(p.trim())).filter(p => !isNaN(p)) : [];
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
ports,
|
||||||
|
directory,
|
||||||
|
start_cmd: startCmd,
|
||||||
|
stop_cmd: stopCmd,
|
||||||
|
health_url: healthUrl,
|
||||||
|
admin_url: adminUrl,
|
||||||
|
git_repo: gitRepo,
|
||||||
|
log_file: logFile,
|
||||||
|
version,
|
||||||
|
description
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/projects/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.error) {
|
||||||
|
alert('保存失败: ' + result.error);
|
||||||
|
} else {
|
||||||
|
alert(result.message || '保存成功');
|
||||||
|
closeEditProjectModal();
|
||||||
|
loadProjects();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('保存失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Cron 管理 ====================
|
// ==================== Cron 管理 ====================
|
||||||
|
|
||||||
async function loadCronTasks() {
|
async function loadCronTasks() {
|
||||||
|
|||||||
6
utils.py
6
utils.py
@@ -50,9 +50,11 @@ def load_projects_full():
|
|||||||
|
|
||||||
|
|
||||||
def save_projects(projects):
|
def save_projects(projects):
|
||||||
"""保存项目配置"""
|
"""保存项目配置(保留 server 和 external_services)"""
|
||||||
|
full_data = load_projects_full()
|
||||||
|
full_data['projects'] = projects
|
||||||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||||||
json.dump({'projects': projects}, f, ensure_ascii=False, indent=2)
|
json.dump(full_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def check_port(port):
|
def check_port(port):
|
||||||
|
|||||||
Reference in New Issue
Block a user