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:
2026-03-01 15:09:22 +01:00
parent 54313fd3e0
commit b8f3a1c562
12 changed files with 2050 additions and 50 deletions

View File

@@ -0,0 +1,59 @@
/**
* TypeScript types for the ban history API.
*/
/** Optional time-range filter for history queries. */
export type TimeRange = "24h" | "7d" | "30d" | "365d";
/** A single row in the history ban-list table. */
export interface HistoryBanItem {
ip: string;
jail: string;
banned_at: string;
ban_count: number;
failures: number;
matches: string[];
country_code: string | null;
country_name: string | null;
asn: string | null;
org: string | null;
}
/** Paginated response from GET /api/history */
export interface HistoryListResponse {
items: HistoryBanItem[];
total: number;
page: number;
page_size: number;
}
/** A single ban event in a per-IP timeline. */
export interface IpTimelineEvent {
jail: string;
banned_at: string;
ban_count: number;
failures: number;
matches: string[];
}
/** Full historical record for a single IP address. */
export interface IpDetailResponse {
ip: string;
total_bans: number;
total_failures: number;
last_ban_at: string | null;
country_code: string | null;
country_name: string | null;
asn: string | null;
org: string | null;
timeline: IpTimelineEvent[];
}
/** Query parameters supported by GET /api/history */
export interface HistoryQuery {
range?: TimeRange;
jail?: string;
ip?: string;
page?: number;
page_size?: number;
}