Display BanGUI version in dashboard and server config UI
This commit is contained in:
@@ -189,6 +189,8 @@ Add a `bangui_version` field to every API response that already carries the fail
|
|||||||
- All existing backend tests pass.
|
- All existing backend tests pass.
|
||||||
- Add one test per endpoint asserting that `bangui_version` matches `app.__version__`.
|
- 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
|
### 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`).
|
- No TypeScript compile errors (`tsc --noEmit`).
|
||||||
- Both values originate from the same API field (`bangui_version`) and therefore always match the backend version.
|
- Both values originate from the same API field (`bangui_version`) and therefore always match the backend version.
|
||||||
|
|
||||||
|
**Status:** ✅ Completed (2026-03-19)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const useStyles = makeStyles({
|
|||||||
*/
|
*/
|
||||||
export function ServerStatusBar(): React.JSX.Element {
|
export function ServerStatusBar(): React.JSX.Element {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { status, loading, error, refresh } = useServerStatus();
|
const { status, banguiVersion, loading, error, refresh } = useServerStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.bar} role="status" aria-label="fail2ban server status">
|
<div className={styles.bar} role="status" aria-label="fail2ban server status">
|
||||||
@@ -116,6 +116,14 @@ export function ServerStatusBar(): React.JSX.Element {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{banguiVersion != null && (
|
||||||
|
<Tooltip content="BanGUI version" relationship="description">
|
||||||
|
<Badge appearance="filled" size="small">
|
||||||
|
BanGUI v{banguiVersion}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ---------------------------------------------------------------- */}
|
{/* ---------------------------------------------------------------- */}
|
||||||
{/* Stats (only when online) */}
|
{/* Stats (only when online) */}
|
||||||
{/* ---------------------------------------------------------------- */}
|
{/* ---------------------------------------------------------------- */}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ describe("ServerStatusBar", () => {
|
|||||||
it("shows a spinner while the initial load is in progress", () => {
|
it("shows a spinner while the initial load is in progress", () => {
|
||||||
mockedUseServerStatus.mockReturnValue({
|
mockedUseServerStatus.mockReturnValue({
|
||||||
status: null,
|
status: null,
|
||||||
|
banguiVersion: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -59,6 +60,7 @@ describe("ServerStatusBar", () => {
|
|||||||
total_bans: 10,
|
total_bans: 10,
|
||||||
total_failures: 5,
|
total_failures: 5,
|
||||||
},
|
},
|
||||||
|
banguiVersion: "1.1.0",
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -76,6 +78,7 @@ describe("ServerStatusBar", () => {
|
|||||||
total_bans: 0,
|
total_bans: 0,
|
||||||
total_failures: 0,
|
total_failures: 0,
|
||||||
},
|
},
|
||||||
|
banguiVersion: "1.1.0",
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -93,6 +96,7 @@ describe("ServerStatusBar", () => {
|
|||||||
total_bans: 0,
|
total_bans: 0,
|
||||||
total_failures: 0,
|
total_failures: 0,
|
||||||
},
|
},
|
||||||
|
banguiVersion: "1.2.3",
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -101,6 +105,24 @@ describe("ServerStatusBar", () => {
|
|||||||
expect(screen.getByText("v1.2.3")).toBeInTheDocument();
|
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", () => {
|
it("does not render the version element when version is null", () => {
|
||||||
mockedUseServerStatus.mockReturnValue({
|
mockedUseServerStatus.mockReturnValue({
|
||||||
status: {
|
status: {
|
||||||
@@ -110,6 +132,7 @@ describe("ServerStatusBar", () => {
|
|||||||
total_bans: 0,
|
total_bans: 0,
|
||||||
total_failures: 0,
|
total_failures: 0,
|
||||||
},
|
},
|
||||||
|
banguiVersion: "1.2.3",
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -128,6 +151,7 @@ describe("ServerStatusBar", () => {
|
|||||||
total_bans: 21,
|
total_bans: 21,
|
||||||
total_failures: 99,
|
total_failures: 99,
|
||||||
},
|
},
|
||||||
|
banguiVersion: "1.0.0",
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
@@ -143,6 +167,7 @@ describe("ServerStatusBar", () => {
|
|||||||
it("renders an error message when the status fetch fails", () => {
|
it("renders an error message when the status fetch fails", () => {
|
||||||
mockedUseServerStatus.mockReturnValue({
|
mockedUseServerStatus.mockReturnValue({
|
||||||
status: null,
|
status: null,
|
||||||
|
banguiVersion: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: "Network error",
|
error: "Network error",
|
||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
|
|||||||
@@ -352,6 +352,12 @@ export function ServerHealthSection(): React.JSX.Element {
|
|||||||
<Text className={styles.statValue}>{status.version}</Text>
|
<Text className={styles.statValue}>{status.version}</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{status.bangui_version && (
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<Text className={styles.statLabel}>BanGUI</Text>
|
||||||
|
<Text className={styles.statValue}>{status.bangui_version}</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.statCard}>
|
<div className={styles.statCard}>
|
||||||
<Text className={styles.statLabel}>Active Jails</Text>
|
<Text className={styles.statLabel}>Active Jails</Text>
|
||||||
<Text className={styles.statValue}>{status.jail_count}</Text>
|
<Text className={styles.statValue}>{status.jail_count}</Text>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<FluentProvider theme={webLightTheme}>
|
||||||
|
<ServerHealthSection />
|
||||||
|
</FluentProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,6 +17,8 @@ const POLL_INTERVAL_MS = 30_000;
|
|||||||
export interface UseServerStatusResult {
|
export interface UseServerStatusResult {
|
||||||
/** The most recent server status snapshot, or `null` before the first fetch. */
|
/** The most recent server status snapshot, or `null` before the first fetch. */
|
||||||
status: ServerStatus | null;
|
status: ServerStatus | null;
|
||||||
|
/** BanGUI application version string. */
|
||||||
|
banguiVersion: string | null;
|
||||||
/** Whether a fetch is currently in flight. */
|
/** Whether a fetch is currently in flight. */
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** Error message string when the last fetch failed, otherwise `null`. */
|
/** Error message string when the last fetch failed, otherwise `null`. */
|
||||||
@@ -32,6 +34,7 @@ export interface UseServerStatusResult {
|
|||||||
*/
|
*/
|
||||||
export function useServerStatus(): UseServerStatusResult {
|
export function useServerStatus(): UseServerStatusResult {
|
||||||
const [status, setStatus] = useState<ServerStatus | null>(null);
|
const [status, setStatus] = useState<ServerStatus | null>(null);
|
||||||
|
const [banguiVersion, setBanguiVersion] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ export function useServerStatus(): UseServerStatusResult {
|
|||||||
try {
|
try {
|
||||||
const data = await fetchServerStatus();
|
const data = await fetchServerStatus();
|
||||||
setStatus(data.status);
|
setStatus(data.status);
|
||||||
|
setBanguiVersion(data.bangui_version);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to fetch server status");
|
setError(err instanceof Error ? err.message : "Failed to fetch server status");
|
||||||
@@ -77,5 +81,5 @@ export function useServerStatus(): UseServerStatusResult {
|
|||||||
void doFetch().catch((): void => undefined);
|
void doFetch().catch((): void => undefined);
|
||||||
}, [doFetch]);
|
}, [doFetch]);
|
||||||
|
|
||||||
return { status, loading, error, refresh };
|
return { status, banguiVersion, loading, error, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export function MainLayout(): React.JSX.Element {
|
|||||||
<div className={styles.sidebarFooter}>
|
<div className={styles.sidebarFooter}>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<Text className={styles.versionText}>
|
<Text className={styles.versionText}>
|
||||||
BanGUI v{__APP_VERSION__}
|
BanGUI
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -63,16 +63,16 @@ describe("MainLayout", () => {
|
|||||||
expect(screen.getByRole("navigation", { name: "Main navigation" })).toBeInTheDocument();
|
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();
|
renderLayout();
|
||||||
// __APP_VERSION__ is stubbed to "0.0.0-test" via vitest.config.ts define.
|
// __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();
|
renderLayout();
|
||||||
const toggleButton = screen.getByRole("button", { name: /collapse sidebar/i });
|
const toggleButton = screen.getByRole("button", { name: /collapse sidebar/i });
|
||||||
await userEvent.click(toggleButton);
|
await userEvent.click(toggleButton);
|
||||||
expect(screen.queryByText("BanGUI v0.0.0-test")).not.toBeInTheDocument();
|
expect(screen.queryByText("BanGUI")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -661,6 +661,8 @@ export interface ServiceStatusResponse {
|
|||||||
online: boolean;
|
online: boolean;
|
||||||
/** fail2ban version string, or null when offline. */
|
/** fail2ban version string, or null when offline. */
|
||||||
version: string | null;
|
version: string | null;
|
||||||
|
/** BanGUI application version (from the API). */
|
||||||
|
bangui_version: string;
|
||||||
/** Number of currently active jails. */
|
/** Number of currently active jails. */
|
||||||
jail_count: number;
|
jail_count: number;
|
||||||
/** Aggregated current ban count across all jails. */
|
/** Aggregated current ban count across all jails. */
|
||||||
|
|||||||
@@ -21,4 +21,6 @@ export interface ServerStatus {
|
|||||||
/** Response shape for ``GET /api/dashboard/status``. */
|
/** Response shape for ``GET /api/dashboard/status``. */
|
||||||
export interface ServerStatusResponse {
|
export interface ServerStatusResponse {
|
||||||
status: ServerStatus;
|
status: ServerStatus;
|
||||||
|
/** BanGUI application version (from the API). */
|
||||||
|
bangui_version: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user