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 = (
);
+ 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 小时已恢复健康事件
+
+
+
+