When useJailConfigs performs a background refresh, it may deliver an updated
JailConfig object for an already-selected jail. Previously, JailConfigDetail
would continue displaying stale locally-edited form values because the component
only re-initialized on jail name changes (via the key prop), not on object
identity changes.
Added a useEffect that detects when the jail prop reference has changed
(indicating a server refresh) and automatically resets all form fields to the
new server state, but only if autoSave is idle and has no pending changes.
This prevents accidentally overwriting external changes when the user saves,
while still letting users continue editing unsaved changes without interruption.
The implementation:
- Tracks the last-synced jail object in a ref
- Compares incoming jail reference to detect server updates
- Checks autoSave status to ensure no pending saves
- Verifies that current form state matches the old jail values
- Resets all 20+ form fields when conditions are met
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add validationError state to show network/API failures to user
- Use handleFetchError to properly handle auth errors (suppress generic error banner, trigger session-expiry flow)
- Clear validationError when user clicks Validate again
- Ensure error MessageBar renders instead of success banner when validation fails
- Fix InactiveJailDetail onValidate to return Promise as expected by prop type
- Fix useJailConfigs test to use correct JailConfig interface
Fixes TASK-BUG-05: prevents silent validation failures where user cannot distinguish between clean 'no issues' result and server error.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TASK-BUG-04: The autoSavePayload was using the || operator to fall back
to server values when ban_time, find_time, or max_retry were empty or zero.
This silently dropped user intent to set these fields to 0, which is a
valid and meaningful value in fail2ban (e.g., ban_time=0 means permanent ban).
Replace the || fallback with explicit NaN and empty-string guards that
only fall back when:
1. The trimmed input is empty (user cleared the field)
2. The input is non-numeric (NaN)
This preserves valid zero values while still falling back appropriately
for invalid input.
- ban_time: 0 now correctly sends permanent ban instead of falling back
- find_time: 0 now sends the intended value instead of falling back
- max_retry: 0 now sends the intended value instead of falling back
Added comprehensive tests for:
- Preserving zero values in the payload
- Falling back for empty input
- Falling back for non-numeric input
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Toolbar and ToolbarButton to HistoryPage imports
- Add Tooltip import and filterBar style to MapPage
- Fix JailsTab test: use new Set<string>() instead of [] for activeJails
- Add --security-opt=no-new-privileges:true to push.sh build commands
Task 2: rename 'Failures:' label to 'Failed Attempts:' and update tooltip
to 'Total failed authentication attempts currently tracked by fail2ban
across all active jails' — more accurate than 'Currently failing IPs'.
Task 3: move <ServerHealthSection /> to the top of ServerTab so users
see connectivity status before scrolling through all settings fields.
Add key={selectedActiveJail.name} and key={selectedInactiveJail.name} to
JailConfigDetail and InactiveJailDetail in JailsTab.tsx so React unmounts
and remounts the detail component whenever the selected jail changes,
resetting all internal state including the loadedRef guard.
- Delete RecoveryBanner.tsx component and its test
- Remove RecoveryBanner from MainLayout
- Remove onCrashDetected prop from ActivateJailDialog, JailsTab
- Remove fetchPendingRecovery, rollbackJail API functions
- Remove configJailRollback, configPendingRecovery endpoints
- Remove PendingRecovery type
- Add has_local_override field to InactiveJail model
- Update _build_inactive_jail and list_inactive_jails to compute the field
- Add delete_jail_local_override() service function
- Add DELETE /api/config/jails/{name}/local router endpoint
- Surface has_local_override in frontend InactiveJail type
- Show Deactivate Jail button in JailsTab when has_local_override is true
- Add tests: TestBuildInactiveJail, TestListInactiveJails, TestDeleteJailLocalOverride
Adds ability to reload or restart fail2ban service from the Server tab UI.
Backend changes:
- Add new restart() method to jail_service.py that sends 'restart' command
- Add new POST /api/config/restart endpoint in config router
- Endpoint returns 204 on success, 502 if fail2ban unreachable
- Includes structured logging via 'fail2ban_restarted' log entry
Frontend changes:
- Add configRestart endpoint to endpoints.ts
- Add restartFail2Ban() API function in config.ts API module
- Import ArrowSync24Regular icon from Fluent UI
- Add reload and restart button handlers to ServerTab
- Display 'Reload fail2ban' and 'Restart fail2ban' buttons in action row
- Show loading spinner during operation
- Display success/error MessageBar with appropriate feedback
- Update ServerTab docstring to document new buttons
All 115 frontend tests pass.
The Log tab provided a service health panel and log viewer. These are
consolidated into the Server tab with a new ServerHealthSection component
that encapsulates all log-related functionality.
- Extract service health panel and log viewer into ServerHealthSection component
- Add severity-based log line color coding (ERROR=red, WARNING=yellow, DEBUG=gray)
- Implement log filtering, line count selection, and auto-refresh controls
- Scroll to bottom when new log data arrives
- Render health metrics grid with version, jail count, bans, failures
- Show read-only log level and log target in health section
- Handle non-file targets with informational banner
- Import ServerHealthSection in ServerTab and render after map thresholds
- Remove LogTab component import from ConfigPage
- Remove 'log' from TabValue type
- Remove Log tab element from TabList
- Remove conditional render for LogTab
- Remove LogTab from barrel export (index.ts)
- Delete LogTab.tsx and LogTab.test.tsx files
- Update ConfigPage docstring
All 115 frontend tests pass (8 fewer due to deleted LogTab tests).
The Map tab provided a form for editing world-map color thresholds
(low, medium, high). Moving this into the Server tab consolidates all
server-side configuration in one place.
- Add map color thresholds section to ServerTab with full validation
- Load map thresholds on component mount with useEffect
- Implement auto-save for threshold changes via useAutoSave hook
- Display threshold color interpolation guide
- Remove MapTab component import from ConfigPage
- Remove 'map' from TabValue type
- Remove Map tab element from TabList
- Remove conditional render for MapTab
- Remove MapTab from barrel export (index.ts)
- Delete MapTab.tsx file
- Update ConfigPage test to remove MapTab mock
All 123 frontend tests pass.
Global tab provided the same four editable fields as Server tab:
log_level, log_target, db_purge_age, db_max_matches. Server tab already
has these fields plus additional read-only info (db_path, syslog_socket)
and a Flush Logs button.
- Add hint text to DB Purge Age and DB Max Matches fields in ServerTab
- Remove GlobalTab component import from ConfigPage
- Remove 'global' from TabValue type
- Remove Global tab element from TabList
- Remove conditional render for GlobalTab
- Remove GlobalTab from barrel export (index.ts)
- Delete GlobalTab.tsx file
- Update ConfigPage test to remove Global tab test case
All 123 frontend tests pass.
Task 1: Move Configuration to last position in sidebar NAV_ITEMS
Task 2: Add automatic rollback when jail activation fails
- Back up .local override file before writing
- Restore original file (or delete) on reload failure, health-check
failure, or jail not appearing post-reload
- Return recovered=True/False in JailActivationResponse
- Show warning/critical banner in ActivateJailDialog based on recovery
- Add _restore_local_file_sync and _rollback_activation_async helpers
- Add 3 new tests: rollback on reload failure, health-check failure,
and double failure (recovered=False)
Task 3: Color pie chart legend labels to match their slice color
- legendFormatter now returns ReactNode with span style={{ color }}
- Import LegendPayload from recharts/types/component/DefaultLegendContent
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).
Task 2: adds a new Log tab to the Configuration page.
Backend:
- New Pydantic models: Fail2BanLogResponse, ServiceStatusResponse
(backend/app/models/config.py)
- New service methods in config_service.py:
read_fail2ban_log() — queries socket for log target/level, validates the
resolved path against a safe-prefix allowlist (/var/log) to prevent
path traversal, then reads the tail of the file via the existing
_read_tail_lines() helper; optional substring filter applied server-side.
get_service_status() — delegates to health_service.probe() and appends
log level/target from the socket.
- New endpoints in routers/config.py:
GET /api/config/fail2ban-log?lines=200&filter=...
GET /api/config/service-status
Both require authentication; log endpoint returns 400 for non-file log
targets or path-traversal attempts, 502 when fail2ban is unreachable.
Frontend:
- New LogTab.tsx component:
Service Health panel (Running/Offline badge, version, jail count, bans,
failures, log level/target, offline warning banner).
Log viewer with color-coded lines (error=red, warning=yellow,
debug=grey), toolbar (filter input + debounce, lines selector, manual
refresh, auto-refresh with interval selector), truncation notice, and
auto-scroll to bottom on data updates.
fetchData uses Promise.allSettled so a log-read failure never hides the
service-health panel.
- Types: Fail2BanLogResponse, ServiceStatusResponse (types/config.ts)
- API functions: fetchFail2BanLog, fetchServiceStatus (api/config.ts)
- Endpoint constants (api/endpoints.ts)
- ConfigPage.tsx: Log tab added after existing tabs
Tests:
- Backend service tests: TestReadFail2BanLog (6), TestGetServiceStatus (2)
- Backend router tests: TestGetFail2BanLog (8), TestGetServiceStatus (3)
- Frontend: LogTab.test.tsx (8 tests)
Docs:
- Features.md: Log section added under Configuration View
- Architekture.md: config.py router and config_service.py descriptions updated
- Tasks.md: Task 2 marked done
Add alignItems: "end" to the fieldRow grid style so that all grid
cells align their content to the bottom edge of the row. This ensures
the DNS Mode <Select> and the Date Pattern <Combobox> sit on the same
horizontal baseline even though Date Pattern carries a hint line that
makes it taller.
All other fieldRow usages have consistent hint presence across their
fields, so no visual regressions are introduced.
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
- Remove Export tab and all its imports from ConfigPage.tsx
- Remove Refresh and Reload fail2ban buttons from JailsTab; clean up
associated state (reloading, reloadMsg, deactivating) and handlers
- Add Create Config button to Jails tab list pane (listHeader pattern);
create CreateJailDialog component that calls createJailConfigFile API
- Remove Active/Inactive and 'Has local override' badges from FilterDetail
and ActionDetail; remove now-unused Badge imports
- Replace read-only log path spans with editable Input fields in JailConfigDetail
- Export CreateJailDialog from components/config/index.ts
- Mark all 5 tasks done in Docs/Tasks.md
- ActionsTab rewritten with master/detail layout (mirrors FiltersTab)
- New AssignActionDialog and CreateActionDialog components
- ActionConfig type extended with active, used_by_jails, source_file, has_local_override
- New API functions: fetchActions, fetchAction, updateAction, createAction, deleteAction, assignActionToJail, removeActionFromJail
- useActionConfig updated to use new structured endpoints
- index.ts barrel exports updated
- Rewrite FiltersTab: use fetchFilters() for FilterConfig[] with embedded active
status; show 'Active — sshd, apache-auth' badge labels; FilterDetail sub-
component with source_file/override badges, FilterForm, Assign button, raw
config section
- New AssignFilterDialog: selects jail from enabled-jails list, calls
POST /config/jails/{name}/filter with optional fail2ban reload
- New CreateFilterDialog: name+failregex+ignoreregex form, calls
POST /config/filters, closes and selects new filter on success
- Extend ConfigListDetail: add listHeader (for Create button) and
itemBadgeLabel (for custom badge text) optional props
- Fix updateFilterFile bug: was PUT /config/filters/{name} (structured
endpoint), now correctly PUT /config/filters/{name}/raw
- Fix createFilterFile bug: was POST /config/filters, now POST /config/filters/raw
- Add updateFilter, createFilter, deleteFilter, assignFilterToJail to api/config.ts
- Add FilterUpdateRequest, FilterCreateRequest, AssignFilterRequest to types/config.ts
- Add configFiltersRaw, configJailFilter endpoints
- Tests: 24 new tests across FiltersTab, AssignFilterDialog, CreateFilterDialog
(all 89 frontend tests passing)
- Add list_filters() and get_filter() to config_file_service.py:
scans filter.d/, parses [Definition] + [Init] sections, merges .local
overrides, and cross-references running jails to set active/used_by_jails
- Add FilterConfig.active, used_by_jails, source_file, has_local_override
fields to the Pydantic model; add FilterListResponse and FilterNotFoundError
- Add GET /api/config/filters and GET /api/config/filters/{name} to config.py
- Remove the shadowed GET /api/config/filters list route from file_config.py;
rename GET /api/config/filters/{name} raw variant to /filters/{name}/raw
- Update frontend: fetchFilterFiles() adapts FilterListResponse -> ConfFilesResponse;
add fetchFilters() and fetchFilter() to api/config.ts; remove unused
fetchFilterFiles/fetchActionFiles calls from useConfigActiveStatus
- Fix ConfigPageLogPath test mock to include fetchInactiveJails and related
exports introduced by Stage 1
- Backend: 169 tests pass, mypy --strict clean, ruff clean
- Frontend: 63 tests pass, tsc --noEmit clean, eslint clean
- Backend: config_file_service.py parses jail.conf/jail.local/jail.d/*
following fail2ban merge order; discovers jails not running in fail2ban
- Backend: 3 new API endpoints (GET /jails/inactive, POST /jails/{name}/activate,
POST /jails/{name}/deactivate); moved /jails/inactive before /jails/{name}
to fix route-ordering conflict
- Frontend: ActivateJailDialog component with optional parameter overrides
- Frontend: JailsTab extended with inactive jail list and InactiveJailDetail pane
- Frontend: JailsPage JailOverviewSection shows inactive jails with toggle
- Tests: 57 service tests + 16 router tests for all new endpoints (all pass)
- Docs: Features.md, Architekture.md, Tasks.md updated; Tasks 1.1-1.5 marked done
Replace Accordion-based config tabs with the new ConfigListDetail two-pane
layout. Each tab now shows a searchable list with active/inactive badges
(active items sorted first) on the left and a structured form editor with
a collapsible raw-text export section on the right.
- ConfigListDetail: reusable two-pane master/detail layout (list + detail)
with active/inactive badges, sorted active-first, keyboard navigation,
and responsive collapse to Dropdown below 900 px
- RawConfigSection: collapsible raw-text editor with save/feedback for
any config file, backed by configurable fetch/save callbacks
- useConfigActiveStatus: hook that derives active jail, filter, and action
sets from the live jails list and jail config data