import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { toast } from "sonner"; import { type Agent, type Alert, type AlertTransition, type AuditEvent, fetchAgents, fetchAlertHistory, fetchAlerts, fetchAudit, revokeAgent, setAgentNickname, } from "@/api"; import { Toaster } from "@/components/ui/sonner"; import { Skeleton } from "@/components/ui/skeleton"; import { AppLayout } from "@/layout/AppLayout"; import { AgentsPage } from "@/pages/AgentsPage"; import { AlertsPage } from "@/pages/AlertsPage"; import { AuditPage } from "@/pages/AuditPage"; import { CommandsPage } from "@/pages/CommandsPage"; import { DashboardPage } from "@/pages/DashboardPage"; import { DevicesPage } from "@/pages/DevicesPage"; import { TokensPage } from "@/pages/TokensPage"; const TerminalPage = lazy(() => import("@/pages/TerminalPage").then((module) => ({ default: module.TerminalPage, })), ); import { WakeToolsPage } from "@/pages/WakeToolsPage"; type LoadState = "idle" | "loading" | "ready" | "error"; export function App() { const [agents, setAgents] = useState([]); const [alerts, setAlerts] = useState([]); const [history, setHistory] = useState([]); const [audit, setAudit] = useState([]); const [selectedAgentId, setSelectedAgentId] = useState(""); const [alertsHealth, setAlertsHealth] = useState<{ available: boolean; reason?: string; } | null>(null); const [alertsStreamConnected, setAlertsStreamConnected] = useState(false); const streamHealthReceived = useRef(false); const alertsRevision = useRef(0); const [state, setState] = useState("idle"); async function loadAlertsWithoutBlocking() { try { const nextAlerts = await fetchAlerts(); if (!streamHealthReceived.current) { setAlertsHealth({ available: true }); } return nextAlerts; } catch (err) { if (!streamHealthReceived.current) { setAlertsHealth({ available: false, reason: String(err) }); } return null; } } async function loadAll() { const revision = alertsRevision.current; setState("loading"); try { const [nextAgents, nextAlerts, nextHistory, nextAudit] = await Promise.all([ fetchAgents(), loadAlertsWithoutBlocking(), fetchAlertHistory(20), fetchAudit(30), ]); setAgents(nextAgents); // Streamed state wins over HTTP requests started before its arrival. if (revision === alertsRevision.current) { if (nextAlerts) setAlerts(nextAlerts); setHistory(nextHistory); } setAudit(nextAudit); const firstConnectedAgentId = nextAgents.find((agent) => agent.connected)?.agent_id ?? ""; if (!nextAgents.length) { setSelectedAgentId(""); } else if ( !selectedAgentId || !nextAgents.some( (agent) => agent.agent_id === selectedAgentId && agent.connected, ) ) { setSelectedAgentId(firstConnectedAgentId); } setState("ready"); } catch (err) { setState("error"); toast.error("Failed to load data", { description: String(err) }); } } async function onRevokeAgent(agentId: string): Promise { const result = await revokeAgent(agentId); await loadAll(); if (result.revoked) { toast.success(`Revoked ${agentId}`); } return result.revoked; } async function onSetAgentNickname( agentId: string, nickname: string | null, ): Promise { const result = await setAgentNickname(agentId, nickname); await loadAll(); if (result.updated) { toast.success(`Nickname updated for ${agentId}`, { description: nickname || "(cleared)", }); } return result.updated; } async function refreshAlertsAndHistory() { const revision = alertsRevision.current; const [nextAlerts, nextHistory] = await Promise.all([ loadAlertsWithoutBlocking(), fetchAlertHistory(20), ]); if (revision === alertsRevision.current) { if (nextAlerts) setAlerts(nextAlerts); setHistory(nextHistory); } } useEffect(() => { void loadAll(); }, []); useEffect(() => { let socket: WebSocket | null = null; let disposed = false; let attempt = 0; let openedAt = 0; let reconnectTimer: number | undefined; const resync = () => { const generation = alertsRevision.current; void Promise.all([fetchAlerts(), fetchAlertHistory(20)]) .then(([nextAlerts, nextHistory]) => { // A WS snapshot delivered while this request was in flight is newer. if (disposed || generation !== alertsRevision.current) return; setAlerts(nextAlerts); setHistory(nextHistory); }) .catch(() => undefined); }; const connect = () => { if (disposed) return; const wsUrl = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/api/v1/control/alerts/ws`; socket = new WebSocket(wsUrl); socket.onopen = () => { if (disposed) return; openedAt = Date.now(); setAlertsStreamConnected(true); // Pull anything missed while the stream was down. resync(); }; socket.onmessage = (evt) => { if (disposed) return; alertsRevision.current += 1; try { const payload = JSON.parse(String(evt.data)) as { type?: string; available?: boolean; reason?: string; alerts?: Alert[]; recent_transitions?: AlertTransition[]; }; if (payload.type === "alerts_health") { streamHealthReceived.current = true; setAlertsHealth({ available: payload.available === true, reason: payload.reason, }); return; } if (payload.alerts) setAlerts(payload.alerts); if (payload.recent_transitions) setHistory(payload.recent_transitions); } catch { // Ignore malformed stream payloads and keep current UI state. } }; socket.onclose = () => { socket = null; if (disposed) return; setAlertsStreamConnected(false); // Only a connection that stayed up proves the stream is healthy; an // accept-then-close server must not pin us to the first backoff step. if (openedAt && Date.now() - openedAt > 10_000) attempt = 0; openedAt = 0; // Browsers never reconnect a WebSocket on their own. const delay = Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000; attempt += 1; reconnectTimer = window.setTimeout(connect, delay); }; }; connect(); return () => { disposed = true; if (reconnectTimer !== undefined) window.clearTimeout(reconnectTimer); socket?.close(); }; }, []); return ( <> }> } /> } /> } /> } /> } /> fetchAudit(30).then(setAudit)} /> } /> } /> } /> } > } /> } /> ); }