2026-04-14 18:05:32 +08:00

194 lines
7.1 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.

import { formatBytes, getDashboardMetrics } from "@/lib/store";
function getNodePill(node: {
enabled: number;
last_error_message: string | null;
}) {
if (!node.enabled) {
return { label: "已禁用", className: "status-pill status-pill--off" };
}
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">
<section className="hero-panel">
<div className="hero-copy">
<span className="eyebrow">OVERVIEW</span>
<h2></h2>
<p className="page-lede">
线
</p>
<div className="badge-row">
<span className="status-pill status-pill--ok">HTTP 线</span>
<span className="status-pill status-pill--neutral"></span>
<span className="status-pill status-pill--warm"> {data.totals.enabled_nodes}</span>
</div>
</div>
<div className="hero-aside">
<div className="stat-tile">
<span></span>
<strong>{data.totals.total_online_connections}</strong>
</div>
<div className="stat-tile">
<span></span>
<strong>{formatBytes(data.totals.total_traffic)}</strong>
</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> kick </small>
</article>
<article className="metric-card metric-card--paper">
<span></span>
<strong>{formatBytes(data.totals.total_traffic)}</strong>
<small> SQLite</small>
</article>
</section>
<section className="panel-grid">
<article className="panel-card">
<div className="section-title">
<h3></h3>
</div>
<div className="stack-md">
{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.last_online_users} / {node.last_stream_count}
</p>
<p className="muted">
{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>
</div>
<div className="stack-md">
{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>
</div>
<div className="stack-md">
{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">{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.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">{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">{item.created_at}</p>
</div>
))
)}
</div>
</article>
</section>
</main>
);
}