import { useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { formatTimeAgo } from '@/lib/relativeTime'; import { toast } from '@/components/ui/toast-store'; import { SystemSheet, SheetSection } from '@/components/ui/system-sheet'; import { Badge } from '@/components/ui/badge'; import { ConfirmModal } from '@/components/ui/modal'; import { Loader2, Activity, Hash, Trash2 } from 'lucide-react'; import type { MeshAlias, MeshNodeStatus, MeshRouteDiagnostic, MeshActivityEvent, MeshProbeResult } from '@/types/mesh'; import { meshRouteStateFromBackend, meshRouteStateTokens } from './meshRouteState'; import { describeTransport } from './meshTransport'; import { MeshStackTopologyView } from './MeshStackTopologyView'; interface Props { open: boolean; onOpenChange: (open: boolean) => void; alias: string | null; /** Removing a route opts its owning stack out of the mesh; admin-only, mirrors the backend gate. */ canManage: boolean; status: MeshNodeStatus[]; aliases: MeshAlias[]; onChanged: () => void; } type RouteTab = 'overview' | 'events' | 'topology' | 'raw'; export function MeshRouteDetailSheet({ open, onOpenChange, alias, canManage, status, aliases, onChanged }: Props) { const [diag, setDiag] = useState(null); const [events, setEvents] = useState([]); const [probe, setProbe] = useState(null); const [probing, setProbing] = useState(false); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState('overview'); const [confirmRemove, setConfirmRemove] = useState(false); const [removing, setRemoving] = useState(false); useEffect(() => { if (!open || !alias) return; // Reset per-alias state so a stale target can never drive the destructive // remove action (or a misleading transport row) during the refetch window. setDiag(null); setProbe(null); setEvents([]); let cancelled = false; const refresh = async () => { setLoading(true); try { const [diagRes, evRes] = await Promise.all([ apiFetch(`/mesh/aliases/${encodeURIComponent(alias)}/diagnostic`, { localOnly: true }), apiFetch(`/mesh/activity?alias=${encodeURIComponent(alias)}&limit=20`, { localOnly: true }), ]); // Parse both bodies before any state write, then re-check // `cancelled` after every await. Reading the body is itself async, // so a check before `json()` would still let a superseded alias's // diagnostic call setDiag and expose Remove for the wrong stack. const diagBody = diagRes.ok ? await diagRes.json() as MeshRouteDiagnostic : null; const evBody = evRes.ok ? await evRes.json() as { events: MeshActivityEvent[] } : null; if (cancelled) return; if (diagBody) setDiag(diagBody); if (evBody) setEvents(evBody.events); } finally { if (!cancelled) setLoading(false); } }; void refresh(); return () => { cancelled = true; }; }, [open, alias]); const runProbe = async () => { if (!alias) return; setProbing(true); setProbe(null); try { const res = await apiFetch(`/mesh/aliases/${encodeURIComponent(alias)}/test`, { method: 'POST', localOnly: true, }); const body = await res.json() as MeshProbeResult; setProbe(body); } finally { setProbing(false); } }; const removeFromMesh = async () => { const tgt = diag?.target; if (!tgt) return; setRemoving(true); try { const res = await apiFetch( `/mesh/nodes/${tgt.nodeId}/stacks/${encodeURIComponent(tgt.stack)}/opt-out`, { method: 'POST', localOnly: true }, ); if (!res.ok) throw new Error(`status ${res.status}`); toast.success(`${tgt.stack} removed from mesh, redeploying`); onChanged(); onOpenChange(false); } catch (err) { toast.error(`Failed to remove from mesh: ${(err as Error).message}`); } finally { setRemoving(false); } }; if (!alias) return null; const pillState = diag ? meshRouteStateFromBackend(diag.state) : 'not-authorized'; const pill = meshRouteStateTokens(pillState); const meta = diag?.target ? `${diag.target.stack}/${diag.target.service}:${diag.target.port} · node #${diag.target.nodeId}` : (loading ? 'Loading…' : 'No target resolved'); const footerContext = probe ? (probe.ok ? `Last probe ok · ${probe.latencyMs}ms` : `Last probe failed · ${probe.where ?? 'unknown'}`) : diag?.lastProbeMs != null ? (diag.lastProbeAt != null ? `Last probe ${formatTimeAgo(diag.lastProbeAt)} · ${diag.lastProbeMs}ms` : `Last probe ${diag.lastProbeMs}ms`) : 'No probe run yet'; const target = diag?.target ?? null; const targetNode = target ? status.find((s) => s.nodeId === target.nodeId) : undefined; const stackAliasCount = target ? aliases.filter((a) => a.nodeId === target.nodeId && a.stackName === target.stack).length : 0; const transport = describeTransport(targetNode, diag?.pilot.connected ?? false); return ( <> { void runProbe(); }, disabled: probing, }} destructiveAction={canManage && target ? { label: removing ? 'Removing…' : 'Remove from mesh', icon: Trash2, onClick: () => setConfirmRemove(true), disabled: removing, } : undefined} tabs={[ { id: 'overview', label: 'Overview' }, { id: 'events', label: 'Events', count: events.length }, { id: 'topology', label: 'Topology' }, { id: 'raw', label: 'Raw' }, ]} activeTab={activeTab} onTabChange={(id) => setActiveTab(id as RouteTab)} footerContext={footerContext} size="md" > {activeTab === 'overview' && ( <>
{pill.label} {probe && ( {probe.ok ? `ok ${probe.latencyMs}ms` : `${probe.where ?? 'fail'}: ${probe.code ?? 'error'}`} )}
{diag?.target && (
Target node
#{diag.target.nodeId}
Stack / service
{diag.target.stack}/{diag.target.service}
Port
{diag.target.port}
{transport.label}
{transport.value}
)} {diag?.lastError && (
{diag.lastError.message}
{new Date(diag.lastError.ts).toLocaleString()}
)} )} {activeTab === 'events' && (
{loading && } {!loading && events.length === 0 && (
No events yet for this alias.
)} {events.map((e, i) => (
{e.source === 'pilot' && } {e.source === 'mesh' && } {new Date(e.ts).toLocaleTimeString()} {e.type} {e.message}
))}
)} {activeTab === 'topology' && ( {diag?.target ? ( ) : (
No target resolved for this alias.
)}
)} {activeTab === 'raw' && (
                        {diag ? JSON.stringify(diag, null, 2) : 'No diagnostic data loaded.'}
                    
)}
{ if (!o) setConfirmRemove(false); }} variant="destructive" kicker={`Mesh / ${target?.stack ?? ''}`} title={`Remove ${target?.stack ?? 'stack'} from the mesh?`} description={ target ? `${target.stack} will be redeployed on node #${target.nodeId} so its containers drop the mesh routing entries. This removes ${stackAliasCount} ${stackAliasCount === 1 ? 'alias' : 'aliases'} published by this stack.` : undefined } confirmLabel="Remove and redeploy" onConfirm={() => { setConfirmRemove(false); void removeFromMesh(); }} onCancel={() => setConfirmRemove(false)} /> ); }