diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 355889f..3bf0ed0 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -263,7 +263,7 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. --- -### Task 12 โ€” Standardise error handling in frontend hooks +### Task 12 โ€” Standardise error handling in frontend hooks (โœ… completed) **Priority**: Low **Refactoring ref**: Refactoring.md ยง9 diff --git a/frontend/src/hooks/useBanTrend.ts b/frontend/src/hooks/useBanTrend.ts index cc5d2bd..2e45660 100644 --- a/frontend/src/hooks/useBanTrend.ts +++ b/frontend/src/hooks/useBanTrend.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBanTrend } from "../api/dashboard"; +import { handleFetchError } from "../utils/fetchError"; import type { BanTrendBucket, BanOriginFilter, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- @@ -65,7 +66,7 @@ export function useBanTrend( }) .catch((err: unknown) => { if (controller.signal.aborted) return; - setError(err instanceof Error ? err.message : "Failed to fetch trend data"); + handleFetchError(err, setError, "Failed to fetch trend data"); }) .finally(() => { if (!controller.signal.aborted) { diff --git a/frontend/src/hooks/useBans.ts b/frontend/src/hooks/useBans.ts index c3a979f..9e36f45 100644 --- a/frontend/src/hooks/useBans.ts +++ b/frontend/src/hooks/useBans.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBans } from "../api/dashboard"; +import { handleFetchError } from "../utils/fetchError"; import type { DashboardBanItem, TimeRange, BanOriginFilter } from "../types/ban"; /** Items per page for the ban table. */ @@ -63,7 +64,7 @@ export function useBans( setBanItems(data.items); setTotal(data.total); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to fetch data"); + handleFetchError(err, setError, "Failed to fetch bans"); } finally { setLoading(false); } diff --git a/frontend/src/hooks/useBlocklist.ts b/frontend/src/hooks/useBlocklist.ts index 11a61c9..f539f54 100644 --- a/frontend/src/hooks/useBlocklist.ts +++ b/frontend/src/hooks/useBlocklist.ts @@ -14,12 +14,14 @@ import { updateBlocklist, updateSchedule, } from "../api/blocklist"; +import { handleFetchError } from "../utils/fetchError"; import type { BlocklistSource, BlocklistSourceCreate, BlocklistSourceUpdate, ImportLogListResponse, ImportRunResult, + PreviewResponse, ScheduleConfig, ScheduleInfo, } from "../types/blocklist"; @@ -65,7 +67,7 @@ export function useBlocklists(): UseBlocklistsReturn { }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { - setError(err instanceof Error ? err.message : "Failed to load blocklists"); + handleFetchError(err, setError, "Failed to load blocklists"); setLoading(false); } }); @@ -144,7 +146,7 @@ export function useSchedule(): UseScheduleReturn { setLoading(false); }) .catch((err: unknown) => { - setError(err instanceof Error ? err.message : "Failed to load schedule"); + handleFetchError(err, setError, "Failed to load schedule"); setLoading(false); }); }, []); @@ -200,7 +202,7 @@ export function useImportLog( }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { - setError(err instanceof Error ? err.message : "Failed to load import log"); + handleFetchError(err, setError, "Failed to load import log"); setLoading(false); } }); @@ -242,7 +244,7 @@ export function useRunImport(): UseRunImportReturn { const result = await runImportNow(); setLastResult(result); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Import failed"); + handleFetchError(err, setError, "Import failed"); } finally { setRunning(false); } diff --git a/frontend/src/hooks/useConfig.ts b/frontend/src/hooks/useConfig.ts index c27b148..f1fa12e 100644 --- a/frontend/src/hooks/useConfig.ts +++ b/frontend/src/hooks/useConfig.ts @@ -12,11 +12,13 @@ import { flushLogs, previewLog, reloadConfig, + restartFail2Ban, testRegex, updateGlobalConfig, updateJailConfig, updateServerSettings, } from "../api/config"; +import { handleFetchError } from "../utils/fetchError"; import type { AddLogPathRequest, GlobalConfig, @@ -65,9 +67,7 @@ export function useJailConfigs(): UseJailConfigsResult { setTotal(resp.total); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch jail configs"); }) .finally(() => { setLoading(false); @@ -128,9 +128,7 @@ export function useJailConfigDetail(name: string): UseJailConfigDetailResult { setJail(resp.jail); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch jail config"); }) .finally(() => { setLoading(false); @@ -191,9 +189,7 @@ export function useGlobalConfig(): UseGlobalConfigResult { fetchGlobalConfig() .then(setConfig) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch global config"); }) .finally(() => { setLoading(false); @@ -251,9 +247,7 @@ export function useServerSettings(): UseServerSettingsResult { setSettings(resp.settings); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch server settings"); }) .finally(() => { setLoading(false); diff --git a/frontend/src/hooks/useConfigActiveStatus.ts b/frontend/src/hooks/useConfigActiveStatus.ts index d7f091f..43763fe 100644 --- a/frontend/src/hooks/useConfigActiveStatus.ts +++ b/frontend/src/hooks/useConfigActiveStatus.ts @@ -13,6 +13,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchJails } from "../api/jails"; import { fetchJailConfigs } from "../api/config"; +import { handleFetchError } from "../utils/fetchError"; import type { JailConfig } from "../types/config"; import type { JailSummary } from "../types/jail"; @@ -110,7 +111,7 @@ export function useConfigActiveStatus(): UseConfigActiveStatusResult { }) .catch((err: unknown) => { if (ctrl.signal.aborted) return; - setError(err instanceof Error ? err.message : "Failed to load status."); + handleFetchError(err, setError, "Failed to load active status."); setLoading(false); }); }, []); diff --git a/frontend/src/hooks/useConfigItem.ts b/frontend/src/hooks/useConfigItem.ts index bf537cb..2f7700d 100644 --- a/frontend/src/hooks/useConfigItem.ts +++ b/frontend/src/hooks/useConfigItem.ts @@ -2,6 +2,7 @@ * Generic config hook for loading and saving a single entity. */ import { useCallback, useEffect, useRef, useState } from "react"; +import { handleFetchError } from "../utils/fetchError"; export interface UseConfigItemResult { data: T | null; @@ -46,7 +47,7 @@ export function useConfigItem( }) .catch((err: unknown) => { if (controller.signal.aborted) return; - setError(err instanceof Error ? err.message : "Failed to load data"); + handleFetchError(err, setError, "Failed to load data"); setLoading(false); }); }, [fetchFn]); diff --git a/frontend/src/hooks/useDashboardCountryData.ts b/frontend/src/hooks/useDashboardCountryData.ts index bcdbca6..250fcff 100644 --- a/frontend/src/hooks/useDashboardCountryData.ts +++ b/frontend/src/hooks/useDashboardCountryData.ts @@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByCountry } from "../api/map"; +import { handleFetchError } from "../utils/fetchError"; import type { DashboardBanItem, BanOriginFilter, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- @@ -77,7 +78,7 @@ export function useDashboardCountryData( }) .catch((err: unknown) => { if (controller.signal.aborted) return; - setError(err instanceof Error ? err.message : "Failed to fetch data"); + handleFetchError(err, setError, "Failed to fetch dashboard country data"); }) .finally(() => { if (!controller.signal.aborted) { diff --git a/frontend/src/hooks/useHistory.ts b/frontend/src/hooks/useHistory.ts index 8d03f9a..dff104c 100644 --- a/frontend/src/hooks/useHistory.ts +++ b/frontend/src/hooks/useHistory.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchHistory, fetchIpHistory } from "../api/history"; +import { handleFetchError } from "../utils/fetchError"; import type { HistoryBanItem, HistoryQuery, @@ -44,9 +45,7 @@ export function useHistory(query: HistoryQuery = {}): UseHistoryResult { setTotal(resp.total); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch history"); }) .finally((): void => { setLoading(false); @@ -91,9 +90,7 @@ export function useIpHistory(ip: string): UseIpHistoryResult { setDetail(resp); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch IP history"); }) .finally((): void => { setLoading(false); diff --git a/frontend/src/hooks/useJailDistribution.ts b/frontend/src/hooks/useJailDistribution.ts index bd1db4f..ef52a8c 100644 --- a/frontend/src/hooks/useJailDistribution.ts +++ b/frontend/src/hooks/useJailDistribution.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByJail } from "../api/dashboard"; +import { handleFetchError } from "../utils/fetchError"; import type { BanOriginFilter, JailBanCount, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- @@ -65,9 +66,7 @@ export function useJailDistribution( }) .catch((err: unknown) => { if (controller.signal.aborted) return; - setError( - err instanceof Error ? err.message : "Failed to fetch jail distribution", - ); + handleFetchError(err, setError, "Failed to fetch jail distribution"); }) .finally(() => { if (!controller.signal.aborted) { diff --git a/frontend/src/hooks/useJails.ts b/frontend/src/hooks/useJails.ts index f985213..cb23962 100644 --- a/frontend/src/hooks/useJails.ts +++ b/frontend/src/hooks/useJails.ts @@ -7,6 +7,7 @@ */ import { useCallback, useEffect, useRef, useState } from "react"; +import { handleFetchError } from "../utils/fetchError"; import { addIgnoreIp, banIp, @@ -92,7 +93,7 @@ export function useJails(): UseJailsResult { }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { - setError(err instanceof Error ? err.message : String(err)); + handleFetchError(err, setError, "Failed to load jails"); } }) .finally(() => { @@ -195,7 +196,7 @@ export function useJailDetail(name: string): UseJailDetailResult { }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { - setError(err instanceof Error ? err.message : String(err)); + handleFetchError(err, setError, "Failed to fetch jail detail"); } }) .finally(() => { @@ -309,7 +310,7 @@ export function useJailBannedIps(jailName: string): UseJailBannedIpsResult { setItems(resp.items); setTotal(resp.total); } catch (err: unknown) { - setError(err instanceof Error ? err.message : String(err)); + handleFetchError(err, setError, "Failed to fetch jailed IPs"); } finally { setLoading(false); } @@ -415,7 +416,7 @@ export function useActiveBans(): UseActiveBansResult { }) .catch((err: unknown) => { if (!ctrl.signal.aborted) { - setError(err instanceof Error ? err.message : String(err)); + handleFetchError(err, setError, "Failed to fetch active bans"); } }) .finally(() => { @@ -496,7 +497,7 @@ export function useIpLookup(): UseIpLookupResult { setResult(res); }) .catch((err: unknown) => { - setError(err instanceof Error ? err.message : String(err)); + handleFetchError(err, setError, "Failed to lookup IP"); }) .finally(() => { setLoading(false); diff --git a/frontend/src/hooks/useMapColorThresholds.ts b/frontend/src/hooks/useMapColorThresholds.ts index 8b73e48..79b46d7 100644 --- a/frontend/src/hooks/useMapColorThresholds.ts +++ b/frontend/src/hooks/useMapColorThresholds.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { fetchMapColorThresholds, updateMapColorThresholds } from "../api/config"; +import { handleFetchError } from "../utils/fetchError"; import type { MapColorThresholdsResponse, MapColorThresholdsUpdate, @@ -26,7 +27,7 @@ export function useMapColorThresholds(): UseMapColorThresholdsResult { const data = await fetchMapColorThresholds(); setThresholds(data); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to fetch map color thresholds"); + handleFetchError(err, setError, "Failed to fetch map color thresholds"); } finally { setLoading(false); } diff --git a/frontend/src/hooks/useMapData.ts b/frontend/src/hooks/useMapData.ts index a5d63e2..cae4c59 100644 --- a/frontend/src/hooks/useMapData.ts +++ b/frontend/src/hooks/useMapData.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByCountry } from "../api/map"; +import { handleFetchError } from "../utils/fetchError"; import type { BansByCountryResponse, MapBanItem, TimeRange } from "../types/map"; import type { BanOriginFilter } from "../types/ban"; @@ -68,9 +69,7 @@ export function useMapData( setData(resp); }) .catch((err: unknown) => { - if (err instanceof Error && err.name !== "AbortError") { - setError(err.message); - } + handleFetchError(err, setError, "Failed to fetch map data"); }) .finally((): void => { setLoading(false); diff --git a/frontend/src/hooks/useServerStatus.ts b/frontend/src/hooks/useServerStatus.ts index f4a37fd..826ccf9 100644 --- a/frontend/src/hooks/useServerStatus.ts +++ b/frontend/src/hooks/useServerStatus.ts @@ -8,6 +8,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchServerStatus } from "../api/dashboard"; +import { handleFetchError } from "../utils/fetchError"; import type { ServerStatus } from "../types/server"; /** How often to poll the status endpoint (milliseconds). */ @@ -49,7 +50,7 @@ export function useServerStatus(): UseServerStatusResult { setBanguiVersion(data.bangui_version); setError(null); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to fetch server status"); + handleFetchError(err, setError, "Failed to fetch server status"); } finally { setLoading(false); } diff --git a/frontend/src/hooks/useSetup.ts b/frontend/src/hooks/useSetup.ts index 9b5c660..db06968 100644 --- a/frontend/src/hooks/useSetup.ts +++ b/frontend/src/hooks/useSetup.ts @@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react"; import { ApiError } from "../api/client"; +import { handleFetchError } from "../utils/fetchError"; import { getSetupStatus, submitSetup } from "../api/setup"; import type { SetupRequest, @@ -44,9 +45,11 @@ export function useSetup(): UseSetupResult { const resp = await getSetupStatus(); setStatus(resp); } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : "Failed to fetch setup status"; - console.warn("Setup status check failed:", errorMessage); - setError(errorMessage); + const fallback = "Failed to fetch setup status"; + handleFetchError(err, setError, fallback); + if (!(err instanceof DOMException && err.name === "AbortError")) { + console.warn("Setup status check failed:", err instanceof Error ? err.message : fallback); + } } finally { setLoading(false); } diff --git a/frontend/src/hooks/useTimezoneData.ts b/frontend/src/hooks/useTimezoneData.ts index 6f919a9..dbfa69f 100644 --- a/frontend/src/hooks/useTimezoneData.ts +++ b/frontend/src/hooks/useTimezoneData.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { fetchTimezone } from "../api/setup"; +import { handleFetchError } from "../utils/fetchError"; export interface UseTimezoneDataResult { timezone: string; @@ -21,7 +22,7 @@ export function useTimezoneData(): UseTimezoneDataResult { const resp = await fetchTimezone(); setTimezone(resp.timezone); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to fetch timezone"); + handleFetchError(err, setError, "Failed to fetch timezone"); setTimezone("UTC"); } finally { setLoading(false); diff --git a/frontend/src/utils/fetchError.ts b/frontend/src/utils/fetchError.ts new file mode 100644 index 0000000..7cee108 --- /dev/null +++ b/frontend/src/utils/fetchError.ts @@ -0,0 +1,14 @@ +/** + * Normalize fetch error handling across hooks. + */ +export function handleFetchError( + err: unknown, + setError: (value: string | null) => void, + fallback: string = "Unknown error", +): void { + if (err instanceof DOMException && err.name === "AbortError") { + return; + } + + setError(err instanceof Error ? err.message : fallback); +}