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
This commit is contained in:
2026-03-13 13:48:20 +01:00
parent 9b73f6719d
commit 44f3fb8718
3 changed files with 158 additions and 262 deletions

0
.github/agents/ProcessTasks.agent.md vendored Normal file
View File

View File

@@ -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. 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.
- 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. ### References
- 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.). - [Web-Design.md](Web-Design.md) — Design rules: Fluent UI components, tokens, spacing, accessibility.
- The repository layer exposes `get_by_ip` and `upsert` (or equivalent) methods. - [Web-Development.md](Web-Development.md) — Code rules: TypeScript strict, `makeStyles`, component structure, hooks, API layer.
- The service checks the cache before calling the external API. - Fluent UI v9 docs: https://github.com/microsoft/fluentui — components reference.
- Bulk inserts are used where multiple IPs need to be resolved at once (see Task 3).
--- ---
## Task 2Fix `geo_lookup_request_failed` Warnings ✅ DONE ### Task AShared 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: Create a reusable layout component that renders a two-pane master/detail view.
- 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.
**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.
``` **Right pane (detail):**
{"ip": "197.221.98.153", "error": "", "event": "geo_lookup_request_failed", ...} - Takes remaining width. Renders whatever `children` or render-prop content the parent tab passes for the currently selected item.
{"ip": "197.231.178.38", "error": "", "event": "geo_lookup_request_failed", ...} - Displays an empty state (`"Select an item from the list"`) when nothing is selected.
{"ip": "197.234.201.154", "error": "", "event": "geo_lookup_request_failed", ...} - Uses `Skeleton` / `Spinner` while the detail is loading.
{"ip": "197.234.206.108", "error": "", "event": "geo_lookup_request_failed", ...}
**Props interface (suggestion):**
```typescript
interface ConfigListDetailProps<T extends { name: string }> {
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. 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.
- 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.
--- ---
## Task 3Non-Blocking Web Requests & Bulk DB Operations ✅ DONE ### Task BActive 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:** **Jails:**
- **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. - 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.
- **`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. - 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.
- **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.
**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. **Actions:**
- **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. - 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.
- **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. **Implementation:**
- **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. - 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<string>, activeFilters: Set<string>, activeActions: Set<string>, 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 4Better Jail Configuration ✅ DONE ### Task CRedesign 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:** 1. Fetch jail configs via `useJailConfigs` (already exists in `hooks/useConfig.ts`).
2. Use the active-status hook from Task B to get `activeJails`.
- **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. 3. Render `ConfigListDetail` with:
- **Backend (socket):** Added `delete_log_path()` to `config_service.py` + `DELETE /api/config/jails/{name}/logpath` endpoint. - `items` = jail config list.
- **Docker:** Both compose files updated with `BANGUI_FAIL2BAN_CONFIG_DIR` env var; volume mount changed `:ro``:rw`. - `isActive` = `(jail) => activeJails.has(jail.name)`.
- **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`. - `onSelect` updates `selectedName` state.
- **Tests:** 44 service unit tests + 19 router integration tests; all pass; ruff clean. 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):
**Task 4c audit findings — options not yet exposed in the UI:** - A collapsible section (use `Accordion` with a single item, or a `Button` toggle) titled **"Raw Configuration"**.
- 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 - 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.
- Global: `allowipv6`, `before` includes - 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`.
### 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 <jail> 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.
--- ---
## Task 5Add Log Path to Jail (Config UI) ✅ DONE ### Task DRedesign 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`: 1. Fetch filter file list via `fetchFilterFiles` (already used).
- Added `addLogPath` and `AddLogPathRequest` imports. 2. Use `activeFilters` from the active-status hook (Task B).
- Added state: `newLogPath`, `newLogPathTail` (default `true`), `addingLogPath`. 3. Render `ConfigListDetail` with:
- Added `handleAddLogPath` callback: calls `addLogPath(jail.name, { log_path, tail })`, appends path to `logPaths` state, clears input, shows success/error feedback. - `items` = filter file entries.
- 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"`. - `isActive` = `(f) => activeFilters.has(f.name)`.
- 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. 4. On item select, lazily load the parsed filter via `useFilterConfig` (already exists in `hooks/useFilterConfig.ts`).
- `tsc --noEmit`, `eslint`: zero errors. 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 6Expose Ban-Time Escalation Settings ✅ DONE ### Task ERedesign 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:** Same pattern as Task D but for actions.
- §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."
**Tasks:** 1. Fetch action file list via `fetchActionFiles`.
2. Use `activeActions` from the active-status hook (Task B).
### 6a — Backend: Add `BantimeEscalation` model and extend jail + config models 3. Render `ConfigListDetail` with:
- `items` = action file entries.
- 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). - `isActive` = `(a) => activeActions.has(a.name)`.
- Add `bantime_escalation: BantimeEscalation | None` field to `Jail` in `app/models/jail.py`. 4. On item select, lazily load the parsed action via `useActionConfig` (already exists).
- Add escalation fields to `JailConfig` in `app/models/config.py` (mirrored via `BantimeEscalation`). 5. Right pane renders the **existing `ActionForm`** component.
- Add escalation fields to `JailConfigUpdate` in `app/models/config.py`. 6. **Export section** at the bottom:
- Collapsible "Raw Configuration" section.
### 6b — Backend: Read escalation settings from fail2ban socket - `Textarea` (monospace) with raw file content from `fetchActionFile(name)`.
- "Save Raw" button calling `updateActionFile(name, { content })`.
- 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`. - Feedback messages.
- 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 <jail> 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`.
--- ---
## Task 7Expose Remaining Per-Jail Config Fields (usedns, date_pattern, prefregex) ✅ DONE ### Task FRaw 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`): **Props:**
- 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 <jail> usedns` and `get <jail> 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 <jail> 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.
**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<string>;
/** Async function that saves updated raw content. */
saveContent: (content: string) => Promise<void>;
/** 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: **Behaviour:**
- `usedns` — controls whether fail2ban resolves hostnames ("yes" / "warn" / "no" / "raw"). - Renders a collapsible section (single `AccordionItem` or a disclosure `Button`).
- `datepattern` — custom date format for log parsing; empty / unset means fail2ban auto-detects. - When expanded for the first time, calls `fetchContent()` and fills the `Textarea`.
- `prefregex` — a prefix regex prepended to every `failregex` for pre-filtering log lines; empty means disabled. - 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)`.
**Tasks:** - Shows `AutoSaveIndicator`-style feedback (idle → saving → saved / error).
- The `Textarea` is resizable vertically, minimum 15 rows.
### 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 <jail> usedns` and `get <jail> 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 <jail> prefregex <value>` 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`.
--- ---
## Task 8Improve Test Coverage for Background Tasks and Utilities ✅ DONE ### Task GUpdate 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 | ### Task H — Testing and Validation
|---|---|
| `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 % |
### 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`: 1. Task F (RawConfigSection) — standalone, no dependencies.
- `_run_probe` online status: verify `app.state.server_status` is updated correctly. 2. Task A (ConfigListDetail layout) — standalone component.
- `_run_probe` offline→online transition: verify `"fail2ban_came_online"` log event. 3. Task B (useConfigActiveStatus hook) — needs only the existing API layer.
- `_run_probe` online→offline transition: verify `"fail2ban_went_offline"` log event. 4. Task C (JailsTab redesign) — depends on A, B, F.
- `_run_probe` stable online (no transition): verify no transition log events. 5. Task D (FiltersTab redesign) — depends on A, B, F.
- `register`: verify `add_job` is called with `"interval"` trigger and initial offline status set. 6. Task E (ActionsTab redesign) — depends on A, B, F.
7. Task G (exports and wiring) — after C/D/E.
### 8c — Tests for `geo_cache_flush` 8. Task H (testing) — last.
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.
---

View File

@@ -21,7 +21,7 @@ export default tseslint.config(
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
}, },
}, },
prettierConfig, prettierConfig,