From 1e33220f599a018fb7bd1dd0e571133abfe9331b Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 22:03:58 +0100 Subject: [PATCH] Add reload and restart buttons to Server tab Adds ability to reload or restart fail2ban service from the Server tab UI. Backend changes: - Add new restart() method to jail_service.py that sends 'restart' command - Add new POST /api/config/restart endpoint in config router - Endpoint returns 204 on success, 502 if fail2ban unreachable - Includes structured logging via 'fail2ban_restarted' log entry Frontend changes: - Add configRestart endpoint to endpoints.ts - Add restartFail2Ban() API function in config.ts API module - Import ArrowSync24Regular icon from Fluent UI - Add reload and restart button handlers to ServerTab - Display 'Reload fail2ban' and 'Restart fail2ban' buttons in action row - Show loading spinner during operation - Display success/error MessageBar with appropriate feedback - Update ServerTab docstring to document new buttons All 115 frontend tests pass. --- backend/app/routers/config.py | 34 +++++++++++ backend/app/services/jail_service.py | 23 ++++++++ frontend/src/api/config.ts | 7 ++- frontend/src/api/endpoints.ts | 1 + frontend/src/components/config/ServerTab.tsx | 60 +++++++++++++++++++- 5 files changed, 122 insertions(+), 3 deletions(-) diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index e56e2f8..572d168 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -366,6 +366,40 @@ async def reload_fail2ban( raise _bad_gateway(exc) from exc +# Restart endpoint +# --------------------------------------------------------------------------- + + +@router.post( + "/restart", + status_code=status.HTTP_204_NO_CONTENT, + summary="Restart the fail2ban service", +) +async def restart_fail2ban( + request: Request, + _auth: AuthDep, +) -> None: + """Trigger a full fail2ban service restart. + + The fail2ban daemon is completely stopped and then started again, + re-reading all configuration files in the process. + + Args: + request: Incoming request. + _auth: Validated session. + + Raises: + HTTPException: 502 when fail2ban is unreachable. + """ + socket_path: str = request.app.state.settings.fail2ban_socket + try: + # Perform restart by sending the restart command via the fail2ban socket. + # If fail2ban is not running, this will raise an exception, and we return 502. + await jail_service.restart(socket_path) + except Fail2BanConnectionError as exc: + raise _bad_gateway(exc) from exc + + # --------------------------------------------------------------------------- # Regex tester (stateless) # --------------------------------------------------------------------------- diff --git a/backend/app/services/jail_service.py b/backend/app/services/jail_service.py index d89d2d5..9b87c19 100644 --- a/backend/app/services/jail_service.py +++ b/backend/app/services/jail_service.py @@ -596,6 +596,29 @@ async def reload_all( raise JailOperationError(str(exc)) from exc +async def restart(socket_path: str) -> None: + """Restart the fail2ban service (daemon). + + Sends the 'restart' command to the fail2ban daemon via the Unix socket. + All jails are stopped and the daemon is restarted, re-reading all + configuration from scratch. + + Args: + socket_path: Path to the fail2ban Unix domain socket. + + Raises: + JailOperationError: If fail2ban reports the operation failed. + ~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket + cannot be reached. + """ + client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT) + try: + _ok(await client.send(["restart"])) + log.info("fail2ban_restarted") + except ValueError as exc: + raise JailOperationError(str(exc)) from exc + + # --------------------------------------------------------------------------- # Public API — Ban / Unban # --------------------------------------------------------------------------- diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index 430849a..738ae3e 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -88,7 +88,7 @@ export async function updateGlobalConfig( } // --------------------------------------------------------------------------- -// Reload +// Reload and Restart // --------------------------------------------------------------------------- export async function reloadConfig( @@ -96,6 +96,11 @@ export async function reloadConfig( await post(ENDPOINTS.configReload, undefined); } +export async function restartFail2Ban( +): Promise { + await post(ENDPOINTS.configRestart, undefined); +} + // --------------------------------------------------------------------------- // Regex tester // --------------------------------------------------------------------------- diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index c3f7037..df6f9f1 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -78,6 +78,7 @@ export const ENDPOINTS = { configPendingRecovery: "/config/pending-recovery" as string, configGlobal: "/config/global", configReload: "/config/reload", + configRestart: "/config/restart", configRegexTest: "/config/regex-test", configPreviewLog: "/config/preview-log", configMapColorThresholds: "/config/map-color-thresholds", diff --git a/frontend/src/components/config/ServerTab.tsx b/frontend/src/components/config/ServerTab.tsx index 7520ae4..addc3d8 100644 --- a/frontend/src/components/config/ServerTab.tsx +++ b/frontend/src/components/config/ServerTab.tsx @@ -2,8 +2,9 @@ * ServerTab — fail2ban server-level settings editor. * * Provides form fields for live server settings (log level, log target, - * DB purge age, DB max matches), a "Flush Logs" action button, - * world map color threshold configuration, and service health + log viewer. + * DB purge age, DB max matches), action buttons (flush logs, reload fail2ban, + * restart fail2ban), world map color threshold configuration, and service + * health + log viewer. */ import { useCallback, useEffect, useMemo, useState } from "react"; @@ -21,6 +22,7 @@ import { } from "@fluentui/react-components"; import { DocumentArrowDown24Regular, + ArrowSync24Regular, } from "@fluentui/react-icons"; import { ApiError } from "../../api/client"; import type { ServerSettingsUpdate, MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config"; @@ -29,6 +31,8 @@ import { useAutoSave } from "../../hooks/useAutoSave"; import { fetchMapColorThresholds, updateMapColorThresholds, + reloadConfig, + restartFail2Ban, } from "../../api/config"; import { AutoSaveIndicator } from "./AutoSaveIndicator"; import { ServerHealthSection } from "./ServerHealthSection"; @@ -53,6 +57,10 @@ export function ServerTab(): React.JSX.Element { const [flushing, setFlushing] = useState(false); const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null); + // Reload/Restart state + const [isReloading, setIsReloading] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + // Map color thresholds const [mapThresholds, setMapThresholds] = useState(null); const [mapThresholdHigh, setMapThresholdHigh] = useState(""); @@ -97,6 +105,38 @@ export function ServerTab(): React.JSX.Element { } }, [flush]); + const handleReload = useCallback(async () => { + setIsReloading(true); + setMsg(null); + try { + await reloadConfig(); + setMsg({ text: "fail2ban reloaded successfully", ok: true }); + } catch (err: unknown) { + setMsg({ + text: err instanceof ApiError ? err.message : "Reload failed.", + ok: false, + }); + } finally { + setIsReloading(false); + } + }, []); + + const handleRestart = useCallback(async () => { + setIsRestarting(true); + setMsg(null); + try { + await restartFail2Ban(); + setMsg({ text: "fail2ban restart initiated", ok: true }); + } catch (err: unknown) { + setMsg({ + text: err instanceof ApiError ? err.message : "Restart failed.", + ok: false, + }); + } finally { + setIsRestarting(false); + } + }, []); + // Load map color thresholds on mount. const loadMapThresholds = useCallback(async (): Promise => { try { @@ -263,6 +303,22 @@ export function ServerTab(): React.JSX.Element { > {flushing ? "Flushing…" : "Flush Logs"} + + {msg && (