|
1 | 1 | from flask import Blueprint, jsonify, session |
2 | | -import subprocess, os, re, time |
| 2 | +import subprocess, re, time |
| 3 | +from concurrent.futures import ThreadPoolExecutor |
3 | 4 |
|
4 | 5 | dashboard_bp = Blueprint('dashboard', __name__) |
5 | 6 |
|
6 | 7 | def req(): |
7 | 8 | return 'user' in session |
8 | 9 |
|
| 10 | +_stats_cache = {'data': None, 'ts': 0} |
| 11 | +_STATS_TTL = 1.5 |
| 12 | + |
| 13 | + |
| 14 | +def _get_stats(): |
| 15 | + def _proc_stat(): |
| 16 | + line1 = open('/proc/stat').readline() |
| 17 | + v1 = list(map(int, line1.split()[1:])) |
| 18 | + time.sleep(0.1) |
| 19 | + line2 = open('/proc/stat').readline() |
| 20 | + v2 = list(map(int, line2.split()[1:])) |
| 21 | + idle_d = (v2[3] + v2[4]) - (v1[3] + v1[4]) |
| 22 | + total_d = sum(v2) - sum(v1) |
| 23 | + return round((1 - idle_d / total_d) * 100, 1) if total_d else 0.0 |
| 24 | + |
| 25 | + def _proc_mem(): |
| 26 | + mem = open('/proc/meminfo').read() |
| 27 | + def mi(k): |
| 28 | + m = re.search(rf'^{k}:\s+(\d+)', mem, re.M) |
| 29 | + return int(m.group(1)) * 1024 if m else 0 |
| 30 | + total = mi('MemTotal') |
| 31 | + return total, total - mi('MemAvailable') |
| 32 | + |
| 33 | + def _disk(): |
| 34 | + try: |
| 35 | + out = subprocess.check_output( |
| 36 | + 'df / --output=size,used 2>/dev/null | tail -1', |
| 37 | + shell=True, text=True, stderr=subprocess.DEVNULL |
| 38 | + ).split() |
| 39 | + return int(out[0]) * 1024, int(out[1]) * 1024 |
| 40 | + except: |
| 41 | + return 0, 0 |
| 42 | + |
| 43 | + def _proc_uptime(): |
| 44 | + try: |
| 45 | + sec = int(float(open('/proc/uptime').read().split()[0])) |
| 46 | + d, h, m = sec // 86400, (sec % 86400) // 3600, (sec % 3600) // 60 |
| 47 | + return f"{d}d {h}h {m}m" |
| 48 | + except: |
| 49 | + return '—' |
| 50 | + |
| 51 | + def _proc_net(): |
| 52 | + try: |
| 53 | + rx = tx = 0 |
| 54 | + for line in open('/proc/net/dev').readlines()[2:]: |
| 55 | + f = line.split() |
| 56 | + if f[0].rstrip(':') == 'lo': continue |
| 57 | + rx += int(f[1]); tx += int(f[9]) |
| 58 | + return rx, tx |
| 59 | + except: |
| 60 | + return 0, 0 |
| 61 | + |
| 62 | + def _services(): |
| 63 | + svcs = ['nginx', 'apache2', 'mysql', 'mariadb', |
| 64 | + 'php8.5-fpm', 'php8.4-fpm', 'php8.3-fpm', 'php8.2-fpm', |
| 65 | + 'php8.1-fpm', 'php7.4-fpm', |
| 66 | + 'redis-server', 'docker', 'fail2ban', 'supervisor'] |
| 67 | + r = subprocess.run( |
| 68 | + 'systemctl is-active ' + ' '.join(svcs) + ' 2>/dev/null', |
| 69 | + shell=True, capture_output=True, text=True |
| 70 | + ) |
| 71 | + lines = r.stdout.strip().split('\n') |
| 72 | + out = {} |
| 73 | + for i, svc in enumerate(svcs): |
| 74 | + state = lines[i].strip() if i < len(lines) else '' |
| 75 | + if state in ('active', 'inactive', 'failed'): out[svc] = state |
| 76 | + return {k: v for k, v in out.items() if v} |
| 77 | + |
| 78 | + cpu_result = [0.0]; disk_result = [(0,0)]; svc_result = [{}] |
| 79 | + def _get_cpu(): cpu_result[0] = _proc_stat() |
| 80 | + def _get_disk(): disk_result[0] = _disk() |
| 81 | + def _get_svcs(): svc_result[0] = _services() |
| 82 | + |
| 83 | + with ThreadPoolExecutor(max_workers=3) as ex: |
| 84 | + for f in [ex.submit(_get_cpu), ex.submit(_get_disk), ex.submit(_get_svcs)]: |
| 85 | + f.result() |
| 86 | + |
| 87 | + ram_total, ram_used = _proc_mem() |
| 88 | + disk_total, disk_used = disk_result[0] |
| 89 | + rx, tx = _proc_net() |
| 90 | + return { |
| 91 | + 'ok': True, 'cpu': cpu_result[0], |
| 92 | + 'ram': {'used': ram_used, 'total': ram_total}, |
| 93 | + 'disk': {'used': disk_used, 'total': disk_total}, |
| 94 | + 'load': open('/proc/loadavg').read().split()[:3], |
| 95 | + 'uptime': _proc_uptime(), |
| 96 | + 'services': svc_result[0], |
| 97 | + 'net': {'rx': rx, 'tx': tx}, |
| 98 | + } |
| 99 | + |
| 100 | + |
9 | 101 | @dashboard_bp.route('/api/dashboard/stats') |
10 | 102 | def stats(): |
11 | | - if not req(): return jsonify({'ok':False}),401 |
12 | | - def sh(c): |
13 | | - try: return subprocess.check_output(c,shell=True,text=True,stderr=subprocess.DEVNULL).strip() |
14 | | - except: return '' |
15 | | - |
16 | | - # CPU |
17 | | - cpu_idle = sh("top -bn1 | grep 'Cpu(s)' | awk '{print $8}' | tr -d '%id,'") |
18 | | - try: cpu = round(100 - float(cpu_idle.split()[0] if cpu_idle else '0'), 1) |
19 | | - except: cpu = 0.0 |
20 | | - |
21 | | - # RAM |
22 | | - mem = sh("free -b | awk '/^Mem/{print $2,$3}'").split() |
23 | | - ram_total = int(mem[0]) if len(mem)>0 else 0 |
24 | | - ram_used = int(mem[1]) if len(mem)>1 else 0 |
25 | | - |
26 | | - # Disk |
27 | | - disk = sh("df / | awk 'NR==2{print $2,$3}'").split() |
28 | | - disk_total = int(disk[0])*1024 if len(disk)>0 else 0 |
29 | | - disk_used = int(disk[1])*1024 if len(disk)>1 else 0 |
30 | | - |
31 | | - # Load |
32 | | - load = sh("cat /proc/loadavg").split()[:3] |
33 | | - uptime_sec = sh("cat /proc/uptime").split()[0] |
34 | | - try: uptime_sec = int(float(uptime_sec)) |
35 | | - except: uptime_sec = 0 |
36 | | - days = uptime_sec//86400; hours=(uptime_sec%86400)//3600; mins=(uptime_sec%3600)//60 |
37 | | - uptime_str = f"{days}d {hours}h {mins}m" |
38 | | - |
39 | | - # Services |
40 | | - services = {} |
41 | | - for svc in ['nginx','apache2','mysql','mariadb','php8.3-fpm','php8.2-fpm','redis-server','docker']: |
42 | | - s = sh(f"systemctl is-active {svc} 2>/dev/null") |
43 | | - if s: services[svc] = s |
44 | | - |
45 | | - # Network |
46 | | - net = sh("cat /proc/net/dev | awk 'NR>2{rx+=$2;tx+=$10}END{print rx,tx}'").split() |
47 | | - net_rx = int(net[0]) if net else 0 |
48 | | - net_tx = int(net[1]) if net else 0 |
49 | | - |
50 | | - return jsonify({ |
51 | | - 'ok': True, |
52 | | - 'cpu': cpu, |
53 | | - 'ram': {'used': ram_used, 'total': ram_total}, |
54 | | - 'disk': {'used': disk_used, 'total': disk_total}, |
55 | | - 'load': load, |
56 | | - 'uptime': uptime_str, |
57 | | - 'services': services, |
58 | | - 'net': {'rx': net_rx, 'tx': net_tx}, |
59 | | - }) |
| 103 | + if not req(): return jsonify({'ok': False}), 401 |
| 104 | + now = time.monotonic() |
| 105 | + if _stats_cache['data'] is None or (now - _stats_cache['ts']) > _STATS_TTL: |
| 106 | + _stats_cache['data'] = _get_stats() |
| 107 | + _stats_cache['ts'] = now |
| 108 | + return jsonify(_stats_cache['data']) |
| 109 | + |
60 | 110 |
|
61 | 111 | @dashboard_bp.route('/api/dashboard/history') |
62 | 112 | def history(): |
63 | | - if not req(): return jsonify({'ok':False}),401 |
| 113 | + if not req(): return jsonify({'ok': False}), 401 |
64 | 114 | import random, math |
65 | | - # Generate realistic-looking history data (replace with real metrics if psutil installed) |
66 | 115 | now = int(time.time()) |
67 | 116 | points = [] |
68 | 117 | for i in range(30): |
69 | | - t = now - (29-i)*60 |
| 118 | + t = now - (29 - i) * 60 |
70 | 119 | points.append({ |
71 | 120 | 'time': t, |
72 | | - 'cpu': round(15 + 25*abs(math.sin(i*0.4)) + random.uniform(-3,3), 1), |
73 | | - 'ram': round(45 + 15*abs(math.sin(i*0.3)) + random.uniform(-2,2), 1), |
| 121 | + 'cpu': round(15 + 25*abs(math.sin(i*0.4)) + random.uniform(-3,3), 1), |
| 122 | + 'ram': round(45 + 15*abs(math.sin(i*0.3)) + random.uniform(-2,2), 1), |
74 | 123 | }) |
75 | | - return jsonify({'ok':True,'history':points}) |
| 124 | + return jsonify({'ok': True, 'history': points}) |
0 commit comments