From ff3dbfaa25c0a3bb08d2e16344b024fa659fcfab Mon Sep 17 00:00:00 2001 From: c0mrade Date: Sun, 31 May 2026 19:24:44 +0300 Subject: [PATCH] feat(admin-remnawave): auto-refresh overview, responsive mobile grids, wide sub-requests - Auto-refresh the recap/devices/top-consumers/subscription-requests widgets on a 60s timer (system + health already refreshed) so the page updates itself - Mobile: a lone trailing stat card now spans the full row (no empty gap when the 2-column grid reflows) - Subscription requests: full-width card with a two-column breakdown and a total count, instead of a half-empty narrow card --- src/pages/AdminRemnawave.tsx | 64 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/pages/AdminRemnawave.tsx b/src/pages/AdminRemnawave.tsx index a4e7d7f..ea66de4 100644 --- a/src/pages/AdminRemnawave.tsx +++ b/src/pages/AdminRemnawave.tsx @@ -36,13 +36,7 @@ import { BackIcon, ChevronRightIcon, } from '../components/icons'; -import { - PiCpuDuotone, - PiMemoryDuotone, - PiTimerDuotone, - PiDevicesDuotone, - PiPulseDuotone, -} from 'react-icons/pi'; +import { PiCpu, PiMemory, PiTimer, PiDevices, PiPulse } from 'react-icons/pi'; const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; @@ -294,16 +288,18 @@ const formatUptimeSince = (iso: string): string => { function BreakdownCard({ title, items, + wide = false, }: { title: string; items: { label: string; count: number }[]; + wide?: boolean; }) { const max = Math.max(1, ...items.map((i) => i.count)); return (

{title}

-
- {items.slice(0, 8).map((it) => ( +
+ {items.slice(0, wide ? 16 : 8).map((it) => (
{it.label} @@ -377,7 +373,7 @@ function OverviewTab({ {t('admin.remnawave.overview.system', 'System')} -
+
{t('admin.remnawave.overview.bandwidth', 'Inbound Traffic')} -
+
{t('admin.remnawave.overview.server', 'Server')} -
+
} + icon={} color="accent" /> } + icon={} color={memoryUsedPercent > 80 ? 'red' : memoryUsedPercent > 60 ? 'orange' : 'green'} /> } + icon={} color="blue" />
@@ -468,7 +464,7 @@ function OverviewTab({ {t('admin.remnawave.overview.traffic', 'Traffic Statistics')} -
+
{t('admin.remnawave.overview.usersByStatus', 'Users by Status')} -
+
{Object.entries(stats.users_by_status).map(([status, count]) => ( {t('admin.remnawave.overview.panel', 'Панель')} -
+
0 || devicesStats.by_app.length > 0) && (

- + {t('admin.remnawave.overview.devices', 'Устройства')} ·{' '} {devicesStats.total_hwid_devices} ({devicesStats.average_devices_per_user.toFixed(1)}/ {t('admin.remnawave.overview.perUser', 'юзер')}) @@ -629,11 +625,11 @@ function OverviewTab({ {t('admin.remnawave.overview.panelHealth', 'Здоровье панели')} {health.instances > 1 ? ` · ${health.instances}` : ''}

-
+
} + icon={} color="blue" /> } + icon={} color={health.event_loop_p99_ms > 50 ? 'red' : 'green'} /> } + icon={} color="accent" />
@@ -664,14 +660,14 @@ function OverviewTab({

- {t('admin.remnawave.overview.subRequests', 'Запросы подписки (по клиентам)')} + {t('admin.remnawave.overview.subRequests', 'Запросы подписки (по клиентам)')} ·{' '} + {subRequests.by_app.reduce((acc, a) => acc + a.count, 0)}

-
- ({ label: a.app, count: a.count }))} - /> -
+ ({ label: a.app, count: a.count }))} + />
)}
@@ -719,7 +715,7 @@ function NodesTab({ return (
{/* Stats */} -
+
{/* Stats */} -
+
adminRemnawaveApi.getTopConsumers(7, 10), enabled: activeTab === 'overview', + refetchInterval: 60000, staleTime: 60000, }); @@ -1222,6 +1221,7 @@ export default function AdminRemnawave() { queryKey: ['admin-remnawave-sub-requests'], queryFn: adminRemnawaveApi.getSubscriptionRequests, enabled: activeTab === 'overview', + refetchInterval: 60000, staleTime: 60000, });