/** * `useJailDistribution` hook. * * Fetches per-jail ban counts for the jail distribution chart. * Re-fetches automatically when `timeRange` or `origin` changes. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByJail } from "../api/dashboard"; import { handleFetchError, createStringErrorAdapter } from "../utils/fetchError"; import type { BanOriginFilter, JailBanCount, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- // Return type // --------------------------------------------------------------------------- /** Return value shape for {@link useJailDistribution}. */ export interface UseJailDistributionResult { /** Jails ordered by ban count descending. */ jails: JailBanCount[]; /** Total ban count for the selected window. */ total: number; /** True while a fetch is in flight. */ loading: boolean; /** Error message or `null`. */ error: string | null; /** Re-fetch the data immediately. */ reload: () => void; } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- /** * Fetch and expose per-jail ban counts for the `JailDistributionChart` component. * * @param timeRange - Time-range preset: `"24h"`, `"7d"`, `"30d"`, or `"365d"`. * @param origin - Origin filter: `"all"`, `"blocklist"`, or `"selfblock"`. * @returns Jail list, total count, loading state, and error. */ export function useJailDistribution( timeRange: TimeRange, origin: BanOriginFilter, ): UseJailDistributionResult { const [jails, setJails] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); fetchBansByJail(timeRange, origin, "fail2ban", controller.signal) .then((data) => { if (controller.signal.aborted) return; setJails(data.jails); setTotal(data.total); }) .catch((err: unknown) => { if (controller.signal.aborted) return; handleFetchError(err, createStringErrorAdapter(setError), "Failed to fetch jail distribution"); }) .finally(() => { if (!controller.signal.aborted) { setLoading(false); } }); }, [timeRange, origin]); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); return { jails, total, loading, error, reload: load }; }