Skip to content

✨(maintenance) add maintenance mode#504

Open
providenz wants to merge 1 commit into
mainfrom
providenz/maintenance-mode
Open

✨(maintenance) add maintenance mode#504
providenz wants to merge 1 commit into
mainfrom
providenz/maintenance-mode

Conversation

@providenz
Copy link
Copy Markdown
Collaborator

@providenz providenz commented May 27, 2026

Maintenance mode with admin toggle, env-var escape hatch and DSFR-style maintenance page

Purpose

Give admins a way to take the app offline cleanly for planned downtime (deploys, migrations) and emergencies. End-users see a DSFR-styled "Service indisponible" page; staff retain access to /admin/ to flip the switch.
Also toggleable through MAINTENANCE_MODE env variable

Proposal

  • Add a MaintenanceMode singleton (django-solo) with enabled, message, starts_at, ends_at
  • Add MAINTENANCE_MODE env var as escape hatch, ORed with the DB singleton
  • Add MaintenanceMiddleware returning 503 with maintenance_mode code for non-exempt paths
  • Expose the maintenance state through the config endpoint so the SPA can render the page even when anonymous
  • Add a MaintenancePage React component mirroring current design
  • Gate the SPA in ConfigProvider and refetch config on any 503 +
    maintenance_mode
  • Backend and frontend tests

Summary by CodeRabbit

  • New Features

    • Global maintenance mode with a user-facing "Service indisponible" page (French) and CTA to "La Suite".
    • Operators can enable maintenance, set messages and optional start/end windows; clients poll status every 30s.
  • API

    • Public config now includes maintenance state so clients can react appropriately.
  • Admin

    • Admin UI surfaces maintenance controls and emits warnings when the env escape is active.
  • Tests & Docs

    • End-to-end tests added; changelog updated.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Walkthrough

Adds application-wide maintenance mode: DB singleton + migration, MAINTENANCE_MODE setting and middleware, admin UI, API exposure, frontend types/error handling, MaintenancePage UI with polling, and tests.

Changes

Maintenance Mode Feature

Layer / File(s) Summary
Database model and migration
src/backend/core/migrations/0008_maintenancemode.py, src/backend/core/models.py
Migration and MaintenanceMode singleton with enabled, message, optional starts_at/ends_at, updated_at, updated_by, is_active_now(), and save() logging.
Settings and middleware infrastructure
src/backend/conversations/settings.py, src/backend/core/middleware.py
Adds MAINTENANCE_MODE env flag, inserts core.middleware.MaintenanceMiddleware into MIDDLEWARE, provides is_maintenance_active() and middleware that returns 503 JSON for non-exempt requests and sets Retry-After when ends_at is present.
Admin interface
src/backend/core/admin.py
Registers MaintenanceMode in Django admin, configures editable/readonly fields, sets updated_by from request.user on save, and warns when MAINTENANCE_MODE setting is enabled.
API config exposure
src/backend/core/api/viewsets.py
ConfigView.get() adds maintenance to the public config payload via _get_maintenance(), returning null when inactive or {enabled,message,ends_at} when active.
Backend tests
src/backend/core/tests/*, src/backend/chat/tests/views/test_file_stream.py
Tests for is_active_now(), is_maintenance_active() env override, middleware 503 and exemptions, Retry-After header; adds pytestmark = django_db to a chat test module.
Frontend types
src/frontend/.../useConfig.tsx
Adds MaintenanceConfig interface and extends ConfigResponse with optional `maintenance?: MaintenanceConfig
Frontend error handling
src/frontend/.../AppProvider.tsx
Centralizes maintenance detection (isMaintenanceError) and invalidates KEY_CONFIG on 503 via onAnyError; wires into QueryCache for both queries and mutations while keeping existing 401 handling.
Frontend maintenance UI
src/frontend/.../config/ConfigProvider.tsx, src/frontend/.../components/*, src/frontend/.../index.ts
ConfigProvider early-returns MaintenancePage when maintenance.enabled is true. MaintenancePage renders French maintenance UI, optional operator message, Marianne image, and polls config every 30s; MaintenanceIllustration provides SVG; tests validate rendering and link.
Changelog
CHANGELOG.md
Adds [Unreleased] Added entry: “✨(maintenance) add maintenance mode”.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Middleware
  participant DB as MaintenanceModeDB
  participant ConfigAPI as /api/v1.0/config/
  participant FrontendApp

  Client->>Middleware: Request /api/endpoint
  Middleware->>DB: is_maintenance_active()? (env OR get_solo().is_active_now())
  DB-->>Middleware: active
  Middleware-->>Client: 503 JSON (maintenance_mode) + Retry-After?

  Client->>Middleware: Request /api/v1.0/config/
  Middleware->>DB: is_maintenance_active()? (exempt path allows)
  DB-->>ConfigAPI: MaintenanceMode.get_solo()
  ConfigAPI-->>Client: 200 JSON with maintenance:{enabled,message,ends_at}

  Client->>FrontendApp: receives 503 JSON
  FrontendApp->>FrontendApp: isMaintenanceError detects maintenance_mode
  FrontendApp->>ConfigAPI: invalidate/refetch KEY_CONFIG
  ConfigAPI-->>FrontendApp: maintenance:{enabled:true}
  FrontendApp->>FrontendApp: render MaintenancePage, start 30s polling
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • qbey
  • elvoisin
  • maxenceh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature addition: adding a maintenance mode capability to the application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch providenz/maintenance-mode

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/tests/test_api_config.py (1)

195-221: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sync and async config expectations are inconsistent (maintenance missing in async test).

ConfigView.get() now always includes maintenance, but test_api_config_async still asserts the old payload shape.

Suggested change
         "project_files_max_count": 10,
         "project_images_max_count": 3,
         "status_banner": None,
+        "maintenance": None,
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/tests/test_api_config.py` around lines 195 - 221, The async
test expectation is missing the new "maintenance" key returned by
ConfigView.get(); update the expected payload in test_api_config_async (the dict
asserted against response.json()) to include "maintenance" with the correct
value (matching how ConfigView.get() populates it in the sync test), ensuring
the expected shape matches the actual response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 9-11: The file contains Markdown headings "## [Unreleased]" and
"### Added" with leading spaces; remove the leading whitespace characters before
these heading markers so the headings start at column 0 (e.g., change "  ##
[Unreleased]" to "## [Unreleased]" and "  ### Added" to "### Added") to comply
with MD023 and match the rest of the file's heading formatting.

In `@src/backend/core/middleware.py`:
- Around line 48-56: Currently the middleware always queries
MaintenanceMode.get_solo() even when settings.MAINTENANCE_MODE is True; that can
raise DB errors and turn a 503 into a 500. Change the logic in the middleware to
short-circuit when settings.MAINTENANCE_MODE is truthy (return the JsonResponse
with 503 immediately) and only call MaintenanceMode.get_solo() when the env flag
is not set; if you still want to include Retry-After from MaintenanceMode, wrap
the call to MaintenanceMode.get_solo() in a try/except that swallows DB errors
and omits Retry-After on failure so the handler still returns 503 instead of
raising.

---

Outside diff comments:
In `@src/backend/core/tests/test_api_config.py`:
- Around line 195-221: The async test expectation is missing the new
"maintenance" key returned by ConfigView.get(); update the expected payload in
test_api_config_async (the dict asserted against response.json()) to include
"maintenance" with the correct value (matching how ConfigView.get() populates it
in the sync test), ensuring the expected shape matches the actual response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5b439cc5-a6a2-44e3-88f8-0e4f010fa022

📥 Commits

Reviewing files that changed from the base of the PR and between f03e101 and 320c4c7.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/backend/conversations/settings.py
  • src/backend/core/admin.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/middleware.py
  • src/backend/core/migrations/0008_maintenancemode.py
  • src/backend/core/models.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_maintenance.py
  • src/frontend/apps/conversations/src/core/AppProvider.tsx
  • src/frontend/apps/conversations/src/core/config/ConfigProvider.tsx
  • src/frontend/apps/conversations/src/core/config/api/useConfig.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenanceIllustration.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/__tests__/MaintenancePage.test.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/index.ts
  • src/frontend/apps/conversations/src/features/maintenance/index.ts

Comment thread CHANGELOG.md Outdated
Comment on lines +48 to +56
singleton = MaintenanceMode.get_solo()
response = JsonResponse(
{"code": "maintenance_mode", "detail": "Service under maintenance"},
status=503,
)
if singleton.ends_at:
retry_after = int((singleton.ends_at - timezone.now()).total_seconds())
if retry_after > 0:
response["Retry-After"] = str(retry_after)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid unconditional DB access in the 503 path when env override is enabled.

Even when settings.MAINTENANCE_MODE is True, this block still queries MaintenanceMode. If DB is unavailable, the escape hatch can fail with 500 instead of returning maintenance 503.

Suggested change
-        singleton = MaintenanceMode.get_solo()
+        singleton = None if settings.MAINTENANCE_MODE else MaintenanceMode.get_solo()
         response = JsonResponse(
             {"code": "maintenance_mode", "detail": "Service under maintenance"},
             status=503,
         )
-        if singleton.ends_at:
+        if singleton and singleton.ends_at:
             retry_after = int((singleton.ends_at - timezone.now()).total_seconds())
             if retry_after > 0:
                 response["Retry-After"] = str(retry_after)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/middleware.py` around lines 48 - 56, Currently the
middleware always queries MaintenanceMode.get_solo() even when
settings.MAINTENANCE_MODE is True; that can raise DB errors and turn a 503 into
a 500. Change the logic in the middleware to short-circuit when
settings.MAINTENANCE_MODE is truthy (return the JsonResponse with 503
immediately) and only call MaintenanceMode.get_solo() when the env flag is not
set; if you still want to include Retry-After from MaintenanceMode, wrap the
call to MaintenanceMode.get_solo() in a try/except that swallows DB errors and
omits Retry-After on failure so the handler still returns 503 instead of
raising.

@providenz providenz force-pushed the providenz/maintenance-mode branch from 320c4c7 to fc4378e Compare May 27, 2026 16:11
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/frontend/apps/conversations/src/features/maintenance/components/__tests__/MaintenancePage.test.tsx`:
- Around line 39-43: The test for "does not render an extra paragraph when
message is blank" is relying on queryByText('   '), which can pass due to
whitespace normalization; instead render the page with renderPage({ enabled:
true, message: '   ' }) and assert that only the expected <p> elements are
present (e.g., use the render result container and check
container.querySelectorAll('p').length === 2) so the test fails if an extra
paragraph node is rendered; update the assertion in the test to use
container.querySelectorAll('p') (or another DOM-counting approach) referencing
the existing renderPage helper and this test case name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4acb1608-974f-4248-8a38-6416f1fc95d9

📥 Commits

Reviewing files that changed from the base of the PR and between 320c4c7 and fc4378e.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • src/backend/chat/tests/views/test_file_stream.py
  • src/backend/conversations/settings.py
  • src/backend/core/admin.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/middleware.py
  • src/backend/core/migrations/0008_maintenancemode.py
  • src/backend/core/models.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_maintenance.py
  • src/frontend/apps/conversations/src/core/AppProvider.tsx
  • src/frontend/apps/conversations/src/core/config/ConfigProvider.tsx
  • src/frontend/apps/conversations/src/core/config/api/useConfig.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenanceIllustration.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/__tests__/MaintenancePage.test.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/index.ts
  • src/frontend/apps/conversations/src/features/maintenance/index.ts
✅ Files skipped from review due to trivial changes (3)
  • src/backend/chat/tests/views/test_file_stream.py
  • src/frontend/apps/conversations/src/features/maintenance/index.ts
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/frontend/apps/conversations/src/features/maintenance/components/index.ts
  • src/frontend/apps/conversations/src/core/config/api/useConfig.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenanceIllustration.tsx
  • src/backend/core/migrations/0008_maintenancemode.py
  • src/backend/core/admin.py
  • src/backend/core/api/viewsets.py
  • src/backend/conversations/settings.py
  • src/backend/core/models.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_maintenance.py
  • src/backend/core/middleware.py

Comment on lines +39 to +43
it('does not render an extra paragraph when message is blank', () => {
renderPage({ enabled: true, message: ' ' });
// Only the two static paragraphs should be present (the body sentence and Actions block)
expect(screen.queryByText(' ')).not.toBeInTheDocument();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Blank-message test does not actually validate the “no extra paragraph” path.

queryByText(' ') is too weak here; whitespace normalization makes this pass even if an empty extra paragraph is rendered.

Proposed test fix
   it('does not render an extra paragraph when message is blank', () => {
-    renderPage({ enabled: true, message: '   ' });
-    // Only the two static paragraphs should be present (the body sentence and Actions block)
-    expect(screen.queryByText('   ')).not.toBeInTheDocument();
+    const { container } = renderPage({ enabled: true, message: '   ' });
+    const paragraphs = Array.from(container.querySelectorAll('p'));
+    expect(paragraphs).toHaveLength(2);
+    expect(paragraphs.some((p) => p.textContent?.trim() === '')).toBe(false);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/conversations/src/features/maintenance/components/__tests__/MaintenancePage.test.tsx`
around lines 39 - 43, The test for "does not render an extra paragraph when
message is blank" is relying on queryByText('   '), which can pass due to
whitespace normalization; instead render the page with renderPage({ enabled:
true, message: '   ' }) and assert that only the expected <p> elements are
present (e.g., use the render result container and check
container.querySelectorAll('p').length === 2) so the test fails if an extra
paragraph node is rendered; update the assertion in the test to use
container.querySelectorAll('p') (or another DOM-counting approach) referencing
the existing renderPage helper and this test case name.

Maintenance mode with admin toggle, env-var escape
hatch and DSFR-style maintenance page
@providenz providenz force-pushed the providenz/maintenance-mode branch from fc4378e to 862e0fd Compare May 27, 2026 19:45
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/apps/conversations/src/core/AppProvider.tsx`:
- Line 40: Remove the unnecessary void operator on the Promise returned by
queryClient.invalidateQueries in AppProvider.tsx (the statement using
queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] })); instead handle the
Promise explicitly—either await the call from an async function or attach a
.catch to handle/log errors—so the Promise is not ignored and Sonar's
quality-gate is satisfied.

In
`@src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx`:
- Line 171: Remove the promise-suppression "void" before
queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] }) inside the polling
interval callback and instead handle the returned promise explicitly: either
make the interval callback async and await queryClient.invalidateQueries(...) or
remove "void" and append a .catch(err => /* log or noop */) to handle rejections
(e.g., console.error or an existing logger). This targets the
queryClient.invalidateQueries call and the KEY_CONFIG queryKey used in the
MaintenancePage polling logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d05626d8-3996-4c3f-86da-8adf63cc3ec1

📥 Commits

Reviewing files that changed from the base of the PR and between fc4378e and 862e0fd.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • src/backend/chat/tests/views/test_file_stream.py
  • src/backend/conversations/settings.py
  • src/backend/core/admin.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/middleware.py
  • src/backend/core/migrations/0008_maintenancemode.py
  • src/backend/core/models.py
  • src/backend/core/tests/test_api_config.py
  • src/backend/core/tests/test_maintenance.py
  • src/frontend/apps/conversations/src/core/AppProvider.tsx
  • src/frontend/apps/conversations/src/core/config/ConfigProvider.tsx
  • src/frontend/apps/conversations/src/core/config/api/useConfig.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenanceIllustration.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/__tests__/MaintenancePage.test.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/index.ts
  • src/frontend/apps/conversations/src/features/maintenance/index.ts
✅ Files skipped from review due to trivial changes (4)
  • src/frontend/apps/conversations/src/features/maintenance/index.ts
  • CHANGELOG.md
  • src/backend/chat/tests/views/test_file_stream.py
  • src/frontend/apps/conversations/src/features/maintenance/components/index.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/backend/core/migrations/0008_maintenancemode.py
  • src/backend/core/tests/test_api_config.py
  • src/frontend/apps/conversations/src/features/maintenance/components/tests/MaintenancePage.test.tsx
  • src/frontend/apps/conversations/src/core/config/ConfigProvider.tsx
  • src/frontend/apps/conversations/src/features/maintenance/components/MaintenanceIllustration.tsx
  • src/backend/conversations/settings.py
  • src/backend/core/api/viewsets.py
  • src/backend/core/middleware.py
  • src/backend/core/models.py
  • src/frontend/apps/conversations/src/core/config/api/useConfig.tsx
  • src/backend/core/admin.py
  • src/backend/core/tests/test_maintenance.py

// reports maintenance mode so the SPA flips to the maintenance page.
const onAnyError = (error: unknown) => {
if (isMaintenanceError(error)) {
void queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and print the relevant section with line numbers
FILE="src/frontend/apps/conversations/src/core/AppProvider.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== $FILE (lines 1-120) =="
nl -ba "$FILE" | sed -n '1,120p'

echo
echo "== Search for invalidateQueries usage in repo =="
rg -n "invalidateQueries\(" src/frontend/apps/conversations/src/core/AppProvider.tsx src/frontend/apps/conversations/src -S || true

echo
echo "== Check TanStack Query versions (package.json) =="
# try common locations
for p in \
  src/frontend/apps/conversations/package.json \
  src/frontend/package.json \
  package.json
do
  if [ -f "$p" ]; then
    echo "== $p =="
    cat "$p" | rg -n "(`@tanstack/query`|tanstack/query|react-query|`@tanstack/react-query`)" || true
  fi
done

Repository: suitenumerique/conversations

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/conversations/src/core/AppProvider.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== $FILE (lines 1-200) =="
awk 'NR>=1 && NR<=200 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "== Search for invalidateQueries usage in conversations app =="
rg -n "invalidateQueries\(" src/frontend/apps/conversations/src -S || true

echo
echo "== Search for KEY_CONFIG usage =="
rg -n "KEY_CONFIG" src/frontend/apps/conversations/src -S || true

echo
echo "== Check TanStack Query versions (package.json) =="
for p in \
  src/frontend/apps/conversations/package.json \
  src/frontend/package.json \
  package.json
do
  if [ -f "$p" ]; then
    echo "== $p =="
    cat "$p" | rg -n "(`@tanstack/query`|tanstack/query|react-query|`@tanstack/react-query`)" || true
  fi
done

Repository: suitenumerique/conversations

Length of output: 6490


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for no-floating-promises rule =="
rg -n "no-floating-promises" -S . || true

echo
echo "== Search for eslint rule overrides around void/no-floating-promises =="
rg -n "eslint.*void|void operator|no-floating" -S . || true

echo
echo "== Search for `@typescript-eslint` in eslint configs =="
for f in \
  .eslintrc* \
  eslint.config.* \
  .*/eslint.config.* \
  .*/.eslintrc* \
  ./*/eslint.config.* 2>/dev/null; do
  if [ -f "$f" ]; then
    echo "== $f =="
    rg -n "`@typescript-eslint`" "$f" || true
  fi
done

echo
echo "== List likely lint config files =="
ls -la | head
find . -maxdepth 3 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) | sed 's|^\./||' | head -n 50 || true

Repository: suitenumerique/conversations

Length of output: 324


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find eslint config files =="
find . -maxdepth 4 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) | sed 's|^\./||' | sort | head -n 50

echo
echo "== Search for floating-promise related rules =="
rg -n "floating-promises|no-floating-promises|no-floating|promise.*catch|catch-or-return|typescript-eslint/no-floating" -S . || true

echo
echo "== Search for Sonar rule mention 'void operator' =="
rg -n "void operator|void\\s+queryClient\\.invalidateQueries|Sonar" -S . || true

Repository: suitenumerique/conversations

Length of output: 2293


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find eslint config files =="
find . -maxdepth 4 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) | sed 's|^\./||' | sort | head -n 50

echo
echo "== Search for floating-promise related rules =="
rg -n "floating-promises|no-floating-promises|no-floating|promise.*catch|catch-or-return|typescript-eslint/no-floating" -S . || true

echo
echo "== Search for Sonar rule mention 'void operator' =="
rg -n "void operator|void\\s+queryClient\\.invalidateQueries|Sonar" -S . || true

Repository: suitenumerique/conversations

Length of output: 2293


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find eslint config files (deeper) =="
find . -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) | sed 's|^\./||' | sort | head -n 100

echo
echo "== Find eslint-related config/package entries =="
for p in package.json src/frontend/package.json src/frontend/apps/conversations/package.json; do
  if [ -f "$p" ]; then
    echo "--- $p ---"
    rg -n "\"eslint\"|\"`@typescript-eslint`\"|no-floating-promises|floating-promises" "$p" || true
  fi
done

echo
echo "== Locate any TS/ESLint rule about floating promises =="
rg -n "no-floating-promises|floating-promises|promise.*reject|unhandled.*promise" -S . || true

Repository: suitenumerique/conversations

Length of output: 1373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect conversations eslint config for floating promise rules
CONF="src/frontend/apps/conversations/eslint.config.mjs"
if [ -f "$CONF" ]; then
  echo "== $CONF (relevant lines) =="
  # Print whole file if small; else print likely rule section
  size=$(wc -l < "$CONF")
  if [ "$size" -le 260 ]; then
    sed -n '1,260p' "$CONF"
  else
    # Print sections around `@typescript-eslint/no-floating-promises` and `@typescript-eslint`
    rg -n "`@typescript-eslint/no-floating-promises`|floating-promises|no-floating-promises|promise" "$CONF" || true
    # also print full config header area
    sed -n '1,220p' "$CONF"
  fi
else
  echo "Missing $CONF"
fi

echo
echo "== Search all eslint configs for no-floating-promises/floating-promises =="
rg -n "no-floating-promises|floating-promises|unhandled.*promise|promise.*reject|no-floating" src/frontend -S || true

echo
echo "== Look for existing pattern: invalidateQueries(...).catch in repo =="
rg -n "invalidateQueries\\([^\\)]*\\)\\s*\\.catch" src/frontend/apps/conversations/src -S || true

Repository: suitenumerique/conversations

Length of output: 1249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Shared eslint config used by conversations app
CONF="src/frontend/packages/eslint-config-conversations/next.mjs"
if [ -f "$CONF" ]; then
  echo "== $CONF =="
  sed -n '1,260p' "$CONF"
else
  echo "Missing $CONF"
fi

echo
echo "== Search for floating promise rules in shared eslint config package =="
rg -n "no-floating-promises|floating-promises|promise.*reject|unhandled.*promise|`@typescript-eslint/no-floating-promises`" \
  src/frontend/packages/eslint-config-conversations -S || true

echo
echo "== Search for .catch(() => undefined) patterns without void/await =="
rg -n "\\.catch\\(\\(\\)\\s*=>\\s*undefined\\)" src/frontend/apps/conversations/src -S || true

echo
echo "== Search for .catch( in general (small sample) =="
rg -n "\\.catch\\(" src/frontend/apps/conversations/src -S | head -n 50 || true

Repository: suitenumerique/conversations

Length of output: 2874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "await queryClient\.invalidateQueries" src/frontend/apps/conversations/src -S
echo
echo "== Show ModalProjectForm.tsx invalidateQueries await block =="
FILE="src/frontend/apps/conversations/src/features/left-panel/components/projects/ModalProjectForm.tsx"
# print around line with await (find line numbers dynamically)
LINE=$(rg -n "await queryClient\.invalidateQueries" "$FILE" -S --no-filename | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
  start=$((LINE-25))
  end=$((LINE+25))
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
else
  echo "No await invalidateQueries found in $FILE"
fi

echo
echo "== Show MaintenancePage.tsx invalidateQueries with void =="
FILE="src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx"
LINE=$(rg -n "void queryClient\.invalidateQueries\\(\\{ queryKey: \\[KEY_CONFIG\\] \\}\\)" "$FILE" -S --no-filename | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
  start=$((LINE-20)); end=$((LINE+20))
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
fi

echo
echo "== Show AppProvider.tsx onAnyError block =="
FILE="src/frontend/apps/conversations/src/core/AppProvider.tsx"
LINE=$(rg -n "onAnyError" "$FILE" -S --no-filename | head -n1 | cut -d: -f1)
start=$((LINE-10)); end=$((LINE+30))
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "== Search for invalidateQueries(...).catch(...) usage =="
rg -n "invalidateQueries\\([\\s\\S]*\\)\\s*\\.catch\\(" src/frontend/apps/conversations/src -S || true

Repository: suitenumerique/conversations

Length of output: 5591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

CONF_DIR="src/frontend/packages/eslint-config-conversations"
echo "== ls $CONF_DIR =="
ls -ლა "$CONF_DIR"

echo
echo "== Print common.mjs (first 260 lines) =="
COMMON="$CONF_DIR/common.mjs"
if [ -f "$COMMON" ]; then
  sed -n '1,260p' "$COMMON"
else
  echo "Missing $COMMON"
fi

echo
echo "== Search eslint-config-conversations for floating/promise rules =="
rg -n "no-floating-promises|floating-promises|unhandled.*promise|no-floating|PromiseRejectedResult|promise.*reject" "$CONF_DIR" -S || true

echo
echo "== Search for typescriptConfigs implementation =="
# typescriptConfigs comes from ./common.mjs; also look for any helper files
rg -n "function typescriptConfigs|typescriptConfigs\\(" "$CONF_DIR" -S || true

Repository: suitenumerique/conversations

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

CONF_DIR="src/frontend/packages/eslint-config-conversations"
echo "== ls $CONF_DIR =="
ls -la "$CONF_DIR"

echo
echo "== Print common.mjs (first 260 lines) =="
COMMON="$CONF_DIR/common.mjs"
if [ -f "$COMMON" ]; then
  sed -n '1,260p' "$COMMON"
else
  echo "Missing $COMMON"
fi

echo
echo "== Search eslint-config-conversations for floating/promise rules =="
rg -n "no-floating-promises|floating-promises|unhandled.*promise|no-floating|promise.*catch|promise.*reject|`@typescript-eslint/no-floating-promises`" "$CONF_DIR" -S || true

echo
echo "== Search for typescriptConfigs implementation =="
rg -n "function typescriptConfigs|typescriptConfigs\\(" "$CONF_DIR" -S || true

Repository: suitenumerique/conversations

Length of output: 3506


Fix Sonar quality-gate failure in AppProvider.tsx by removing void on invalidateQueries
src/frontend/apps/conversations/src/core/AppProvider.tsx line 40 uses void on the Promise returned by queryClient.invalidateQueries, which Sonar flags.

Suggested patch
-    void queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] });
+    queryClient
+      .invalidateQueries({ queryKey: [KEY_CONFIG] })
+      .catch(() => undefined);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] });
queryClient
.invalidateQueries({ queryKey: [KEY_CONFIG] })
.catch(() => undefined);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 40-40: Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=suitenumerique_conversations&issues=AZ5ppWWrCNAooa3Q_0lv&open=AZ5ppWWrCNAooa3Q_0lv&pullRequest=504

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/conversations/src/core/AppProvider.tsx` at line 40, Remove
the unnecessary void operator on the Promise returned by
queryClient.invalidateQueries in AppProvider.tsx (the statement using
queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] })); instead handle the
Promise explicitly—either await the call from an async function or attach a
.catch to handle/log errors—so the Promise is not ignored and Sonar's
quality-gate is satisfied.


useEffect(() => {
const id = window.setInterval(() => {
void queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx"

# Show surrounding lines for the reported location
echo "== Context around $FILE:171 =="
nl -ba "$FILE" | sed -n '140,210p'

# Search for void usage patterns in the file
echo
echo "== void operator occurrences in $FILE =="
rg -n "^\s*void\b|void\s+queryClient\.invalidateQueries" "$FILE" || true

# Search repository-wide for the same pattern (to see if Sonar is consistent elsewhere)
echo
echo "== Repo-wide occurrences of void ... invalidateQueries =="
rg -n "void\s+queryClient\.invalidateQueries" src || true

Repository: suitenumerique/conversations

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx"

echo "== Context around $FILE:171 =="
cat -n "$FILE" | sed -n '140,210p'

echo
echo "== void operator occurrences in $FILE =="
rg -n "^\s*void\b|void\s+queryClient\.invalidateQueries" "$FILE" || true

echo
echo "== Repo-wide occurrences of void ... invalidateQueries =="
rg -n "void\s+queryClient\.invalidateQueries" src || true

Repository: suitenumerique/conversations

Length of output: 5133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx"

echo "== Lines defining POLL_INTERVAL_MS =="
rg -n "POLL_INTERVAL_MS" "$FILE"
echo

echo "== Lines defining KEY_CONFIG =="
rg -n "KEY_CONFIG" "$FILE"
echo

echo "== Context around POLL_INTERVAL_MS usage/definition =="
# print a bit above/below the first occurrence
line=$(rg -n "POLL_INTERVAL_MS" "$FILE" | head -n1 | cut -d: -f1)
start=$((line-10))
end=$((line+10))
sed -n "${start},${end}p" "$FILE" | nl -ba -w2 -s: | head -n 60 || sed -n "${start},${end}p" "$FILE" | head -n 60

Repository: suitenumerique/conversations

Length of output: 2054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "invalidateQueries\\(\\{[^}]*\\}\\).*\\.catch\\(" src/frontend/apps/conversations/src || true
rg -n "invalidateQueries\\(" src/frontend/apps/conversations/src | head -n 50

Repository: suitenumerique/conversations

Length of output: 2275


🌐 Web query:

TanStack Query QueryClient invalidateQueries return type Promise<void>

💡 Result:

In TanStack Query, queryClient.invalidateQueries(...) returns a Promise. You can treat it as awaitable: it resolves once the invalidation work (and any refetching triggered by the invalidation) has completed. The core implementation types it as Promise and the return value is the result of refetchQueries when refetching is enabled (default refetchType is 'active'). [1] Practical notes: - invalidateQueries marks matching queries as stale, and by default it refetches active queries in the background. [2] - If you set refetchType: 'none', it will only mark as invalid and resolve without refetching. [2][1] - If you want to wait for refetch completion in code, use await: await queryClient.invalidateQueries({ queryKey: ['todos'] }); [2][1] Cited type signature / implementation: - invalidateQueries(...): Promise [1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show surrounding context for a few repo-wide void usages
files=(
  "src/frontend/apps/conversations/src/core/AppProvider.tsx:40"
  "src/frontend/apps/conversations/src/features/chat/api/useRenameConversation.tsx:50"
  "src/frontend/apps/conversations/src/features/left-panel/components/projects/ModalProjectForm.tsx:544"
  "src/frontend/apps/conversations/src/features/attachments/api/useDeleteProjectAttachment.tsx:35"
)

for item in "${files[@]}"; do
  f="${item%%:*}"
  line="${item##*:}"
  echo
  echo "== $f:$line =="
  # Print 20 lines around the target line
  start=$((line-12))
  end=$((line+12))
  sed -n "${start},${end}p" "$f" | cat -n
done

Repository: suitenumerique/conversations

Length of output: 3922


Resolve Sonar rule: remove void from polling invalidateQueries (line 171)

Sonar flags the void queryClient.invalidateQueries(...) promise-suppression in the interval callback.

Suggested patch
-      void queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] });
+      queryClient
+        .invalidateQueries({ queryKey: [KEY_CONFIG] })
+        .catch(() => undefined);
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 171-171: Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=suitenumerique_conversations&issues=AZ5ppWRKCNAooa3Q_0lt&open=AZ5ppWRKCNAooa3Q_0lt&pullRequest=504

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/apps/conversations/src/features/maintenance/components/MaintenancePage.tsx`
at line 171, Remove the promise-suppression "void" before
queryClient.invalidateQueries({ queryKey: [KEY_CONFIG] }) inside the polling
interval callback and instead handle the returned promise explicitly: either
make the interval callback async and await queryClient.invalidateQueries(...) or
remove "void" and append a .catch(err => /* log or noop */) to handle rejections
(e.g., console.error or an existing logger). This targets the
queryClient.invalidateQueries call and the KEY_CONFIG queryKey used in the
MaintenancePage polling logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant