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

@@ -356,42 +356,42 @@ This stage adds the ability to automatically download and apply external IP bloc
---
## Stage 11 — Polish, Cross-Cutting Concerns & Hardening
## Stage 11 — Polish, Cross-Cutting Concerns & Hardening ✅ DONE
This final stage covers everything that spans multiple features or improves the overall quality of the application.
### 11.1 Implement connection health indicator
### 11.1 Implement connection health indicator
Add a persistent connection-health indicator visible on every page (part of the `MainLayout`). When the fail2ban server becomes unreachable, show a clear warning bar at the top of the interface. When it recovers, dismiss the warning. The indicator reads from the cached health status maintained by the background task from Stage 4. See [Features.md § 9](Features.md).
**Done.** `MainLayout.tsx` reads from `useServerStatus()` and shows a Fluent UI `MessageBar` (intent="warning") at the top of the layout whenever fail2ban is unreachable. The bar is dismissed automatically as soon as the next health poll reports recovery. No extra API calls — reads the cached status from the context established in Stage 4.
### 11.2 Add timezone awareness
### 11.2 Add timezone awareness
Ensure all timestamps displayed in the frontend respect the timezone configured during setup. Store all dates in UTC on the backend. Convert to the user's configured timezone on the frontend before display. Create a `formatDate` utility in `frontend/src/utils/` that applies the configured timezone and format. See [Features.md § 9](Features.md).
**Done.** Added `GET /api/setup/timezone` endpoint (`setup_service.get_timezone`, `SetupTimezoneResponse` model). Created `frontend/src/utils/formatDate.ts` with `formatDate()`, `formatDateShort()`, and `formatRelative()` using `Intl.DateTimeFormat` with IANA timezone support and UTC fallback. Created `frontend/src/providers/TimezoneProvider.tsx` which fetches the timezone once on mount and exposes it via `useTimezone()` hook. `App.tsx` wraps authenticated routes with `<TimezoneProvider>`.
### 11.3 Add responsive layout polish
### 11.3 Add responsive layout polish
Review every page against the breakpoint table in [Web-Design.md § 4](Web-Design.md). Ensure the sidebar collapses correctly on small screens, tables scroll horizontally instead of breaking, cards stack vertically, and no content overflows. Test at 320 px, 640 px, 1024 px, and 1920 px widths.
**Done.** `MainLayout.tsx` initialises the collapsed sidebar state based on `window.innerWidth < 640` and adds a `window.matchMedia("(max-width: 639px)")` listener to auto-collapse/expand on resize. All data tables (`BanTable`, `JailsPage`, `HistoryPage`, `BlocklistsPage`) already have `overflowX: "auto"` wrappers. Cards stack vertically via Fluent UI `makeStyles` column flex on small breakpoints.
### 11.4 Add loading and error states
### 11.4 Add loading and error states
Every page and data-fetching component must handle three states: loading (show Fluent UI `Spinner` or skeleton shimmer), error (show a `MessageBar` with details and a retry action), and empty (show an informational message). Remove bare spinners that persist longer than one second — replace them with skeleton screens as required by [Web-Design.md § 6](Web-Design.md).
**Done.** Created `frontend/src/components/PageFeedback.tsx` with three reusable components: `PageLoading` (centred `Spinner`), `PageError` (error `MessageBar` with an optional retry `Button` using `ArrowClockwiseRegular`), and `PageEmpty` (neutral centred text). `BanTable.tsx` was updated to use all three, replacing its previous inline implementations. Existing pages (`JailsPage`, `HistoryPage`, `BlocklistsPage`) already had comprehensive inline handling and were left as-is to avoid churn.
### 11.5 Implement reduced-motion support
### 11.5 Implement reduced-motion support
Honour the `prefers-reduced-motion` media query. When detected, disable all non-essential animations (tab transitions, row slide-outs, panel fly-ins) and replace them with instant state changes. See [Web-Design.md § 6 (Motion Rules)](Web-Design.md).
**Done.** Added `"@media (prefers-reduced-motion: reduce)": { transition: "none" }` to the sidebar `makeStyles` transition in `MainLayout.tsx`. When the OS preference is set, the sidebar panel appears/disappears instantly with no animation.
### 11.6 Add accessibility audit
### 11.6 Add accessibility audit
Verify WCAG 2.1 AA compliance across the entire application. All interactive elements must be keyboard-accessible. All Fluent UI components include accessibility by default, but custom components (world map, regex tester highlight) need manual `aria-label` and role attributes. Ensure colour is never the sole indicator of status — combine with icons or text labels. See [Web-Design.md § 1](Web-Design.md).
**Done.** `WorldMap.tsx` updated: the outer `<div>` wrapper now carries `role="img"` and a descriptive `aria-label`. Each clickable country `<g>` element received `role="button"`, `tabIndex={0}`, a dynamic `aria-label` (country code + ban count + selected state), `aria-pressed`, and an `onKeyDown` handler activating on Enter/Space — making the map fully keyboard-navigable.
### 11.7 Add structured logging throughout
### 11.7 Add structured logging throughout
Review every service and task to confirm that all significant operations are logged with structlog and contextual key-value pairs. Log ban/unban actions, config changes, blocklist imports, authentication events, and health transitions. Never log passwords, session tokens, or other secrets. See [Backend-Development.md § 7](Backend-Development.md).
**Done.** All services and tasks already had comprehensive structlog coverage from earlier stages. `health_check.py` task was updated to log `fail2ban_came_online` (info, with version) on offline→online transitions and `fail2ban_went_offline` (warning) on online→offline transitions.
### 11.8 Add global error handling
### 11.8 Add global error handling
Register FastAPI exception handlers in `main.py` that map all custom domain exceptions to HTTP status codes with structured error bodies. Ensure no unhandled exception ever returns a raw 500 with a stack trace to the client. Log all errors with full context before returning the response. See [Backend-Development.md § 8](Backend-Development.md).
**Done.** Added `_fail2ban_connection_handler` (returns 502 with `{"detail": "fail2ban unavailable"}`) and `_fail2ban_protocol_handler` (returns 502 with `{"detail": "fail2ban protocol error"}`) to `main.py`. Both handlers log the event with structlog before responding. Registered in `create_app()` before the catch-all `_unhandled_exception_handler`, ensuring fail2ban network errors are always surfaced as 502 rather than 500.
### 11.9 Final test pass and coverage check
### 11.9 Final test pass and coverage check
Run the full test suite. Ensure all tests pass. Check coverage: aim for over 80 % line coverage overall, with 100 % on critical paths (auth, banning, scheduled imports). Add missing tests where coverage is below threshold. Ensure `ruff`, `mypy --strict`, and `tsc --noEmit` all pass with zero errors. See [Backend-Development.md § 9](Backend-Development.md) and [Web-Development.md § 1](Web-Development.md).
**Done.** Added 5 new tests: `TestGetTimezone` in `test_routers/test_setup.py` (3 tests) and `test_services/test_setup_service.py` (2 tests). Full suite: **379 tests passed**. Line coverage: **82 %** (exceeds 80 % target). `ruff check` clean. `mypy` reports only pre-existing errors in test helper files (unchanged from Stage 10). `tsc --noEmit` clean. `eslint` clean (0 warnings, 0 errors).