Files
hy2-panel/app/admin/dashboard/page.tsx
T

256 lines
8.9 KiB
TypeScript

import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { formatLocalDateTime } from "@/lib/datetime";
import { formatBytes, getDashboardMetrics } from "@/lib/store";
import { DashboardCharts } from "./dashboard-charts";
function getNodePill(node: {
enabled: number;
node_type: "hy2" | "custom";
last_error_message: string | null;
}) {
if (!node.enabled) {
return { label: "已禁用", className: "status-pill status-pill--off" };
}
if (node.node_type === "custom")
return { label: "自定义订阅", className: "status-pill status-pill--ink" };
if (node.last_error_message) {
return { label: "同步异常", className: "status-pill status-pill--danger" };
}
return { label: "运行健康", className: "status-pill status-pill--ok" };
}
export default async function DashboardPage() {
const data = await getDashboardMetrics();
return (
<main className="page-stack dashboard-page">
<section className="hero-panel">
<div className="hero-copy">
<span className="eyebrow">概览</span>
<h2>仪表盘</h2>
<div className="badge-row">
<span className="status-pill status-pill--ok">
在线 {data.totals.total_online_connections}
</span>
<span className="status-pill status-pill--warm">
节点 {data.totals.enabled_nodes}
</span>
</div>
</div>
</section>
<section className="metrics-grid">
<article className="metric-card metric-card--warm">
<span>总用户</span>
<strong>{data.totals.total_users}</strong>
<small>
启用 {data.totals.enabled_users} / 管理员 {data.totals.admin_users}
</small>
</article>
<article className="metric-card metric-card--ink">
<span>活跃用户</span>
<strong>{data.totals.active_presence_rows}</strong>
<small>在线连接 {data.totals.total_online_connections}</small>
</article>
<article className="metric-card metric-card--sage">
<span>启用节点</span>
<strong>{data.totals.enabled_nodes}</strong>
<small>当前可用</small>
</article>
<article className="metric-card metric-card--paper">
<span>累计流量</span>
<strong>{formatBytes(data.totals.total_traffic)}</strong>
<small>全量累计</small>
</article>
</section>
<DashboardCharts
nodeHealth={data.nodeHealth.filter((node) => node.node_type === "hy2")}
trafficTrend={data.trafficTrend}
trafficBuckets={data.trafficBuckets}
trafficUserOptions={data.trafficUserOptions}
loadTrend={data.loadTrend}
failureTrend={data.failureTrend}
topConsumerRows={data.topConsumerRows}
/>
<section className="panel-grid">
<article className="panel-card">
<div className="section-title">
<h3>节点健康</h3>
<Link
href="/admin/nodes"
className={buttonVariants({ variant: "ghost", size: "xs" })}
>
管理节点 →
</Link>
</div>
<div className="stack-md">
{data.nodeHealth.length === 0 && (
<p className="muted">暂无节点,请先添加节点。</p>
)}
{data.nodeHealth.map((node) => (
<div key={node.id} className="list-row list-row--card">
<div>
<strong>{node.name}</strong>
<p className="muted mono">/{node.slug}</p>
</div>
<div className="align-right">
<p>
<span className={getNodePill(node).className}>
{getNodePill(node).label}
</span>
</p>
<p className="muted">
{node.node_type === "custom"
? "仅订阅 · 不参与轮询"
: `在线 ${node.last_online_users} / 流 ${node.last_stream_count}`}
</p>
{node.node_type === "hy2" && (
<p className="muted">
最近成功 {formatLocalDateTime(node.last_sync_ok_at)}
</p>
)}
{node.last_error_message ? (
<p className="error-text">{node.last_error_message}</p>
) : null}
</div>
</div>
))}
</div>
</article>
<article className="panel-card">
<div className="section-title">
<h3>用量排行</h3>
<Link
href="/admin/users"
className={buttonVariants({ variant: "ghost", size: "xs" })}
>
管理用户 →
</Link>
</div>
<div className="stack-md">
{data.topUsers.length === 0 && (
<p className="muted">暂无用户用量。</p>
)}
{data.topUsers.map((user) => (
<div key={user.auth_id} className="list-row list-row--card">
<div>
<strong>{user.username}</strong>
<p className="muted mono">{user.auth_id}</p>
</div>
<div className="align-right">
<p>{formatBytes(user.total_bytes)}</p>
<p className="muted">
TX {formatBytes(user.used_tx_bytes)} / RX{" "}
{formatBytes(user.used_rx_bytes)}
</p>
</div>
</div>
))}
</div>
</article>
<article className="panel-card">
<div className="section-title">
<h3>最近鉴权失败</h3>
<Link
href="/admin/audits"
className={buttonVariants({ variant: "ghost", size: "xs" })}
>
查看日志 →
</Link>
</div>
<div className="stack-md">
{data.recentFailures.length === 0 && (
<p className="muted">暂无鉴权失败。</p>
)}
{data.recentFailures.map((item, index) => (
<div
key={`${item.created_at}-${index}`}
className="list-row list-row--card"
>
<div>
<strong>{item.reason}</strong>
<p className="muted">
{item.node_name ?? "未知节点"} /{" "}
{item.username ?? "未知用户"}
</p>
</div>
<div className="align-right">
<p className="muted mono">{item.addr ?? "-"}</p>
<p className="muted">
{formatLocalDateTime(item.created_at)}
</p>
</div>
</div>
))}
</div>
</article>
<article className="panel-card">
<div className="section-title">
<h3>最近流量入账</h3>
</div>
<div className="stack-md">
{data.recentUsage.length === 0 && (
<p className="muted">暂无流量入账记录。</p>
)}
{data.recentUsage.map((item, index) => (
<div
key={`${item.created_at}-${item.auth_id}-${index}`}
className="list-row list-row--card"
>
<div>
<strong>{item.username ?? item.auth_id}</strong>
<p className="muted">{item.node_name ?? "未知节点"}</p>
</div>
<div className="align-right">
<p>+{formatBytes(item.tx_bytes + item.rx_bytes)}</p>
<p className="muted">
{formatLocalDateTime(item.created_at)}
</p>
</div>
</div>
))}
</div>
</article>
<article className="panel-card full-span">
<div className="section-title">
<h3>节点同步错误</h3>
</div>
<div className="stack-md">
{data.syncErrors.length === 0 ? (
<p className="muted">暂无同步错误。</p>
) : (
data.syncErrors.map((item, index) => (
<div
key={`${item.created_at}-${index}`}
className="list-row list-row--card"
>
<div>
<strong>{item.node_name}</strong>
<p className="error-text">
{item.error_message ?? "unknown error"}
</p>
</div>
<p className="muted">
{formatLocalDateTime(item.created_at)}
</p>
</div>
))
)}
</div>
</article>
</section>
</main>
);
}