diff --git a/internal/handler/agent_query_handler.go b/internal/handler/agent_query_handler.go index a710077..bed58ff 100644 --- a/internal/handler/agent_query_handler.go +++ b/internal/handler/agent_query_handler.go @@ -73,13 +73,9 @@ func (h *AgentHandler) buildAgentListItem(agent models.Agent, isAuthenticated bo trafficStats := agent.TrafficStats.Data() if trafficStats.Enabled { - item["traffic"] = map[string]any{ - "enabled": true, - "limit": trafficStats.Limit, - "used": trafficStats.Used, - } + item["trafficStats"] = trafficStats } else { - item["traffic"] = map[string]any{ + item["trafficStats"] = map[string]any{ "enabled": false, } } diff --git a/web/src/admin/pages/Agents/TrafficStats.tsx b/web/src/admin/pages/Agents/TrafficStats.tsx index be54504..f35731f 100644 --- a/web/src/admin/pages/Agents/TrafficStats.tsx +++ b/web/src/admin/pages/Agents/TrafficStats.tsx @@ -4,10 +4,12 @@ import { App, Button, Card, + Col, Descriptions, Form, InputNumber, Progress, + Row, Select, Space, Statistic, @@ -20,6 +22,7 @@ import {getTrafficStats, resetAgentTraffic, updateTrafficConfig} from '@/api/age import {getErrorMessage} from '@/lib/utils'; import type {TrafficStats as TrafficStatsType, UpdateTrafficConfigRequest} from '@/types'; import dayjs from 'dayjs'; +import {formatBytes} from "@/lib/format.ts"; interface TrafficStatsProps { agentId: string; @@ -131,214 +134,232 @@ const TrafficStats: React.FC = ({agentId}) => { return '#52c41a'; }; - // 格式化字节数 - const formatBytes = (bytes: number) => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; - }; - return ( } /> - {/* 当前流量使用情况 */} - {stats && stats.enabled && stats.limit > 0 && ( - - -
- - - 0 ? '#52c41a' : '#ff4d4f'} - }} - /> -
- -
-
- 使用率 - {usagePercent.toFixed(2)}% + + {/* 左侧:流量配置 */} + + + + 流量统计配置
- -
- - - - {stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'} - - - {dayjs(stats.periodStart).format('YYYY-MM-DD')} ~ {dayjs(stats.periodEnd).format('YYYY-MM-DD')} - - - {stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'} - - - - {stats.alerts.sent80 && 80%告警已发送} - {stats.alerts.sent90 && 90%告警已发送} - {stats.alerts.sent100 && 100%告警已发送} - {!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && ( - 正常 - )} - - - - - + } + loading={isLoading} + > +
- 立即重置流量 - - - - )} - - {/* 流量配置 */} - - - 流量统计配置 - - } - extra={ - - } - loading={isLoading} - > - - - - - - {enabled && ( - <> - + - - - + + + - - ({ + label: `每月${i + 1}号`, + value: i + 1, + })), + ]} + /> + + + )} + +
+ + + {/* 右侧:流量统计 */} + + {stats && stats.enabled ? ( + + + {/* 基础统计 */} +
+ 0 ? getProgressColor(usagePercent) : undefined} + }} + /> + {stats.limit > 0 && ( + <> + + 0 ? '#52c41a' : '#ff4d4f'} + }} + /> + + )} +
+ + {/* 使用率进度条 - 仅在有限额时显示 */} + {stats.limit > 0 && ( +
+
+ 使用率 + {usagePercent.toFixed(2)}% +
+ +
+ )} + + {/* 详细信息 */} + + + {stats.type === 'recv' ? '进站流量 (下载)' : stats.type === 'send' ? '出站流量 (上传)' : '全部流量 (上传+下载)'} + + + {stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'} + + + {dayjs(stats.periodStart).format('YYYY-MM-DD')} ~ {dayjs(stats.periodEnd).format('YYYY-MM-DD')} + + + {stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'} + + {/* 告警状态 - 仅在有限额时显示 */} + {stats.limit > 0 && ( + + + {stats.alerts.sent80 && 80%告警已发送} + {stats.alerts.sent90 && 90%告警已发送} + {stats.alerts.sent100 && 100%告警已发送} + {!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && ( + 正常 + )} + + + )} + + + +
+
+ ) : ( + +
+ +

请先启用流量统计功能

+
+
)} - -
+ +
); }; diff --git a/web/src/portal/utils/util.ts b/web/src/lib/format.ts similarity index 92% rename from web/src/portal/utils/util.ts rename to web/src/lib/format.ts index 5c9f437..dbd2268 100644 --- a/web/src/portal/utils/util.ts +++ b/web/src/lib/format.ts @@ -6,16 +6,6 @@ export const formatTime = (ms: number): string => { return `${(ms / 1000).toFixed(2)} s`; }; -export const formatDate = (timestamp: number): string => { - if (!timestamp) return '-'; - const date = new Date(timestamp); - return date.toLocaleDateString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }); -}; - export const formatDateTime = (value: string | number | undefined | null): string => { if (value === undefined || value === null || value === '') { return '-'; diff --git a/web/src/portal/components/Card.tsx b/web/src/portal/components/Card.tsx index d185709..bbc3c6a 100644 --- a/web/src/portal/components/Card.tsx +++ b/web/src/portal/components/Card.tsx @@ -2,8 +2,8 @@ import type {ReactNode} from 'react'; import CyberCard from "@portal/components/CyberCard.tsx"; interface CardProps { - title?: string; - description?: string; + title?: ReactNode; + description?: ReactNode; action?: ReactNode; children: ReactNode; className?: string; diff --git a/web/src/portal/components/NetworkStatCard.tsx b/web/src/portal/components/NetworkStatCard.tsx index c1e684e..2e1b15f 100644 --- a/web/src/portal/components/NetworkStatCard.tsx +++ b/web/src/portal/components/NetworkStatCard.tsx @@ -1,6 +1,6 @@ import type {FC} from 'react'; import {ArrowDown, ArrowUp, Network} from 'lucide-react'; -import {formatBytes, formatSpeed} from '@portal/utils/util'; +import {formatBytes, formatSpeed} from '@/lib/format.ts'; import {cn} from "@/lib/utils.ts"; interface NetworkStatCardProps { diff --git a/web/src/portal/components/ServerCard.tsx b/web/src/portal/components/ServerCard.tsx index 4af192a..4ce72cc 100644 --- a/web/src/portal/components/ServerCard.tsx +++ b/web/src/portal/components/ServerCard.tsx @@ -14,7 +14,7 @@ import { } from 'lucide-react'; import type {Agent, LatestMetrics} from '@/types'; import CompactResourceBar from '@portal/components/CompactResourceBar'; -import {formatBytes, formatSpeed, formatUptime} from '@portal/utils/util'; +import {formatBytes, formatSpeed, formatUptime} from '@/lib/format.ts'; import CyberCard from "@portal/components/CyberCard.tsx"; import {Link} from "react-router-dom"; import {isExpired} from "@portal/utils/server.ts"; diff --git a/web/src/portal/components/monitor/AgentStatsTable.tsx b/web/src/portal/components/monitor/AgentStatsTable.tsx index ea1f262..044188c 100644 --- a/web/src/portal/components/monitor/AgentStatsTable.tsx +++ b/web/src/portal/components/monitor/AgentStatsTable.tsx @@ -2,7 +2,7 @@ import {AlertCircle, Clock, MapPin} from 'lucide-react'; import {StatusBadge} from '@portal/components/StatusBadge'; import {CertBadge} from './CertBadge'; import {AGENT_COLORS} from '@portal/constants/colors'; -import {formatDateTime, formatTime} from '@portal/utils/util'; +import {formatDateTime, formatTime} from '@/lib/format.ts'; import type {AgentMonitorStat} from '@/types'; import CyberCard from "@portal/components/CyberCard.tsx"; @@ -72,7 +72,8 @@ export const AgentStatsTable = ({monitorStats, monitorType}: AgentStatsTableProp {monitorType === 'https' && stat.certExpiryTime && (
- 证书: + 证书:
- + {formatTime(stat.responseTime)}
diff --git a/web/src/portal/components/monitor/MonitorCard.tsx b/web/src/portal/components/monitor/MonitorCard.tsx index 42fdaa2..4181505 100644 --- a/web/src/portal/components/monitor/MonitorCard.tsx +++ b/web/src/portal/components/monitor/MonitorCard.tsx @@ -6,7 +6,7 @@ import {useMemo} from "react"; import CyberCard from "@portal/components/CyberCard.tsx"; import {StatusBadge} from "@portal/components/StatusBadge"; import {CertBadge} from "@portal/components/monitor/CertBadge.tsx"; -import {formatDateTime} from "@portal/utils/util.ts"; +import {formatDateTime} from "@/lib/format.ts"; import {MiniChart} from "@portal/components/monitor/MiniChart.tsx"; import {TypeIcon} from "@portal/components/monitor/TypeIcon"; diff --git a/web/src/portal/components/monitor/MonitorHero.tsx b/web/src/portal/components/monitor/MonitorHero.tsx index 2e2bbb9..a02334e 100644 --- a/web/src/portal/components/monitor/MonitorHero.tsx +++ b/web/src/portal/components/monitor/MonitorHero.tsx @@ -5,7 +5,7 @@ import {CertBadge} from './CertBadge'; import LittleStatCard from '@portal/components/LittleStatCard'; import type {PublicMonitor} from '@/types'; import CyberCard from "@portal/components/CyberCard.tsx"; -import {formatDateTime} from "@portal/utils/util.ts"; +import {formatDateTime} from "@/lib/format.ts"; interface MonitorHeroProps { monitor: PublicMonitor; diff --git a/web/src/portal/components/monitor/ResponseTimeChart.tsx b/web/src/portal/components/monitor/ResponseTimeChart.tsx index 0b9a195..df8f6e1 100644 --- a/web/src/portal/components/monitor/ResponseTimeChart.tsx +++ b/web/src/portal/components/monitor/ResponseTimeChart.tsx @@ -10,7 +10,7 @@ import CyberCard from "@portal/components/CyberCard.tsx"; import {ChartPlaceholder} from "@portal/components/ChartPlaceholder"; import {CustomTooltip} from "@portal/components/CustomTooltip"; import {TimeRangeSelector} from "@portal/components/TimeRangeSelector"; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface ResponseTimeChartProps { monitorId: string; diff --git a/web/src/portal/components/server/CpuChart.tsx b/web/src/portal/components/server/CpuChart.tsx index b09232c..ff1cdec 100644 --- a/web/src/portal/components/server/CpuChart.tsx +++ b/web/src/portal/components/server/CpuChart.tsx @@ -5,7 +5,7 @@ import {ChartPlaceholder} from '@portal/components/ChartPlaceholder'; import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface CpuChartProps { agentId: string; diff --git a/web/src/portal/components/server/DiskIOChart.tsx b/web/src/portal/components/server/DiskIOChart.tsx index bcfcb83..48bec12 100644 --- a/web/src/portal/components/server/DiskIOChart.tsx +++ b/web/src/portal/components/server/DiskIOChart.tsx @@ -5,7 +5,7 @@ import {ChartPlaceholder} from '@portal/components/ChartPlaceholder'; import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface DiskIOChartProps { agentId: string; diff --git a/web/src/portal/components/server/GpuChart.tsx b/web/src/portal/components/server/GpuChart.tsx index 09da44e..c87f964 100644 --- a/web/src/portal/components/server/GpuChart.tsx +++ b/web/src/portal/components/server/GpuChart.tsx @@ -5,7 +5,7 @@ import {ChartPlaceholder} from '@portal/components/ChartPlaceholder'; import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface GpuChartProps { agentId: string; diff --git a/web/src/portal/components/server/GpuMonitorSection.tsx b/web/src/portal/components/server/GpuMonitorSection.tsx index 4bdc903..504baa6 100644 --- a/web/src/portal/components/server/GpuMonitorSection.tsx +++ b/web/src/portal/components/server/GpuMonitorSection.tsx @@ -1,6 +1,6 @@ import {Zap} from 'lucide-react'; import {Card} from '@portal/components/Card'; -import {formatBytes} from '@portal/utils/util'; +import {formatBytes} from '@/lib/format.ts'; import type {LatestMetrics} from '@/types'; interface GpuMonitorSectionProps { diff --git a/web/src/portal/components/server/MemoryChart.tsx b/web/src/portal/components/server/MemoryChart.tsx index a216fcf..4ed100f 100644 --- a/web/src/portal/components/server/MemoryChart.tsx +++ b/web/src/portal/components/server/MemoryChart.tsx @@ -5,7 +5,7 @@ import {ChartPlaceholder} from '@portal/components/ChartPlaceholder'; import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface MemoryChartProps { agentId: string; diff --git a/web/src/portal/components/server/MonitorChart.tsx b/web/src/portal/components/server/MonitorChart.tsx index 702b825..b48717b 100644 --- a/web/src/portal/components/server/MonitorChart.tsx +++ b/web/src/portal/components/server/MonitorChart.tsx @@ -6,7 +6,7 @@ import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {useIsMobile} from '@portal/hooks/use-mobile'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface MonitorChartProps { agentId: string; diff --git a/web/src/portal/components/server/NetworkChart.tsx b/web/src/portal/components/server/NetworkChart.tsx index 1f9663e..6ddf512 100644 --- a/web/src/portal/components/server/NetworkChart.tsx +++ b/web/src/portal/components/server/NetworkChart.tsx @@ -6,7 +6,7 @@ import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery, useNetworkInterfacesQuery} from '@portal/hooks/server'; import {INTERFACE_COLORS} from '@portal/constants/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface NetworkChartProps { agentId: string; diff --git a/web/src/portal/components/server/NetworkConnectionChart.tsx b/web/src/portal/components/server/NetworkConnectionChart.tsx index 2876c53..c4fe154 100644 --- a/web/src/portal/components/server/NetworkConnectionChart.tsx +++ b/web/src/portal/components/server/NetworkConnectionChart.tsx @@ -5,7 +5,7 @@ import {ChartPlaceholder} from '@portal/components/ChartPlaceholder'; import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface NetworkConnectionChartProps { agentId: string; diff --git a/web/src/portal/components/server/ServerHero.tsx b/web/src/portal/components/server/ServerHero.tsx index ac2f296..16aec97 100644 --- a/web/src/portal/components/server/ServerHero.tsx +++ b/web/src/portal/components/server/ServerHero.tsx @@ -1,5 +1,5 @@ import {ArrowLeft} from 'lucide-react'; -import {formatBytes, formatDateTime, formatUptime} from '@portal/utils/util'; +import {formatBytes, formatDateTime, formatUptime} from '@/lib/format.ts'; import type {Agent, LatestMetrics} from '@/types'; import LittleStatCard from '@portal/components/LittleStatCard'; import CyberCard from "@portal/components/CyberCard.tsx"; diff --git a/web/src/portal/components/server/SystemInfoSection.tsx b/web/src/portal/components/server/SystemInfoSection.tsx index 5a392b0..cea11ea 100644 --- a/web/src/portal/components/server/SystemInfoSection.tsx +++ b/web/src/portal/components/server/SystemInfoSection.tsx @@ -2,7 +2,7 @@ import {Cpu, HardDrive, MemoryStick, Network} from 'lucide-react'; import {InfoGrid} from '@portal/components/server/InfoGrid'; import {SnapshotSection} from '@portal/components/server/SnapshotSection'; import type {SnapshotCardData} from '@portal/components/server/SnapshotGrid'; -import {formatBytes, formatDateTime, formatPercentValue, formatUptime} from '@portal/utils/util'; +import {formatBytes, formatDateTime, formatPercentValue, formatUptime} from '@/lib/format.ts'; import type {Agent, LatestMetrics} from '@/types'; import CyberCard from "@portal/components/CyberCard.tsx"; @@ -50,6 +50,7 @@ export const SystemInfoSection = ({agent, latestMetrics}: SystemInfoSectionProps {label: '最近心跳', value: lastSeenDisplay}, {label: '进程数', value: latestMetrics?.host?.procs ?? '-'}, {label: '网络累计', value: networkSummary}, + {label: 'Load', value: `${latestMetrics?.host?.load1?.toFixed(2)} / ${latestMetrics?.host?.load5?.toFixed(2)} / ${latestMetrics?.host?.load15?.toFixed(2)}`}, ]; // 快照卡片 diff --git a/web/src/portal/components/server/TemperatureChart.tsx b/web/src/portal/components/server/TemperatureChart.tsx index 85d26f7..f1cdc92 100644 --- a/web/src/portal/components/server/TemperatureChart.tsx +++ b/web/src/portal/components/server/TemperatureChart.tsx @@ -6,7 +6,7 @@ import {CustomTooltip} from '@portal/components/CustomTooltip'; import {useMetricsQuery} from '@portal/hooks/server'; import {TEMPERATURE_COLORS} from '@portal/constants/server'; import {ChartContainer} from './ChartContainer'; -import {formatChartTime} from '@portal/utils/util'; +import {formatChartTime} from '@/lib/format.ts'; interface TemperatureChartProps { agentId: string; diff --git a/web/src/portal/pages/ServerDetail.tsx b/web/src/portal/pages/ServerDetail.tsx index 2562106..c93f879 100644 --- a/web/src/portal/pages/ServerDetail.tsx +++ b/web/src/portal/pages/ServerDetail.tsx @@ -48,6 +48,7 @@ const ServerDetail = () => { const formatLoad = (value?: number) => ( typeof value === 'number' && Number.isFinite(value) ? value.toFixed(2) : '-' ); + const deviceIpInterfaces = (latestMetrics?.networkInterfaces || []) .map((netInterface) => ({ name: netInterface.interface, @@ -87,18 +88,6 @@ const ServerDetail = () => { {/* 系统信息 */} - {/* 系统负载 */} - -
- - - -
-
- {/* 历史趋势图表 */} { />
+ {/* 流量统计卡片 */} + {agents.filter(a => a.trafficStats?.enabled).length > 0 && ( +
+
+ + 流量统计 + ({agents.filter(a => a.trafficStats?.enabled).length} 台设备已启用) +
+
+ {agents.filter(a => a.trafficStats?.enabled).map(agent => { + const traffic = agent.trafficStats!; + const usagePercent = traffic.limit > 0 ? Math.min(100, (traffic.used / traffic.limit) * 100) : 0; + const getProgressColor = (percent: number) => { + if (percent >= 100) return 'bg-red-500'; + if (percent >= 90) return 'bg-orange-500'; + if (percent >= 80) return 'bg-yellow-500'; + return 'bg-emerald-500'; + }; + const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; + }; + + return ( +
handleNavigate(agent.id)} + className="bg-white dark:bg-black/40 border border-slate-200 dark:border-cyan-900/30 rounded-lg p-3 hover:border-cyan-500 dark:hover:border-cyan-500 transition-all cursor-pointer group" + > +
+
+
+ {agent.name} +
+
+ {traffic.type === 'recv' ? '进站' : traffic.type === 'send' ? '出站' : '全部'}流量 +
+
+
+ + {traffic.limit > 0 ? ( + <> +
+ + {formatBytes(traffic.used)} / {formatBytes(traffic.limit)} + + + {usagePercent.toFixed(1)}% + +
+
+
+
+
+ 重置日期: 每月 {traffic.resetDay} 号 +
+ + ) : ( +
+ 已使用: {formatBytes(traffic.used)} +
仅统计模式
+
+ )} +
+ ); + })} +
+
+ )} + {/* 标签过滤器 */} {allTags.length > 1 && (
@@ -377,20 +453,6 @@ const ServerList = () => { {formatSpeed(upload)}
- {server.traffic?.enabled && ( -
-
- TRAFFIC - {Math.round((server.traffic?.used || 0) / server.traffic?.limit * 100)}% -
-
-
-
-
- )} {/* Connections */} diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 23373fd..37e7aa2 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -34,18 +34,11 @@ export interface Agent { createdAt?: string; updatedAt?: string; // 流量统计相关字段 - traffic?: TrafficData; // 流量 trafficStats?: TrafficStatsData; // 流量统计配置 tamperProtectConfig?: TamperProtectConfig; // 防篡改保护配置 sshLoginConfig?: SSHLoginConfigData; // SSH登录监控配置 } -export interface TrafficData { - enabled: boolean; - limit?: number; - used?: number; -} - export interface TrafficStatsData { enabled: boolean; type: string; // 统计类型: recv进站/send出站/both全部 @@ -53,6 +46,8 @@ export interface TrafficStatsData { used: number; // 当前周期已使用流量(字节) resetDay: number; // 流量重置日期(1-31), 0表示不自动重置 periodStart: number; // 当前周期开始时间(时间戳毫秒) + periodEnd: number; // 当前周期结束时间(时间戳毫秒) + daysUntilReset: number; // 距离重置的天数 baselineRecv: number; // 当前周期流量基线(接收) baselineSend: number; // 当前周期流量基线(发送) alertSent80: boolean;