import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useReactTable, getCoreRowModel, flexRender, type ColumnDef, type SortingState, type RowData, } from '@tanstack/react-table'; import { adminTrafficApi, type UserTrafficItem, type TrafficNodeInfo, type TrafficUsageResponse, type TrafficParams, type TrafficEnrichmentData, } from '../api/adminTraffic'; import { usePlatform } from '../platform/hooks/usePlatform'; // ============ TanStack Table module augmentation ============ declare module '@tanstack/react-table' { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { sticky?: boolean; align?: 'left' | 'center'; bold?: boolean; } } // ============ Utils ============ const formatBytes = (bytes: number): string => { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getFlagEmoji = (countryCode: string): string => { if (!countryCode || countryCode.length !== 2) return ''; const codePoints = countryCode .toUpperCase() .split('') .map((char) => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); }; const formatCurrency = (kopeks: number): string => { const rubles = kopeks / 100; if (rubles === 0) return '0'; if (rubles < 10) return rubles.toFixed(2); if (rubles < 1000) return Math.round(rubles).toString(); return `${(rubles / 1000).toFixed(1)}k`; }; const formatShortDate = (iso: string | null): string => { if (!iso) return '\u2014'; const d = new Date(iso); if (isNaN(d.getTime())) return '\u2014'; return `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getFullYear()).slice(2)}`; }; const toBackendSortField = (columnId: string): string => { if (columnId === 'user') return 'full_name'; return columnId; }; // ============ Risk assessment helpers ============ const bytesToGbPerDay = (bytes: number, days: number): number => days > 0 ? bytes / days / 1024 ** 3 : 0; const getRatio = (gbPerDay: number, threshold: number): number => threshold > 0 ? gbPerDay / threshold : 0; const getRowBgColor = (ratio: number): string | undefined => { if (ratio <= 0) return undefined; const clamped = Math.min(ratio, 1.5); const hue = 120 - Math.min(clamped, 1) * 120; const opacity = clamped <= 1 ? 0.06 + clamped * 0.07 : 0.13 + (clamped - 1) * 0.14; return `hsla(${hue}, 70%, 45%, ${opacity})`; }; const getNodeTextColor = (ratio: number): string => { const clamped = Math.min(Math.max(ratio, 0), 1.5); let hue: number; if (clamped <= 0.7) { hue = 210 - (clamped / 0.7) * 180; // 210 (blue) → 30 (amber) } else { hue = Math.max(0, 30 - ((clamped - 0.7) / 0.8) * 30); // 30 (amber) → 0 (red) } const saturation = 70 + clamped * 15; const lightness = 65 - clamped * 10; return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }; type RiskLevel = 'low' | 'medium' | 'high' | 'critical'; const getRiskLevel = (ratio: number): RiskLevel => { if (ratio < 0.5) return 'low'; if (ratio < 0.8) return 'medium'; if (ratio < 1.2) return 'high'; return 'critical'; }; 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, ): RiskResult => { const dailyTotal = bytesToGbPerDay(row.total_bytes, days); const totalR = totalThreshold > 0 ? getRatio(dailyTotal, totalThreshold) : 0; 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', 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 ============ const SearchIcon = () => ( ); const ChevronLeftIcon = () => ( ); const ChevronRightIcon = () => ( ); const RefreshIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( ); const DownloadIcon = () => ( ); const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) => ( {direction === 'asc' ? ( ) : direction === 'desc' ? ( ) : ( )} ); const FilterIcon = () => ( ); const ChevronDownIcon = () => ( ); const ServerIcon = () => ( ); const CalendarIcon = () => ( ); const XIcon = () => ( ); const StatusIcon = () => ( ); const GlobeIcon = () => ( ); const ShieldIcon = () => ( ); const ServerSmallIcon = () => ( ); // ============ Progress Bar ============ function ProgressBar({ loading }: { loading: boolean }) { const [progress, setProgress] = useState(0); const [visible, setVisible] = useState(false); const intervalRef = useRef>(undefined); useEffect(() => { if (loading) { setProgress(0); setVisible(true); // Fast initial progress, then slow down intervalRef.current = setInterval(() => { setProgress((prev) => { if (prev < 30) return prev + 8; if (prev < 60) return prev + 3; if (prev < 85) return prev + 1; if (prev < 95) return prev + 0.3; return prev; }); }, 100); } else { if (visible) { setProgress(100); clearInterval(intervalRef.current); const timer = setTimeout(() => { setVisible(false); setProgress(0); }, 300); return () => clearTimeout(timer); } } return () => clearInterval(intervalRef.current); }, [loading, visible]); if (!visible) return null; return (
); } // ============ Components ============ const PERIODS = [1, 3, 7, 14, 30] as const; function PeriodSelector({ value, onChange, label, dateMode, customStart, customEnd, onToggleDateMode, onCustomStartChange, onCustomEndChange, }: { value: number; onChange: (v: number) => void; label: string; dateMode: boolean; customStart: string; customEnd: string; onToggleDateMode: () => void; onCustomStartChange: (v: string) => void; onCustomEndChange: (v: string) => void; }) { const { t } = useTranslation(); // Limit: last 31 days const today = new Date().toISOString().split('T')[0]; const minDate = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; if (dateMode) { return (
{t('admin.trafficUsage.dateFrom')} onCustomStartChange(e.target.value)} className="rounded-lg border border-dark-700 bg-dark-800 px-2 py-1 text-xs text-dark-200 focus:border-dark-600 focus:outline-none" /> {t('admin.trafficUsage.dateTo')} onCustomEndChange(e.target.value)} className="rounded-lg border border-dark-700 bg-dark-800 px-2 py-1 text-xs text-dark-200 focus:border-dark-600 focus:outline-none" />
); } return (
{label}
{PERIODS.map((p) => ( ))}
); } function TariffFilter({ available, selected, onChange, }: { available: string[]; selected: Set; onChange: (next: Set) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); if (available.length === 0) return null; const allSelected = selected.size === 0; const activeCount = selected.size; const toggle = (tariff: string) => { const next = new Set(selected); if (next.has(tariff)) { next.delete(tariff); } else { next.add(tariff); } onChange(next); }; const selectAll = () => onChange(new Set()); return (
{open && (
{available.map((tariff) => { const checked = selected.has(tariff); return ( ); })}
)}
); } const STATUS_COLORS: Record = { active: 'bg-success-500', trial: 'bg-warning-500', expired: 'bg-error-500', disabled: 'bg-dark-500', }; function StatusFilter({ available, selected, onChange, }: { available: string[]; selected: Set; onChange: (next: Set) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); if (available.length === 0) return null; const allSelected = selected.size === 0; const activeCount = selected.size; const toggle = (status: string) => { const next = new Set(selected); if (next.has(status)) { next.delete(status); } else { next.add(status); } onChange(next); }; const selectAll = () => onChange(new Set()); const statusLabel = (s: string) => { const key = `admin.trafficUsage.status${s.charAt(0).toUpperCase() + s.slice(1)}`; return t(key, s); }; return (
{open && (
{available.map((s) => { const checked = selected.has(s); return ( ); })}
)}
); } function NodeFilter({ available, selected, onChange, }: { available: TrafficNodeInfo[]; selected: Set; onChange: (next: Set) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); if (available.length === 0) return null; const allSelected = selected.size === 0; const activeCount = selected.size; const toggle = (uuid: string) => { const next = new Set(selected); if (next.has(uuid)) { next.delete(uuid); } else { next.add(uuid); } onChange(next); }; const selectAll = () => onChange(new Set()); return (
{open && (
{available.map((node) => { const checked = selected.has(node.node_uuid); return ( ); })}
)}
); } function CountryFilter({ available, selected, onChange, }: { available: { code: string; count: number }[]; selected: Set; onChange: (next: Set) => void; }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); if (available.length === 0) return null; const allSelected = selected.size === 0; const activeCount = selected.size; const toggle = (code: string) => { const next = new Set(selected); if (next.has(code)) { next.delete(code); } else { next.add(code); } onChange(next); }; const selectAll = () => onChange(new Set()); return (
{open && (
{available.map(({ code, count }) => { const checked = selected.has(code); return ( ); })}
)}
); } // ============ Risk Badge ============ function RiskBadge({ level, ratio, gbPerDay, }: { level: RiskLevel; ratio: number; gbPerDay: number; }) { const style = RISK_STYLES[level]; const barWidth = Math.min(ratio * 100, 100); return (
{formatGbPerDay(gbPerDay)} GB/d
{/* Mini progress bar showing ratio to threshold */}
); } // ============ Main Page ============ export default function AdminTrafficUsage() { const { t } = useTranslation(); const navigate = useNavigate(); const { capabilities } = usePlatform(); const [items, setItems] = useState([]); const [nodes, setNodes] = useState([]); const [availableTariffs, setAvailableTariffs] = useState([]); const [availableStatuses, setAvailableStatuses] = useState([]); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [period, setPeriod] = useState(30); const [dateMode, setDateMode] = useState(false); const [customStart, setCustomStart] = useState(''); const [customEnd, setCustomEnd] = useState(''); const [searchInput, setSearchInput] = useState(''); const [committedSearch, setCommittedSearch] = useState(''); const [selectedTariffs, setSelectedTariffs] = useState>(new Set()); const [selectedStatuses, setSelectedStatuses] = useState>(new Set()); const [selectedNodes, setSelectedNodes] = useState>(new Set()); const [selectedCountries, setSelectedCountries] = useState>(new Set()); const [offset, setOffset] = useState(0); const [total, setTotal] = useState(0); const [enrichment, setEnrichment] = useState | null>(null); const [enrichmentLoading, setEnrichmentLoading] = useState(false); const [exporting, setExporting] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const [sorting, setSorting] = useState([{ id: 'total_bytes', desc: true }]); const [columnSizing, setColumnSizing] = useState>({}); const [totalThreshold, setTotalThreshold] = useState(''); const [nodeThreshold, setNodeThreshold] = useState(''); const [periodDays, setPeriodDays] = useState(30); const limit = 50; const hasData = items.length > 0 || nodes.length > 0; const sortBy = sorting[0] ? toBackendSortField(sorting[0].id) : 'total_bytes'; const sortDesc = sorting[0]?.desc ?? true; const tariffsParam = selectedTariffs.size > 0 ? [...selectedTariffs].join(',') : undefined; const statusesParam = selectedStatuses.size > 0 ? [...selectedStatuses].join(',') : undefined; // Merge country filter into node UUIDs so backend filters data consistently const mergedNodesParam = useMemo(() => { const countryUuids = selectedCountries.size > 0 ? new Set( nodes.filter((n) => selectedCountries.has(n.country_code)).map((n) => n.node_uuid), ) : null; const nodeUuids = selectedNodes.size > 0 ? new Set(selectedNodes) : null; let merged: Set | null = null; if (countryUuids && nodeUuids) { merged = new Set([...countryUuids].filter((id) => nodeUuids.has(id))); } else { merged = countryUuids || nodeUuids; } return merged && merged.size > 0 ? [...merged].join(',') : undefined; }, [nodes, selectedCountries, selectedNodes]); const buildParams = useCallback((): TrafficParams => { const params: TrafficParams = { limit, offset, search: committedSearch || undefined, sort_by: sortBy, sort_desc: sortDesc, tariffs: tariffsParam, statuses: statusesParam, nodes: mergedNodesParam, }; if (dateMode && customStart && customEnd) { params.start_date = customStart; params.end_date = customEnd; } else { params.period = period; } return params; }, [ period, offset, committedSearch, sortBy, sortDesc, tariffsParam, statusesParam, mergedNodesParam, dateMode, customStart, customEnd, ]); const applyData = useCallback((data: TrafficUsageResponse) => { setItems(data.items); setNodes(data.nodes); setTotal(data.total); setAvailableTariffs(data.available_tariffs); setAvailableStatuses(data.available_statuses); setPeriodDays(data.period_days); }, []); const loadData = useCallback( async (skipCache = false) => { const params = buildParams(); // Check cache first — apply instantly without any loading state if (!skipCache) { const cached = adminTrafficApi.getCached(params); if (cached) { applyData(cached); setInitialLoading(false); return; } } try { setLoading(true); const data = await adminTrafficApi.getTrafficUsage(params, { skipCache }); applyData(data); } catch { // silently fail — keep stale data visible } finally { setLoading(false); setInitialLoading(false); } }, [buildParams, applyData], ); // Load on param change useEffect(() => { loadData(); }, [loadData]); // Load enrichment after main data arrives useEffect(() => { if (initialLoading || items.length === 0) return; let cancelled = false; const load = async () => { setEnrichmentLoading(true); try { const res = await adminTrafficApi.getEnrichment(); if (!cancelled) setEnrichment(res.data); } catch { // silently fail — enrichment is optional } finally { if (!cancelled) setEnrichmentLoading(false); } }; load(); return () => { cancelled = true; }; }, [initialLoading, items.length]); // Prefetch adjacent periods in background (only in period mode) useEffect(() => { if (dateMode) return; const prefetchPeriods = PERIODS.filter((p) => p !== period); const timer = setTimeout(() => { prefetchPeriods.forEach((p) => { const params: TrafficParams = { period: p, limit, offset: 0, sort_by: 'total_bytes', sort_desc: true, }; if (!adminTrafficApi.getCached(params)) { adminTrafficApi.getTrafficUsage(params).catch(() => {}); } }); }, 500); return () => clearTimeout(timer); }, [period, dateMode]); useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setOffset(0); setCommittedSearch(searchInput); }; const handleExport = async () => { try { setExporting(true); const exportData: { period: number; start_date?: string; end_date?: string; tariffs?: string; statuses?: string; nodes?: string; total_threshold_gb?: number; node_threshold_gb?: number; } = { period }; if (dateMode && customStart && customEnd) { exportData.start_date = customStart; exportData.end_date = customEnd; } if (tariffsParam) exportData.tariffs = tariffsParam; if (statusesParam) exportData.statuses = statusesParam; if (mergedNodesParam) exportData.nodes = mergedNodesParam; if (totalThresholdNum > 0) exportData.total_threshold_gb = totalThresholdNum; if (nodeThresholdNum > 0) exportData.node_threshold_gb = nodeThresholdNum; await adminTrafficApi.exportCsv(exportData); setToast({ message: t('admin.trafficUsage.exportSuccess'), type: 'success' }); } catch { setToast({ message: t('admin.trafficUsage.exportError'), type: 'error' }); } finally { setExporting(false); } }; const handlePeriodChange = (p: number) => { setPeriod(p); setOffset(0); }; const handleToggleDateMode = () => { if (dateMode) { // Switch back to period mode setDateMode(false); setCustomStart(''); setCustomEnd(''); setOffset(0); } else { // Switch to date mode — pre-fill with last N days const end = new Date(); const start = new Date(end.getTime() - period * 24 * 60 * 60 * 1000); setCustomStart(start.toISOString().split('T')[0]); setCustomEnd(end.toISOString().split('T')[0]); setDateMode(true); setOffset(0); } }; const handleCustomStartChange = (v: string) => { setCustomStart(v); setOffset(0); }; const handleCustomEndChange = (v: string) => { setCustomEnd(v); setOffset(0); }; const handleSortingChange = (updater: SortingState | ((old: SortingState) => SortingState)) => { const next = typeof updater === 'function' ? updater(sorting) : updater; setSorting(next); setOffset(0); }; const handleTariffChange = (next: Set) => { setSelectedTariffs(next); setOffset(0); }; const handleStatusChange = (next: Set) => { setSelectedStatuses(next); setOffset(0); }; const handleNodeChange = (next: Set) => { setSelectedNodes(next); setOffset(0); }; const handleCountryChange = (next: Set) => { setSelectedCountries(next); setOffset(0); }; const handleRefresh = () => { loadData(true); setEnrichment(null); setEnrichmentLoading(true); adminTrafficApi .getEnrichment({ skipCache: true }) .then((res) => setEnrichment(res.data)) .catch(() => {}) .finally(() => setEnrichmentLoading(false)); }; const availableCountries = useMemo(() => { const map = new Map(); for (const n of nodes) { if (n.country_code) map.set(n.country_code, (map.get(n.country_code) || 0) + 1); } return Array.from(map.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([code, count]) => ({ code, count })); }, [nodes]); // When country/node filter is active, show only matching node columns const displayNodes = useMemo(() => { let filtered = nodes; if (selectedCountries.size > 0) { filtered = filtered.filter((n) => selectedCountries.has(n.country_code)); } if (selectedNodes.size > 0) { filtered = filtered.filter((n) => selectedNodes.has(n.node_uuid)); } return filtered; }, [nodes, selectedCountries, selectedNodes]); const totalThresholdNum = Math.max(0, parseFloat(totalThreshold) || 0); const hasTotalThreshold = totalThresholdNum > 0; const nodeThresholdNum = Math.max(0, parseFloat(nodeThreshold) || 0); const hasNodeThreshold = nodeThresholdNum > 0; const hasAnyThreshold = hasTotalThreshold || hasNodeThreshold; const columns = useMemo[]>(() => { const cols: ColumnDef[] = [ { id: 'user', accessorFn: (row) => row.full_name, header: t('admin.trafficUsage.user'), enableSorting: true, size: 120, minSize: 40, maxSize: 200, cell: ({ row }) => { const item = row.original; return (
{item.full_name?.[0] || '?'}
{item.full_name}
{item.username ? (
@{item.username}
) : item.email ? (
{item.email}
) : null}
); }, meta: { sticky: true }, }, { accessorKey: 'tariff_name', header: t('admin.trafficUsage.tariff'), enableSorting: true, size: 120, minSize: 80, cell: ({ getValue }) => ( {(getValue() as string | null) || t('admin.trafficUsage.noTariff')} ), }, { accessorKey: 'device_limit', header: t('admin.trafficUsage.devices'), enableSorting: true, size: 80, minSize: 60, meta: { align: 'center' as const }, cell: ({ getValue }) => ( {getValue() as number} ), }, { accessorKey: 'traffic_limit_gb', header: t('admin.trafficUsage.trafficLimit'), enableSorting: true, size: 80, minSize: 60, meta: { align: 'center' as const }, cell: ({ getValue }) => { const gb = getValue() as number; return {gb > 0 ? `${gb} GB` : '\u221E'}; }, }, // ---- Enrichment columns ---- { id: 'connected', header: t('admin.trafficUsage.connected'), size: 65, minSize: 50, enableSorting: true, meta: { align: 'center' as const }, cell: ({ row }) => { const e = enrichment?.[row.original.user_id]; if (enrichmentLoading && !enrichment) return
; return {e?.devices_connected ?? '\u2014'}; }, }, { id: 'total_spent', header: t('admin.trafficUsage.totalSpent'), size: 75, minSize: 55, enableSorting: true, meta: { align: 'center' as const }, cell: ({ row }) => { const e = enrichment?.[row.original.user_id]; if (enrichmentLoading && !enrichment) return
; if (!e || e.total_spent_kopeks === 0) return {'\u2014'}; return ( {formatCurrency(e.total_spent_kopeks)} ); }, }, { id: 'sub_start', header: t('admin.trafficUsage.subStart'), size: 80, minSize: 65, enableSorting: true, meta: { align: 'center' as const }, cell: ({ row }) => { const e = enrichment?.[row.original.user_id]; if (enrichmentLoading && !enrichment) return
; return ( {formatShortDate(e?.subscription_start_date ?? null)} ); }, }, { id: 'sub_end', header: t('admin.trafficUsage.subEnd'), size: 80, minSize: 65, enableSorting: true, meta: { align: 'center' as const }, cell: ({ row }) => { const e = enrichment?.[row.original.user_id]; if (enrichmentLoading && !enrichment) return
; return ( {formatShortDate(e?.subscription_end_date ?? null)} ); }, }, { id: 'last_node', header: t('admin.trafficUsage.lastNode'), size: 100, minSize: 70, enableSorting: true, meta: { align: 'center' as const }, cell: ({ row }) => { const e = enrichment?.[row.original.user_id]; if (enrichmentLoading && !enrichment) return
; return {e?.last_node_name ?? '\u2014'}; }, }, // ---- Dynamic node columns ---- ...displayNodes.map( (node): ColumnDef => ({ id: `node_${node.node_uuid}`, accessorFn: (row) => row.node_traffic[node.node_uuid] || 0, header: `${getFlagEmoji(node.country_code)} ${node.node_name}`, enableSorting: true, size: 110, minSize: 80, meta: { align: 'center' as const }, cell: ({ getValue }) => { const bytes = getValue() as number; if (bytes <= 0) { return {'\u2014'}; } const dailyNode = bytesToGbPerDay(bytes, periodDays); const nodeRatio = hasNodeThreshold ? getRatio(dailyNode, nodeThresholdNum) : 0; const textColor = hasNodeThreshold ? getNodeTextColor(nodeRatio) : undefined; return (
0.8 ? 600 : undefined, }} > {formatBytes(bytes)} {hasNodeThreshold && ( {formatGbPerDay(dailyNode)} GB/d )}
); }, }), ), ]; // Risk column — insert before total when any threshold is set if (hasAnyThreshold) { cols.push({ id: 'risk', header: t('admin.trafficUsage.risk'), size: 100, minSize: 80, meta: { align: 'center' as const }, accessorFn: (row) => { const result = getCompositeRisk(row, totalThresholdNum, nodeThresholdNum, periodDays); return result.ratio; }, enableSorting: false, cell: ({ row }) => { const result = getCompositeRisk( row.original, totalThresholdNum, nodeThresholdNum, periodDays, ); const level = getRiskLevel(result.ratio); return ; }, }); } cols.push({ accessorKey: 'total_bytes', header: t('admin.trafficUsage.total'), enableSorting: true, size: 110, minSize: 80, 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 (
{formatBytes(bytes)} {hasTotalThreshold && ( {formatGbPerDay(dailyTotal)} GB/d )}
); }, }); return cols; }, [ displayNodes, t, hasAnyThreshold, hasTotalThreshold, hasNodeThreshold, totalThresholdNum, nodeThresholdNum, periodDays, enrichment, enrichmentLoading, ]); const table = useReactTable({ data: items, columns, state: { sorting, columnSizing }, onSortingChange: handleSortingChange, onColumnSizingChange: setColumnSizing, getCoreRowModel: getCoreRowModel(), manualSorting: true, enableSortingRemoval: false, enableColumnResizing: true, columnResizeMode: 'onChange', }); const totalPages = Math.ceil(total / limit); const currentPage = Math.floor(offset / limit) + 1; return (
{/* Progress bar — shown during background refresh */} {/* Toast */} {toast && (
{toast.message}
)} {/* Header */}
{!capabilities.hasBackButton && ( )}

{t('admin.trafficUsage.title')}

{t('admin.trafficUsage.subtitle')}

{/* Controls */}
{/* Threshold inputs */}
setTotalThreshold(e.target.value)} placeholder={t('admin.trafficUsage.totalThreshold')} step="0.1" min="0" max="9999" className="w-20 bg-transparent text-xs text-dark-200 placeholder-dark-500 [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" /> {totalThreshold && ( )}
setNodeThreshold(e.target.value)} placeholder={t('admin.trafficUsage.nodeThreshold')} step="0.1" min="0" max="9999" className="w-20 bg-transparent text-xs text-dark-200 placeholder-dark-500 [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" /> {nodeThreshold && ( )}
setSearchInput(e.target.value)} placeholder={t('admin.trafficUsage.search')} className="w-full rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-4 text-dark-100 placeholder-dark-500 focus:border-dark-600 focus:outline-none" />
{/* Table */} {initialLoading && !hasData ? (
) : !hasData && !loading ? (
{t('admin.trafficUsage.noData')}
) : (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = header.column.columnDef.meta; const isSticky = meta?.sticky; const align = meta?.align === 'center' ? 'text-center' : 'text-left'; const isBold = meta?.bold; return ( ); })} ))} {table.getRowModel().rows.map((row) => { const compositeRatio = hasAnyThreshold ? getCompositeRisk( row.original, totalThresholdNum, nodeThresholdNum, periodDays, ).ratio : 0; const rowBg = hasAnyThreshold ? getRowBgColor(compositeRatio) : undefined; return ( navigate(`/admin/users/${row.original.user_id}`)} > {row.getVisibleCells().map((cell) => { const meta = cell.column.columnDef.meta; const isSticky = meta?.sticky; const align = meta?.align === 'center' ? 'text-center' : 'text-left'; return ( ); })} ); })}
{flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanSort() && ( )}
e.stopPropagation()} className="absolute -right-2 top-0 z-20 h-full w-5 cursor-col-resize select-none" style={{ touchAction: 'none' }} >
{flexRender(cell.column.columnDef.cell, cell.getContext())}
)} {/* Pagination */} {totalPages > 1 && (
{offset + 1} {'\u2013'} {Math.min(offset + limit, total)} / {total}
{currentPage} / {totalPages}
)}
); }