import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { useAuth } from '@/lib/auth' import { metricsApi, tenantsApi, type FleetDevice } from '@/lib/api' import { useUIStore } from '@/lib/store' import { alertsApi } from '@/lib/alertsApi' import { useEventStreamContext } from '@/contexts/EventStreamContext' import { LayoutDashboard, MapPin } from 'lucide-react' import { cn } from '@/lib/utils' import { LoadingText } from '@/components/ui/skeleton' import { EmptyState } from '@/components/ui/empty-state' // ─── Dashboard Widgets ─────────────────────────────────────────────────────── import { HealthScore } from '@/components/dashboard/HealthScore' import { EventsTimeline } from '@/components/dashboard/EventsTimeline' import { BandwidthChart, type BandwidthDevice } from '@/components/dashboard/BandwidthChart' import { AlertSummary } from '@/components/dashboard/AlertSummary' import { QuickActions } from '@/components/dashboard/QuickActions' import { WirelessIssues } from '@/components/dashboard/WirelessIssues' // ─── Types ─────────────────────────────────────────────────────────────────── type RefreshInterval = 15000 | 30000 | 60000 | false const REFRESH_OPTIONS: { label: string; value: RefreshInterval }[] = [ { label: '15s', value: 15000 }, { label: '30s', value: 30000 }, { label: '60s', value: 60000 }, { label: 'Off', value: false }, ] // ─── Dashboard Loading ─────────────────────────────────────────────────────── function DashboardLoading() { return (
) } // ─── Needs Attention (inline component) ───────────────────────────────────── interface AttentionItem { id: string deviceId: string tenantId: string hostname: string model: string | null severity: 'error' | 'warning' reason: string hasCoords: boolean } function NeedsAttention({ devices }: { devices: FleetDevice[] }) { const items = useMemo(() => { const result: AttentionItem[] = [] for (const d of devices) { const base = { deviceId: d.id, tenantId: d.tenant_id, hostname: d.hostname, model: d.model, hasCoords: d.latitude != null && d.longitude != null, } if (d.status === 'offline') { result.push({ ...base, id: `${d.id}-offline`, severity: 'error', reason: 'Offline' }) } else if (d.status === 'degraded') { result.push({ ...base, id: `${d.id}-degraded`, severity: 'warning', reason: 'Degraded' }) } if (d.last_cpu_load != null && d.last_cpu_load > 80) { result.push({ ...base, id: `${d.id}-cpu`, severity: 'warning', reason: `CPU ${d.last_cpu_load}%` }) } } result.sort((a, b) => { if (a.severity === b.severity) return 0 return a.severity === 'error' ? -1 : 1 }) return result.slice(0, 10) }, [devices]) const count = items.length return (
Needs Attention · {count}
{count > 0 ? (
{items.map((item) => (
{item.hostname} {item.model} {item.hasCoords && ( )}
{item.reason}
))}
) : (
No issues detected
)}
) } // ─── Fleet Dashboard ───────────────────────────────────────────────────────── export function FleetDashboard() { const { user } = useAuth() const isSuperAdmin = user?.role === 'super_admin' const { selectedTenantId } = useUIStore() const tenantId = isSuperAdmin ? (selectedTenantId ?? '') : (user?.tenant_id ?? '') // Fetch tenants for super admins to resolve selected org name const { data: tenants } = useQuery({ queryKey: ['tenants'], queryFn: tenantsApi.list, enabled: !!isSuperAdmin, }) const selectedTenantName = tenants?.find((t) => t.id === selectedTenantId)?.name const [refreshInterval, setRefreshInterval] = useState(30000) // ── SSE connection state (disable polling when connected) ──────────────── const { connectionState } = useEventStreamContext() const isSSEConnected = connectionState === 'connected' // ── Fleet summary query ────────────────────────────────────────────────── const { data: fleetDevices, isLoading: fleetLoading, isFetching: fleetFetching, dataUpdatedAt, } = useQuery({ queryKey: ['fleet-summary', isSuperAdmin && !selectedTenantId ? 'all' : tenantId], queryFn: () => isSuperAdmin && !selectedTenantId ? metricsApi.fleetSummaryAll() : metricsApi.fleetSummary(tenantId), // Disable polling when SSE is connected (events update cache directly) refetchInterval: isSSEConnected ? false : refreshInterval, enabled: !!user, }) // ── Alerts query (for counts by severity) ──────────────────────────────── const { data: alertsData } = useQuery({ queryKey: ['dashboard-alerts', tenantId, 'firing'], queryFn: () => alertsApi.getAlerts(tenantId, { status: 'firing', per_page: 200, }), // Disable polling when SSE is connected (events invalidate cache) refetchInterval: isSSEConnected ? false : refreshInterval, enabled: !!user && !isSuperAdmin && !!tenantId, }) // ── Derived data ───────────────────────────────────────────────────────── const totalDevices = fleetDevices?.length ?? 0 const onlineDevices = useMemo( () => fleetDevices?.filter((d) => d.status === 'online') ?? [], [fleetDevices], ) const onlinePercent = totalDevices > 0 ? (onlineDevices.length / totalDevices) * 100 : 0 const degradedCount = useMemo( () => fleetDevices?.filter((d) => d.status === 'degraded').length ?? 0, [fleetDevices], ) const offlineCount = useMemo( () => fleetDevices?.filter((d) => d.status === 'offline').length ?? 0, [fleetDevices], ) // Alert counts const alerts = alertsData?.items ?? [] const criticalCount = alerts.filter((a) => a.severity === 'critical').length const warningCount = alerts.filter((a) => a.severity === 'warning').length const infoCount = alerts.filter((a) => a.severity === 'info').length const totalAlerts = criticalCount + warningCount + infoCount // Health score device data const healthDevices = useMemo( () => fleetDevices?.map((d) => ({ status: d.status, last_cpu_load: d.last_cpu_load, last_memory_used_pct: d.last_memory_used_pct, })) ?? [], [fleetDevices], ) // Top resource consumers (using CPU load as proxy for bandwidth) // Sort by CPU load descending, take top 10 const topConsumers: BandwidthDevice[] = useMemo(() => { if (!fleetDevices) return [] return [...fleetDevices] .filter((d) => d.status === 'online' && d.last_cpu_load != null) .sort((a, b) => (b.last_cpu_load ?? 0) - (a.last_cpu_load ?? 0)) .slice(0, 10) .map((d) => ({ hostname: d.hostname, deviceId: d.id, tenantId: d.tenant_id, // Use CPU load percentage as the bandwidth metric for visualization bandwidthBps: (d.last_cpu_load ?? 0) * 10_000_000, // Scale to make chart readable })) }, [fleetDevices]) // Total "bandwidth" (sum of CPU loads scaled) const totalBandwidthBps = useMemo( () => topConsumers.reduce((sum, d) => sum + d.bandwidthBps, 0), [topConsumers], ) // Last updated timestamp const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', }) : null const isRefreshing = fleetFetching && !fleetLoading return (
{/* ── Page Header ─────────────────────────────────────────────────── */}

Overview

Fleet overview across{' '} {isSuperAdmin ? selectedTenantId && selectedTenantName ? selectedTenantName : 'all tenants' : 'your organization'}

{/* Refresh indicator */} {isRefreshing && ( Refreshing )} {/* Last updated */} {lastUpdated && !isRefreshing && ( Updated {lastUpdated} )} {/* Refresh interval selector */}
{REFRESH_OPTIONS.map((opt) => ( ))}
{/* ── Dashboard Content ───────────────────────────────────────────── */} {fleetLoading ? ( ) : totalDevices === 0 ? ( ) : ( <> {/* Metrics Strip — joined 4-column bar */}
{totalDevices}
Devices
{onlineDevices.length}
Online
0 ? 'text-warning' : 'text-text-primary', )} > {degradedCount}
Degraded
0 ? 'text-error' : 'text-text-primary', )} > {offlineCount}
Offline
{/* Needs Attention — full width */} {/* Widget Grid — responsive 3 columns */}
{/* Events Timeline — spans 2 columns on desktop */}
{/* Right column: Alert Summary + Quick Actions stacked */}
{/* Bandwidth / Top Resource Consumers — spans 2 columns on desktop */}
{/* Health Score */}
{/* Wireless Issues */}
)}
) }