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

@@ -123,6 +123,7 @@ backend/
│ │ └── import_log_repo.py # Import run history records
│ ├── tasks/ # APScheduler background jobs
│ │ ├── blocklist_import.py# Scheduled blocklist download and application
│ │ ├── geo_cache_flush.py # Periodic geo cache persistence (dirty-set flush to SQLite)
│ │ └── health_check.py # Periodic fail2ban connectivity probe
│ └── utils/ # Helpers, constants, shared types
│ ├── fail2ban_client.py # Async wrapper around the fail2ban socket protocol
@@ -200,6 +201,7 @@ APScheduler background jobs that run on a schedule without user interaction.
| Task | Purpose |
|---|---|
| `blocklist_import.py` | Downloads all enabled blocklist sources, validates entries, applies bans, records results in the import log |
| `geo_cache_flush.py` | Periodically flushes newly resolved IPs from the in-memory dirty set to the `geo_cache` SQLite table (default: every 60 seconds). GET requests populate only the in-memory cache; this task persists them without blocking any request. |
| `health_check.py` | Periodically pings the fail2ban socket and updates the cached server status so the frontend always has fresh data |
#### Utils (`app/utils/`)
@@ -586,6 +588,7 @@ BanGUI maintains its **own SQLite database** (separate from the fail2ban databas
|---|---|
| `settings` | Key-value store for application configuration (master password hash, fail2ban socket path, database path, timezone, session duration) |
| `sessions` | Active session tokens with expiry timestamps |
| `geo_cache` | Resolved IP geolocation results (ip, country_code, country_name, asn, org, cached_at). Loaded into memory at startup via `load_cache_from_db()`; new entries are flushed back by the `geo_cache_flush` background task. |
| `blocklist_sources` | Registered blocklist URLs (id, name, url, enabled, created_at, updated_at) |
| `import_logs` | Record of every blocklist import run (id, source_id, timestamp, ips_imported, ips_skipped, errors, status) |
@@ -606,6 +609,8 @@ BanGUI maintains its **own SQLite database** (separate from the fail2ban databas
- Session expiry is configurable (set during setup, stored in `settings`).
- The frontend `AuthProvider` checks session validity on mount and redirects to `/login` if invalid.
- The backend `dependencies.py` provides an `authenticated` dependency that validates the session cookie on every protected endpoint.
- **Session validation cache** — validated session tokens are cached in memory for 10 seconds (`_session_cache` dict in `dependencies.py`) to avoid a SQLite round-trip on every request from the same browser. The cache is invalidated immediately on logout.
- **Setup-completion flag** — once `is_setup_complete()` returns `True`, the result is stored in `app.state._setup_complete_cached`. The `SetupRedirectMiddleware` skips the DB query on all subsequent requests, removing 1 SQL query per request for the common post-setup case.
---
@@ -619,6 +624,7 @@ APScheduler 4.x (async mode) manages recurring background tasks.
│ (async, in-process) │
├──────────────────────┤
│ blocklist_import │ ── runs on configured schedule (default: daily 03:00)
│ geo_cache_flush │ ── runs every 60 seconds
│ health_check │ ── runs every 30 seconds
└──────────────────────┘
```

View File

@@ -353,7 +353,15 @@ Add the `BanTrendChart` to the dashboard page **above** the two country charts a
### Task 5.1 — Add a backend endpoint for ban counts per jail
**Status:** `not started`
**Status:** `done`
Added `GET /api/dashboard/bans/by-jail`. New Pydantic models `JailBanCount` and
`BansByJailResponse` added to `ban.py`. Service function `bans_by_jail()` in
`ban_service.py` queries the `bans` table with `GROUP BY jail ORDER BY COUNT(*) DESC`
and applies the origin filter. Route added to `dashboard.py`. 7 new service tests
(happy path, total equality, empty DB, time-window exclusion, origin filter variants)
and 10 new router tests — all pass, total suite 497 passed, 83% coverage.
`ruff check` and `mypy --strict` pass.
The existing `GET /api/jails` endpoint returns jail metadata with `status.currently_banned` — but this counts **currently active** bans, not historical bans in the selected time window. The dashboard needs historical ban counts per jail within the selected time range.
@@ -395,7 +403,16 @@ class BansByJailResponse(BaseModel):
### Task 5.2 — Create the `JailDistributionChart` component
**Status:** `not started`
**Status:** `done`
Created `frontend/src/components/JailDistributionChart.tsx` — a horizontal
bar chart using Recharts `BarChart` showing ban counts per jail sorted descending.
Added `JailBanCount`/`BansByJailResponse` types to `types/ban.ts`,
`dashboardBansByJail` constant to `api/endpoints.ts`, `fetchBansByJail()` to
`api/dashboard.ts`, and the `useJailDistribution` hook at
`hooks/useJailDistribution.ts`. Component handles loading (Spinner), error
(MessageBar), and empty states inline. `tsc --noEmit` and ESLint pass with zero
warnings.
Create `frontend/src/components/JailDistributionChart.tsx`. This component renders a **horizontal bar chart** showing the distribution of bans across jails.
@@ -422,7 +439,14 @@ Create `frontend/src/components/JailDistributionChart.tsx`. This component rende
### Task 5.3 — Integrate the jail distribution chart into `DashboardPage`
**Status:** `not started`
**Status:** `done`
Added a full-width "Jail Distribution" section card to `DashboardPage` below the
"Top Countries" section (2-column country charts on row 1, jail chart full-width
on row 2). The section renders `<JailDistributionChart timeRange={timeRange}
origin={originFilter} />`, sharing the same state already used by the other
charts. Loading, error, and empty states are handled inside
`JailDistributionChart` itself. `tsc --noEmit` and ESLint pass with zero warnings.
Add the `JailDistributionChart` as a third chart card alongside the two country charts, or in a second chart row below them if space is constrained.