/** * Event Log Viewer * * Compact log of relay operations for debugging and introspection. */ import { useState, useMemo, useCallback } from "react"; import { Check, X, Loader2, Wifi, WifiOff, Shield, ShieldAlert, MessageSquare, Send, RotateCcw, Trash2, ChevronDown, ChevronRight, AlertTriangle, } from "lucide-react"; import { Virtuoso } from "react-virtuoso"; import { Button } from "./ui/button"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { RelayLink } from "./nostr/RelayLink"; import { useEventLog } from "@/hooks/useEventLog"; import { type LogEntry, type EventLogType, type PublishLogEntry, type ConnectLogEntry, type ErrorLogEntry, type AuthLogEntry, type NoticeLogEntry, type RelayStatusEntry, } from "@/services/event-log"; import { formatTimestamp } from "@/hooks/useLocale"; import { cn } from "@/lib/utils"; import { KindBadge } from "./KindBadge"; import { KindRenderer } from "./nostr/kinds"; import { EventErrorBoundary } from "./EventErrorBoundary"; // ============================================================================ // Tab Filters // ============================================================================ type TabFilter = "all" | "publish" | "connect" | "auth" | "notice"; const TAB_TYPE_MAP: Record = { all: undefined, publish: ["PUBLISH"], connect: ["CONNECT", "DISCONNECT", "ERROR"], auth: ["AUTH"], notice: ["NOTICE"], }; const TAB_FILTERS: { value: TabFilter; label: string }[] = [ { value: "all", label: "All" }, { value: "publish", label: "Publish" }, { value: "connect", label: "Connect" }, { value: "auth", label: "Auth" }, { value: "notice", label: "Notice" }, ]; // ============================================================================ // Constants // ============================================================================ const AUTH_STATUS_TOOLTIP: Record = { challenge: "Auth challenge", success: "Auth success", failed: "Auth failed", rejected: "Auth rejected", }; // ============================================================================ // Helpers // ============================================================================ /** Format relay response time relative to publish start */ function formatRelayTime( publishTimestamp: number, relayUpdatedAt: number, ): string { const ms = relayUpdatedAt - publishTimestamp; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } // ============================================================================ // Shared row layout // ============================================================================ function EntryRow({ icon, tooltip, children, timestamp, className, expanded, onToggle, details, }: { icon: React.ReactNode; tooltip: string; children: React.ReactNode; timestamp: number; className?: string; expanded?: boolean; onToggle?: () => void; details?: React.ReactNode; }) { return (
{icon}
{tooltip}
{children}
{formatTimestamp(timestamp / 1000, "relative")} {onToggle && (
{expanded ? ( ) : ( )}
)}
{expanded && details && (
{details}
)}
); } // ============================================================================ // Entry Renderers // ============================================================================ function PublishRelayRow({ relay, status, publishTimestamp, onRetry, }: { relay: string; status: RelayStatusEntry; publishTimestamp: number; onRetry?: () => void; }) { const isTerminal = status.status === "success" || status.status === "error"; return (
{status.status === "success" && ( )} {status.status === "error" && ( )} {(status.status === "pending" || status.status === "publishing") && ( )} {isTerminal && ( {formatRelayTime(publishTimestamp, status.updatedAt)} )} {status.status === "error" && onRetry && ( )}
{status.error && (
{status.error}
)}
); } function PublishEntry({ entry, onRetry, onRetryRelay, }: { entry: PublishLogEntry; onRetry?: (entryId: string) => void; onRetryRelay?: (entryId: string, relay: string) => void; }) { const [expanded, setExpanded] = useState(false); const statuses = Array.from(entry.relayStatus.values()); const successCount = statuses.filter((s) => s.status === "success").length; const errorCount = statuses.filter((s) => s.status === "error").length; const isPending = statuses.some( (s) => s.status === "pending" || s.status === "publishing", ); return ( } tooltip="Publish" timestamp={entry.timestamp} expanded={expanded} onToggle={() => setExpanded(!expanded)} details={ <>
{Array.from(entry.relayStatus.entries()).map(([relay, status]) => ( onRetryRelay(entry.id, relay) : undefined } /> ))}
{errorCount > 0 && onRetry && (
)} } > {isPending && ( )} {!isPending && successCount > 0 && ( {successCount} ok )} {!isPending && errorCount > 0 && ( {errorCount} fail )}
); } function ConnectEntry({ entry }: { entry: ConnectLogEntry }) { const [expanded, setExpanded] = useState(false); const isConnect = entry.type === "CONNECT"; return ( ) : ( ) } tooltip={isConnect ? "Connected" : "Disconnected"} timestamp={entry.timestamp} expanded={expanded} onToggle={() => setExpanded(!expanded)} details={
Event: {isConnect ? "Connected" : "Disconnected"}
Time: {formatTimestamp(entry.timestamp / 1000, "absolute")}
} >
); } function ErrorEntry({ entry }: { entry: ErrorLogEntry }) { const [expanded, setExpanded] = useState(false); return ( } tooltip="Connection error" timestamp={entry.timestamp} expanded={expanded} onToggle={() => setExpanded(!expanded)} details={
{entry.message}
Time: {formatTimestamp(entry.timestamp / 1000, "absolute")}
} >
); } function AuthEntry({ entry }: { entry: AuthLogEntry }) { const [expanded, setExpanded] = useState(false); return ( ) : entry.status === "failed" ? ( ) : entry.status === "challenge" ? ( ) : ( ) } tooltip={AUTH_STATUS_TOOLTIP[entry.status] ?? "Auth"} timestamp={entry.timestamp} expanded={expanded} onToggle={() => setExpanded(!expanded)} details={
Status: {entry.status}
{entry.challenge && (
challenge: {entry.challenge}
)}
Time: {formatTimestamp(entry.timestamp / 1000, "absolute")}
} >
); } function NoticeEntry({ entry }: { entry: NoticeLogEntry }) { const [expanded, setExpanded] = useState(false); return ( } tooltip="Notice" timestamp={entry.timestamp} expanded={expanded} onToggle={() => setExpanded(!expanded)} details={
{entry.message}
Time: {formatTimestamp(entry.timestamp / 1000, "absolute")}
} >
); } function LogEntryRenderer({ entry, onRetry, onRetryRelay, }: { entry: LogEntry; onRetry?: (entryId: string) => void; onRetryRelay?: (entryId: string, relay: string) => void; }) { switch (entry.type) { case "PUBLISH": return ( ); case "CONNECT": case "DISCONNECT": return ; case "ERROR": return ; case "AUTH": return ; case "NOTICE": return ; default: return null; } } // ============================================================================ // Main Component // ============================================================================ function getTabCount( tab: TabFilter, totalCount: number, typeCounts: Record, ): number { const types = TAB_TYPE_MAP[tab]; if (!types) return totalCount; return types.reduce((sum, t) => sum + (typeCounts[t] || 0), 0); } export function EventLogViewer() { const [activeTab, setActiveTab] = useState("all"); const filterTypes = useMemo(() => TAB_TYPE_MAP[activeTab], [activeTab]); const { entries, clear, retryFailedRelays, retryRelay, totalCount, typeCounts, } = useEventLog({ types: filterTypes }); const renderItem = useCallback( (_index: number, entry: LogEntry) => ( ), [retryFailedRelays, retryRelay], ); return (
{/* Header */}
setActiveTab(v as TabFilter)} > {TAB_FILTERS.map((tab) => { const count = getTabCount(tab.value, totalCount, typeCounts); return ( {tab.label} {count > 0 && ( {count} )} ); })}
{/* Log entries */}
{entries.length === 0 ? (

No events logged yet

) : ( )}
); }