feat: implement dashboard ban overview (Stage 5)

- Add ban_service reading fail2ban SQLite DB via read-only aiosqlite
- Add geo_service resolving IPs via ip-api.com with 10k in-memory cache
- Add GET /api/dashboard/bans and GET /api/dashboard/accesses endpoints
- Add TimeRange, DashboardBanItem, DashboardBanListResponse, AccessListItem,
  AccessListResponse models in models/ban.py
- Build BanTable component (Fluent UI DataGrid) with bans/accesses modes,
  pagination, loading/error/empty states, and ban-count badges
- Build useBans hook managing time-range and pagination state
- Update DashboardPage: status bar + time-range toolbar + tab switcher
- Add 37 new backend tests (ban service, geo service, dashboard router)
- All 141 tests pass; ruff/mypy --strict/tsc --noEmit clean
This commit is contained in:
2026-03-01 12:57:19 +01:00
parent 94661d7877
commit 9ac7f8d22d
15 changed files with 2346 additions and 29 deletions

113
frontend/src/types/ban.ts Normal file
View File

@@ -0,0 +1,113 @@
/**
* TypeScript interfaces mirroring the backend ban Pydantic models.
*
* `backend/app/models/ban.py` — dashboard dashboard sections.
*/
// ---------------------------------------------------------------------------
// Time-range selector
// ---------------------------------------------------------------------------
/** The four supported time-range presets for dashboard views. */
export type TimeRange = "24h" | "7d" | "30d" | "365d";
/** Human-readable labels for each time-range preset. */
export const TIME_RANGE_LABELS: Record<TimeRange, string> = {
"24h": "Last 24 h",
"7d": "Last 7 days",
"30d": "Last 30 days",
"365d": "Last 365 days",
} as const;
// ---------------------------------------------------------------------------
// Ban-list table item
// ---------------------------------------------------------------------------
/**
* A single row in the dashboard ban-list table.
*
* Mirrors `DashboardBanItem` from `backend/app/models/ban.py`.
*/
export interface DashboardBanItem {
/** Banned IP address. */
ip: string;
/** Jail that issued the ban. */
jail: string;
/** ISO 8601 UTC timestamp of the ban. */
banned_at: string;
/** First matched log line (context for the ban), or null. */
service: string | null;
/** ISO 3166-1 alpha-2 country code, or null if unknown. */
country_code: string | null;
/** Human-readable country name, or null if unknown. */
country_name: string | null;
/** Autonomous System Number string, e.g. "AS3320", or null. */
asn: string | null;
/** Organisation name associated with the IP, or null. */
org: string | null;
/** How many times this IP was banned. */
ban_count: number;
}
/**
* Paginated ban-list response from `GET /api/dashboard/bans`.
*
* Mirrors `DashboardBanListResponse` from `backend/app/models/ban.py`.
*/
export interface DashboardBanListResponse {
/** Ban items for the current page. */
items: DashboardBanItem[];
/** Total number of bans in the selected time window. */
total: number;
/** Current 1-based page number. */
page: number;
/** Maximum items per page. */
page_size: number;
}
// ---------------------------------------------------------------------------
// Access-list table item
// ---------------------------------------------------------------------------
/**
* A single row in the dashboard access-list table.
*
* Each row represents one matched log line (failure attempt) that
* contributed to a ban.
*
* Mirrors `AccessListItem` from `backend/app/models/ban.py`.
*/
export interface AccessListItem {
/** IP address of the access event. */
ip: string;
/** Jail that recorded the access. */
jail: string;
/** ISO 8601 UTC timestamp of the ban that captured this access. */
timestamp: string;
/** Raw matched log line. */
line: string;
/** ISO 3166-1 alpha-2 country code, or null. */
country_code: string | null;
/** Human-readable country name, or null. */
country_name: string | null;
/** ASN string, or null. */
asn: string | null;
/** Organisation name, or null. */
org: string | null;
}
/**
* Paginated access-list response from `GET /api/dashboard/accesses`.
*
* Mirrors `AccessListResponse` from `backend/app/models/ban.py`.
*/
export interface AccessListResponse {
/** Access items for the current page. */
items: AccessListItem[];
/** Total number of access events in the selected window. */
total: number;
/** Current 1-based page number. */
page: number;
/** Maximum items per page. */
page_size: number;
}