Stage 9: ban history — backend service, router, frontend history page
- history.py models: HistoryBanItem, HistoryListResponse, IpTimelineEvent, IpDetailResponse
- history_service.py: list_history() with dynamic WHERE clauses (range/jail/ip
prefix/all-time), get_ip_detail() with timeline aggregation
- history.py router: GET /api/history + GET /api/history/{ip} (404 for unknown)
- Fixed latent bug in ban_service._parse_data_json: json.loads('null') -> None
-> AttributeError; now checks isinstance(parsed, dict) before assigning obj
- 317 tests pass (27 new), ruff + mypy clean (46 files)
- types/history.ts, api/history.ts, hooks/useHistory.ts created
- HistoryPage.tsx: filter bar (time range/jail/IP), DataGrid table,
high-ban-count row highlighting, per-IP IpDetailView with timeline,
pagination
- Frontend tsc + ESLint clean (0 errors/warnings)
- Tasks.md Stage 9 marked done
This commit is contained in:
@@ -300,29 +300,29 @@ Added `TestBansByCountry` class (5 tests) to `backend/tests/test_routers/test_da
|
||||
|
||||
---
|
||||
|
||||
## Stage 9 — Ban History
|
||||
## Stage 9 — Ban History ✅ DONE
|
||||
|
||||
This stage exposes historical ban data from the fail2ban database for forensic exploration.
|
||||
|
||||
### 9.1 Implement the history service
|
||||
### 9.1 Implement the history service ✅ DONE
|
||||
|
||||
Build `backend/app/services/history_service.py`. Query the fail2ban database for all past ban records (not just currently active ones). Support filtering by jail, IP address, and time range. Compute ban count per IP to identify repeat offenders. Provide a per-IP timeline method that returns every ban event for a given IP: which jail triggered it, when it started, how long it lasted, and any matched log lines stored in the database. See [Features.md § 7](Features.md).
|
||||
Built `backend/app/services/history_service.py`. `list_history()` queries the fail2ban DB with dynamic WHERE clauses: time range (`range_=None` = all-time, otherwise filters by `timeofban >= now - delta`), jail (exact match), IP (LIKE prefix `%`), and page/page_size. `get_ip_detail()` aggregates all ban events for a given IP into an `IpDetailResponse` with timeline, total bans, total failures, last_ban_at, and geo data — returns `None` if no records. Reuses `_get_fail2ban_db_path`, `_parse_data_json`, `_ts_to_iso` from `ban_service`. Also fixed a latent bug in `_parse_data_json` in `ban_service.py`: `json.loads("null")` returns Python `None` rather than a dict, causing `AttributeError` on `.get()`; fixed by checking `isinstance(parsed, dict)` before assigning `obj`.
|
||||
|
||||
### 9.2 Implement the history router
|
||||
### 9.2 Implement the history router ✅ DONE
|
||||
|
||||
Create `backend/app/routers/history.py`:
|
||||
- `GET /api/history` — paginated list of all historical bans with filters (jail, IP, time range). Returns time, IP, jail, duration, ban count, country.
|
||||
- `GET /api/history/{ip}` — per-IP detail: full ban timeline, total failures, matched log lines.
|
||||
Created `backend/app/routers/history.py`:
|
||||
- `GET /api/history` — paginated list with optional filters: `range` (`TimeRange` enum or omit for all-time), `jail` (exact), `ip` (prefix), `page`, `page_size`. Returns `HistoryListResponse`.
|
||||
- `GET /api/history/{ip}` — per-IP detail returning `IpDetailResponse`; raises `HTTPException(404)` if `get_ip_detail()` returns `None`.
|
||||
|
||||
Define models in `backend/app/models/history.py`. Enrich results with geo data. See [Architekture.md § 2.2](Architekture.md).
|
||||
Models defined in `backend/app/models/history.py`: `HistoryBanItem`, `HistoryListResponse`, `IpTimelineEvent`, `IpDetailResponse`. Results enriched with geo data via `geo_service.lookup`. Router registered in `main.py`.
|
||||
|
||||
### 9.3 Build the history page (frontend)
|
||||
### 9.3 Build the history page (frontend) ✅ DONE
|
||||
|
||||
Create `frontend/src/pages/HistoryPage.tsx`. Display a `DataGrid` table of all past bans with columns for time, IP (monospace), jail, ban duration, ban count, and country. Add filter controls above the table: a jail dropdown, an IP search input, and the standard time-range selector. Highlight rows with high ban counts to flag repeat offenders. Clicking an IP row navigates to a per-IP detail view showing the full ban timeline and aggregated failures. See [Features.md § 7](Features.md).
|
||||
Replaced placeholder `frontend/src/pages/HistoryPage.tsx` with full implementation. Filter bar: time-range `Select` (All time + 4 presets), jail `Input`, IP prefix `Input`, Apply/Clear buttons. FluentUI `DataGrid` table with columns: Banned At, IP (monospace, clickable), Jail, Country, Failures, Times Banned. Rows with `ban_count ≥ 5` highlighted amber. Clicking an IP opens `IpDetailView` sub-component with summary grid and timeline `Table`. Pagination with ChevronLeft/ChevronRight buttons. Created supporting files: `frontend/src/types/history.ts`, `frontend/src/api/history.ts`, `frontend/src/hooks/useHistory.ts` (`useHistory` pagination hook + `useIpHistory` detail hook).
|
||||
|
||||
### 9.4 Write tests for history features
|
||||
### 9.4 Write tests for history features ✅ DONE
|
||||
|
||||
Test history queries with various filters, per-IP timeline construction, ban count computation, and edge cases (IP with no history, jail that no longer exists).
|
||||
Added `tests/test_routers/test_history.py` (11 tests) and `tests/test_services/test_history_service.py` (16 tests). Service tests use a real temporary SQLite DB seeded with 4 rows across two jails and 3 IPs. Router tests mock the service layer. Coverage: time-range filter, jail filter, IP prefix filter, combined filters, unknown IP → `None`, pagination, null data column, geo enrichment, 404 response, timeline aggregation, total_failures. All 317 backend tests pass (27 new), ruff clean, mypy clean (46 files). Frontend `tsc --noEmit` and `npm run lint` clean (0 errors/warnings).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user