Allow useNavigationAbortSignal to opt out of navigation-based abort for long-lived background tasks like polling. Set ignoreCancellation: true to keep requests alive across route changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
10 KiB
Issue #60: MEDIUM - NavigationCancellationProvider Orphans Requests on Rapid Navigation
Where found:
frontend/src/providers/NavigationCancellationProvider.tsxfrontend/src/hooks/useNavigationAbortSignal.ts:42-52
Why this is needed: When a user navigates A → B → C rapidly, B's in-flight requests are not cancelled because B's signal is replaced before B's requests check it. These requests complete and may write stale data into the wrong page's state.
Goal: Every request initiated for a page is cancelled when that page is navigated away from, regardless of navigation speed.
What to do:
- Associate each request with the pathname that was active when it started, not the current pathname.
- On navigation, abort all controllers whose associated pathname no longer matches the current route.
Possible traps and issues:
- Requests that intentionally survive navigation (e.g., background syncs) must opt out; provide an
ignoreCancellationflag.
Docs changes needed:
frontend/src/providers/PROVIDER_ORDER.md: document the cancellation contract.
Doc references:
frontend/src/providers/NavigationCancellationProvider.tsx
Issue #61: MEDIUM - Pagination Offset vs Cursor Mode Indistinguishable to Frontend
Where found:
backend/app/utils/pagination.py:265-305backend/app/models/response.py:125-180
Why this is needed:
The PaginationMetadata object uses sentinel values (total=-1, total_pages=-1) for cursor mode. If a backend endpoint silently switches pagination modes, frontend code using total_pages to render page controls will display -1 with no error.
Goal: Frontend code can reliably detect which pagination mode is in use and render accordingly.
What to do:
- Add a
mode: "offset" | "cursor"discriminator field toPaginationMetadata. - Update frontend pagination components to branch on
moderather than checking for-1.
Possible traps and issues:
- Adding a required field is a breaking change; make it optional with a default of
"offset"for backward compatibility.
Docs changes needed:
- API reference: document the
modefield and its values.
Doc references:
backend/app/utils/pagination.py
Issue #62: MEDIUM - Blocklist URL Validation Is Async With No Rollback on Failure
Where found:
backend/app/services/blocklist_service.pybackend/app/models/blocklist.py:36-40
Why this is needed: DNS validation runs asynchronously after the model is validated. If validation fails or is slow, concurrent requests can insert duplicate or invalid blocklist sources before the validation result is checked, leaving the database in a dirty state.
Goal: Blocklist source creation is atomic: either validation passes and the row is committed, or validation fails and no row exists.
What to do:
- Perform DNS/URL validation inside a database transaction; roll back on failure.
- Add a unique constraint on the URL column to catch duplicates at the DB level.
- Return a conflict error (409) on duplicate URL submissions.
Possible traps and issues:
- Async DNS lookup inside a transaction holds the transaction open longer; use a short timeout.
Docs changes needed:
- API reference: document the 409 conflict response for duplicate URLs.
Doc references:
backend/app/services/blocklist_service.py
Issue #63: MEDIUM - Correlation ID Lost Across Background Task Boundaries
Where found:
backend/app/tasks/health_check.py:70-74backend/app/utils/correlation.py
Why this is needed:
Background tasks that spawn sub-tasks (e.g., health check triggering failover logic) do not propagate the correlation ID ContextVar to child asyncio tasks. Logs from child tasks appear without a correlation ID, breaking distributed tracing.
Additionally, reset_correlation_id() in the finally block clears the ID before all child tasks have logged.
Goal: Every log line emitted during a background job carries its originating correlation ID.
What to do:
- Use
asyncio.create_task(coro, context=copy_context())to propagate theContextVarto child tasks. - Move
reset_correlation_id()to after all child tasks have completed.
Possible traps and issues:
copy_context()captures a snapshot; mutations in the parent after the copy won't be seen by the child (this is the desired behavior).
Docs changes needed:
- Add inline comment in
health_check.pyexplaining context propagation.
Doc references:
backend/app/utils/correlation.py
Issue #64: MEDIUM - External Logging Failure Silently Swallowed
Where found:
backend/app/main.py:192-213
Why this is needed: When Datadog, Papertrail, or Elasticsearch log handler initialization fails, the error is caught, logged as a warning to stdout, and the application continues. In production this means critical logs may never reach the monitoring system, and operators will not know until an incident occurs.
Goal: External logging failures are surfaced to operators at deployment time.
What to do:
- Promote the warning to an error and expose it via the health endpoint (Issue #57).
- Add a startup check: if
EXTERNAL_LOG_REQUIRED=trueand initialization fails, abort startup. - Emit a metric/alert on initialization failure.
Possible traps and issues:
- Making startup fail on logging issues may be too strict for some environments; make
EXTERNAL_LOG_REQUIREDoptional and default tofalse.
Docs changes needed:
Docs/Deployment.md: documentEXTERNAL_LOG_REQUIREDand the health check for logging status.
Doc references:
backend/app/main.pylogging initialization block
Issue #65: MEDIUM - Abort Selector Inconsistency in useFetchData
Where found:
frontend/src/hooks/useFetchData.ts:124-131
Why this is needed:
When a request is aborted, refresh() returns the raw response without running the selector() function. In non-aborted paths the selector runs. Callers of refresh() receive different types depending on the abort state, making the return type unreliable and causing subtle state shape mismatches.
Goal:
refresh() returns a consistent type regardless of abort state.
What to do:
- On abort, return
null(or a typed sentinel) instead of the raw response, so callers can handle the aborted case explicitly. - Update the TypeScript return type of
refresh()to reflect the nullable result.
Possible traps and issues:
- Existing callers that ignore the return value are unaffected; callers that use it need to handle
null.
Docs changes needed:
frontend/src/hooks/README.md: document thenullreturn on abort.
Doc references:
frontend/src/hooks/README.md
Issue #67: LOW - Default Page Size Inconsistently Applied Across Routers
Where found:
backend/app/routers/history.py:80-84– usesDEFAULT_PAGE_SIZEconstant- Multiple other routers – may hardcode page size values
Why this is needed:
Endpoints with different default page sizes create an inconsistent API experience and make it hard to reason about server load. A client that does not pass page_size gets different result counts from different endpoints.
Goal: All paginated endpoints use the same default page size driven by a single constant.
What to do:
- Audit all
page_sizeQuery parameters across routers. - Replace all hardcoded defaults with
DEFAULT_PAGE_SIZEfromconstants.py. - Add a linting check or unit test that asserts no hardcoded page size defaults exist in routers.
Possible traps and issues:
- Some endpoints may intentionally use a different page size for performance reasons; document exceptions explicitly.
Docs changes needed:
- API reference: document the default page size and how to override it.
Doc references:
backend/app/utils/constants.py–DEFAULT_PAGE_SIZE
Issue #68: LOW - No Reserved Keyword Validation for Jail Names
Where found:
backend/app/models/jail.py– jail name validated against alphanumeric regex onlybackend/app/routers/jail_config.py
Why this is needed:
Fail2ban uses reserved jail names and command keywords (e.g., all, status, purge). A user-created jail with a reserved name could shadow fail2ban built-in commands or produce confusing behavior when management commands are issued.
Goal: Reject jail names that conflict with fail2ban reserved words at model validation time.
What to do:
- Define a
FAIL2BAN_RESERVED_JAIL_NAMESset inconstants.py. - Add a Pydantic validator on the jail name field that rejects reserved words.
- Return a 422 with a descriptive error message.
Possible traps and issues:
- The reserved word list may change across fail2ban versions; source it from fail2ban documentation and version-gate if necessary.
Docs changes needed:
- API reference: document the list of reserved jail names.
Doc references:
- Fail2ban documentation on reserved jail identifiers
Issue #69: LOW - Jail Names Echoed in Error Messages Without Sanitization
Where found:
backend/app/exceptions.py:138,351– jail names interpolated directly into error strings
Why this is needed:
Although Python's repr() provides basic escaping, user-supplied jail names are reflected back in error messages. If these messages are ever rendered in an HTML context (e.g., a future admin UI or email notification), they become XSS vectors. They also act as confirmation oracles when combined with timing attacks.
Goal: Error messages referencing user input are sanitized before inclusion.
What to do:
- Pass user-supplied values through a dedicated
sanitize_for_display()helper before interpolation. - Ensure the helper strips or escapes HTML special characters.
- For API responses, always return the original (validated) field name rather than the raw user input.
Possible traps and issues:
- Over-escaping in JSON responses is not needed (JSON is not HTML); apply sanitization only at HTML render boundaries.
Docs changes needed:
CONTRIBUTING.md: document the rule that user input must not be echoed raw in messages.
Doc references:
backend/app/exceptions.py