/** * `useHistory` hook — fetches and manages ban history data. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchHistory, fetchIpHistory } from "../api/history"; import { handleFetchError, createStringErrorAdapter } from "../utils/fetchError"; import { useListData } from "./useListData"; import type { HistoryBanItem, IpDetailResponse, HistoryListResponse } from "../types/history"; import type { BanOriginFilter, TimeRange } from "../types/ban"; import type { FetchError } from "../types/api"; export interface UseHistoryResult { items: HistoryBanItem[]; total: number; page: number; loading: boolean; error: FetchError | null; setPage: (page: number) => void; refresh: () => void; } /** * Fetch and manage paginated ban history with optional filters. * * @param page - Current page number (1-indexed) * @param pageSize - Items per page * @param range - Time range filter (e.g., "7d", "30d") * @param origin - Ban origin filter (e.g., "fail2ban", "blocklist") * @param jail - Jail name filter (optional) * @param ip - IP address filter (optional) * @param source - Data source ("fail2ban" | "archive") * @returns History data with pagination and error state */ export function useHistory( page: number = 1, pageSize: number = 50, range?: TimeRange, origin?: BanOriginFilter, jail?: string, ip?: string, source: "fail2ban" | "archive" = "archive", ): UseHistoryResult { const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(page); const fetcher = useCallback( (signal: AbortSignal) => fetchHistory( { page: currentPage, page_size: pageSize, range, origin, jail, ip, source, }, signal, ), [currentPage, pageSize, range, origin, jail, ip, source], ); const selector = useCallback((response: HistoryListResponse) => response.items, []); const onSuccess = useCallback((response: HistoryListResponse) => { setTotal(response.total); }, []); const { items, loading, error, refresh } = useListData({ fetcher, selector, errorMessage: "Failed to fetch history", onSuccess, }); return { items, total, page: currentPage, loading, error, setPage: setCurrentPage, refresh, }; } // --------------------------------------------------------------------------- // useIpHistory — per-IP detail // --------------------------------------------------------------------------- export interface UseIpHistoryResult { detail: IpDetailResponse | null; loading: boolean; error: string | null; refresh: () => void; } /** * Fetch and manage IP detail history. * * @param ip - IP address to fetch history for * @returns IP detail response, loading state, error, and refresh callback */ export function useIpHistory(ip: string): UseIpHistoryResult { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); abortRef.current = new AbortController(); setLoading(true); setError(null); fetchIpHistory(ip, abortRef.current.signal) .then((resp) => { if (!abortRef.current?.signal.aborted) { setDetail(resp); } }) .catch((err: unknown) => { if (!abortRef.current?.signal.aborted) { handleFetchError(err, createStringErrorAdapter(setError), "Failed to fetch IP history"); } }) .finally((): void => { if (!abortRef.current?.signal.aborted) { setLoading(false); } }); }, [ip]); useEffect((): (() => void) => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); return { detail, loading, error, refresh: load }; }