Files
BanGUI/frontend/src/hooks/useServerStatus.ts

103 lines
3.1 KiB
TypeScript

/**
* `useServerStatus` hook.
*
* Fetches and periodically refreshes the fail2ban server health snapshot
* from `GET /api/dashboard/status`. Also refetches on window focus so the
* status is always fresh when the user returns to the tab.
*/
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). */
const POLL_INTERVAL_MS = 30_000;
/** Return value of the {@link useServerStatus} hook. */
export interface UseServerStatusResult {
/** The most recent server status snapshot, or `null` before the first fetch. */
status: ServerStatus | null;
/** Whether a fetch is currently in flight. */
loading: boolean;
/** Error message string when the last fetch failed, otherwise `null`. */
error: string | null;
/** Manually trigger a refresh immediately. */
refresh: () => void;
}
/**
* Poll `GET /api/dashboard/status` every 30 seconds and on window focus.
*
* @returns Current status, loading state, error, and a `refresh` callback.
*/
export function useServerStatus(): UseServerStatusResult {
const [status, setStatus] = useState<ServerStatus | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Use a ref so the fetch function identity is stable.
const fetchRef = useRef<() => Promise<void>>(async () => Promise.resolve());
const abortRef = useRef<AbortController | null>(null);
const doFetch = useCallback(async (): Promise<void> => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
try {
const data = await fetchServerStatus(controller.signal);
if (controller.signal.aborted) {
return;
}
setStatus(data.status);
setError(null);
} catch (err: unknown) {
if (controller.signal.aborted) {
return;
}
handleFetchError(err, setError, "Failed to fetch server status");
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}, []);
fetchRef.current = doFetch;
// Initial fetch + polling interval.
useEffect((): (() => void) => {
void doFetch().catch((): void => undefined);
const id = setInterval((): void => {
void fetchRef.current().catch((): void => undefined);
}, POLL_INTERVAL_MS);
return (): void => { clearInterval(id); };
}, [doFetch]);
// Refetch on window focus.
useEffect(() => {
const onFocus = (): void => {
void fetchRef.current();
};
window.addEventListener("focus", onFocus);
return (): void => { window.removeEventListener("focus", onFocus); };
}, []);
useEffect(() => {
return (): void => {
abortRef.current?.abort();
};
}, []);
const refresh = useCallback((): void => {
void doFetch().catch((): void => undefined);
}, [doFetch]);
return { status, loading, error, refresh };
}