# 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](Refactoring.md) and [Architekture.md](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 157–165: 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.py` → `backend/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 304–316), `_persist_neg_entry()` (lines 329–338), `flush_dirty()` (lines 795+), and geo-data batch persist blocks (lines 588–612). **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.: ```python 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 164–168: 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 37–44, 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 28–36: 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 57–62: 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.py` — `get_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.