Fix ActivateJailDialog blocking logic and mypy false positive

Two frontend bugs and one mypy false positive fixed:

- ActivateJailDialog: Activate button was never disabled when
  blockingIssues.length > 0 (missing condition in disabled prop).
- ActivateJailDialog: handleConfirm called onActivated() even when
  the backend returned active=false (blocked activation). Dialog now
  stays open and shows result.message instead.
- config.py: Settings() call flagged by mypy --strict because
  pydantic-settings loads required fields from env vars at runtime;
  suppressed with a targeted type: ignore[call-arg] comment.

Tests: added ActivateJailDialog.test.tsx (5 tests covering button state,
backend-rejection handling, success path, and crash detection callback).
This commit is contained in:
2026-03-14 19:50:55 +01:00
parent 936946010f
commit 4b6e118a88
4 changed files with 369 additions and 2 deletions

View File

@@ -331,3 +331,134 @@ The `deactivate_jail` endpoint in `backend/app/routers/config.py` is inconsisten
3. **Manual test with running server:**
- Go to `/config`, find an active jail and click "Deactivate".
- Immediately navigate to the Dashboard — "Active Jails" count should already reflect the reduced count without any delay.
---
## Task 7 — Fix ActivateJailDialog not honouring backend rejection and mypy false positive
**Status:** in progress
### Problem description
Two independent bugs were introduced during Tasks 56:
**Bug 1 — "Activate" button is never disabled on validation errors (frontend)**
In `frontend/src/components/config/ActivateJailDialog.tsx`, Task 5 Part B set:
```tsx
const blockingIssues = validationIssues; // all issues block activation
```
but the "Activate" `<Button>` `disabled` prop was never updated to include `blockingIssues.length > 0`:
```tsx
disabled={submitting || validating} // BUG: missing `|| blockingIssues.length > 0`
```
The pre-validation error message renders correctly, but the button stays clickable. A user can press "Activate" despite seeing a red error — the backend will refuse (returning `active=false`) but the UX is broken and confusing.
**Bug 2 — Dialog closes and fires `onActivated()` even when backend rejects activation (frontend)**
`handleConfirm`'s `.then()` handler never inspects `result.active`. When the backend blocks activation and returns `{ active: false, message: "...", validation_warnings: [...] }`, the frontend still:
1. Calls `setValidationWarnings(result.validation_warnings)` — sets warnings in state.
2. Immediately calls `resetForm()` — which **clears** the newly-set warnings.
3. Calls `onActivated()` — which triggers the parent to refresh the jail list (and may close the dialog).
The user sees the dialog briefly appear to succeed, the parent refreshes, but the jail never activated.
**Bug 3 — mypy strict false positive in `config.py`**
`get_settings()` calls `Settings()` without arguments. mypy strict mode flags this as:
```
backend/app/config.py:88: error: Missing named argument "session_secret" for "Settings" [call-arg]
```
This is a known pydantic-settings limitation: the library loads required fields from environment variables at runtime, which mypy cannot see statically. A targeted suppression with an explanatory comment is the correct fix.
### What to do
#### Part A — Disable "Activate" button when blocking issues are present (frontend)
**File:** `frontend/src/components/config/ActivateJailDialog.tsx`
Find the "Activate" `<Button>` near the bottom of the returned JSX and change its `disabled` prop:
```tsx
// Before:
disabled={submitting || validating}
// After:
disabled={submitting || validating || blockingIssues.length > 0}
```
#### Part B — Handle `active=false` response from backend (frontend)
**File:** `frontend/src/components/config/ActivateJailDialog.tsx`
In `handleConfirm`'s `.then()` callback, add a check for `result.active` before calling `resetForm()` and `onActivated()`:
```tsx
.then((result) => {
if (!result.active) {
// Backend rejected the activation (e.g. missing logpath).
// Show the server's message and keep the dialog open.
setError(result.message);
return;
}
if (result.validation_warnings.length > 0) {
setValidationWarnings(result.validation_warnings);
}
resetForm();
if (!result.fail2ban_running) {
onCrashDetected?.();
}
onActivated();
})
```
#### Part C — Fix mypy false positive (backend)
**File:** `backend/app/config.py`
Add a targeted `# type: ignore[call-arg]` with an explanatory comment to the `Settings()` call in `get_settings()`:
```python
return Settings() # type: ignore[call-arg] # pydantic-settings populates required fields from env vars
```
### Tests to add or update
**File:** `frontend/src/components/config/__tests__/ActivateJailDialog.test.tsx` (new file)
Write tests covering:
1. **`test_activate_button_disabled_when_blocking_issues`** — render the dialog with mocked `validateJailConfig` returning an issue with `field="logpath"`. Assert the "Activate" button is disabled.
2. **`test_activate_button_enabled_when_no_issues`** — render the dialog with mocked `validateJailConfig` returning no issues. Assert the "Activate" button is enabled after validation completes.
3. **`test_dialog_stays_open_when_backend_returns_active_false`** — mock `activateJail` to return `{ active: false, message: "Jail cannot be activated", validation_warnings: [], fail2ban_running: true, name: "test" }`. Click "Activate". Assert: (a) `onActivated` is NOT called; (b) the error message text appears.
4. **`test_dialog_calls_on_activated_when_backend_returns_active_true`** — mock `activateJail` to return `{ active: true, message: "ok", validation_warnings: [], fail2ban_running: true, name: "test" }`. Click "Activate". Assert `onActivated` is called once.
5. **`test_crash_detected_callback_fires_when_fail2ban_not_running`** — mock `activateJail` to return `active: true, fail2ban_running: false`. Assert `onCrashDetected` is called.
### Verification
1. Run frontend type check and lint:
```bash
cd frontend && npx tsc --noEmit && npx eslint src/components/config/ActivateJailDialog.tsx
```
Zero errors and zero warnings.
2. Run frontend tests:
```bash
cd frontend && npx vitest run src/components/config/__tests__/ActivateJailDialog
```
All 5 new tests pass.
3. Run mypy:
```bash
.venv/bin/mypy backend/app/ --strict
```
Zero errors.