Files
BanGUI/Docs/Tasks.md
2026-03-16 20:51:07 +01:00

26 KiB
Raw Blame History

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 separate aiosqlite.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 direct aiosqlite.connect() blocks against the fail2ban database.

What to do:

  1. Create backend/app/repositories/fail2ban_db_repo.py.
  2. 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) -> bool
    • get_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.
  3. Replace the inline aiosqlite.connect blocks in ban_service.py and history_service.py with calls to the new repository functions.
  4. 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 157165: the re_resolve_geo handler runs db.execute("SELECT ip FROM geo_cache WHERE country_code IS NULL") directly.

What to do:

  1. 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 to settings_repo.py if the table belongs there).
  2. 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.
  3. The final handler body must contain no db.execute calls.

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; the get_import_log handler (around line 220) calls import_log_repo.list_logs() directly.

What to do:

  1. Add a list_import_logs(db, source_id, page, page_size) -> tuple[list[ImportRunResult], int] method to blocklist_service.py (it can be a thin wrapper that calls import_log_repo.list_logs internally).
  2. In the router, replace the direct import_log_repo.list_logs(...) call with await blocklist_service.list_import_logs(...).
  3. Remove the import_log_repo import 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 from app.services.conffile_parser.

What to do:

  1. Move the file: backend/app/services/conffile_parser.pybackend/app/utils/conffile_parser.py.
  2. Update every import in the codebase from from app.services.conffile_parser import ... to from app.utils.conffile_parser import ....
  3. 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 direct db.execute / db.executemany calls in cache_stats() (line 187), load_cache_from_db() (line 271), _persist_entry() (lines 304316), _persist_neg_entry() (lines 329338), flush_dirty() (lines 795+), and geo-data batch persist blocks (lines 588612).

What to do:

  1. Create backend/app/repositories/geo_cache_repo.py with typed async functions for every SQL operation currently inline in geo_service.py:
    • load_all(db) -> list[GeoCacheRow]
    • upsert_entry(db, geo_row) -> None
    • upsert_neg_entry(db, ip) -> None
    • flush_dirty(db, entries) -> int
    • get_stats(db) -> dict[str, int]
    • get_unresolved_ips(db) -> list[str] (also needed by B-2)
  2. Replace every db.execute / db.executemany call in geo_service.py with calls to the new repository.
  3. 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.

  1. Replace the inline SQL block in _run_re_resolve with a call to that service method (e.g., unresolved = await geo_service.get_unresolved_ips(db)).
  2. The task function must contain no db.execute calls 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 of Any for geo_enricher parameter and geo_map dict value type.

What to do:

  1. 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]]
    
  2. Replace geo_enricher: Any | None with geo_enricher: GeoEnricher | None (both occurrences).
  3. Replace geo_map: dict[str, Any] with geo_map: dict[str, GeoInfo] (both occurrences).
  4. Replace the inner _safe_lookup return type tuple[str, Any] with tuple[str, GeoInfo | None].
  5. Run mypy --strict or pyright to 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 164168: the lifespan handler runs db.execute("SELECT COUNT(*) FROM geo_cache WHERE country_code IS NULL") directly to log a startup warning about unresolved geo entries.

What to do:

  1. After TASK B-5 is complete, geo_cache_repo will expose a get_stats(db) -> dict[str, int] function (or a dedicated count_unresolved(db) -> int). Use that.
  2. If B-5 is not yet merged, add an interim function count_unresolved(db: aiosqlite.Connection) -> int to geo_cache_repo.py now and call it from geo_service as geo_service.count_unresolved_cached(db) -> Awaitable[int].
  3. Replace the inline async with db.execute(...) block in main.py with a single await geo_service.count_unresolved_cached(db) call.
  4. The main.py lifespan function must contain no db.execute calls 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 — uses Any for geo_enricher and query parameter lists.

What to do:

  1. Define a shared GeoEnricher type alias (e.g., in app/services/geo_service.py or a new app/models/geo.py) similar to TASK B-7.
  2. Update history_service.py to use GeoEnricher | None for the geo_enricher parameter.
  3. Replace list[Any] for SQL parameters with a more precise type (e.g., list[object] or a custom SqlParam alias).
  4. Run mypy --strict or pyright to confirm there are no remaining Any usages in history_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 — uses Any for raw socket response values and command parameters.

What to do:

  1. Define typed aliases for the expected response and command shapes used by Fail2BanClient (e.g., Fail2BanResponse = tuple[int, object], Fail2BanCommand = list[str | int | None]).
  2. Replace Any with those aliases in _ok, _safe_get, and other helper functions.
  3. Ensure the public API functions (get_settings, etc.) have explicit return types and avoid propagating Any to callers.
  4. Run mypy --strict or pyright to confirm no remaining Any usages in server_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: imports getSetupStatus and submitSetup from ../api/setup and calls them directly inside the component.

What to do:

  1. Create frontend/src/hooks/useSetup.ts that encapsulates:
    • Fetching setup status on mount ({ isSetupComplete, loading, error }).
    • A submitSetup(payload) mutation that returns { submitting, submitError, submit }.
  2. Update SetupPage.tsx to use useSetup exclusively; remove all direct api/setup imports 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 3744, 262, 272, 285, 295: imports and directly calls startJail, stopJail, setJailIdle, reloadJail from ../api/jails.

What to do:

  1. Check whether useJailDetail or useJails already expose these control actions. If so, use those hook-provided callbacks instead of calling the API directly.
  2. If they do not, add start(), stop(), reload(), setIdle(idle: boolean) actions to the appropriate hook (e.g., useJailDetail).
  3. Remove all direct startJail / stopJail / setJailIdle / reloadJail API imports from the page.
  4. The ApiError import may remain if it is used only for instanceof type-narrowing in error handlers, but prefer exposing an error: ApiError | null from 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: imports fetchMapColorThresholds from ../api/config and calls it in a useEffect.

What to do:

  1. Create frontend/src/hooks/useMapColorThresholds.ts (or add the fetch to the existing useMapData hook if it is cohesive).
  2. Replace the inline useEffect + fetchMapColorThresholds pattern in MapPage with the new hook call.
  3. Remove the direct api/config import 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: imports previewBlocklist from ../api/blocklist.

What to do:

  1. Add a previewBlocklist(url) action to the existing useBlocklists hook (or create a useBlocklistPreview hook), returning { preview, previewing, previewError, runPreview }.
  2. Update BlocklistsPage to call the hook action instead of the raw API function.
  3. Remove the direct api/blocklist import for previewBlocklist from 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 calls fetchJailBannedIps and unbanIp from ../../api/jails.

What to do:

  1. Create frontend/src/hooks/useJailBannedIps.ts with state { bannedIps, loading, error, page, totalPages, refetch } and an unban(ip) action.
  2. Invoke this hook in the parent page (JailDetailPage) and pass bannedIps, loading, error, onUnban, and pagination props down to BannedIpsSection.
  3. Remove all api/ imports from BannedIpsSection.tsx; the component receives everything through props.
  4. Update BannedIpsSection tests 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 — calls fetchFilters, fetchFilterFile, updateFilterFile from ../../api/config directly.
  • 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 — calls fetchFail2BanLog, fetchServiceStatus from ../../api/config.
  • CreateFilterDialog.tsx — calls createFilter from ../../api/config.
  • CreateJailDialog.tsx — calls createJailConfigFile from ../../api/config.
  • CreateActionDialog.tsx — calls createAction from ../../api/config.
  • ActivateJailDialog.tsx — calls activateJail, validateJailConfig from ../../api/config.
  • AssignFilterDialog.tsx — calls assignFilterToJail from ../../api/config and fetchJails from ../../api/jails.
  • AssignActionDialog.tsx — calls assignActionToJail from ../../api/config and fetchJails from ../../api/jails.

What to do:

For each component listed:

  1. Identify or create the appropriate hook in frontend/src/hooks/. Group related concerns — for example, a single useFiltersConfig hook can cover fetch, update, and create actions for filters.
  2. Move all useEffect + API call patterns from the component into the hook. The hook must return { data, loading, error, refetch, ...actions }.
  3. The component must receive data and action callbacks exclusively through props or a hook called in its closest page ancestor.
  4. Remove all ../../api/ imports from the component files listed above.
  5. 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: imports getSetupStatus from ../api/setup; lines 2836: calls it directly inside a useEffect.

What to do:

  1. The useSetup hook created for TASK F-1 exposes setup-status fetching. Reuse it here, or extract the status-only slice into a useSetupStatus() hook that SetupGuard and SetupPage can both consume.
  2. Replace the inline useEffect + getSetupStatus pattern in SetupGuard with a call to the hook.
  3. Remove the direct ../api/setup import from SetupGuard.tsx.
  4. Update SetupGuard tests — they currently mock ../../api/setup directly; 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, restartFail2Ban from ../../api/config and calls each directly inside useCallback/useEffect handlers.

Note: This component was inadvertently omitted from the TASK F-6 file list despite belonging to the same components/config/ family.

What to do:

  1. The fetchMapColorThresholds / updateMapColorThresholds concern overlaps with TASK F-3 (useMapColorThresholds hook). Extend that hook or create a dedicated useMapColorThresholdsConfig hook that also exposes an update(payload) action.
  2. Add reload() and restart() actions to a suitable config hook (e.g., a useServerActions hook or extend useServerSettings in src/hooks/useConfig.ts).
  3. Replace all direct reloadConfig(), restartFail2Ban(), fetchMapColorThresholds(), and updateMapColorThresholds() calls in ServerTab with the hook-provided actions.
  4. Remove all ../../api/config imports for these four functions from ServerTab.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: imports fetchTimezone from ../api/setup; lines 5762: calls it directly inside a useCallback that is invoked from useEffect.

What to do:

  1. Create frontend/src/hooks/useTimezoneData.ts (or add to an existing setup-related hook) that fetches the timezone and returns { timezone, loading, error }.
  2. Call this hook inside TimezoneProvider and drive the context value from the hook's timezone output — removing the inline fetchTimezone() call.
  3. Remove the direct ../api/setup import from TimezoneProvider.tsx.
  4. 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) use Any for inputs/outputs.

What to do:

  1. Define typed aliases for the fail2ban client response and command shapes (e.g., Fail2BanResponse = tuple[int, object | None], Fail2BanCommand = list[str | int | None]).
  2. Replace Any in helper signatures with the new aliases (and use object/str/int where appropriate).
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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.) use Any for raw fail2ban responses and command parameters.

What to do:

  1. Define typed aliases for fail2ban response and command shapes (e.g., Fail2BanResponse, Fail2BanCommand).
  2. Update helper function signatures to use the new types instead of Any.
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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 _ok and _to_dict and their callers currently use Any.

What to do:

  1. Define typed aliases for fail2ban responses (e.g. Fail2BanResponse = tuple[int, object | None]).
  2. Update _ok, _to_dict, and any helper usage sites to use concrete types instead of Any.
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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 use Any.

What to do:

  1. Replace Any with precise types for repository row dictionaries (e.g. dict[str, object] or a dedicated BlocklistSourceRow TypedDict).
  2. Update helper signatures and any call sites accordingly.
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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 — returns dict[str, Any] and accepts list[Any] parameters.

What to do:

  1. Define a typed row model (e.g. ImportLogRow = TypedDict[...]) or a Pydantic model for import log entries.
  2. Update public function signatures to return typed structures instead of dict[str, Any] and to accept properly typed query parameters.
  3. Update callers (e.g. routers/blocklist.py and services/blocklist_service.py) to work with the new types.
  4. Run mypy --strict or pyright to confirm no remaining Any usages 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.) use Any for fail2ban response objects.

What to do:

  1. Introduce typed aliases for fail2ban command/response shapes (e.g. Fail2BanResponse, Fail2BanCommand).
  2. Replace Any in helper function signatures and return types with these aliases.
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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 uses Any for command and response types.

What to do:

  1. Define clear type aliases such as Fail2BanCommand = list[str | int | bool | None] and Fail2BanResponse = object (or a more specific union of expected response shapes).
  2. Update _send_command_sync, _coerce_command_token, and Fail2BanClient.send signatures to use these aliases.
  3. Run mypy --strict or pyright to confirm no remaining Any usages 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 — uses app: Any and last_activation: dict[str, Any] | None.
  • backend/app/tasks/geo_re_resolve.py — uses app: Any.

What to do:

  1. Define a typed model for the shared application state (e.g., a TypedDict or Protocol) that includes the expected properties on app.state (e.g., settings, db, server_status, last_activation, pending_recovery).
  2. Change task callbacks to accept FastAPI (or the typed app) instead of Any.
  3. Replace dict[str, Any] with a lean typed record (e.g., a TypedDict or a small @dataclass) for last_activation.
  4. Run mypy --strict or pyright to confirm no remaining Any usages 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.pyget_settings currently uses # type: ignore[no-any-return].

What to do:

  1. Introduce a typed model (e.g., TypedDict or Protocol) for app.state to declare settings: Settings and other shared state properties.
  2. Update get_settings (and any other helpers that read from app.state) so the return type is inferred as Settings without needing a type: ignore comment.
  3. Run mypy --strict or pyright to confirm the type ignore is no longer needed.