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:
131
Docs/Tasks.md
131
Docs/Tasks.md
@@ -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 5–6:
|
||||
|
||||
**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.
|
||||
|
||||
Reference in New Issue
Block a user