Stage 8: world map view — backend endpoint, map component, map page
- BansByCountryResponse model added to ban.py - bans_by_country() service: parallel geo lookup via asyncio.gather, aggregation by ISO alpha-2 country code (up to 2 000 bans) - GET /api/dashboard/bans/by-country endpoint in dashboard router - 290 tests pass (5 new), ruff + mypy clean (44 files) - isoNumericToAlpha2.ts: 249-entry ISO numeric → alpha-2 static map - types/map.ts, api/map.ts, hooks/useMapData.ts created - WorldMap.tsx: react-simple-maps Mercator SVG map, per-country ban count overlay, colour intensity scaling, country click filtering, GeoLayer nested-component pattern for useGeographies context - MapPage.tsx: time-range selector, WorldMap, country filter info bar, summary line, companion FluentUI Table with country filter - Frontend tsc + ESLint clean (0 errors/warnings)
This commit is contained in:
@@ -20,6 +20,7 @@ from fastapi import APIRouter, Query, Request
|
||||
from app.dependencies import AuthDep
|
||||
from app.models.ban import (
|
||||
AccessListResponse,
|
||||
BansByCountryResponse,
|
||||
DashboardBanListResponse,
|
||||
TimeRange,
|
||||
)
|
||||
@@ -156,3 +157,41 @@ async def get_dashboard_accesses(
|
||||
geo_enricher=_enricher,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/bans/by-country",
|
||||
response_model=BansByCountryResponse,
|
||||
summary="Return ban counts aggregated by country",
|
||||
)
|
||||
async def get_bans_by_country(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
|
||||
) -> BansByCountryResponse:
|
||||
"""Return ban counts aggregated by ISO country code.
|
||||
|
||||
Fetches up to 2 000 ban records in the selected time window, enriches
|
||||
every record with geo data, and returns a ``{country_code: count}`` map
|
||||
plus the full enriched ban list for the companion access table.
|
||||
|
||||
Args:
|
||||
request: The incoming request.
|
||||
_auth: Validated session dependency.
|
||||
range: Time-range preset.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.ban.BansByCountryResponse` with per-country
|
||||
aggregation and the full ban list.
|
||||
"""
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
http_session: aiohttp.ClientSession = request.app.state.http_session
|
||||
|
||||
async def _enricher(ip: str) -> geo_service.GeoInfo | None:
|
||||
return await geo_service.lookup(ip, http_session)
|
||||
|
||||
return await ban_service.bans_by_country(
|
||||
socket_path,
|
||||
range,
|
||||
geo_enricher=_enricher,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user