From 44f3fb8718f3135ebc0449285a634bc4d67752f4 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 13 Mar 2026 13:48:20 +0100 Subject: [PATCH] chore: add GitHub Copilot agent, fix ESLint config, update task list - .github/agents/ProcessTasks.agent.md: Copilot agent definition - eslint.config.ts: minor lint rule adjustment - Docs/Tasks.md: update completed and in-progress task status --- .github/agents/ProcessTasks.agent.md | 0 Docs/Tasks.md | 418 ++++++++++----------------- frontend/eslint.config.ts | 2 +- 3 files changed, 158 insertions(+), 262 deletions(-) create mode 100644 .github/agents/ProcessTasks.agent.md diff --git a/.github/agents/ProcessTasks.agent.md b/.github/agents/ProcessTasks.agent.md new file mode 100644 index 0000000..e69de29 diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 796f3d8..7dac8ed 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -4,321 +4,217 @@ This document breaks the entire BanGUI project into development stages, ordered --- -## Task 1 — Make Geo-Cache Persistent ✅ DONE +## Config View Redesign — List/Detail Layout with Active Status and Raw Export -**Goal:** Minimise calls to the external geo-IP lookup service by caching results in the database. +### Overview -**Details:** +Redesign the Jails, Filters, and Actions tabs on the Configuration page (`frontend/src/pages/ConfigPage.tsx`) to use a **master/detail list layout** instead of the current accordion pattern. The left pane shows a scrollable list of config items (jail names, filter names, action names). Each item displays an active/inactive badge. Active items sort to the top. Clicking an item shows its structured form editor in the right pane, with a collapsible raw-text export section appended at the bottom. -- Currently geo-IP results may only live in memory and are lost on restart. Persist every successful geo-lookup result into the database so the external service is called as rarely as possible. -- On each geo-lookup request, first query the database for a cached entry for that IP. Only call the external service if no cached entry exists (or the entry has expired, if a TTL policy is desired). -- After a successful external lookup, write the result back to the database immediately. -- Review the existing implementation in `app/services/geo_service.py` and the related repository/model code. Verify that: - - The DB table/model for geo-cache entries exists and has the correct schema (IP, country, city, latitude, longitude, looked-up timestamp, etc.). - - The repository layer exposes `get_by_ip` and `upsert` (or equivalent) methods. - - The service checks the cache before calling the external API. - - Bulk inserts are used where multiple IPs need to be resolved at once (see Task 3). +Use only **Fluent UI React v9** (`@fluentui/react-components`, `@fluentui/react-icons`) components as specified in [Web-Design.md](Web-Design.md) and [Web-Development.md](Web-Development.md). No additional UI libraries. + +### References + +- [Web-Design.md](Web-Design.md) — Design rules: Fluent UI components, tokens, spacing, accessibility. +- [Web-Development.md](Web-Development.md) — Code rules: TypeScript strict, `makeStyles`, component structure, hooks, API layer. +- Fluent UI v9 docs: https://github.com/microsoft/fluentui — components reference. --- -## Task 2 — Fix `geo_lookup_request_failed` Warnings ✅ DONE +### Task A — Shared List/Detail Layout Component -**Goal:** Investigate and fix the frequent `geo_lookup_request_failed` log warnings that occur with an empty `error` field. +**File:** `frontend/src/components/config/ConfigListDetail.tsx` -**Resolution:** The root cause was `str(exc)` returning `""` for aiohttp exceptions with no message (e.g. `ServerDisconnectedError`). Fixed by: -- Replacing `error=str(exc)` with `error=repr(exc)` in both `lookup()` and `_batch_api_call()` so the exception class name is always present in the log. -- Adding `exc_type=type(exc).__name__` field to every network-error log event for easy filtering. -- Moving `import aiohttp` from the `TYPE_CHECKING` block to a regular runtime import and replacing the raw-float `timeout` arguments with `aiohttp.ClientTimeout(total=...)`, removing the `# type: ignore[arg-type]` workarounds. -- Three new tests in `TestErrorLogging` verify empty-message exceptions are correctly captured. +Create a reusable layout component that renders a two-pane master/detail view. -**Observed behaviour (from container logs):** +**Left pane (list):** +- Fixed width ~280 px, full height of the tab content area, with its own vertical scroll. +- Use a vertical stack of clickable items. Each item is a Fluent UI `Card` (or a simple styled `div` with `tokens.colorNeutralBackground2` on hover) displaying: + - The config **name** (e.g. `sshd`, `iptables-multiport`), truncated with ellipsis + tooltip for long names. + - A Fluent UI `Badge` to the right of the name: **"Active"** (`appearance="filled"`, `color="success"`) or **"Inactive"** (`appearance="outline"`, `color="informative"`). +- The selected item gets a left border accent (`tokens.colorBrandBackground`) and a highlighted background (`tokens.colorNeutralBackground1Selected`). +- Items are sorted: **active items first**, then inactive, alphabetical within each group. +- Keyboard navigable (arrow keys, Enter to select). Follow the accessibility rules from Web-Design.md §15. -``` -{"ip": "197.221.98.153", "error": "", "event": "geo_lookup_request_failed", ...} -{"ip": "197.231.178.38", "error": "", "event": "geo_lookup_request_failed", ...} -{"ip": "197.234.201.154", "error": "", "event": "geo_lookup_request_failed", ...} -{"ip": "197.234.206.108", "error": "", "event": "geo_lookup_request_failed", ...} +**Right pane (detail):** +- Takes remaining width. Renders whatever `children` or render-prop content the parent tab passes for the currently selected item. +- Displays an empty state (`"Select an item from the list"`) when nothing is selected. +- Uses `Skeleton` / `Spinner` while the detail is loading. + +**Props interface (suggestion):** + +```typescript +interface ConfigListDetailProps { + items: T[]; + isActive: (item: T) => boolean; + selectedName: string | null; + onSelect: (name: string) => void; + loading: boolean; + error: string | null; + children: React.ReactNode; // detail content for selected item +} ``` -**Details:** +**Styles:** Add new style slots to `frontend/src/components/config/configStyles.ts`: +- `listDetailRoot` — flex row, gap `tokens.spacingHorizontalM`. +- `listPane` — fixed width 280 px, overflow-y auto, border-right `tokens.colorNeutralStroke2`. +- `listItem` — padding, hover background, cursor pointer. +- `listItemSelected` — left brand-colour border, selected background. +- `detailPane` — flex 1, overflow-y auto, padding. -- Open `app/services/geo_service.py` and trace the code path that emits the `geo_lookup_request_failed` event. -- The `error` field is empty, which suggests the request may silently fail (e.g. the external service returns a non-200 status, an empty body, or the response parsing swallows the real error). -- Ensure the actual HTTP status code and response body (or exception message) are captured and logged in the `error` field so failures are diagnosable. -- Check whether the external geo-IP service has rate-limiting or IP-range restrictions that could explain the failures. -- Add proper error handling: distinguish between transient errors (timeout, 429, 5xx) and permanent ones (invalid IP, 404) so retries can be applied only when appropriate. +Responsive: On screens < 900 px, collapse the list pane into a `Dropdown` / `Select` above the detail pane so it doesn't take too much horizontal space. --- -## Task 3 — Non-Blocking Web Requests & Bulk DB Operations ✅ DONE +### Task B — Active Status for Jails, Filters, and Actions -**Goal:** Ensure the web UI remains responsive while geo-IP lookups and database writes are in progress. +Determine "active" status for each config type so it can be shown in the list. -**Resolution:** -- **Bulk DB writes:** `geo_service.lookup_batch` now collects resolved IPs into `pos_rows` / `neg_ips` lists across the chunk loop and flushes them with two `executemany` calls per chunk instead of one `execute` per IP. -- **`lookup_cached_only`:** New function that returns `(geo_map, uncached)` immediately from the in-memory + SQLite cache with no API calls. Used by `bans_by_country` for its hot path. -- **Background geo resolution:** `bans_by_country` calls `lookup_cached_only` for an instant response, then fires `asyncio.create_task(geo_service.lookup_batch(uncached, …))` to populate the cache in the background for subsequent requests. -- **Batch enrichment for `get_active_bans`:** `jail_service.get_active_bans` now accepts `http_session` / `app_db` and resolves all banned IPs in a single `lookup_batch` call (chunked 100-IP batches) instead of firing one coroutine per IP through `asyncio.gather`. -- 12 new tests across `test_geo_service.py`, `test_jail_service.py`, and `test_ban_service.py`; `ruff` and `mypy --strict` clean; 145 tests pass. +**Jails:** +- The existing `JailConfig` type does not carry an `enabled` flag directly. The jails API (`GET /api/jails`) returns `JailSummary` objects which contain `enabled: boolean`. To get the active status, fetch the jails list from `fetchJails()` (in `api/jails.ts`) alongside the jail configs from `fetchJailConfigs()`. Merge the two by name: a jail is "active" if `enabled === true` in the jails list. +- Alternatively, check the `JailConfigFile` entries from the jail files API which have `enabled: boolean` — determine which data source is more reliable for showing runtime active state. -**Details:** +**Filters:** +- A filter is "active" if it is referenced by at least one active jail. After fetching jail configs (`JailConfig[]`), collect all unique filter names used by enabled jails. Cross-reference with the filter files list (`ConfFileEntry[]`). Mark used ones as active. +- This requires correlating data: jail config has a `name` field that often matches the filter name (convention in fail2ban: jail name = filter name unless overridden). For accuracy, the filter name a jail uses can be inferred from the jail name or, if the backend provides a `filter` field on `JailConfig`, from that. -- After the geo-IP service was integrated, web UI requests became slow or appeared to hang because geo lookups and individual DB writes block the async event loop. -- **Bulk DB operations:** When multiple IPs need geo data at once (e.g. loading the ban list), collect all uncached IPs and resolve them in a single batch. Use bulk `INSERT … ON CONFLICT` (or equivalent) to write results to the DB in one round-trip instead of one query per IP. -- **Non-blocking external calls:** Make sure all HTTP calls to the external geo-IP service use an async HTTP client (`httpx.AsyncClient` or similar) so the event loop is never blocked by network I/O. -- **Non-blocking DB access:** Ensure all database operations use the async SQLAlchemy session (or are off-loaded to a thread) so they do not block request handling. -- **Background processing:** Consider moving bulk geo-lookups into a background task (e.g. the existing task infrastructure in `app/tasks/`) so the API endpoint returns immediately and the UI is updated once results are ready. +**Actions:** +- An action is "active" if it is referenced by at least one active jail's `actions` array. After fetching jail configs, collect all unique action names from `JailConfig.actions[]` arrays of enabled jails. Cross-reference with action files. + +**Implementation:** +- Create a new hook `frontend/src/hooks/useConfigActiveStatus.ts` that: + 1. Fetches jails list (`fetchJails`), jail configs (`fetchJailConfigs`), filter files (`fetchFilterFiles`), and action files (`fetchActionFiles`) in parallel. + 2. Computes and returns `{ activeJails: Set, activeFilters: Set, activeActions: Set, loading: boolean, error: string | null }`. + 3. Cache results and re-fetch on demand (expose a `refresh` function). +- The hook is consumed by the three tabs so each tab knows which items are active. --- -## Task 4 — Better Jail Configuration ✅ DONE +### Task C — Redesign JailsTab to List/Detail Layout -**Goal:** Expose the full fail2ban configuration surface (jails, filters, actions) in the web UI. +**File:** `frontend/src/components/config/JailsTab.tsx` -Reference config directory: `/home/lukas/Volume/repo/BanGUI/Docker/fail2ban-dev-config/fail2ban/` +Replace the current `Accordion`-based layout with the `ConfigListDetail` component from Task A. -**Implementation summary:** - -- **Backend:** New `app/models/file_config.py`, `app/services/file_config_service.py`, and `app/routers/file_config.py` with full CRUD for `jail.d/`, `filter.d/`, `action.d/` files. Path-traversal prevention via `_assert_within()` + `_validate_new_name()`. `app/config.py` extended with `fail2ban_config_dir` setting. -- **Backend (socket):** Added `delete_log_path()` to `config_service.py` + `DELETE /api/config/jails/{name}/logpath` endpoint. -- **Docker:** Both compose files updated with `BANGUI_FAIL2BAN_CONFIG_DIR` env var; volume mount changed `:ro` → `:rw`. -- **Frontend:** New `Jail Files`, `Filters`, `Actions` tabs in `ConfigPage.tsx`. Delete buttons for log paths in jail accordion. Full API call layer in `api/config.ts` + new types in `types/config.ts`. -- **Tests:** 44 service unit tests + 19 router integration tests; all pass; ruff clean. - -**Task 4c audit findings — options not yet exposed in the UI:** -- Per-jail: `ignoreip`, `bantime.increment`, `bantime.rndtime`, `bantime.maxtime`, `bantime.factor`, `bantime.formula`, `bantime.multipliers`, `bantime.overalljails`, `ignorecommand`, `prefregex`, `timezone`, `journalmatch`, `usedns`, `backend` (read-only shown), `destemail`, `sender`, `action` override -- Global: `allowipv6`, `before` includes - -### 4a — Activate / Deactivate Jail Configs ✅ DONE - -- Listed all `.conf` and `.local` files in `jail.d/` via `GET /api/config/jail-files`. -- Toggle enabled/disabled via `PUT /api/config/jail-files/{filename}/enabled` which patches the `enabled = true/false` line in the config file, preserving all comments. -- Frontend: **Jail Files** tab with enabled `Switch` per file and read-only content viewer. - -### 4b — Editable Log Paths ✅ DONE - -- Added `DELETE /api/config/jails/{name}/logpath?log_path=…` endpoint (uses fail2ban socket `set dellogpath`). -- Frontend: each log path in the Jails accordion now has a dismiss button to remove it. - -### 4c — Audit Missing Config Options ✅ DONE - -- Audit findings documented above. - -### 4d — Filter Configuration (`filter.d`) ✅ DONE - -- Listed all filter files via `GET /api/config/filters`. -- View and edit individual filters via `GET/PUT /api/config/filters/{name}`. -- Create new filter via `POST /api/config/filters`. -- Frontend: **Filters** tab with accordion-per-file, editable textarea, save button, and create-new form. - -### 4e — Action Configuration (`action.d`) ✅ DONE - -- Listed all action files via `GET /api/config/actions`. -- View and edit individual actions via `GET/PUT /api/config/actions/{name}`. -- Create new action via `POST /api/config/actions`. -- Frontend: **Actions** tab with identical structure to Filters tab. - -### 4f — Create New Configuration Files ✅ DONE - -- Create filter and action files via `POST /api/config/filters` and `POST /api/config/actions` with name validation (`_SAFE_NAME_RE`) and 512 KB content size limit. -- Frontend: "New Filter/Action File" section at the bottom of each tab with name input, content textarea, and create button. +1. Fetch jail configs via `useJailConfigs` (already exists in `hooks/useConfig.ts`). +2. Use the active-status hook from Task B to get `activeJails`. +3. Render `ConfigListDetail` with: + - `items` = jail config list. + - `isActive` = `(jail) => activeJails.has(jail.name)`. + - `onSelect` updates `selectedName` state. +4. The right-pane detail content is the **existing `JailAccordionPanel` logic** (the form fields for ban_time, find_time, max_retry, regex patterns, log paths, escalation, etc.) — extract it into a standalone `JailConfigDetail` component if not already. Remove the accordion wrapper; it is just the form body now. +5. **Export section** (new, at the bottom of the detail pane): + - A collapsible section (use `Accordion` with a single item, or a `Button` toggle) titled **"Raw Configuration"**. + - Contains a Fluent UI `Textarea` (monospace font, full width, ~20 rows) pre-filled with the raw plain-text representation of the jail config. The raw text is fetched via the existing `fetchJailConfig` or `fetchJailConfigFile` endpoint that returns the file content as a string. + - The textarea is **editable**: the user can modify the raw text and click a "Save Raw" button to push the changes back via the existing `updateJailConfigFile` PUT endpoint. + - Show an `AutoSaveIndicator` or manual save button + success/error `MessageBar`. --- -## Task 5 — Add Log Path to Jail (Config UI) ✅ DONE +### Task D — Redesign FiltersTab to List/Detail Layout -**Goal:** Allow users to add new log file paths to an existing fail2ban jail directly from the Configuration → Jails tab, completing the "Add Log Observation" feature from [Features.md § 6.3](Features.md). +**File:** `frontend/src/components/config/FiltersTab.tsx` -**Implementation summary:** +Replace the current `Accordion`-based layout with `ConfigListDetail`. -- `ConfigPage.tsx` `JailAccordionPanel`: - - Added `addLogPath` and `AddLogPathRequest` imports. - - Added state: `newLogPath`, `newLogPathTail` (default `true`), `addingLogPath`. - - Added `handleAddLogPath` callback: calls `addLogPath(jail.name, { log_path, tail })`, appends path to `logPaths` state, clears input, shows success/error feedback. - - Added inline "Add Log Path" form below the existing log-path list — an `Input` for the file path, a `Switch` for tail/head selection, and an "Add" button with `aria-label="Add log path"`. -- 6 new frontend tests in `src/components/__tests__/ConfigPageLogPath.test.tsx` covering: rendering, disabled state, enabled state, successful add, success message, and API error surfacing. -- `tsc --noEmit`, `eslint`: zero errors. +1. Fetch filter file list via `fetchFilterFiles` (already used). +2. Use `activeFilters` from the active-status hook (Task B). +3. Render `ConfigListDetail` with: + - `items` = filter file entries. + - `isActive` = `(f) => activeFilters.has(f.name)`. +4. On item select, lazily load the parsed filter via `useFilterConfig` (already exists in `hooks/useFilterConfig.ts`). +5. Right pane renders the **existing `FilterForm`** component with the loaded config. +6. **Export section** at the bottom of the detail pane: + - Collapsible "Raw Configuration" section. + - `Textarea` (monospace) pre-filled with the raw file content fetched via `fetchFilterFile(name)` (returns `ConfFileContent` with a `content: string` field). + - Editable with a "Save Raw" `Button` that calls `updateFilterFile(name, { content })`. + - Success/error feedback via `MessageBar`. --- -## Task 6 — Expose Ban-Time Escalation Settings ✅ DONE +### Task E — Redesign ActionsTab to List/Detail Layout -**Goal:** Surface fail2ban's incremental ban-time escalation settings in the web UI, as called out in [Features.md § 5 (Jail Detail)](Features.md) and [Features.md § 6 (Edit Configuration)](Features.md). +**File:** `frontend/src/components/config/ActionsTab.tsx` -**Features.md requirements:** -- §5 Jail Detail: "Shows ban-time escalation settings if incremental banning is enabled (factor, formula, multipliers, max time)." -- §6 Edit Configuration: "Configure ban-time escalation: enable incremental banning and set factor, formula, multipliers, maximum ban time, and random jitter." +Same pattern as Task D but for actions. -**Tasks:** - -### 6a — Backend: Add `BantimeEscalation` model and extend jail + config models - -- Add `BantimeEscalation` Pydantic model with fields: `increment` (bool), `factor` (float|None), `formula` (str|None), `multipliers` (str|None), `max_time` (int|None), `rnd_time` (int|None), `overall_jails` (bool). -- Add `bantime_escalation: BantimeEscalation | None` field to `Jail` in `app/models/jail.py`. -- Add escalation fields to `JailConfig` in `app/models/config.py` (mirrored via `BantimeEscalation`). -- Add escalation fields to `JailConfigUpdate` in `app/models/config.py`. - -### 6b — Backend: Read escalation settings from fail2ban socket - -- In `jail_service.get_jail_detail()`: fetch the seven `bantime.*` socket commands in the existing `asyncio.gather()` block; populate `bantime_escalation` on the returned `Jail`. -- In `config_service.get_jail_config()`: same gather pattern; populate `bantime_escalation` on `JailConfig`. - -### 6c — Backend: Write escalation settings to fail2ban socket - -- In `config_service.update_jail_config()`: when `JailConfigUpdate.bantime_escalation` is provided, `set bantime.increment`, and any non-None sub-fields. - -### 6d — Frontend: Update types - -- `types/jail.ts`: add `BantimeEscalation` interface; add `bantime_escalation: BantimeEscalation | null` to `Jail`. -- `types/config.ts`: add `bantime_escalation: BantimeEscalation | null` to `JailConfig`; add `BantimeEscalationUpdate` and include it in `JailConfigUpdate`. - -### 6e — Frontend: Show escalation in Jail Detail - -- In `JailDetailPage.tsx`, add a "Ban-time Escalation" info card that is only rendered when `bantime_escalation?.increment === true`. -- Show: increment enabled indicator, factor, formula, multipliers, max time, random jitter. - -### 6f — Frontend: Edit escalation in ConfigPage - -- In `ConfigPage.tsx` `JailAccordionPanel`, add a "Ban-time Escalation" section with: - - A `Switch` for `increment` (enable/disable). - - When enabled: numeric inputs for `max_time` (seconds), `rnd_time` (seconds), `factor`; text inputs for `formula` and `multipliers`; Switch for `overall_jails`. - - Saving triggers `updateJailConfig` with the escalation payload. - -### 6g — Tests - -- Backend: unit tests in `test_config_service.py` verifying that escalation fields are fetched and written. -- Backend: router integration tests in `test_config.py` verifying the escalation round-trip. -- Frontend: update `ConfigPageLogPath.test.tsx` mock `JailConfig` to include `bantime_escalation: null`. +1. Fetch action file list via `fetchActionFiles`. +2. Use `activeActions` from the active-status hook (Task B). +3. Render `ConfigListDetail` with: + - `items` = action file entries. + - `isActive` = `(a) => activeActions.has(a.name)`. +4. On item select, lazily load the parsed action via `useActionConfig` (already exists). +5. Right pane renders the **existing `ActionForm`** component. +6. **Export section** at the bottom: + - Collapsible "Raw Configuration" section. + - `Textarea` (monospace) with raw file content from `fetchActionFile(name)`. + - "Save Raw" button calling `updateActionFile(name, { content })`. + - Feedback messages. --- -## Task 7 — Expose Remaining Per-Jail Config Fields (usedns, date_pattern, prefregex) ✅ DONE +### Task F — Raw Export Section Component -**Goal:** Surface the three remaining per-jail configuration fields — DNS look-up mode (`usedns`), custom date pattern (`datepattern`), and prefix regex (`prefregex`) — in both the backend API response model and the Configuration → Jails UI, completing the editable jail config surface defined in [Features.md § 6](Features.md). +**File:** `frontend/src/components/config/RawConfigSection.tsx` -**Implementation summary:** +Extract the raw-export pattern into a reusable component so Jails, Filters, and Actions tabs don't duplicate logic. -- **Backend model** (`app/models/config.py`): - - Added `use_dns: str` (default `"warn"`) and `prefregex: str` (default `""`) to `JailConfig`. - - Added `prefregex: str | None` to `JailConfigUpdate` (`None` = skip, `""` = clear, non-empty = set). -- **Backend service** (`app/services/config_service.py`): - - Added `get usedns` and `get prefregex` to the `asyncio.gather()` block in `get_jail_config()`. - - Populated `use_dns` and `prefregex` on the returned `JailConfig`. - - Added `prefregex` validation (regex compile-check) and `set prefregex` write in `update_jail_config()`. -- **Frontend types** (`types/config.ts`): - - Added `use_dns: string` and `prefregex: string` to `JailConfig`. - - Added `prefregex?: string | null` to `JailConfigUpdate`. -- **Frontend ConfigPage** (`ConfigPage.tsx` `JailAccordionPanel`): - - Added state and editable `Input` for `date_pattern` (hints "Leave blank for auto-detect"). - - Added state and `Select` dropdown for `dns_mode` with options yes / warn / no / raw. - - Added state and editable `Input` for `prefregex` (hints "Leave blank to disable"). - - All three included in `handleSave()` update payload. -- **Tests**: 8 new service unit tests + 3 new router integration tests; `ConfigPageLogPath.test.tsx` mock updated; 628 tests pass; 85% coverage; ruff + mypy + tsc + eslint clean. +**Props:** -**Goal:** Surface the three remaining per-jail configuration fields — DNS look-up mode (`usedns`), custom date pattern (`datepattern`), and prefix regex (`prefregex`) — in both the backend API response model and the Configuration → Jails UI, completing the editable jail config surface defined in [Features.md § 6](Features.md). +```typescript +interface RawConfigSectionProps { + /** Async function that returns the raw file content string. */ + fetchContent: () => Promise; + /** Async function that saves updated raw content. */ + saveContent: (content: string) => Promise; + /** Label shown in the collapsible header, e.g. "Raw Jail Configuration". */ + label?: string; +} +``` -**Background:** Task 4c audit found several options not yet exposed in the UI. Task 6 covered ban-time escalation. This task covers the three remaining fields that are most commonly changed through the fail2ban configuration: -- `usedns` — controls whether fail2ban resolves hostnames ("yes" / "warn" / "no" / "raw"). -- `datepattern` — custom date format for log parsing; empty / unset means fail2ban auto-detects. -- `prefregex` — a prefix regex prepended to every `failregex` for pre-filtering log lines; empty means disabled. - -**Tasks:** - -### 7a — Backend: Add `use_dns` and `prefregex` to `JailConfig` model - -- Add `use_dns: str` field to `JailConfig` in `app/models/config.py` (default `"warn"`). -- Add `prefregex: str` field to `JailConfig` (default `""`; empty string means not set). -- Add `prefregex: str | None` to `JailConfigUpdate` (`None` = skip, `""` = clear, non-empty = set). - -### 7b — Backend: Read `usedns` and `prefregex` from fail2ban socket - -- In `config_service.get_jail_config()`: add `get usedns` and `get prefregex` to the existing `asyncio.gather()` block. -- Populate `use_dns` and `prefregex` on the returned `JailConfig`. - -### 7c — Backend: Write `prefregex` to fail2ban socket - -- In `config_service.update_jail_config()`: validate `prefregex` with `_validate_regex` if non-empty, then `set prefregex ` when `JailConfigUpdate.prefregex is not None`. - -### 7d — Frontend: Update types - -- `types/config.ts`: add `use_dns: string` and `prefregex: string` to `JailConfig`. -- `types/config.ts`: add `prefregex?: string | null` to `JailConfigUpdate`. - -### 7e — Frontend: Edit `date_pattern`, `use_dns`, and `prefregex` in ConfigPage - -- In `ConfigPage.tsx` `JailAccordionPanel`, add: - - Text input for `date_pattern` (empty = auto-detect; non-empty value is sent as-is). - - `Select` dropdown for `use_dns` with options "yes" / "warn" / "no" / "raw". - - Text input for `prefregex` (empty = not set / cleared). - - All three are included in the `handleSave()` update payload. - -### 7f — Tests - -- Backend: add `usedns` and `prefregex` entries to `_DEFAULT_JAIL_RESPONSES` in `test_config_service.py`. -- Backend: add unit tests verifying new fields are fetched and `prefregex` is written via `update_jail_config()`. -- Backend: update `_make_jail_config()` in `test_config.py` to include `use_dns` and `prefregex`. -- Backend: add router integration tests for the new update fields. -- Frontend: update `ConfigPageLogPath.test.tsx` mock `JailConfig` to include `use_dns` and `prefregex`. +**Behaviour:** +- Renders a collapsible section (single `AccordionItem` or a disclosure `Button`). +- When expanded for the first time, calls `fetchContent()` and fills the `Textarea`. +- Uses monospace font (`fontFamily: "monospace"`) and a left brand-colour accent border (reuse `styles.codeInput` from `configStyles.ts`). +- "Save Raw" `Button` with `appearance="primary"` calls `saveContent(text)`. +- Shows `AutoSaveIndicator`-style feedback (idle → saving → saved / error). +- The `Textarea` is resizable vertically, minimum 15 rows. --- -## Task 8 — Improve Test Coverage for Background Tasks and Utilities ✅ DONE +### Task G — Update Barrel Exports and ConfigPage -**Goal:** Raise test coverage for the background-task modules and the fail2ban client utility to ≥ 80 %, closing the critical-path gap flagged in the Step 6.2 review. +1. **`frontend/src/components/config/index.ts`** — Add exports for `ConfigListDetail` and `RawConfigSection`. +2. **`frontend/src/pages/ConfigPage.tsx`** — No structural changes needed if the tabs internally switch to the new layout. Verify the page still renders all tabs correctly. +3. **`frontend/src/components/config/configStyles.ts`** — Add the new style slots described in Task A. Do not remove existing styles that may still be used by other tabs (Global, Server, Map, Regex Tester, Export). -**Coverage before this task (from last full run):** +--- -| Module | Before | -|---|---| -| `app/tasks/blocklist_import.py` | 23 % | -| `app/tasks/health_check.py` | 43 % | -| `app/tasks/geo_cache_flush.py` | 60 % | -| `app/utils/fail2ban_client.py` | 58 % | +### Task H — Testing and Validation -### 8a — Tests for `blocklist_import` +1. **Type-check:** Run `npx tsc --noEmit` — zero errors. +2. **Lint:** Run `npm run lint` — zero warnings. +3. **Existing tests:** Run `npx vitest run` — all existing tests pass. +4. **Manual verification:** + - Navigate to Config → Jails tab. List pane shows jail names with active badges. Active jails appear at the top. Click a jail → right pane shows configuration form + collapsible raw editor. + - Navigate to Config → Filters tab. Same list/detail pattern. Active filters (used by running jails) show "Active" badge. + - Navigate to Config → Actions tab. Same pattern. + - Resize window below 900 px — list collapses to a dropdown selector above the detail. + - Keyboard: Tab into the list, arrow-key navigate, Enter to select. +5. **New tests (optional but recommended):** + - Unit test `ConfigListDetail` rendering with mock items, verifying sort order (active first) and selection callback. + - Unit test `RawConfigSection` with mocked fetch/save functions. -Create `tests/test_tasks/test_blocklist_import.py`: -- `_run_import` happy path: mock `blocklist_service.import_all`, verify structured log emitted. -- `_run_import` exception path: simulate unexpected exception, verify `log.exception` called. -- `_apply_schedule` hourly: mock scheduler, verify `add_job` called with `"interval"` trigger and correct `hours`. -- `_apply_schedule` daily: verify `"cron"` trigger with `hour` and `minute`. -- `_apply_schedule` weekly: verify `"cron"` trigger with `day_of_week`, `hour`, `minute`. -- `_apply_schedule` replaces an existing job: confirm `remove_job` called first when job already exists. +--- -### 8b — Tests for `health_check` +### Implementation Order -Create `tests/test_tasks/test_health_check.py`: -- `_run_probe` online status: verify `app.state.server_status` is updated correctly. -- `_run_probe` offline→online transition: verify `"fail2ban_came_online"` log event. -- `_run_probe` online→offline transition: verify `"fail2ban_went_offline"` log event. -- `_run_probe` stable online (no transition): verify no transition log events. -- `register`: verify `add_job` is called with `"interval"` trigger and initial offline status set. - -### 8c — Tests for `geo_cache_flush` - -Create `tests/test_tasks/test_geo_cache_flush.py`: -- `_run_flush` with dirty IPs: verify `geo_service.flush_dirty` is called and debug log emitted when count > 0. -- `_run_flush` with nothing: verify `flush_dirty` called but no debug log. -- `register`: verify `add_job` called with correct interval and stable job ID. - -### 8d — Extended tests for `fail2ban_client` - -Extend `tests/test_services/test_fail2ban_client.py`: -- `send()` success path: mock `run_in_executor`, verify response is returned and debug log emitted. -- `send()` `Fail2BanConnectionError`: verify exception is re-raised and warning log emitted. -- `send()` `Fail2BanProtocolError`: verify exception is re-raised and error log emitted. -- `_send_command_sync` connection closed mid-stream (empty chunk): verify `Fail2BanConnectionError`. -- `_send_command_sync` pickle parse error (bad bytes in response): verify `Fail2BanProtocolError`. -- `_coerce_command_token` for `str`, `bool`, `int`, `float`, `list`, `dict`, `set`, and a custom object (stringified). - -**Result:** 50 new tests added (678 total). Coverage after: - -| Module | Before | After | -|---|---|---| -| `app/tasks/blocklist_import.py` | 23 % | 96 % | -| `app/tasks/health_check.py` | 43 % | 100 % | -| `app/tasks/geo_cache_flush.py` | 60 % | 100 % | -| `app/utils/fail2ban_client.py` | 58 % | 96 % | - -Overall backend coverage: 85 % → 87 %. ruff, mypy --strict, tsc, and eslint all clean. +1. Task F (RawConfigSection) — standalone, no dependencies. +2. Task A (ConfigListDetail layout) — standalone component. +3. Task B (useConfigActiveStatus hook) — needs only the existing API layer. +4. Task C (JailsTab redesign) — depends on A, B, F. +5. Task D (FiltersTab redesign) — depends on A, B, F. +6. Task E (ActionsTab redesign) — depends on A, B, F. +7. Task G (exports and wiring) — after C/D/E. +8. Task H (testing) — last. +--- diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index b5077cb..47ba3b6 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -21,7 +21,7 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], }, }, prettierConfig,