• Event log viewer with filtering, search, pagination, and export functionality. Displays application and user action events with severity levels and metadata.

    Returns ReactElement

    JSX element rendering the event logger panel

    export function EventLogger(): React.ReactElement {
    const { eventLogEntries, maxEventLogEntries } = useDebugState();
    const { clearEventLog, recordEvent } = useDebugActions();
    const [activeTypes, setActiveTypes] = useState<string[]>([]);
    const [searchTerm, setSearchTerm] = useState("");
    const [visibleCount, setVisibleCount] = useState(DEFAULT_VISIBLE_COUNT);
    const searchInputId = useId();

    // Listen for custom events from PerformanceMonitor to set filter
    useEffect(() => {
    const handleSetFilter = (event: CustomEvent) => {
    const detail = event.detail as { types?: string[] } | undefined;
    if (detail?.types) {
    // Set type filter when custom event is triggered
    setActiveTypes(detail.types);
    setSearchTerm(""); // Clear search to focus on type filter
    setVisibleCount(DEFAULT_VISIBLE_COUNT);
    }
    };

    globalThis.addEventListener(
    "debug:events:set-filter" as unknown as string,
    handleSetFilter as EventListener,
    );

    return () => {
    globalThis.removeEventListener(
    "debug:events:set-filter" as unknown as string,
    handleSetFilter as EventListener,
    );
    };
    }, []);

    const availableTypes = useMemo(() => {
    const unique = new Set<string>();
    for (const entry of eventLogEntries) {
    unique.add(entry.type);
    }
    return Array.from(unique).sort((a, b) => a.localeCompare(b));
    }, [eventLogEntries]);

    const filteredEvents = useMemo(() => {
    const query = searchTerm.trim().toLowerCase();
    return eventLogEntries.filter(
    (entry) =>
    eventMatchesType(entry, activeTypes) &&
    eventMatchesSearch(entry, query),
    );
    }, [activeTypes, eventLogEntries, searchTerm]);

    const sortedEvents = useMemo(() => {
    return [...filteredEvents].sort((a, b) => {
    const aTime = Date.parse(a.timestamp);
    const bTime = Date.parse(b.timestamp);

    const aValid = !Number.isNaN(aTime);
    const bValid = !Number.isNaN(bTime);

    // Sort by parsed timestamp (newest first), fallback to string compare if parse fails
    if (aValid && bValid) {
    return bTime - aTime;
    }

    // Prioritize valid dates over invalid ones
    if (aValid) return -1;
    if (bValid) return 1;

    return b.timestamp.localeCompare(a.timestamp);
    });
    }, [filteredEvents]);

    const visibleEvents = useMemo(() => {
    return sortedEvents.slice(0, visibleCount);
    }, [sortedEvents, visibleCount]);

    const handleClear = () => {
    if (!eventLogEntries.length) {
    toast.info("Event log already empty");
    return;
    }
    clearEventLog();
    setVisibleCount(DEFAULT_VISIBLE_COUNT);
    toast.success("Event log cleared");
    recordEvent({
    type: "debug.event-logger",
    message: "Event log cleared",
    level: "warn",
    });
    };

    const handleExport = async () => {
    if (!filteredEvents.length) {
    toast.info("Nothing to export for current filters");
    return;
    }

    try {
    const payload = {
    exportedAt: new Date().toISOString(),
    totalEntries: filteredEvents.length,
    filters: {
    types: activeTypes.length ? activeTypes : undefined,
    search: searchTerm || undefined,
    },
    events: filteredEvents,
    };
    await exportToJson(payload, "kenmei-event-log");
    toast.success("Exported filtered events");
    recordEvent({
    type: "debug.event-logger",
    message: "Exported event log snapshot",
    level: "info",
    metadata: { totalEntries: filteredEvents.length },
    });
    } catch (error) {
    console.error("Failed to export event log", error);
    toast.error("Unable to export events");
    recordEvent(
    {
    type: "debug.event-logger",
    message: "Event log export failed",
    level: "error",
    metadata: {
    error: error instanceof Error ? error.message : String(error),
    },
    },
    { force: true },
    );
    }
    };

    const resetFilters = () => {
    setActiveTypes([]);
    setSearchTerm("");
    setVisibleCount(DEFAULT_VISIBLE_COUNT);
    };

    const loadMore = () =>
    setVisibleCount((value) => value + DEFAULT_VISIBLE_COUNT);
    const resetVisibleWindow = () => setVisibleCount(DEFAULT_VISIBLE_COUNT);

    return (
    <div className="flex h-full flex-col gap-4">
    <EventLoggerHeader
    filteredCount={filteredEvents.length}
    totalCount={eventLogEntries.length}
    maxEntries={maxEventLogEntries}
    availableTypesCount={availableTypes.length}
    activeTypesCount={activeTypes.length}
    />

    <FilterControls
    availableTypes={availableTypes}
    activeTypes={activeTypes}
    onTypesChange={setActiveTypes}
    searchTerm={searchTerm}
    onSearchChange={setSearchTerm}
    searchInputId={searchInputId}
    onResetFilters={resetFilters}
    onExport={handleExport}
    onClear={handleClear}
    />

    <Separator />

    <PaginationControls
    visibleCount={visibleEvents.length}
    filteredCount={filteredEvents.length}
    totalVisible={visibleCount}
    totalSorted={sortedEvents.length}
    defaultCount={DEFAULT_VISIBLE_COUNT}
    onLoadMore={loadMore}
    onResetWindow={resetVisibleWindow}
    />

    <EventList events={visibleEvents} />
    </div>
    );
    }