Config page tasks 1-4: dropdowns, key props, inactive jail full GUI, banaction fix
Task 1: Backend/LogEncoding/DatePattern dropdowns in JailConfigDetail
- Added BACKENDS, LOG_ENCODINGS, DATE_PATTERN_PRESETS constants
- Backend and Log Encoding: <Input readOnly> → <Select> (editable, auto-saves)
- Date Pattern: <Input> → <Combobox freeform> with presets
- Extended JailConfigUpdate model (backend, log_encoding) and service
- Added readOnly prop to JailConfigDetail (all fields, toggles, buttons)
- Extended RegexList with readOnly prop
Task 2: Fix raw action/filter config always blank
- Added key={selectedAction.name} to ActionDetail in ActionsTab
- Added key={selectedFilter.name} to FilterDetail in FiltersTab
Task 3: Inactive jail full GUI same as active jails
- Extended InactiveJail Pydantic model with all config fields
- Added _parse_time_to_seconds helper to config_file_service
- Updated _build_inactive_jail to populate all extended fields
- Extended InactiveJail TypeScript type to match
- Rewrote InactiveJailDetail to reuse JailConfigDetail (readOnly=true)
Task 4: Fix banaction interpolation error when activating jails
- _write_local_override_sync now includes banaction=iptables-multiport
and banaction_allports=iptables-allports in every .local file
This commit is contained in:
296
Docs/Tasks.md
296
Docs/Tasks.md
@@ -4,157 +4,203 @@ This document breaks the entire BanGUI project into development stages, ordered
|
||||
|
||||
---
|
||||
|
||||
## Stage: Config Page Cleanup & Improvements
|
||||
## ✅ Task 1 — Convert Backend, Log Encoding, and Date Pattern to Dropdowns in Jail Config
|
||||
|
||||
These tasks refine the Configuration page UI. Each task is self-contained and can be completed independently. References point to the specific files that need modification.
|
||||
**Completed.** Backend and Log Encoding are now `<Select>` dropdowns; Date Pattern is a `<Combobox>` with presets. Both are wired to auto-save. The `JailConfigDetail` component also gained a `readOnly` prop (wired throughout all fields, toggles, and buttons) in preparation for Task 3. Backend updated `JailConfigUpdate` model to accept `backend` and `log_encoding`, and `update_jail_config` sends them to the daemon.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Task 1 — Remove the Export tab from the Config page
|
||||
## ✅ Task 2 — Fix Raw Action Configuration Always Blank in Actions Tab
|
||||
|
||||
**Status:** Done — Removed `ExportTab` import, `"export"` from `TabValue`, the `<Tab>` element, and the conditional render. `DocumentArrowDown24Regular` icon import also cleaned up.
|
||||
|
||||
**Goal:** Remove the "Export" tab entirely from the Configuration page. Users should no longer see the Export option in the tab bar, and its rendering logic should be cleaned up.
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/pages/ConfigPage.tsx`
|
||||
|
||||
**What to do:**
|
||||
1. Remove the `"export"` value from the `TabValue` union type.
|
||||
2. Remove the `<Tab value="export" icon={<DocumentArrowDown24Regular />}>Export</Tab>` element from the `<TabList>`.
|
||||
3. Remove the `{tab === "export" && <ExportTab />}` conditional render from the tab content area.
|
||||
4. Remove the `ExportTab` import from the component imports.
|
||||
5. Remove the `DocumentArrowDown24Regular` icon import if it is no longer used elsewhere.
|
||||
|
||||
**Verification:** The Config page should render without the Export tab. No TypeScript errors. All other tabs continue to work.
|
||||
**Completed.** Added `key={selectedAction.name}` to `<ActionDetail>` in `ActionsTab.tsx` and `key={selectedFilter.name}` to `<FilterDetail>` in `FiltersTab.tsx`. This forces a full remount on item switch, resetting `RawConfigSection`'s `loadedRef` so it re-fetches content for every selected item.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Task 2 — Remove "Refresh" and "Reload fail2ban" buttons from the Jails tab
|
||||
## ✅ Task 3 — Give Inactive Jail Configs the Same GUI as Active Ones
|
||||
|
||||
**Status:** Done — Removed `buttonRow` div with both buttons, the `reloadMsg` `MessageBar`, all associated state (`reloading`, `reloadMsg`, `deactivating`) and handlers (`handleRefresh`, `handleReload`). Cleaned up `reloadAll` from `useJailConfigs()` destructure and `ArrowClockwise24Regular` icon import.
|
||||
|
||||
**Goal:** Remove the two action buttons ("Refresh" and "Reload fail2ban") that appear at the top of the Jails tab.
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/config/JailsTab.tsx`
|
||||
|
||||
**What to do:**
|
||||
1. In the `JailsTab` component (the public export, starting around line 584), locate the `<div className={styles.buttonRow}>` block that renders the "Refresh" and "Reload fail2ban" buttons (around lines 722–740).
|
||||
2. Remove the entire `<div className={styles.buttonRow}>...</div>` block containing both buttons.
|
||||
3. Remove the `reloadMsg` `<MessageBar>` block that follows the button row (around lines 741–748), since it only displays messages from the reload action.
|
||||
4. Clean up the now-unused state variables and handlers: `reloading`, `reloadMsg`, `setReloading`, `setReloadMsg`, `handleRefresh`, and `handleReload`.
|
||||
5. Remove the `reloadAll` destructured value from the `useJailConfigs()` call if it is no longer used.
|
||||
6. Remove the `ArrowClockwise24Regular` icon import if it is no longer used elsewhere in this file.
|
||||
7. Keep `refresh` and `loadInactive` — they are still used by `handleDeactivate` and `handleActivated`.
|
||||
|
||||
**Verification:** The Jails tab no longer shows the two buttons or any reload message bar. Active and inactive jails still load and display correctly. Deactivate and activate flows still work.
|
||||
**Completed.** Extended `InactiveJail` backend Pydantic model with all config fields (`ban_time_seconds`, `find_time_seconds`, `log_encoding`, `backend`, `date_pattern`, `use_dns`, `prefregex`, `fail_regex`, `ignore_regex`, `bantime_escalation`). Added `_parse_time_to_seconds` helper to `config_file_service.py` and updated `_build_inactive_jail` to populate all new fields. Extended the `InactiveJail` TypeScript type to match. Rewrote `InactiveJailDetail` to map fields into a `JailConfig`-compatible object and render `JailConfigDetail` with `readOnly=true`, showing filter/port/source_file as informational fields above the shared form.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Task 3 — Add a "Create Config" button to the Jails tab
|
||||
## ✅ Task 4 — Fix `banaction` Interpolation Error When Activating Jails
|
||||
|
||||
**Status:** Done — Created `CreateJailDialog.tsx`, wired `listHeader` prop into `ConfigListDetail` with an `Add24Regular` button, added `createDialogOpen` state. Dialog calls `createJailConfigFile` API and refreshes list on success. Exported from `config/index.ts`.
|
||||
**Completed.** `_write_local_override_sync` now always writes `banaction = iptables-multiport` and `banaction_allports = iptables-allports` into the per-jail `.local` file (Option A), ensuring fail2ban can resolve the `%(banaction)s` interpolation used in `action_`.
|
||||
|
||||
**Goal:** Add a button at the top of the Jails tab list pane that opens a dialog to create a new jail configuration file by name. This is analogous to the "Create Filter" button already present in the Filters tab.
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/config/JailsTab.tsx` — add button + dialog trigger
|
||||
- Optionally create a new `frontend/src/components/config/CreateJailDialog.tsx` dialog component
|
||||
|
||||
**What to do:**
|
||||
1. Add a "Create Config" button at the top of the Jails tab. The recommended approach is to use the `listHeader` prop of `<ConfigListDetail>`, the same pattern used in `FiltersTab.tsx` (see lines 233–240). Pass a `<Button appearance="outline" icon={<Add24Regular />} size="small">Create Config</Button>` as the `listHeader`.
|
||||
2. Create a `CreateJailDialog` component (or inline the logic). The dialog should:
|
||||
- Accept a name input from the user (the jail config file name, e.g. `my-custom-jail`).
|
||||
- On confirm, call the existing `createJailConfigFile` API function from `frontend/src/api/config.ts` with `{ name, content: "# <name>\n" }` as the payload.
|
||||
- Show a loading state while the request is in flight.
|
||||
- Display any errors from the API.
|
||||
- On success, close the dialog and refresh the jail list (call `refresh()` and `loadInactive()`).
|
||||
3. Wire the button's `onClick` to open the dialog, and handle the `onCreated` callback to refresh data.
|
||||
**Problem:** In the Config → Jails page, the **Backend**, **Log Encoding**, and **Date Pattern** fields for active jails are currently plain text `<Input>` fields (Backend and Log Encoding are read-only, Date Pattern is a free-text input). These should be `<Select>` dropdowns so users pick from valid options instead of typing arbitrary values.
|
||||
|
||||
**Reference:** See `CreateFilterDialog.tsx` for the established dialog pattern, and `JailFilesTab.tsx` lines 98–110 for the `createJailConfigFile` API usage.
|
||||
**Where to change:**
|
||||
|
||||
**Verification:** Clicking "Create Config" opens a dialog. Entering a name and confirming creates a new jail config file. The new file appears in the jail list after creation.
|
||||
- **`frontend/src/components/config/JailsTab.tsx`** — the `JailConfigDetail` component, around lines 250–270. This is the right-pane form for active jails. Replace the three `<Input>` fields with `<Select>` dropdowns.
|
||||
|
||||
**Dropdown options to use:**
|
||||
|
||||
### Backend
|
||||
Reference: `fail2ban-master/config/jail.conf` lines 115–132. The `JailFileForm.tsx` already defines a `BACKENDS` constant at line 163 — reuse or mirror it.
|
||||
|
||||
| Value | Label |
|
||||
|-------------|----------------------------------------------|
|
||||
| `auto` | auto — pyinotify, then polling |
|
||||
| `polling` | polling — standard polling algorithm |
|
||||
| `pyinotify` | pyinotify — requires pyinotify library |
|
||||
| `systemd` | systemd — uses systemd journal |
|
||||
| `gamin` | gamin — legacy file alteration monitor |
|
||||
|
||||
### Log Encoding
|
||||
Reference: `fail2ban-master/config/jail.conf` lines 145–150. The option `auto` uses the system locale; the rest are standard Python codec names.
|
||||
|
||||
| Value | Label |
|
||||
|----------|---------------------------------------|
|
||||
| `auto` | auto — use system locale |
|
||||
| `ascii` | ascii |
|
||||
| `utf-8` | utf-8 |
|
||||
| `latin-1` | latin-1 (ISO 8859-1) |
|
||||
|
||||
### Date Pattern
|
||||
Date Pattern should remain a **combobox** (editable dropdown) since users may enter custom patterns, but offer common presets as suggestions.
|
||||
|
||||
| Value | Label |
|
||||
|--------------------------------|---------------------------------------|
|
||||
| *(empty)* | auto-detect (leave blank) |
|
||||
| `{^LN-BEG}` | {^LN-BEG} — line beginning |
|
||||
| `%%Y-%%m-%%d %%H:%%M:%%S` | YYYY-MM-DD HH:MM:SS |
|
||||
| `%%d/%%b/%%Y:%%H:%%M:%%S` | DD/Mon/YYYY:HH:MM:SS (Apache) |
|
||||
| `%%b %%d %%H:%%M:%%S` | Mon DD HH:MM:SS (syslog) |
|
||||
| `EPOCH` | EPOCH — Unix timestamp |
|
||||
| `%%Y-%%m-%%dT%%H:%%M:%%S` | ISO 8601 |
|
||||
| `TAI64N` | TAI64N |
|
||||
|
||||
**Implementation notes:**
|
||||
|
||||
1. Backend and Log Encoding are currently `readOnly`. They must become editable `<Select>` fields. Wire their `onChange` to local state and include them in the auto-save payload sent to the backend.
|
||||
2. The backend model at `backend/app/models/config.py` line 67 already accepts `backend: str` and `log_encoding: str`, so no backend changes are needed unless you want to add server-side validation.
|
||||
3. Date Pattern should use a Fluent UI `Combobox` (editable dropdown) instead of a `Select`, so users can still type a custom pattern.
|
||||
4. Follow the existing DNS Mode dropdown pattern at lines 271–280 of `JailsTab.tsx` as a reference for styling and onChange handling.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Task 4 — Remove the "Active" badge from the Filter and Action detail panes
|
||||
## Task 2 — Fix Raw Action Configuration Always Blank in Actions Tab
|
||||
|
||||
**Status:** Done — Removed the `<div>` wrapper with Active/Inactive and "Has local override" badges from `FilterDetail` in `FiltersTab.tsx` and from `ActionDetail` in `ActionsTab.tsx`. Removed unused `Badge` import from both files.
|
||||
**Problem:** In the Config → Actions page, the **"Raw Action Configuration"** accordion section is always blank when expanded.
|
||||
|
||||
**Goal:** Remove the `<Badge>` component that shows "Active" or "Inactive" in the detail (right) pane of both the Filters tab and the Actions tab. The badge in the list pane (left side, inside `ConfigListDetail`) should remain — only the one in the detail section needs to be removed.
|
||||
**Where to look:**
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/config/FiltersTab.tsx`
|
||||
- `frontend/src/components/config/ActionsTab.tsx`
|
||||
- **`frontend/src/components/config/ActionsTab.tsx`** — the `ActionDetail` component (lines 65–185). The `fetchRaw` callback (line 82) calls `fetchActionFile(action.name)` and returns `result.content`.
|
||||
- **`frontend/src/components/config/RawConfigSection.tsx`** — the collapsible raw editor. It uses a `loadedRef` (line 62) to fetch content only on first accordion expansion, and never resets.
|
||||
- **`frontend/src/api/config.ts`** line 257 — `fetchActionFile()` calls `GET /config/actions/{name}`.
|
||||
- **`backend/app/services/file_config_service.py`** line 734 — `get_action_file()` reads from `action.d/` using `_read_conf_file()`, which tries `.conf` first, then `.local`.
|
||||
|
||||
**What to do in `FiltersTab.tsx`:**
|
||||
1. In the `FilterDetail` component (around lines 90–112), locate the `<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>` block that wraps the Active/Inactive `<Badge>` and the optional "Has local override" `<Badge>`.
|
||||
2. Remove this entire `<div>` block, including both badges inside it.
|
||||
3. If the `Badge` import from `@fluentui/react-components` is no longer used in this file, remove it.
|
||||
**Root cause investigation — check these two possibilities:**
|
||||
|
||||
**What to do in `ActionsTab.tsx`:**
|
||||
1. In the `ActionDetail` component (around lines 115–137), locate the same `<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>` block with the Active/Inactive badge and the "Has local override" badge.
|
||||
2. Remove this entire `<div>` block, including both badges.
|
||||
3. Clean up unused `Badge` import if applicable.
|
||||
|
||||
**Verification:** Selecting a filter or action in the list still works. The detail pane no longer shows the Active/Inactive badge or the "Has local override" badge. The list pane badges are unaffected.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Task 5 — Fix Jail log paths: remove path text spans, populate editable fields
|
||||
|
||||
**Status:** Done — Replaced the read-only `<span>` elements for existing log paths with editable `<Input>` fields, each with an `onChange` handler that updates the corresponding index in `logPaths` state. Delete button preserved.
|
||||
|
||||
**Goal:** In the active jail detail pane, the "Log Paths" field currently renders each existing log path as a read-only `<span>` text element with a delete button. The actual `<Input>` field below is always empty and is only used for adding new paths. The user wants to remove the read-only text spans and instead make each log path an editable field.
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/config/JailsTab.tsx`
|
||||
|
||||
**What to do:**
|
||||
1. In the `JailConfigDetail` component, locate the `<Field label="Log Paths">` block (around lines 292–350).
|
||||
2. Remove the read-only span list that renders existing log paths:
|
||||
```tsx
|
||||
{logPaths.length === 0 ? (
|
||||
<Text ...>(none)</Text>
|
||||
) : (
|
||||
logPaths.map((p) => (
|
||||
<div key={p} className={styles.regexItem}>
|
||||
<span className={styles.codeFont} style={{ flexGrow: 1 }}>{p}</span>
|
||||
<Button ... title="Remove log path" ... />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
```
|
||||
3. Replace it with editable `<Input>` fields for each existing log path. Each row should have:
|
||||
- An `<Input>` pre-filled with the path value (`value={p}`), editable.
|
||||
- An `onChange` handler that updates the corresponding entry in the `logPaths` state array.
|
||||
- The same delete button (reuse the existing `handleDeleteLogPath` callback).
|
||||
4. Keep the "Add new log path" form at the bottom unchanged (the `<Input>` with placeholder, `<Switch>` for tail/head, and "Add" button).
|
||||
5. Ensure the `logPaths` state is properly initialized from `jail.log_paths` (this is already the case at line 87).
|
||||
|
||||
**Implementation hint:** Map over `logPaths` with an index, render an `<Input value={logPaths[i]}>` for each, and on change update the array at that index. Example:
|
||||
### Possibility A: Stale `loadedRef` across action switches
|
||||
`ActionDetail` at line 301 of `ActionsTab.tsx` is rendered **without a `key` prop**:
|
||||
```tsx
|
||||
logPaths.map((p, i) => (
|
||||
<div key={i} className={styles.regexItem}>
|
||||
<Input
|
||||
className={styles.codeFont}
|
||||
style={{ flexGrow: 1 }}
|
||||
value={p}
|
||||
onChange={(_e, d) => {
|
||||
setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v)));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
size="small"
|
||||
disabled={deletingPath === p}
|
||||
title="Remove log path"
|
||||
onClick={() => void handleDeleteLogPath(p)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
<ActionDetail
|
||||
action={selectedAction}
|
||||
onAssignClick={() => { setAssignOpen(true); }}
|
||||
onRemovedFromJail={handleRemovedFromJail}
|
||||
/>
|
||||
```
|
||||
Without `key={selectedAction.name}`, React reuses the same component instance when switching between actions. The `RawConfigSection` inside `ActionDetail` keeps its `loadedRef.current = true` from a previous expansion, so it never re-fetches for the new action.
|
||||
|
||||
**Fix:** Add `key={selectedAction.name}` to `ActionDetail` so it fully unmounts/remounts on action switch, resetting all internal state including `loadedRef`.
|
||||
|
||||
### Possibility B: Backend returning empty content
|
||||
The `_read_conf_file` function at `file_config_service.py` line 492 tries `.conf` first then `.local`. If the action only has a `.conf` file and it's readable, content should be returned. Verify by:
|
||||
1. Checking browser DevTools Network tab for the `GET /api/config/actions/{name}` response body.
|
||||
2. Confirming the response has a non-empty `content` field.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add `key={selectedAction.name}` to the `<ActionDetail>` component in `ActionsTab.tsx` line 301.
|
||||
2. Test by selecting different actions and expanding the raw config accordion — each should show the correct file content.
|
||||
3. If the backend is returning empty content, debug `_read_conf_file` to check which file path it resolves and whether it can read the file.
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — Give Inactive Jail Configs the Same GUI as Active Ones
|
||||
|
||||
**Problem:** In the Config → Jails page, **inactive jails** show a minimal read-only preview (`InactiveJailDetail` component) with only basic fields (filter, port, ban time, find time, max retry, log paths, actions, source file). **Active jails** like `bangui-sim` show a full editable form (`JailConfigDetail` component) with all fields including Backend, Log Encoding, Date Pattern, DNS Mode, Prefix Regex, editable log paths, Fail/Ignore Regex lists, Ban-time Escalation, and a Raw Configuration editor.
|
||||
|
||||
The inactive jail detail should display the same rich GUI as the active one — same layout, same fields — but in read-only mode (since the jail is not running on fail2ban).
|
||||
|
||||
**Where to change:**
|
||||
|
||||
- **`frontend/src/components/config/JailsTab.tsx`**:
|
||||
- `InactiveJailDetail` component (lines 493–580): the current minimal read-only view.
|
||||
- `JailConfigDetail` component (lines ~200–490): the full editable form for active jails.
|
||||
- Selection logic at lines 721–752 determines which component renders based on jail kind.
|
||||
|
||||
- **`frontend/src/types/config.ts`** — `InactiveJail` type (around line 484) defines the data shape for inactive jails. Compare with `JailConfig` to see which fields are missing.
|
||||
|
||||
- **Backend** — check that the inactive jail endpoint returns enough data. The backend model `InactiveJail` may need additional fields (backend, log_encoding, date_pattern, dns_mode, prefix_regex, failregex, ignoreregex, ban-time escalation settings) to match the active `JailConfig` model.
|
||||
|
||||
**Implementation approach:**
|
||||
|
||||
1. **Extend the `InactiveJail` model** in the backend (`backend/app/models/config.py`) to include all fields that `JailConfig` has: `backend`, `log_encoding`, `date_pattern`, `usedns`, `prefixregex`, `failregex`, `ignoreregex`, `bantime_increment` (escalation settings), etc.
|
||||
2. **Update the backend service** that parses inactive jail configs to extract these additional fields from the `.conf`/`.local` files.
|
||||
3. **Rewrite `InactiveJailDetail`** to mirror `JailConfigDetail`'s layout but with all fields in **read-only** mode (all `<Input>` have `readOnly`, all `<Select>` are disabled). Keep the "Activate Jail" button at the bottom.
|
||||
4. Alternatively, refactor `JailConfigDetail` to accept a `readOnly` prop and reuse it for both active and inactive jails, avoiding code duplication.
|
||||
5. Include the **Raw Configuration** accordion (using `RawConfigSection`) for inactive jails as well, pointed at the jail's config file.
|
||||
|
||||
**Testing:** Select an inactive jail and verify it shows the same fields and layout as an active jail like `bangui-sim`, with all values populated from the config file but not editable.
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Fix `banaction` Interpolation Error When Activating Jails
|
||||
|
||||
**Problem:** Activating certain jails (e.g. `airsonic-auth`) causes fail2ban to crash with:
|
||||
|
||||
```
|
||||
ERROR Failed during configuration: Bad value substitution: option 'action'
|
||||
in section 'airsonic-auth' contains an interpolation key 'banaction' which
|
||||
is not a valid option name. Raw value: '%(action_)s'
|
||||
```
|
||||
|
||||
**Verification:** When selecting an active jail that has log paths configured, the paths appear as editable input fields (not plain text). The input values can be modified. The delete button still works. The "Add" form at the bottom still works for adding new paths.
|
||||
**Root cause — the interpolation chain breaks:**
|
||||
|
||||
1. Most jails inherit `action = %(action_)s` from `[DEFAULT]`.
|
||||
2. `action_` is defined in `fail2ban-master/config/jail.conf` line 212 as:
|
||||
```ini
|
||||
action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
```
|
||||
3. `banaction` is **commented out** in the `[DEFAULT]` section of `jail.conf` (line 208):
|
||||
```ini
|
||||
#banaction = iptables-multiport
|
||||
#banaction_allports = iptables-allports
|
||||
```
|
||||
4. When BanGUI activates a jail, `_write_local_override_sync()` in `backend/app/services/config_file_service.py` (line 478) writes a `.local` file with only `enabled`, `bantime`, `findtime`, `maxretry`, `port`, and `logpath` — it does **not** include `banaction` or `action`.
|
||||
5. fail2ban merges the configs, hits `%(banaction)s`, finds no value, and crashes.
|
||||
|
||||
**Where to change:**
|
||||
|
||||
- **`backend/app/services/config_file_service.py`** — function `_write_local_override_sync()` around line 478. This writes the per-jail `.local` override file.
|
||||
- **`backend/app/models/config.py`** — the `ActivateJailRequest` or equivalent model that defines which fields are accepted when activating a jail.
|
||||
|
||||
**Fix options (pick one):**
|
||||
|
||||
### Option A: Write `banaction` in every jail `.local` file (recommended)
|
||||
When BanGUI writes a jail `.local` file, include a `banaction` field with a sensible default (e.g. `iptables-multiport`). This ensures the interpolation chain always resolves.
|
||||
|
||||
1. Add a `banaction` field (defaulting to `iptables-multiport`) to the jail activation model/request.
|
||||
2. In `_write_local_override_sync()`, always write `banaction = <value>` into the `[jailname]` section of the `.local` file.
|
||||
3. Also include `banaction_allports = iptables-allports` as a fallback for jails that reference it.
|
||||
|
||||
### Option B: Write a global `jail.local` with DEFAULT banaction
|
||||
Create or update a `jail.local` file in the fail2ban config directory with:
|
||||
```ini
|
||||
[DEFAULT]
|
||||
banaction = iptables-multiport
|
||||
banaction_allports = iptables-allports
|
||||
```
|
||||
This provides the missing defaults for all jails at once.
|
||||
|
||||
### Option C: Expose `banaction` in the jail config UI
|
||||
Add `banaction` as a configurable field in the jail config form so users can choose the ban action (iptables-multiport, nftables-multiport, firewallcmd-rich-rules, etc.). Write the selected value to the `.local` file on save/activate.
|
||||
|
||||
**Testing:**
|
||||
1. Activate `airsonic-auth` (or any jail that inherits `action = %(action_)s` without defining its own `banaction`).
|
||||
2. Verify fail2ban starts without the interpolation error.
|
||||
3. Verify `fail2ban-client status airsonic-auth` shows the jail running with the correct ban action.
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ class JailConfigUpdate(BaseModel):
|
||||
prefregex: str | None = Field(default=None, description="Prefix regex; None = skip, '' = clear, non-empty = set.")
|
||||
date_pattern: str | None = Field(default=None)
|
||||
dns_mode: str | None = Field(default=None, description="DNS lookup mode: yes | warn | no | raw.")
|
||||
backend: str | None = Field(default=None, description="Log monitoring backend.")
|
||||
log_encoding: str | None = Field(default=None, description="Log file encoding.")
|
||||
enabled: bool | None = Field(default=None)
|
||||
bantime_escalation: BantimeEscalationUpdate | None = Field(
|
||||
default=None,
|
||||
@@ -751,6 +753,47 @@ class InactiveJail(BaseModel):
|
||||
default=None,
|
||||
description="Number of failures before a ban is issued.",
|
||||
)
|
||||
# ---- Extended fields for full GUI display ----
|
||||
ban_time_seconds: int = Field(
|
||||
default=600,
|
||||
description="Ban duration in seconds, parsed from bantime string.",
|
||||
)
|
||||
find_time_seconds: int = Field(
|
||||
default=600,
|
||||
description="Failure-counting window in seconds, parsed from findtime string.",
|
||||
)
|
||||
log_encoding: str = Field(
|
||||
default="auto",
|
||||
description="Log encoding, e.g. ``utf-8`` or ``auto``.",
|
||||
)
|
||||
backend: str = Field(
|
||||
default="auto",
|
||||
description="Log-monitoring backend, e.g. ``auto``, ``pyinotify``, ``polling``.",
|
||||
)
|
||||
date_pattern: str | None = Field(
|
||||
default=None,
|
||||
description="Date pattern for log parsing, or None for auto-detect.",
|
||||
)
|
||||
use_dns: str = Field(
|
||||
default="warn",
|
||||
description="DNS resolution mode: ``yes``, ``warn``, ``no``, or ``raw``.",
|
||||
)
|
||||
prefregex: str = Field(
|
||||
default="",
|
||||
description="Prefix regex prepended to every failregex.",
|
||||
)
|
||||
fail_regex: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of failure regex patterns.",
|
||||
)
|
||||
ignore_regex: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of ignore regex patterns.",
|
||||
)
|
||||
bantime_escalation: BantimeEscalation | None = Field(
|
||||
default=None,
|
||||
description="Ban-time escalation configuration, if enabled.",
|
||||
)
|
||||
source_file: str = Field(
|
||||
...,
|
||||
description="Absolute path to the config file where this jail is defined.",
|
||||
|
||||
@@ -41,6 +41,7 @@ from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
AssignActionRequest,
|
||||
AssignFilterRequest,
|
||||
BantimeEscalation,
|
||||
FilterConfig,
|
||||
FilterConfigUpdate,
|
||||
FilterCreateRequest,
|
||||
@@ -290,6 +291,44 @@ def _parse_int_safe(value: str) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_time_to_seconds(value: str | None, default: int) -> int:
|
||||
"""Convert a fail2ban time string (e.g. ``1h``, ``10m``, ``3600``) to seconds.
|
||||
|
||||
Supports the suffixes ``s`` (seconds), ``m`` (minutes), ``h`` (hours),
|
||||
``d`` (days), ``w`` (weeks), and plain integers (already seconds).
|
||||
``-1`` is treated as a permanent ban and returned as-is.
|
||||
|
||||
Args:
|
||||
value: Raw time string from config, or ``None``.
|
||||
default: Value to return when ``value`` is absent or unparseable.
|
||||
|
||||
Returns:
|
||||
Duration in seconds, or ``-1`` for permanent, or ``default`` on failure.
|
||||
"""
|
||||
if not value:
|
||||
return default
|
||||
stripped = value.strip()
|
||||
if stripped == "-1":
|
||||
return -1
|
||||
multipliers: dict[str, int] = {
|
||||
"w": 604800,
|
||||
"d": 86400,
|
||||
"h": 3600,
|
||||
"m": 60,
|
||||
"s": 1,
|
||||
}
|
||||
for suffix, factor in multipliers.items():
|
||||
if stripped.endswith(suffix) and len(stripped) > 1:
|
||||
try:
|
||||
return int(stripped[:-1]) * factor
|
||||
except ValueError:
|
||||
return default
|
||||
try:
|
||||
return int(stripped)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _parse_multiline(raw: str) -> list[str]:
|
||||
"""Split a multi-line INI value into individual non-blank lines.
|
||||
|
||||
@@ -413,6 +452,42 @@ def _build_inactive_jail(
|
||||
maxretry_raw = settings.get("maxretry", "")
|
||||
maxretry = _parse_int_safe(maxretry_raw)
|
||||
|
||||
# Extended fields for full GUI display
|
||||
ban_time_seconds = _parse_time_to_seconds(settings.get("bantime"), 600)
|
||||
find_time_seconds = _parse_time_to_seconds(settings.get("findtime"), 600)
|
||||
log_encoding = settings.get("logencoding") or "auto"
|
||||
backend = settings.get("backend") or "auto"
|
||||
date_pattern = settings.get("datepattern") or None
|
||||
use_dns = settings.get("usedns") or "warn"
|
||||
prefregex = settings.get("prefregex") or ""
|
||||
fail_regex = _parse_multiline(settings.get("failregex", ""))
|
||||
ignore_regex = _parse_multiline(settings.get("ignoreregex", ""))
|
||||
|
||||
# Ban-time escalation
|
||||
esc_increment = _is_truthy(settings.get("bantime.increment", "false"))
|
||||
esc_factor_raw = settings.get("bantime.factor")
|
||||
esc_factor = float(esc_factor_raw) if esc_factor_raw else None
|
||||
esc_formula = settings.get("bantime.formula") or None
|
||||
esc_multipliers = settings.get("bantime.multipliers") or None
|
||||
esc_max_raw = settings.get("bantime.maxtime")
|
||||
esc_max_time = _parse_time_to_seconds(esc_max_raw, 0) if esc_max_raw else None
|
||||
esc_rnd_raw = settings.get("bantime.rndtime")
|
||||
esc_rnd_time = _parse_time_to_seconds(esc_rnd_raw, 0) if esc_rnd_raw else None
|
||||
esc_overall = _is_truthy(settings.get("bantime.overalljails", "false"))
|
||||
bantime_escalation = (
|
||||
BantimeEscalation(
|
||||
increment=esc_increment,
|
||||
factor=esc_factor,
|
||||
formula=esc_formula,
|
||||
multipliers=esc_multipliers,
|
||||
max_time=esc_max_time,
|
||||
rnd_time=esc_rnd_time,
|
||||
overall_jails=esc_overall,
|
||||
)
|
||||
if esc_increment
|
||||
else None
|
||||
)
|
||||
|
||||
return InactiveJail(
|
||||
name=name,
|
||||
filter=filter_name,
|
||||
@@ -422,6 +497,16 @@ def _build_inactive_jail(
|
||||
bantime=settings.get("bantime") or None,
|
||||
findtime=settings.get("findtime") or None,
|
||||
maxretry=maxretry,
|
||||
ban_time_seconds=ban_time_seconds,
|
||||
find_time_seconds=find_time_seconds,
|
||||
log_encoding=log_encoding,
|
||||
backend=backend,
|
||||
date_pattern=date_pattern,
|
||||
use_dns=use_dns,
|
||||
prefregex=prefregex,
|
||||
fail_regex=fail_regex,
|
||||
ignore_regex=ignore_regex,
|
||||
bantime_escalation=bantime_escalation,
|
||||
source_file=source_file,
|
||||
enabled=enabled,
|
||||
)
|
||||
@@ -513,6 +598,10 @@ def _write_local_override_sync(
|
||||
f"[{jail_name}]",
|
||||
"",
|
||||
f"enabled = {'true' if enabled else 'false'}",
|
||||
# Provide explicit banaction defaults so fail2ban can resolve the
|
||||
# %(banaction)s interpolation used in the built-in action_ chain.
|
||||
"banaction = iptables-multiport",
|
||||
"banaction_allports = iptables-allports",
|
||||
]
|
||||
|
||||
if overrides.get("bantime") is not None:
|
||||
|
||||
@@ -366,6 +366,10 @@ async def update_jail_config(
|
||||
await _set("datepattern", update.date_pattern)
|
||||
if update.dns_mode is not None:
|
||||
await _set("usedns", update.dns_mode)
|
||||
if update.backend is not None:
|
||||
await _set("backend", update.backend)
|
||||
if update.log_encoding is not None:
|
||||
await _set("logencoding", update.log_encoding)
|
||||
if update.prefregex is not None:
|
||||
await _set("prefregex", update.prefregex)
|
||||
if update.enabled is not None:
|
||||
|
||||
@@ -299,6 +299,7 @@ export function ActionsTab(): React.JSX.Element {
|
||||
>
|
||||
{selectedAction !== null && (
|
||||
<ActionDetail
|
||||
key={selectedAction.name}
|
||||
action={selectedAction}
|
||||
onAssignClick={() => { setAssignOpen(true); }}
|
||||
onRemovedFromJail={handleRemovedFromJail}
|
||||
|
||||
@@ -233,6 +233,7 @@ export function FiltersTab(): React.JSX.Element {
|
||||
>
|
||||
{selectedFilter !== null && (
|
||||
<FilterDetail
|
||||
key={selectedFilter.name}
|
||||
filter={selectedFilter}
|
||||
onAssignClick={() => { setAssignOpen(true); }}
|
||||
/>
|
||||
|
||||
@@ -10,10 +10,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Combobox,
|
||||
Field,
|
||||
Input,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Option,
|
||||
Select,
|
||||
Skeleton,
|
||||
SkeletonItem,
|
||||
@@ -55,6 +57,36 @@ import { RawConfigSection } from "./RawConfigSection";
|
||||
import { RegexList } from "./RegexList";
|
||||
import { useConfigStyles } from "./configStyles";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BACKENDS = [
|
||||
{ value: "auto", label: "auto — pyinotify, then polling" },
|
||||
{ value: "polling", label: "polling — standard polling algorithm" },
|
||||
{ value: "pyinotify", label: "pyinotify — requires pyinotify library" },
|
||||
{ value: "systemd", label: "systemd — uses systemd journal" },
|
||||
{ value: "gamin", label: "gamin — legacy file alteration monitor" },
|
||||
] as const;
|
||||
|
||||
const LOG_ENCODINGS = [
|
||||
{ value: "auto", label: "auto — use system locale" },
|
||||
{ value: "ascii", label: "ascii" },
|
||||
{ value: "utf-8", label: "utf-8" },
|
||||
{ value: "latin-1", label: "latin-1 (ISO 8859-1)" },
|
||||
] as const;
|
||||
|
||||
const DATE_PATTERN_PRESETS = [
|
||||
{ value: "", label: "auto-detect (leave blank)" },
|
||||
{ value: "{^LN-BEG}", label: "{^LN-BEG} — line beginning" },
|
||||
{ value: "%%Y-%%m-%%d %%H:%%M:%%S", label: "YYYY-MM-DD HH:MM:SS" },
|
||||
{ value: "%%d/%%b/%%Y:%%H:%%M:%%S", label: "DD/Mon/YYYY:HH:MM:SS (Apache)" },
|
||||
{ value: "%%b %%d %%H:%%M:%%S", label: "Mon DD HH:MM:SS (syslog)" },
|
||||
{ value: "EPOCH", label: "EPOCH — Unix timestamp" },
|
||||
{ value: "%%Y-%%m-%%dT%%H:%%M:%%S", label: "ISO 8601" },
|
||||
{ value: "TAI64N", label: "TAI64N" },
|
||||
] as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JailConfigDetail
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -63,6 +95,10 @@ interface JailConfigDetailProps {
|
||||
jail: JailConfig;
|
||||
onSave: (name: string, update: JailConfigUpdate) => Promise<void>;
|
||||
onDeactivate?: () => void;
|
||||
/** When true all fields are read-only and auto-save is suppressed. */
|
||||
readOnly?: boolean;
|
||||
/** When provided (and readOnly=true) shows an Activate Jail button. */
|
||||
onActivate?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +114,8 @@ function JailConfigDetail({
|
||||
jail,
|
||||
onSave,
|
||||
onDeactivate,
|
||||
readOnly = false,
|
||||
onActivate,
|
||||
}: JailConfigDetailProps): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
const [banTime, setBanTime] = useState(String(jail.ban_time));
|
||||
@@ -88,6 +126,8 @@ function JailConfigDetail({
|
||||
const [logPaths, setLogPaths] = useState<string[]>(jail.log_paths);
|
||||
const [datePattern, setDatePattern] = useState(jail.date_pattern ?? "");
|
||||
const [dnsMode, setDnsMode] = useState(jail.use_dns);
|
||||
const [backend, setBackend] = useState(jail.backend);
|
||||
const [logEncoding, setLogEncoding] = useState(jail.log_encoding);
|
||||
const [prefRegex, setPrefRegex] = useState(jail.prefregex);
|
||||
const [deletingPath, setDeletingPath] = useState<string | null>(null);
|
||||
const [newLogPath, setNewLogPath] = useState("");
|
||||
@@ -165,6 +205,8 @@ function JailConfigDetail({
|
||||
ignore_regex: ignoreRegex,
|
||||
date_pattern: datePattern !== "" ? datePattern : null,
|
||||
dns_mode: dnsMode,
|
||||
backend,
|
||||
log_encoding: logEncoding,
|
||||
prefregex: prefRegex !== "" ? prefRegex : null,
|
||||
bantime_escalation: {
|
||||
increment: escEnabled,
|
||||
@@ -178,17 +220,18 @@ function JailConfigDetail({
|
||||
}),
|
||||
[
|
||||
banTime, findTime, maxRetry, failRegex, ignoreRegex, datePattern,
|
||||
dnsMode, prefRegex, escEnabled, escFactor, escFormula, escMultipliers,
|
||||
escMaxTime, escRndTime, escOverallJails,
|
||||
dnsMode, backend, logEncoding, prefRegex, escEnabled, escFactor,
|
||||
escFormula, escMultipliers, escMaxTime, escRndTime, escOverallJails,
|
||||
jail.ban_time, jail.find_time, jail.max_retry,
|
||||
],
|
||||
);
|
||||
|
||||
const saveCurrent = useCallback(
|
||||
async (update: JailConfigUpdate): Promise<void> => {
|
||||
if (readOnly) return;
|
||||
await onSave(jail.name, update);
|
||||
},
|
||||
[jail.name, onSave],
|
||||
[jail.name, onSave, readOnly],
|
||||
);
|
||||
|
||||
const { status: saveStatus, errorText: saveErrorText, retry: retrySave } =
|
||||
@@ -220,6 +263,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={banTime}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setBanTime(d.value);
|
||||
}}
|
||||
@@ -229,6 +273,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={findTime}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setFindTime(d.value);
|
||||
}}
|
||||
@@ -238,6 +283,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={maxRetry}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setMaxRetry(d.value);
|
||||
}}
|
||||
@@ -246,26 +292,57 @@ function JailConfigDetail({
|
||||
</div>
|
||||
<div className={styles.fieldRow}>
|
||||
<Field label="Backend">
|
||||
<Input readOnly value={jail.backend} />
|
||||
<Select
|
||||
value={backend}
|
||||
disabled={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setBackend(d.value);
|
||||
}}
|
||||
>
|
||||
{BACKENDS.map((b) => (
|
||||
<option key={b.value} value={b.value}>{b.label}</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Log Encoding">
|
||||
<Input readOnly value={jail.log_encoding} />
|
||||
<Select
|
||||
value={logEncoding.toLowerCase()}
|
||||
disabled={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setLogEncoding(d.value);
|
||||
}}
|
||||
>
|
||||
{LOG_ENCODINGS.map((e) => (
|
||||
<option key={e.value} value={e.value}>{e.label}</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<div className={styles.fieldRow}>
|
||||
<Field label="Date Pattern" hint="Leave blank for auto-detect.">
|
||||
<Input
|
||||
<Combobox
|
||||
className={styles.codeFont}
|
||||
placeholder="auto-detect"
|
||||
value={datePattern}
|
||||
onChange={(_e, d) => {
|
||||
setDatePattern(d.value);
|
||||
selectedOptions={[datePattern]}
|
||||
freeform
|
||||
disabled={readOnly}
|
||||
onOptionSelect={(_e, d) => {
|
||||
setDatePattern(d.optionValue ?? "");
|
||||
}}
|
||||
/>
|
||||
onChange={(e) => {
|
||||
setDatePattern(e.target.value);
|
||||
}}
|
||||
>
|
||||
{DATE_PATTERN_PRESETS.map((p) => (
|
||||
<Option key={p.value} value={p.value}>{p.label}</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="DNS Mode">
|
||||
<Select
|
||||
value={dnsMode}
|
||||
disabled={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setDnsMode(d.value);
|
||||
}}
|
||||
@@ -285,6 +362,7 @@ function JailConfigDetail({
|
||||
className={styles.codeFont}
|
||||
placeholder="e.g. ^%(__prefix_line)s"
|
||||
value={prefRegex}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setPrefRegex(d.value);
|
||||
}}
|
||||
@@ -302,22 +380,26 @@ function JailConfigDetail({
|
||||
className={styles.codeFont}
|
||||
style={{ flexGrow: 1 }}
|
||||
value={p}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setLogPaths((prev) => prev.map((v, j) => (j === i ? d.value : v)));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
size="small"
|
||||
disabled={deletingPath === p}
|
||||
title="Remove log path"
|
||||
onClick={() => void handleDeleteLogPath(p)}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
size="small"
|
||||
disabled={deletingPath === p}
|
||||
title="Remove log path"
|
||||
onClick={() => void handleDeleteLogPath(p)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{/* Add log path inline form */}
|
||||
{/* Add log path inline form — hidden in read-only mode */}
|
||||
{!readOnly && (
|
||||
<div className={styles.regexItem} style={{ marginTop: tokens.spacingVerticalXS }}>
|
||||
<Input
|
||||
className={styles.codeFont}
|
||||
@@ -347,12 +429,14 @@ function JailConfigDetail({
|
||||
{addingLogPath ? "Adding…" : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
||||
<RegexList
|
||||
label="Fail Regex"
|
||||
patterns={failRegex}
|
||||
onChange={setFailRegex}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
||||
@@ -360,6 +444,7 @@ function JailConfigDetail({
|
||||
label="Ignore Regex"
|
||||
patterns={ignoreRegex}
|
||||
onChange={setIgnoreRegex}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
{jail.actions.length > 0 && (
|
||||
@@ -387,6 +472,7 @@ function JailConfigDetail({
|
||||
<Switch
|
||||
label="Enable incremental banning"
|
||||
checked={escEnabled}
|
||||
disabled={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscEnabled(d.checked);
|
||||
}}
|
||||
@@ -398,6 +484,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={escFactor}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscFactor(d.value);
|
||||
}}
|
||||
@@ -407,6 +494,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={escMaxTime}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscMaxTime(d.value);
|
||||
}}
|
||||
@@ -416,6 +504,7 @@ function JailConfigDetail({
|
||||
<Input
|
||||
type="number"
|
||||
value={escRndTime}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscRndTime(d.value);
|
||||
}}
|
||||
@@ -425,6 +514,7 @@ function JailConfigDetail({
|
||||
<Field label="Formula">
|
||||
<Input
|
||||
value={escFormula}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscFormula(d.value);
|
||||
}}
|
||||
@@ -433,6 +523,7 @@ function JailConfigDetail({
|
||||
<Field label="Multipliers (space-separated)">
|
||||
<Input
|
||||
value={escMultipliers}
|
||||
readOnly={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscMultipliers(d.value);
|
||||
}}
|
||||
@@ -441,6 +532,7 @@ function JailConfigDetail({
|
||||
<Switch
|
||||
label="Count repeat offences across all jails"
|
||||
checked={escOverallJails}
|
||||
disabled={readOnly}
|
||||
onChange={(_e, d) => {
|
||||
setEscOverallJails(d.checked);
|
||||
}}
|
||||
@@ -449,15 +541,17 @@ function JailConfigDetail({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
||||
<AutoSaveIndicator
|
||||
status={saveStatus}
|
||||
errorText={saveErrorText}
|
||||
onRetry={retrySave}
|
||||
/>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div style={{ marginTop: tokens.spacingVerticalS }}>
|
||||
<AutoSaveIndicator
|
||||
status={saveStatus}
|
||||
errorText={saveErrorText}
|
||||
onRetry={retrySave}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onDeactivate !== undefined && (
|
||||
{!readOnly && onDeactivate !== undefined && (
|
||||
<div style={{ marginTop: tokens.spacingVerticalM }}>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
@@ -469,14 +563,28 @@ function JailConfigDetail({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw Configuration */}
|
||||
<div style={{ marginTop: tokens.spacingVerticalL }}>
|
||||
<RawConfigSection
|
||||
fetchContent={fetchRaw}
|
||||
saveContent={saveRaw}
|
||||
label="Raw Jail Configuration"
|
||||
/>
|
||||
</div>
|
||||
{readOnly && onActivate !== undefined && (
|
||||
<div style={{ marginTop: tokens.spacingVerticalM }}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<Play24Regular />}
|
||||
onClick={onActivate}
|
||||
>
|
||||
Activate Jail
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw Configuration — hidden in read-only (inactive jail) mode */}
|
||||
{!readOnly && (
|
||||
<div style={{ marginTop: tokens.spacingVerticalL }}>
|
||||
<RawConfigSection
|
||||
fetchContent={fetchRaw}
|
||||
saveContent={saveRaw}
|
||||
label="Raw Jail Configuration"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -491,7 +599,12 @@ interface InactiveJailDetailProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only detail view for an inactive jail, with an Activate button.
|
||||
* Detail view for an inactive jail.
|
||||
*
|
||||
* Maps the parsed config fields to a JailConfig-compatible object and renders
|
||||
* JailConfigDetail in read-only mode, so the UI is identical to the active
|
||||
* jail view but with all fields disabled and an Activate button instead of
|
||||
* a Deactivate button.
|
||||
*
|
||||
* @param props - Component props.
|
||||
* @returns JSX element.
|
||||
@@ -502,13 +615,29 @@ function InactiveJailDetail({
|
||||
}: InactiveJailDetailProps): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.detailPane}>
|
||||
<Text weight="semibold" size={500} block style={{ marginBottom: tokens.spacingVerticalM }}>
|
||||
{jail.name}
|
||||
</Text>
|
||||
const jailConfig = useMemo<JailConfig>(
|
||||
() => ({
|
||||
name: jail.name,
|
||||
ban_time: jail.ban_time_seconds,
|
||||
find_time: jail.find_time_seconds,
|
||||
max_retry: jail.maxretry ?? 5,
|
||||
fail_regex: jail.fail_regex,
|
||||
ignore_regex: jail.ignore_regex,
|
||||
log_paths: jail.logpath,
|
||||
date_pattern: jail.date_pattern,
|
||||
log_encoding: jail.log_encoding,
|
||||
backend: jail.backend,
|
||||
use_dns: jail.use_dns,
|
||||
prefregex: jail.prefregex,
|
||||
actions: jail.actions,
|
||||
bantime_escalation: jail.bantime_escalation,
|
||||
}),
|
||||
[jail],
|
||||
);
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.fieldRow} style={{ marginBottom: tokens.spacingVerticalS }}>
|
||||
<Field label="Filter">
|
||||
<Input readOnly value={jail.filter || "(none)"} className={styles.codeFont} />
|
||||
</Field>
|
||||
@@ -516,63 +645,15 @@ function InactiveJailDetail({
|
||||
<Input readOnly value={jail.port ?? "(auto)"} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className={styles.fieldRowThree}>
|
||||
<Field label="Ban time">
|
||||
<Input readOnly value={jail.bantime ?? "(default)"} />
|
||||
</Field>
|
||||
<Field label="Find time">
|
||||
<Input readOnly value={jail.findtime ?? "(default)"} />
|
||||
</Field>
|
||||
<Field label="Max retry">
|
||||
<Input readOnly value={jail.maxretry != null ? String(jail.maxretry) : "(default)"} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="Log path(s)">
|
||||
{jail.logpath.length === 0 ? (
|
||||
<Text size={200} className={styles.infoText}>(none)</Text>
|
||||
) : (
|
||||
<div>
|
||||
{jail.logpath.map((p) => (
|
||||
<Text key={p} block size={200} className={styles.codeFont}>
|
||||
{p}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{jail.actions.length > 0 && (
|
||||
<Field label="Actions">
|
||||
<div>
|
||||
{jail.actions.map((a) => (
|
||||
<Badge
|
||||
key={a}
|
||||
appearance="tint"
|
||||
color="informative"
|
||||
style={{ marginRight: tokens.spacingHorizontalXS }}
|
||||
>
|
||||
{a}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Source file">
|
||||
<Field label="Source file" style={{ marginBottom: tokens.spacingVerticalM }}>
|
||||
<Input readOnly value={jail.source_file} className={styles.codeFont} />
|
||||
</Field>
|
||||
|
||||
<div style={{ marginTop: tokens.spacingVerticalL }}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<Play24Regular />}
|
||||
onClick={onActivate}
|
||||
>
|
||||
Activate Jail
|
||||
</Button>
|
||||
</div>
|
||||
<JailConfigDetail
|
||||
jail={jailConfig}
|
||||
onSave={async () => { /* read-only — never called */ }}
|
||||
readOnly
|
||||
onActivate={onActivate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface RegexListProps {
|
||||
patterns: string[];
|
||||
/** Called when the list changes (add, delete, or edit). */
|
||||
onChange: (next: string[]) => void;
|
||||
/** When true, patterns are displayed read-only with no add/delete controls. */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ export function RegexList({
|
||||
label,
|
||||
patterns,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
}: RegexListProps): React.JSX.Element {
|
||||
const styles = useConfigStyles();
|
||||
const [newPattern, setNewPattern] = useState("");
|
||||
@@ -64,6 +67,7 @@ export function RegexList({
|
||||
<Input
|
||||
className={styles.regexInput}
|
||||
value={p}
|
||||
readOnly={readOnly}
|
||||
aria-label={`${label} pattern ${String(i + 1)}`}
|
||||
onChange={(_e, d) => {
|
||||
const next = [...patterns];
|
||||
@@ -71,33 +75,37 @@ export function RegexList({
|
||||
onChange(next);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
size="small"
|
||||
aria-label={`Remove ${label} pattern ${String(i + 1)}`}
|
||||
onClick={() => {
|
||||
handleDelete(i);
|
||||
}}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
appearance="subtle"
|
||||
icon={<Dismiss24Regular />}
|
||||
size="small"
|
||||
aria-label={`Remove ${label} pattern ${String(i + 1)}`}
|
||||
onClick={() => {
|
||||
handleDelete(i);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.regexItem}>
|
||||
<Input
|
||||
className={styles.regexInput}
|
||||
placeholder="New pattern…"
|
||||
value={newPattern}
|
||||
onChange={(_e, d) => {
|
||||
setNewPattern(d.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
/>
|
||||
<Button size="small" onClick={handleAdd}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className={styles.regexItem}>
|
||||
<Input
|
||||
className={styles.regexInput}
|
||||
placeholder="New pattern…"
|
||||
value={newPattern}
|
||||
onChange={(_e, d) => {
|
||||
setNewPattern(d.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
/>
|
||||
<Button size="small" onClick={handleAdd}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface JailConfigUpdate {
|
||||
prefregex?: string | null;
|
||||
date_pattern?: string | null;
|
||||
dns_mode?: string | null;
|
||||
backend?: string | null;
|
||||
log_encoding?: string | null;
|
||||
enabled?: boolean | null;
|
||||
bantime_escalation?: BantimeEscalationUpdate | null;
|
||||
}
|
||||
@@ -498,6 +500,26 @@ export interface InactiveJail {
|
||||
findtime: string | null;
|
||||
/** Number of failures before a ban is issued, or null. */
|
||||
maxretry: number | null;
|
||||
/** Ban duration in seconds, parsed from bantime. */
|
||||
ban_time_seconds: number;
|
||||
/** Failure-counting window in seconds, parsed from findtime. */
|
||||
find_time_seconds: number;
|
||||
/** Log encoding, e.g. ``"auto"`` or ``"utf-8"``. */
|
||||
log_encoding: string;
|
||||
/** Log-monitoring backend. */
|
||||
backend: string;
|
||||
/** Date pattern for log parsing, or null for auto-detect. */
|
||||
date_pattern: string | null;
|
||||
/** DNS resolution mode. */
|
||||
use_dns: string;
|
||||
/** Prefix regex prepended to every failregex. */
|
||||
prefregex: string;
|
||||
/** List of failure regex patterns. */
|
||||
fail_regex: string[];
|
||||
/** List of ignore regex patterns. */
|
||||
ignore_regex: string[];
|
||||
/** Ban-time escalation configuration, or null. */
|
||||
bantime_escalation: BantimeEscalation | null;
|
||||
/** Absolute path to the config file where this jail is defined. */
|
||||
source_file: string;
|
||||
/** Effective ``enabled`` value — always ``false`` for inactive jails. */
|
||||
|
||||
Reference in New Issue
Block a user