/** * React hooks for blocklist management data fetching. */ import { useCallback, useEffect, useRef, useState } from "react"; import { createBlocklist, deleteBlocklist, fetchBlocklists, fetchImportLog, fetchSchedule, previewBlocklist, runImportNow, updateBlocklist, updateSchedule, } from "../api/blocklist"; import { handleFetchError } from "../utils/fetchError"; import type { BlocklistSource, BlocklistSourceCreate, BlocklistSourceUpdate, ImportLogListResponse, ImportRunResult, PreviewResponse, ScheduleConfig, ScheduleInfo, } from "../types/blocklist"; // --------------------------------------------------------------------------- // useBlocklists // --------------------------------------------------------------------------- export interface UseBlocklistsReturn { sources: BlocklistSource[]; loading: boolean; error: string | null; refresh: () => void; createSource: (payload: BlocklistSourceCreate) => Promise; updateSource: (id: number, payload: BlocklistSourceUpdate) => Promise; removeSource: (id: number) => Promise; previewSource: (id: number) => Promise; } /** * Load all blocklist sources and expose CRUD operations. */ export function useBlocklists(): UseBlocklistsReturn { const [sources, setSources] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchBlocklists() .then((data) => { if (!ctrl.signal.aborted) { setSources(data.sources); setLoading(false); } }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { handleFetchError(err, setError, "Failed to load blocklists"); setLoading(false); } }); }, []); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); const createSource = useCallback( async (payload: BlocklistSourceCreate): Promise => { const created = await createBlocklist(payload); setSources((prev) => [...prev, created]); return created; }, [], ); const updateSource = useCallback( async (id: number, payload: BlocklistSourceUpdate): Promise => { const updated = await updateBlocklist(id, payload); setSources((prev) => prev.map((s) => (s.id === id ? updated : s))); return updated; }, [], ); const removeSource = useCallback(async (id: number): Promise => { await deleteBlocklist(id); setSources((prev) => prev.filter((s) => s.id !== id)); }, []); const previewSource = useCallback(async (id: number): Promise => { return previewBlocklist(id); }, []); return { sources, loading, error, refresh: load, createSource, updateSource, removeSource, previewSource, }; } // --------------------------------------------------------------------------- // useSchedule // --------------------------------------------------------------------------- export interface UseScheduleReturn { info: ScheduleInfo | null; loading: boolean; error: string | null; saveSchedule: (config: ScheduleConfig) => Promise; } /** * Fetch and update the blocklist import schedule. */ export function useSchedule(): UseScheduleReturn { const [info, setInfo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setLoading(true); fetchSchedule() .then((data) => { setInfo(data); setLoading(false); }) .catch((err: unknown) => { handleFetchError(err, setError, "Failed to load schedule"); setLoading(false); }); }, []); const saveSchedule = useCallback(async (config: ScheduleConfig): Promise => { const updated = await updateSchedule(config); setInfo(updated); }, []); return { info, loading, error, saveSchedule }; } // --------------------------------------------------------------------------- // useImportLog // --------------------------------------------------------------------------- export interface UseImportLogReturn { data: ImportLogListResponse | null; loading: boolean; error: string | null; page: number; setPage: (n: number) => void; refresh: () => void; } /** * Fetch the paginated import log with optional source filter. */ export function useImportLog( sourceId?: number, pageSize = 50, ): UseImportLogReturn { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [page, setPage] = useState(1); const abortRef = useRef(null); const load = useCallback((): void => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setLoading(true); setError(null); fetchImportLog(page, pageSize, sourceId) .then((result) => { if (!ctrl.signal.aborted) { setData(result); setLoading(false); } }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { handleFetchError(err, setError, "Failed to load import log"); setLoading(false); } }); }, [page, pageSize, sourceId]); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); return { data, loading, error, page, setPage, refresh: load }; } // --------------------------------------------------------------------------- // useRunImport // --------------------------------------------------------------------------- export interface UseRunImportReturn { running: boolean; lastResult: ImportRunResult | null; error: string | null; runNow: () => Promise; } /** * Trigger and track a manual blocklist import run. */ export function useRunImport(): UseRunImportReturn { const [running, setRunning] = useState(false); const [lastResult, setLastResult] = useState(null); const [error, setError] = useState(null); const runNow = useCallback(async (): Promise => { setRunning(true); setError(null); try { const result = await runImportNow(); setLastResult(result); } catch (err: unknown) { handleFetchError(err, setError, "Import failed"); } finally { setRunning(false); } }, []); return { running, lastResult, error, runNow }; } // --------------------------------------------------------------------------- // useBlocklistStatus // --------------------------------------------------------------------------- /** How often to re-check the schedule endpoint for import errors (ms). */ const BLOCKLIST_POLL_INTERVAL_MS = 60_000; export interface UseBlocklistStatusReturn { /** `true` when the most recent import run completed with errors. */ hasErrors: boolean; } /** * Poll `GET /api/blocklists/schedule` every 60 seconds to detect whether * the most recent blocklist import had errors. * * Network failures during polling are silently ignored — the indicator * simply retains its previous value until the next successful poll. */ export function useBlocklistStatus(): UseBlocklistStatusReturn { const [hasErrors, setHasErrors] = useState(false); useEffect(() => { let cancelled = false; const poll = (): void => { fetchSchedule() .then((info) => { if (!cancelled) { setHasErrors(info.last_run_errors === true); } }) .catch(() => { // Silently swallow network errors — do not change indicator state. }); }; poll(); const id = window.setInterval(poll, BLOCKLIST_POLL_INTERVAL_MS); return (): void => { cancelled = true; window.clearInterval(id); }; }, []); return { hasErrors }; }