The useDashboardCountryData hook was creating an AbortController and checking
signal.aborted in callbacks, but was not passing the signal to the fetchBansByCountry
API call. This meant the HTTP request itself was never actually aborted.
Now the signal is forwarded, allowing proper request cancellation when the hook
unmounts or dependencies change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add optional signal?: AbortSignal parameter to all API GET functions so they can be
cancelled when components unmount. This prevents state-update warnings and wasted
resources.
Changes:
- frontend/src/api/history.ts: fetchHistory, fetchIpHistory
- frontend/src/api/map.ts: fetchBansByCountry
- frontend/src/api/jails.ts: fetchJails, fetchActiveBans
- frontend/src/api/config.ts: fetchJailConfig, fetchInactiveJails, fetchJailConfigFiles,
fetchFilterFiles (threads signal through fetchFilters), fetchFilterFile, fetchActionFiles,
fetchActionFile
- frontend/src/api/blocklist.ts: fetchImportLog, previewBlocklist
Updated all calling hooks to pass the abort signal from their controllers:
- useHistory, useIpHistory
- useMapData
- useActiveBans
- useJails
- useConfigActiveStatus (fetchJails and fetchJailConfigs)
- useJailAdmin (fetchInactiveJails)
- useJailConfigDetail (fetchJailConfig)
- useImportLog (fetchImportLog)
- useBlocklists (previewBlocklist with AbortController)
Updated Docs/Web-Development.md to document the convention.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Introduce linesCountRaw state to capture raw input values
- Add handleLinesCountChange callback with 300ms debounce delay
- Reuse existing filterDebounceRef pattern with linesCountDebounceRef
- Guard against zero/negative values by enforcing minimum of 100 lines
- Update Select component to use debounced value and new handler
- Add comprehensive test coverage for debounce behavior and input validation
Fixes TASK-BUG-09: Typing '500' in the Lines field now fires single API
request instead of three (one per keystroke). This mirrors the existing
debounce pattern used for the filter input.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactored AssignActionDialog and AssignFilterDialog to only render
dialog content when open=true. This prevents useJails() from being called
when dialogs are closed, eliminating unnecessary GET /api/jails requests.
Implementation uses inner components (AssignActionDialogInner,
AssignFilterDialogInner) that are only mounted when the dialog is open.
The Dialog wrapper remains in the outer component to preserve Fluent UI
animation behavior.
Fixed test setup for AssignFilterDialog to properly call
assignFilterToJail from the mocked onAssign callback.
Fixes TASK-BUG-08.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TASK-BUG-07: Remove duplicate useJails() hook call on JailsPage
Previously, useJails() was called twice on page load:
1. In JailsPage to extract jailNames for BanUnbanForm
2. In JailOverviewSection to manage the jail table
This caused two parallel GET /api/jails requests on every page load.
Changes:
- Lift useJails() to JailsPage as the single source of truth
- Accept jail state as props in JailOverviewSection
- Thread all required state (jails, total, loading, error, and action
handlers) down from JailsPage to JailOverviewSection
- Remove useJails hook import from JailOverviewSection
This consolidation reduces unnecessary HTTP requests and improves
page load performance, especially with many jails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Remove 'bans' from the useEffect dependency array that resets pagination.
Since 'bans' changes with every background data refresh (new array reference),
the page was being reset to 1 every 30 seconds, making the table unusable for
pagination beyond the first page.
Add a separate effect that clamps the current page to totalPages when the
data shrinks below the current page offset (edge case when filtered results
are fewer than displayed page).
Fixes TASK-BUG-03.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, the tab content wrapper used 'key={tab}' which caused React to
unmount and remount the entire subtree when switching tabs. This destroyed
all component state, including unsaved form data and pending auto-saves.
Changes:
- Removed 'key={tab}' from the wrapper div
- All tab panels now render at page initialization
- Inactive tabs use CSS 'display: none' to hide without unmounting
- Tabs remain mounted throughout the page lifetime
- Users can now switch tabs without losing form input
Updated ConfigPage.test.tsx to reflect that inactive tabs remain in the DOM
(just hidden with CSS) rather than being removed entirely.
Documentation: Added 'Tab Panels' section to Web-Development.md
explaining the rule and rationale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hook was passing an inline onSuccess callback to useListData, which
included onSuccess in its internal refresh function's dependency array.
This caused refresh to be recreated on each render, which triggered the
useEffect, which fired the fetch, which completed and caused a re-render,
creating an infinite loop.
Wrap onSuccess in useCallback with empty dependencies so it maintains a
stable reference across renders. This allows refresh to be stable when
its dependencies don't change, breaking the cycle.
Add documentation to Refactoring.md explaining the onSuccess stability
requirement for useListData callers.
Also add tests for useJailConfigs to verify it doesn't trigger infinite
refetches with stable onSuccess callback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validate the ?next= query parameter to prevent open redirects to
external URLs. The parameter is validated to ensure it is a relative
path (starts with / but not //) before using it for navigation.
Invalid paths fall back to '/'.
This prevents attackers from crafting login links like /login?next=https://evil.com
that would transparently redirect authenticated users to malicious sites.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add abortable API signals for setup status and server health/log fetches, document hook cancellation patterns, and cover stale refresh cancellation with tests.