feat(devices): filter device table by platform and bundle version - #2792
Conversation
Allow filtering the devices list by platform and/or bundle version so large fleets can be narrowed without exporting data offline. Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDeviceTable adds platform and bundle/version filters with debounced reloads. The private devices endpoint validates and forwards platform filters to Cloudflare Analytics Engine and Supabase queries, with tests covering validation, propagation, and generated query conditions. ChangesDevice filtering
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant DeviceTable
participant PrivateDevices
participant Stats
participant CloudflareAnalytics
participant Supabase
DeviceTable->>PrivateDevices: POST platform and versionName filters
PrivateDevices->>Stats: Forward platform to countDevices
Stats->>CloudflareAnalytics: Count devices by latest platform
Stats->>Supabase: Count devices with platform filter as fallback
PrivateDevices->>CloudflareAnalytics: Read devices with latest-state conditions
PrivateDevices->>Supabase: Read devices with platform filter as fallback
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
Visual diff passedVisual changesGenerated at 2026-07-30T23:52:51.679Z. Threshold: 0.1% pixel difference.
Commit: Open |
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot completed as skipped and reported an unresolved finding, and this PR is above the low-risk approval threshold (user-facing filters plus private device list/count API changes). Assigning human reviewers.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot completed as skipped on the latest commit, and this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API changes). Human reviewers are already assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/tables/DeviceTable.vue`:
- Around line 104-109: Update getVersionNameFilter so an empty
selectedVersionName explicitly clears the bundle filter instead of falling back
to props.versionName. Also synchronize selectedVersionName with later non-empty
prop changes using the component’s existing reactive/watch mechanism, while
preserving the user’s current selection when appropriate.
- Around line 128-149: Update loadBundleNames to capture the current props.appId
before the Supabase await, then verify it still matches props.appId immediately
after the await and return if it changed. Only update bundleNames from responses
belonging to the current app, preserving the existing error and option-building
behavior.
In `@supabase/functions/_backend/utils/cloudflare.ts`:
- Around line 984-1005: Update the platform-filtered query construction in the
device-count logic, including the corresponding path around the related second
occurrence, so the inner aggregation selects the latest platform and
version_name per blob1 before the outer query applies both platform and
bundle/version predicates. Remove version filtering from the inner device_info
conditions, preserve current-state semantics matching Supabase, and update the
associated SQL test expectations.
🪄 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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: da1b13e4-3fa2-47ab-8e67-e473e01fb13d
📒 Files selected for processing (9)
messages/en.jsonsrc/components/tables/DeviceTable.vuesupabase/functions/_backend/private/devices.tssupabase/functions/_backend/utils/cloudflare.tssupabase/functions/_backend/utils/stats.tssupabase/functions/_backend/utils/supabase.tssupabase/functions/_backend/utils/types.tstests/cloudflare-device-pagination.unit.test.tstests/private-analytics-validation.unit.test.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
Use selected bundle only (no prop fallback), sync prop changes into the dropdown, and filter CF device queries on latest version_name + platform after aggregation so combinations match Supabase semantics. Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot did not reach a successful terminal state within the wait window (pending/skipped) and is not a clean pass, so this is not approved. Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/tables/DeviceTable.vue (3)
464-468: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGuard the entire app-switch watcher against stale executions.
When an app switch makes
loadBundleNames()return because its request is stale, this watcher still callsrefreshData(). It also leaves the previous app’s bundle options visible while the new request is pending.Proposed fix
-watch(() => props.appId, async () => { +watch(() => props.appId, async (appId) => { selectedPlatform.value = '' selectedVersionName.value = props.versionName ?? '' + bundleNames.value = [] await loadBundleNames() + if (appId !== props.appId) + return await refreshData() })🤖 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/tables/DeviceTable.vue` around lines 464 - 468, Update the appId watcher around loadBundleNames and refreshData so stale executions stop before refreshing data, and clear the bundle options immediately when the app changes. Ensure only the current app-switch execution proceeds to refreshData after loadBundleNames completes, using the existing stale-request signal or return value from loadBundleNames.
464-468: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick winAvoid duplicate reloads when the application changes.
Resetting
selectedPlatformorselectedVersionNametriggersdebouncedReload(), while the app watcher separately callsrefreshData(). A typical app switch therefore performs two count/list cycles. Suppress the filter watcher during app reset or make a single watcher responsible for the refresh.Also applies to: 476-478
🤖 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/tables/DeviceTable.vue` around lines 464 - 468, Update the appId watcher and related filter watcher in the DeviceTable component so resetting selectedPlatform and selectedVersionName does not trigger debouncedReload during the app reset. Ensure the appId watcher performs only one refresh flow after applying the new values, while preserving normal filter-triggered reloads outside app changes.
286-289: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInvalidate in-flight loads immediately when filters change.
The watcher changes filter state but waits 300 ms before
reload()incrementsactiveLoadId. During that window, an existing reload can count with the old filters and then fetch rows with the newplatform/versionName, producing mismatched totals and pages. Invalidate the active load in the watcher or pass one immutable filter snapshot through both requests.Also applies to: 307-308, 476-478
🤖 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/tables/DeviceTable.vue` around lines 286 - 289, Update the filter watcher and debouncedReload flow in DeviceTable so changing filters immediately invalidates any in-flight load before the 300 ms debounce, preventing old requests from using new platform/versionName values. Increment or otherwise invalidate activeLoadId at watcher time, while preserving the existing debounced reload behavior and applying the same fix to the additional watcher locations.
🤖 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.
Outside diff comments:
In `@src/components/tables/DeviceTable.vue`:
- Around line 464-468: Update the appId watcher around loadBundleNames and
refreshData so stale executions stop before refreshing data, and clear the
bundle options immediately when the app changes. Ensure only the current
app-switch execution proceeds to refreshData after loadBundleNames completes,
using the existing stale-request signal or return value from loadBundleNames.
- Around line 464-468: Update the appId watcher and related filter watcher in
the DeviceTable component so resetting selectedPlatform and selectedVersionName
does not trigger debouncedReload during the app reset. Ensure the appId watcher
performs only one refresh flow after applying the new values, while preserving
normal filter-triggered reloads outside app changes.
- Around line 286-289: Update the filter watcher and debouncedReload flow in
DeviceTable so changing filters immediately invalidates any in-flight load
before the 300 ms debounce, preventing old requests from using new
platform/versionName values. Increment or otherwise invalidate activeLoadId at
watcher time, while preserving the existing debounced reload behavior and
applying the same fix to the additional watcher locations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e590877f-0f25-42ff-baec-7bbcf913d25b
📒 Files selected for processing (1)
src/components/tables/DeviceTable.vue
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed with no unresolved findings, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/tables/DeviceTable.vue (1)
126-152: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftDo not truncate bundle filter options at 200 versions.
Apps with more than 200 active versions cannot select older bundles, so filtering is incomplete. Paginate this query or replace the static list with server-side search.
🤖 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/tables/DeviceTable.vue` around lines 126 - 152, Update loadBundleNames so it retrieves all active app_versions instead of limiting results to 200, using pagination or server-side search while preserving the existing stale-response, error, deduplication, and selected-filter behavior.
🤖 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.
Outside diff comments:
In `@src/components/tables/DeviceTable.vue`:
- Around line 126-152: Update loadBundleNames so it retrieves all active
app_versions instead of limiting results to 200, using pagination or server-side
search while preserving the existing stale-response, error, deduplication, and
selected-filter behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: af759e92-6e0c-4a9d-8074-12d33c7335a3
📒 Files selected for processing (3)
src/components/tables/DeviceTable.vuesupabase/functions/_backend/utils/cloudflare.tstests/cloudflare-device-pagination.unit.test.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
Cap-go/capacitor-updater(manual)
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed and all findings are resolved, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
Keep keyboard users on the Filters control after dismissing the modal instead of dropping focus to the document. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed with no unresolved findings, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed with no unresolved findings, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
…wait Use a per-instance heading id for accessibility, and only fall back to the legacy Override wait when the modal itself is missing. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Stale comment
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed with no unresolved findings, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
|
There was a problem hiding this comment.
Risk: medium. Left a non-blocking comment — Cursor Bugbot passed with no unresolved findings, but this PR is above the low-risk approval threshold (user-facing device filters plus private list/count API/query changes). Human reviewers are already requested (2); no new reviewers assigned.
Sent by Cursor Approval Agent: Pull Request Approver External
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Confidence score: 4/5
- In
src/components/DataTable.vue, checkbox IDs for filters are not instance-unique/stable, so in pages with multiple tables or reordered filters a label can toggle the wrong input, causing confusing filter behavior and accessibility regressions—derive IDs from a component-scopeduseId()prefix plus a stable filter key/index.
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/components/DataTable.vue">
<violation number="1" location="src/components/DataTable.vue:665">
P3: Filter checkbox IDs are not instance-unique or stable across filter ordering, so labels can target the wrong checkbox when multiple tables/modals exist. Generate them from a component `useId()` prefix plus the filter key.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| 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}`" |
There was a problem hiding this comment.
P3: Filter checkbox IDs are not instance-unique or stable across filter ordering, so labels can target the wrong checkbox when multiple tables/modals exist. Generate them from a component useId() prefix plus the filter key.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/DataTable.vue, line 663:
<comment>Filter checkbox IDs are not instance-unique or stable across filter ordering, so labels can target the wrong checkbox when multiple tables/modals exist. Generate them from a component `useId()` prefix plus the filter key.</comment>
<file context>
@@ -509,30 +602,105 @@ const paginationClass = computed(() => props.mobileFixedPagination
+ 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"
</file context>
There was a problem hiding this comment.
1 issue found across 13 files
Confidence score: 5/5
- In
src/components/DataTable.vue, the shared filter subtitle now over-describes controls for non-device tables (e.g., mentioning platform/bundle where those filters don’t exist), which can mislead users and cause incorrect filtering expectations—make the subtitle generic (or context-specific per table) to keep the UI copy accurate.
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/components/DataTable.vue">
<violation number="1" location="src/components/DataTable.vue:631">
P3: Filter dialogs outside the device table now describe controls they do not have; for example API-key scope and bundle storage/deletion filters claim to filter platform and bundle. Make this shared subtitle generic or provide it from the device-specific slot.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| {{ t(filterText ?? 'Filters') }} | ||
| </h2> | ||
| <p class="mt-1 text-sm leading-5 text-slate-600 dark:text-slate-300"> | ||
| {{ t('filter-modal-subtitle') }} |
There was a problem hiding this comment.
P3: Filter dialogs outside the device table now describe controls they do not have; for example API-key scope and bundle storage/deletion filters claim to filter platform and bundle. Make this shared subtitle generic or provide it from the device-specific slot.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/DataTable.vue, line 631:
<comment>Filter dialogs outside the device table now describe controls they do not have; for example API-key scope and bundle storage/deletion filters claim to filter platform and bundle. Make this shared subtitle generic or provide it from the device-specific slot.</comment>
<file context>
@@ -509,30 +604,105 @@ const paginationClass = computed(() => props.mobileFixedPagination
+ {{ 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>
</file context>





Summary (AI generated)
platformthrough/private/devices(count + list) for Supabase and Cloudflare Analytics Engine backends.Motivation (AI generated)
Customers with tens of thousands of devices reported that search + existing filters are not enough to find devices on a specific bundle/platform combination. The previous Filters dropdown was too tight for selects/inputs; a modal matches Capgo console patterns and gives readable, accessible controls.
Business Impact (AI generated)
Makes the console usable for large fleets, reducing support friction and helping customers inspect rollout status by platform/bundle without leaving Capgo.
Visual changes (AI generated)
Filters button opens a modal with platform chips, bundle input, options, Clear, and Done:
Full page context:
Test Plan (AI generated)
/private/devicesacceptsplatform+versionName; rejects invalid platform/app/<app>/devices, set platform + bundle, confirm table + badge count updateGenerated with AI