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

View File

@@ -1,20 +1,68 @@
/**
* Dashboard API module.
*
* Wraps the `GET /api/dashboard/status` endpoint.
* Wraps `GET /api/dashboard/status`, `GET /api/dashboard/bans`, and
* `GET /api/dashboard/accesses`.
*/
import { get } from "./client";
import { ENDPOINTS } from "./endpoints";
import type { AccessListResponse, DashboardBanListResponse, TimeRange } from "../types/ban";
import type { ServerStatusResponse } from "../types/server";
/**
* Fetch the cached fail2ban server status from the backend.
*
* @returns The server status response containing ``online``, ``version``,
* ``active_jails``, ``total_bans``, and ``total_failures``.
* @returns The server status response containing `online`, `version`,
* `active_jails`, `total_bans`, and `total_failures`.
* @throws {ApiError} When the server returns a non-2xx status.
*/
export async function fetchServerStatus(): Promise<ServerStatusResponse> {
return get<ServerStatusResponse>(ENDPOINTS.dashboardStatus);
}
/**
* Fetch a paginated ban list for the selected time window.
*
* @param range - Time-range preset: `"24h"`, `"7d"`, `"30d"`, or `"365d"`.
* @param page - 1-based page number (default `1`).
* @param pageSize - Items per page (default `100`).
* @returns Paginated {@link DashboardBanListResponse}.
* @throws {ApiError} When the server returns a non-2xx status.
*/
export async function fetchBans(
range: TimeRange,
page = 1,
pageSize = 100,
): Promise<DashboardBanListResponse> {
const params = new URLSearchParams({
range,
page: String(page),
page_size: String(pageSize),
});
return get<DashboardBanListResponse>(`${ENDPOINTS.dashboardBans}?${params.toString()}`);
}
/**
* Fetch a paginated access list (individual matched log lines) for the
* selected time window.
*
* @param range - Time-range preset.
* @param page - 1-based page number (default `1`).
* @param pageSize - Items per page (default `100`).
* @returns Paginated {@link AccessListResponse}.
* @throws {ApiError} When the server returns a non-2xx status.
*/
export async function fetchAccesses(
range: TimeRange,
page = 1,
pageSize = 100,
): Promise<AccessListResponse> {
const params = new URLSearchParams({
range,
page: String(page),
page_size: String(pageSize),
});
return get<AccessListResponse>(`/api/dashboard/accesses?${params.toString()}`);
}