diff --git a/atsf_server/web/features/dashboard/components/world-stage.tsx b/atsf_server/web/features/dashboard/components/world-stage.tsx index d9725968..74c08012 100644 --- a/atsf_server/web/features/dashboard/components/world-stage.tsx +++ b/atsf_server/web/features/dashboard/components/world-stage.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; +import { EmptyState } from '@/components/feedback/empty-state'; import { StatusBadge } from '@/components/ui/status-badge'; import type { DashboardConfig, @@ -117,6 +118,22 @@ function HeroMetric({ ); } +function SignalLegend() { + return ( +
+
+ 绿色: 运行健康 +
+
+ 黄色: 容量压力 +
+
+ 红色: 异常或离线 +
+
+ ); +} + function CountrySignal({ item, index, @@ -210,6 +227,7 @@ export function WorldStage({
{geoConfiguredNodes > 0 ? '真实节点点位' : '节点信号覆盖'}
+ - {signalNodes.map((node, index) => { - const anchor = projectNodeCoordinates(node, index); - const tone = getNodeSignalTone(node); - return ( - -
-
- - -
-
-

- {node.name} -

-

- {(node.geo_name || node.name) + ' · '} - 请求 {node.request_count.toLocaleString('zh-CN')} · 异常{' '} - {node.active_event_count} -

- {!anchor.derivedFromGeo ? ( -

- 未配置经纬度,当前使用备用落点 + {signalNodes.length === 0 ? ( +

+ +
+ ) : ( + signalNodes.map((node, index) => { + const anchor = projectNodeCoordinates(node, index); + const tone = getNodeSignalTone(node); + return ( + +
+
+ + +
+
+

+ {node.name}

- ) : null} +

+ {(node.geo_name || node.name) + ' · '} + 请求 {node.request_count.toLocaleString('zh-CN')} · 异常{' '} + {node.active_event_count} +

+ {!anchor.derivedFromGeo ? ( +

+ 未配置经纬度,当前使用备用落点 +

+ ) : null} +
-
- - ); - })} + + ); + }) + )}
{sourceCountries.length > 0 ? ( 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 6e66752d..3795e82d 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -266,6 +266,28 @@ function MetricBar({ ); } +function SummaryStat({ + label, + value, + hint, +}: { + label: string; + value: string; + hint: string; +}) { + return ( +
+

+ {label} +

+

+ {value} +

+

{hint}

+
+ ); +} + export function NodeDetailPage({ nodeId }: { nodeId: string }) { const router = useRouter(); const queryClient = useQueryClient(); @@ -542,6 +564,11 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { ); const dominantStatusCode = statusCodeDistribution[0] ?? null; const dominantDomain = topDomains[0] ?? null; + const topSourceCountry = + observability?.analytics.distributions.source_countries[0] ?? null; + const trafficSummary = observability?.analytics.traffic ?? null; + const healthSummary = observability?.analytics.health ?? null; + const latestHealthEvent = activeHealthEvents[0] ?? null; const resolvedHealthEvents = useMemo( () => observability?.health_events.filter( @@ -669,6 +696,55 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { ) : null} +
+ + + + +
+
+
+ ) : ( + + )} + + + + {observabilityQuery.isLoading ? ( + + ) : observabilityQuery.isError ? ( + + ) : latestMetricSnapshot ? ( +
+
+ + + +

- 网络流量 + OpenResty 吞吐 +

+
+

+ 入站: + {formatBytes(latestMetricSnapshot.openresty_rx_bytes)} +

+

+ 出站: + {formatBytes(latestMetricSnapshot.openresty_tx_bytes)} +

+
+
+
+

+ 节点网络

@@ -840,99 +971,48 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {

+
+ +

- 磁盘 IO + 最近窗口请求 +

+

+ {trafficSummary + ? trafficSummary.request_count.toLocaleString('zh-CN') + : '—'} +

+

+ {trafficSummary + ? `QPS ${trafficSummary.estimated_qps.toFixed(1)} · UV ${trafficSummary.unique_visitor_count}` + : '暂无窗口流量摘要'} +

+
+
+

+ 最近窗口错误 +

+

+ {trafficSummary + ? trafficSummary.error_count.toLocaleString('zh-CN') + : '—'} +

+

+ {trafficSummary + ? `错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%` + : '暂无错误率摘要'}

-
-

- 读取: - {formatBytes(latestMetricSnapshot.disk_read_bytes)} -

-

- 写入: - {formatBytes(latestMetricSnapshot.disk_write_bytes)} -

-
) : ( )}
- - -
-
- - - -
- -
-

- 最近心跳: - {isMeaningfulTime(node.last_seen_at) - ? ` ${formatRelativeTime(node.last_seen_at)} · ${formatDateTime(node.last_seen_at)}` - : ' 暂无'} -

-

IP:{node.ip || '暂无'}

-

当前配置:{node.current_version || '未应用'}

- {latestTrafficReport ? ( -

- 最近窗口请求:{latestTrafficReport.request_count} · UV{' '} - {latestTrafficReport.unique_visitor_count} · 错误{' '} - {latestTrafficReport.error_count} -

- ) : null} -
- -
- {activeHealthEvents.slice(0, 3).map((event) => ( -
-
- -
-

- {event.message || '暂无详细消息'} -

-
- ))} - - {activeHealthEvents.length === 0 ? ( -
- 当前没有活动中的健康事件。 -
- ) : null} -
-
-
@@ -1005,6 +1085,78 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
+
+ + + formatTrendHour(point.bucket_started_at), + ) ?? [] + } + series={[ + { + label: 'OpenResty 入站', + color: '#22c55e', + fillColor: 'rgba(34, 197, 94, 0.14)', + variant: 'area', + values: + observability?.trends.network_24h.map( + (point) => point.openresty_rx_bytes, + ) ?? [], + valueFormatter: formatBytes, + }, + { + label: 'OpenResty 出站', + color: '#38bdf8', + values: + observability?.trends.network_24h.map( + (point) => point.openresty_tx_bytes, + ) ?? [], + valueFormatter: formatBytes, + }, + ]} + /> + + + + + formatTrendHour(point.bucket_started_at), + ) ?? [] + } + series={[ + { + label: '磁盘读', + color: '#a78bfa', + fillColor: 'rgba(167, 139, 250, 0.14)', + variant: 'area', + values: + observability?.trends.disk_io_24h.map( + (point) => point.disk_read_bytes, + ) ?? [], + valueFormatter: formatBytes, + }, + { + label: '磁盘写', + color: '#fb7185', + values: + observability?.trends.disk_io_24h.map( + (point) => point.disk_write_bytes, + ) ?? [], + valueFormatter: formatBytes, + }, + ]} + /> + +
+