From 368b4e83b649a8ea5b6c782bd721611a9f72514d Mon Sep 17 00:00:00 2001 From: 2GT-Rich <107219997+2GT-Rich@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:31:59 -0700 Subject: [PATCH] feat(dashboard): unify Top Talkers with NetFlow traffic data (v0.13.1-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dashboard "Top talkers" card and the Traffic page previously used different data sources: the card ranked clients by device-reported since-connection byte counters (wireless-centric, reset on reconnect, no time window), while the Traffic page uses NetFlow-measured bytes. The card now prefers the NetFlow top-clients data (last 24h — identical numbers to the Traffic page) and falls back to the legacy counters only when the collector has no data, with the source labeled in the card header ("by bytes · 24h" vs "since connection"). Rows now click through to the client detail page. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- backend/package.json | 2 +- frontend/package.json | 2 +- frontend/src/pages/DashboardPage.tsx | 49 ++++++++++++++++++++++++---- frontend/src/version.ts | 2 +- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fe7f68a..308b3f2 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.0_Beta-blue) +![Version](https://img.shields.io/badge/version-0.13.1_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 d0bc0f0..2c0e839 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.13.0-beta", + "version": "0.13.1-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/frontend/package.json b/frontend/package.json index f152b4c..f5747c8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.13.0-beta", + "version": "0.13.1-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 58f34c3..abbbea8 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -10,7 +10,7 @@ import { } from 'lucide-react'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; -import { metricsApi, eventsApi, devicesApi, clientsApi } from '../services/api'; +import { metricsApi, eventsApi, devicesApi, clientsApi, trafficApi } from '../services/api'; import { useSocket } from '../hooks/useSocket'; import { useQueryClient } from '@tanstack/react-query'; import { format, formatDistanceToNow } from 'date-fns'; @@ -259,7 +259,7 @@ function HealthBar({ // eslint-disable-next-line @typescript-eslint/no-explicit-any function SummaryView(props: Record) { const { summary, devices, wirelessCount, clientSparkline, clientsOverTime, - chartRange, setChartRange, topClients, recentEvents, severities, toggleSeverity, navigate } = props; + chartRange, setChartRange, topClients, usingNetflowTop, recentEvents, severities, toggleSeverity, navigate } = props; const formatBytes = (bytes: number) => { if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`; if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`; @@ -435,19 +435,35 @@ function SummaryView(props: Record) { - {/* Top talkers */} + {/* Top talkers — NetFlow-backed (same numbers as the Traffic page) when + the collector has data; otherwise device-reported connection counters. */}
Top talkers
- by bytes · 1h + + {usingNetflowTop ? 'by bytes · 24h' : 'since connection'} +
{(topClients as { mac_address: string; hostname?: string; total_bytes: number }[]).length > 0 ? (
{(topClients as { mac_address: string; hostname?: string; total_bytes: number }[]).slice(0, 5).map((c, i) => { const maxB = (topClients as { total_bytes: number }[])[0]?.total_bytes ?? 1; const label = c.hostname || c.mac_address; + const isRealMac = c.mac_address !== 'unknown' && c.mac_address !== 'other'; return ( -
+
isRealMac && navigate(`/clients/${encodeURIComponent(c.mac_address)}`)} + title={isRealMac ? `${c.mac_address} — open client` : undefined} + > {String(i + 1).padStart(2, '0')} @@ -824,11 +840,31 @@ export default function DashboardPage() { }); const clientsOverTime = clientsOverTimeRaw.map(p => ({ ...p, ts: new Date(p.time).getTime() })); - const { data: topClients = [] } = useQuery({ + // Top talkers: prefer NetFlow data (same source as the Traffic page) so the + // numbers match everywhere; fall back to the device-reported per-connection + // counters when the collector has no data (e.g. NetFlow disabled). + const { data: netflowTopClients = [], isLoading: netflowTopLoading } = useQuery({ + queryKey: ['traffic-top-clients', '24h', 8], + queryFn: () => trafficApi.topClients('24h', 8).then(r => r.data), + refetchInterval: 60_000, + }); + const usingNetflowTop = netflowTopClients.length > 0; + const { data: counterTopClients = [] } = useQuery({ queryKey: ['top-clients'], queryFn: () => metricsApi.topClients(8).then(r => r.data), refetchInterval: 60_000, + enabled: !netflowTopLoading && !usingNetflowTop, }); + const topClients = usingNetflowTop + ? netflowTopClients.map((c) => ({ + mac_address: c.mac, + hostname: + c.mac === 'unknown' ? 'Unattributed (local)' + : c.mac === 'other' ? 'Other clients' + : c.custom_name || c.hostname || c.vendor || undefined, + total_bytes: c.total_bytes, + })) + : counterTopClients; const { data: wirelessClientsData } = useQuery({ queryKey: ['wireless-clients-active'], @@ -994,6 +1030,7 @@ export default function DashboardPage() { chartRange={chartRange} setChartRange={setChartRange} topClients={topClients} + usingNetflowTop={usingNetflowTop} recentEvents={recentEvents} severities={severities} toggleSeverity={toggleSeverity} diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 7ee87b3..c16763c 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.13.0 Beta'; +export const APP_VERSION = 'v0.13.1 Beta';