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
|
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)
|
# Regex tester (stateless)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -596,6 +596,29 @@ async def reload_all(
|
|||||||
raise JailOperationError(str(exc)) from exc
|
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
|
# Public API — Ban / Unban
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export async function updateGlobalConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Reload
|
// Reload and Restart
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export async function reloadConfig(
|
export async function reloadConfig(
|
||||||
@@ -96,6 +96,11 @@ export async function reloadConfig(
|
|||||||
await post<undefined>(ENDPOINTS.configReload, undefined);
|
await post<undefined>(ENDPOINTS.configReload, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restartFail2Ban(
|
||||||
|
): Promise<void> {
|
||||||
|
await post<undefined>(ENDPOINTS.configRestart, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Regex tester
|
// Regex tester
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const ENDPOINTS = {
|
|||||||
configPendingRecovery: "/config/pending-recovery" as string,
|
configPendingRecovery: "/config/pending-recovery" as string,
|
||||||
configGlobal: "/config/global",
|
configGlobal: "/config/global",
|
||||||
configReload: "/config/reload",
|
configReload: "/config/reload",
|
||||||
|
configRestart: "/config/restart",
|
||||||
configRegexTest: "/config/regex-test",
|
configRegexTest: "/config/regex-test",
|
||||||
configPreviewLog: "/config/preview-log",
|
configPreviewLog: "/config/preview-log",
|
||||||
configMapColorThresholds: "/config/map-color-thresholds",
|
configMapColorThresholds: "/config/map-color-thresholds",
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
* ServerTab — fail2ban server-level settings editor.
|
* ServerTab — fail2ban server-level settings editor.
|
||||||
*
|
*
|
||||||
* Provides form fields for live server settings (log level, log target,
|
* Provides form fields for live server settings (log level, log target,
|
||||||
* DB purge age, DB max matches), a "Flush Logs" action button,
|
* DB purge age, DB max matches), action buttons (flush logs, reload fail2ban,
|
||||||
* world map color threshold configuration, and service health + log viewer.
|
* restart fail2ban), world map color threshold configuration, and service
|
||||||
|
* health + log viewer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {
|
import {
|
||||||
DocumentArrowDown24Regular,
|
DocumentArrowDown24Regular,
|
||||||
|
ArrowSync24Regular,
|
||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
import { ApiError } from "../../api/client";
|
import { ApiError } from "../../api/client";
|
||||||
import type { ServerSettingsUpdate, MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config";
|
import type { ServerSettingsUpdate, MapColorThresholdsResponse, MapColorThresholdsUpdate } from "../../types/config";
|
||||||
@@ -29,6 +31,8 @@ import { useAutoSave } from "../../hooks/useAutoSave";
|
|||||||
import {
|
import {
|
||||||
fetchMapColorThresholds,
|
fetchMapColorThresholds,
|
||||||
updateMapColorThresholds,
|
updateMapColorThresholds,
|
||||||
|
reloadConfig,
|
||||||
|
restartFail2Ban,
|
||||||
} from "../../api/config";
|
} from "../../api/config";
|
||||||
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
import { AutoSaveIndicator } from "./AutoSaveIndicator";
|
||||||
import { ServerHealthSection } from "./ServerHealthSection";
|
import { ServerHealthSection } from "./ServerHealthSection";
|
||||||
@@ -53,6 +57,10 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
const [flushing, setFlushing] = useState(false);
|
const [flushing, setFlushing] = useState(false);
|
||||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
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
|
// Map color thresholds
|
||||||
const [mapThresholds, setMapThresholds] = useState<MapColorThresholdsResponse | null>(null);
|
const [mapThresholds, setMapThresholds] = useState<MapColorThresholdsResponse | null>(null);
|
||||||
const [mapThresholdHigh, setMapThresholdHigh] = useState("");
|
const [mapThresholdHigh, setMapThresholdHigh] = useState("");
|
||||||
@@ -97,6 +105,38 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [flush]);
|
}, [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.
|
// Load map color thresholds on mount.
|
||||||
const loadMapThresholds = useCallback(async (): Promise<void> => {
|
const loadMapThresholds = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -263,6 +303,22 @@ export function ServerTab(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
{flushing ? "Flushing…" : "Flush Logs"}
|
{flushing ? "Flushing…" : "Flush Logs"}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
{msg && (
|
{msg && (
|
||||||
<MessageBar intent={msg.ok ? "success" : "error"}>
|
<MessageBar intent={msg.ok ? "success" : "error"}>
|
||||||
|
|||||||
Reference in New Issue
Block a user