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

280 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. 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 `${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")}`;
}
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 }: { percent: number }) {
const color = percent > 90 ? "bg-red-500" : percent > 70 ? "bg-amber-500" : "bg-blue-500";
return (
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
<div className={`h-full rounded-full transition-all duration-500 ${color}`} style={{ width: `${Math.min(percent, 100)}%` }} />
</div>
);
}
function numberFmt(n: number) {
return n.toLocaleString("en-US");
}
export default function ServerPage() {
const { isLoggedIn } = useAuth();
const [data, setData] = useState<ServerStatus | null>(null);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
try {
const res = await authFetch("/api/server/status");
if (!res.ok) return;
const json = await res.json();
setData(json);
} catch (e) {
// ignore
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!isLoggedIn) return;
fetchData();
const iv = setInterval(fetchData, 10000);
return () => clearInterval(iv);
}, [isLoggedIn, fetchData]);
if (!isLoggedIn) {
return (
<div className="flex flex-col items-center justify-center h-64 gap-3">
<p className="text-slate-500 text-sm"></p>
<div className="flex gap-2">
<Link href="/login" className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm"></Link>
<Link href="/register" className="border border-slate-300 text-slate-600 px-4 py-2 rounded-lg text-sm"></Link>
</div>
</div>
);
}
return (
<div className="space-y-5">
{/* 标题 */}
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h1 className="text-xl font-bold text-slate-900"></h1>
<p className="text-slate-500 text-xs mt-0.5">GCP asia-northeast1-b · 10</p>
</div>
{data?.backfill_running && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-200 text-xs text-blue-700 font-medium">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse" />
</span>
)}
</div>
{loading ? (
<div className="flex items-center justify-center h-48 text-slate-400 text-sm">...</div>
) : !data ? (
<div className="flex items-center justify-center h-48 text-red-500 text-sm"></div>
) : (
<>
{/* 系统概览 4卡片 */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{/* CPU */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-slate-500 font-medium">CPU</span>
<span className="text-xs text-slate-400">{data.cpu.cores} </span>
</div>
<div className="text-2xl font-bold text-slate-900 mb-2">{data.cpu.percent}%</div>
<ProgressBar percent={data.cpu.percent} />
<p className="mt-2 text-xs text-slate-400">
{data.load.load1} / {data.load.load5} / {data.load.load15}
</p>
</div>
{/* 内存 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-slate-500 font-medium"></span>
<span className="text-xs text-slate-400">{data.memory.used_gb}G / {data.memory.total_gb}G</span>
</div>
<div className="text-2xl font-bold text-slate-900 mb-2">{data.memory.percent}%</div>
<ProgressBar percent={data.memory.percent} />
{data.memory.swap_percent > 0 && (
<p className="mt-2 text-xs text-amber-600">Swap {data.memory.swap_percent}%</p>
)}
</div>
{/* 硬盘 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-slate-500 font-medium"></span>
<span className="text-xs text-slate-400">{data.disk.free_gb}G </span>
</div>
<div className="text-2xl font-bold text-slate-900 mb-2">{data.disk.percent}%</div>
<ProgressBar percent={data.disk.percent} />
<p className="mt-2 text-xs text-slate-400">{data.disk.used_gb}G / {data.disk.total_gb}G</p>
</div>
{/* 运行时间 & 网络 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-slate-500 font-medium"></span>
</div>
<div className="text-2xl font-bold text-slate-900 mb-2">
{data.uptime_hours > 24 ? `${Math.floor(data.uptime_hours/24)}` : `${data.uptime_hours}h`}
</div>
<div className="text-xs text-slate-400 space-y-0.5">
<p> {data.network.bytes_sent_gb} GB</p>
<p> {data.network.bytes_recv_gb} GB</p>
</div>
</div>
</div>
{/* PM2 进程 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100">
<h3 className="font-semibold text-sm text-slate-800">PM2 </h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-slate-400 border-b border-slate-100 text-left">
<th className="px-4 py-2.5 font-medium"></th>
<th className="px-4 py-2.5 font-medium text-center"></th>
<th className="px-4 py-2.5 font-medium text-right">CPU</th>
<th className="px-4 py-2.5 font-medium text-right"></th>
<th className="px-4 py-2.5 font-medium text-right"></th>
<th className="px-4 py-2.5 font-medium text-right"></th>
</tr>
</thead>
<tbody>
{data.pm2.map((p, i) => (
<tr key={i} className="border-b border-slate-50 hover:bg-slate-50/50">
<td className="px-4 py-2.5 font-mono text-slate-800">{p.name}</td>
<td className="px-4 py-2.5 text-center">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
p.status === "online"
? "bg-emerald-50 text-emerald-700 border border-emerald-200"
: "bg-red-50 text-red-700 border border-red-200"
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${p.status === "online" ? "bg-emerald-500" : "bg-red-500"}`} />
{p.status}
</span>
</td>
<td className="px-4 py-2.5 text-right font-mono text-slate-600">{p.cpu}%</td>
<td className="px-4 py-2.5 text-right font-mono text-slate-600">{p.memory_mb} MB</td>
<td className="px-4 py-2.5 text-right font-mono text-slate-600">{p.restarts}</td>
<td className="px-4 py-2.5 text-right text-slate-400">{uptimeStr(p.uptime_ms)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* PostgreSQL */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100">
<h3 className="font-semibold text-sm text-slate-800">PostgreSQL</h3>
</div>
<div className="p-4 grid grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<p className="text-xs text-slate-400 mb-1"></p>
<p className="text-lg font-bold text-slate-900">
{data.postgres.db_size_mb > 1024
? `${(data.postgres.db_size_mb / 1024).toFixed(1)} GB`
: `${data.postgres.db_size_mb} MB`}
</p>
</div>
<div>
<p className="text-xs text-slate-400 mb-1">aggTrades</p>
<p className="text-lg font-bold text-blue-600">{numberFmt(data.postgres.agg_trades_count)}</p>
</div>
<div>
<p className="text-xs text-slate-400 mb-1"></p>
<p className="text-lg font-bold text-slate-900">{numberFmt(data.postgres.rate_snapshots_count)}</p>
</div>
<div>
<p className="text-xs text-slate-400 mb-1"></p>
<p className={`text-lg font-bold ${data.backfill_running ? "text-blue-600" : "text-slate-400"}`}>
{data.backfill_running ? "运行中" : "已停止"}
</p>
</div>
</div>
{/* 数据覆盖范围 */}
{data.postgres.symbols && Object.keys(data.postgres.symbols).length > 0 && (
<div className="px-4 pb-4">
<p className="text-xs text-slate-400 mb-2"></p>
<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-50 rounded-lg px-3 py-2 border border-slate-100">
<span className="font-mono text-sm font-medium text-slate-800">{sym}</span>
<div className="text-xs text-slate-500 text-right">
<p>{bjtStr(info.earliest_ms)} {bjtStr(info.latest_ms)}</p>
<p className="text-blue-600 font-medium">
{info.span_hours > 24 ? `${(info.span_hours/24).toFixed(1)}` : `${info.span_hours.toFixed(1)} 小时`}
</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* 说明 */}
<div className="rounded-lg border border-blue-100 bg-blue-50 px-4 py-3 text-xs text-slate-600">
<span className="text-blue-600 font-medium"></span>10PM2进程状态实时反映服务运行情况CPU和网络负载会偏高属正常现象
</div>
</>
)}
</div>
);
}