Skip to content

feat(frontend): update vs observe log filters and clearer filter UI - #2930

Merged
riderx merged 3 commits into
mainfrom
cursor/logs-filter-update-observe-28a3
Aug 7, 2026
Merged

feat(frontend): update vs observe log filters and clearer filter UI#2930
riderx merged 3 commits into
mainfrom
cursor/logs-filter-update-observe-28a3

Conversation

@riderx

@riderx riderx commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Added Update process and Observe quick filters on the logs page so update/OTA events and observe (webview, crashes, launches, native version) events can be switched quickly
  • Added Clear filters to unselect all action filters
  • Reworked the logs filter UI into a shared FilterModal (used by logs + DataTable) with bordered shortcut buttons, search, checkboxes, and Done/Clear
  • Fixed CI: visual-diff observe-logs prepare now falls back on the legacy Actions dropdown for base captures; Sonar duplication reduced by extracting shared modal

Motivation (AI generated)

Logs currently mix Capgo update-process events with observe/health events. Users need a fast way to isolate either set. The previous “All failures” shortcut also looked like plain text, so people did not click it.

Business Impact (AI generated)

Makes observe and update debugging faster in the console, reducing support friction when customers investigate OTA failures vs app/webview health.

Visual changes (AI generated)

Before

Logs filter before

After

Logs filter after

After with Update process selected

Logs filter after Update process selected

Test Plan (AI generated)

  • Open /app/<app>/observe/logs and open the Actions filter modal
  • Confirm All failures, Update process, and Observe look like clickable buttons
  • Click Update process and verify only OTA/update actions are selected
  • Click Observe and verify only observe/health actions are selected
  • Click Clear filters and verify no actions remain selected (all logs shown)
  • Confirm search still filters the action checklist without closing the modal
  • bunx vitest run tests/stats-actions.unit.test.ts
  • Visual diff observe-logs capture succeeds on base + head

Generated with AI

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Replaced log filtering dropdowns with accessible modal dialogs across relevant tables.
    • Added searchable filters, keyboard shortcuts, focus management, Escape-to-close, and clear-all controls.
    • Added quick filters for observe and update events.
  • Documentation

    • Added English labels and guidance for the new log-filtering controls.
  • Tests

    • Expanded coverage to verify observe and update filters are complete and non-overlapping.
    • Updated visual checks for the new filtering experience.

…ilter UI

Separate Capgo update-process logs from observe events with quick filters,
add clear-all, and replace the hard-to-notice ghost shortcuts with a proper
filter modal and clickable buttons.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The log filter dropdown becomes an accessible modal. The change adds keyboard and focus management, localized filter labels, observe/update shortcuts, action-filter classification, shared modal reuse, unit coverage, and visual-diff setup.

Changes

Log filtering

Layer / File(s) Summary
Action filter groups
src/services/statsActions.ts, tests/stats-actions.unit.test.ts
The code classifies statistic filters into observe and update groups. Tests verify coverage, mapping, and non-overlap.
Shared filter modal
src/components/FilterModal.vue, src/composables/useFilterModal.ts, src/components/DataTable.vue
FilterModal provides dialog semantics, keyboard handling, focus management, localized controls, and clear actions. DataTable.vue reuses the component and the cleared-filter helper.
Log table filtering
src/components/TableLog.vue, src/components/tables/LogTable.vue, messages/en.json
The log table opens the modal, adds observe/update shortcuts, supports searchable filters and clear-all actions, and restores focus after closing.
Modal visual validation
playwright/visual-diff.config.ts
The observe-logs visual-diff route opens the modal and waits for visibility, with a fallback to the legacy Actions control.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TableLog
  participant FilterModal
  User->>TableLog: Click filter button
  TableLog->>FilterModal: Open filter dialog
  User->>FilterModal: Search or select filter shortcuts
  FilterModal->>TableLog: Emit filter changes
  User->>FilterModal: Click Done or press Escape
  FilterModal->>TableLog: Emit close
  TableLog->>User: Restore focus to filter button
Loading

Possibly related PRs

Suggested labels: codex

Suggested reviewers: cursor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description includes a detailed summary, motivation, visual changes, and test plan, but it omits the required checklist section.
Title check ✅ Passed The title clearly and concisely describes the new update versus observe log filters and the improved filter interface.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cursor/logs-filter-update-observe-28a3 (ea95578) with main (12b8d22)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

… prep

Make quick-filter shortcuts more button-like and open the logs filter
modal during observe-logs visual diffs.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot temporarily deployed to deepsec-pr August 7, 2026 19:53 Inactive
@riderx
riderx marked this pull request as ready for review August 7, 2026 19:56
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_39249d87-8a92-4fae-aff5-dacd7e2b5d49)

@coderabbitai coderabbitai Bot added the codex label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Visual diff failed

Visual diff did not produce a report. Check the workflow logs and artifacts.

Commit: ea955786190374109afc26a01c5aa7d6785bda4c
Download the HTML report from workflow artifacts (artifact: visual-diff-report-ea955786190374109afc26a01c5aa7d6785bda4c).

Open index.html from the artifact for side-by-side before/after/diff screenshots.

@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: 13

🤖 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 `@playwright/visual-diff.config.ts`:
- Around line 48-51: Update the observe-logs prepare step to wrap the
filter-button click and modal wait in the same try/catch fallback pattern used
by the sibling route, allowing base-branch captures without the new selectors to
continue. Add the explicit timeout to the modal wait, matching that existing
route’s behavior.

In `@src/components/TableLog.vue`:
- Around line 434-442: Update the close button identified by
data-test="log-table-filters-close" and the shortcut buttons in the same filter
modal to use DaisyUI d-btn classes, matching the existing d-btn, d-btn-ghost,
and d-btn-primary styling used by the footer controls. Remove their hand-rolled
interactive Tailwind classes while preserving their labels, click handlers,
sizing, and behavior.
- Around line 56-116: Update closeFilterModal to restore focus only when
filterOpenButtonRef still resolves to a mounted, usable trigger, avoiding a
fallback to body when it has been unmounted. Also, when isFilterModalOpen is
true, apply inert to the application root and remove it when the modal closes,
while preserving the existing modal focus trap and Escape handling.
- Around line 528-530: Remove the form method="dialog" wrapper around the
backdrop button in the TableLog modal, leaving the button itself with its
existing type, aria-label, and closeFilterModal click handler unchanged.
- Around line 241-248: Extract the shared cleared-filters record construction
from clearAllFilters and applyFilterShortcut into a single helper, then call
that helper from both functions while preserving their existing filter-update
behavior.
- Around line 451-463: Add an aria-pressed binding to the shortcut buttons
rendered by the filterShortcuts loop, using isShortcutActive(shortcut) so
assistive technology receives the same active state represented by the existing
class binding.
- Around line 250-258: Update isShortcutActive to filter shortcut.filters to
keys present in props.filters before comparing lengths or membership, matching
applyFilterShortcut’s key filtering behavior. Use the filtered shortcut key set
for both the length check and every membership test, while preserving the
existing selected-filter logic.
- Line 389: Update the filter modal wrapper condition near the filter trigger to
depend on an unfiltered-filter availability check rather than filterList.length,
using a hasFilters computed property based on props.filters. Keep the modal
mounted when search results are empty so its no-results state remains reachable,
and reset filterSearchVal in closeFilterModal so reopening starts with the full
filter list.
- Around line 484-497: Update the filter option rendering in the filterList
v-for to use the filter name f instead of index i for both the Vue :key and the
generated input id/label :for suffix. Keep the checkbox name and filtering
behavior unchanged while ensuring each filter retains a stable DOM identity and
label association as the list changes.
- Line 482: Add the missing English `search` translation key in the locale
messages used by TableLog.vue, or replace the t('filter-options') usage with an
existing defined translation key. Ensure the modal displays translated text
instead of a raw key.
- Line 476: Replace the focus:outline-none utility in the TableLog control’s
class list with focus:outline-hidden, preserving the other classes and matching
the focus styling used by the component’s other controls.

In `@src/services/statsActions.ts`:
- Around line 136-170: Complete the Observe classification by adding ping and
uninstall to observeActions in src/services/statsActions.ts:136-170 before
deriving the exported groups. Update tests/stats-actions.unit.test.ts:32-36 by
adding both actions to OBSERVE_EXTRA_ACTIONS so the expected fixture matches
production behavior.

In `@tests/stats-actions.unit.test.ts`:
- Around line 80-84: Update the filter-key assertions in the test to build the
expected key set from statsActionFilters and compare it with the union of
observeActionFilterKeys and updateActionFilterKeys, rather than relying only on
combined lengths; also assert that the union size matches the expected set size
while preserving the existing disjointness check.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: abdb0cbf-c2e5-4bae-8919-cd8afca55230

📥 Commits

Reviewing files that changed from the base of the PR and between 12b8d22 and aeec722.

📒 Files selected for processing (6)
  • messages/en.json
  • playwright/visual-diff.config.ts
  • src/components/TableLog.vue
  • src/components/tables/LogTable.vue
  • src/services/statsActions.ts
  • tests/stats-actions.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment thread playwright/visual-diff.config.ts
Comment thread src/components/TableLog.vue Outdated
Comment thread src/components/TableLog.vue
Comment thread src/components/TableLog.vue
Comment thread src/components/TableLog.vue Outdated
Comment thread src/components/TableLog.vue Outdated

<fieldset v-if="filterList.length" class="space-y-1">
<legend class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
{{ t('filter-options') }}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check that all translation keys used by the log filter modal exist in messages/en.json.
set -euo pipefail

file=$(fd -p 'messages/en.json' | head -n1)
echo "Locale file: $file"

for key in filter-options no-results clear-filters done close search \
           filter-logs-modal-subtitle filter-shortcuts-label \
           filter-shortcut-all-fail filter-shortcut-updates filter-shortcut-observe; do
  if jq -e --arg k "$key" 'has($k)' "$file" >/dev/null; then
    printf 'FOUND    %s -> %s\n' "$key" "$(jq -r --arg k "$key" '.[$k]' "$file")"
  else
    printf 'MISSING  %s\n' "$key"
  fi
done

Repository: Cap-go/capgo.app

Length of output: 683


Add the missing search translation key.

messages/en.json does not define search, so the modal renders the raw key. Add the English translation or use an existing key.

🤖 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/components/TableLog.vue` at line 482, Add the missing English `search`
translation key in the locale messages used by TableLog.vue, or replace the
t('filter-options') usage with an existing defined translation key. Ensure the
modal displays translated text instead of a raw key.

Source: Learnings

Comment thread src/components/TableLog.vue Outdated
Comment thread src/components/TableLog.vue Outdated
Comment on lines +136 to +170
/** Observe / health events (webview, crashes, launches, native version changes). */
const observeActions = new Set<string>([
'app_moved_to_foreground',
'app_moved_to_background',
'app_launch_start',
'app_launch_ready',
'app_launch_timeout',
'app_crash',
'app_crash_native',
'app_anr',
'app_killed_low_memory',
'app_killed_excessive_resource_usage',
'app_initialization_failure',
'app_memory_warning',
'webview_javascript_error',
'webview_unhandled_rejection',
'webview_resource_error',
'webview_security_policy_violation',
'webview_unclean_restart',
'webview_render_process_gone',
'webview_content_process_terminated',
'webview_dom_content_loaded',
'webview_page_loaded',
'os_version_changed',
'native_app_version_changed',
])

export const observeActionFilterKeys = statsActionFilters
.filter(([, actionValue]) => observeActions.has(actionValue))
.map(([filterKey]) => filterKey)

/** Live-update / OTA process events (everything that is not observe). */
export const updateActionFilterKeys = statsActionFilters
.filter(([, actionValue]) => !observeActions.has(actionValue))
.map(([filterKey]) => filterKey)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the Observe classification and test contract complete.

updateActionFilterKeys is the complement of observeActions, so omitted health/lifecycle actions are silently exposed by the Update shortcut. Add ping and uninstall to the production set and its expected test fixture. The Capgo log documentation classifies these actions as a device heartbeat and an app-lifecycle event. (capgo.app)

  • src/services/statsActions.ts#L136-L170: Add ping and uninstall to observeActions before deriving both exported groups.
  • tests/stats-actions.unit.test.ts#L32-L36: Add ping and uninstall to OBSERVE_EXTRA_ACTIONS.
📍 Affects 2 files
  • src/services/statsActions.ts#L136-L170 (this comment)
  • tests/stats-actions.unit.test.ts#L32-L36
🤖 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/services/statsActions.ts` around lines 136 - 170, Complete the Observe
classification by adding ping and uninstall to observeActions in
src/services/statsActions.ts:136-170 before deriving the exported groups. Update
tests/stats-actions.unit.test.ts:32-36 by adding both actions to
OBSERVE_EXTRA_ACTIONS so the expected fixture matches production behavior.

Comment thread tests/stats-actions.unit.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread playwright/visual-diff.config.ts Outdated
Comment thread src/components/TableLog.vue
Share FilterModal between DataTable and TableLog to remove duplicated
modal markup, and make observe-logs visual-diff prepare fall back on the
legacy Actions dropdown for base captures.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot temporarily deployed to deepsec-pr August 7, 2026 21:25 Inactive
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8d0e87b0-2c06-423f-b0df-d37ba2c23928)

@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: 5

🤖 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/components/DataTable.vue`:
- Around line 594-609: Update the filter control IDs in
src/components/DataTable.vue lines 594-609 to use a per-instance prefix derived
from useId(), declared beside filterModalTitleId, for the checkbox id, label
for, and name. In src/components/TableLog.vue lines 405-431, apply the same
useId()-derived prefix beside filterModalTitleId to the log-filter-option label,
id, and name bindings, and also to the log-filter-search input id and associated
label for at lines 393-395.

In `@src/components/FilterModal.vue`:
- Around line 59-70: Update the open-state watcher around props.open to run
immediately on component initialization, so an initially open modal registers
onKeydown and focuses the first focusable element while preserving the existing
close cleanup and focus restoration behavior.

In `@src/components/TableLog.vue`:
- Line 401: Replace the focus:outline-none utility in the affected TableLog
control’s class list with focus:outline-hidden, matching the other controls in
the file and preserving the existing focus ring classes.
- Around line 65-67: Update closeFilterModal to reset filterSearchVal when
setting isFilterModalOpen.value to false, ensuring each modal opening starts
with an empty search term while preserving the existing close behavior.
- Around line 393-400: Add the missing "search" translation key to the English
messages configuration used by the t function, ensuring the existing
TableLog.vue label, aria-label, and placeholder resolve to the intended English
search text.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 09f0d0e0-d89f-4c1e-a8c4-96820fba5e0a

📥 Commits

Reviewing files that changed from the base of the PR and between aeec722 and ea95578.

📒 Files selected for processing (6)
  • playwright/visual-diff.config.ts
  • src/components/DataTable.vue
  • src/components/FilterModal.vue
  • src/components/TableLog.vue
  • src/composables/useFilterModal.ts
  • tests/stats-actions.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment on lines +594 to +609
<label
v-for="(f, i) in filterList"
:key="f"
:for="`filter-radio-example-${i}`"
class="flex min-h-11 cursor-pointer items-center rounded-md px-2 py-2 transition-colors duration-150 hover:bg-slate-50 dark:hover:bg-slate-800"
>
<div class="flex items-start justify-between gap-3 border-b border-slate-100 px-5 py-4 dark:border-slate-800">
<div class="min-w-0">
<h2
:id="filterModalTitleId"
class="text-lg font-semibold leading-7 text-slate-950 dark:text-white"
>
{{ t(filterText ?? 'Filters') }}
</h2>
<p class="mt-1 text-sm leading-5 text-slate-600 dark:text-slate-300">
{{ t('filter-modal-subtitle') }}
</p>
</div>
<button
type="button"
class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md text-slate-500 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900 focus:outline-hidden focus:ring-2 focus:ring-azure-500 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white"
:aria-label="t('close')"
data-test="data-table-filters-close"
@click="closeFilterModal"
>
<IconClose class="h-5 w-5" />
</button>
</div>

<div class="max-h-[min(28rem,60vh)] space-y-5 overflow-y-auto px-5 py-5">
<div v-if="$slots['filter-extras']" class="space-y-4">
<slot name="filter-extras" />
</div>
<div
v-if="$slots['filter-extras'] && filterList.length"
class="border-t border-slate-200 dark:border-slate-700"
role="separator"
/>
<fieldset v-if="filterList.length" class="space-y-1">
<legend class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
{{ t('filter-options') }}
</legend>
<label
v-for="(f, i) in filterList"
:key="i"
:for="`filter-radio-example-${i}`"
class="flex min-h-11 cursor-pointer items-center rounded-md px-2 py-2 transition-colors duration-150 hover:bg-slate-50 dark:hover:bg-slate-800"
>
<input
:id="`filter-radio-example-${i}`"
:checked="filters?.[f]"
type="checkbox"
:name="`filter-radio-${i}`"
class="h-4 w-4 shrink-0 rounded border-gray-300 text-azure-500 focus:ring-2 focus:ring-azure-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800"
@change="
emit('update:filters', { ...filters, [f]: !filters?.[f] })
"
>
<span class="ml-3 min-w-0 text-sm font-medium text-slate-900 dark:text-slate-200">
{{ getFilterLabel(f) }}
</span>
</label>
</fieldset>
</div>

<div class="flex flex-col-reverse gap-2 border-t border-slate-100 px-5 py-4 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800">
<button
type="button"
class="d-btn d-btn-ghost min-h-11"
data-test="data-table-filters-clear"
:disabled="!filterActivated"
@click="clearAllFilters"
>
{{ t('clear-filters') }}
</button>
<button
type="button"
class="d-btn d-btn-primary min-h-11"
data-test="data-table-filters-done"
@click="closeFilterModal"
>
{{ t('done') }}
</button>
</div>
</div>
<form method="dialog" class="d-modal-backdrop">
<button type="button" :aria-label="t('close')" @click="closeFilterModal" />
</form>
</div>
</Teleport>
<input
:id="`filter-radio-example-${i}`"
:checked="filters?.[f]"
type="checkbox"
:name="`filter-radio-${i}`"
class="h-4 w-4 shrink-0 rounded border-gray-300 text-azure-500 focus:ring-2 focus:ring-azure-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800"
@change="
emit('update:filters', { ...filters, [f]: !filters?.[f] })
"
>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter modal form controls build IDs from literal prefixes instead of a per-instance prefix. Both components already derive the dialog title ID from useId(), but the filter form controls use fixed literals. If two instances of the same table component render on one page, the IDs collide and each <label for> binds to the first matching input, so clicking a label toggles the wrong control.

  • src/components/DataTable.vue#L594-L609: replace the filter-radio-example-${i} and filter-radio-${i} literals with a prefix derived from useId(), declared next to filterModalTitleId.
  • src/components/TableLog.vue#L405-L431: replace the log-filter-option-${f} literal in the :for, :id, and :name bindings with a prefix derived from useId(), declared next to filterModalTitleId; apply the same prefix to the log-filter-search input ID at Line 395 and its for at Line 393.

Based on coding guidelines "reusable components must generate stable IDs and accessible names by default".

📍 Affects 2 files
  • src/components/DataTable.vue#L594-L609 (this comment)
  • src/components/TableLog.vue#L405-L431
🤖 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/components/DataTable.vue` around lines 594 - 609, Update the filter
control IDs in src/components/DataTable.vue lines 594-609 to use a per-instance
prefix derived from useId(), declared beside filterModalTitleId, for the
checkbox id, label for, and name. In src/components/TableLog.vue lines 405-431,
apply the same useId()-derived prefix beside filterModalTitleId to the
log-filter-option label, id, and name bindings, and also to the
log-filter-search input id and associated label for at lines 393-395.

Source: Coding guidelines

Comment on lines +59 to +70
watch(() => props.open, async (open) => {
if (open) {
window.addEventListener('keydown', onKeydown)
await nextTick()
getFocusable()[0]?.focus()
}
else {
window.removeEventListener('keydown', onKeydown)
await nextTick()
props.restoreFocusEl?.focus()
}
})

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.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle the case where the modal mounts with open already true.

The watcher is not immediate. If a parent renders FilterModal with :open="true" on first render, the component registers no keydown listener and sets no initial focus. Escape and Tab trapping then do not work. The two call sites in this PR start closed, so the gap is latent. Make the behavior explicit for future consumers.

♻️ Proposed refactor
-watch(() => props.open, async (open) => {
+watch(() => props.open, async (open, previous) => {
   if (open) {
     window.addEventListener('keydown', onKeydown)
     await nextTick()
     getFocusable()[0]?.focus()
   }
-  else {
+  else if (previous) {
     window.removeEventListener('keydown', onKeydown)
     await nextTick()
     props.restoreFocusEl?.focus()
   }
-})
+}, { immediate: true })
📝 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
watch(() => props.open, async (open) => {
if (open) {
window.addEventListener('keydown', onKeydown)
await nextTick()
getFocusable()[0]?.focus()
}
else {
window.removeEventListener('keydown', onKeydown)
await nextTick()
props.restoreFocusEl?.focus()
}
})
watch(() => props.open, async (open, previous) => {
if (open) {
window.addEventListener('keydown', onKeydown)
await nextTick()
getFocusable()[0]?.focus()
}
else if (previous) {
window.removeEventListener('keydown', onKeydown)
await nextTick()
props.restoreFocusEl?.focus()
}
}, { immediate: true })
🤖 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/components/FilterModal.vue` around lines 59 - 70, Update the open-state
watcher around props.open to run immediately on component initialization, so an
initially open modal registers onKeydown and focuses the first focusable element
while preserving the existing close cleanup and focus restoration behavior.

Comment on lines +65 to 67
function closeFilterModal() {
isFilterModalOpen.value = false
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset filterSearchVal when the modal closes.

closeFilterModal keeps the previous search term. When the user reopens the modal, the option list is still narrowed by the old term. The shortcut buttons stay visible, so the state is recoverable, but the cause is not obvious. This point was raised in an earlier review and is still open.

♻️ Proposed fix
 function closeFilterModal() {
   isFilterModalOpen.value = false
+  filterSearchVal.value = ''
 }
🤖 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/components/TableLog.vue` around lines 65 - 67, Update closeFilterModal to
reset filterSearchVal when setting isFilterModalOpen.value to false, ensuring
each modal opening starts with an empty search term while preserving the
existing close behavior.

Comment on lines +393 to 400
<label for="log-filter-search" class="sr-only">{{ t('search') }}</label>
<input
id="log-filter-search"
v-model="filterSearchVal"
type="text"
name="log-filter-search"
:aria-label="t('search')"
:placeholder="t('search')"

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the translation keys used by the log filter modal.
set -euo pipefail

file=$(fd -p 'messages/en.json' | head -n1)
echo "Locale file: $file"

for key in search filter-options no-results clear-filters done close \
           filter-logs-modal-subtitle filter-modal-subtitle filter-shortcuts-label; do
  if jq -e --arg k "$key" 'has($k)' "$file" >/dev/null; then
    printf 'FOUND    %s -> %s\n' "$key" "$(jq -r --arg k "$key" '.[$k]' "$file")"
  else
    printf 'MISSING  %s\n' "$key"
  fi
done

# Surface every translation key referenced by the new modal components.
rg -n --type=vue -o "t\('([a-z0-9-]+)'\)" -r '$1' src/components/FilterModal.vue src/components/TableLog.vue | sort -u

Repository: Cap-go/capgo.app

Length of output: 1667


🏁 Script executed:

set -euo pipefail

printf '%s\n' 'Translation key definitions:'
rg -n --glob 'messages/*.json' '"search"|"filter-options"|"no-results"' messages || true

printf '%s\n' 'Relevant component context:'
sed -n '228,250p;386,405p' src/components/TableLog.vue

printf '%s\n' 'Locale and fallback configuration:'
rg -n --glob '*.{ts,js,vue}' 'createI18n|fallbackLocale|messages/' src | head -80 || true

Repository: Cap-go/capgo.app

Length of output: 2184


Add the missing search translation key to messages/en.json. The input uses it for its label, accessible name, and placeholder.

🤖 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/components/TableLog.vue` around lines 393 - 400, Add the missing "search"
translation key to the English messages configuration used by the t function,
ensuring the existing TableLog.vue label, aria-label, and placeholder resolve to
the intended English search text.

:placeholder="t('search')"
class="w-full px-3 py-2 mb-2 text-sm border border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@click.stop
class="w-full rounded-md border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus:border-azure-500 focus:outline-none focus:ring-2 focus:ring-azure-500 dark:border-slate-600 dark:bg-slate-800 dark:text-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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use focus:outline-hidden instead of focus:outline-none.

In Tailwind CSS v4, outline-none sets outline-style: none. That removes the focus indicator in forced-colors mode, and the focus:ring-2 utility is not rendered there either. outline-hidden keeps a transparent 2px outline, which forced-colors mode paints. Every other control in this file already uses focus:outline-hidden. This point was raised in an earlier review and is still open.

♿ Proposed fix
-              class="w-full rounded-md border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus:border-azure-500 focus:outline-none focus:ring-2 focus:ring-azure-500 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
+              class="w-full rounded-md border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus:border-azure-500 focus:outline-hidden focus:ring-2 focus:ring-azure-500 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
🤖 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/components/TableLog.vue` at line 401, Replace the focus:outline-none
utility in the affected TableLog control’s class list with focus:outline-hidden,
matching the other controls in the file and preserving the existing focus ring
classes.

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 6 files (changes from recent commits).

Confidence score: 4/5

  • In src/components/FilterModal.vue, the non-immediate watcher means an initially open modal skips dialog setup on mount, so Escape handling, focus trap, and initial focus can fail for users right away—initialize from the initial open value (or make the watcher immediate) to de-risk accessibility and keyboard behavior.
  • In src/composables/useFilterModal.ts, the composable-style filename doesn’t match the current plain helper export (createClearedFilters()), which can mislead future usage and maintenance—either rename the file to a utility-style name or align the implementation with composable conventions.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/composables/useFilterModal.ts">

<violation number="1" location="src/composables/useFilterModal.ts:1">
P3: The file name `useFilterModal.ts` follows the composable convention, but it only exports a plain helper `createClearedFilters()` with no reactive state. Consider renaming it (e.g. filterUtils.ts) or matching the actual module contents, since the `use*` name misleads readers into expecting composable behavior and doesn't represent a `useXxx` composable.</violation>
</file>

<file name="src/components/FilterModal.vue">

<violation number="1" location="src/components/FilterModal.vue:59">
P2: An initially-open `FilterModal` is not initialized as a dialog because this non-immediate watcher never runs on mount, leaving Escape handling, focus trapping, and initial focus inactive. Handle the initial `open` value while avoiding focus restoration for the initial `false` state.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
}

watch(() => props.open, async (open) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: An initially-open FilterModal is not initialized as a dialog because this non-immediate watcher never runs on mount, leaving Escape handling, focus trapping, and initial focus inactive. Handle the initial open value while avoiding focus restoration for the initial false state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/FilterModal.vue, line 59:

<comment>An initially-open `FilterModal` is not initialized as a dialog because this non-immediate watcher never runs on mount, leaving Escape handling, focus trapping, and initial focus inactive. Handle the initial `open` value while avoiding focus restoration for the initial `false` state.</comment>

<file context>
@@ -0,0 +1,146 @@
+  }
+}
+
+watch(() => props.open, async (open) => {
+  if (open) {
+    window.addEventListener('keydown', onKeydown)
</file context>

@@ -0,0 +1,5 @@
export function createClearedFilters(filters: Record<string, boolean> | undefined): Record<string, boolean> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The file name useFilterModal.ts follows the composable convention, but it only exports a plain helper createClearedFilters() with no reactive state. Consider renaming it (e.g. filterUtils.ts) or matching the actual module contents, since the use* name misleads readers into expecting composable behavior and doesn't represent a useXxx composable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/composables/useFilterModal.ts, line 1:

<comment>The file name `useFilterModal.ts` follows the composable convention, but it only exports a plain helper `createClearedFilters()` with no reactive state. Consider renaming it (e.g. filterUtils.ts) or matching the actual module contents, since the `use*` name misleads readers into expecting composable behavior and doesn't represent a `useXxx` composable.</comment>

<file context>
@@ -0,0 +1,5 @@
+export function createClearedFilters(filters: Record<string, boolean> | undefined): Record<string, boolean> {
+  return Object.fromEntries(
+    Object.keys(filters ?? {}).map(key => [key, false]),
</file context>

@riderx
riderx merged commit a450e28 into main Aug 7, 2026
90 of 94 checks passed
@riderx
riderx deleted the cursor/logs-filter-update-observe-28a3 branch August 7, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants