arbitrage-engine/frontend/app/server/page.tsx

287 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<string, { latest_ms: number; earliest_ms: number; span_hours: number }>;
};
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 (
<div className="w-full bg-slate-700 rounded-full h-2.5 overflow-hidden">
<div className={`h-full rounded-full transition-all duration-500 ${colorClass}`} style={{ width: `${Math.min(percent, 100)}%` }} />
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const isOnline = status === "online";
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${isOnline ? "bg-emerald-500/20 text-emerald-400" : "bg-red-500/20 text-red-400"}`}>
<span className={`w-1.5 h-1.5 rounded-full ${isOnline ? "bg-emerald-400" : "bg-red-400"}`} />
{status}
</span>
);
}
function numberFmt(n: number) {
return n.toLocaleString("en-US");
}
export default function ServerPage() {
const { isLoggedIn, accessToken } = useAuth();
const [data, setData] = useState<ServerStatus | null>(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 (!isLoggedIn) return;
fetchData();
const iv = setInterval(fetchData, 10000); // 10秒刷新
return () => clearInterval(iv);
}, [isLoggedIn, fetchData]);
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
<div className="text-center">
<p className="text-slate-400 mb-4"></p>
<Link href="/login" className="text-cyan-400 hover:text-cyan-300 underline"></Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-950 text-slate-200">
{/* 顶栏 */}
<header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 h-14 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="text-slate-400 hover:text-slate-200 text-sm"> </Link>
<h1 className="text-base font-semibold text-white">🖥 </h1>
<span className="text-xs text-slate-500">GCP asia-northeast1-b</span>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
{data?.backfill_running && (
<span className="flex items-center gap-1 text-cyan-400">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
</span>
)}
<span>10</span>
{lastUpdate > 0 && <span>· {bjtStr(lastUpdate)}</span>}
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6 space-y-6">
{loading ? (
<div className="flex items-center justify-center h-64 text-slate-400">...</div>
) : !data ? (
<div className="flex items-center justify-center h-64 text-red-400"></div>
) : (
<>
{/* 系统概览 4卡片 */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{/* CPU */}
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">CPU</span>
<span className="text-xs text-slate-500">{data.cpu.cores} </span>
</div>
<div className="text-2xl font-bold text-white mb-2">{data.cpu.percent}%</div>
<ProgressBar percent={data.cpu.percent} />
<div className="mt-2 text-xs text-slate-500">
: {data.load.load1} / {data.load.load5} / {data.load.load15}
</div>
</div>
{/* 内存 */}
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400"></span>
<span className="text-xs text-slate-500">{data.memory.used_gb}G / {data.memory.total_gb}G</span>
</div>
<div className="text-2xl font-bold text-white mb-2">{data.memory.percent}%</div>
<ProgressBar percent={data.memory.percent} />
{data.memory.swap_percent > 0 && (
<div className="mt-2 text-xs text-amber-400">Swap: {data.memory.swap_percent}%</div>
)}
</div>
{/* 硬盘 */}
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400"></span>
<span className="text-xs text-slate-500">{data.disk.free_gb}G </span>
</div>
<div className="text-2xl font-bold text-white mb-2">{data.disk.percent}%</div>
<ProgressBar percent={data.disk.percent} color="emerald" />
<div className="mt-2 text-xs text-slate-500">
{data.disk.used_gb}G / {data.disk.total_gb}G
</div>
</div>
{/* 运行时间 & 网络 */}
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400"></span>
<span className="text-xs text-slate-500">Uptime</span>
</div>
<div className="text-2xl font-bold text-white mb-2">
{data.uptime_hours > 24 ? `${Math.floor(data.uptime_hours/24)}` : `${data.uptime_hours}h`}
</div>
<div className="text-xs text-slate-500 space-y-0.5">
<div> : {data.network.bytes_sent_gb} GB</div>
<div> : {data.network.bytes_recv_gb} GB</div>
</div>
</div>
</div>
{/* PM2 进程列表 */}
<div className="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
<div className="px-4 py-3 border-b border-slate-800">
<h3 className="font-semibold text-sm text-white">📦 PM2 </h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-400 border-b border-slate-800">
<th className="text-left px-4 py-2 font-medium"></th>
<th className="text-center px-4 py-2 font-medium"></th>
<th className="text-right px-4 py-2 font-medium">CPU</th>
<th className="text-right px-4 py-2 font-medium"></th>
<th className="text-right px-4 py-2 font-medium"></th>
<th className="text-right px-4 py-2 font-medium"></th>
<th className="text-right px-4 py-2 font-medium">PID</th>
</tr>
</thead>
<tbody>
{data.pm2.map((p, i) => (
<tr key={i} className="border-b border-slate-800/50 hover:bg-slate-800/30">
<td className="px-4 py-2.5 font-mono text-cyan-400">{p.name}</td>
<td className="px-4 py-2.5 text-center"><StatusBadge status={p.status} /></td>
<td className="px-4 py-2.5 text-right font-mono">{p.cpu}%</td>
<td className="px-4 py-2.5 text-right font-mono">{p.memory_mb} MB</td>
<td className="px-4 py-2.5 text-right font-mono">{p.restarts}</td>
<td className="px-4 py-2.5 text-right text-slate-400">{uptimeStr(p.uptime_ms)}</td>
<td className="px-4 py-2.5 text-right text-slate-500 font-mono">{p.pid || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 数据库信息 */}
<div className="rounded-xl border border-slate-800 bg-slate-900 overflow-hidden">
<div className="px-4 py-3 border-b border-slate-800">
<h3 className="font-semibold text-sm text-white">🗄 PostgreSQL</h3>
</div>
<div className="p-4 grid grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<div className="text-xs text-slate-400 mb-1"></div>
<div className="text-lg font-bold text-white">
{data.postgres.db_size_mb > 1024
? `${(data.postgres.db_size_mb / 1024).toFixed(1)} GB`
: `${data.postgres.db_size_mb} MB`}
</div>
</div>
<div>
<div className="text-xs text-slate-400 mb-1">aggTrades </div>
<div className="text-lg font-bold text-cyan-400">{numberFmt(data.postgres.agg_trades_count)}</div>
</div>
<div>
<div className="text-xs text-slate-400 mb-1"></div>
<div className="text-lg font-bold text-white">{numberFmt(data.postgres.rate_snapshots_count)}</div>
</div>
<div>
<div className="text-xs text-slate-400 mb-1"></div>
<div className={`text-lg font-bold ${data.backfill_running ? "text-cyan-400" : "text-slate-500"}`}>
{data.backfill_running ? "🔄 运行中" : "⏸ 停止"}
</div>
</div>
</div>
{data.postgres.symbols && Object.keys(data.postgres.symbols).length > 0 && (
<div className="px-4 pb-4">
<div className="text-xs text-slate-400 mb-2"></div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{Object.entries(data.postgres.symbols).map(([sym, info]) => (
<div key={sym} className="flex items-center justify-between bg-slate-800/50 rounded-lg px-3 py-2">
<span className="font-mono text-sm text-white">{sym}</span>
<div className="text-xs text-slate-400 text-right">
<div>{bjtStr(info.earliest_ms)} {bjtStr(info.latest_ms)}</div>
<div className="text-cyan-400">{info.span_hours > 24 ? `${(info.span_hours/24).toFixed(1)}` : `${info.span_hours.toFixed(1)} 小时`}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</>
)}
</main>
</div>
);
}