From e98fd1de93a78c25f4f8e1df078c971e248802c6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 17 Mar 2026 09:06:42 +0100 Subject: [PATCH] Fix global version handling and unify app version across backend/frontend --- Docker/push.sh | 7 +- Docs/Tasks.md | 493 ------------------------------------- backend/app/__init__.py | 51 +++- backend/app/main.py | 3 +- backend/pyproject.toml | 2 +- frontend/package-lock.json | 4 +- 6 files changed, 57 insertions(+), 503 deletions(-) diff --git a/Docker/push.sh b/Docker/push.sh index 299d845..7438817 100644 --- a/Docker/push.sh +++ b/Docker/push.sh @@ -56,11 +56,8 @@ echo " Registry : ${REGISTRY}" echo " Tag : ${TAG}" echo "============================================" -if [[ "${ENGINE}" == "podman" ]]; then - if ! podman login --get-login "${REGISTRY}" &>/dev/null; then - err "Not logged in. Run:\n podman login ${REGISTRY}" - fi -fi +log "Logging in to ${REGISTRY}" +"${ENGINE}" login "${REGISTRY}" # --------------------------------------------------------------------------- # Build diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 3c0e095..ade2917 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -12,496 +12,3 @@ This document breaks the entire BanGUI project into development stages, ordered --- -### 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. diff --git a/backend/app/__init__.py b/backend/app/__init__.py index a9d4aeb..065f373 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1 +1,50 @@ -"""BanGUI backend application package.""" +"""BanGUI backend application package. + +This package exposes the application version based on the project metadata. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Final + +import importlib.metadata +import tomllib + +PACKAGE_NAME: Final[str] = "bangui-backend" + + +def _read_pyproject_version() -> str: + """Read the project version from ``pyproject.toml``. + + This is used as a fallback when the package metadata is not available (e.g. + when running directly from a source checkout without installing the package). + """ + + project_root = Path(__file__).resolve().parents[1] + pyproject_path = project_root / "pyproject.toml" + if not pyproject_path.exists(): + raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}") + + data = tomllib.loads(pyproject_path.read_text(encoding="utf-8")) + return str(data["project"]["version"]) + + +def _read_version() -> str: + """Return the current package version. + + Prefer the project metadata in ``pyproject.toml`` when available, since this + is the single source of truth for local development and is kept in sync with + the frontend and Docker release version. + + When running from an installed distribution where the ``pyproject.toml`` + is not available, fall back to installed package metadata. + """ + + try: + return _read_pyproject_version() + except FileNotFoundError: + return importlib.metadata.version(PACKAGE_NAME) + + +__version__ = _read_version() diff --git a/backend/app/main.py b/backend/app/main.py index bb25f20..d486cde 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -31,6 +31,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, RedirectResponse from starlette.middleware.base import BaseHTTPMiddleware +from app import __version__ from app.config import Settings, get_settings from app.db import init_db from app.routers import ( @@ -365,7 +366,7 @@ def create_app(settings: Settings | None = None) -> FastAPI: app: FastAPI = FastAPI( title="BanGUI", description="Web interface for monitoring, managing, and configuring fail2ban.", - version="0.1.0", + version=__version__, lifespan=_lifespan, ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7b8ad69..3ae85a1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "bangui-backend" -version = "0.9.0" +version = "0.9.4" description = "BanGUI backend — fail2ban web management interface" requires-python = ">=3.12" dependencies = [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0c058e6..648d22f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "bangui-frontend", - "version": "0.1.0", + "version": "0.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bangui-frontend", - "version": "0.1.0", + "version": "0.9.4", "dependencies": { "@fluentui/react-components": "^9.55.0", "@fluentui/react-icons": "^2.0.257",