diff --git a/src/pages/AdminTrafficUsage.tsx b/src/pages/AdminTrafficUsage.tsx index d84c568..32beb08 100644 --- a/src/pages/AdminTrafficUsage.tsx +++ b/src/pages/AdminTrafficUsage.tsx @@ -91,31 +91,74 @@ const getRiskLevel = (ratio: number): RiskLevel => { return 'critical'; }; -const getCompositeRatio = ( +interface RiskResult { + ratio: number; + gbPerDay: number; // the dominant daily value (total or worst node) + totalRatio: number; + maxNodeRatio: number; +} + +const getCompositeRisk = ( row: UserTrafficItem, totalThreshold: number, nodeThreshold: number, days: number, -): number => { +): RiskResult => { const dailyTotal = bytesToGbPerDay(row.total_bytes, days); const totalR = totalThreshold > 0 ? getRatio(dailyTotal, totalThreshold) : 0; - const nodeR = - nodeThreshold > 0 - ? Math.max( - 0, - ...Object.values(row.node_traffic).map((b) => - getRatio(bytesToGbPerDay(b || 0, days), nodeThreshold), - ), - ) - : 0; - return Math.max(totalR, nodeR); + + let maxNodeR = 0; + let worstNodeGbPerDay = 0; + if (nodeThreshold > 0) { + for (const b of Object.values(row.node_traffic)) { + const daily = bytesToGbPerDay(b || 0, days); + const r = getRatio(daily, nodeThreshold); + if (r > maxNodeR) { + maxNodeR = r; + worstNodeGbPerDay = daily; + } + } + } + + // The dominant metric determines what GB/d we show + const ratio = Math.max(totalR, maxNodeR); + const gbPerDay = totalR >= maxNodeR ? dailyTotal : worstNodeGbPerDay; + + return { ratio, gbPerDay, totalRatio: totalR, maxNodeRatio: maxNodeR }; }; -const RISK_STYLES: Record = { - low: { dot: 'bg-success-400', text: 'text-success-400' }, - medium: { dot: 'bg-warning-400', text: 'text-warning-400' }, - high: { dot: 'bg-orange-400', text: 'text-orange-400' }, - critical: { dot: 'bg-error-400 animate-pulse', text: 'text-error-400' }, +const RISK_STYLES: Record = { + low: { + dot: 'bg-success-400', + text: 'text-success-400', + bar: 'bg-success-400', + bg: 'bg-success-400/10', + }, + medium: { + dot: 'bg-warning-400', + text: 'text-warning-400', + bar: 'bg-warning-400', + bg: 'bg-warning-400/10', + }, + high: { + dot: 'bg-orange-400', + text: 'text-orange-400', + bar: 'bg-orange-400', + bg: 'bg-orange-400/10', + }, + critical: { + dot: 'bg-error-400 animate-pulse', + text: 'text-error-400', + bar: 'bg-error-400', + bg: 'bg-error-400/10', + }, +}; + +const formatGbPerDay = (gbPerDay: number): string => { + if (gbPerDay < 0.01) return '<0.01'; + if (gbPerDay < 10) return gbPerDay.toFixed(2); + if (gbPerDay < 100) return gbPerDay.toFixed(1); + return Math.round(gbPerDay).toString(); }; // ============ Icons ============ @@ -811,17 +854,34 @@ function NodeFilter({ // ============ Risk Badge ============ -function RiskBadge({ level, ratio }: { level: RiskLevel; ratio: number }) { - const { t } = useTranslation(); +function RiskBadge({ + level, + ratio, + gbPerDay, +}: { + level: RiskLevel; + ratio: number; + gbPerDay: number; +}) { const style = RISK_STYLES[level]; - const labelKey = `admin.trafficUsage.risk${level.charAt(0).toUpperCase() + level.slice(1)}`; - const pct = Math.round(ratio * 100); + const barWidth = Math.min(ratio * 100, 100); return ( -
- - {pct}% - {t(labelKey)} +
+
+ + + {formatGbPerDay(gbPerDay)} + + GB/d +
+ {/* Mini progress bar showing ratio to threshold */} +
+
+
); } @@ -1155,15 +1215,25 @@ export default function AdminTrafficUsage() { const nodeRatio = hasNodeThreshold ? getRatio(dailyNode, nodeThresholdNum) : 0; const textColor = hasNodeThreshold ? getNodeTextColor(nodeRatio) : undefined; return ( - 0.8 ? 600 : undefined, - }} - > - {formatBytes(bytes)} - +
+ 0.8 ? 600 : undefined, + }} + > + {formatBytes(bytes)} + + {hasNodeThreshold && ( + + {formatGbPerDay(dailyNode)} GB/d + + )} +
); }, }), @@ -1178,13 +1248,20 @@ export default function AdminTrafficUsage() { size: 100, minSize: 80, meta: { align: 'center' as const }, - accessorFn: (row) => - getCompositeRatio(row, totalThresholdNum, nodeThresholdNum, periodDays), + accessorFn: (row) => { + const result = getCompositeRisk(row, totalThresholdNum, nodeThresholdNum, periodDays); + return result.ratio; + }, enableSorting: false, - cell: ({ getValue }) => { - const maxRatio = getValue() as number; - const level = getRiskLevel(maxRatio); - return ; + cell: ({ row }) => { + const result = getCompositeRisk( + row.original, + totalThresholdNum, + nodeThresholdNum, + periodDays, + ); + const level = getRiskLevel(result.ratio); + return ; }, }); } @@ -1198,10 +1275,19 @@ export default function AdminTrafficUsage() { meta: { align: 'center' as const, bold: true }, cell: ({ getValue }) => { const bytes = getValue() as number; + if (bytes <= 0) { + return {'\u2014'}; + } + const dailyTotal = bytesToGbPerDay(bytes, periodDays); return ( - - {bytes > 0 ? formatBytes(bytes) : '\u2014'} - +
+ {formatBytes(bytes)} + {hasTotalThreshold && ( + + {formatGbPerDay(dailyTotal)} GB/d + + )} +
); }, }); @@ -1429,12 +1515,12 @@ export default function AdminTrafficUsage() { {table.getRowModel().rows.map((row) => { const compositeRatio = hasAnyThreshold - ? getCompositeRatio( + ? getCompositeRisk( row.original, totalThresholdNum, nodeThresholdNum, periodDays, - ) + ).ratio : 0; const rowBg = hasAnyThreshold ? getRowBgColor(compositeRatio) : undefined;