26 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.
Open Issues
Architectural Review — 2026-03-16 The findings below were identified by auditing every backend and frontend module against the rules in Refactoring.md and Architekture.md. Tasks are grouped by layer and ordered so that lower-level fixes (repositories, services) are done before the layers that depend on them.
BACKEND
TASK B-1 — Create a fail2ban_db repository for direct fail2ban database queries
Violated rule: Refactoring.md §2.2 — Services must not perform direct aiosqlite calls; go through a repository.
Files affected:
backend/app/services/ban_service.py— lines 247, 398, 568, 646: four separateaiosqlite.connect(f"file:{db_path}?mode=ro", uri=True)blocks that execute raw SQL against the fail2ban SQLite database.backend/app/services/history_service.py— lines 118, 208: two more directaiosqlite.connect()blocks against the fail2ban database.
What to do:
- Create
backend/app/repositories/fail2ban_db_repo.py. - Move all SQL that touches the fail2ban database into clearly named async functions in that module. Each function must accept the fail2ban database path (
db_path: str) as a parameter (connection management stays inside the repository function, since the fail2ban database is an external, read-only resource not managed by BanGUI's own connection pool).get_currently_banned(db_path, jail_filter, since) -> list[BanRecord]get_ban_counts_by_bucket(db_path, ...) -> list[int]check_db_nonempty(db_path) -> boolget_history_for_ip(db_path, ip) -> list[HistoryRecord]get_history_page(db_path, ...) -> tuple[list[HistoryRecord], int]— Adjust signatures as needed to cover all query sites.
- Replace the inline
aiosqlite.connectblocks inban_service.pyandhistory_service.pywith calls to the new repository functions. - Add the new repository to
backend/tests/test_repositories/with unit tests that mock the SQLite file.
TASK B-2 — Remove direct SQL query from routers/geo.py
Violated rule: Refactoring.md §2.1 — Routers must contain zero business logic; no SQL or repository imports.
Files affected:
backend/app/routers/geo.py— lines 157–165: there_resolve_geohandler runsdb.execute("SELECT ip FROM geo_cache WHERE country_code IS NULL")directly.
What to do:
- Add a function
get_unresolved_ips(db: aiosqlite.Connection) -> list[str]to the appropriate repository (geo_cache_repo.py— create it if it does not yet exist, or add it tosettings_repo.pyif the table belongs there). - In the router handler, replace the inline SQL block with a single call to the new repository function via
geo_service(preferred) or directly if the service layer already handles this path. - The final handler body must contain no
db.executecalls.
TASK B-3 — Remove repository import from routers/blocklist.py
Violated rule: Refactoring.md §2.1 — Routers must not import from repositories; all data access must go through services.
Files affected:
backend/app/routers/blocklist.py— line 45:from app.repositories import import_log_repo; theget_import_loghandler (around line 220) callsimport_log_repo.list_logs()directly.
What to do:
- Add a
list_import_logs(db, source_id, page, page_size) -> tuple[list[ImportRunResult], int]method toblocklist_service.py(it can be a thin wrapper that callsimport_log_repo.list_logsinternally). - In the router, replace the direct
import_log_repo.list_logs(...)call withawait blocklist_service.list_import_logs(...). - Remove the
import_log_repoimport from the router.
TASK B-4 — Move conffile_parser.py from services/ to utils/
Violated rule: Refactoring.md §2.2 and Architecture §2.1 — services/ is for business logic. conffile_parser.py is a pure, stateless parsing library with no framework dependencies (no FastAPI, no aiosqlite). It belongs in utils/.
Files affected:
backend/app/services/conffile_parser.py— all callers that import fromapp.services.conffile_parser.
What to do:
- Move the file:
backend/app/services/conffile_parser.py→backend/app/utils/conffile_parser.py. - Update every import in the codebase from
from app.services.conffile_parser import ...tofrom app.utils.conffile_parser import .... - Run the full test suite to confirm nothing is broken.
TASK B-5 — Create a geo_cache_repo and remove direct SQL from geo_service.py
Violated rule: Refactoring.md §2.2 — Services must not execute raw SQL; go through a repository.
Files affected:
backend/app/services/geo_service.py— multiple directdb.execute/db.executemanycalls incache_stats()(line 187),load_cache_from_db()(line 271),_persist_entry()(lines 304–316),_persist_neg_entry()(lines 329–338),flush_dirty()(lines 795+), and geo-data batch persist blocks (lines 588–612).
What to do:
- Create
backend/app/repositories/geo_cache_repo.pywith typed async functions for every SQL operation currently inline ingeo_service.py:load_all(db) -> list[GeoCacheRow]upsert_entry(db, geo_row) -> Noneupsert_neg_entry(db, ip) -> Noneflush_dirty(db, entries) -> intget_stats(db) -> dict[str, int]get_unresolved_ips(db) -> list[str](also needed by B-2)
- Replace every
db.execute/db.executemanycall ingeo_service.pywith calls to the new repository. - Add tests in
backend/tests/test_repositories/test_geo_cache_repo.py.
TASK B-6 — Remove direct SQL from tasks/geo_re_resolve.py
Violated rule: Refactoring.md §2.5 — Tasks must not use repositories directly; they must call a service method.
Files affected:
backend/app/tasks/geo_re_resolve.py— line 53:async with db.execute("SELECT ip FROM geo_cache WHERE country_code IS NULL").
What to do:
After completing TASK B-5, a geo_service method (or via geo_cache_repo through geo_service) that returns unresolved IPs will exist.
- Replace the inline SQL block in
_run_re_resolvewith a call to that service method (e.g.,unresolved = await geo_service.get_unresolved_ips(db)). - The task function must contain no
db.executecalls of its own.
TASK B-7 — Replace Any type annotations in ban_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/ban_service.py— lines 192, 271, 346, 434, 455: uses ofAnyforgeo_enricherparameter andgeo_mapdict value type.
What to do:
- Define a precise callable type alias for the geo enricher, e.g.:
from collections.abc import Awaitable, Callable GeoEnricher: TypeAlias = Callable[[str], Awaitable[GeoInfo | None]] - Replace
geo_enricher: Any | Nonewithgeo_enricher: GeoEnricher | None(both occurrences). - Replace
geo_map: dict[str, Any]withgeo_map: dict[str, GeoInfo](both occurrences). - Replace the inner
_safe_lookupreturn typetuple[str, Any]withtuple[str, GeoInfo | None]. - Run
mypy --strictorpyrightto confirm zero remaining type errors in this file.
TASK B-8 — Remove print() from geo_service.py docstring example
Violated rule: Refactoring.md §4 / Backend-Development.md §2 — Never use print() in production code; use structlog.
Files affected:
backend/app/services/geo_service.py— line 33:print(info.country_code) # "DE"appears inside a module-level docstring usage example.
What to do:
Remove or rewrite the docstring snippet so it does not contain a bare print() call. If the example is kept, annotate it clearly as a documentation-only code block that should not be copied into production code, or replace with a comment like # info.country_code == "DE".
TASK B-9 — Remove direct SQL from main.py lifespan into geo_service
Violated rule: Refactoring.md §2 — Application startup code must not execute raw SQL; data-access logic belongs in a repository (or, when count semantics belong to a domain concern, a service method).
Files affected:
backend/app/main.py— lines 164–168: the lifespan handler runsdb.execute("SELECT COUNT(*) FROM geo_cache WHERE country_code IS NULL")directly to log a startup warning about unresolved geo entries.
What to do:
- After TASK B-5 is complete,
geo_cache_repowill expose aget_stats(db) -> dict[str, int]function (or a dedicatedcount_unresolved(db) -> int). Use that. - If B-5 is not yet merged, add an interim function
count_unresolved(db: aiosqlite.Connection) -> inttogeo_cache_repo.pynow and call it fromgeo_serviceasgeo_service.count_unresolved_cached(db) -> Awaitable[int]. - Replace the inline
async with db.execute(...)block inmain.pywith a singleawait geo_service.count_unresolved_cached(db)call. - The
main.pylifespan function must contain nodb.executecalls of its own.
TASK B-10 — Replace Any type usage in history_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/history_service.py— usesAnyforgeo_enricherand query parameter lists.
What to do:
- Define a shared
GeoEnrichertype alias (e.g., inapp/services/geo_service.pyor a newapp/models/geo.py) similar to TASK B-7. - Update
history_service.pyto useGeoEnricher | Nonefor thegeo_enricherparameter. - Replace
list[Any]for SQL parameters with a more precise type (e.g.,list[object]or a customSqlParamalias). - Run
mypy --strictorpyrightto confirm there are no remainingAnyusages inhistory_service.py.
TASK B-11 — Reduce Any usage in server_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/server_service.py— usesAnyfor raw socket response values and command parameters.
What to do:
- Define typed aliases for the expected response and command shapes used by
Fail2BanClient(e.g.,Fail2BanResponse = tuple[int, object],Fail2BanCommand = list[str | int | None]). - Replace
Anywith those aliases in_ok,_safe_get, and other helper functions. - Ensure the public API functions (
get_settings, etc.) have explicit return types and avoid propagatingAnyto callers. - Run
mypy --strictorpyrightto confirm no remainingAnyusages inserver_service.py.
FRONTEND
TASK F-1 — Wrap SetupPage API calls in a dedicated hook
Violated rule: Refactoring.md §3.1 — Pages must not call API functions from src/api/ directly; all data fetching goes through hooks.
Files affected:
frontend/src/pages/SetupPage.tsx— lines 24, 114, 179: importsgetSetupStatusandsubmitSetupfrom../api/setupand calls them directly inside the component.
What to do:
- Create
frontend/src/hooks/useSetup.tsthat encapsulates:- Fetching setup status on mount (
{ isSetupComplete, loading, error }). - A
submitSetup(payload)mutation that returns{ submitting, submitError, submit }.
- Fetching setup status on mount (
- Update
SetupPage.tsxto useuseSetupexclusively; remove all directapi/setupimports from the page.
TASK F-2 — Wrap JailDetailPage jail-control API calls in a hook
Violated rule: Refactoring.md §3.1 — Pages must not call API functions directly.
Files affected:
frontend/src/pages/JailDetailPage.tsx— lines 37–44, 262, 272, 285, 295: imports and directly callsstartJail,stopJail,setJailIdle,reloadJailfrom../api/jails.
What to do:
- Check whether
useJailDetailoruseJailsalready expose these control actions. If so, use those hook-provided callbacks instead of calling the API directly. - If they do not, add
start(),stop(),reload(),setIdle(idle: boolean)actions to the appropriate hook (e.g.,useJailDetail). - Remove all direct
startJail/stopJail/setJailIdle/reloadJailAPI imports from the page. - The
ApiErrorimport may remain if it is used only forinstanceoftype-narrowing in error handlers, but prefer exposing anerror: ApiError | nullfrom the hook instead.
TASK F-3 — Wrap MapPage config API call in a hook
Violated rule: Refactoring.md §3.1 — Pages must not call API functions directly.
Files affected:
frontend/src/pages/MapPage.tsx— line 34: importsfetchMapColorThresholdsfrom../api/configand calls it in auseEffect.
What to do:
- Create
frontend/src/hooks/useMapColorThresholds.ts(or add the fetch to the existinguseMapDatahook if it is cohesive). - Replace the inline
useEffect+fetchMapColorThresholdspattern inMapPagewith the new hook call. - Remove the direct
api/configimport from the page.
TASK F-4 — Wrap BlocklistsPage preview API call in a hook
Violated rule: Refactoring.md §3.1 — Pages must not call API functions directly.
Files affected:
frontend/src/pages/BlocklistsPage.tsx— line 54: importspreviewBlocklistfrom../api/blocklist.
What to do:
- Add a
previewBlocklist(url)action to the existinguseBlocklistshook (or create auseBlocklistPreviewhook), returning{ preview, previewing, previewError, runPreview }. - Update
BlocklistsPageto call the hook action instead of the raw API function. - Remove the direct
api/blocklistimport forpreviewBlocklistfrom the page.
TASK F-5 — Move all API calls out of BannedIpsSection into a hook
Violated rule: Refactoring.md §3.2 — Components must not call API functions; all data must come via props or hooks invoked in the parent.
Files affected:
frontend/src/components/jail/BannedIpsSection.tsx— imports and directly callsfetchJailBannedIpsandunbanIpfrom../../api/jails.
What to do:
- Create
frontend/src/hooks/useJailBannedIps.tswith state{ bannedIps, loading, error, page, totalPages, refetch }and anunban(ip)action. - Invoke this hook in the parent page (
JailDetailPage) and passbannedIps,loading,error,onUnban, and pagination props down toBannedIpsSection. - Remove all
api/imports fromBannedIpsSection.tsx; the component receives everything through props. - Update
BannedIpsSectiontests to use props instead of mocking API calls directly.
TASK F-6 — Move all API calls out of config tab and dialog components into hooks
Violated rule: Refactoring.md §3.2 — Components must not call API functions.
Files affected (all in frontend/src/components/config/):
FiltersTab.tsx— callsfetchFilters,fetchFilterFile,updateFilterFilefrom../../api/configdirectly.JailsTab.tsx— calls multiple config API functions directly.ActionsTab.tsx— calls config API functions directly.ExportTab.tsx— calls multiple file-management API functions directly.JailFilesTab.tsx— calls API functions for jail file management.ServerHealthSection.tsx— callsfetchFail2BanLog,fetchServiceStatusfrom../../api/config.CreateFilterDialog.tsx— callscreateFilterfrom../../api/config.CreateJailDialog.tsx— callscreateJailConfigFilefrom../../api/config.CreateActionDialog.tsx— callscreateActionfrom../../api/config.ActivateJailDialog.tsx— callsactivateJail,validateJailConfigfrom../../api/config.AssignFilterDialog.tsx— callsassignFilterToJailfrom../../api/configandfetchJailsfrom../../api/jails.AssignActionDialog.tsx— callsassignActionToJailfrom../../api/configandfetchJailsfrom../../api/jails.
What to do:
For each component listed:
- Identify or create the appropriate hook in
frontend/src/hooks/. Group related concerns — for example, a singleuseFiltersConfighook can cover fetch, update, and create actions for filters. - Move all
useEffect+ API call patterns from the component into the hook. The hook must return{ data, loading, error, refetch, ...actions }. - The component must receive data and action callbacks exclusively through props or a hook called in its closest page ancestor.
- Remove all
../../api/imports from the component files listed above. - Update or add unit tests for any new hooks created.
TASK F-7 — Move SetupGuard API call into a hook
Violated rule: Refactoring.md §3.2 — Components must not contain a useEffect that calls an API function.
Files affected:
frontend/src/components/SetupGuard.tsx— line 12: importsgetSetupStatusfrom../api/setup; lines 28–36: calls it directly inside auseEffect.
What to do:
- The
useSetuphook created for TASK F-1 exposes setup-status fetching. Reuse it here, or extract the status-only slice into auseSetupStatus()hook thatSetupGuardandSetupPagecan both consume. - Replace the inline
useEffect+getSetupStatuspattern inSetupGuardwith a call to the hook. - Remove the direct
../api/setupimport fromSetupGuard.tsx. - Update
SetupGuardtests — they currently mock../../api/setupdirectly; update them to mock the hook instead.
Dependency: Can share hook infrastructure with TASK F-1.
TASK F-8 — Move ServerTab direct API calls into hooks
Violated rule: Refactoring.md §3.2 — Components must not call API functions.
Files affected:
frontend/src/components/config/ServerTab.tsx:- lines 36-41: imports
fetchMapColorThresholds,updateMapColorThresholds,reloadConfig,restartFail2Banfrom../../api/configand calls each directly insideuseCallback/useEffecthandlers.
- lines 36-41: imports
Note: This component was inadvertently omitted from the TASK F-6 file list despite belonging to the same components/config/ family.
What to do:
- The
fetchMapColorThresholds/updateMapColorThresholdsconcern overlaps with TASK F-3 (useMapColorThresholdshook). Extend that hook or create a dedicateduseMapColorThresholdsConfighook that also exposes anupdate(payload)action. - Add
reload()andrestart()actions to a suitable config hook (e.g., auseServerActionshook or extenduseServerSettingsinsrc/hooks/useConfig.ts). - Replace all direct
reloadConfig(),restartFail2Ban(),fetchMapColorThresholds(), andupdateMapColorThresholds()calls inServerTabwith the hook-provided actions. - Remove all
../../api/configimports for these four functions fromServerTab.tsx.
Dependency: Coordinate with TASK F-3 to avoid creating duplicate useMapColorThresholds hook logic.
TASK F-9 — Move TimezoneProvider API call into a hook
Violated rule: Refactoring.md §3.2 — A component (including a provider component) must not contain a useEffect that calls an API function directly; API calls belong in src/hooks/.
Files affected:
frontend/src/providers/TimezoneProvider.tsx— line 20: importsfetchTimezonefrom../api/setup; lines 57–62: calls it directly inside auseCallbackthat is invoked fromuseEffect.
What to do:
- Create
frontend/src/hooks/useTimezoneData.ts(or add to an existing setup-related hook) that fetches the timezone and returns{ timezone, loading, error }. - Call this hook inside
TimezoneProviderand drive the context value from the hook'stimezoneoutput — removing the inlinefetchTimezone()call. - Remove the direct
../api/setupimport fromTimezoneProvider.tsx. - The hook may be reused in any future component that needs the configured timezone without going through the context.
TASK B-12 — Remove Any type annotations in config_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/config_service.py— several helper functions (_ok,_to_dict,_ensure_list,_safe_get,_set,_set_global) useAnyfor inputs/outputs.
What to do:
- Define typed aliases for the fail2ban client response and command shapes (e.g.,
Fail2BanResponse = tuple[int, object | None],Fail2BanCommand = list[str | int | None]). - Replace
Anyin helper signatures with the new aliases (and useobject/str/intwhere appropriate). - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-13 — Remove Any type annotations in jail_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/jail_service.py— helper utilities (_ok,_to_dict,_ensure_list,_safe_get, etc.) useAnyfor raw fail2ban responses and command parameters.
What to do:
- Define typed aliases for fail2ban response and command shapes (e.g.,
Fail2BanResponse,Fail2BanCommand). - Update helper function signatures to use the new types instead of
Any. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-14 — Remove Any type annotations in health_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/health_service.py— helper functions_okand_to_dictand their callers currently useAny.
What to do:
- Define typed aliases for fail2ban responses (e.g.
Fail2BanResponse = tuple[int, object | None]). - Update
_ok,_to_dict, and any helper usage sites to use concrete types instead ofAny. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-15 — Remove Any type annotations in blocklist_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/blocklist_service.py— helper_row_to_source()and other internal functions currently useAny.
What to do:
- Replace
Anywith precise types for repository row dictionaries (e.g.dict[str, object]or a dedicatedBlocklistSourceRowTypedDict). - Update helper signatures and any call sites accordingly.
- Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-16 — Remove Any type annotations in import_log_repo.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/repositories/import_log_repo.py— returnsdict[str, Any]and acceptslist[Any]parameters.
What to do:
- Define a typed row model (e.g.
ImportLogRow = TypedDict[...]) or a Pydantic model for import log entries. - Update public function signatures to return typed structures instead of
dict[str, Any]and to accept properly typed query parameters. - Update callers (e.g.
routers/blocklist.pyandservices/blocklist_service.py) to work with the new types. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-17 — Remove Any type annotations in config_file_service.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/services/config_file_service.py— internal helpers (_to_dict_inner,_ok, etc.) useAnyfor fail2ban response objects.
What to do:
- Introduce typed aliases for fail2ban command/response shapes (e.g.
Fail2BanResponse,Fail2BanCommand). - Replace
Anyin helper function signatures and return types with these aliases. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-18 — Remove Any type annotations in fail2ban_client.py
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/utils/fail2ban_client.py— the public client interface usesAnyfor command and response types.
What to do:
- Define clear type aliases such as
Fail2BanCommand = list[str | int | bool | None]andFail2BanResponse = object(or a more specific union of expected response shapes). - Update
_send_command_sync,_coerce_command_token, andFail2BanClient.sendsignatures to use these aliases. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in this file.
TASK B-19 — Remove Any annotations from background tasks
Violated rule: Backend-Development.md §1 — Never use Any; all functions must have explicit type annotations.
Files affected:
backend/app/tasks/health_check.py— usesapp: Anyandlast_activation: dict[str, Any] | None.backend/app/tasks/geo_re_resolve.py— usesapp: Any.
What to do:
- Define a typed model for the shared application state (e.g., a
TypedDictorProtocol) that includes the expected properties onapp.state(e.g.,settings,db,server_status,last_activation,pending_recovery). - Change task callbacks to accept
FastAPI(or the typed app) instead ofAny. - Replace
dict[str, Any]with a lean typed record (e.g., aTypedDictor a small@dataclass) forlast_activation. - Run
mypy --strictorpyrightto confirm no remainingAnyusages in these files.
TASK B-20 — Remove type: ignore in dependencies.get_settings
Violated rule: Backend-Development.md §1 — Avoid Any and ignored type errors.
Files affected:
backend/app/dependencies.py—get_settingscurrently uses# type: ignore[no-any-return].
What to do:
- Introduce a typed model (e.g.,
TypedDictorProtocol) forapp.stateto declaresettings: Settingsand other shared state properties. - Update
get_settings(and any other helpers that read fromapp.state) so the return type is inferred asSettingswithout needing atype: ignorecomment. - Run
mypy --strictorpyrightto confirm the type ignore is no longer needed.