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) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-16 18:36:44 -05:00
parent 1800330545
commit 133e6e50e4
2 changed files with 400 additions and 10 deletions

View File

@@ -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 <span className={cn('inline-block h-2 w-2 rounded-full', color)} />
}
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 (
<div className="space-y-4">
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-text-muted" />
<h1 className="text-lg font-semibold text-text-primary">Traffic</h1>
</div>
<p className="text-sm text-text-secondary">
Bandwidth monitoring and traffic analysis coming soon.
</p>
{/* KPI Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Fleet Avg CPU
</p>
<p
className={cn(
'mt-1 text-2xl font-mono',
isLoading ? 'text-text-muted' : cpuColor(avgCpu),
)}
>
{isLoading ? '--' : avgCpu !== null ? `${avgCpu.toFixed(1)}%` : 'N/A'}
</p>
</CardContent>
</Card>
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Fleet Avg Memory
</p>
<p
className={cn(
'mt-1 text-2xl font-mono',
isLoading ? 'text-text-muted' : memColor(avgMem),
)}
>
{isLoading ? '--' : avgMem !== null ? `${avgMem.toFixed(1)}%` : 'N/A'}
</p>
</CardContent>
</Card>
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Devices Online
</p>
<p className="mt-1 text-2xl font-mono text-text-secondary">
{isLoading ? '--' : `${onlineCount} / ${devices.length}`}
</p>
</CardContent>
</Card>
</div>
{/* Top Resource Consumers */}
{isLoading ? (
<Card className="border-border bg-surface">
<CardContent className="p-6">
<p className="text-sm text-text-muted">Loading fleet data...</p>
</CardContent>
</Card>
) : devices.length === 0 ? (
<Card className="border-border bg-surface">
<CardContent className="flex flex-col items-center justify-center gap-3 p-12">
<Inbox className="h-10 w-10 text-text-muted" />
<p className="text-sm font-medium text-text-secondary">
No device data available
</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
<h2 className="text-sm font-semibold text-text-primary">
Top Resource Consumers
</h2>
<Card className="border-border bg-surface overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-border">
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Hostname
</th>
{superAdmin && !tenantId && (
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Tenant
</th>
)}
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
IP Address
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
CPU Load
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
Memory %
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-center">
Status
</th>
</tr>
</thead>
<tbody>
{top10.map((device) => (
<tr
key={device.id}
className="border-b border-border/50 hover:bg-surface-hover transition-colors"
>
<td className="px-4 py-3 text-sm text-text-secondary">
{device.hostname}
</td>
{superAdmin && !tenantId && (
<td className="px-4 py-3 text-sm font-mono text-text-muted">
{device.tenant_name}
</td>
)}
<td className="px-4 py-3 text-sm font-mono text-text-secondary">
{device.ip_address}
</td>
<td
className={cn(
'px-4 py-3 text-sm font-mono text-right',
cpuColor(device.last_cpu_load),
)}
>
{device.last_cpu_load !== null
? `${device.last_cpu_load}%`
: '--'}
</td>
<td
className={cn(
'px-4 py-3 text-sm font-mono text-right',
memColor(device.last_memory_used_pct),
)}
>
{device.last_memory_used_pct !== null
? `${device.last_memory_used_pct.toFixed(1)}%`
: '--'}
</td>
<td className="px-4 py-3 text-center">
<div className="flex items-center justify-center gap-2">
{statusDot(device.status)}
<span className="text-sm text-text-secondary capitalize">
{device.status}
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
</div>
)
}

View File

@@ -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<number | null>((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 (
<div className="space-y-4">
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-2">
<Wifi className="h-5 w-5 text-text-muted" />
<h1 className="text-lg font-semibold text-text-primary">Wireless</h1>
</div>
<p className="text-sm text-text-secondary">
Wireless monitoring and statistics coming soon.
</p>
{/* KPI Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
APs with Issues
</p>
<p className="mt-1 text-2xl font-mono text-text-secondary">
{isLoading ? '--' : issues.length}
</p>
</CardContent>
</Card>
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Worst Signal
</p>
<p
className={cn(
'mt-1 text-2xl font-mono',
isLoading ? 'text-text-muted' : signalColor(worstSignal),
)}
>
{isLoading ? '--' : worstSignal !== null ? `${worstSignal} dBm` : 'N/A'}
</p>
</CardContent>
</Card>
<Card className="border-border bg-surface">
<CardContent className="p-4">
<p className="text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Total Clients
</p>
<p className="mt-1 text-2xl font-mono text-text-secondary">
{isLoading ? '--' : totalClients}
</p>
</CardContent>
</Card>
</div>
{/* Issues Table or All Clear */}
{isLoading ? (
<Card className="border-border bg-surface">
<CardContent className="p-6">
<p className="text-sm text-text-muted">Loading wireless data...</p>
</CardContent>
</Card>
) : issues.length === 0 ? (
<Card className="border-border bg-surface">
<CardContent className="flex flex-col items-center justify-center gap-3 p-12">
<CheckCircle2 className="h-10 w-10 text-emerald-400" />
<p className="text-sm font-medium text-text-secondary">
All Clear no wireless issues detected
</p>
</CardContent>
</Card>
) : (
<Card className="border-border bg-surface overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-border">
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Hostname
</th>
{superAdmin && !tenantId && (
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Tenant
</th>
)}
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Interface
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted">
Issue
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
Signal
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
CCQ
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
Clients
</th>
<th className="px-4 py-3 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-right">
Frequency
</th>
</tr>
</thead>
<tbody>
{issues.map((issue, idx) => (
<tr
key={`${issue.device_id}-${issue.interface}-${idx}`}
className="border-b border-border/50 hover:bg-surface-hover transition-colors"
>
<td className="px-4 py-3 text-sm text-text-secondary">
<div className="flex items-center gap-2">
<span className="inline-block h-2 w-2 rounded-full bg-red-400 shadow-[0_0_6px_rgba(248,113,113,0.6)]" />
{issue.hostname}
</div>
</td>
{superAdmin && !tenantId && (
<td className="px-4 py-3 text-sm font-mono text-text-muted">
{issue.tenant_name ?? '--'}
</td>
)}
<td className="px-4 py-3 text-sm font-mono text-text-secondary">
{issue.interface}
</td>
<td className="px-4 py-3 text-sm text-text-secondary">
{issue.issue}
</td>
<td
className={cn(
'px-4 py-3 text-sm font-mono text-right',
signalColor(issue.signal),
)}
>
{issue.signal !== null ? `${issue.signal} dBm` : '--'}
</td>
<td className="px-4 py-3 text-sm font-mono text-text-secondary text-right">
{issue.ccq !== null ? `${issue.ccq}%` : '--'}
</td>
<td className="px-4 py-3 text-sm font-mono text-text-secondary text-right">
{issue.client_count}
</td>
<td className="px-4 py-3 text-sm font-mono text-text-secondary text-right">
{issue.frequency ? `${issue.frequency} MHz` : '--'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
)
}