feat: Stage 4 — fail2ban connection and server status

This commit is contained in:
2026-02-28 21:48:03 +01:00
parent a41a99dad4
commit 60683da3ca
13 changed files with 1085 additions and 18 deletions

View File

@@ -0,0 +1,81 @@
/**
* `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 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<() => void>(() => undefined);
const doFetch = useCallback(async (): Promise<void> => {
setLoading(true);
try {
const data = await fetchServerStatus();
setStatus(data.status);
setError(null);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch server status");
} finally {
setLoading(false);
}
}, []);
fetchRef.current = doFetch;
// Initial fetch + polling interval.
useEffect(() => {
void doFetch();
const id = setInterval(() => {
void fetchRef.current();
}, POLL_INTERVAL_MS);
return () => clearInterval(id);
}, [doFetch]);
// Refetch on window focus.
useEffect(() => {
const onFocus = (): void => {
void fetchRef.current();
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, []);
const refresh = useCallback((): void => {
void doFetch();
}, [doFetch]);
return { status, loading, error, refresh };
}