diff --git a/README.md b/README.md index 308b3f2..3810117 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface. -![Version](https://img.shields.io/badge/version-0.13.1_Beta-blue) +![Version](https://img.shields.io/badge/version-0.13.2_Beta-blue) ![License](https://img.shields.io/badge/license-AGPLv3-blue) ![Docker](https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white) diff --git a/backend/package.json b/backend/package.json index 2c0e839..a3d8f58 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.13.1-beta", + "version": "0.13.2-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/backend/src/routes/trafficAnalytics.ts b/backend/src/routes/trafficAnalytics.ts index 82c6cdf..12d893e 100644 --- a/backend/src/routes/trafficAnalytics.ts +++ b/backend/src/routes/trafficAnalytics.ts @@ -8,7 +8,7 @@ const router = Router(); router.use(requireAuth); function rangeToFlux(range: string): string { - const allowed = ['1h', '3h', '6h', '12h', '24h', '7d', '30d']; + const allowed = ['1h', '2h', '3h', '6h', '12h', '24h', '7d', '30d']; return allowed.includes(range) ? range : '24h'; } @@ -16,7 +16,7 @@ function rangeToFlux(range: string): string { // written every 60s by the collector flush). function windowForRange(range: string): string { const map: Record = { - '1h': '1m', '3h': '5m', '6h': '5m', '12h': '10m', + '1h': '1m', '2h': '2m', '3h': '5m', '6h': '5m', '12h': '10m', '24h': '15m', '7d': '1h', '30d': '6h', }; return map[range] || '15m'; diff --git a/frontend/package.json b/frontend/package.json index f5747c8..d3b3762 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.13.1-beta", + "version": "0.13.2-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/ClientDetailPage.tsx b/frontend/src/pages/ClientDetailPage.tsx index 3be74f2..3f65867 100644 --- a/frontend/src/pages/ClientDetailPage.tsx +++ b/frontend/src/pages/ClientDetailPage.tsx @@ -589,7 +589,7 @@ const APP_COLORS = [ ]; function AppTrafficCard({ mac }: { mac: string }) { - const [range, setRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h'); + const [range, setRange] = useState<'2h' | '24h' | '7d'>('24h'); const { data, isLoading } = useQuery({ queryKey: ['client-app-traffic', mac, range], @@ -600,22 +600,27 @@ function AppTrafficCard({ mac }: { mac: string }) { const series = data?.series ?? []; const apps = data?.apps ?? []; - // NetFlow data only exists for clients on networks with export enabled — - // hide the card entirely rather than showing a permanently empty chart. - if (!isLoading && series.length === 0 && apps.length === 0) return null; + // NetFlow data only exists for clients on networks with export enabled. Latch + // on the first non-empty result so the card stays mounted once it has shown + // data — otherwise switching to a range with no traffic (e.g. an idle client + // over the last 2h) would unmount the whole card, range buttons and all. + // Setting state during render (guarded) is React's recommended way to derive + // sticky state without an effect. + const [hadData, setHadData] = useState(false); + if (!hadData && (series.length > 0 || apps.length > 0)) setHadData(true); + if (!isLoading && series.length === 0 && apps.length === 0 && !hadData) return null; const ranges = [ - { value: '1h', label: '1h' }, + { value: '2h', label: '2h' }, { value: '24h', label: '24h' }, { value: '7d', label: '7d' }, - { value: '30d', label: '30d' }, ] as const; const chartData = series.map(p => ({ time: p.time, Upload: p.upload, Download: p.download, - label: format(parseISO(p.time), range === '7d' || range === '30d' ? 'MMM d HH:mm' : 'HH:mm'), + label: format(parseISO(p.time), range === '7d' ? 'MMM d HH:mm' : 'HH:mm'), })); const totalAppBytes = apps.reduce((s, a) => s + a.bytes, 0); @@ -648,6 +653,11 @@ function AppTrafficCard({ mac }: { mac: string }) { {isLoading ? (
+ ) : chartData.length === 0 && apps.length === 0 ? ( +
+ + No traffic data yet for this range +
) : ( <> {chartData.length > 0 && ( diff --git a/frontend/src/version.ts b/frontend/src/version.ts index c16763c..2ea1723 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.13.1 Beta'; +export const APP_VERSION = 'v0.13.2 Beta';