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.
This commit is contained in:
@@ -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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<undefined>(ENDPOINTS.configReload, undefined);
|
||||
}
|
||||
|
||||
export async function restartFail2Ban(
|
||||
): Promise<void> {
|
||||
await post<undefined>(ENDPOINTS.configRestart, undefined);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Regex tester
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<MapColorThresholdsResponse | null>(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<void> => {
|
||||
try {
|
||||
@@ -263,6 +303,22 @@ export function ServerTab(): React.JSX.Element {
|
||||
>
|
||||
{flushing ? "Flushing…" : "Flush Logs"}
|
||||
</Button>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
icon={<ArrowSync24Regular />}
|
||||
disabled={isReloading}
|
||||
onClick={() => void handleReload()}
|
||||
>
|
||||
{isReloading ? "Reloading…" : "Reload fail2ban"}
|
||||
</Button>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
icon={<ArrowSync24Regular />}
|
||||
disabled={isRestarting}
|
||||
onClick={() => void handleRestart()}
|
||||
>
|
||||
{isRestarting ? "Restarting…" : "Restart fail2ban"}
|
||||
</Button>
|
||||
</div>
|
||||
{msg && (
|
||||
<MessageBar intent={msg.ok ? "success" : "error"}>
|
||||
|
||||
Reference in New Issue
Block a user