Commit Graph

183 Commits

Author SHA1 Message Date
29daaa9906 TASK-004: Bootstrap frontend auth state from backend session check
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>
2026-04-26 12:00:21 +02:00
045c8048fe Refactor: Replace inline style objects with makeStyles classes
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>
2026-04-25 19:48:25 +02:00
c1135150c3 Refactor: Move DashboardFilterProvider to pages directory
- 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>
2026-04-25 19:45:10 +02:00
69a0296c47 T-18: Merge useDashboardCountryData and useMapData into shared base hook
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>
2026-04-25 19:39:51 +02:00
3b527244aa Update task documentation and test fixes
- Update Tasks.md with current progress and status
- Fix useListData hook tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 19:34:16 +02:00
6490e9d3df T-16: Centralize PAGE_SIZE frontend constants
- 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>
2026-04-25 19:27:24 +02:00
f84aeef249 Refactor authentication logic and API client
- 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>
2026-04-25 19:23:12 +02:00
6a062a72a7 refactor: move jail detail sub-sections from pages/jail to components/jail
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>
2026-04-25 19:17:03 +02:00
8bd5713d38 Refactor jail detail hooks: split into useJailData and useJailCommands
- 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>
2026-04-25 19:14:16 +02:00
8d30a81346 feat(hooks): consolidate data-fetching patterns with useListData and usePolledData
- 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>
2026-04-25 19:08:26 +02:00
6d21a53620 Update tasks and history page tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 09:59:21 +02:00
eaff272aae feat: Add dismissible warning UI for threshold loading errors
- 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>
2026-04-23 09:58:27 +02:00
ac44bab8e6 Update documentation and refactor useAutoSave hook
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 09:52:22 +02:00
9c5757eeb0 Refactor useHistory hook: replace HistoryQuery with explicit parameters and add documentation
- 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>
2026-04-23 09:51:16 +02:00
8904e180d1 Fix: Prevent session-expiry errors from briefly showing in useConfigItem.save()
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>
2026-04-23 09:45:11 +02:00
6d5be523ab fix: KVEditor effect dependency uses stable JSON serialization
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>
2026-04-23 09:41:49 +02:00
9375430e02 Refactor schedule functionality in frontend
- 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>
2026-04-23 09:40:19 +02:00
f3d6574160 Fix misleading auth token storage in sessionStorage
- 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>
2026-04-23 09:30:29 +02:00
941502b710 Fix BanTable to use props exclusively (complete dual state source refactoring)
- 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>
2026-04-23 09:26:45 +02:00
814000fe68 Refactor DashboardFilterBar to use props exclusively, eliminate dual state source
- 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>
2026-04-23 09:24:16 +02:00
10c534d090 Stabilize function references in useJails with useCallback
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>
2026-04-23 09:18:31 +02:00
1bcc336c9b Add tests and documentation updates for log preview and regex tester hooks
- 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>
2026-04-23 09:14:58 +02:00
584588e363 fix: add AbortController and unmount guard to useIpLookup hook
- 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>
2026-04-23 08:43:11 +02:00
4e3f2005f9 fix: capture AbortController in local variable to avoid race condition in three hooks
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>
2026-04-23 08:40:03 +02:00
0223cb12a4 fix(hooks): Forward abort signal to fetchBansByCountry in useDashboardCountryData
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>
2026-04-23 08:36:27 +02:00
5a6cb640d8 Add AbortSignal support to API functions for request cancellation
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>
2026-04-23 08:33:28 +02:00
f0caa24d91 fix(ServerHealthSection): add debounce to linesCount input to prevent rapid API calls
- 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>
2026-04-23 08:24:58 +02:00
1510dfc851 fix(config): gate useJails() calls behind dialog open prop
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>
2026-04-23 08:18:19 +02:00
97d47fae81 fix(jails): consolidate useJails() calls to eliminate double HTTP request
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>
2026-04-23 08:12:31 +02:00
3024a4ef07 fix(config): re-sync JailConfigDetail form when jail prop updates from server refresh
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>
2026-04-23 08:08:33 +02:00
1d50bc1a73 fix: add validation error handling to InactiveJailDetail
- 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>
2026-04-23 08:02:24 +02:00
649ebf2dc7 fix: preserve zero values in autoSavePayload
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>
2026-04-22 21:30:31 +02:00
d1674add90 fix: Stop MapPage pagination resetting on every data refresh
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>
2026-04-22 21:24:05 +02:00
0bfa975222 Fix: Keep ConfigPage tabs mounted to preserve form state
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>
2026-04-22 21:21:36 +02:00
0f261e31c2 Fix infinite re-fetch loop in useJailConfigs
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>
2026-04-22 21:16:42 +02:00
0481810226 Fix open redirect vulnerability in LoginPage
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>
2026-04-22 21:04:17 +02:00
a286ede49c Refactor frontend components and dependencies
- Update ESLint configuration for frontend
- Refactor dialog components (ActivateJail, CreateAction, CreateFilter, CreateJail)
- Update JailsTab and RegexTesterTab components
- Refactor TopCountriesPieChart component
- Update package.json dependencies
- Update documentation (Tasks.md)
- Refactor CodeList component for jail page

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 20:26:43 +02:00
b7fbad0328 Add dashboard filter context to remove prop drilling 2026-04-21 20:08:54 +02:00
b6d9c649ca Delete hook barrel files and switch to direct hook imports 2026-04-21 20:02:50 +02:00
1ba82d56e7 Refactor ServerTab and ConfFilesTab to use reducers 2026-04-21 19:52:05 +02:00
260ce7e875 Fix frontend config tests for strict type narrowing 2026-04-21 19:40:51 +02:00
4c313af1c5 Narrow jail config types with explicit union values 2026-04-21 19:39:36 +02:00
fef8f60ee2 Add dark mode support with persisted OS-aware theme selection 2026-04-21 19:30:29 +02:00
4f91e8fdd3 Persist sidebar collapsed preference to localStorage 2026-04-21 19:17:00 +02:00
b3eb5dc6ec Standardise loading state naming across dashboard hooks 2026-04-21 19:12:43 +02:00
094fb4fece Replace index keys with stable keys in editable list components 2026-04-21 19:04:18 +02:00
4da2703966 Move constant inline styles into makeStyles 2026-04-21 18:47:18 +02:00
86a7336ac0 Refactor shared data source selection for dashboard and map 2026-04-21 17:56:59 +02:00
e244a85291 Extract generic useListData hook for shared list fetching 2026-04-21 17:53:58 +02:00
e683108965 Standardise AbortController cancellation in setup and server health hooks
Add abortable API signals for setup status and server health/log fetches, document hook cancellation patterns, and cover stale refresh cancellation with tests.
2026-04-21 17:38:35 +02:00