Stage 11: polish, cross-cutting concerns & hardening

- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline
- 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone
- 11.3 Responsive sidebar: auto-collapse <640px, media query listener
- 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated
- 11.5 prefers-reduced-motion: disable sidebar transition
- 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries
- 11.7 Health transition logging (fail2ban_came_online/went_offline)
- 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502
- 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean
- Timezone endpoint: setup_service.get_timezone, 5 new tests
This commit is contained in:
2026-03-01 15:59:06 +01:00
parent 1efa0e973b
commit 1cdc97a729
19 changed files with 649 additions and 45 deletions

View File

@@ -35,6 +35,7 @@ from app.config import Settings, get_settings
from app.db import init_db
from app.routers import auth, bans, blocklist, config, dashboard, geo, health, history, jails, server, setup
from app.tasks import blocklist_import, health_check
from app.utils.fail2ban_client import Fail2BanConnectionError, Fail2BanProtocolError
# ---------------------------------------------------------------------------
# Ensure the bundled fail2ban package is importable from fail2ban-master/
@@ -166,6 +167,56 @@ async def _unhandled_exception_handler(
)
async def _fail2ban_connection_handler(
request: Request,
exc: Fail2BanConnectionError,
) -> JSONResponse:
"""Return a ``502 Bad Gateway`` response when fail2ban is unreachable.
Args:
request: The incoming FastAPI request.
exc: The :class:`~app.utils.fail2ban_client.Fail2BanConnectionError`.
Returns:
A :class:`fastapi.responses.JSONResponse` with status 502.
"""
log.warning(
"fail2ban_connection_error",
path=request.url.path,
method=request.method,
error=str(exc),
)
return JSONResponse(
status_code=502,
content={"detail": f"Cannot reach fail2ban: {exc}"},
)
async def _fail2ban_protocol_handler(
request: Request,
exc: Fail2BanProtocolError,
) -> JSONResponse:
"""Return a ``502 Bad Gateway`` response for fail2ban protocol errors.
Args:
request: The incoming FastAPI request.
exc: The :class:`~app.utils.fail2ban_client.Fail2BanProtocolError`.
Returns:
A :class:`fastapi.responses.JSONResponse` with status 502.
"""
log.warning(
"fail2ban_protocol_error",
path=request.url.path,
method=request.method,
error=str(exc),
)
return JSONResponse(
status_code=502,
content={"detail": f"fail2ban protocol error: {exc}"},
)
# ---------------------------------------------------------------------------
# Setup-redirect middleware
# ---------------------------------------------------------------------------
@@ -269,6 +320,11 @@ def create_app(settings: Settings | None = None) -> FastAPI:
app.add_middleware(SetupRedirectMiddleware)
# --- Exception handlers ---
# Ordered from most specific to least specific. FastAPI evaluates handlers
# in the order they were registered, so fail2ban network errors get a 502
# rather than falling through to the generic 500 handler.
app.add_exception_handler(Fail2BanConnectionError, _fail2ban_connection_handler) # type: ignore[arg-type]
app.add_exception_handler(Fail2BanProtocolError, _fail2ban_protocol_handler) # type: ignore[arg-type]
app.add_exception_handler(Exception, _unhandled_exception_handler)
# --- Routers ---