Implement request deduplication to prevent multiple duplicate calls to GET
/api/setup when multiple components mount simultaneously. The fix introduces:
1. New 'useSharedSetupStatus' hook with module-level caching
- Shares a single in-flight request across all consumers
- Implements 30-second cache TTL with cache invalidation
- Notifies all subscribers when cache is invalidated
2. Refactored 'useSetup' hook to use shared cache
- Internally uses useSharedSetupStatus for status checks
- Calls invalidateSetupStatus() after successful setup submission
- Maintains backward-compatible API
3. Updated components using setup status
- SetupGuard and SetupPage automatically benefit from deduplication
- No changes needed to consumer code
4. Updated tests
- Mocked useSharedSetupStatus in component tests
- Added comprehensive tests for cache behavior
- All existing tests pass
5. Documentation updates
- Added 'Request Deduplication & Shared Caching' section to Web-Development.md
- Explains when and how to use shared hooks
- Provides complete implementation example
This eliminates wasted resources from duplicate API calls and potential
race conditions where different requests return slightly different states.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validates session on app mount by calling GET /api/auth/session instead of relying
solely on cached sessionStorage. This ensures the UI state always reflects server
reality — expired or revoked sessions are detected immediately.
Changes:
- Backend: Add GET /api/auth/session endpoint (requires valid session, returns 200/401)
- Frontend: Add useSessionValidation hook for mount-time validation
- Frontend: Add SessionValidationLoading component for validation spinner
- Frontend: Update AuthProvider to call validation on mount with loading state
- Frontend: Add validateSession API function
- Docs: Update Features.md with session validation behavior
- Docs: Update Web-Development.md with session validation pattern
Handles three outcomes:
1. Valid session (200): Proceed with cached state
2. Invalid session (401): Clear sessionStorage and redirect to login
3. Network error: Don't logout (backend may be temporarily unreachable)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
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 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>
- 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>
- 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>
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>
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>
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>
Vite runs inside the frontend container where 'localhost' resolves to
the container itself, not the backend. Change the /api proxy target
from http://localhost:8000 to http://backend:8000 so the request is
routed to the backend service over the compose network.