import { useEffect, useState } from 'react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Button } from '@/components/ui/button'; import { Loader2, RefreshCw, ServerCog } from 'lucide-react'; import type { MeshNodeDiagnostic } from '@/types/mesh'; interface Props { open: boolean; onOpenChange: (open: boolean) => void; nodeId: number | null; nodeName: string | null; } function bytesFmt(n: number): string { if (n < 1024) return `${n}B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`; return `${(n / (1024 * 1024)).toFixed(1)}MB`; } function ageFmt(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${Math.floor(ms / 1000)}s`; return `${Math.floor(ms / 60_000)}m`; } export function MeshDiagnosticsSheet({ open, onOpenChange, nodeId, nodeName }: Props) { const [diag, setDiag] = useState(null); const [loading, setLoading] = useState(false); const [restarting, setRestarting] = useState(false); const refresh = async () => { if (nodeId == null) return; setLoading(true); try { const res = await apiFetch(`/mesh/nodes/${nodeId}/diagnostic`, { localOnly: true }); if (res.ok) setDiag(await res.json()); } finally { setLoading(false); } }; useEffect(() => { if (open) void refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, nodeId]); const restart = async () => { if (nodeId == null) return; setRestarting(true); try { const res = await apiFetch(`/mesh/nodes/${nodeId}/sidecar/restart`, { method: 'POST', localOnly: true, }); if (res.ok) { toast.success('Sidecar restart requested'); await refresh(); } else { toast.error('Sidecar restart failed'); } } finally { setRestarting(false); } }; return ( Diagnostics{nodeName ? ` · ${nodeName}` : ''}
Sidecar
{diag?.sidecar.running ? 'running' : 'off'}
Pilot tunnel
{diag?.pilot.connected ? 'connected' : 'disconnected'}
Buffered
{diag ? bytesFmt(diag.pilot.bufferedAmount) : '-'}
Last seen
{diag?.pilot.lastSeen ? new Date(diag.pilot.lastSeen).toLocaleTimeString() : '-'}
Active streams
{(!diag || diag.activeStreams.length === 0) && (
No active streams.
)}
{diag?.activeStreams.map((s) => (
#{s.streamId} {s.alias ?? ''} in {bytesFmt(s.bytesIn)} / out {bytesFmt(s.bytesOut)} · {ageFmt(s.ageMs)}
))}
Resolver cache
{(!diag || diag.aliasCache.length === 0) && (
No aliases registered.
)}
{diag?.aliasCache.map((a) => (
{a.host} node #{a.targetNodeId}:{a.port}
))}
); }