Conversation
Add ability to control which domains contribute to combined stats when trackers are merged. This allows users to avoid double-counting when the same tracker has multiple URLs (e.g., TorrentLeech) while still being able to sum stats for different trackers grouped together (e.g., Public Trackers). - Add includedInStats field to TrackerCustomization model - Add database migration for included_in_stats column - Add checkbox per domain in customize dialog (primary always included) - Add ScrollArea for long domain lists in dialog - Add X button to remove domains from within dialog - Add toast notifications for mutation failures - Backend sanitizes includedInStats to only valid secondary domains Empty includedInStats means only primary domain stats shown (backwards compatible with existing customizations).
WalkthroughAdds an Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~45 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/src/pages/Dashboard.tsx (1)
1983-2023: Well-designed dialog UI with ScrollArea for domain lists.The per-domain checkbox pattern clearly communicates that:
- Primary domain (index 0) is always included (checkbox disabled)
- Secondary domains can be toggled for stats inclusion
- The "Primary" badge provides visual clarity
The X button to remove domains enhances usability for large merge groups.
For improved screen reader accessibility, consider associating each checkbox with an explicit label using
htmlForandidattributes:-<Checkbox - checked={isIncluded} - disabled={isPrimary} - onCheckedChange={(checked) => handleToggleStatsInclusion(domain, !!checked)} - className="h-4 w-4" -/> -<span className={isPrimary ? "font-medium flex-1" : "flex-1"}>{domain}</span> +<Checkbox + id={`include-stats-${domain}`} + checked={isIncluded} + disabled={isPrimary} + onCheckedChange={(checked) => handleToggleStatsInclusion(domain, !!checked)} + className="h-4 w-4" +/> +<Label htmlFor={`include-stats-${domain}`} className={isPrimary ? "font-medium flex-1 cursor-pointer" : "flex-1 cursor-pointer"}> + {domain} +</Label>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
internal/api/handlers/tracker_customizations.go(1 hunks)internal/database/migrations/034_add_tracker_customization_included_stats.sql(1 hunks)internal/models/tracker_customization.go(6 hunks)web/src/hooks/useTrackerCustomizations.ts(4 hunks)web/src/pages/Dashboard.tsx(18 hunks)web/src/types/index.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-07T21:15:46.265Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: internal/api/handlers/tracker_customizations.go:138-154
Timestamp: 2025-12-07T21:15:46.265Z
Learning: In the qui codebase (internal/api/handlers/tracker_customizations.go and internal/models/tracker_customization.go), domain normalization follows a layered pattern: the handler's normalizeDomains performs input sanitization at the API boundary (trim, lowercase, deduplicate), while the store's joinDomains is only for serialization. All domains flow through the handler first, ensuring they're normalized before reaching the store layer.
Applied to files:
internal/api/handlers/tracker_customizations.gointernal/models/tracker_customization.goweb/src/pages/Dashboard.tsx
📚 Learning: 2025-11-25T11:39:54.748Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: web/src/pages/Dashboard.tsx:805-831
Timestamp: 2025-11-25T11:39:54.748Z
Learning: In web/src/pages/Dashboard.tsx, the TrackerIconImage component intentionally receives displayDomain (incognito-mapped name) instead of the real domain in incognito mode. This causes icon lookups to fail and show only fallback letters, which is desired behavior for privacy - hiding both tracker names and icons when incognito mode is enabled.
Applied to files:
web/src/pages/Dashboard.tsx
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/pages/Dashboard.tsx
🧬 Code graph analysis (2)
internal/models/tracker_customization.go (1)
web/src/types/index.ts (1)
TrackerCustomization(320-327)
web/src/pages/Dashboard.tsx (5)
web/src/components/ui/dialog.tsx (3)
DialogContent(138-138)DialogDescription(139-139)DialogHeader(141-141)web/src/components/ui/label.tsx (1)
Label(29-29)web/src/components/ui/scroll-area.tsx (1)
ScrollArea(61-61)web/src/components/ui/checkbox.tsx (1)
Checkbox(43-43)web/src/components/ui/badge.tsx (1)
Badge(51-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (16)
internal/database/migrations/034_add_tracker_customization_included_stats.sql (1)
1-7: LGTM!Clean migration that adds the new column with appropriate defaults for backwards compatibility. The TEXT type with empty string default aligns with the existing
domainscolumn pattern.web/src/types/index.ts (1)
320-333: LGTM!Type definitions correctly mirror the backend model with optional
includedInStatsarrays. The optional nature preserves backwards compatibility with existing customizations.web/src/hooks/useTrackerCustomizations.ts (1)
36-40: LGTM!Toast notifications provide good user feedback on mutation failures. The generic error messages are appropriate to avoid leaking implementation details.
internal/models/tracker_customization.go (3)
16-23: LGTM!The struct updates follow the established pattern for the
domainsfield. TheomitemptyJSON tag ensures backwards-compatible serialization where empty arrays are omitted.
33-64: LGTM!List and Get methods correctly extend the SELECT queries and scan logic to handle the new
included_in_statscolumn, reusing the existingsplitDomainshelper for consistent deserialization.
86-136: LGTM!Create and Update methods properly persist the
IncludedInStatsfield using the existingjoinDomainshelper, which also handles deduplication.internal/api/handlers/tracker_customizations.go (2)
34-44: LGTM!The
toModelmethod correctly normalizes domains first, then sanitizesIncludedInStatsto ensure only valid secondary domains are persisted.
46-70: Well-implemented sanitization logic.The
sanitizeIncludedInStatsfunction correctly:
- Returns nil for empty inputs (avoiding unnecessary allocations)
- Excludes the primary domain (index 0) from valid inclusion candidates
- Filters to only domains that exist in the customization
This ensures data integrity at the API boundary, consistent with the layered normalization pattern. Based on learnings, this follows the codebase convention where handlers perform input sanitization.
web/src/pages/Dashboard.tsx (8)
910-911: LGTM!State additions properly track
includedInStatsfor both editing existing customizations and creating new ones. Using aSetfor new customizations aligns with the existingselectedDomainspattern.
945-997: Well-designed two-pass aggregation.The algorithm correctly handles the case where domains may appear in any order in the aggregated map:
- Pass 1 creates entries for primary domains (always included) and standalone domains
- Pass 2 merges stats from secondary domains only when explicitly in
includedInStatsThe
if (existing)guard in Pass 2 (line 987) safely handles edge cases where the primary domain has no stats.
1062-1135: LGTM!The save handler correctly propagates
includedInStatsthrough create, update, and merge flows. The merge scenario (lines 1100-1108) appropriately combines existing and new inclusions, relying on backend sanitization for deduplication.
1203-1244: LGTM!Domain management handlers correctly maintain state consistency:
handleRemoveDomainFromDialogremoves domains from both the domain list andincludedInStatshandleToggleStatsInclusionuses case-insensitive filtering before adding to prevent duplicatesThe guard
newDomains.length > 0prevents accidentally removing all domains.
1253-1266: LGTM!Export logic correctly includes
includedInStatsonly when non-empty, keeping the JSON clean and backwards-compatible with older imports that don't have this field.
1325-1347: LGTM!Import parsing correctly handles the optional
includedInStatsfield with a default empty array, ensuring backwards compatibility with exports from before this feature was added.
1158-1168: LGTM!The
openEditDialogfunction correctly retrieves the full customization from the query cache to populateincludedInStats, with appropriate fallback to an empty array.
1196-1201: LGTM!Dialog close handler properly resets
includedInStatsstate to prevent stale data when reopening the dialog.
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.9.1` -> `v1.10.0` | --- ### Release Notes <details> <summary>autobrr/qui (ghcr.io/autobrr/qui)</summary> ### [`v1.10.0`](https://github.com/autobrr/qui/releases/tag/v1.10.0) [Compare Source](autobrr/qui@v1.9.1...v1.10.0) #### Changelog ##### New Features - [`f2b17e6`](autobrr/qui@f2b17e6): feat(config): add SESSION\_SECRET\_FILE env var ([#​661](autobrr/qui#661)) ([@​undefined-landmark](https://github.com/undefined-landmark)) - [`f5ede56`](autobrr/qui@f5ede56): feat(crossseed): add RSS source filters for categories and tags ([#​757](autobrr/qui#757)) ([@​s0up4200](https://github.com/s0up4200)) - [`9dee7bb`](autobrr/qui@9dee7bb): feat(crossseed): add Unicode normalization for title and file matching ([#​742](autobrr/qui#742)) ([@​s0up4200](https://github.com/s0up4200)) - [`d44058f`](autobrr/qui@d44058f): feat(crossseed): add skip auto-resume settings per mode ([#​755](autobrr/qui#755)) ([@​s0up4200](https://github.com/s0up4200)) - [`9e3534a`](autobrr/qui@9e3534a): feat(crossseed): add webhook source filters for categories and tags ([#​763](autobrr/qui#763)) ([@​s0up4200](https://github.com/s0up4200)) - [`c8bbe07`](autobrr/qui@c8bbe07): feat(crossseed): only poll status endpoints when features are enabled ([#​738](autobrr/qui#738)) ([@​s0up4200](https://github.com/s0up4200)) - [`fda8101`](autobrr/qui@fda8101): feat(sidebar): add size tooltips and deduplicate cross-seed sizes ([#​724](autobrr/qui#724)) ([@​s0up4200](https://github.com/s0up4200)) - [`e4c0556`](autobrr/qui@e4c0556): feat(torrent): add sequential download toggles ([#​776](autobrr/qui#776)) ([@​rare-magma](https://github.com/rare-magma)) - [`2a43f15`](autobrr/qui@2a43f15): feat(torrents): autocomplete paths ([#​634](autobrr/qui#634)) ([@​rare-magma](https://github.com/rare-magma)) - [`1c07b33`](autobrr/qui@1c07b33): feat(torrents): replace filtered speeds with global ([#​745](autobrr/qui#745)) ([@​jabloink](https://github.com/jabloink)) - [`cd0deee`](autobrr/qui@cd0deee): feat(tracker): add per-domain stats inclusion toggle for merged trackers ([#​781](autobrr/qui#781)) ([@​s0up4200](https://github.com/s0up4200)) - [`b6a6200`](autobrr/qui@b6a6200): feat(web): add Size column to Tracker Breakdown table ([#​770](autobrr/qui#770)) ([@​s0up4200](https://github.com/s0up4200)) - [`560071b`](autobrr/qui@560071b): feat(web): add zebra striping to torrent table ([#​726](autobrr/qui#726)) ([@​s0up4200](https://github.com/s0up4200)) - [`f8f65a8`](autobrr/qui@f8f65a8): feat(web): improve auto-search on completion UX ([#​743](autobrr/qui#743)) ([@​s0up4200](https://github.com/s0up4200)) - [`e36312f`](autobrr/qui@e36312f): feat(web): improve torrent selection UX with unified click and escape behavior ([#​782](autobrr/qui#782)) ([@​s0up4200](https://github.com/s0up4200)) - [`27c1daa`](autobrr/qui@27c1daa): feat(web): napster theme ([#​728](autobrr/qui#728)) ([@​s0up4200](https://github.com/s0up4200)) - [`e3950de`](autobrr/qui@e3950de): feat(web): new torrent details panel for desktop ([#​760](autobrr/qui#760)) ([@​s0up4200](https://github.com/s0up4200)) - [`6c66ba5`](autobrr/qui@6c66ba5): feat(web): persist tab state in URL for CrossSeed and Settings pages ([#​775](autobrr/qui#775)) ([@​s0up4200](https://github.com/s0up4200)) - [`59884a9`](autobrr/qui@59884a9): feat(web): share tracker customizations with filtersidebar ([#​717](autobrr/qui#717)) ([@​s0up4200](https://github.com/s0up4200)) ##### Bug Fixes - [`fafd278`](autobrr/qui@fafd278): fix(api): add webhook source filter fields to PATCH settings endpoint ([#​774](autobrr/qui#774)) ([@​s0up4200](https://github.com/s0up4200)) - [`bdf0339`](autobrr/qui@bdf0339): fix(api): support apikey query param with custom base URL ([#​748](autobrr/qui#748)) ([@​s0up4200](https://github.com/s0up4200)) - [`c3c8d66`](autobrr/qui@c3c8d66): fix(crossseed): compare Site and Sum fields for anime releases ([#​769](autobrr/qui#769)) ([@​s0up4200](https://github.com/s0up4200)) - [`cb4c965`](autobrr/qui@cb4c965): fix(crossseed): detect file name differences and fix hasExtraSourceFiles ([#​741](autobrr/qui#741)) ([@​s0up4200](https://github.com/s0up4200)) - [`fd9e054`](autobrr/qui@fd9e054): fix(crossseed): fix batch completion searches and remove legacy settings ([#​744](autobrr/qui#744)) ([@​s0up4200](https://github.com/s0up4200)) - [`26706a0`](autobrr/qui@26706a0): fix(crossseed): normalize punctuation in title matching ([#​718](autobrr/qui#718)) ([@​s0up4200](https://github.com/s0up4200)) - [`db30566`](autobrr/qui@db30566): fix(crossseed): rename files before folder to avoid path conflicts ([#​752](autobrr/qui#752)) ([@​s0up4200](https://github.com/s0up4200)) - [`8886ac4`](autobrr/qui@8886ac4): fix(crossseed): resolve category creation race condition and relax autoTMM ([#​767](autobrr/qui#767)) ([@​s0up4200](https://github.com/s0up4200)) - [`f8f2a05`](autobrr/qui@f8f2a05): fix(crossseed): support game scene releases with RAR files ([#​768](autobrr/qui#768)) ([@​s0up4200](https://github.com/s0up4200)) - [`918adee`](autobrr/qui@918adee): fix(crossseed): treat x264/H.264/H264/AVC as equivalent codecs ([#​766](autobrr/qui#766)) ([@​s0up4200](https://github.com/s0up4200)) - [`c4b1f0a`](autobrr/qui@c4b1f0a): fix(dashboard): merge tracker customizations with duplicate displayName ([#​751](autobrr/qui#751)) ([@​jabloink](https://github.com/jabloink)) - [`3c6e0f9`](autobrr/qui@3c6e0f9): fix(license): remove redundant validation call after activation ([#​749](autobrr/qui#749)) ([@​s0up4200](https://github.com/s0up4200)) - [`a9c7754`](autobrr/qui@a9c7754): fix(reannounce): simplify tracker detection to match qbrr logic ([#​746](autobrr/qui#746)) ([@​s0up4200](https://github.com/s0up4200)) - [`3baa007`](autobrr/qui@3baa007): fix(rss): skip download when torrent already exists by infohash ([#​715](autobrr/qui#715)) ([@​s0up4200](https://github.com/s0up4200)) - [`55d0ccc`](autobrr/qui@55d0ccc): fix(swagger): respect base URL for API docs routes ([#​758](autobrr/qui#758)) ([@​s0up4200](https://github.com/s0up4200)) - [`47695fd`](autobrr/qui@47695fd): fix(web): add height constraint to filter sidebar wrapper for proper scrolling ([#​778](autobrr/qui#778)) ([@​s0up4200](https://github.com/s0up4200)) - [`4b3bfea`](autobrr/qui@4b3bfea): fix(web): default torrent format to v1 in creator dialog ([#​723](autobrr/qui#723)) ([@​s0up4200](https://github.com/s0up4200)) - [`2d54b79`](autobrr/qui@2d54b79): fix(web): pin submit button in Services sheet footer ([#​756](autobrr/qui#756)) ([@​s0up4200](https://github.com/s0up4200)) - [`2bcd6a3`](autobrr/qui@2bcd6a3): fix(web): preserve folder collapse state during file tree sync ([#​740](autobrr/qui#740)) ([@​ewenjo](https://github.com/ewenjo)) - [`57f3f1d`](autobrr/qui@57f3f1d): fix(web): sort Peers column by total peers instead of connected ([#​759](autobrr/qui#759)) ([@​s0up4200](https://github.com/s0up4200)) - [`53a8818`](autobrr/qui@53a8818): fix(web): sort Seeds column by total seeds instead of connected ([#​747](autobrr/qui#747)) ([@​s0up4200](https://github.com/s0up4200)) - [`d171915`](autobrr/qui@d171915): fix(web): sort folders before files in torrent file tree ([#​764](autobrr/qui#764)) ([@​s0up4200](https://github.com/s0up4200)) ##### Other Changes - [`172b4aa`](autobrr/qui@172b4aa): chore(assets): replace napster.svg with napster.png for logo update ([@​s0up4200](https://github.com/s0up4200)) - [`dc83102`](autobrr/qui@dc83102): chore(deps): bump the github group with 3 updates ([#​761](autobrr/qui#761)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`75357d3`](autobrr/qui@75357d3): chore: fix napster logo ([@​s0up4200](https://github.com/s0up4200)) - [`206c4b2`](autobrr/qui@206c4b2): refactor(web): extract CrossSeed completion to accordion component ([#​762](autobrr/qui#762)) ([@​s0up4200](https://github.com/s0up4200)) **Full Changelog**: <autobrr/qui@v1.9.1...v1.10.0> #### Docker images - `docker pull ghcr.io/autobrr/qui:v1.10.0` - `docker pull ghcr.io/autobrr/qui:latest` #### What to do next? - Join our [Discord server](https://discord.autobrr.com/qui) Thank you for using qui! </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zOS4xIiwidXBkYXRlZEluVmVyIjoiNDIuMzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=--> Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/2664 Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net> Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Summary
This addresses the use case where:
Primary domain is always included. Secondary domains are only included if explicitly checked.
Empty
includedInStatsmeans only primary domain stats shown, which is backwards compatible with existing customizations.Test plan
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.