Skip to content

Support inbox-only filters, prevent 429s from Google getting in the w… - #113

Open
kevin-jones wants to merge 1 commit into
Gururagavendra:mainfrom
kevin-jones:feature/inbox-only-clean
Open

Support inbox-only filters, prevent 429s from Google getting in the w…#113
kevin-jones wants to merge 1 commit into
Gururagavendra:mainfrom
kevin-jones:feature/inbox-only-clean

Conversation

@kevin-jones

@kevin-jones kevin-jones commented Jun 17, 2026

Copy link
Copy Markdown

Description

When using gmail-cleaner's "Delete Emails" feature I realised that the tool was scanning ALL of my emails, not only those in my Gmail "inbox". This felt wrong to me as I was using the tool to Delete or Archive email and get them out of my inbox. But after Archive/Delete actions were completed, those emails could still appear in the scan, so it felt like I was seeing the same archived emails again and again.

I have added an inbox-only filter to make my experience better, but in doing so I found bugs with 429 errors from the Google API not being handled and the API being used more than necessary. When rebuilding the container, sometimes I had to hard-refresh to get new JS changes loaded - this again felt wrong and therefore I added asset hashing to auto-update broser caches.

Changes include:

  • This PR fixes delete-scan accuracy and scope handling for Gmail cleanup.
  • Makes Delete Emails scan inbox-only by default using in:inbox and labelIds=["INBOX"]
  • Adds an Inbox / All mail-scope selector to the filter bar
  • Threads mail_scope through delete scan, single delete, and bulk delete actions
  • Retries failed Gmail metadata batch fetches to avoid silently dropping messages when Gmail returns 429 Too many concurrent requests
  • Reduces delete scan metadata batch size to lower rate-limit pressure
  • Adds frontend asset content hashing so Docker rebuilds force browsers to load updated JS/CSS instead of stale v=1.0.0 assets
  • Adds regression coverage for inbox/all scope behavior and retrying failed batch metadata fetches

Checklist

  • uv run pytest tests/unit
  • Verified live delete scan results against Gmail API for pleo.io:Inbox scope returns expected inbox senders/counts
    All scope is accepted and does not force INBOX filtering

Changes

  • Mail Scope Selection: Added inbox/all mail scope selector to the filter bar UI with mail_scope field across FiltersModel, DeleteEmailsRequest, and DeleteBulkRequest schemas; defaults to inbox to limit scans to user's inbox only
  • Mail Scope Propagation: Extended delete_emails_by_sender(), delete_emails_bulk(), and delete_emails_bulk_background() functions to accept and use mail_scope parameter throughout the delete scan and deletion flows
  • Inbox Query Integration: Implemented mail scope helpers in delete service to map scope selection to Gmail search queries (in:inbox filter) and label filters (labelIds=["INBOX"]) for consistent filtering across operations
  • Batch Fetch Retry Logic: Added callback-based batch metadata processing with per-message retry handling for failed requests, addressing 429 rate-limit errors that previously caused messages to be silently dropped; reduced batch size to lower rate-limit pressure
  • Asset-Based Cache Busting: Implemented get_asset_hash() helper to compute content hashes from frontend assets in static/ and templates/, enabling automatic browser cache updates when container rebuilds occur without manual hard-refreshes
  • Frontend Integration: Updated GmailCleaner.Delete and GmailCleaner.Filters JavaScript to capture and pass mail_scope in delete request payloads; added UI controls and state management for mail scope selection
  • Test Coverage: Added 7 unit tests validating inbox/all scope behavior, filter combination, batch fetch retry logic, and bulk deletion operations

…ay, ensure browser cache doesn't prevent new features working after container rebuild
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a mail_scope parameter ("inbox" or "all") across the full stack: Pydantic schemas, Gmail service helpers and delete functions, API endpoints, and frontend filter/delete JS with a new radio-group UI and CSS. Also introduces a get_asset_hash() helper in app/main.py that computes a SHA-256-based cache-busting fallback from CSS/HTML/JS asset content when a git hash is unavailable.

Changes

Mail Scope Filtering

Layer / File(s) Summary
mail_scope field and validators in request/filter schemas
app/models/schemas.py
FiltersModel, DeleteEmailsRequest, and DeleteBulkRequest each gain a mail_scope field (default "inbox") with a lowercase-normalizing validator enforcing "inbox" or "all".
Gmail service: scope helpers, scan listing, batch retry
app/services/gmail/delete.py
Private helpers added for scope derivation, query construction, and label params. scan_senders_for_delete uses scoped list calls with reworked batch processing (smaller batch size, process_message_response callback, per-message retry for failures). delete_emails_by_sender, delete_emails_bulk, and delete_emails_bulk_background all accept and propagate mail_scope.
API endpoints pass mail_scope to service calls
app/api/actions.py
api_delete_emails and api_delete_emails_bulk forward request.mail_scope to the respective service functions.
Frontend radio group UI, CSS, and JS wiring
templates/index.html, static/css/filters.css, static/js/filters.js, static/js/delete.js
HTML adds a mailScope radiogroup (Inbox/All). CSS styles the radio controls. filters.js reads and resets the scope selection. delete.js stores it in GmailCleaner.deleteMailScope and includes it in single and bulk delete POST payloads.
Unit tests: API endpoints and Gmail delete service
tests/unit/api/test_api_actions.py, tests/unit/services/gmail/test_delete_service.py
API tests updated to assert "inbox" is passed to mocked service functions. New service test module adds fake Gmail classes and seven tests covering inbox-only defaults, filter merging, all-mail override, and batch retry behavior.

Asset Content-Based Cache Busting

Layer / File(s) Summary
get_asset_hash() helper and get_cache_bust_value() fallback
app/main.py
Adds os import and get_asset_hash() which walks static/ and templates/ for .css/.html/.js files, returning an 8-char SHA-256 hash. get_cache_bust_value() uses it as a fallback before app_version and timestamp.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant FiltersJS as filters.js
    participant DeleteJS as delete.js
    participant APIActions as /api/delete-emails
    participant GmailService as delete_emails_by_sender

    Browser->>FiltersJS: user selects "Inbox" or "All" radio
    Browser->>DeleteJS: startScan() called
    DeleteJS->>FiltersJS: Filters.get()
    FiltersJS-->>DeleteJS: { mail_scope: "inbox"|"all", ... }
    DeleteJS->>DeleteJS: store GmailCleaner.deleteMailScope

    Browser->>DeleteJS: delete sender clicked
    DeleteJS->>APIActions: POST { sender, mail_scope }
    APIActions->>GmailService: delete_emails_by_sender(sender, mail_scope)
    GmailService->>GmailService: _get_mail_scope(), _build_scoped_query(), _get_scope_params()
    GmailService-->>APIActions: { success, count }
    APIActions-->>Browser: JSON response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

enhancement

Poem

📬 Inbox or all, you now get to choose,
A scope radio clicked, no more emails to lose.
The helpers sweep cleanly through Gmail's domain,
With retries and batches to handle the strain.
SHA hashes stand guard when git's out of town —
No stale cache shall ever bring your app down! 🛡️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title describes supporting inbox-only filters and preventing 429s, which are core features implemented across the changeset. However, it's truncated and incomplete, ending with 'w…' rather than conveying the full scope or being a complete sentence. Complete the title to form a clear, full sentence. For example: 'Support inbox-only filters and prevent 429 rate-limiting errors from Google'
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
static/js/delete.js (1)

300-310: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-2xx bulk-delete start responses before polling.

If /api/delete-emails-bulk returns an error, polling still starts. That can leave the overlay in a misleading state even though no background task started.

Suggested fix
-            await fetch('/api/delete-emails-bulk', {
+            const response = await fetch('/api/delete-emails-bulk', {
                 method: 'POST',
                 headers: { 'Content-Type': 'application/json' },
                 body: JSON.stringify({
                     senders: senderEmails,
                     mail_scope: GmailCleaner.deleteMailScope || 'inbox'
                 })
             });
+
+            if (!response.ok) {
+                const errorData = await response.json().catch(() => ({}));
+                throw new Error(errorData.detail || `Request failed with status ${response.status}`);
+            }
 
             // Poll for progress
             this.pollDeleteProgress(checkboxes);
🤖 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 `@static/js/delete.js` around lines 300 - 310, The fetch call to
/api/delete-emails-bulk does not validate the response status before proceeding
to call this.pollDeleteProgress. If the endpoint returns a non-2xx error status,
polling will still start even though the bulk delete failed, leaving the UI in a
misleading state. Add a check after the fetch call to verify the response.ok
property (or check response.status), and only call this.pollDeleteProgress if
the response indicates success. Handle the error case appropriately by either
throwing an error, returning early, or displaying an error message to prevent
the polling from starting.
🧹 Nitpick comments (3)
tests/unit/services/gmail/test_delete_service.py (1)

5-7: ⚡ Quick win

Add an autouse state-reset fixture for test isolation.

These tests mutate shared app.core.state; an explicit reset fixture prevents order-dependent bleed as the suite grows.

Suggested fixture
+import pytest
 from app.core import state
 from app.services.gmail import delete as delete_service
+
+
+@pytest.fixture(autouse=True)
+def reset_delete_state():
+    state.reset_delete_scan()
+    state.reset_delete_bulk()
+    state.delete_scan_results = []
+    yield
+    state.reset_delete_scan()
+    state.reset_delete_bulk()
+    state.delete_scan_results = []
🤖 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 `@tests/unit/services/gmail/test_delete_service.py` around lines 5 - 7, The
tests in this module mutate the shared app.core.state object without resetting
it between test runs, which can cause test order dependencies and failures as
the test suite grows. Add an autouse pytest fixture to this test file that
resets app.core.state to a clean state before and after each test runs. This
fixture should be defined at the module level and use the `@pytest.fixture`
decorator with autouse=True parameter to ensure it executes automatically for
every test in the file without needing explicit invocation.
tests/unit/api/test_api_actions.py (1)

155-163: ⚡ Quick win

Add API tests for explicit mail_scope="all" forwarding.

Current assertions cover the default scope; adding explicit "all" cases will lock down the non-default contract at the endpoint boundary.

Suggested tests
 class TestDeleteEmailsEndpoint:
@@
     `@patch`("app.api.actions.delete_emails_by_sender")
     def test_delete_emails_by_sender(self, mock_delete, client):
@@
         mock_delete.assert_called_once_with("newsletter@example.com", "inbox")
+
+    `@patch`("app.api.actions.delete_emails_by_sender")
+    def test_delete_emails_by_sender_all_scope(self, mock_delete, client):
+        mock_delete.return_value = {"success": True, "deleted": 10}
+        response = client.post(
+            "/api/delete-emails",
+            json={"sender": "newsletter@example.com", "mail_scope": "all"},
+        )
+        assert response.status_code == 200
+        mock_delete.assert_called_once_with("newsletter@example.com", "all")
@@
 class TestDeleteBulkEndpoint:
@@
     `@patch`("app.api.actions.delete_emails_bulk_background")
     def test_delete_bulk_with_valid_senders(self, mock_delete, client):
@@
         mock_delete.assert_called_once_with(senders, "inbox")
+
+    `@patch`("app.api.actions.delete_emails_bulk_background")
+    def test_delete_bulk_with_all_scope(self, mock_delete, client):
+        senders = ["sender1@example.com", "sender2@example.com"]
+        response = client.post(
+            "/api/delete-emails-bulk",
+            json={"senders": senders, "mail_scope": "all"},
+        )
+        assert response.status_code == 200
+        assert response.json() == {"status": "started"}
+        mock_delete.assert_called_once_with(senders, "all")

Also applies to: 169-177

🤖 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 `@tests/unit/api/test_api_actions.py` around lines 155 - 163, Add new test
cases to cover the explicit mail_scope="all" parameter for the delete-emails
endpoint. The current test_delete_emails_by_sender test only validates the
default scope behavior (inbox). Create additional test methods that send a POST
request to /api/delete-emails with the JSON payload including both the sender
and an explicit mail_scope parameter set to "all", then assert that the mocked
delete_emails_by_sender function is called with the sender and "all" as
arguments instead of the default "inbox" scope. This ensures the non-default
mail_scope contract is properly validated at the endpoint boundary.
app/models/schemas.py (1)

101-109: ⚡ Quick win

Centralize mail_scope normalization in one helper.

The same validation logic is copied in three models. Extracting one normalizer avoids drift and keeps behavior consistent when this contract changes.

Proposed refactor
+def _normalize_mail_scope(v: Optional[str]) -> str:
+    if v is None or v == "":
+        return "inbox"
+    value = v.strip().lower()
+    if value not in ("inbox", "all"):
+        raise ValueError('mail_scope must be either "inbox" or "all"')
+    return value
+
 class FiltersModel(BaseModel):
@@
     def validate_mail_scope(cls, v) -> str:
-        if v is None or v == "":
-            return "inbox"
-        value = v.strip().lower()
-        if value not in ("inbox", "all"):
-            raise ValueError('mail_scope must be either "inbox" or "all"')
-        return value
+        return _normalize_mail_scope(v)
@@
 class DeleteEmailsRequest(BaseModel):
@@
     def validate_mail_scope(cls, v) -> str:
-        if v is None or v == "":
-            return "inbox"
-        value = v.strip().lower()
-        if value not in ("inbox", "all"):
-            raise ValueError('mail_scope must be either "inbox" or "all"')
-        return value
+        return _normalize_mail_scope(v)
@@
 class DeleteBulkRequest(BaseModel):
@@
     def validate_mail_scope(cls, v) -> str:
-        if v is None or v == "":
-            return "inbox"
-        value = v.strip().lower()
-        if value not in ("inbox", "all"):
-            raise ValueError('mail_scope must be either "inbox" or "all"')
-        return value
+        return _normalize_mail_scope(v)

Also applies to: 160-168, 177-185

🤖 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 `@app/models/schemas.py` around lines 101 - 109, Extract the mail_scope
validation logic from the validate_mail_scope method that appears in three
separate models into a single centralized helper function. Create a reusable
function (outside of the class definitions) that handles the normalization and
validation logic for mail_scope values (checking for None or empty string,
stripping and lowercasing, and validating against the allowed values "inbox" and
"all"). Then replace each of the three copies of the validate_mail_scope
validator method with a call to this centralized helper function to ensure
consistent behavior and avoid duplication.
🤖 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 `@app/services/gmail/delete.py`:
- Around line 149-174: The retry_message_get function currently increments the
processed counter even when all retry attempts are exhausted and the message
fetch fails. This makes failed messages appear as successful in the aggregation
count, masking incomplete results. Modify the retry_message_get function to only
increment processed when process_message_response successfully completes, and
ensure that when all three retry attempts fail, the function returns early
without incrementing the counter or signals the failure state so callers can
detect that the message was not successfully processed.

In `@static/css/filters.css`:
- Around line 57-79: The filter-radio component is missing a visible keyboard
focus state. Add a CSS rule for the `.filter-radio input:focus + span` selector
that applies a visible focus style (such as an outline, box-shadow, or border)
to the span element. This will make the focused state clearly visible when users
navigate through the radios using the keyboard, improving accessibility and
usability.

---

Outside diff comments:
In `@static/js/delete.js`:
- Around line 300-310: The fetch call to /api/delete-emails-bulk does not
validate the response status before proceeding to call this.pollDeleteProgress.
If the endpoint returns a non-2xx error status, polling will still start even
though the bulk delete failed, leaving the UI in a misleading state. Add a check
after the fetch call to verify the response.ok property (or check
response.status), and only call this.pollDeleteProgress if the response
indicates success. Handle the error case appropriately by either throwing an
error, returning early, or displaying an error message to prevent the polling
from starting.

---

Nitpick comments:
In `@app/models/schemas.py`:
- Around line 101-109: Extract the mail_scope validation logic from the
validate_mail_scope method that appears in three separate models into a single
centralized helper function. Create a reusable function (outside of the class
definitions) that handles the normalization and validation logic for mail_scope
values (checking for None or empty string, stripping and lowercasing, and
validating against the allowed values "inbox" and "all"). Then replace each of
the three copies of the validate_mail_scope validator method with a call to this
centralized helper function to ensure consistent behavior and avoid duplication.

In `@tests/unit/api/test_api_actions.py`:
- Around line 155-163: Add new test cases to cover the explicit mail_scope="all"
parameter for the delete-emails endpoint. The current
test_delete_emails_by_sender test only validates the default scope behavior
(inbox). Create additional test methods that send a POST request to
/api/delete-emails with the JSON payload including both the sender and an
explicit mail_scope parameter set to "all", then assert that the mocked
delete_emails_by_sender function is called with the sender and "all" as
arguments instead of the default "inbox" scope. This ensures the non-default
mail_scope contract is properly validated at the endpoint boundary.

In `@tests/unit/services/gmail/test_delete_service.py`:
- Around line 5-7: The tests in this module mutate the shared app.core.state
object without resetting it between test runs, which can cause test order
dependencies and failures as the test suite grows. Add an autouse pytest fixture
to this test file that resets app.core.state to a clean state before and after
each test runs. This fixture should be defined at the module level and use the
`@pytest.fixture` decorator with autouse=True parameter to ensure it executes
automatically for every test in the file without needing explicit invocation.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b81e8e22-98fa-4564-b07a-9ae3da244fd3

📥 Commits

Reviewing files that changed from the base of the PR and between a8dda59 and c6ad8e9.

📒 Files selected for processing (10)
  • app/api/actions.py
  • app/main.py
  • app/models/schemas.py
  • app/services/gmail/delete.py
  • static/css/filters.css
  • static/js/delete.js
  • static/js/filters.js
  • templates/index.html
  • tests/unit/api/test_api_actions.py
  • tests/unit/services/gmail/test_delete_service.py

Comment on lines +149 to +174
def retry_message_get(msg_id: str) -> None:
nonlocal processed

for attempt in range(3):
try:
if attempt > 0:
time.sleep(0.5 * attempt)
response = (
service.users()
.messages()
.get(
userId="me",
id=msg_id,
format="metadata",
metadataHeaders=["From", "Subject", "Date"],
)
.execute()
)
process_message_response(response)
processed += 1
return
except Exception as e:
if attempt == 2:
logger.warning("Failed to fetch message %s: %s", msg_id, e)
processed += 1

Copy link
Copy Markdown
Contributor

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

Surface exhausted metadata retries; current flow still drops messages silently.

When retries fail, the message is skipped from aggregation and only logged, but status still looks fully successful. That can produce incomplete sender counts without user-visible signal.

Proposed fix
         processed = 0
         batch_size = 25
+        failed_metadata_ids: list[str] = []
@@
         def retry_message_get(msg_id: str) -> None:
             nonlocal processed
@@
                 except Exception as e:
                     if attempt == 2:
                         logger.warning("Failed to fetch message %s: %s", msg_id, e)
+                        failed_metadata_ids.append(msg_id)
             processed += 1
@@
             state.delete_scan_status["progress"] = progress
             state.delete_scan_status["message"] = f"Scanned {processed}/{total} emails"
@@
+        if failed_metadata_ids:
+            state.delete_scan_status["error"] = (
+                f"Skipped metadata for {len(failed_metadata_ids)} message(s) after retries"
+            )
+
         # Sort by count

Also applies to: 208-214

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 170-170: Do not catch blind exception: Exception

(BLE001)

🤖 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 `@app/services/gmail/delete.py` around lines 149 - 174, The retry_message_get
function currently increments the processed counter even when all retry attempts
are exhausted and the message fetch fails. This makes failed messages appear as
successful in the aggregation count, masking incomplete results. Modify the
retry_message_get function to only increment processed when
process_message_response successfully completes, and ensure that when all three
retry attempts fail, the function returns early without incrementing the counter
or signals the failure state so callers can detect that the message was not
successfully processed.

Comment thread static/css/filters.css
Comment on lines +57 to +79
.filter-radio input {
position: absolute;
opacity: 0;
pointer-events: none;
}

.filter-radio span {
display: block;
min-width: 54px;
padding: 6px 12px;
text-align: center;
font-size: 13px;
line-height: 1.2;
}

.filter-radio + .filter-radio {
border-left: 1px solid var(--border-color);
}

.filter-radio input:checked + span {
background: var(--primary-color);
color: white;
}

Copy link
Copy Markdown
Contributor

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

Add visible keyboard focus state for mail-scope radios.

Lines 57-79 style checked state, but there’s no visible focus style when tabbing through the radios. This makes keyboard interaction hard to use reliably.

Suggested fix
 .filter-radio input:checked + span {
     background: var(--primary-color);
     color: white;
 }
+
+.filter-radio input:focus-visible + span {
+    outline: 2px solid var(--primary-color);
+    outline-offset: -2px;
+}
📝 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
.filter-radio input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.filter-radio span {
display: block;
min-width: 54px;
padding: 6px 12px;
text-align: center;
font-size: 13px;
line-height: 1.2;
}
.filter-radio + .filter-radio {
border-left: 1px solid var(--border-color);
}
.filter-radio input:checked + span {
background: var(--primary-color);
color: white;
}
.filter-radio input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.filter-radio span {
display: block;
min-width: 54px;
padding: 6px 12px;
text-align: center;
font-size: 13px;
line-height: 1.2;
}
.filter-radio + .filter-radio {
border-left: 1px solid var(--border-color);
}
.filter-radio input:checked + span {
background: var(--primary-color);
color: white;
}
.filter-radio input:focus-visible + span {
outline: 2px solid var(--primary-color);
outline-offset: -2px;
}
🤖 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 `@static/css/filters.css` around lines 57 - 79, The filter-radio component is
missing a visible keyboard focus state. Add a CSS rule for the `.filter-radio
input:focus + span` selector that applies a visible focus style (such as an
outline, box-shadow, or border) to the span element. This will make the focused
state clearly visible when users navigate through the radios using the keyboard,
improving accessibility and usability.

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