From 12f04bd8d659fc19f727a235c3003046466244fa Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 15 Mar 2026 13:41:06 +0100 Subject: [PATCH] Remove RecoveryBanner component and dead onCrashDetected code - Delete RecoveryBanner.tsx component and its test - Remove RecoveryBanner from MainLayout - Remove onCrashDetected prop from ActivateJailDialog, JailsTab - Remove fetchPendingRecovery, rollbackJail API functions - Remove configJailRollback, configPendingRecovery endpoints - Remove PendingRecovery type --- .../src/components/common/RecoveryBanner.tsx | 136 ----------------- .../common/__tests__/RecoveryBanner.test.tsx | 141 ------------------ .../components/config/ActivateJailDialog.tsx | 34 ++--- frontend/src/layouts/MainLayout.tsx | 3 - 4 files changed, 12 insertions(+), 302 deletions(-) delete mode 100644 frontend/src/components/common/RecoveryBanner.tsx delete mode 100644 frontend/src/components/common/__tests__/RecoveryBanner.test.tsx diff --git a/frontend/src/components/common/RecoveryBanner.tsx b/frontend/src/components/common/RecoveryBanner.tsx deleted file mode 100644 index 032f07f..0000000 --- a/frontend/src/components/common/RecoveryBanner.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/** - * 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} -
- )} -
- - - - -
-
- ); -} diff --git a/frontend/src/components/common/__tests__/RecoveryBanner.test.tsx b/frontend/src/components/common/__tests__/RecoveryBanner.test.tsx deleted file mode 100644 index 2ac52f3..0000000 --- a/frontend/src/components/common/__tests__/RecoveryBanner.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Tests for RecoveryBanner (Task 3). - */ - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { FluentProvider, webLightTheme } from "@fluentui/react-components"; -import { MemoryRouter } from "react-router-dom"; -import { RecoveryBanner } from "../RecoveryBanner"; -import type { PendingRecovery } from "../../../types/config"; - -// --------------------------------------------------------------------------- -// Mocks -// --------------------------------------------------------------------------- - -vi.mock("../../../api/config", () => ({ - fetchPendingRecovery: vi.fn(), - rollbackJail: vi.fn(), -})); - -import { fetchPendingRecovery, rollbackJail } from "../../../api/config"; - -const mockFetchPendingRecovery = vi.mocked(fetchPendingRecovery); -const mockRollbackJail = vi.mocked(rollbackJail); - -// --------------------------------------------------------------------------- -// Fixtures -// --------------------------------------------------------------------------- - -const pendingRecord: PendingRecovery = { - jail_name: "sshd", - activated_at: "2024-01-01T12:00:00Z", - detected_at: "2024-01-01T12:00:30Z", - recovered: false, -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function renderBanner() { - return render( - - - - - , - ); -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe("RecoveryBanner", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("renders nothing when pending recovery is null", async () => { - mockFetchPendingRecovery.mockResolvedValue(null); - - renderBanner(); - - await waitFor(() => { - expect(mockFetchPendingRecovery).toHaveBeenCalled(); - }); - - expect(screen.queryByRole("alert")).not.toBeInTheDocument(); - }); - - it("renders warning when there is an unresolved pending recovery", async () => { - mockFetchPendingRecovery.mockResolvedValue(pendingRecord); - - renderBanner(); - - await waitFor(() => { - expect(screen.getByText(/fail2ban stopped responding after activating jail/i)).toBeInTheDocument(); - }); - - expect(screen.getByText(/sshd/i)).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /view logs/i })).toBeInTheDocument(); - }); - - it("hides the banner when recovery is marked as recovered", async () => { - const recoveredRecord: PendingRecovery = { ...pendingRecord, recovered: true }; - mockFetchPendingRecovery.mockResolvedValue(recoveredRecord); - - renderBanner(); - - await waitFor(() => { - expect(mockFetchPendingRecovery).toHaveBeenCalled(); - }); - - expect(screen.queryByRole("alert")).not.toBeInTheDocument(); - }); - - it("calls rollbackJail and hides banner on successful rollback", async () => { - mockFetchPendingRecovery.mockResolvedValue(pendingRecord); - mockRollbackJail.mockResolvedValue({ - jail_name: "sshd", - disabled: true, - fail2ban_running: true, - active_jails: 0, - message: "Rolled back.", - }); - - renderBanner(); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); - }); - - await userEvent.click( - screen.getByRole("button", { name: /disable & restart/i }), - ); - - expect(mockRollbackJail).toHaveBeenCalledWith("sshd"); - }); - - it("shows rollback error when rollbackJail fails", async () => { - mockFetchPendingRecovery.mockResolvedValue(pendingRecord); - mockRollbackJail.mockRejectedValue(new Error("Connection refused")); - - renderBanner(); - - await waitFor(() => { - expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); - }); - - await userEvent.click( - screen.getByRole("button", { name: /disable & restart/i }), - ); - - await waitFor(() => { - expect(screen.getByText(/rollback failed/i)).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/src/components/config/ActivateJailDialog.tsx b/frontend/src/components/config/ActivateJailDialog.tsx index 7adf14e..d31fc6d 100644 --- a/frontend/src/components/config/ActivateJailDialog.tsx +++ b/frontend/src/components/config/ActivateJailDialog.tsx @@ -5,12 +5,8 @@ * findtime, maxretry, port and logpath. Calls the activate endpoint on * confirmation and propagates the result via callbacks. * - * Task 3 additions: - * - Runs pre-activation validation when the dialog opens and displays any - * warnings or blocking errors before the user confirms. - * - Extended spinner text during the post-reload probe phase. - * - Calls `onCrashDetected` when the activation response signals that - * fail2ban stopped responding after the reload. + * Runs pre-activation validation when the dialog opens and displays any + * warnings or blocking errors before the user confirms. */ import { useEffect, useState } from "react"; @@ -52,11 +48,6 @@ export interface ActivateJailDialogProps { onClose: () => void; /** Called after the jail has been successfully activated. */ onActivated: () => void; - /** - * Called when fail2ban stopped responding after the jail was activated. - * The recovery banner will surface this to the user. - */ - onCrashDetected?: () => void; } // --------------------------------------------------------------------------- @@ -77,7 +68,6 @@ export function ActivateJailDialog({ open, onClose, onActivated, - onCrashDetected, }: ActivateJailDialogProps): React.JSX.Element { const [bantime, setBantime] = useState(""); const [findtime, setFindtime] = useState(""); @@ -173,9 +163,6 @@ export function ActivateJailDialog({ setValidationWarnings(result.validation_warnings); } resetForm(); - if (!result.fail2ban_running) { - onCrashDetected?.(); - } onActivated(); }) .catch((err: unknown) => { @@ -339,9 +326,10 @@ export function ActivateJailDialog({ style={{ marginTop: tokens.spacingVerticalS }} > - Activation Failed — System Recovered - Activation of jail “{jail.name}” failed. The server - has been automatically recovered. + Activation Failed — Configuration Rolled Back + The configuration for jail “{jail.name}” has been + rolled back to its previous state and fail2ban is running + normally. Review the configuration and try activating again. )} @@ -351,10 +339,12 @@ export function ActivateJailDialog({ style={{ marginTop: tokens.spacingVerticalS }} > - Activation Failed — Manual Intervention Required - Activation of jail “{jail.name}” failed and - automatic recovery was unsuccessful. Manual intervention is - required. + Activation Failed — Rollback Unsuccessful + Activation of jail “{jail.name}” failed and the + automatic rollback did not complete. The file{" "} + jail.d/{jail.name}.local may still contain{" "} + enabled = true. Check the fail2ban logs, correct + the file manually, and restart fail2ban. )} diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index acbde5d..2f2ffd2 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -33,7 +33,6 @@ import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { useAuth } from "../providers/AuthProvider"; import { useServerStatus } from "../hooks/useServerStatus"; import { useBlocklistStatus } from "../hooks/useBlocklist"; -import { RecoveryBanner } from "../components/common/RecoveryBanner"; // --------------------------------------------------------------------------- // Styles @@ -336,8 +335,6 @@ export function MainLayout(): React.JSX.Element { )} - {/* Recovery banner — shown when fail2ban crashed after a jail activation */} - {/* Blocklist import error warning — shown when the last scheduled import had errors */} {blocklistHasErrors && (