Moved all static layout properties (display, gap, margin, padding, colour)
from inline style props to makeStyles classes in:
- MapBansTable.tsx: Pagination row flexbox layout
- JailDetailPage.tsx: Link styling for textDecoration
- HistoryPage.tsx: Summary text styling
- IpDetailView.tsx: Loading container and text formatting
Kept inline styles only for genuinely dynamic values:
- WorldMap.tsx: Tooltip position (follows mouse)
- TopCountriesPieChart.tsx: Legend color (from recharts data)
- TopCountriesBarChart.tsx: Chart height (derives from data length)
This change improves performance by leveraging Griffel's atomic CSS cache
and ensures consistency with the established Fluent UI pattern.
Updated Docs/Web-Development.md with explicit rule: inline styles only
for runtime-dynamic values, all static properties go in makeStyles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move DashboardFilterProvider component and tests from providers/ to pages/
- Update DashboardPage imports to reflect new structure
- Update documentation with latest task progress
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create useBansByCountry as the shared base hook containing all common
fetch logic, abort-controller pattern, and state management. Both
useDashboardCountryData and useMapData now wrap this base hook:
- useDashboardCountryData: Thin wrapper that calls base hook with autoFetch=true
- useMapData: Wraps base hook with 300ms debounce layer
Changes:
- Create useBansByCountry.ts (base hook with optional autoFetch parameter)
- Refactor useDashboardCountryData.ts to use base hook
- Refactor useMapData.ts to use base hook with debounce wrapper
- Add tests for all three hooks
Benefits:
- Single source of truth for ban-by-country logic
- Bug fixes in base hook apply to both consumers
- Eliminates code duplication (~80 lines reduced)
- Maintains backward compatibility: existing call sites work unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add BAN_PAGE_SIZE (100) and HISTORY_PAGE_SIZE (50) to frontend/src/utils/constants.ts
- Replace local PAGE_SIZE definitions in useBans.ts and HistoryPage.tsx with imports
- Eliminates risk of pagination constants silently diverging from backend defaults
- Single source of truth for all pagination sizes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Update AuthProvider with improved error handling and token management
- Enhance API client with better request/response handling
- Add comprehensive test coverage for auth flows
- Update documentation with current tasks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move reusable UI section components (JailInfoSection, PatternsSection,
BantimeEscalationSection, IgnoreListSection, CodeList) from pages/jail/
to components/jail/, aligning with the project convention that pages/
contains only route-level entry points while components/ contains reusable
UI building blocks.
Changes:
- Move 5 section components + jailDetailPageStyles.ts to components/jail/
- Update import paths in moved components (relative paths to commonStyles)
- Update JailDetailPage.tsx imports to reference components/jail/
- Delete empty pages/jail/ directory
- Document pages/ vs components/ distinction in Web-Development.md
All components use standard import structure and TypeScript passes type
checking. BannedIpsSection was already correctly placed in components/jail/.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Split monolithic useJailDetail hook into separate concerns
- Created useJailData for fetching and managing jail data
- Created useJailCommands for jail operations (power, console, etc.)
- Updated JailDetailPage to use new hooks
- Updated tests to reflect new hook structure
- Removed old useJailDetail hook
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Refactor useJails (useJailList.ts) to use useListData with onSuccess for total
- Refactor useBanTrend to use useListData with onSuccess for bucket_size
- Refactor useDashboardCountryData to use useListData with onSuccess for aggregated data
- Refactor useHistory to use useListData with proper abort guard in finally()
- Create usePolledData for single-item endpoints with polling and window focus refetch
- Refactor useServerStatus to use usePolledData for 30s polling + window focus refetch
- Keep useIpHistory with manual pattern (single-item, no list semantics)
- Document deferred refactoring of useJailDetail (depends on T-13 for data/command split)
All data-fetching hooks now follow one of two consistent patterns:
1. useListData: for paginated/list endpoints with refresh semantics
2. usePolledData: for single-item endpoints with polling and focus-refetch
This eliminates code duplication, centralizes abort-guard logic, and enables
consistent fixes across all data-fetching hooks.
Resolves T-12.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Problem: Repository modules use structural typing to satisfy Protocol interfaces via
cast(). A function rename, parameter change, or signature mismatch would silently pass
mypy but fail at runtime.
Solution (Option B — minimal):
1. Aligned Protocol signatures in protocols.py with actual implementations:
- BlocklistRepository: dict[str, object] → dict[str, Any] (matches implementation)
- ImportLogRepository: dict[str, object] → ImportLogRow (typed model)
- GeoCacheRepository: dict[str, object] → GeoCacheRow; Iterable → Sequence
- HistoryArchiveRepository: dict[str, object] → dict[str, Any]
- ImportLogRepository: async compute_total_pages → sync (matches implementation)
2. Created CI validation script (backend/scripts/validate_repository_protocols.py)
that runs at build time to ensure all repository modules satisfy their Protocol
interfaces. Exit 0 if valid, 1 if any mismatch. Detects:
- Missing functions
- Parameter count mismatches
- Type annotation mismatches
- Return type mismatches
3. Updated backend/app/dependencies.py with explicit docstrings linking each
get_*_repo() provider to Backend-Development.md § 13.7.1, explaining the
module-as-Protocol pattern and that it is intentional and validated.
4. Documented the pattern in Backend-Development.md § 13.7.1:
'Repository Module Pattern — Module-as-Protocol Structural Compatibility'
explaining why the pattern works, risks (silent breakage), and how the
validation mitigates it.
5. Fixed type annotation in history_archive_repo.py:
- get_all_archived_history returns list[dict] → list[dict[str, Any]]
- Imported Any type
Benefits:
- Prevents silent breakage of repository interfaces
- Formalizes the module-as-Protocol pattern as intentional
- CI validation prevents regressions without refactoring cost
- All repository tests pass (53/53)
- mypy --strict passes on modified files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of returning a bound method (geo_cache.lookup_batch), now inject
the GeoCache instance directly into routers and services. This provides
proper runtime isolation since T-04 made GeoCache a proper object.
Changes:
- Remove get_geo_batch_lookup() dependency provider
- Add GeoCacheDep type alias for injecting GeoCache instances
- Update all routers (bans, blocklist, dashboard, jails) to use GeoCacheDep
- Update ban_service, blocklist_service, jail_service to accept GeoCache
- Update service protocols to match new signatures
- Update docstrings to reference GeoCache methods instead of module functions
All callers now call geo_cache.lookup_batch(...) directly instead of
geo_batch_lookup(...), providing real dependency injection with proper
testing isolation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate the two divergent implementations of _since_unix from ban_service.py
and history_service.py into a single shared utility function in time_utils.py.
Changes:
- Move _since_unix to app/utils/time_utils.py with consistent time.time() approach
- Move TIME_RANGE_SLACK_SECONDS constant to app/utils/constants.py
- Update ban_service.py to import since_unix from time_utils
- Update history_service.py to import since_unix from time_utils
- Both services now use the same window boundary calculation with 60-second slack
- Add comprehensive tests for the shared since_unix function
- Document timestamp handling rationale in Backend-Development.md
This ensures dashboard and history queries return consistent row counts for the
same time range by using the same timestamp calculation and slack window across
all services.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Refactor action_config_service, filter_config_service, jail_config_service, and jail_service
- Add jail_socket utility module for socket communication
- Update test_jail_service with new test cases
- Update architecture and task documentation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The AppState Protocol (lines 42-54) and ApplicationContext dataclass
(lines 57-69) described identical fields, creating maintenance burden.
The Protocol was only used for a single cast() in _build_app_context.
Changes:
- Remove AppState Protocol class
- Import ApplicationState from runtime_state.py
- Replace cast('AppState', request.app.state) with
cast(ApplicationState, request.app.state)
- Remove unused Protocol import
This eliminates the redundancy while maintaining the same typing
guarantees. request.app.state is set to ApplicationState instances
during app initialization in main.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move session cache initialization from per-request _build_app_context to
startup lifespan handler. The session cache type is now decided once at app
startup based on settings, making _build_app_context pure (read-only).
Changes:
- Move cache initialization logic to new _update_session_cache() in main.py
- Call _update_session_cache() during lifespan startup to initialize cache
- Remove three if/elif/elif branches mutating state.session_cache from _build_app_context
- Add cache swap logic to set_runtime_settings() in runtime_state.py to handle
runtime settings changes (e.g., setup wizard updates)
- Keep app.state.session_cache initialization in create_app() for test compatibility
This ensures:
- _build_app_context is pure and doesn't mutate app state on each request
- Session cache configuration decisions are centralized at startup
- Settings changes during runtime (via setup wizard) also trigger cache swap
- Cache initialization logic is isolated in one place
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create GeoCache class with all mutable state as instance attributes:
- _cache, _neg_cache, _dirty, _geoip_reader, _geoip_initialized, _cache_lock
- All public methods: lookup(), lookup_batch(), lookup_cached_only(), flush_dirty(), load_from_db(), clear(), etc.
Initialization & Dependency Injection:
- Instantiate GeoCache in startup.py and store on app.state.geo_cache
- Add get_geo_cache() dependency function in dependencies.py
- Inject into routes and tasks via FastAPI's dependency system
Backward Compatibility:
- Maintain module-level functions in geo_service.py as deprecated wrappers
- All old callers continue to work through _default_geo_cache instance
- Remove test-escape-hatch functions (clear_cache, clear_neg_cache moved to methods)
Background Tasks:
- Update geo_cache_flush.py and geo_re_resolve.py to receive GeoCache instance
- Tasks now operate on injected instance rather than module globals
Tests:
- Refactor test_geo_service.py with geo_cache fixture providing fresh instances
- Update patch paths to target GeoCache methods correctly
- Fix internal state assertions to access instance attributes
Documentation:
- Update Architekture.md to document GeoCache as managed stateful service
- Describe cache lifecycle (load on startup, flush periodically, re-resolve stale)
- Note process-local limitations for multi-worker deployments
Fixes violation of Single Responsibility Principle: module no longer owns both
lookup logic and cache lifecycle management. Cache is now a first-class
injectable service with transparent lifecycle.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reorganized dashboard router with improved structure
- Enhanced ban_service with better separation of concerns
- Updated history service with cleaner logic
- Improved constants and configuration handling
- Updated documentation of completed tasks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After removing all try/except blocks that used HTTPException for domain
exception conversion, these imports are no longer needed in the routers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate duplicate _ok(), _to_dict(), ensure_list(), and is_not_found_error()
functions from 6 service modules into a single canonical implementation at
backend/app/utils/fail2ban_response.py.
Changes:
- Create fail2ban_response.py with canonical implementations
- Remove local duplicates from: ban_service, jail_service, config_service,
health_service, server_service, config_file_utils
- Update all imports to use shared module
- Add comprehensive docstrings and examples
- Update Architecture.md and Backend-Development.md documentation
Benefits:
- Single source of truth for response parsing logic
- Eliminates code duplication across service layer
- Improves maintainability and consistency
- Enables centralized bug fixes and improvements
Tests: All 228 service tests passing, no regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace console.warn with visible MessageBar warning when map color thresholds fail to load
- Add DismissRegular icon button to allow users to dismiss the warning
- Add dismissedThresholdWarning state to manage warning visibility
- Add mock and test for useMapColorThresholds hook
- Add test case verifying warning displays and can be dismissed
- Remove TASK-QUALITY-04 from Tasks.md (completed)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Split useHistory interface to accept explicit parameters (page, pageSize, range, origin, jail, ip, source) instead of HistoryQuery object
- Add comprehensive JSDoc for useHistory function
- Update HistoryPage and tests to use new parameter structure
- Move TaskList documentation from Tasks.md to Web-Development.md
- Improve type safety with explicit TimeRange and BanOriginFilter types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When save() encounters a 401 or 403 error, the HTTP client dispatches
SESSION_EXPIRED_EVENT which triggers auth handling and navigation to login.
However, setSaveError was called first, causing a brief flash of an
'Unauthorized' message before the redirect.
Now, isAuthError(err) checks if the error is a 401/403 before setting
saveError. Auth errors are rethrown without setting error state, allowing
the auth handler to deal with session expiry cleanly without UX confusion.
- Import isAuthError from api/client in useConfigItem hook
- Check for auth errors in the save() catch block before setSaveError
- Add tests for 401 and 403 error handling
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the flawed join-based comparison (entryKeys.join(',')) with
JSON.stringify() to properly handle keys containing commas. The previous
implementation could produce false equality when different key sets
shared the same comma-separated representation (e.g., 'a,b' key vs
separate 'a' and 'b' keys).
This ensures the effect fires correctly when keys change, fixing silent
failures to update derived state.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract schedule logic into custom useSchedule hook
- Update BlocklistScheduleSection to use the new hook
- Add tests for useSchedule hook
- Update documentation with task progress
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove JWT token and expires_at from sessionStorage
- Simplify AuthProvider to use boolean isAuthenticated flag
- Persist only isAuthenticated boolean for page-reload continuity
- Update AuthProvider test to verify new auth model
- Add comprehensive auth documentation to Web-Development.md explaining:
- Cookie-based authentication model
- How frontend auth state persists
- Why tokens are no longer stored
- Error handling flow for 401/403 responses
The authentication model is cookie-based: the backend sets bangui_session
cookie on login, frontend automatically includes it via credentials:
'include', and the backend is the sole authority on session validity.
Previously stored tokens were never actually used and made the auth model
misleading during development.
Fixes TASK-STATE-04.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- BanTable now requires all filter props (timeRange, origin, source)
- Removed useDashboardFilters() hook dependency from BanTable
- Eliminated context fallback chain using ?? operator
- Component now exclusively reads from props, never from context
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DashboardFilterBar now requires all filter props (timeRange, onTimeRangeChange, originFilter, onOriginFilterChange) instead of falling back to context
- Removed useDashboardFilters() hook dependency from DashboardFilterBar, BanTrendChart, and JailDistributionChart
- Updated DashboardPage to explicitly pass all filter values and callbacks from context to components
- Made props required on BanTrendChart and JailDistributionChart
- Updated all tests to reflect new prop requirements
- This eliminates the silent dual-source behavior that could lead to subtle bugs when components are used with different data sources
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, the withRefresh helper and all operations (startJail, stopJail, setIdle, reloadJail, reloadAll) were recreated on every render because they were defined in the hook body without useCallback. This caused unnecessary re-renders of child components using React.memo when parent state changed.
Now each operation is wrapped in useCallback with [load] as its dependency. This ensures function references remain stable between renders, allowing React.memo optimizations to work correctly in JailOverviewSection.
Tests confirm that function references are now stable between consecutive renders.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add useLogPreview.test.ts with comprehensive test coverage
- Add useRegexTester.test.ts with comprehensive test coverage
- Update Docs/Tasks.md and Docs/Web-Development.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add AbortController to cancel pending IP lookups when component unmounts
- Prevent state updates on unmounted components by checking abort signal before setState calls
- Add useEffect cleanup that aborts pending requests on unmount
- Update lookupIp API function to accept optional AbortSignal parameter
- Converts callback-based fetch to async/await pattern for better control flow
- Aligns with other API functions that already support abort signals (fetchJails, fetchJailBannedIps)
Fixes TASK-ABORT-04
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TASK-ABORT-03: Fix stale abortRef read in .finally() callbacks
In useGlobalConfig, useServerSettings, and useJailConfigDetail hooks, the
.finally() block was reading abortRef.current instead of using the locally
captured controller reference. If load() is called while a fetch is in flight,
the previous fetch's .finally() would read the new controller (not aborted)
and prematurely clear the loading state while the new fetch is still pending.
Changes:
- useGlobalConfig.ts: use locally-captured ctrl in .finally() (line 46)
- useServerSettings.ts: use locally-captured ctrl in .finally() (line 50)
- useJailConfigDetail.ts: use locally-captured ctrl in .finally() (line 47)
All three hooks already use ctrl correctly in .then() and .catch() callbacks.
Documentation:
- Add 'AbortController in Hooks' section to Web-Development.md
- Explains the pattern and shows incorrect vs correct examples
- Prevents future regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>