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
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
Create the backend/ directory with the full folder structure defined in Backend-Development.md § 3. Set up pyproject.toml with all required dependencies (FastAPI, Pydantic v2, aiosqlite, aiohttp, APScheduler 4.x, structlog, pydantic-settings) and dev dependencies (pytest, pytest-asyncio, httpx, ruff, mypy). Configure ruff for 120-character line length and double-quote strings. Configure mypy in strict mode. Add a .env.example with placeholder keys for BANGUI_DATABASE_PATH, BANGUI_FAIL2BAN_SOCKET, and BANGUI_SESSION_SECRET. Make sure the bundled fail2ban client at ./fail2ban-master is importable by configuring the path in pyproject.toml or a startup shim as described in Backend-Development.md § 2.
1.2 Initialise the frontend project
Scaffold a Vite + React + TypeScript project inside frontend/. Install @fluentui/react-components, @fluentui/react-icons, and react-router-dom. Set up tsconfig.json with "strict": true. Configure ESLint with @typescript-eslint, eslint-plugin-react-hooks, and eslint-config-prettier. Add Prettier with the project defaults. Create the directory structure from Web-Development.md § 4: src/api/, src/components/, src/hooks/, src/layouts/, src/pages/, src/providers/, src/theme/, src/types/, src/utils/. Create a minimal App.tsx that wraps the application in <FluentProvider> and <BrowserRouter> as shown in Web-Development.md § 5.
1.3 Set up the Fluent UI custom theme
Create the light and dark brand-colour themes inside frontend/src/theme/. Follow the colour rules in Web-Design.md § 2: use the Fluent UI Theme Designer to generate a brand ramp, ensure the primary colour meets the 4.5 : 1 contrast ratio, and export both lightTheme and darkTheme. Wire the theme into App.tsx via the FluentProvider theme prop.
1.4 Create the central API client
Build the typed API client in frontend/src/api/client.ts. It should be a thin wrapper around fetch that returns typed responses, includes credentials, and throws a custom ApiError on non-OK responses. Define the BASE_URL from import.meta.env.VITE_API_URL with a fallback to "/api". Create frontend/src/api/endpoints.ts for path constants. See Web-Development.md § 3 for the pattern.
1.5 Create the FastAPI application factory
Implement backend/app/main.py with the create_app() factory function. Register the async lifespan context manager that opens the aiosqlite database connection, creates a shared aiohttp.ClientSession, and initialises the APScheduler instance on startup, then closes all three on shutdown. Store these on app.state. Register a placeholder router so the app can start and respond to a health-check request. See Backend-Development.md § 6 and Architekture.md § 2 for details.
1.6 Create the Pydantic settings model
Implement backend/app/config.py using pydantic-settings. Define the Settings class with fields for database_path, fail2ban_socket, session_secret, session_duration_minutes, and timezone. Load from environment variables prefixed BANGUI_ and from .env. Validate at startup — the app must fail fast with a clear error if required values are missing. See Backend-Development.md § 11.
1.7 Set up the application database schema
Design and create the SQLite schema for BanGUI's own data. The database needs tables for application settings (key-value pairs for master password hash, database path, fail2ban socket path, preferences), sessions (token, created-at, expires-at), blocklist sources (name, URL, enabled flag), and import log entries (timestamp, source URL, IPs imported, IPs skipped, errors). Write an initialisation function that creates these tables on first run via aiosqlite. This schema is for BanGUI's internal state — it does not replace the fail2ban database. See Architekture.md § 2.2 for the repository breakdown.
1.8 Write the fail2ban socket client wrapper
Implement backend/app/utils/fail2ban_client.py — an async wrapper around the fail2ban Unix domain socket protocol. Study ./fail2ban-master/fail2ban/client/csocket.py and ./fail2ban-master/fail2ban/client/fail2banclient.py to understand the wire protocol (pickle-based command/response). The wrapper should provide async methods for sending commands and receiving responses, handle connection errors gracefully, and log every interaction with structlog. This module is the single point of contact between BanGUI and the fail2ban daemon. See Backend-Development.md § 2 (fail2ban Client Usage) and Architekture.md § 2.2 (Utils).
Stage 2 — Authentication & Setup Flow
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
Build backend/app/services/setup_service.py and backend/app/repositories/settings_repo.py. The setup service accepts the initial configuration (master password, database path, fail2ban socket path, general preferences), hashes the password with a secure algorithm (e.g. bcrypt or argon2), and persists everything through the settings repository. It must enforce the one-time-only rule: once a configuration is saved, setup cannot run again. Add a method to check whether setup has been completed (i.e. whether any configuration exists in the database). See Features.md § 1.
2.2 Implement the setup router
Create backend/app/routers/setup.py with a POST /api/setup endpoint that accepts a Pydantic request model containing all setup fields and delegates to the setup service. If setup has already been completed, return a 409 Conflict. Define request and response models in backend/app/models/setup.py.
2.3 Implement the setup-redirect middleware
Add middleware to the FastAPI app that checks on every incoming request whether setup has been completed. If not, redirect all requests (except those to /api/setup itself) to /api/setup with a 307 Temporary Redirect or return a 403 with a clear message. Once setup is done, the middleware becomes a no-op. See Features.md § 1.
2.4 Implement the authentication service
Build backend/app/services/auth_service.py. It must verify the master password against the stored hash, create session tokens on successful login, store sessions through backend/app/repositories/session_repo.py, validate tokens on every subsequent request, and enforce session expiry. Sessions should be stored in the SQLite database so they survive server restarts. See Features.md § 2 and Architekture.md § 2.2.
2.5 Implement the auth router
Create backend/app/routers/auth.py with two endpoints: POST /api/auth/login (accepts a password, returns a session token or sets a cookie) and POST /api/auth/logout (invalidates the session). Define request and response models in backend/app/models/auth.py.
2.6 Implement the auth dependency
Create a FastAPI dependency in backend/app/dependencies.py that extracts the session token from the request (cookie or header), validates it through the auth service, and either returns the authenticated session or raises a 401 Unauthorized. Every protected router must declare this dependency. See Backend-Development.md § 4 for the Depends pattern.
2.7 Build the setup page (frontend)
Create frontend/src/pages/SetupPage.tsx. The page should present a form with fields for the master password (with confirmation), database path, fail2ban socket path, and general preferences (timezone, date format, session duration). Use Fluent UI form components (Input, Button, Field, Dropdown for timezone). On submission, call POST /api/setup through the API client. Show validation errors inline. After successful setup, redirect to the login page. Create the corresponding API function in frontend/src/api/setup.ts and types in frontend/src/types/setup.ts. See Features.md § 1 and Web-Design.md § 8 for component choices.
2.8 Build the login page (frontend)
Create frontend/src/pages/LoginPage.tsx. A single password input and a submit button — no username field. On submission, call POST /api/auth/login. On success, store the session (cookie or context) and redirect to the originally requested page or the dashboard. Show an error message on wrong password. Create frontend/src/api/auth.ts and frontend/src/types/auth.ts. See Features.md § 2.
2.9 Implement the auth context and route guard
Create frontend/src/providers/AuthProvider.tsx that manages authentication state (logged in / not logged in) and exposes login, logout, and session-check methods via React context. Create a route guard component that wraps all protected routes: if the user is not authenticated, redirect to the login page and remember the intended destination. After login, redirect back. See Features.md § 2 and Web-Development.md § 7.
2.10 Write tests for setup and auth
Write backend tests covering: setup endpoint accepts valid data, setup endpoint rejects a second call, login succeeds with correct password, login fails with wrong password, protected endpoints reject unauthenticated requests, logout invalidates the session for both router and service. Use pytest-asyncio and httpx AsyncClient as described in Backend-Development.md § 9.
Stage 3 — Application Shell & Navigation
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
Create frontend/src/layouts/MainLayout.tsx. This is the outer shell visible on every authenticated page. It contains a fixed-width sidebar navigation (240 px, collapsing to 48 px on small screens) and a main content area. Use the Fluent UI Nav component for the sidebar with groups for Dashboard, World Map, Jails, Configuration, History, Blocklists, and a Logout action at the bottom. The layout must be responsive following the breakpoints in Web-Design.md § 4. The main content area is capped at 1440 px and centred on wide screens.
3.2 Set up client-side routing
Configure React Router in frontend/src/App.tsx (or a dedicated AppRoutes.tsx). Define routes for every page: / (dashboard), /map, /jails, /jails/:name, /config, /history, /blocklists, /setup, /login. Wrap all routes except setup and login inside the auth guard from Stage 2. Use the MainLayout for authenticated routes. Create placeholder page components for each route so navigation works end to end.
3.3 Implement the logout flow
Wire the Logout button in the sidebar to call POST /api/auth/logout, clear the client-side session state, and redirect to the login page. The logout option must be accessible from every page as specified in Features.md § 2.
Stage 4 — fail2ban Connection & Server Status
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
Build backend/app/services/health_service.py. It connects to the fail2ban socket using the wrapper from Stage 1.8, sends a status command, and parses the response to extract: whether the server is reachable, the fail2ban version, the number of active jails, and aggregated ban/failure counts. Expose a method that returns a structured health status object. Log connectivity changes (online → offline and vice versa) via structlog. See Features.md § 3 (Server Status Bar).
4.2 Implement the health-check background task
Create backend/app/tasks/health_check.py — an APScheduler job that runs the health service probe every 30 seconds and caches the result in memory (e.g. on app.state). This ensures the dashboard endpoint can return fresh status without blocking on a socket call. See Architekture.md § 2.2 (Tasks).
4.3 Implement the dashboard status endpoint
Create backend/app/routers/dashboard.py with a GET /api/dashboard/status endpoint that returns the cached server status (online/offline, version, jail count, total bans, total failures). Define response models in backend/app/models/server.py. This endpoint is lightweight — it reads from the in-memory cache populated by the health-check task.
4.4 Build the server status bar component (frontend)
Create frontend/src/components/ServerStatusBar.tsx. This persistent bar appears at the top of the dashboard (and optionally on other pages). It displays the fail2ban connection status (green badge for online, red for offline), the server version, active jail count, and total bans/failures. Use Fluent UI Badge and Text components. Poll GET /api/dashboard/status at a reasonable interval or on page focus. Create frontend/src/api/dashboard.ts, frontend/src/types/server.ts, and a useServerStatus hook.
4.5 Write tests for health service and dashboard
Test that the health service correctly parses a mock fail2ban status response, handles socket errors gracefully, and that the dashboard endpoint returns the expected shape. Mock the fail2ban socket — tests must never touch a real daemon.
Stage 5 — Ban Overview (Dashboard)
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)
Build backend/app/services/ban_service.py with a method that queries the fail2ban database for bans within a given time range. The fail2ban SQLite database stores ban records — read them using aiosqlite (open the fail2ban DB path from settings, read-only). Return structured ban objects including IP, jail, timestamp, and any additional metadata available. See Features.md § 3 (Ban List).
5.2 Implement the geo service
Build backend/app/services/geo_service.py. Given an IP address, resolve its country of origin (and optionally ASN and RIR). Use an external API via aiohttp or a local GeoIP database. Cache results to avoid repeated lookups for the same IP. The geo service is used throughout the application wherever country information is displayed. See Features.md § 5 (IP Lookup) and Architekture.md § 2.2.
5.3 Implement the dashboard bans endpoint
Add GET /api/dashboard/bans to backend/app/routers/dashboard.py. It accepts a time-range query parameter (hours or a preset like 24h, 7d, 30d, 365d). It calls the ban service to retrieve bans in that window, enriches each ban with country data from the geo service, and returns a paginated list. Define request/response models in backend/app/models/ban.py.
5.4 Build the ban list table (frontend)
Create frontend/src/components/BanTable.tsx using Fluent UI DataGrid. Columns: time of ban, IP address (monospace), requested URL/service, country, domain, subdomain. Rows are sorted newest-first. Above the table, place a time-range selector implemented as a Toolbar with ToggleButton for the four presets (24 h, 7 d, 30 d, 365 d). Create a useBans hook that calls GET /api/dashboard/bans with the selected range. See Features.md § 3 (Ban List) and Web-Design.md § 8 (Data Display).
5.5 Build the dashboard page
Create frontend/src/pages/DashboardPage.tsx. Compose the server status bar at the top, then a Pivot (tab control) switching between "Ban List" and "Access List". The Ban List tab renders the BanTable. The Access List tab uses the same table component but fetches all recorded accesses, not just bans. If the access list requires a separate endpoint, add GET /api/dashboard/accesses to the backend with the same time-range support. See Features.md § 3.
5.6 Write tests for ban service and dashboard endpoints
Test ban queries for each time-range preset, test that geo enrichment works with mocked API responses, and test that the endpoint returns the correct response shape. Verify edge cases: no bans in the selected range, an IP that fails geo lookup.
Stage 6 — Jail Management
This stage exposes fail2ban's jail system through the UI — listing jails, viewing details, and executing control commands.
6.1 Implement the jail service
Build backend/app/services/jail_service.py. Using the fail2ban socket client, implement methods to: list all jails with their status and key metrics, retrieve the full detail of a single jail (log paths, regex patterns, date pattern, encoding, actions, ban-time escalation settings), start a jail, stop a jail, toggle idle mode, reload a single jail, and reload all jails. Each method sends the appropriate command through the socket wrapper and parses the response. See Features.md § 5 (Jail Overview, Jail Detail, Jail Controls).
6.2 Implement the jails router
Create backend/app/routers/jails.py:
GET /api/jails— list all jails with status and metrics.GET /api/jails/{name}— full detail for a single jail.POST /api/jails/{name}/start— start a jail.POST /api/jails/{name}/stop— stop a jail.POST /api/jails/{name}/idle— toggle idle mode.POST /api/jails/{name}/reload— reload a single jail.POST /api/jails/reload-all— reload all jails.
Define request/response models in backend/app/models/jail.py. Use appropriate HTTP status codes (404 if a jail name does not exist, 409 if a jail is already in the requested state). See Architekture.md § 2.2 (Routers).
6.3 Implement ban and unban endpoints
Add to backend/app/routers/bans.py:
POST /api/bans— ban an IP in a specified jail. Validate the IP withipaddressbefore sending.DELETE /api/bans— unban an IP from a specific jail or all jails. Support anunban_allflag.GET /api/bans/active— list all currently banned IPs across all jails, with jail name, ban start time, expiry, and ban count.
Delegate to the ban service. See Features.md § 5 (Ban an IP, Unban an IP, Currently Banned IPs).
6.4 Build the jail overview page (frontend)
Create frontend/src/pages/JailsPage.tsx. Display a card or table for each jail showing name, status badge (running/stopped/idle), backend type, banned count, total bans, failure counts, find time, ban time, and max retries. Each jail links to a detail view. Use Fluent UI Card or DataGrid. Create frontend/src/api/jails.ts, frontend/src/types/jail.ts, and a useJails hook. See Features.md § 5 (Jail Overview).
6.5 Build the jail detail page (frontend)
Create frontend/src/pages/JailDetailPage.tsx — reached via /jails/:name. Fetch the full jail detail and display: monitored log paths, fail regex and ignore regex lists (rendered in monospace), date pattern, log encoding, attached actions and their config, and ban-time escalation settings. Include control buttons (Start, Stop, Idle, Reload) that call the corresponding API endpoints with confirmation dialogs (Fluent UI Dialog). See Features.md § 5 (Jail Detail, Jail Controls).
6.6 Build the ban/unban UI (frontend)
On the Jails page (or a dedicated sub-section), add a "Ban an IP" form with an IP input field and a jail selector dropdown. Add an "Unban an IP" form with an IP input (or selection from the currently-banned list), a jail selector (or "all jails"), and an "unban all" option. Show success/error feedback using Fluent UI MessageBar or Toast. Build a "Currently Banned IPs" table showing IP, jail, ban start, expiry, ban count, and a direct unban button per row. See Features.md § 5 (Ban an IP, Unban an IP, Currently Banned IPs).
6.7 Implement IP lookup endpoint and UI
Add GET /api/geo/lookup/{ip} to backend/app/routers/geo.py. The endpoint checks whether the IP is currently banned (and in which jails), retrieves its ban history (count, timestamps, jails), and fetches enriched info (country, ASN, RIR) from the geo service. On the frontend, create an IP Lookup section in the Jails area where the user can enter any IP and see all this information. See Features.md § 5 (IP Lookup).
6.8 Implement the ignore list (whitelist) endpoints and UI
Add endpoints to backend/app/routers/jails.py for managing ignore lists:
GET /api/jails/{name}/ignoreip— get the ignore list for a jail.POST /api/jails/{name}/ignoreip— add an IP or network to a jail's ignore list.DELETE /api/jails/{name}/ignoreip— remove an IP from the ignore list.POST /api/jails/{name}/ignoreself— toggle the "ignore self" option.
On the frontend, add an "IP Whitelist" section to the jail detail page showing the ignore list with add/remove controls. See Features.md § 5 (IP Whitelist).
6.9 Write tests for jail and ban features
Test jail listing with mocked socket responses, jail detail parsing, start/stop/reload commands, ban and unban execution, currently-banned list retrieval, IP lookup with and without ban history, and ignore list operations. Ensure all socket interactions are mocked.
Stage 7 — Configuration View
This stage lets users inspect and edit fail2ban configuration directly from the web interface.
7.1 Implement the config service
Build backend/app/services/config_service.py. It reads the active fail2ban configuration by querying the daemon for jail settings, filter regex patterns, and global parameters. It also writes configuration changes by sending the appropriate set commands through the socket (or by editing config files and triggering a reload, depending on what fail2ban supports for each setting). The service must validate regex patterns before applying them — attempting to compile each pattern and returning a clear error if it is invalid. See Features.md § 6 (View Configuration, Edit Configuration).
7.2 Implement the config router
Create backend/app/routers/config.py:
GET /api/config/jails— list all jails with their current configuration.GET /api/config/jails/{name}— full configuration for a single jail (filter, regex, dates, actions, escalation).PUT /api/config/jails/{name}— update a jail's configuration (ban time, max retries, enabled, regex patterns, date pattern, DNS mode, escalation settings).GET /api/config/global— global fail2ban settings.PUT /api/config/global— update global settings.POST /api/config/reload— reload fail2ban to apply changes.
Define models in backend/app/models/config.py. Return validation errors before saving. See Architekture.md § 2.2 (Routers).
7.3 Implement log observation endpoints
Add endpoints for registering new log files that fail2ban should monitor. The user needs to specify a log file path, one or more failure-detection regex patterns, a jail name, and basic jail settings. Include a preview endpoint that reads the specified log file and tests the provided regex against its contents, returning matching lines so the user can verify the pattern before saving. See Features.md § 6 (Add Log Observation).
7.4 Implement the regex tester endpoint
Add POST /api/config/regex-test to the config router. It accepts a sample log line and a fail regex pattern, attempts to match them, and returns whether the pattern matched along with any captured groups highlighted by position. This is a stateless utility endpoint. See Features.md § 6 (Regex Tester).
7.5 Implement server settings endpoints
Create backend/app/routers/server.py:
GET /api/server/settings— current log level, log target, syslog socket, DB path, purge age, max matches.PUT /api/server/settings— update server-level settings.POST /api/server/flush-logs— flush and re-open log files.
Delegate to backend/app/services/server_service.py. See Features.md § 6 (Server Settings).
7.6 Build the configuration page (frontend)
Create frontend/src/pages/ConfigPage.tsx. The page should show all jails with their current settings in a readable format. Each jail section expands to show filter regex, ignore regex, date pattern, actions, and escalation settings. Provide inline editing: clicking a value turns it into an editable field. Add/remove buttons for regex patterns. A "Save" button persists changes and optionally triggers a reload. Show validation errors inline. Use Fluent UI Accordion, Input, Textarea, Switch, and Button. See Features.md § 6 and Web-Design.md.
7.7 Build the regex tester UI (frontend)
Add a "Regex Tester" section to the configuration page (or as a dialog/panel). Two input fields: one for a sample log line, one for the regex pattern. On every change (debounced), call the regex-test endpoint and display the result — whether it matched, and highlight the matched groups. Use monospace font for both inputs. See Features.md § 6 (Regex Tester).
7.8 Build the server settings UI (frontend)
Add a "Server Settings" section to the configuration page. Display current values for log level, log target, syslog socket, DB path, purge age, and max matches. Provide dropdowns for log level and log target, text inputs for paths and numeric values. Include a "Flush Logs" button. See Features.md § 6 (Server Settings).
7.9 Write tests for configuration features
Test config read and write operations with mocked fail2ban responses, regex validation (valid and invalid patterns), the regex tester with matching and non-matching inputs, and server settings read/write. Verify that changes are only applied after validation passes.
Stage 8 — World Map View
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
Add GET /api/dashboard/bans/by-country to the dashboard router. It accepts the same time-range parameter as the ban list endpoint. It queries bans in the selected window, enriches them with geo data, and returns an aggregated count per country (ISO country code → ban count). Also return the full ban list so the frontend can display the companion table. See Features.md § 4.
8.2 Build the world map component (frontend)
Create frontend/src/components/WorldMap.tsx. Render a full world map with country outlines only — no fill colours, no satellite imagery. For each country with bans, display the ban count centred inside the country's borders. Countries with zero bans remain blank. Consider a lightweight SVG-based map library or a TopoJSON/GeoJSON world outline rendered with D3 or a comparable tool. The map must be interactive: clicking a country filters the companion access list. Include the same time-range selector as the dashboard. See Features.md § 4.
8.3 Build the map page (frontend)
Create frontend/src/pages/MapPage.tsx. Compose the time-range selector, the WorldMap component, and an access list table below. When a country is selected on the map, the table filters to show only entries from that country. Clicking the map background (or a "Clear filter" button) removes the country filter. Create frontend/src/hooks/useMapData.ts to fetch and manage the aggregated data. See Features.md § 4.
8.4 Write tests for the map data endpoint
Test aggregation correctness: multiple bans from the same country should be summed, unknown countries should be handled gracefully, and empty time ranges should return an empty map object.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.