Files
BanGUI/Docs/Tasks.md
Lukas 1efa0e973b Stage 10: external blocklist importer — backend + frontend
- blocklist_repo.py: CRUD for blocklist_sources table
- import_log_repo.py: add/list/get-last log entries
- blocklist_service.py: source CRUD, preview, import (download/validate/ban),
  import_all, schedule get/set/info
- blocklist_import.py: APScheduler task (hourly/daily/weekly schedule triggers)
- blocklist.py router: 9 endpoints (list/create/update/delete/preview/import/
  schedule-get+put/log)
- blocklist.py models: ScheduleFrequency (StrEnum), ScheduleConfig, ScheduleInfo,
  ImportSourceResult, ImportRunResult, PreviewResponse
- 59 new tests (18 repo + 19 service + 22 router); 374 total pass
- ruff clean, mypy clean for Stage 10 files
- types/blocklist.ts, api/blocklist.ts, hooks/useBlocklist.ts
- BlocklistsPage.tsx: source management, schedule picker, import log table
- Frontend tsc + ESLint clean
2026-03-01 15:33:24 +01:00

37 KiB

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.pycreate_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.pySettings 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.pyinit_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.pyFail2BanClient 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.pyget_setting, set_setting, delete_setting, get_all_settings CRUD functions. backend/app/repositories/session_repo.pycreate_session, get_session, delete_session, delete_expired_sessions. backend/app/services/setup_service.pyrun_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.pyGET /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.pylogin() 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.pyPOST /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.pyprobe(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.pyregister(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.pyGET /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.tsServerStatus and ServerStatusResponse interfaces. frontend/src/api/dashboard.tsfetchServerStatus(). frontend/src/hooks/useServerStatus.tsuseServerStatus() 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.pylist_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.pylookup(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.tsxServerStatusBar 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.pyGET /api/bans/active, POST /api/bans, DELETE /api/bans. backend/app/routers/geo.pyGET /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/jailsJailConfigListResponse
  • 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/globalGlobalConfigResponse
  • PUT /api/config/global → 204
  • POST /api/config/reload → 204
  • POST /api/config/regex-testRegexTestResponse
  • POST /api/config/jails/{name}/logpath → 204
  • POST /api/config/preview-logLogPreviewResponse

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/settingsServerSettingsResponse (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.ElementReact.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.pyTestGetJailConfig, TestListJailConfigs, TestUpdateJailConfig, TestGetGlobalConfig, TestUpdateGlobalConfig, TestTestRegex, TestPreviewLog
  • backend/tests/test_services/test_server_service.pyTestGetSettings, TestUpdateSettings, TestFlushLogs
  • backend/tests/test_routers/test_config.pyTestGetJailConfigs, TestGetJailConfig, TestUpdateJailConfig, TestGetGlobalConfig, TestUpdateGlobalConfig, TestReloadFail2ban, TestRegexTest, TestAddLogPath, TestPreviewLog
  • backend/tests/test_routers/test_server.pyTestGetServerSettings, 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 DONE

This stage exposes historical ban data from the fail2ban database for forensic exploration.

9.1 Implement the history service DONE

Built backend/app/services/history_service.py. list_history() queries the fail2ban DB with dynamic WHERE clauses: time range (range_=None = all-time, otherwise filters by timeofban >= now - delta), jail (exact match), IP (LIKE prefix %), and page/page_size. get_ip_detail() aggregates all ban events for a given IP into an IpDetailResponse with timeline, total bans, total failures, last_ban_at, and geo data — returns None if no records. Reuses _get_fail2ban_db_path, _parse_data_json, _ts_to_iso from ban_service. Also fixed a latent bug in _parse_data_json in ban_service.py: json.loads("null") returns Python None rather than a dict, causing AttributeError on .get(); fixed by checking isinstance(parsed, dict) before assigning obj.

9.2 Implement the history router DONE

Created backend/app/routers/history.py:

  • GET /api/history — paginated list with optional filters: range (TimeRange enum or omit for all-time), jail (exact), ip (prefix), page, page_size. Returns HistoryListResponse.
  • GET /api/history/{ip} — per-IP detail returning IpDetailResponse; raises HTTPException(404) if get_ip_detail() returns None.

Models defined in backend/app/models/history.py: HistoryBanItem, HistoryListResponse, IpTimelineEvent, IpDetailResponse. Results enriched with geo data via geo_service.lookup. Router registered in main.py.

9.3 Build the history page (frontend) DONE

Replaced placeholder frontend/src/pages/HistoryPage.tsx with full implementation. Filter bar: time-range Select (All time + 4 presets), jail Input, IP prefix Input, Apply/Clear buttons. FluentUI DataGrid table with columns: Banned At, IP (monospace, clickable), Jail, Country, Failures, Times Banned. Rows with ban_count ≥ 5 highlighted amber. Clicking an IP opens IpDetailView sub-component with summary grid and timeline Table. Pagination with ChevronLeft/ChevronRight buttons. Created supporting files: frontend/src/types/history.ts, frontend/src/api/history.ts, frontend/src/hooks/useHistory.ts (useHistory pagination hook + useIpHistory detail hook).

9.4 Write tests for history features DONE

Added tests/test_routers/test_history.py (11 tests) and tests/test_services/test_history_service.py (16 tests). Service tests use a real temporary SQLite DB seeded with 4 rows across two jails and 3 IPs. Router tests mock the service layer. Coverage: time-range filter, jail filter, IP prefix filter, combined filters, unknown IP → None, pagination, null data column, geo enrichment, 404 response, timeline aggregation, total_failures. All 317 backend tests pass (27 new), ruff clean, mypy clean (46 files). Frontend tsc --noEmit and npm run lint clean (0 errors/warnings).


Stage 10 — External Blocklist Importer DONE

This stage adds the ability to automatically download and apply external IP blocklists on a schedule.

10.1 Implement the blocklist repository DONE

backend/app/repositories/blocklist_repo.py — CRUD for blocklist_sources table (create_source, get_source, list_sources, list_enabled_sources, update_source, delete_source). backend/app/repositories/import_log_repo.pyadd_log, list_logs (paginated, optional source_id filter), get_last_log, compute_total_pages. 18 repository tests pass.

10.2 Implement the blocklist service DONE

backend/app/services/blocklist_service.py — source CRUD, preview_source (downloads max 64 KB, validates, returns sample), import_source (downloads, validates IPs via ipaddress, bans through fail2ban blocklist jail, logs result), import_all (iterates all enabled sources), get_schedule/set_schedule (JSON in settings table), get_schedule_info (config + last run + next run). 19 service tests pass.

10.3 Implement the blocklist import scheduled task DONE

backend/app/tasks/blocklist_import.py — APScheduler job with stable ID "blocklist_import". Supports three frequencies: hourly (interval trigger, every N hours), daily (cron trigger, UTC hour+minute), weekly (cron trigger, day_of_week+hour+minute). register(app) called at startup; reschedule(app) replaces the job after a schedule update.

10.4 Implement the blocklist router DONE

backend/app/routers/blocklist.py — 9 endpoints on /api/blocklists: list sources, create source (201), trigger manual import (POST /import), get schedule info, update schedule, paginated import log, get/update/delete single source, preview blocklist contents. Static path segments (/import, /schedule, /log) registered before /{source_id}. All endpoints authenticated. 22 router tests pass.

10.5 Build the blocklist management page (frontend) DONE

frontend/src/pages/BlocklistsPage.tsx (≈950 lines) — SourceFormDialog (create/edit), PreviewDialog, ImportResultDialog, SourcesSection (table with enable/disable switch), ScheduleSection (frequency picker + hour/minute/day-of-week selectors + next run display), ImportLogSection (paginated table with error badge). Supporting files: frontend/src/types/blocklist.ts, frontend/src/api/blocklist.ts, frontend/src/hooks/useBlocklist.ts. TypeScript + ESLint clean.

10.6 Write tests for blocklist features DONE

18 repository tests, 19 service tests, 22 router tests — 59 total. All 374 backend tests pass. ruff clean, mypy clean for Stage 10 files.


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.

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.

11.3 Add responsive layout polish

Review every page against the breakpoint table in Web-Design.md § 4. 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.

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).

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.

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.

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.

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 and Web-Development.md § 1.