Add jail distribution chart (Stage 5)

- backend: GET /api/dashboard/bans/by-jail endpoint
  - JailBanCount + BansByJailResponse Pydantic models in ban.py
  - bans_by_jail() service function with origin filter support
  - Route added to dashboard router
  - 17 new tests (7 service, 10 router); full suite 497 passed, 83% coverage

- frontend: JailDistributionChart component
  - JailBanCount / BansByJailResponse types in types/ban.ts
  - dashboardBansByJail endpoint constant in api/endpoints.ts
  - fetchBansByJail() in api/dashboard.ts
  - useJailDistribution hook in hooks/useJailDistribution.ts
  - JailDistributionChart component (horizontal bar chart, Recharts)
  - DashboardPage: full-width Jail Distribution section below Top Countries
This commit is contained in:
2026-03-11 17:01:19 +01:00
parent df0528b2c2
commit fe8eefa173
13 changed files with 799 additions and 6 deletions

View File

@@ -5,8 +5,9 @@ fail2ban server health snapshot. The snapshot is maintained by the
background health-check task and refreshed every 30 seconds.
Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table,
``GET /api/dashboard/bans/by-country`` for country aggregation, and
``GET /api/dashboard/bans/trend`` for time-bucketed ban counts.
``GET /api/dashboard/bans/by-country`` for country aggregation,
``GET /api/dashboard/bans/trend`` for time-bucketed ban counts, and
``GET /api/dashboard/bans/by-jail`` for per-jail ban counts.
"""
from __future__ import annotations
@@ -22,6 +23,7 @@ from app.dependencies import AuthDep
from app.models.ban import (
BanOrigin,
BansByCountryResponse,
BansByJailResponse,
BanTrendResponse,
DashboardBanListResponse,
TimeRange,
@@ -206,3 +208,39 @@ async def get_ban_trend(
socket_path: str = request.app.state.settings.fail2ban_socket
return await ban_service.ban_trend(socket_path, range, origin=origin)
@router.get(
"/bans/by-jail",
response_model=BansByJailResponse,
summary="Return ban counts aggregated by jail",
)
async def get_bans_by_jail(
request: Request,
_auth: AuthDep,
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
origin: BanOrigin | None = Query(
default=None,
description="Filter by ban origin: 'blocklist' or 'selfblock'. Omit for all.",
),
) -> BansByJailResponse:
"""Return ban counts grouped by jail name for the selected time window.
Queries the fail2ban database and returns a list of jails sorted by
ban count descending. This endpoint is intended for the dashboard jail
distribution bar chart.
Args:
request: The incoming request (used to access ``app.state``).
_auth: Validated session dependency.
range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or
``"365d"``.
origin: Optional filter by ban origin.
Returns:
:class:`~app.models.ban.BansByJailResponse` with per-jail counts
sorted descending and the total for the selected window.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
return await ban_service.bans_by_jail(socket_path, range, origin=origin)