/** * RecoveryBanner — full-width warning shown when fail2ban stopped responding * shortly after a jail was activated (indicating the new jail config may be * invalid). * * Polls ``GET /api/config/pending-recovery`` every 10 seconds and renders a * dismissible ``MessageBar`` when an unresolved crash record is present. * The "Disable & Restart" button calls the rollback endpoint to disable the * offending jail and attempt to restart fail2ban. */ import { useCallback, useEffect, useRef, useState } from "react"; import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarTitle, Spinner, tokens, } from "@fluentui/react-components"; import { useNavigate } from "react-router-dom"; import { fetchPendingRecovery, rollbackJail } from "../../api/config"; import type { PendingRecovery } from "../../types/config"; const POLL_INTERVAL_MS = 10_000; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /** * Recovery banner that polls for pending crash-recovery records. * * Mount this once at the layout level so it is visible across all pages * while a recovery is pending. * * @returns A MessageBar element, or null when nothing is pending. */ export function RecoveryBanner(): React.JSX.Element | null { const navigate = useNavigate(); const [pending, setPending] = useState(null); const [rolling, setRolling] = useState(false); const [rollbackError, setRollbackError] = useState(null); const timerRef = useRef | null>(null); const poll = useCallback((): void => { fetchPendingRecovery() .then((record) => { // Hide the banner once fail2ban has recovered on its own. if (record?.recovered) { setPending(null); } else { setPending(record); } }) .catch(() => { /* ignore network errors — will retry */ }); }, []); // Start polling on mount. useEffect(() => { poll(); timerRef.current = setInterval(poll, POLL_INTERVAL_MS); return (): void => { if (timerRef.current !== null) clearInterval(timerRef.current); }; }, [poll]); const handleRollback = useCallback((): void => { if (!pending || rolling) return; setRolling(true); setRollbackError(null); rollbackJail(pending.jail_name) .then(() => { setPending(null); }) .catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); setRollbackError(msg); }) .finally(() => { setRolling(false); }); }, [pending, rolling]); const handleViewDetails = useCallback((): void => { navigate("/config"); }, [navigate]); if (pending === null) return null; return (
fail2ban Stopped After Jail Activation fail2ban stopped responding after activating jail{" "} {pending.jail_name}. The jail's configuration may be invalid. {rollbackError && (
Rollback failed: {rollbackError}
)}
); }