diff --git a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx index 41fe8e64..2d2add5a 100644 --- a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx +++ b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx @@ -76,11 +76,13 @@ function RiskSignal({ value, tone, hint, + href, }: { label: string; value: number; tone: 'danger' | 'warning' | 'info' | 'success'; hint: string; + href?: string; }) { const toneClass = tone === 'danger' @@ -91,25 +93,38 @@ function RiskSignal({ ? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100' : 'border-sky-400/30 bg-sky-500/10 text-sky-100'; - return ( + const content = (

{label}

{value}

{hint}

); + if (!href) { + return content; + } + return ( + + {content} + + ); } function PeakCard({ label, value, hint, + href, }: { label: string; value: string; hint: string; + href?: string; }) { - return ( + const content = (

{label} @@ -120,6 +135,17 @@ function PeakCard({

{hint}

); + if (!href) { + return content; + } + return ( + + {content} + + ); } function formatPeakNode(node: DashboardPeakNode | null) { @@ -135,6 +161,13 @@ function formatPeakNode(node: DashboardPeakNode | null) { }; } +function buildNodeDetailHref(id?: number | null) { + if (!id) { + return '/node'; + } + return `/node/detail?id=${id}`; +} + function OverviewMetric({ label, value, @@ -163,7 +196,10 @@ function OverviewMetric({ function NodeHealthRow({ node }: { node: DashboardNodeHealth }) { return ( -
+

@@ -229,7 +265,7 @@ function NodeHealthRow({ node }: { node: DashboardNodeHealth }) { : ' 暂无'}

-
+ ); } @@ -263,6 +299,10 @@ export function DashboardOverview() { ); } + const nodeIdMap = new Map( + overview.nodes.map((node) => [node.node_id, node.id] as const), + ); + return (
+
@@ -373,11 +422,17 @@ export function DashboardOverview() { label="最忙节点" value={formatPeakNode(overview.peaks.busiest_node).title} hint={formatPeakNode(overview.peaks.busiest_node).hint} + href={buildNodeDetailHref( + nodeIdMap.get(overview.peaks.busiest_node?.node_id ?? ''), + )} />
@@ -510,9 +565,10 @@ export function DashboardOverview() { ) : (
{overview.active_alerts.map((alert) => ( -
-
+ ))}
)} diff --git a/atsf_server/web/features/nodes/components/node-detail-page.tsx b/atsf_server/web/features/nodes/components/node-detail-page.tsx index e82debcc..f86b1865 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -526,6 +526,15 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { ), [observability?.traffic_reports], ); + const dominantStatusCode = statusCodeDistribution[0] ?? null; + const dominantDomain = topDomains[0] ?? null; + const resolvedHealthEvents = useMemo( + () => + observability?.health_events.filter( + (event) => event.status === 'resolved', + ) ?? [], + [observability?.health_events], + ); const memoryUsageRatio = formatUsageRatio( latestMetricSnapshot?.memory_used_bytes, latestMetricSnapshot?.memory_total_bytes, @@ -972,6 +981,46 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { title="请求结构分布" description="聚合最近 24 小时窗口上报,帮助判断错误集中在哪些状态码、流量集中在哪些域名。" > +
+
+

+ 主状态码 +

+

+ {dominantStatusCode?.label ?? '—'} +

+

+ {dominantStatusCode + ? `${dominantStatusCode.value} 次` + : '暂无状态码聚合'} +

+
+
+

+ Top Domain +

+

+ {dominantDomain?.label ?? '—'} +

+

+ {dominantDomain + ? `${dominantDomain.value} 次` + : '暂无域名聚合'} +

+
+
+

+ 已恢复事件 +

+

+ {resolvedHealthEvents.length} +

+

+ 最近 24 小时已恢复健康事件 +

+
+
+