From 930c8d3a9cfc3d70da6a2c07be53ddea46bdccbb Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 17:30:41 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E9=9D=A2=E6=9D=BF(/server)=20-=20CPU/=E5=86=85?= =?UTF-8?q?=E5=AD=98/=E7=A1=AC=E7=9B=98/PM2=E8=BF=9B=E7=A8=8B/PG=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93/=E5=9B=9E=E8=A1=A5=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 122 ++++++++++++++ backend/requirements.txt | 1 + frontend/app/server/page.tsx | 286 ++++++++++++++++++++++++++++++++ frontend/components/Sidebar.tsx | 3 +- 4 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 frontend/app/server/page.tsx diff --git a/backend/main.py b/backend/main.py index 2100713..35291f5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -452,3 +452,125 @@ async def get_signal_trades( status, limit ) return {"count": len(rows), "data": rows} + + +# ─── 服务器状态监控 ─────────────────────────────────────────────── + +import shutil, subprocess, psutil + +@app.get("/api/server/status") +async def get_server_status(user: dict = Depends(get_current_user)): + """服务器全状态:CPU/内存/硬盘/负载/PM2进程/PG数据库/回补进度""" + # CPU + cpu_percent = psutil.cpu_percent(interval=0.5) + cpu_count = psutil.cpu_count() + + # 内存 + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + + # 硬盘 + disk = shutil.disk_usage("/") + + # 负载 + load1, load5, load15 = os.getloadavg() + + # Uptime + boot_time = psutil.boot_time() + uptime_s = time.time() - boot_time + + # 网络IO + net = psutil.net_io_counters() + + # PM2进程状态 + pm2_procs = [] + try: + result = subprocess.run( + ["npx", "pm2", "jlist"], + capture_output=True, text=True, timeout=10 + ) + import json as _json + procs = _json.loads(result.stdout) + for p in procs: + pm2_procs.append({ + "name": p.get("name", ""), + "status": p.get("pm2_env", {}).get("status", "unknown"), + "cpu": p.get("monit", {}).get("cpu", 0), + "memory_mb": round(p.get("monit", {}).get("memory", 0) / 1024 / 1024, 1), + "restarts": p.get("pm2_env", {}).get("restart_time", 0), + "uptime_ms": p.get("pm2_env", {}).get("pm_uptime", 0), + "pid": p.get("pid", 0), + }) + except Exception: + pm2_procs = [] + + # PG数据库大小 + agg_trades条数 + pg_info = {} + try: + row = await async_fetchrow( + "SELECT pg_database_size(current_database()) as db_size" + ) + pg_info["db_size_mb"] = round(row["db_size"] / 1024 / 1024, 1) if row else 0 + + row2 = await async_fetchrow("SELECT COUNT(*) as cnt FROM agg_trades") + pg_info["agg_trades_count"] = row2["cnt"] if row2 else 0 + + row3 = await async_fetchrow("SELECT COUNT(*) as cnt FROM rate_snapshots") + pg_info["rate_snapshots_count"] = row3["cnt"] if row3 else 0 + + # 各symbol最新数据时间 + meta_rows = await async_fetch("SELECT symbol, last_time_ms, earliest_time_ms FROM agg_trades_meta") + pg_info["symbols"] = {} + for m in meta_rows: + sym = m["symbol"].replace("USDT", "") + pg_info["symbols"][sym] = { + "latest_ms": m["last_time_ms"], + "earliest_ms": m["earliest_time_ms"], + "span_hours": round((m["last_time_ms"] - m["earliest_time_ms"]) / 3600000, 1), + } + except Exception: + pass + + # 回补进程 + backfill_running = False + try: + for proc in psutil.process_iter(["pid", "cmdline"]): + cmdline = " ".join(proc.info.get("cmdline") or []) + if "backfill_agg_trades" in cmdline: + backfill_running = True + break + except Exception: + pass + + return { + "timestamp": int(time.time() * 1000), + "cpu": { + "percent": cpu_percent, + "cores": cpu_count, + }, + "memory": { + "total_gb": round(mem.total / 1024**3, 1), + "used_gb": round(mem.used / 1024**3, 1), + "percent": mem.percent, + "swap_percent": swap.percent, + }, + "disk": { + "total_gb": round(disk.total / 1024**3, 1), + "used_gb": round(disk.used / 1024**3, 1), + "free_gb": round(disk.free / 1024**3, 1), + "percent": round(disk.used / disk.total * 100, 1), + }, + "load": { + "load1": round(load1, 2), + "load5": round(load5, 2), + "load15": round(load15, 2), + }, + "uptime_hours": round(uptime_s / 3600, 1), + "network": { + "bytes_sent_gb": round(net.bytes_sent / 1024**3, 2), + "bytes_recv_gb": round(net.bytes_recv / 1024**3, 2), + }, + "pm2": pm2_procs, + "postgres": pg_info, + "backfill_running": backfill_running, + } diff --git a/backend/requirements.txt b/backend/requirements.txt index 3263361..2acb9a0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,3 +2,4 @@ fastapi uvicorn httpx python-dotenv +psutil diff --git a/frontend/app/server/page.tsx b/frontend/app/server/page.tsx new file mode 100644 index 0000000..5f140c8 --- /dev/null +++ b/frontend/app/server/page.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { authFetch } from "@/lib/auth"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; + +interface PM2Proc { + name: string; + status: string; + cpu: number; + memory_mb: number; + restarts: number; + uptime_ms: number; + pid: number; +} + +interface ServerStatus { + timestamp: number; + cpu: { percent: number; cores: number }; + memory: { total_gb: number; used_gb: number; percent: number; swap_percent: number }; + disk: { total_gb: number; used_gb: number; free_gb: number; percent: number }; + load: { load1: number; load5: number; load15: number }; + uptime_hours: number; + network: { bytes_sent_gb: number; bytes_recv_gb: number }; + pm2: PM2Proc[]; + postgres: { + db_size_mb: number; + agg_trades_count: number; + rate_snapshots_count: number; + symbols: Record; + }; + backfill_running: boolean; +} + +function bjtStr(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,"0")}-${String(d.getUTCDate()).padStart(2,"0")} ${String(d.getUTCHours()).padStart(2,"0")}:${String(d.getUTCMinutes()).padStart(2,"0")}:${String(d.getUTCSeconds()).padStart(2,"0")}`; +} + +function uptimeStr(ms: number) { + if (!ms) return "-"; + const now = Date.now(); + const diff = now - ms; + const h = Math.floor(diff / 3600000); + const m = Math.floor((diff % 3600000) / 60000); + if (h > 24) return `${Math.floor(h/24)}d ${h%24}h`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} + +function ProgressBar({ percent, color = "cyan" }: { percent: number; color?: string }) { + const colorClass = percent > 90 ? "bg-red-500" : percent > 70 ? "bg-amber-500" : color === "cyan" ? "bg-cyan-500" : "bg-emerald-500"; + return ( +
+
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + const isOnline = status === "online"; + return ( + + + {status} + + ); +} + +function numberFmt(n: number) { + return n.toLocaleString("en-US"); +} + +export default function ServerPage() { + const { token } = useAuth(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [lastUpdate, setLastUpdate] = useState(0); + + const fetchData = useCallback(async () => { + try { + const res = await authFetch("/api/server/status"); + if (!res.ok) return; + const json = await res.json(); + setData(json); + setLastUpdate(Date.now()); + } catch (e) { + // ignore + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (!token) return; + fetchData(); + const iv = setInterval(fetchData, 10000); // 10秒刷新 + return () => clearInterval(iv); + }, [token, fetchData]); + + if (!token) { + return ( +
+
+

请先登录

+ 登录 +
+
+ ); + } + + return ( +
+ {/* 顶栏 */} +
+
+
+ ← 返回 +

🖥️ 服务器监控

+ GCP asia-northeast1-b +
+
+ {data?.backfill_running && ( + + + 回补运行中 + + )} + 每10秒刷新 + {lastUpdate > 0 && · 更新于 {bjtStr(lastUpdate)}} +
+
+
+ +
+ {loading ? ( +
加载中...
+ ) : !data ? ( +
获取失败
+ ) : ( + <> + {/* 系统概览 4卡片 */} +
+ {/* CPU */} +
+
+ CPU + {data.cpu.cores} 核 +
+
{data.cpu.percent}%
+ +
+ 负载: {data.load.load1} / {data.load.load5} / {data.load.load15} +
+
+ + {/* 内存 */} +
+
+ 内存 + {data.memory.used_gb}G / {data.memory.total_gb}G +
+
{data.memory.percent}%
+ + {data.memory.swap_percent > 0 && ( +
Swap: {data.memory.swap_percent}%
+ )} +
+ + {/* 硬盘 */} +
+
+ 硬盘 + {data.disk.free_gb}G 可用 +
+
{data.disk.percent}%
+ +
+ {data.disk.used_gb}G / {data.disk.total_gb}G +
+
+ + {/* 运行时间 & 网络 */} +
+
+ 系统 + Uptime +
+
+ {data.uptime_hours > 24 ? `${Math.floor(data.uptime_hours/24)}天` : `${data.uptime_hours}h`} +
+
+
↑ 发送: {data.network.bytes_sent_gb} GB
+
↓ 接收: {data.network.bytes_recv_gb} GB
+
+
+
+ + {/* PM2 进程列表 */} +
+
+

📦 PM2 进程

+
+
+ + + + + + + + + + + + + + {data.pm2.map((p, i) => ( + + + + + + + + + + ))} + +
名称状态CPU内存重启运行时间PID
{p.name}{p.cpu}%{p.memory_mb} MB{p.restarts}{uptimeStr(p.uptime_ms)}{p.pid || "-"}
+
+
+ + {/* 数据库信息 */} +
+
+

🗄️ PostgreSQL

+
+
+
+
数据库大小
+
+ {data.postgres.db_size_mb > 1024 + ? `${(data.postgres.db_size_mb / 1024).toFixed(1)} GB` + : `${data.postgres.db_size_mb} MB`} +
+
+
+
aggTrades 总条数
+
{numberFmt(data.postgres.agg_trades_count)}
+
+
+
费率快照
+
{numberFmt(data.postgres.rate_snapshots_count)}
+
+
+
回补状态
+
+ {data.backfill_running ? "🔄 运行中" : "⏸ 停止"} +
+
+
+ {data.postgres.symbols && Object.keys(data.postgres.symbols).length > 0 && ( +
+
数据覆盖范围
+
+ {Object.entries(data.postgres.symbols).map(([sym, info]) => ( +
+ {sym} +
+
{bjtStr(info.earliest_ms)} → {bjtStr(info.latest_ms)}
+
{info.span_hours > 24 ? `${(info.span_hours/24).toFixed(1)} 天` : `${info.span_hours.toFixed(1)} 小时`}
+
+
+ ))} +
+
+ )} +
+ + )} +
+
+ ); +} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 69beb7e..9aa0005 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -7,13 +7,14 @@ import { useAuth } from "@/lib/auth"; import { LayoutDashboard, Info, Menu, X, Zap, LogIn, UserPlus, - ChevronLeft, ChevronRight, Activity, LogOut, Crosshair + ChevronLeft, ChevronRight, Activity, LogOut, Crosshair, Monitor } from "lucide-react"; const navItems = [ { href: "/", label: "仪表盘", icon: LayoutDashboard }, { href: "/trades", label: "成交流", icon: Activity }, { href: "/signals", label: "信号引擎", icon: Crosshair }, + { href: "/server", label: "服务器", icon: Monitor }, { href: "/about", label: "说明", icon: Info }, ];