- Add use_dns and prefregex fields to JailConfig model (backend + frontend types) - Add prefregex to JailConfigUpdate; validate as regex before writing - Fetch usedns and prefregex in get_jail_config via asyncio.gather - Write usedns and prefregex in update_jail_config - ConfigPage JailAccordionPanel: editable date_pattern input, dns_mode Select dropdown (yes/warn/no/raw), and prefregex input - 8 new service unit tests + 3 new router integration tests - 628 tests pass; 85% line coverage; ruff/mypy/tsc/eslint clean
18 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.
Task 1 — Make Geo-Cache Persistent ✅ DONE
Goal: Minimise calls to the external geo-IP lookup service by caching results in the database.
Details:
- Currently geo-IP results may only live in memory and are lost on restart. Persist every successful geo-lookup result into the database so the external service is called as rarely as possible.
- On each geo-lookup request, first query the database for a cached entry for that IP. Only call the external service if no cached entry exists (or the entry has expired, if a TTL policy is desired).
- After a successful external lookup, write the result back to the database immediately.
- Review the existing implementation in
app/services/geo_service.pyand the related repository/model code. Verify that:- The DB table/model for geo-cache entries exists and has the correct schema (IP, country, city, latitude, longitude, looked-up timestamp, etc.).
- The repository layer exposes
get_by_ipandupsert(or equivalent) methods. - The service checks the cache before calling the external API.
- Bulk inserts are used where multiple IPs need to be resolved at once (see Task 3).
Task 2 — Fix geo_lookup_request_failed Warnings ✅ DONE
Goal: Investigate and fix the frequent geo_lookup_request_failed log warnings that occur with an empty error field.
Resolution: The root cause was str(exc) returning "" for aiohttp exceptions with no message (e.g. ServerDisconnectedError). Fixed by:
- Replacing
error=str(exc)witherror=repr(exc)in bothlookup()and_batch_api_call()so the exception class name is always present in the log. - Adding
exc_type=type(exc).__name__field to every network-error log event for easy filtering. - Moving
import aiohttpfrom theTYPE_CHECKINGblock to a regular runtime import and replacing the raw-floattimeoutarguments withaiohttp.ClientTimeout(total=...), removing the# type: ignore[arg-type]workarounds. - Three new tests in
TestErrorLoggingverify empty-message exceptions are correctly captured.
Observed behaviour (from container logs):
{"ip": "197.221.98.153", "error": "", "event": "geo_lookup_request_failed", ...}
{"ip": "197.231.178.38", "error": "", "event": "geo_lookup_request_failed", ...}
{"ip": "197.234.201.154", "error": "", "event": "geo_lookup_request_failed", ...}
{"ip": "197.234.206.108", "error": "", "event": "geo_lookup_request_failed", ...}
Details:
- Open
app/services/geo_service.pyand trace the code path that emits thegeo_lookup_request_failedevent. - The
errorfield is empty, which suggests the request may silently fail (e.g. the external service returns a non-200 status, an empty body, or the response parsing swallows the real error). - Ensure the actual HTTP status code and response body (or exception message) are captured and logged in the
errorfield so failures are diagnosable. - Check whether the external geo-IP service has rate-limiting or IP-range restrictions that could explain the failures.
- Add proper error handling: distinguish between transient errors (timeout, 429, 5xx) and permanent ones (invalid IP, 404) so retries can be applied only when appropriate.
Task 3 — Non-Blocking Web Requests & Bulk DB Operations ✅ DONE
Goal: Ensure the web UI remains responsive while geo-IP lookups and database writes are in progress.
Resolution:
- Bulk DB writes:
geo_service.lookup_batchnow collects resolved IPs intopos_rows/neg_ipslists across the chunk loop and flushes them with twoexecutemanycalls per chunk instead of oneexecuteper IP. lookup_cached_only: New function that returns(geo_map, uncached)immediately from the in-memory + SQLite cache with no API calls. Used bybans_by_countryfor its hot path.- Background geo resolution:
bans_by_countrycallslookup_cached_onlyfor an instant response, then firesasyncio.create_task(geo_service.lookup_batch(uncached, …))to populate the cache in the background for subsequent requests. - Batch enrichment for
get_active_bans:jail_service.get_active_bansnow acceptshttp_session/app_dband resolves all banned IPs in a singlelookup_batchcall (chunked 100-IP batches) instead of firing one coroutine per IP throughasyncio.gather. - 12 new tests across
test_geo_service.py,test_jail_service.py, andtest_ban_service.py;ruffandmypy --strictclean; 145 tests pass.
Details:
- After the geo-IP service was integrated, web UI requests became slow or appeared to hang because geo lookups and individual DB writes block the async event loop.
- Bulk DB operations: When multiple IPs need geo data at once (e.g. loading the ban list), collect all uncached IPs and resolve them in a single batch. Use bulk
INSERT … ON CONFLICT(or equivalent) to write results to the DB in one round-trip instead of one query per IP. - Non-blocking external calls: Make sure all HTTP calls to the external geo-IP service use an async HTTP client (
httpx.AsyncClientor similar) so the event loop is never blocked by network I/O. - Non-blocking DB access: Ensure all database operations use the async SQLAlchemy session (or are off-loaded to a thread) so they do not block request handling.
- Background processing: Consider moving bulk geo-lookups into a background task (e.g. the existing task infrastructure in
app/tasks/) so the API endpoint returns immediately and the UI is updated once results are ready.
Task 4 — Better Jail Configuration ✅ DONE
Goal: Expose the full fail2ban configuration surface (jails, filters, actions) in the web UI.
Reference config directory: /home/lukas/Volume/repo/BanGUI/Docker/fail2ban-dev-config/fail2ban/
Implementation summary:
- Backend: New
app/models/file_config.py,app/services/file_config_service.py, andapp/routers/file_config.pywith full CRUD forjail.d/,filter.d/,action.d/files. Path-traversal prevention via_assert_within()+_validate_new_name().app/config.pyextended withfail2ban_config_dirsetting. - Backend (socket): Added
delete_log_path()toconfig_service.py+DELETE /api/config/jails/{name}/logpathendpoint. - Docker: Both compose files updated with
BANGUI_FAIL2BAN_CONFIG_DIRenv var; volume mount changed:ro→:rw. - Frontend: New
Jail Files,Filters,Actionstabs inConfigPage.tsx. Delete buttons for log paths in jail accordion. Full API call layer inapi/config.ts+ new types intypes/config.ts. - Tests: 44 service unit tests + 19 router integration tests; all pass; ruff clean.
Task 4c audit findings — options not yet exposed in the UI:
- Per-jail:
ignoreip,bantime.increment,bantime.rndtime,bantime.maxtime,bantime.factor,bantime.formula,bantime.multipliers,bantime.overalljails,ignorecommand,prefregex,timezone,journalmatch,usedns,backend(read-only shown),destemail,sender,actionoverride - Global:
allowipv6,beforeincludes
4a — Activate / Deactivate Jail Configs ✅ DONE
- Listed all
.confand.localfiles injail.d/viaGET /api/config/jail-files. - Toggle enabled/disabled via
PUT /api/config/jail-files/{filename}/enabledwhich patches theenabled = true/falseline in the config file, preserving all comments. - Frontend: Jail Files tab with enabled
Switchper file and read-only content viewer.
4b — Editable Log Paths ✅ DONE
- Added
DELETE /api/config/jails/{name}/logpath?log_path=…endpoint (uses fail2ban socketset <jail> dellogpath). - Frontend: each log path in the Jails accordion now has a dismiss button to remove it.
4c — Audit Missing Config Options ✅ DONE
- Audit findings documented above.
4d — Filter Configuration (filter.d) ✅ DONE
- Listed all filter files via
GET /api/config/filters. - View and edit individual filters via
GET/PUT /api/config/filters/{name}. - Create new filter via
POST /api/config/filters. - Frontend: Filters tab with accordion-per-file, editable textarea, save button, and create-new form.
4e — Action Configuration (action.d) ✅ DONE
- Listed all action files via
GET /api/config/actions. - View and edit individual actions via
GET/PUT /api/config/actions/{name}. - Create new action via
POST /api/config/actions. - Frontend: Actions tab with identical structure to Filters tab.
4f — Create New Configuration Files ✅ DONE
- Create filter and action files via
POST /api/config/filtersandPOST /api/config/actionswith name validation (_SAFE_NAME_RE) and 512 KB content size limit. - Frontend: "New Filter/Action File" section at the bottom of each tab with name input, content textarea, and create button.
Task 5 — Add Log Path to Jail (Config UI) ✅ DONE
Goal: Allow users to add new log file paths to an existing fail2ban jail directly from the Configuration → Jails tab, completing the "Add Log Observation" feature from Features.md § 6.3.
Implementation summary:
ConfigPage.tsxJailAccordionPanel:- Added
addLogPathandAddLogPathRequestimports. - Added state:
newLogPath,newLogPathTail(defaulttrue),addingLogPath. - Added
handleAddLogPathcallback: callsaddLogPath(jail.name, { log_path, tail }), appends path tologPathsstate, clears input, shows success/error feedback. - Added inline "Add Log Path" form below the existing log-path list — an
Inputfor the file path, aSwitchfor tail/head selection, and an "Add" button witharia-label="Add log path".
- Added
- 6 new frontend tests in
src/components/__tests__/ConfigPageLogPath.test.tsxcovering: rendering, disabled state, enabled state, successful add, success message, and API error surfacing. tsc --noEmit,eslint: zero errors.
Task 6 — Expose Ban-Time Escalation Settings ✅ DONE
Goal: Surface fail2ban's incremental ban-time escalation settings in the web UI, as called out in Features.md § 5 (Jail Detail) and Features.md § 6 (Edit Configuration).
Features.md requirements:
- §5 Jail Detail: "Shows ban-time escalation settings if incremental banning is enabled (factor, formula, multipliers, max time)."
- §6 Edit Configuration: "Configure ban-time escalation: enable incremental banning and set factor, formula, multipliers, maximum ban time, and random jitter."
Tasks:
6a — Backend: Add BantimeEscalation model and extend jail + config models
- Add
BantimeEscalationPydantic model with fields:increment(bool),factor(float|None),formula(str|None),multipliers(str|None),max_time(int|None),rnd_time(int|None),overall_jails(bool). - Add
bantime_escalation: BantimeEscalation | Nonefield toJailinapp/models/jail.py. - Add escalation fields to
JailConfiginapp/models/config.py(mirrored viaBantimeEscalation). - Add escalation fields to
JailConfigUpdateinapp/models/config.py.
6b — Backend: Read escalation settings from fail2ban socket
- In
jail_service.get_jail_detail(): fetch the sevenbantime.*socket commands in the existingasyncio.gather()block; populatebantime_escalationon the returnedJail. - In
config_service.get_jail_config(): same gather pattern; populatebantime_escalationonJailConfig.
6c — Backend: Write escalation settings to fail2ban socket
- In
config_service.update_jail_config(): whenJailConfigUpdate.bantime_escalationis provided,set <jail> bantime.increment, and any non-None sub-fields.
6d — Frontend: Update types
types/jail.ts: addBantimeEscalationinterface; addbantime_escalation: BantimeEscalation | nulltoJail.types/config.ts: addbantime_escalation: BantimeEscalation | nulltoJailConfig; addBantimeEscalationUpdateand include it inJailConfigUpdate.
6e — Frontend: Show escalation in Jail Detail
- In
JailDetailPage.tsx, add a "Ban-time Escalation" info card that is only rendered whenbantime_escalation?.increment === true. - Show: increment enabled indicator, factor, formula, multipliers, max time, random jitter.
6f — Frontend: Edit escalation in ConfigPage
- In
ConfigPage.tsxJailAccordionPanel, add a "Ban-time Escalation" section with:- A
Switchforincrement(enable/disable). - When enabled: numeric inputs for
max_time(seconds),rnd_time(seconds),factor; text inputs forformulaandmultipliers; Switch foroverall_jails. - Saving triggers
updateJailConfigwith the escalation payload.
- A
6g — Tests
- Backend: unit tests in
test_config_service.pyverifying that escalation fields are fetched and written. - Backend: router integration tests in
test_config.pyverifying the escalation round-trip. - Frontend: update
ConfigPageLogPath.test.tsxmockJailConfigto includebantime_escalation: null.
Task 7 — Expose Remaining Per-Jail Config Fields (usedns, date_pattern, prefregex) ✅ DONE
Goal: Surface the three remaining per-jail configuration fields — DNS look-up mode (usedns), custom date pattern (datepattern), and prefix regex (prefregex) — in both the backend API response model and the Configuration → Jails UI, completing the editable jail config surface defined in Features.md § 6.
Implementation summary:
- Backend model (
app/models/config.py):- Added
use_dns: str(default"warn") andprefregex: str(default"") toJailConfig. - Added
prefregex: str | NonetoJailConfigUpdate(None= skip,""= clear, non-empty = set).
- Added
- Backend service (
app/services/config_service.py):- Added
get <jail> usednsandget <jail> prefregexto theasyncio.gather()block inget_jail_config(). - Populated
use_dnsandprefregexon the returnedJailConfig. - Added
prefregexvalidation (regex compile-check) andset <jail> prefregexwrite inupdate_jail_config().
- Added
- Frontend types (
types/config.ts):- Added
use_dns: stringandprefregex: stringtoJailConfig. - Added
prefregex?: string | nulltoJailConfigUpdate.
- Added
- Frontend ConfigPage (
ConfigPage.tsxJailAccordionPanel):- Added state and editable
Inputfordate_pattern(hints "Leave blank for auto-detect"). - Added state and
Selectdropdown fordns_modewith options yes / warn / no / raw. - Added state and editable
Inputforprefregex(hints "Leave blank to disable"). - All three included in
handleSave()update payload.
- Added state and editable
- Tests: 8 new service unit tests + 3 new router integration tests;
ConfigPageLogPath.test.tsxmock updated; 628 tests pass; 85% coverage; ruff + mypy + tsc + eslint clean.
Goal: Surface the three remaining per-jail configuration fields — DNS look-up mode (usedns), custom date pattern (datepattern), and prefix regex (prefregex) — in both the backend API response model and the Configuration → Jails UI, completing the editable jail config surface defined in Features.md § 6.
Background: Task 4c audit found several options not yet exposed in the UI. Task 6 covered ban-time escalation. This task covers the three remaining fields that are most commonly changed through the fail2ban configuration:
usedns— controls whether fail2ban resolves hostnames ("yes" / "warn" / "no" / "raw").datepattern— custom date format for log parsing; empty / unset means fail2ban auto-detects.prefregex— a prefix regex prepended to everyfailregexfor pre-filtering log lines; empty means disabled.
Tasks:
7a — Backend: Add use_dns and prefregex to JailConfig model
- Add
use_dns: strfield toJailConfiginapp/models/config.py(default"warn"). - Add
prefregex: strfield toJailConfig(default""; empty string means not set). - Add
prefregex: str | NonetoJailConfigUpdate(None= skip,""= clear, non-empty = set).
7b — Backend: Read usedns and prefregex from fail2ban socket
- In
config_service.get_jail_config(): addget <jail> usednsandget <jail> prefregexto the existingasyncio.gather()block. - Populate
use_dnsandprefregexon the returnedJailConfig.
7c — Backend: Write prefregex to fail2ban socket
- In
config_service.update_jail_config(): validateprefregexwith_validate_regexif non-empty, thenset <jail> prefregex <value>whenJailConfigUpdate.prefregex is not None.
7d — Frontend: Update types
types/config.ts: adduse_dns: stringandprefregex: stringtoJailConfig.types/config.ts: addprefregex?: string | nulltoJailConfigUpdate.
7e — Frontend: Edit date_pattern, use_dns, and prefregex in ConfigPage
- In
ConfigPage.tsxJailAccordionPanel, add:- Text input for
date_pattern(empty = auto-detect; non-empty value is sent as-is). Selectdropdown foruse_dnswith options "yes" / "warn" / "no" / "raw".- Text input for
prefregex(empty = not set / cleared). - All three are included in the
handleSave()update payload.
- Text input for
7f — Tests
- Backend: add
usednsandprefregexentries to_DEFAULT_JAIL_RESPONSESintest_config_service.py. - Backend: add unit tests verifying new fields are fetched and
prefregexis written viaupdate_jail_config(). - Backend: update
_make_jail_config()intest_config.pyto includeuse_dnsandprefregex. - Backend: add router integration tests for the new update fields.
- Frontend: update
ConfigPageLogPath.test.tsxmockJailConfigto includeuse_dnsandprefregex.