fix: 使用完整模板,保留所有网页功能

 包含所有原有功能:
- 阈值设置(CPU/内存/磁盘/间隔)
- 邮件规则管理
- 自定义链接添加
- 进程列表显示
- Docker容器监控
- Cron任务管理
- 系统资源实时监控
- 配置导入导出

📝 templates_full.txt: 从v3.3.5提取的完整HTML模板(2366行)
📝 templates.py: 动态加载完整模板并替换版本号
This commit is contained in:
2026-06-01 15:54:45 +08:00
parent 7904117b1d
commit 50341749b6
2 changed files with 2391 additions and 570 deletions

View File

@@ -1,590 +1,45 @@
#!/usr/bin/env python3
"""
HTML 模板
HTML 模板 - 包含所有完整功能
"""
from config import APP_NAME, APP_VERSION
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)
return template_content
except:
# 如果文件不存在,返回基本模板
return get_basic_template()
def get_basic_template():
"""基本模板(备用)"""
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>
<h1 class="text-3xl font-bold">''' + APP_NAME + ''' ''' + APP_VERSION + '''</h1>
<p>模板加载失败,请检查 templates_full.txt 文件</p>
</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>
'''
</html>'''

2366
templates_full.txt Normal file

File diff suppressed because it is too large Load Diff