diff --git a/app.py b/app.py
index a923af0..b5d80f5 100644
--- a/app.py
+++ b/app.py
@@ -20,6 +20,16 @@ from flask import Flask, render_template_string, jsonify, request, g
import urllib.request
import urllib.error
+# 系统资源监控
+try:
+ import psutil
+ import time
+ HAS_PSUTIL = True
+ # 网速计算用的上次数据
+ _last_net_stats = {'bytes_sent': 0, 'bytes_recv': 0, 'time': 0}
+except ImportError:
+ HAS_PSUTIL = False
+
app = Flask(__name__)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -694,6 +704,101 @@ def api_cron_sync():
return jsonify({'message': f'同步完成,新增 {synced_count} 个任务'})
+# ==================== 系统资源监控 API ====================
+
+@app.route('/api/system/stats')
+def api_system_stats():
+ """获取系统资源信息"""
+ if not HAS_PSUTIL:
+ return jsonify({'error': 'psutil未安装', 'available': False})
+
+ try:
+ # CPU
+ cpu_percent = psutil.cpu_percent(interval=0.5)
+ cpu_count = psutil.cpu_count(logical=False) or psutil.cpu_count()
+ cpu_count_logical = psutil.cpu_count(logical=True)
+
+ # 内存
+ mem = psutil.virtual_memory()
+ mem_total_gb = round(mem.total / (1024**3), 2)
+ mem_used_gb = round(mem.used / (1024**3), 2)
+ mem_available_gb = round(mem.available / (1024**3), 2)
+ mem_percent = mem.percent
+
+ # 磁盘
+ disk = psutil.disk_usage('/')
+ disk_total_gb = round(disk.total / (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
+
+ # 网络流量和网速计算
+ net = psutil.net_io_counters()
+ current_time = time.time()
+
+ # 计算网速 (bytes/s)
+ time_diff = current_time - _last_net_stats['time'] if _last_net_stats['time'] > 0 else 1
+ sent_speed = (net.bytes_sent - _last_net_stats['bytes_sent']) / time_diff if time_diff > 0 else 0
+ recv_speed = (net.bytes_recv - _last_net_stats['bytes_recv']) / time_diff if time_diff > 0 else 0
+
+ # 保存当前数据用于下次计算
+ _last_net_stats['bytes_sent'] = net.bytes_sent
+ _last_net_stats['bytes_recv'] = net.bytes_recv
+ _last_net_stats['time'] = current_time
+
+ # 转换为 KB/s 或 MB/s
+ sent_speed_kb = sent_speed / 1024
+ recv_speed_kb = recv_speed / 1024
+ sent_speed_display = f"{sent_speed_kb:.1f} KB/s" if sent_speed_kb < 1024 else f"{sent_speed_kb/1024:.2f} MB/s"
+ recv_speed_display = f"{recv_speed_kb:.1f} KB/s" if recv_speed_kb < 1024 else f"{recv_speed_kb/1024:.2f} MB/s"
+
+ net_sent_mb = round(net.bytes_sent / (1024**2), 2)
+ net_recv_mb = round(net.bytes_recv / (1024**2), 2)
+
+ # 系统启动时间
+ boot_time = datetime.fromtimestamp(psutil.boot_time()).strftime('%Y-%m-%d %H:%M:%S')
+ uptime_seconds = int(datetime.now().timestamp() - psutil.boot_time())
+ uptime_hours = uptime_seconds // 3600
+ uptime_minutes = (uptime_seconds % 3600) // 60
+
+ # 进程数
+ process_count = len(psutil.pids())
+
+ return jsonify({
+ 'available': True,
+ 'cpu': {
+ 'percent': cpu_percent,
+ 'count': cpu_count,
+ 'logical': cpu_count_logical
+ },
+ 'memory': {
+ 'total_gb': mem_total_gb,
+ 'used_gb': mem_used_gb,
+ 'available_gb': mem_available_gb,
+ 'percent': mem_percent
+ },
+ 'disk': {
+ 'total_gb': disk_total_gb,
+ 'used_gb': disk_used_gb,
+ 'free_gb': disk_free_gb,
+ 'percent': disk_percent
+ },
+ 'network': {
+ 'sent_mb': net_sent_mb,
+ 'recv_mb': net_recv_mb,
+ 'sent_speed': sent_speed_display,
+ 'recv_speed': recv_speed_display
+ },
+ 'system': {
+ 'boot_time': boot_time,
+ 'uptime': f'{uptime_hours}小时{uptime_minutes}分钟',
+ 'process_count': process_count
+ }
+ })
+ except Exception as e:
+ return jsonify({'error': str(e), 'available': False})
+
+
def guess_cron_name(command):
"""从命令推断任务名称"""
keywords = {
@@ -935,7 +1040,7 @@ HTML_TEMPLATE = '''
- 项目服务管理面板 v2.4
+ 项目服务管理面板 v2.6
@@ -965,6 +1070,11 @@ HTML_TEMPLATE = '''
.filter-bar-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
.filter-bar-btn.add-btn { background: #22c55e; border-color: #22c55e; color: #fff; }
.filter-bar-btn.add-btn:hover { background: #16a34a; }
+ .realtime-toggle { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; }
+ .realtime-toggle.active { background: #22c55e/20; border-color: #22c55e; }
+ .speed-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; background: #334155; font-size: 13px; }
+ .speed-badge.upload { color: #f97316; }
+ .speed-badge.download { color: #3b82f6; }
.tab-btn { border-bottom: 2px solid transparent; }
.tab-btn.active { border-bottom-color: #3b82f6; }
.cron-card { transition: all 0.2s; }
@@ -1016,6 +1126,9 @@ HTML_TEMPLATE = '''
+
@@ -1079,6 +1192,121 @@ HTML_TEMPLATE = '''
+
+
+
+
+
+
+
+
+
+
CPU 使用率
+ -
+
+
+
物理核心: - | 逻辑核心: -
+
+
+
+
内存使用
+ -
+
+
+
已用: - GB / 总量: - GB | 可用: - GB
+
+
+
+
+
+
+
+
磁盘使用
+ -
+
+
+
已用: - GB / 总量: - GB | 剩余: - GB
+
+
+
+
网络流量
+
+
+
+ ↑ -
+ ↓ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1341,11 +1569,100 @@ HTML_TEMPLATE = '''
btn.classList.toggle('active', 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');
+ // 切换Tab时关闭实时监控
+ if (tab !== 'system' && realtimeTimer) {
+ clearInterval(realtimeTimer);
+ realtimeTimer = null;
+ document.getElementById('realtimeCheck').checked = false;
+ document.getElementById('realtimeToggle').classList.remove('active');
+ document.getElementById('realtimeIndicator').textContent = '';
+ }
+
if (tab === 'cron') {
loadCronTasks();
}
+ if (tab === 'system') {
+ loadSystemStats();
+ }
+ }
+
+ // 系统资源加载
+ let realtimeTimer = null;
+ let realtimeInterval = 2; // 秒
+
+ async function loadSystemStats() {
+ try {
+ const res = await fetch('/api/system/stats');
+ const data = await res.json();
+
+ if (!data.available) {
+ document.getElementById('sysUptime').textContent = 'psutil未安装';
+ document.getElementById('sysBootTime').textContent = '-';
+ return;
+ }
+
+ // 系统概览
+ document.getElementById('sysUptime').textContent = data.system.uptime;
+ document.getElementById('sysBootTime').textContent = data.system.boot_time;
+ document.getElementById('sysProcessCount').textContent = data.system.process_count;
+ document.getElementById('sysCpuCount').textContent = data.cpu.logical + '核';
+
+ // CPU
+ document.getElementById('cpuPercent').textContent = data.cpu.percent.toFixed(1) + '%';
+ document.getElementById('cpuBar').style.width = data.cpu.percent + '%';
+ document.getElementById('cpuPhysical').textContent = data.cpu.count;
+ document.getElementById('cpuLogical').textContent = data.cpu.logical;
+
+ // 内存
+ document.getElementById('memPercent').textContent = data.memory.percent.toFixed(1) + '%';
+ document.getElementById('memBar').style.width = data.memory.percent + '%';
+ document.getElementById('memUsed').textContent = data.memory.used_gb;
+ document.getElementById('memTotal').textContent = data.memory.total_gb;
+ document.getElementById('memAvailable').textContent = data.memory.available_gb;
+
+ // 磁盘
+ document.getElementById('diskPercent').textContent = data.disk.percent.toFixed(1) + '%';
+ document.getElementById('diskBar').style.width = data.disk.percent + '%';
+ document.getElementById('diskUsed').textContent = data.disk.used_gb;
+ document.getElementById('diskTotal').textContent = data.disk.total_gb;
+ document.getElementById('diskFree').textContent = data.disk.free_gb;
+
+ // 网络 - 累计流量
+ document.getElementById('netSent').textContent = data.network.sent_mb;
+ document.getElementById('netRecv').textContent = data.network.recv_mb;
+ // 实时网速
+ document.getElementById('netSentSpeed').textContent = data.network.sent_speed || '-';
+ document.getElementById('netRecvSpeed').textContent = data.network.recv_speed || '-';
+
+ } catch (e) {
+ console.error('系统资源加载失败:', e);
+ }
+ }
+
+ function toggleRealtime() {
+ const checked = document.getElementById('realtimeCheck').checked;
+ const toggle = document.getElementById('realtimeToggle');
+ const indicator = document.getElementById('realtimeIndicator');
+
+ if (checked) {
+ toggle.classList.add('active');
+ indicator.textContent = '每' + realtimeInterval + '秒';
+ // 立即加载一次
+ loadSystemStats();
+ // 启动定时器
+ realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
+ } else {
+ toggle.classList.remove('active');
+ indicator.textContent = '';
+ // 停止定时器
+ if (realtimeTimer) {
+ clearInterval(realtimeTimer);
+ realtimeTimer = null;
+ }
+ }
}
// IP 保存
diff --git a/logs/app.log b/logs/app.log
index eee7ca4..59498aa 100644
--- a/logs/app.log
+++ b/logs/app.log
@@ -1,8 +1,8 @@
-[2026-04-23 16:56:14] ==================================================
-[2026-04-23 16:56:14] 项目服务管理面板 v2.0.0 启动
-[2026-04-23 16:56:14] 访问地址: http://localhost:19013
-[2026-04-23 16:56:14] 进程PID: 1118301
-[2026-04-23 16:56:14] ==================================================
+[2026-04-23 17:27:44] ==================================================
+[2026-04-23 17:27:44] 项目服务管理面板 v2.0.0 启动
+[2026-04-23 17:27:44] 访问地址: http://localhost:19013
+[2026-04-23 17:27:44] 进程PID: 1130035
+[2026-04-23 17:27:44] ==================================================
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
@@ -10,27 +10,33 @@ WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:19013
* Running on http://192.168.2.17:19013
Press CTRL+C to quit
-127.0.0.1 - - [23/Apr/2026 16:56:17] "GET / HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:17] "GET / HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:18] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:19] "GET /api/projects HTTP/1.1" 200 -
-192.168.2.8 - - [23/Apr/2026 16:56:20] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:21] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:22] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:27] "GET / HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:28] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:29] "GET /api/projects HTTP/1.1" 200 -
-192.168.2.8 - - [23/Apr/2026 16:56:30] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:36] "GET / HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:37] "GET / HTTP/1.1" 200 -
-192.168.2.8 - - [23/Apr/2026 16:56:38] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:38] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:39] "GET /api/projects HTTP/1.1" 200 -
-192.168.2.8 - - [23/Apr/2026 16:56:40] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:47] "GET / HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:48] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:49] "GET /api/projects HTTP/1.1" 200 -
-192.168.2.8 - - [23/Apr/2026 16:56:50] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:50] "GET / HTTP/1.1" 200 -
-192.168.2.14 - - [23/Apr/2026 16:56:52] "GET /api/projects HTTP/1.1" 200 -
-127.0.0.1 - - [23/Apr/2026 16:56:57] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:27:49] "GET /api/system/stats HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:27:51] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:27:52] "GET / HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:27:53] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:27:54] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:02] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:02] "GET / HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:28:03] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:04] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:10] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:12] "GET / HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:12] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:12] "GET / HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:28:13] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:14] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:21] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:22] "GET / HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:28:23] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:24] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:31] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:32] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:33] "GET /api/system/stats HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:28:33] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:34] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:40] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:41] "GET / HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:42] "GET /api/projects HTTP/1.1" 200 -
+127.0.0.1 - - [23/Apr/2026 17:28:43] "GET / HTTP/1.1" 200 -
+192.168.2.14 - - [23/Apr/2026 17:28:43] "GET /api/projects HTTP/1.1" 200 -
+192.168.2.8 - - [23/Apr/2026 17:28:44] "GET /api/projects HTTP/1.1" 200 -