Files
BanGUI/Docs/Tasks.md
Lukas 54313fd3e0 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)
2026-03-01 14:53:49 +01:00

409 lines
36 KiB
Markdown

# BanGUI — Task List
This document breaks the entire BanGUI project into development stages, ordered so that each stage builds on the previous one. Every task is described in prose with enough detail for a developer to begin work. References point to the relevant documentation.
---
## Stage 1 — Project Scaffolding ✅ DONE
Everything in this stage is about creating the project skeleton — folder structures, configuration files, and tooling — so that development can begin on solid ground. No application logic is written here.
### 1.1 Initialise the backend project ✅
**Done.** Created `backend/` with the full directory structure from the docs. `pyproject.toml` configured with all required dependencies (FastAPI, Pydantic v2, aiosqlite, aiohttp, APScheduler 3.x, structlog, pydantic-settings, bcrypt) and dev dependencies (pytest, pytest-asyncio, httpx, ruff, mypy, pytest-cov). Ruff configured for 120-char lines and double-quote strings. mypy in strict mode. `.env.example` with all required placeholder keys. fail2ban-master path injected into `sys.path` at startup in `main.py`.
### 1.2 Initialise the frontend project ✅
**Done.** Vite + React + TypeScript project scaffolded in `frontend/`. Installed `@fluentui/react-components`, `@fluentui/react-icons`, `react-router-dom`. `tsconfig.json` with `"strict": true`. ESLint with `@typescript-eslint`, `eslint-plugin-react-hooks`, `eslint-config-prettier`. Prettier with project defaults. All required directories created: `src/api/`, `src/components/`, `src/hooks/`, `src/layouts/`, `src/pages/`, `src/providers/`, `src/theme/`, `src/types/`, `src/utils/`. `App.tsx` wraps app in `<FluentProvider>` and `<BrowserRouter>`.
### 1.3 Set up the Fluent UI custom theme ✅
**Done.** `frontend/src/theme/customTheme.ts` — BanGUI brand ramp centred on #0F6CBD (contrast ratio ≈ 5.4:1 against white, passes WCAG AA). Both `lightTheme` and `darkTheme` exported and wired into `App.tsx` via `FluentProvider`.
### 1.4 Create the central API client ✅
**Done.** `frontend/src/api/client.ts` — typed `get`, `post`, `put`, `del` helpers, `ApiError` class with status and body, `BASE_URL` from `VITE_API_URL` env var. `frontend/src/api/endpoints.ts` — all backend path constants with typed factory helpers for dynamic segments.
### 1.5 Create the FastAPI application factory ✅
**Done.** `backend/app/main.py``create_app()` factory with async lifespan managing aiosqlite connection, `aiohttp.ClientSession`, and APScheduler. Settings stored on `app.state`. Health-check router registered. Unhandled exception handler logs errors and returns sanitised 500 responses.
### 1.6 Create the Pydantic settings model ✅
**Done.** `backend/app/config.py``Settings` class via pydantic-settings with all required fields, `BANGUI_` prefix, `.env` loading. `get_settings()` factory function. App fails fast with a `ValidationError` if required values are missing.
### 1.7 Set up the application database schema ✅
**Done.** `backend/app/db.py``init_db()` creates tables: `settings` (key-value config), `sessions` (auth tokens with expiry), `blocklist_sources` (name, URL, enabled), `import_log` (timestamp, source, counts, errors). WAL mode and foreign keys enabled. Function is idempotent — safe to call on every startup.
### 1.8 Write the fail2ban socket client wrapper ✅
**Done.** `backend/app/utils/fail2ban_client.py``Fail2BanClient` async class. Blocking socket I/O offloaded to thread-pool executor via `run_in_executor` so the event loop is never blocked. `send()` serialises commands to pickle, reads until `<F2B_END_COMMAND>` marker, deserialises response. `ping()` helper. `Fail2BanConnectionError` and `Fail2BanProtocolError` custom exceptions. Full structlog integration.
---
## Stage 2 — Authentication & Setup Flow ✅ DONE
This stage implements the very first user experience: the setup wizard that runs on first launch and the login system that protects every subsequent visit. All other features depend on these being complete.
### 2.1 Implement the setup service and repository ✅
**Done.** `backend/app/repositories/settings_repo.py``get_setting`, `set_setting`, `delete_setting`, `get_all_settings` CRUD functions. `backend/app/repositories/session_repo.py``create_session`, `get_session`, `delete_session`, `delete_expired_sessions`. `backend/app/services/setup_service.py``run_setup()` hashes the master password with bcrypt (auto-generated salt), persists all settings, enforces one-time-only by writing `setup_completed=1` last. `is_setup_complete()` and `get_password_hash()` helpers.
### 2.2 Implement the setup router ✅
**Done.** `backend/app/routers/setup.py``GET /api/setup` returns `SetupStatusResponse`. `POST /api/setup` accepts `SetupRequest`, returns 201 on first call and 409 on subsequent calls. Registered in `create_app()`.
### 2.3 Implement the setup-redirect middleware ✅
**Done.** `SetupRedirectMiddleware` in `backend/app/main.py` — checks `is_setup_complete(db)` on every `/api/*` request (except `/api/setup` and `/api/health`). Returns `307 → /api/setup` when setup has not been completed. No-op after first run.
### 2.4 Implement the authentication service ✅
**Done.** `backend/app/services/auth_service.py``login()` verifies password with `bcrypt.checkpw`, generates a 64-char hex session token with `secrets.token_hex(32)`, stores the session via `session_repo`. `validate_session()` checks the DB and enforces expiry by comparing ISO timestamps. `logout()` deletes the session row.
### 2.5 Implement the auth router ✅
**Done.** `backend/app/routers/auth.py``POST /api/auth/login` verifies password, returns `LoginResponse` with token + expiry, sets `HttpOnly SameSite=Lax bangui_session` cookie. `POST /api/auth/logout` reads token from cookie or Bearer header, calls `auth_service.logout()`, clears the cookie. Both endpoints registered in `create_app()`.
### 2.6 Implement the auth dependency ✅
**Done.** `require_auth` dependency added to `backend/app/dependencies.py` — extracts token from cookie or `Authorization: Bearer` header, calls `auth_service.validate_session()`, raises 401 on missing/invalid/expired token. `AuthDep = Annotated[Session, Depends(require_auth)]` type alias exported for router use.
### 2.7 Build the setup page (frontend) ✅
**Done.** `frontend/src/pages/SetupPage.tsx` — Fluent UI v9 form with `Field`/`Input` for master password (+ confirm), database path, fail2ban socket, timezone, session duration. Client-side validation before submit. Calls `POST /api/setup` via `frontend/src/api/setup.ts`. Redirects to `/login` on success. `frontend/src/types/setup.ts` typed interfaces.
### 2.8 Build the login page (frontend) ✅
**Done.** `frontend/src/pages/LoginPage.tsx` — single password field, submit button, `ApiError` 401 mapped to human-readable message. After login calls `useAuth().login()` and navigates to `?next=` or `/`. `frontend/src/api/auth.ts` and `frontend/src/types/auth.ts` created.
### 2.9 Implement the auth context and route guard ✅
**Done.** `frontend/src/providers/AuthProvider.tsx` — React context with `isAuthenticated`, `login()`, `logout()`. Session token and expiry stored in `sessionStorage`. `useAuth()` hook exported. `frontend/src/components/RequireAuth.tsx` — wraps protected routes; redirects to `/login?next=<path>` when unauthenticated. `App.tsx` updated with full route tree: `/setup`, `/login`, `/` (guarded), `*` → redirect.
### 2.10 Write tests for setup and auth ✅
**Done.** 85 total tests pass. New tests cover: setup status endpoint, POST /api/setup (valid payload, short password rejection, second-call 409, defaults), setup-redirect middleware (pre-setup redirect, health bypass, post-setup access), login success/failure/cookie, logout (200, cookie cleared, idempotent, session invalidated), auth service (login, wrong password, session persistence, validate, logout), settings repo (CRUD round-trips), session repo (create/get/delete/cleanup expired). ruff 0 errors, mypy --strict 0 errors.
---
## Stage 3 — Application Shell & Navigation ✅ DONE
With authentication working, this stage builds the persistent layout that every page shares: the navigation sidebar, the header, and the routing skeleton.
### 3.1 Build the main layout component ✅
**Done.** `frontend/src/layouts/MainLayout.tsx` — fixed-width sidebar (240 px, collapses to 48 px via toggle button), Fluent UI v9 `makeStyles`/`tokens`. Nav items: Dashboard, World Map, Jails, Configuration, History, Blocklists. Active link highlighted using `NavLink` `isActive` callback. Logout button at the bottom. Main content area: `flex: 1`, `maxWidth: 1440px`, centred.
### 3.2 Set up client-side routing ✅
**Done.** `frontend/src/App.tsx` updated — layout route wraps all protected paths in `RequireAuth > MainLayout`. Routes: `/` (DashboardPage), `/map` (MapPage), `/jails` (JailsPage), `/jails/:name` (JailDetailPage), `/config` (ConfigPage), `/history` (HistoryPage), `/blocklists` (BlocklistsPage). Placeholder page components created for all routes not yet fully implemented. `*` falls back to `/`. tsc --noEmit: 0 errors.
### 3.3 Implement the logout flow ✅
**Done.** `MainLayout.tsx` logout button calls `useAuth().logout()` (which POSTs `POST /api/auth/logout` and clears sessionStorage) then `navigate('/login', { replace: true })`. Accessible from every authenticated page via the persistent sidebar.
---
## Stage 4 — fail2ban Connection & Server Status ✅ DONE
This stage establishes the live connection to the fail2ban daemon and surfaces its health to the user. It is a prerequisite for every data-driven feature.
### 4.1 Implement the health service ✅
**Done.** `backend/app/services/health_service.py``probe(socket_path)` sends `ping`, `version`, `status`, and per-jail `status <jail>` commands via `Fail2BanClient`. Aggregates `Currently failed` and `Currently banned` across all jails. Returns `ServerStatus(online=True/False)`. `Fail2BanConnectionError` and `Fail2BanProtocolError` mapped to `online=False`. `_ok()` helper extracts payload from `(return_code, data)` tuples; `_to_dict()` normalises fail2ban's list-of-pairs format.
### 4.2 Implement the health-check background task ✅
**Done.** `backend/app/tasks/health_check.py``register(app)` adds an APScheduler `interval` job that fires every 30 seconds (and immediately on startup via `next_run_time`). Result stored on `app.state.server_status`. `app.state.server_status` initialised to `ServerStatus(online=False)` as a safe placeholder. Wired into `main.py` lifespan after `scheduler.start()`.
### 4.3 Implement the dashboard status endpoint ✅
**Done.** `backend/app/routers/dashboard.py``GET /api/dashboard/status` reads `app.state.server_status` (falls back to `ServerStatus(online=False)` when not yet set). Response model `ServerStatusResponse` from `backend/app/models/server.py` (pre-existing). Requires `AuthDep`. Registered in `create_app()`.
### 4.4 Build the server status bar component (frontend) ✅
**Done.** `frontend/src/types/server.ts``ServerStatus` and `ServerStatusResponse` interfaces. `frontend/src/api/dashboard.ts``fetchServerStatus()`. `frontend/src/hooks/useServerStatus.ts``useServerStatus()` hook polling every 30 s and on window focus. `frontend/src/components/ServerStatusBar.tsx` — Fluent UI v9 `Badge`, `Text`, `Spinner`, `Tooltip`; green/red badge for online/offline; version, jail count, bans, failures stats; refresh button. `DashboardPage.tsx` updated to render `<ServerStatusBar />` at the top.
### 4.5 Write tests for health service and dashboard ✅
**Done.** 104 total tests pass (+19 new). `backend/tests/test_services/test_health_service.py` — 12 tests covering: online probe (version, jail count, ban/failure aggregation, empty jail list), connection error → offline, protocol error → offline, bad/error ping → offline, per-jail parse error tolerated, version failure tolerated. `backend/tests/test_routers/test_dashboard.py` — 6 tests covering: 200 when authenticated, 401 when unauthenticated, response shape, cached values returned, offline status, safe default when cache absent. fail2ban socket mocked via `unittest.mock.patch`. ruff 0 errors, mypy --strict 0 errors, tsc --noEmit 0 errors.
---
## Stage 5 — Ban Overview (Dashboard) ✅ DONE
The main landing page. This stage delivers the ban list and access list tables that give users a quick picture of recent activity.
### 5.1 Implement the ban service (list recent bans) ✅
**Done.** `backend/app/services/ban_service.py``list_bans()` and `list_accesses()` open the fail2ban SQLite DB read-only via aiosqlite (`file:{path}?mode=ro`). DB path is resolved by sending `["get", "dbfile"]` to the fail2ban Unix socket. Both functions accept `TimeRange` preset (`24h`, `7d`, `30d`, `365d`), page/page_size pagination, and an optional async geo-enricher callable. Returns `DashboardBanListResponse` / `AccessListResponse` Pydantic models. `_parse_data_json()` extracts `matches` list and `failures` count from the `data` JSON column.
### 5.2 Implement the geo service ✅
**Done.** `backend/app/services/geo_service.py``lookup(ip, http_session)` calls `http://ip-api.com/json/{ip}?fields=status,message,country,countryCode,org,as`. Returns `GeoInfo` dataclass (`country_code`, `country_name`, `asn`, `org`). Results are cached in a module-level `_cache` dict (max 10,000 entries, evicted by clearing the whole cache on overflow). Negative results (`status=fail`) are also cached. Network failures return `None` without caching. `clear_cache()` exposed for tests.
### 5.3 Implement the dashboard bans endpoint ✅
**Done.** Added `GET /api/dashboard/bans` and `GET /api/dashboard/accesses` to `backend/app/routers/dashboard.py`. Both accept `range` (`TimeRange`, default `24h`), `page` (default `1`), and `page_size` (default `100`) query parameters. Each endpoint reads `fail2ban_socket` from `app.state.settings` and `http_session` from `app.state`, creates a `geo_service.lookup` closure, and delegates to `ban_service`. All models in `backend/app/models/ban.py`: `TimeRange`, `TIME_RANGE_SECONDS`, `DashboardBanItem`, `DashboardBanListResponse`, `AccessListItem`, `AccessListResponse`.
### 5.4 Build the ban list table (frontend) ✅
**Done.** `frontend/src/components/BanTable.tsx` — Fluent UI v9 `DataGrid` with two modes (`"bans"` / `"accesses"`). Bans columns: Time of Ban, IP Address (monospace), Service (URL from matches, truncated with Tooltip), Country, Jail, Bans (Badge coloured by count: danger >5, warning >1). Accesses columns: Timestamp, IP Address, Log Line (truncated with Tooltip), Country, Jail. Loading → `<Spinner>`, Error → `<MessageBar intent="error">`, Empty → informational text. Pagination buttons. `useBans` hook (`frontend/src/hooks/useBans.ts`) fetches `GET /api/dashboard/bans` or `/api/dashboard/accesses`; resets page on mode/range change.
### 5.5 Build the dashboard page ✅
**Done.** `frontend/src/pages/DashboardPage.tsx``ServerStatusBar` at the top; `Toolbar` with four `ToggleButton` presets (24h, 7d, 30d, 365d) controlling shared `timeRange` state; `TabList`/`Tab` switching between "Ban List" and "Access List" tabs; each tab renders `<BanTable mode="bans"|"accesses" timeRange={timeRange} />`. `frontend/src/api/dashboard.ts` extended with `fetchBans()` and `fetchAccesses()`. `frontend/src/types/ban.ts` mirrors backend models.
### 5.6 Write tests for ban service and dashboard endpoints ✅
**Done.** 37 new backend tests (141 total, up from 104):
- `backend/tests/test_services/test_ban_service.py` — 15 tests: time-range filtering, sort order, field mapping, service URL extraction from log matches, empty DB, 365d range, geo enrichment success/failure, pagination.
- `backend/tests/test_services/test_geo_service.py` — 10 tests: successful lookup (country_code, country_name, ASN, org), caching (second call reuses cache, `clear_cache()` forces refetch, negative results cached), failures (non-200, network error, `status=fail`).
- `backend/tests/test_routers/test_dashboard.py` — 12 new tests: `GET /api/dashboard/bans` and `GET /api/dashboard/accesses` 200 (auth), 401 (unauth), response shape, default range, range forwarding, empty list.
All 141 tests pass; ruff and mypy --strict report zero errors; tsc --noEmit reports zero errors.
---
## Stage 6 — Jail Management ✅ DONE
This stage exposes fail2ban's jail system through the UI — listing jails, viewing details, and executing control commands.
### 6.1 Implement the jail service ✅
**Done.** `backend/app/services/jail_service.py` — ~990 lines. Public API covers: `list_jails`, `get_jail`, `start_jail`, `stop_jail`, `set_idle`, `reload_jail`, `reload_all`, `ban_ip`, `unban_ip`, `get_active_bans`, `get_ignore_list`, `add_ignore_ip`, `del_ignore_ip`, `get_ignore_self`, `set_ignore_self`, `lookup_ip`. Uses `asyncio.gather` for parallel per-jail queries. `_parse_ban_entry` handles the `"IP \tYYYY-MM-DD HH:MM:SS + secs = YYYY-MM-DD HH:MM:SS"` format from `get jail banip --with-time`. `JailNotFoundError` and `JailOperationError` custom exceptions. 40 service tests pass.
### 6.2 Implement the jails router ✅
**Done.** `backend/app/routers/jails.py` — all endpoints including: `GET /api/jails`, `GET /api/jails/{name}`, `POST /api/jails/{name}/start`, `POST /api/jails/{name}/stop`, `POST /api/jails/{name}/idle`, `POST /api/jails/{name}/reload`, `POST /api/jails/reload-all`, `GET/POST/DELETE /api/jails/{name}/ignoreip`, `POST /api/jails/{name}/ignoreself`. Models defined in `backend/app/models/jail.py`.
### 6.3 Implement ban and unban endpoints ✅
**Done.** `backend/app/routers/bans.py``GET /api/bans/active`, `POST /api/bans`, `DELETE /api/bans`. `backend/app/routers/geo.py``GET /api/geo/lookup/{ip}`. New `backend/app/models/geo.py` with `GeoDetail` and `IpLookupResponse`. All three routers registered in `main.py`.
### 6.4 Build the jail overview page (frontend) ✅
**Done.** `frontend/src/pages/JailsPage.tsx` fully implemented. Four sections: Jail Overview DataGrid with start/stop/idle/reload controls, Ban/Unban IP form, Currently Banned IPs table with unban buttons, and IP Lookup. Types in `frontend/src/types/jail.ts`. API module at `frontend/src/api/jails.ts`. Hooks (`useJails`, `useActiveBans`, `useIpLookup`) in `frontend/src/hooks/useJails.ts`.
### 6.5 Build the jail detail page (frontend) ✅
**Done.** `frontend/src/pages/JailDetailPage.tsx` fully implemented. Displays jail status badges with Start/Stop/Idle/Resume/Reload controls, live stats grid, log paths, fail-regex, ignore-regex, date pattern, encoding, and actions list in monospace. Breadcrumb navigation back to the jails list.
### 6.6 Build the ban/unban UI (frontend) ✅
**Done.** Ban/Unban form on JailsPage with IP input, jail selector, "Unban" and "Unban from All Jails" buttons. "Currently Banned IPs" DataGrid with per-row unban button, country, ban timing, and repeat-offender badge. MessageBar feedback on success/error.
### 6.7 Implement IP lookup endpoint and UI ✅
**Done.** `GET /api/geo/lookup/{ip}` returns currently-banned jails and geo info. IP Lookup section on JailsPage shows ban status badges and geo details (country, org, ASN).
### 6.8 Implement the ignore list (whitelist) endpoints and UI ✅
**Done.** All ignore-list endpoints implemented in the jails router. "Ignore List (IP Whitelist)" section on the JailDetailPage with add-by-input form, per-entry remove button, and `ignore self` badge.
### 6.9 Write tests for jail and ban features ✅
**Done.** `backend/tests/test_services/test_jail_service.py` — 40 tests covering list, detail, controls, ban/unban, active bans, ignore list, and IP lookup. `backend/tests/test_routers/test_jails.py`, `test_bans.py`, `test_geo.py` — 36 router tests. Total: 217 tests, all pass. Coverage 76%.
---
## Stage 7 — Configuration View ✅ DONE
This stage lets users inspect and edit fail2ban configuration directly from the web interface.
### 7.1 Implement the config service ✅ DONE
Built `backend/app/services/config_service.py` (~613 lines). Reads active jail config via parallel `asyncio.gather` across 10 socket commands per jail. Writes via `set <jail> <key> <val>` commands. `_replace_regex_list` diffs old/new patterns using `contextlib.suppress(ValueError)`. In-process regex validation via the `re` module with `ConfigValidationError` on failure. `test_regex` is synchronous/pure-Python (no socket). `preview_log` reads file tail via `_read_tail_lines` (executor) and pattern-tests each line. Custom exceptions: `JailNotFoundError`, `ConfigValidationError`, `ConfigOperationError`.
### 7.2 Implement the config router ✅ DONE
Created `backend/app/routers/config.py` (~310 lines) with 9 endpoints:
- `GET /api/config/jails``JailConfigListResponse`
- `GET /api/config/jails/{name}``JailConfigResponse` (404 on unknown jail)
- `PUT /api/config/jails/{name}` → 204 (422 on bad regex, 400 on socket error)
- `GET /api/config/global``GlobalConfigResponse`
- `PUT /api/config/global` → 204
- `POST /api/config/reload` → 204
- `POST /api/config/regex-test``RegexTestResponse`
- `POST /api/config/jails/{name}/logpath` → 204
- `POST /api/config/preview-log``LogPreviewResponse`
Models expanded in `backend/app/models/config.py`: `JailConfig`, `JailConfigResponse`, `JailConfigListResponse`, `JailConfigUpdate`, `GlobalConfigResponse`, `GlobalConfigUpdate`, `AddLogPathRequest`, `LogPreviewRequest`, `LogPreviewLine`, `LogPreviewResponse`.
### 7.3 Implement log observation endpoints ✅ DONE
`POST /api/config/jails/{name}/logpath` — adds a new log path via `set <jail> addlogpath <path> tail|head`. `POST /api/config/preview-log` — reads the last N lines from a server-side log file and tests each line against a provided fail-regex, returning `LogPreviewResponse` with per-line match status and aggregate counts.
### 7.4 Implement the regex tester endpoint ✅ DONE
`POST /api/config/regex-test` implemented as a stateless, synchronous endpoint (no socket). Compiles the provided pattern with `re.compile`, applies it to the sample log line, returns `RegexTestResponse` with `matched` bool, `groups` list, and `error` string on invalid regex.
### 7.5 Implement server settings endpoints ✅ DONE
Created `backend/app/services/server_service.py` (~165 lines) and `backend/app/routers/server.py` (~115 lines):
- `GET /api/server/settings``ServerSettingsResponse` (parallel gather of 6 settings)
- `PUT /api/server/settings` → 204
- `POST /api/server/flush-logs``{"message": str}`
Custom exception: `ServerOperationError`.
### 7.6 Build the configuration page (frontend) ✅ DONE
Created `frontend/src/pages/ConfigPage.tsx` with four tabs:
- **Jails** — Accordion of all jails, each expandable with editable ban_time/find_time/max_retry, `RegexList` component for fail_regex/ignore_regex (add/remove inline), read-only log_paths/backend/actions, Save button per jail, Reload fail2ban button.
- **Global** — log_level dropdown, log_target input, db_purge_age/db_max_matches number inputs, Save button.
- **Server** — same plus read-only db_path/syslog_socket, Flush Logs button.
- **Regex Tester** — pattern + log line inputs, "Test Pattern" button with match badge + groups, plus log file preview section.
### 7.7 Build the regex tester UI (frontend) ✅ DONE
"Regex Tester" tab in `ConfigPage.tsx`. Pattern input (monospace) + sample log-line Textarea. On click calls `POST /api/config/regex-test` via `useRegexTester` hook. Displays match/no-match `Badge` with icon and lists captured groups. Below it: log file preview form calling `POST /api/config/preview-log`, renders each line color-coded (green = matched, neutral = no match) with summary count.
### 7.8 Build the server settings UI (frontend) ✅ DONE
"Server" tab in `ConfigPage.tsx`. Shows all six settings editable (log_level dropdown, log_target, db_purge_age, db_max_matches) plus read-only db_path and syslog_socket fields. Includes "Flush Logs" button via `useServerSettings` hook. All via `frontend/src/api/config.ts` and `frontend/src/hooks/useConfig.ts`.
Also created `frontend/src/types/config.ts` (all TS interfaces) and fixed pre-existing lint errors across the codebase: deprecated `JSX.Element``React.JSX.Element` in 10 files, void/promise patterns in `useServerStatus.ts` and `useJails.ts`, `no-misused-spread` in `client.ts`, `eslint.config.ts` excluded from linting.
### 7.9 Write tests for configuration features ✅ DONE
285 backend tests pass (68 new vs 217 before Stage 7). New test files:
- `backend/tests/test_services/test_config_service.py``TestGetJailConfig`, `TestListJailConfigs`, `TestUpdateJailConfig`, `TestGetGlobalConfig`, `TestUpdateGlobalConfig`, `TestTestRegex`, `TestPreviewLog`
- `backend/tests/test_services/test_server_service.py``TestGetSettings`, `TestUpdateSettings`, `TestFlushLogs`
- `backend/tests/test_routers/test_config.py``TestGetJailConfigs`, `TestGetJailConfig`, `TestUpdateJailConfig`, `TestGetGlobalConfig`, `TestUpdateGlobalConfig`, `TestReloadFail2ban`, `TestRegexTest`, `TestAddLogPath`, `TestPreviewLog`
- `backend/tests/test_routers/test_server.py``TestGetServerSettings`, `TestUpdateServerSettings`, `TestFlushLogs`
Backend linters: `ruff check` clean, `mypy app/` clean (44 files). Frontend: `tsc --noEmit` clean, `eslint` clean (0 errors, 0 warnings).
---
## Stage 8 — World Map View ✅ DONE
A geographical visualisation of ban activity. This stage depends on the geo service from Stage 5 and the ban data pipeline from Stage 5.
### 8.1 Implement the map data endpoint ✅ DONE
Added `GET /api/dashboard/bans/by-country` to `backend/app/routers/dashboard.py`. Added `BansByCountryResponse` model (`countries: dict[str, int]`, `country_names: dict[str, str]`, `bans: list[DashboardBanItem]`, `total: int`) to `backend/app/models/ban.py`. Implemented `bans_by_country()` in `backend/app/services/ban_service.py` — fetches up to 2 000 bans from the window, deduplicates IPs, resolves geo concurrently with `asyncio.gather`, then aggregates by ISO alpha-2 country code.
### 8.2 Build the world map component (frontend) ✅ DONE
Created `frontend/src/data/isoNumericToAlpha2.ts` — static 249-entry mapping of ISO 3166-1 numeric codes (as used in world-atlas TopoJSON `geo.id`) to alpha-2 codes. Created `frontend/src/components/WorldMap.tsx` using `react-simple-maps@3.0.0`. Renders a Mercator SVG world map with per-country colour intensity scaled from the maximum ban count. Countries with bans show the count in text. Selected country highlighted with brand accent colour. Uses a nested `GeoLayer` component (inside `ComposableMap`) to call `useGeographies` within the map context. Clicking a country toggles its filter; clicking again clears it.
### 8.3 Build the map page (frontend) ✅ DONE
Replaced placeholder `frontend/src/pages/MapPage.tsx` with a full implementation. Includes a time-range `Select` (24h/7d/30d/365d), the `WorldMap` component, an active-filter info bar showing the selected country name and ban count with a "Clear filter" button, a summary line (total bans + number of countries), and a companion FluentUI `Table` filtered by selected country (columns: IP, Jail, Banned At, Country, Times Banned). Created `frontend/src/hooks/useMapData.ts` and `frontend/src/api/map.ts` with proper abort-controller cleanup and ESLint-clean void patterns. Created `frontend/src/types/map.ts` with `TimeRange`, `MapBanItem`, `BansByCountryResponse`.
### 8.4 Write tests for the map data endpoint ✅ DONE
Added `TestBansByCountry` class (5 tests) to `backend/tests/test_routers/test_dashboard.py`: `test_returns_200_when_authenticated`, `test_returns_401_when_unauthenticated`, `test_response_shape`, `test_accepts_time_range_param`, `test_empty_window_returns_empty_response`. Total backend tests: 290 (all passing). ruff clean, mypy clean (44 files). Frontend: `tsc --noEmit` clean, `eslint` 0 warnings/errors.
---
## Stage 9 — Ban History
This stage exposes historical ban data from the fail2ban database for forensic exploration.
### 9.1 Implement the history service
Build `backend/app/services/history_service.py`. Query the fail2ban database for all past ban records (not just currently active ones). Support filtering by jail, IP address, and time range. Compute ban count per IP to identify repeat offenders. Provide a per-IP timeline method that returns every ban event for a given IP: which jail triggered it, when it started, how long it lasted, and any matched log lines stored in the database. See [Features.md § 7](Features.md).
### 9.2 Implement the history router
Create `backend/app/routers/history.py`:
- `GET /api/history` — paginated list of all historical bans with filters (jail, IP, time range). Returns time, IP, jail, duration, ban count, country.
- `GET /api/history/{ip}` — per-IP detail: full ban timeline, total failures, matched log lines.
Define models in `backend/app/models/history.py`. Enrich results with geo data. See [Architekture.md § 2.2](Architekture.md).
### 9.3 Build the history page (frontend)
Create `frontend/src/pages/HistoryPage.tsx`. Display a `DataGrid` table of all past bans with columns for time, IP (monospace), jail, ban duration, ban count, and country. Add filter controls above the table: a jail dropdown, an IP search input, and the standard time-range selector. Highlight rows with high ban counts to flag repeat offenders. Clicking an IP row navigates to a per-IP detail view showing the full ban timeline and aggregated failures. See [Features.md § 7](Features.md).
### 9.4 Write tests for history features
Test history queries with various filters, per-IP timeline construction, ban count computation, and edge cases (IP with no history, jail that no longer exists).
---
## Stage 10 — External Blocklist Importer
This stage adds the ability to automatically download and apply external IP blocklists on a schedule.
### 10.1 Implement the blocklist repository
Build `backend/app/repositories/blocklist_repo.py` and `backend/app/repositories/import_log_repo.py`. The blocklist repo persists blocklist source definitions (name, URL, enabled flag) in the application database. The import log repo records every import run with timestamp, source URL, IPs imported, IPs skipped, and any errors encountered. See [Architekture.md § 2.2 (Repositories)](Architekture.md).
### 10.2 Implement the blocklist service
Build `backend/app/services/blocklist_service.py`. It manages blocklist source CRUD (add, edit, remove, toggle enabled). For the actual import: download each enabled source URL via aiohttp, validate every entry as a well-formed IP or CIDR range using the `ipaddress` module, skip malformed lines gracefully, and apply valid IPs as bans through fail2ban (in a dedicated blocklist jail) or via iptables. If using iptables, flush the chain before re-populating. Log every step with structlog. Record import results through the import log repository. Handle unreachable URLs by logging the error and continuing with remaining sources. See [Features.md § 8](Features.md).
### 10.3 Implement the blocklist import scheduled task
Create `backend/app/tasks/blocklist_import.py` — an APScheduler job that runs the blocklist service import at the configured schedule. The default is daily at 03:00. The schedule should be configurable through the blocklist service (saved in the app database). See [Features.md § 8 (Schedule)](Features.md).
### 10.4 Implement the blocklist router
Create `backend/app/routers/blocklist.py`:
- `GET /api/blocklists` — list all blocklist sources with their status.
- `POST /api/blocklists` — add a new source.
- `PUT /api/blocklists/{id}` — edit a source (name, URL, enabled).
- `DELETE /api/blocklists/{id}` — remove a source.
- `GET /api/blocklists/{id}/preview` — download and display a sample of the blocklist contents.
- `POST /api/blocklists/import` — trigger a manual import immediately ("Run Now").
- `GET /api/blocklists/schedule` — get the current schedule and next run time.
- `PUT /api/blocklists/schedule` — update the schedule.
- `GET /api/blocklists/log` — paginated import log, filterable by source and date range.
Define models in `backend/app/models/blocklist.py`. See [Architekture.md § 2.2](Architekture.md).
### 10.5 Build the blocklist management page (frontend)
Create `frontend/src/pages/BlocklistPage.tsx`. Display a list of blocklist sources as cards or rows showing name, URL, enabled toggle, and action buttons (edit, delete, preview). Add a form to create or edit a source. Show the schedule configuration with a simple time-and-frequency picker (no raw cron) — dropdowns for frequency preset and a time input. Include a "Run Now" button and a display of last import time and next scheduled run. Below, show the import log as a table (timestamp, source, IPs imported, IPs skipped, errors) with filters. If the most recent import had errors, show a warning badge in the navigation. See [Features.md § 8](Features.md).
### 10.6 Write tests for blocklist features
Test source CRUD, import with valid/invalid entries, schedule update, manual import trigger, import log persistence, and error handling when a URL is unreachable. Mock all HTTP calls.
---
## Stage 11 — Polish, Cross-Cutting Concerns & Hardening
This final stage covers everything that spans multiple features or improves the overall quality of the application.
### 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).
### 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).
### 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.
### 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).
### 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).
### 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).
### 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).
### 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).
### 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).