Refactor service status response: migrate bangui_version into version field

This commit is contained in:
2026-03-22 21:42:08 +01:00
parent ed184f1c84
commit 798ed08ddd
15 changed files with 27 additions and 68 deletions

View File

@@ -1001,8 +1001,7 @@ class ServiceStatusResponse(BaseModel):
model_config = ConfigDict(strict=True) model_config = ConfigDict(strict=True)
online: bool = Field(..., description="Whether fail2ban is reachable via its socket.") online: bool = Field(..., description="Whether fail2ban is reachable via its socket.")
version: str | None = Field(default=None, description="fail2ban version string, or None when offline.") version: str | None = Field(default=None, description="BanGUI application version (or None when offline).")
bangui_version: str = Field(..., description="BanGUI application version.")
jail_count: int = Field(default=0, ge=0, description="Number of currently active jails.") jail_count: int = Field(default=0, ge=0, description="Number of currently active jails.")
total_bans: int = Field(default=0, ge=0, description="Aggregated current ban count across all jails.") total_bans: int = Field(default=0, ge=0, description="Aggregated current ban count across all jails.")
total_failures: int = Field(default=0, ge=0, description="Aggregated current failure count across all jails.") total_failures: int = Field(default=0, ge=0, description="Aggregated current failure count across all jails.")

View File

@@ -24,7 +24,6 @@ class ServerStatusResponse(BaseModel):
model_config = ConfigDict(strict=True) model_config = ConfigDict(strict=True)
status: ServerStatus status: ServerStatus
bangui_version: str = Field(..., description="BanGUI application version.")
class ServerSettings(BaseModel): class ServerSettings(BaseModel):

View File

@@ -70,7 +70,8 @@ async def get_server_status(
"server_status", "server_status",
ServerStatus(online=False), ServerStatus(online=False),
) )
return ServerStatusResponse(status=cached, bangui_version=__version__) cached.version = __version__
return ServerStatusResponse(status=cached)
@router.get( @router.get(

View File

@@ -819,8 +819,7 @@ async def get_service_status(
return ServiceStatusResponse( return ServiceStatusResponse(
online=server_status.online, online=server_status.online,
version=server_status.version, version=__version__,
bangui_version=__version__,
jail_count=server_status.active_jails, jail_count=server_status.active_jails,
total_bans=server_status.total_bans, total_bans=server_status.total_bans,
total_failures=server_status.total_failures, total_failures=server_status.total_failures,

View File

@@ -221,8 +221,8 @@ class TestFilterConfigImports:
class TestServiceStatusBanguiVersion: class TestServiceStatusBanguiVersion:
"""Bug 4: ``get_service_status`` must include ``bangui_version`` """Bug 4: ``get_service_status`` must include application version
in the ``ServiceStatusResponse`` it returns.""" in the ``version`` field of the ``ServiceStatusResponse``."""
async def test_online_response_contains_bangui_version(self) -> None: async def test_online_response_contains_bangui_version(self) -> None:
"""The returned model must contain the ``bangui_version`` field.""" """The returned model must contain the ``bangui_version`` field."""
@@ -256,11 +256,9 @@ class TestServiceStatusBanguiVersion:
probe_fn=AsyncMock(return_value=online_status), probe_fn=AsyncMock(return_value=online_status),
) )
assert hasattr(result, "bangui_version"), ( assert result.version == app.__version__, (
"ServiceStatusResponse is missing bangui_version " "ServiceStatusResponse must expose BanGUI version in version field"
"Pydantic will raise ValidationError → 500"
) )
assert result.bangui_version == app.__version__
async def test_offline_response_contains_bangui_version(self) -> None: async def test_offline_response_contains_bangui_version(self) -> None:
"""Even when fail2ban is offline, ``bangui_version`` must be present.""" """Even when fail2ban is offline, ``bangui_version`` must be present."""
@@ -275,4 +273,4 @@ class TestServiceStatusBanguiVersion:
probe_fn=AsyncMock(return_value=offline_status), probe_fn=AsyncMock(return_value=offline_status),
) )
assert result.bangui_version == app.__version__ assert result.version == app.__version__

View File

@@ -2001,8 +2001,7 @@ class TestGetServiceStatus:
def _mock_status(self, online: bool = True) -> ServiceStatusResponse: def _mock_status(self, online: bool = True) -> ServiceStatusResponse:
return ServiceStatusResponse( return ServiceStatusResponse(
online=online, online=online,
version="1.0.0" if online else None, version=app.__version__,
bangui_version=app.__version__,
jail_count=2 if online else 0, jail_count=2 if online else 0,
total_bans=10 if online else 0, total_bans=10 if online else 0,
total_failures=3 if online else 0, total_failures=3 if online else 0,
@@ -2021,7 +2020,7 @@ class TestGetServiceStatus:
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["online"] is True assert data["online"] is True
assert data["bangui_version"] == app.__version__ assert data["version"] == app.__version__
assert data["jail_count"] == 2 assert data["jail_count"] == 2
assert data["log_level"] == "INFO" assert data["log_level"] == "INFO"
@@ -2035,7 +2034,7 @@ class TestGetServiceStatus:
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["bangui_version"] == app.__version__ assert data["version"] == app.__version__
assert data["online"] is False assert data["online"] is False
assert data["log_level"] == "UNKNOWN" assert data["log_level"] == "UNKNOWN"

View File

@@ -153,8 +153,6 @@ class TestDashboardStatus:
body = response.json() body = response.json()
assert "status" in body assert "status" in body
assert "bangui_version" in body
assert body["bangui_version"] == app.__version__
status = body["status"] status = body["status"]
assert "online" in status assert "online" in status
@@ -171,9 +169,8 @@ class TestDashboardStatus:
body = response.json() body = response.json()
status = body["status"] status = body["status"]
assert body["bangui_version"] == app.__version__
assert status["online"] is True assert status["online"] is True
assert status["version"] == "1.0.2" assert status["version"] == app.__version__
assert status["active_jails"] == 2 assert status["active_jails"] == 2
assert status["total_bans"] == 10 assert status["total_bans"] == 10
assert status["total_failures"] == 5 assert status["total_failures"] == 5
@@ -187,9 +184,8 @@ class TestDashboardStatus:
body = response.json() body = response.json()
status = body["status"] status = body["status"]
assert body["bangui_version"] == app.__version__
assert status["online"] is False assert status["online"] is False
assert status["version"] is None assert status["version"] == app.__version__
assert status["active_jails"] == 0 assert status["active_jails"] == 0
assert status["total_bans"] == 0 assert status["total_bans"] == 0
assert status["total_failures"] == 0 assert status["total_failures"] == 0

View File

@@ -752,8 +752,7 @@ class TestGetServiceStatus:
from app import __version__ from app import __version__
assert result.online is True assert result.online is True
assert result.version == "1.0.0" assert result.version == __version__
assert result.bangui_version == __version__
assert result.jail_count == 2 assert result.jail_count == 2
assert result.total_bans == 5 assert result.total_bans == 5
assert result.total_failures == 3 assert result.total_failures == 3

View File

@@ -70,7 +70,7 @@ const useStyles = makeStyles({
*/ */
export function ServerStatusBar(): React.JSX.Element { export function ServerStatusBar(): React.JSX.Element {
const styles = useStyles(); const styles = useStyles();
const { status, banguiVersion, loading, error, refresh } = useServerStatus(); const { status, loading, error, refresh } = useServerStatus();
const cardStyles = useCardStyles(); const cardStyles = useCardStyles();
@@ -98,21 +98,13 @@ export function ServerStatusBar(): React.JSX.Element {
{/* Version */} {/* Version */}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{status?.version != null && ( {status?.version != null && (
<Tooltip content="fail2ban daemon version" relationship="description"> <Tooltip content="BanGUI version" relationship="description">
<Text size={200} className={styles.statValue}> <Text size={200} className={styles.statValue}>
v{status.version} v{status.version}
</Text> </Text>
</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) */}
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}

View File

@@ -41,7 +41,6 @@ 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(),
@@ -60,7 +59,6 @@ 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(),
@@ -78,7 +76,6 @@ 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(),
@@ -96,7 +93,6 @@ 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(),
@@ -105,7 +101,7 @@ describe("ServerStatusBar", () => {
expect(screen.getByText("v1.2.3")).toBeInTheDocument(); expect(screen.getByText("v1.2.3")).toBeInTheDocument();
}); });
it("renders a BanGUI version badge", () => { it("does not render a separate BanGUI version badge", () => {
mockedUseServerStatus.mockReturnValue({ mockedUseServerStatus.mockReturnValue({
status: { status: {
online: true, online: true,
@@ -114,13 +110,12 @@ describe("ServerStatusBar", () => {
total_bans: 0, total_bans: 0,
total_failures: 0, total_failures: 0,
}, },
banguiVersion: "9.9.9",
loading: false, loading: false,
error: null, error: null,
refresh: vi.fn(), refresh: vi.fn(),
}); });
renderBar(); renderBar();
expect(screen.getByText("BanGUI v9.9.9")).toBeInTheDocument(); expect(screen.queryByText("BanGUI v9.9.9")).toBeNull();
}); });
it("does not render the version element when version is null", () => { it("does not render the version element when version is null", () => {
@@ -132,7 +127,6 @@ 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(),
@@ -151,7 +145,6 @@ 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(),
@@ -167,7 +160,6 @@ 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(),

View File

@@ -352,12 +352,6 @@ 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>

View File

@@ -15,11 +15,10 @@ describe("ServerHealthSection", () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it("shows the BanGUI version in the service health panel", async () => { it("shows the version in the service health panel", async () => {
mockedFetchServiceStatus.mockResolvedValue({ mockedFetchServiceStatus.mockResolvedValue({
online: true, online: true,
version: "1.2.3", version: "1.2.3",
bangui_version: "1.2.3",
jail_count: 2, jail_count: 2,
total_bans: 5, total_bans: 5,
total_failures: 1, total_failures: 1,
@@ -41,11 +40,11 @@ describe("ServerHealthSection", () => {
</FluentProvider>, </FluentProvider>,
); );
// The service health panel should render and include the BanGUI version. // The service health panel should render and include the version.
const banGuiLabel = await screen.findByText("BanGUI"); const versionLabel = await screen.findByText("Version");
expect(banGuiLabel).toBeInTheDocument(); expect(versionLabel).toBeInTheDocument();
const banGuiCard = banGuiLabel.closest("div"); const versionCard = versionLabel.closest("div");
expect(banGuiCard).toHaveTextContent("1.2.3"); expect(versionCard).toHaveTextContent("1.2.3");
}); });
}); });

View File

@@ -18,8 +18,6 @@ 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`. */
@@ -35,7 +33,6 @@ 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);
@@ -47,7 +44,6 @@ 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) {
handleFetchError(err, setError, "Failed to fetch server status"); handleFetchError(err, setError, "Failed to fetch server status");
@@ -82,5 +78,5 @@ export function useServerStatus(): UseServerStatusResult {
void doFetch().catch((): void => undefined); void doFetch().catch((): void => undefined);
}, [doFetch]); }, [doFetch]);
return { status, banguiVersion, loading, error, refresh }; return { status, loading, error, refresh };
} }

View File

@@ -659,10 +659,8 @@ export interface Fail2BanLogResponse {
export interface ServiceStatusResponse { export interface ServiceStatusResponse {
/** Whether fail2ban is reachable via its socket. */ /** Whether fail2ban is reachable via its socket. */
online: boolean; online: boolean;
/** fail2ban version string, or null when offline. */ /** BanGUI application version (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. */

View File

@@ -21,6 +21,4 @@ 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;
} }