🎯 重构目标: - 将 3700+ 行单文件拆分为多个模块 - 清晰的职责分离,便于维护和扩展 📁 模块结构: - config.py: 配置常量和路径 - database.py: 数据库操作 - utils.py: 工具函数(日志、进程、cron等) - routes_cron.py: Cron管理API - routes_system.py: 系统监控API - routes_config.py: 配置管理API - routes_email.py: 邮件通知API - routes_project.py: 项目管理API - templates.py: HTML模板 - app.py: 主入口文件 ✨ 优势: - 易于维护:每个模块职责明确 - 易于扩展:新增功能只需添加对应模块 - 易于测试:模块独立,便于单元测试 - 易于理解:新人能快速上手 📝 更新 README.md,详细说明项目结构和API接口
590 lines
27 KiB
Python
590 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HTML 模板
|
|
"""
|
|
|
|
from config import APP_NAME, APP_VERSION
|
|
|
|
|
|
def get_html_template():
|
|
"""返回完整的HTML模板"""
|
|
return '''<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>''' + APP_NAME + '''</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<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>
|
|
<body class="min-h-screen text-gray-100">
|
|
<div class="container mx-auto px-4 py-8">
|
|
<!-- 头部 -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<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>
|
|
|
|
<!-- 添加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>
|
|
</html>
|
|
''' |