diff --git a/Docs/Tasks.md b/Docs/Tasks.md
index 916816d..9e63297 100644
--- a/Docs/Tasks.md
+++ b/Docs/Tasks.md
@@ -189,6 +189,8 @@ Add a `bangui_version` field to every API response that already carries the fail
- All existing backend tests pass.
- Add one test per endpoint asserting that `bangui_version` matches `app.__version__`.
+**Status:** ✅ Completed (2026-03-19)
+
---
### Task GV-3 — Display the BanGUI version on Dashboard and Configuration → Server
@@ -215,4 +217,6 @@ After GV-2 the API delivers `bangui_version`; this task makes the frontend show
- No TypeScript compile errors (`tsc --noEmit`).
- Both values originate from the same API field (`bangui_version`) and therefore always match the backend version.
+**Status:** ✅ Completed (2026-03-19)
+
---
diff --git a/frontend/src/components/ServerStatusBar.tsx b/frontend/src/components/ServerStatusBar.tsx
index c0851e8..5def4cb 100644
--- a/frontend/src/components/ServerStatusBar.tsx
+++ b/frontend/src/components/ServerStatusBar.tsx
@@ -83,7 +83,7 @@ const useStyles = makeStyles({
*/
export function ServerStatusBar(): React.JSX.Element {
const styles = useStyles();
- const { status, loading, error, refresh } = useServerStatus();
+ const { status, banguiVersion, loading, error, refresh } = useServerStatus();
return (
@@ -116,6 +116,14 @@ export function ServerStatusBar(): React.JSX.Element {
)}
+ {banguiVersion != null && (
+
+
+ BanGUI v{banguiVersion}
+
+
+ )}
+
{/* ---------------------------------------------------------------- */}
{/* Stats (only when online) */}
{/* ---------------------------------------------------------------- */}
diff --git a/frontend/src/components/__tests__/ServerStatusBar.test.tsx b/frontend/src/components/__tests__/ServerStatusBar.test.tsx
index a827bf8..a70ab47 100644
--- a/frontend/src/components/__tests__/ServerStatusBar.test.tsx
+++ b/frontend/src/components/__tests__/ServerStatusBar.test.tsx
@@ -41,6 +41,7 @@ describe("ServerStatusBar", () => {
it("shows a spinner while the initial load is in progress", () => {
mockedUseServerStatus.mockReturnValue({
status: null,
+ banguiVersion: null,
loading: true,
error: null,
refresh: vi.fn(),
@@ -59,6 +60,7 @@ describe("ServerStatusBar", () => {
total_bans: 10,
total_failures: 5,
},
+ banguiVersion: "1.1.0",
loading: false,
error: null,
refresh: vi.fn(),
@@ -76,6 +78,7 @@ describe("ServerStatusBar", () => {
total_bans: 0,
total_failures: 0,
},
+ banguiVersion: "1.1.0",
loading: false,
error: null,
refresh: vi.fn(),
@@ -93,6 +96,7 @@ describe("ServerStatusBar", () => {
total_bans: 0,
total_failures: 0,
},
+ banguiVersion: "1.2.3",
loading: false,
error: null,
refresh: vi.fn(),
@@ -101,6 +105,24 @@ describe("ServerStatusBar", () => {
expect(screen.getByText("v1.2.3")).toBeInTheDocument();
});
+ it("renders a BanGUI version badge", () => {
+ mockedUseServerStatus.mockReturnValue({
+ status: {
+ online: true,
+ version: "1.2.3",
+ active_jails: 1,
+ total_bans: 0,
+ total_failures: 0,
+ },
+ banguiVersion: "9.9.9",
+ loading: false,
+ error: null,
+ refresh: vi.fn(),
+ });
+ renderBar();
+ expect(screen.getByText("BanGUI v9.9.9")).toBeInTheDocument();
+ });
+
it("does not render the version element when version is null", () => {
mockedUseServerStatus.mockReturnValue({
status: {
@@ -110,6 +132,7 @@ describe("ServerStatusBar", () => {
total_bans: 0,
total_failures: 0,
},
+ banguiVersion: "1.2.3",
loading: false,
error: null,
refresh: vi.fn(),
@@ -128,6 +151,7 @@ describe("ServerStatusBar", () => {
total_bans: 21,
total_failures: 99,
},
+ banguiVersion: "1.0.0",
loading: false,
error: null,
refresh: vi.fn(),
@@ -143,6 +167,7 @@ describe("ServerStatusBar", () => {
it("renders an error message when the status fetch fails", () => {
mockedUseServerStatus.mockReturnValue({
status: null,
+ banguiVersion: null,
loading: false,
error: "Network error",
refresh: vi.fn(),
diff --git a/frontend/src/components/config/ServerHealthSection.tsx b/frontend/src/components/config/ServerHealthSection.tsx
index 65c352e..039ac0d 100644
--- a/frontend/src/components/config/ServerHealthSection.tsx
+++ b/frontend/src/components/config/ServerHealthSection.tsx
@@ -352,6 +352,12 @@ export function ServerHealthSection(): React.JSX.Element {
{status.version}
)}
+ {status.bangui_version && (
+
+ BanGUI
+ {status.bangui_version}
+
+ )}
Active Jails
{status.jail_count}
diff --git a/frontend/src/components/config/__tests__/ServerHealthSection.test.tsx b/frontend/src/components/config/__tests__/ServerHealthSection.test.tsx
new file mode 100644
index 0000000..45575c9
--- /dev/null
+++ b/frontend/src/components/config/__tests__/ServerHealthSection.test.tsx
@@ -0,0 +1,51 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { FluentProvider, webLightTheme } from "@fluentui/react-components";
+import { ServerHealthSection } from "../ServerHealthSection";
+
+vi.mock("../../../api/config");
+
+import { fetchFail2BanLog, fetchServiceStatus } from "../../../api/config";
+
+const mockedFetchServiceStatus = vi.mocked(fetchServiceStatus);
+const mockedFetchFail2BanLog = vi.mocked(fetchFail2BanLog);
+
+describe("ServerHealthSection", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("shows the BanGUI version in the service health panel", async () => {
+ mockedFetchServiceStatus.mockResolvedValue({
+ online: true,
+ version: "1.2.3",
+ bangui_version: "1.2.3",
+ jail_count: 2,
+ total_bans: 5,
+ total_failures: 1,
+ log_level: "INFO",
+ log_target: "STDOUT",
+ });
+
+ mockedFetchFail2BanLog.mockResolvedValue({
+ log_path: "/var/log/fail2ban.log",
+ lines: ["2026-01-01 fail2ban[123]: INFO Test"],
+ total_lines: 1,
+ log_level: "INFO",
+ log_target: "STDOUT",
+ });
+
+ render(
+
+
+ ,
+ );
+
+ // The service health panel should render and include the BanGUI version.
+ const banGuiLabel = await screen.findByText("BanGUI");
+ expect(banGuiLabel).toBeInTheDocument();
+
+ const banGuiCard = banGuiLabel.closest("div");
+ expect(banGuiCard).toHaveTextContent("1.2.3");
+ });
+});
diff --git a/frontend/src/hooks/useServerStatus.ts b/frontend/src/hooks/useServerStatus.ts
index fbbe43f..f4a37fd 100644
--- a/frontend/src/hooks/useServerStatus.ts
+++ b/frontend/src/hooks/useServerStatus.ts
@@ -17,6 +17,8 @@ const POLL_INTERVAL_MS = 30_000;
export interface UseServerStatusResult {
/** The most recent server status snapshot, or `null` before the first fetch. */
status: ServerStatus | null;
+ /** BanGUI application version string. */
+ banguiVersion: string | null;
/** Whether a fetch is currently in flight. */
loading: boolean;
/** Error message string when the last fetch failed, otherwise `null`. */
@@ -32,6 +34,7 @@ export interface UseServerStatusResult {
*/
export function useServerStatus(): UseServerStatusResult {
const [status, setStatus] = useState
(null);
+ const [banguiVersion, setBanguiVersion] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -43,6 +46,7 @@ export function useServerStatus(): UseServerStatusResult {
try {
const data = await fetchServerStatus();
setStatus(data.status);
+ setBanguiVersion(data.bangui_version);
setError(null);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to fetch server status");
@@ -77,5 +81,5 @@ export function useServerStatus(): UseServerStatusResult {
void doFetch().catch((): void => undefined);
}, [doFetch]);
- return { status, loading, error, refresh };
+ return { status, banguiVersion, loading, error, refresh };
}
diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx
index 305a476..833875e 100644
--- a/frontend/src/layouts/MainLayout.tsx
+++ b/frontend/src/layouts/MainLayout.tsx
@@ -313,7 +313,7 @@ export function MainLayout(): React.JSX.Element {
{!collapsed && (
- BanGUI v{__APP_VERSION__}
+ BanGUI
)}
{
expect(screen.getByRole("navigation", { name: "Main navigation" })).toBeInTheDocument();
});
- it("shows the BanGUI version in the sidebar footer when expanded", () => {
+ it("does not show the BanGUI application version in the sidebar footer", () => {
renderLayout();
// __APP_VERSION__ is stubbed to "0.0.0-test" via vitest.config.ts define.
- expect(screen.getByText("BanGUI v0.0.0-test")).toBeInTheDocument();
+ expect(screen.queryByText(/BanGUI v/)).not.toBeInTheDocument();
});
- it("hides the BanGUI version text when the sidebar is collapsed", async () => {
+ it("hides the logo text when the sidebar is collapsed", async () => {
renderLayout();
const toggleButton = screen.getByRole("button", { name: /collapse sidebar/i });
await userEvent.click(toggleButton);
- expect(screen.queryByText("BanGUI v0.0.0-test")).not.toBeInTheDocument();
+ expect(screen.queryByText("BanGUI")).not.toBeInTheDocument();
});
});
diff --git a/frontend/src/types/config.ts b/frontend/src/types/config.ts
index 372c772..ec47f65 100644
--- a/frontend/src/types/config.ts
+++ b/frontend/src/types/config.ts
@@ -661,6 +661,8 @@ export interface ServiceStatusResponse {
online: boolean;
/** fail2ban version string, or null when offline. */
version: string | null;
+ /** BanGUI application version (from the API). */
+ bangui_version: string;
/** Number of currently active jails. */
jail_count: number;
/** Aggregated current ban count across all jails. */
diff --git a/frontend/src/types/server.ts b/frontend/src/types/server.ts
index 7c9525f..272c538 100644
--- a/frontend/src/types/server.ts
+++ b/frontend/src/types/server.ts
@@ -21,4 +21,6 @@ export interface ServerStatus {
/** Response shape for ``GET /api/dashboard/status``. */
export interface ServerStatusResponse {
status: ServerStatus;
+ /** BanGUI application version (from the API). */
+ bangui_version: string;
}