import { useEffect, useMemo, useRef, useState } from "react"; import { Inbox, MoreHorizontal, RefreshCw, Search, Zap } from "lucide-react"; import { toast } from "sonner"; import { fetchFleetDevices, fetchFleetDevice, fetchKnownDevices, fetchKnownDevice, refreshFleetDevices, wakeFleetDevice, type Agent, type FleetDevice, type KnownDevice, } from "@/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { FilterSelect, MobileLabel, PresenceBadge, } from "@/pages/fleet/FleetComponents"; import { FleetDeviceDetailsDialog } from "@/pages/fleet/FleetDeviceDetailsDialog"; import { agentLabel, formatSeen, knownFilters, presenceFilters, presenceRank, summarize, type KnownFilter, type SortDir, type SortKey, } from "@/pages/fleet/utils"; type Props = { agents: Agent[]; selectedAgentId: string; onSelectAgent: (agentId: string) => void; onAfterWake?: () => Promise; onRefresh?: () => Promise; }; export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) { const [devices, setDevices] = useState([]); const [knownDevices, setKnownDevices] = useState([]); const [query, setQuery] = useState(""); const [presence, setPresence] = useState("all"); const [known, setKnown] = useState("all"); const [agentId, setAgentId] = useState("all"); const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>({ key: "last_seen", dir: "desc", }); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [wakeBusyKey, setWakeBusyKey] = useState(""); const [details, setDetails] = useState(null); const loadSeqRef = useRef(0); const detailsKeyRef = useRef(null); const detailsSeqRef = useRef(0); function selectDetails(device: FleetDevice | null) { // Closing or selecting another row invalidates in-flight detail refreshes. detailsSeqRef.current += 1; detailsKeyRef.current = device?.device_key ?? null; setDetails(device); } async function loadFleet() { // Latest request wins: filter changes and debounced searches overlap, and // an older response must never overwrite fresher state. const seq = ++loadSeqRef.current; setLoading(true); try { const [nextDevices, nextKnownDevices] = await Promise.all([ fetchFleetDevices({ query, presence, known, agentId: agentId === "all" ? "" : agentId, visibility: "all", limit: 500, }), fetchKnownDevices(), ]); if (seq !== loadSeqRef.current) return; setDevices(nextDevices); setKnownDevices(nextKnownDevices); } catch (err) { if (seq !== loadSeqRef.current) return; toast.error("Failed to load fleet", { description: String(err) }); } finally { if (seq === loadSeqRef.current) setLoading(false); } } async function refreshView( originKey: string | null = detailsKeyRef.current, knownDeviceId?: string, ) { if (!originKey || originKey !== detailsKeyRef.current) { await loadFleet(); return; } // Mutations supply an explicit identity; never infer it from a shared IP. // Details use their own read, so list filters and limits cannot close them. const key = knownDeviceId ? `known:${knownDeviceId}` : originKey; detailsKeyRef.current = key; const seq = ++detailsSeqRef.current; try { const [device] = await Promise.all([fetchFleetDevice(key), loadFleet()]); if (seq !== detailsSeqRef.current || detailsKeyRef.current !== key) return; const saved = device.known_device ? await fetchKnownDevice(device.known_device.device_id) : null; if (seq === detailsSeqRef.current && detailsKeyRef.current === key) { // The dialog's full identity must stay current even if the list refresh // failed. Dropdown options may come from the list, not mutation controls. if (saved) { setKnownDevices((current) => current.some((item) => item.device_id === saved.device_id) ? current.map((item) => item.device_id === saved.device_id ? saved : item, ) : [...current, saved], ); } setDetails(device); } } catch (err) { if (seq !== detailsSeqRef.current || detailsKeyRef.current !== key) return; // A missing/failed identity read must not leave stale mutation controls. selectDetails(null); toast.error("Failed to refresh device details", { description: String(err), }); } } async function refreshAllConnected() { setRefreshing(true); try { const result = await refreshFleetDevices(); const failed = result.agents.filter((agent) => agent.status !== "ok"); const msg = failed.length ? `Refreshed ${result.total_accepted} devices; ${failed.length} agent(s) failed` : `Refreshed ${result.total_accepted} devices from ${result.agents.length} connected agents`; toast.success(msg); await refreshView(); } catch (err) { toast.error("Fleet refresh failed", { description: String(err) }); } finally { setRefreshing(false); } } async function wakeDevice(device: FleetDevice, routeId?: string | null) { if (!device.recommended_route && !routeId) return; setWakeBusyKey(device.device_key); try { const result = await wakeFleetDevice({ deviceKey: device.device_key, routeId, }); const msg = `Wake sent via ${agentLabel(result.route.agent_id, result.route.nickname)} (${result.route.mac ?? "no mac"})`; toast.success(msg); if (onAfterWake) await onAfterWake(); await refreshView(); } catch (err) { toast.error("Wake failed", { description: String(err) }); } finally { setWakeBusyKey(""); } } async function copyValue(label: string, value: string) { const text = value.trim(); if (!text) return; try { await navigator.clipboard.writeText(text); toast.success(`Copied ${label}`); } catch { const area = document.createElement("textarea"); area.value = text; area.style.cssText = "position:fixed;opacity:0"; document.body.appendChild(area); area.select(); document.execCommand("copy"); document.body.removeChild(area); toast.success(`Copied ${label}`); } } useEffect(() => { void loadFleet(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [presence, known, agentId]); // The filter effect above already loads on mount; only react to query edits. const mountedRef = useRef(false); useEffect(() => { if (!mountedRef.current) { mountedRef.current = true; return; } const timer = window.setTimeout(() => void loadFleet(), 220); return () => window.clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [query]); const sortedDevices = useMemo(() => { const rows = [...devices]; rows.sort((a, b) => { // Pinned always float to top if (a.pinned !== b.pinned) return a.pinned ? -1 : 1; const dir = sort.dir === "asc" ? 1 : -1; if (sort.key === "presence") { return dir * (presenceRank(a.presence) - presenceRank(b.presence)); } if (sort.key === "last_seen") { return dir * ((a.last_seen_unix ?? 0) - (b.last_seen_unix ?? 0)); } if (sort.key === "agents") return dir * (a.agents.length - b.agents.length); if (sort.key === "ip") { return dir * (a.ips[0] ?? "").localeCompare(b.ips[0] ?? ""); } if (sort.key === "mac") { return dir * (a.macs[0] ?? "").localeCompare(b.macs[0] ?? ""); } return dir * a.display_name.localeCompare(b.display_name); }); return rows; }, [devices, sort]); const connectedCount = agents.filter((agent) => agent.connected).length; return (
Devices

{devices.length} fleet rows from agent snapshots and known devices

setKnown(value as KnownFilter)} /> { const [key, dir] = value.split(":") as [SortKey, SortDir]; setSort({ key, dir }); }} />
{/* Skeleton loading */} {loading && !devices.length ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : (
Device Presence IPs MACs Agents Last seen Actions
{sortedDevices.map((device) => ( void wakeDevice(device)} onDetails={() => selectDetails(device)} onCopy={(label, value) => void copyValue(label, value)} /> ))} {!sortedDevices.length && (

No devices match the current filters

)}
)}
{ if (!open) selectDetails(null); }} onWake={(routeId) => { if (details) void wakeDevice(details, routeId); }} onCopy={(label, value) => void copyValue(label, value)} onChanged={(knownDeviceId) => refreshView(details?.device_key ?? null, knownDeviceId) } />
); } function FleetDeviceRow({ device, busy, onWake, onDetails, onCopy, }: { device: FleetDevice; busy: boolean; onWake: () => void; onDetails: () => void; onCopy: (label: string, value: string) => void; }) { const presenceBorder = device.presence === "online" ? "border-l-emerald-500/70" : device.presence === "likely_online" ? "border-l-sky-500/60" : device.presence === "offline" ? "border-l-muted-foreground/20" : ""; return (
{summarize(device.ips)} {summarize(device.macs)} {summarize( device.agents.map((agent) => agentLabel(agent.agent_id, agent.nickname), ), )} {formatSeen(device.last_seen_unix)}
Details onCopy("IP", device.ips[0] ?? "")} data-disabled={!device.ips.length} > Copy first IP onCopy("MAC", device.macs[0] ?? "")} data-disabled={!device.macs.length} > Copy first MAC
); }