From 133e6e50e41c5fb21d08f377f478c47923dd7c3e Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Mon, 16 Mar 2026 18:36:44 -0500 Subject: [PATCH] feat(ui): wire Wireless and Traffic pages to live data Replace placeholder "coming soon" pages with functional implementations that query the fleet APIs and display real-time wireless issues and resource consumption data. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/routes/_authenticated/traffic.tsx | 223 +++++++++++++++++- .../src/routes/_authenticated/wireless.tsx | 187 ++++++++++++++- 2 files changed, 400 insertions(+), 10 deletions(-) diff --git a/frontend/src/routes/_authenticated/traffic.tsx b/frontend/src/routes/_authenticated/traffic.tsx index a118448..5994d72 100644 --- a/frontend/src/routes/_authenticated/traffic.tsx +++ b/frontend/src/routes/_authenticated/traffic.tsx @@ -1,20 +1,233 @@ import { createFileRoute } from '@tanstack/react-router' -import { BarChart3 } from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { BarChart3, Inbox } from 'lucide-react' +import { metricsApi, type FleetDevice } from '@/lib/api' +import { useAuth, isSuperAdmin } from '@/lib/auth' +import { useUIStore } from '@/lib/store' +import { Card, CardContent } from '@/components/ui/card' +import { cn } from '@/lib/utils' export const Route = createFileRoute('/_authenticated/traffic')({ component: TrafficPage, }) +function cpuColor(cpu: number | null): string { + if (cpu === null) return 'text-text-muted' + if (cpu < 50) return 'text-emerald-400' + if (cpu < 80) return 'text-yellow-400' + return 'text-red-400' +} + +function memColor(mem: number | null): string { + if (mem === null) return 'text-text-muted' + if (mem < 60) return 'text-emerald-400' + if (mem < 85) return 'text-yellow-400' + return 'text-red-400' +} + +function statusDot(status: string) { + const color = + status === 'online' + ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.6)]' + : status === 'degraded' + ? 'bg-yellow-400 shadow-[0_0_6px_rgba(250,204,21,0.6)]' + : 'bg-red-400 shadow-[0_0_6px_rgba(248,113,113,0.6)]' + return +} + function TrafficPage() { + const { user } = useAuth() + const selectedTenantId = useUIStore((s) => s.selectedTenantId) + const superAdmin = isSuperAdmin(user) + + const tenantId = superAdmin ? selectedTenantId : user?.tenant_id + + const { data: devices = [], isLoading } = useQuery({ + queryKey: ['fleet-summary', tenantId, superAdmin], + queryFn: () => + superAdmin && !tenantId + ? metricsApi.fleetSummaryAll() + : metricsApi.fleetSummary(tenantId!), + enabled: !!tenantId || superAdmin, + refetchInterval: 30_000, + }) + + // Sort by CPU load descending, nulls last + const sorted = [...devices].sort((a, b) => { + const aCpu = a.last_cpu_load ?? -1 + const bCpu = b.last_cpu_load ?? -1 + return bCpu - aCpu + }) + + const top10 = sorted.slice(0, 10) + + const avgCpu = + devices.length > 0 + ? devices.reduce((sum, d) => sum + (d.last_cpu_load ?? 0), 0) / devices.length + : null + + const avgMem = + devices.length > 0 + ? devices.reduce((sum, d) => sum + (d.last_memory_used_pct ?? 0), 0) / devices.length + : null + + const onlineCount = devices.filter((d) => d.status === 'online').length + return ( -
+
+ {/* Header */}

Traffic

-

- Bandwidth monitoring and traffic analysis — coming soon. -

+ + {/* KPI Cards */} +
+ + +

+ Fleet Avg CPU +

+

+ {isLoading ? '--' : avgCpu !== null ? `${avgCpu.toFixed(1)}%` : 'N/A'} +

+
+
+ + + +

+ Fleet Avg Memory +

+

+ {isLoading ? '--' : avgMem !== null ? `${avgMem.toFixed(1)}%` : 'N/A'} +

+
+
+ + + +

+ Devices Online +

+

+ {isLoading ? '--' : `${onlineCount} / ${devices.length}`} +

+
+
+
+ + {/* Top Resource Consumers */} + {isLoading ? ( + + +

Loading fleet data...

+
+
+ ) : devices.length === 0 ? ( + + + +

+ No device data available +

+
+
+ ) : ( +
+

+ Top Resource Consumers +

+ +
+ + + + + {superAdmin && !tenantId && ( + + )} + + + + + + + + {top10.map((device) => ( + + + {superAdmin && !tenantId && ( + + )} + + + + + + ))} + +
+ Hostname + + Tenant + + IP Address + + CPU Load + + Memory % + + Status +
+ {device.hostname} + + {device.tenant_name} + + {device.ip_address} + + {device.last_cpu_load !== null + ? `${device.last_cpu_load}%` + : '--'} + + {device.last_memory_used_pct !== null + ? `${device.last_memory_used_pct.toFixed(1)}%` + : '--'} + +
+ {statusDot(device.status)} + + {device.status} + +
+
+
+
+
+ )}
) } diff --git a/frontend/src/routes/_authenticated/wireless.tsx b/frontend/src/routes/_authenticated/wireless.tsx index 050cfc7..6c48d1c 100644 --- a/frontend/src/routes/_authenticated/wireless.tsx +++ b/frontend/src/routes/_authenticated/wireless.tsx @@ -1,20 +1,197 @@ import { createFileRoute } from '@tanstack/react-router' -import { Wifi } from 'lucide-react' +import { useQuery } from '@tanstack/react-query' +import { Wifi, CheckCircle2 } from 'lucide-react' +import { metricsApi } from '@/lib/api' +import { useAuth, isSuperAdmin } from '@/lib/auth' +import { useUIStore } from '@/lib/store' +import { Card, CardContent } from '@/components/ui/card' +import { cn } from '@/lib/utils' export const Route = createFileRoute('/_authenticated/wireless')({ component: WirelessPage, }) +function signalColor(signal: number | null): string { + if (signal === null) return 'text-text-muted' + if (signal > -60) return 'text-emerald-400' + if (signal > -70) return 'text-yellow-400' + return 'text-red-400' +} + function WirelessPage() { + const { user } = useAuth() + const selectedTenantId = useUIStore((s) => s.selectedTenantId) + const superAdmin = isSuperAdmin(user) + + const tenantId = superAdmin ? selectedTenantId : user?.tenant_id + + const { data: issues = [], isLoading } = useQuery({ + queryKey: ['wireless-issues', tenantId, superAdmin], + queryFn: () => + superAdmin && !tenantId + ? metricsApi.fleetWirelessIssues() + : metricsApi.wirelessIssues(tenantId!), + enabled: !!tenantId || superAdmin, + refetchInterval: 30_000, + }) + + const worstSignal = + issues.length > 0 + ? issues.reduce((worst, i) => { + if (i.signal === null) return worst + if (worst === null) return i.signal + return i.signal < worst ? i.signal : worst + }, null) + : null + + const totalClients = issues.reduce((sum, i) => sum + i.client_count, 0) + return ( -
+
+ {/* Header */}

Wireless

-

- Wireless monitoring and statistics — coming soon. -

+ + {/* KPI Cards */} +
+ + +

+ APs with Issues +

+

+ {isLoading ? '--' : issues.length} +

+
+
+ + + +

+ Worst Signal +

+

+ {isLoading ? '--' : worstSignal !== null ? `${worstSignal} dBm` : 'N/A'} +

+
+
+ + + +

+ Total Clients +

+

+ {isLoading ? '--' : totalClients} +

+
+
+
+ + {/* Issues Table or All Clear */} + {isLoading ? ( + + +

Loading wireless data...

+
+
+ ) : issues.length === 0 ? ( + + + +

+ All Clear — no wireless issues detected +

+
+
+ ) : ( + +
+ + + + + {superAdmin && !tenantId && ( + + )} + + + + + + + + + + {issues.map((issue, idx) => ( + + + {superAdmin && !tenantId && ( + + )} + + + + + + + + ))} + +
+ Hostname + + Tenant + + Interface + + Issue + + Signal + + CCQ + + Clients + + Frequency +
+
+ + {issue.hostname} +
+
+ {issue.tenant_name ?? '--'} + + {issue.interface} + + {issue.issue} + + {issue.signal !== null ? `${issue.signal} dBm` : '--'} + + {issue.ccq !== null ? `${issue.ccq}%` : '--'} + + {issue.client_count} + + {issue.frequency ? `${issue.frequency} MHz` : '--'} +
+
+
+ )}
) }